cd /news/developer-tools/cli-that-enforces-spec-driven-develo… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-32296] src=github.com β†— pub= topic=developer-tools verified=true sentiment=↑ positive

CLI That Enforces Spec-Driven Development with Claude Code, OpenCode, and Codex

Developer David P. V. released a stack-agnostic CLI tool called opsx that enforces spec-driven development workflows on top of OpenSpec, supporting agent CLIs including OpenCode, Claude Code, and Codex. The tool separates a management plane for tracking work from a governance plane that controls system behavior, ensuring code is only produced after spec changes are reviewed and approved. Opsx requires Node.js 18 or later and can be initialized with npx @davidpv/opsx init.

read11 min views1 publishedJun 18, 2026

A stack-agnostic CLI that scaffolds spec-driven development on top of OpenSpec, for any of opencode, Claude Code and Codex.

The core idea: Spec β†’ Plan β†’ Code. Code is the last artifact produced, never the first. OpenSpec structures every change as proposal + specs + design + tasks, and opencode executes the implementation with full traceability back to the requirements.

Two planes coexist and must not be confused. The management plane (Jira vocabulary) decides what work exists and tracks it; the governance plane (OpenSpec) decides how the system must behave β€” and only the latter authorizes code:

flowchart LR
    subgraph MGMT["Management plane β€” what work exists (Jira)"]
        direction LR
        D["Discovery<br/>backlog/discovery/"] --> T["Task PROJ-123<br/>backlog/tasks/<br/>goal + acceptance criteria"]
    end

    subgraph GOV["Governance plane β€” how the system must behave (OpenSpec)"]
        direction LR
        C["Change<br/>proposal + delta specs<br/>+ design + steps"] -- "/opsx:archive" --> S["openspec/specs/<br/>SOURCE OF TRUTH"]
        S -. "read before proposing" .-> C
    end

    CODE["Code<br/>feature branch, commits, PR"]

    T -- "a task authorizes NO code<br/>it only motivates a change" --> C
    C -- "the ONLY gate to code" --> CODE
    CODE -. "traces back: Jira / Change / Task footers" .-> T

A task being well written changes nothing: implementation starts only when an OpenSpec change exists, is reviewed, and its branch gate is resolved. If a task ever contains design or implementation detail, that content belongs in the change β€” task-reviewer

flags it. This is what keeps the workflow spec-driven even though the backlog speaks Jira: Jira rules the backlog, the spec rules the code.

To run opsx init

itself you only need Node.js >= 18 (npx @davidpv/opsx init

works with nothing else installed β€” it just writes files). To actually use the workflow it scaffolds, you need:

Requirement Needed for Install
Git repository
The whole workflow assumes git (branches, gates, traceability) git init
OpenSpec CLI (required)
Every /opsx:* command shells out to openspec
npm install -g @fission-ai/openspec
At least one agent CLI
Running the commands/skills opencode: npm install -g opencode-ai Β· Claude Code: npm install -g @anthropic-ai/claude-code Β· Codex: npm install -g @openai/codex
gh or glab (optional)
/pr-open and /ship PR automation; without them the PR description is written to backlog/exports/pr/

You don't need all three agent CLIs β€” only the ones you selected as targets during init

. opsx doctor

checks all of this and tells you exactly what's missing.

npx @davidpv/opsx init          # pick targets: opencode / Claude Code / Codex, configure branches, Jira key, language
npx @davidpv/opsx doctor        # verify required tooling (openspec CLI, agent CLIs)
npx @davidpv/opsx update        # after upgrading the opsx package: deploy new commands/skills to your project

init

writes the shared, tool-agnostic layer once (workflow.yaml

, AGENTS.md

managed block, backlog/

, templates/

, openspec/

) and a native layer per agent: .opencode/

(commands + skills + agents + opencode.json

merge), .claude/

(commands + skills + subagents + settings.json

merge + CLAUDE.md

importing AGENTS.md

), .codex/skills/

(skills; commands and reviewers compiled as skills, since Codex has no project-level slash commands or subagents).

Then, inside your agent (shown here with opencode):

/start                            # guided entry: routes you to the right first command
/req-capture checkout-flow        # 0. requirements interview β†’ discovery doc
/task-generate checkout-flow      # 1. tasks with Jira IDs (PROJ-123)
/task-enrich PROJ-123             #    edge cases, scenarios, estimate
/task-jira PROJ-123               #    export as Jira wiki markup
/opsx:propose speed-up-search     # 2. proposal + delta specs + design + tasks
/review-change speed-up-search    # 3. audit before building
/opsx:apply                       # 4. implement task by task
/git-commit                       #    semantic commits traced to tasks
/pr-open                          # 5. PR against the integration branch
/ship                             # 6. validate + archive + merge

Stages 0–1 are optional for small technical changes; everything from /opsx:propose

on is mandatory.

The flow is guided: /start

is the entry point β€” it asks how the work begins (existing Jira ticket β†’ /task-import

; no ticket, propose directly β†’ /opsx:explore

//opsx:propose

; no ticket, create the task first β†’ /task-new

or /req-capture

) and routes you there. Every command ends by suggesting the next step, and /next

tells you where you are and what to do whenever you're lost or coming back to the repo (it inspects branch, task frontmatter, change artifacts, and PR state).

Full map of flows and options. πŸ§‘ = needs human interaction, πŸ€– = agentic (runs on its own), πŸ§‘πŸ€– = agentic with human checkpoint (approval, language gate, or fallback).

flowchart TD
    classDef human  fill:#fff3cd,stroke:#996f00,color:#000
    classDef agent  fill:#d6eaf8,stroke:#1a5276,color:#000
    classDef mixed  fill:#e8daef,stroke:#6c3483,color:#000
    classDef art    fill:#eaeded,stroke:#7f8c8d,color:#000

    START(["New work arrives"]) --> ENTRY["/start πŸ§‘<br/>guided entry"]:::human
    ENTRY --> CASE{"How does this work start?"}

    %% Entry routes
    CASE -- "Jira ticket exists" --> IMP["/task-import πŸ§‘πŸ€–<br/>paste ticket + normalize + task-reviewer"]:::mixed
    IMP --> ST
    CASE -- "no ticket:<br/>investigate / propose" --> PROP
    CASE -- "no ticket:<br/>create task first" --> SIZE{"Initiative or single task?"}
    SIZE -- "single task" --> TN["/task-new πŸ§‘πŸ€–<br/>mini interview + draft ID"]:::mixed
    TN --> ST

    %% Product phase (optional)
    SIZE -- initiative --> RC["/req-capture πŸ§‘<br/>interview + language gate"]:::human
    RC --> DISC["backlog/discovery/topic.md"]:::art
    DISC --> SG["/task-generate πŸ§‘πŸ€–<br/>language gate + Jira IDs from user"]:::mixed
    SG --> ST["backlog/tasks/PROJ-123.md"]:::art
    ST --> SE["/task-enrich πŸ€–<br/>asks only on ambiguity"]:::agent
    SE --> RS["/review-task πŸ€–"]:::agent
    RS -- REVISE --> SE
    RS -- APPROVE --> JIRAQ{"Export to Jira?"}
    JIRAQ -- yes --> SJ["/task-jira πŸ§‘πŸ€–<br/>language gate + translation"]:::mixed
    SJ --> JEXP["backlog/exports/jira/"]:::art
    JEXP --> PASTE["paste into Jira & confirm real key πŸ§‘"]:::human
    JIRAQ -- no --> PROP
    PASTE --> PROP

    %% Spec phase
    PROP["/opsx:propose πŸ§‘πŸ€–<br/>you describe, agent writes artifacts"]:::mixed
    PROP --> CHG["openspec/changes/name/"]:::art
    CHG --> RV["/review-change πŸ€–<br/>spec-reviewer + validate --strict"]:::agent
    RV -- REVISE --> FIX["fix artifacts πŸ€–"]:::agent
    FIX --> RV
    RV -- APPROVE --> BRANCH{"Branch gate πŸ§‘<br/>where to implement? (git.work_mode)<br/>blocks any code until resolved"}
    BRANCH -- "create feature branch<br/>(reviewed PR)" --> APPLY
    BRANCH -- "directly on develop<br/>(flexible mode, no review)" --> APPLY

    %% Build phase
    APPLY["/opsx:apply πŸ€–"]:::agent
    APPLY --> DRIFT{"Spec wrong or drifted?"}
    DRIFT -- yes --> SYNC["/opsx:sync πŸ€–"]:::agent
    SYNC --> APPLY
    DRIFT -- no --> GC["/git-commit πŸ§‘πŸ€–<br/>agent drafts, you approve message"]:::mixed
    GC -- more tasks --> APPLY

    %% Delivery phase
    GC -- all tasks done --> ONBR{"On a feature branch?"}
    ONBR -- "no: on develop" --> SHIPD["/ship πŸ€–<br/>validate + archive + push<br/>(skips PR and review)"]:::agent
    SHIPD --> DONE
    ONBR -- yes --> PRO["/pr-open πŸ§‘πŸ€–<br/>language gate + draft decision"]:::mixed
    PRO --> CLI{"gh / glab available?"}
    CLI -- yes --> PR["PR created against develop"]:::art
    CLI -- no --> PRF["description in backlog/exports/pr/<br/>you open the PR manually πŸ§‘"]:::human
    PRF --> PR
    PR --> REVIEW["code review & approval πŸ§‘"]:::human
    REVIEW --> SHIP["/ship πŸ€–<br/>validate + archive + merge"]:::agent
    SHIP --> MERGEQ{"CLI + CI green?"}
    MERGEQ -- yes --> DONE(["change archived, task done, branch deleted"])
    MERGEQ -- no --> WEBUI["merge in web UI πŸ§‘"]:::human
    WEBUI --> DONE

"Spec wrong or drifted?"β€” checkpoint during implementation.Wrong: while coding you discover a requirement or scenario was incorrect or incomplete.Drifted:openspec/specs/

no longer matches what the code actually does (hotfixes, old unspec'd commits). In both cases the rule is the same: never diverge silently β€” run/opsx:sync

to fix the spec first, then resume/opsx:apply

. Code must always trace back to a correct spec.

Branch policyβ€”main

is release-only and never worked on directly.git.work_mode

inworkflow.yaml

decides the rest:flexible

(default) lets you implement and commit directly on the integration branch β€”/ship

then just validates, archives, and pushes, skipping PR and review;feature

makes feature branches + PR mandatory. A mandatorybranch gateruns before/opsx:apply

writes any code: the working branch must be resolved (created and checked out) first;/git-commit

re-checks at commit time as a safety net. Feature branches are namedfeature/<task id>-<change>

when the change is linked to a backlog task with a real Jira key (e.g.feature/PROJ-123-speed-up-search

),feature/<change>

otherwise.

Human checkpoints, summarized: the /start

entry questions, the /req-capture

interview, pasting an existing Jira ticket (/task-import

), every language gate (es/en, mandatory on client-facing text), choosing where to implement (feature branch vs develop), commit message approval, providing Jira IDs and pasting Jira exports, PR code review, and merging via web UI when no platform CLI exists. Everything else runs agentically.

Traceability chain: Discovery β†’ Task (Jira) β†’ Change β†’ tasks.md step β†’ Commit β†’ PR. Each link is recorded where it happens: task frontmatter (change:

), commit footers (Change:

/Task:

/Jira:

), PR description. Task IDs ARE Jira keys (PROJ-123

, provided by you; PROJ-Dnn

drafts until the issue exists). Note the naming: a task is a backlog/Jira work item; tasks.md

inside a change holds implementation steps.

Each change lives in openspec/changes/<name>/

until archived. Archiving merges its delta specs into openspec/specs/

, the living description of how the system behaves. workflow.yaml

configures branches (git-flow by default: feature/* β†’ develop

), commit convention, and Jira export β€” commands read it, so the pipeline stays platform-agnostic.

.
β”œβ”€β”€ AGENTS.md                  # Rules every agent must follow
β”œβ”€β”€ opencode.json              # opencode project config
β”œβ”€β”€ workflow.yaml              # Pipeline config: branches, commits, Jira (tool-agnostic)
β”œβ”€β”€ templates/                 # discovery.md, task.md, pr-description.md
β”œβ”€β”€ backlog/
β”‚   β”œβ”€β”€ discovery/             # Requirements-gathering notes (/req-capture)
β”‚   β”œβ”€β”€ tasks/                 # Tasks with Jira IDs (/task-generate, /task-enrich)
β”‚   └── exports/               # jira/ (wiki markup) and pr/ (fallback PR descriptions)
β”œβ”€β”€ .opencode/
β”‚   β”œβ”€β”€ agents/
β”‚   β”‚   β”œβ”€β”€ spec-reviewer.md   # Subagent: audits changes before apply/archive
β”‚   β”‚   └── task-reviewer.md   # Subagent: audits tasks (sizing, testability)
β”‚   β”œβ”€β”€ commands/
β”‚   β”‚   β”œβ”€β”€ start.md           # /start β€” guided entry point (routes new work)
β”‚   β”‚   β”œβ”€β”€ req-capture.md     # /req-capture β€” requirements interview
β”‚   β”‚   β”œβ”€β”€ task-*.md          # /task-import|new|generate|enrich|jira
β”‚   β”‚   β”œβ”€β”€ review-*.md        # /review-change, /review-task
β”‚   β”‚   β”œβ”€β”€ git-commit.md      # /git-commit β€” semantic commits with traceability
β”‚   β”‚   β”œβ”€β”€ pr-open.md         # /pr-open β€” platform-agnostic PR creation
β”‚   β”‚   β”œβ”€β”€ ship.md            # /ship β€” validate + archive + merge
β”‚   β”‚   └── opsx-*.md          # /opsx:* β€” OpenSpec lifecycle (generated)
β”‚   └── skills/                # OpenSpec workflow skills (generated)
└── openspec/
    β”œβ”€β”€ config.yaml            # Project context + per-artifact rules
    β”œβ”€β”€ specs/                 # Source of truth (current behavior)
    └── changes/               # In-flight changes; archive/ keeps history
Command Stage What it does
/start
Entry Guided entry point: asks how the work starts and routes you (ticket exists / propose directly / create task first)
/next
Any Detect where you are in the pipeline and suggest the next step
/req-capture <topic>
Discover Guided requirements interview β†’ backlog/discovery/<topic>.md
/task-import <id>
Tasks Import an existing Jira ticket (you paste it) into backlog/tasks/
/task-new <title>
Tasks Create a single task directly, without a discovery doc (draft ID)
/task-generate <topic>
Tasks Slice a discovery doc into tasks; you provide the Jira IDs
/task-enrich <id>
Tasks Add edge cases, unhappy paths, estimate; runs task-reviewer
/review-task <id>
Tasks Audit a task: sizing, testability, traceability
`/task-jira <id all>`
Tasks Export tasks as Jira wiki markup to backlog/exports/jira/
/opsx:propose <name>
Spec Create a change: proposal, delta specs, design, tasks
/opsx:explore
Spec Investigate the codebase/specs before proposing
/review-change <name>
Spec Spec-reviewer audit + openspec validate --strict
/opsx:apply
Build Implement the tasks of a change
/git-commit
Build Conventional commit traced to change/step/Jira task
/opsx:sync
Build Sync specs with reality when they drift
/pr-open [name]
Deliver Create the PR against the integration branch (gh/glab/file fallback)
/ship [name]
Deliver Validate + /opsx:archive + merge + close the task
/opsx:verify <name>
Deliver Validate implementation matches change artifacts (run before archive)
/opsx:archive
Deliver Merge delta specs into openspec/specs/ and file the change
  • Copy or clone this repo and re-init git.
  • Fill the context:

block inopenspec/config.yaml

with your stack and conventions. - Adjust workflow.yaml

: branches (git-flow vs trunk-based), Jira project key, platform. - Extend AGENTS.md

with project-specific rules. - Start with /start

β€” it routes you to/task-import

(existing ticket),/req-capture

//task-new

(create the task first), or/opsx:propose

(direct change).

Since opsx is an evolving toolkit (new commands like /opsx:verify

, skill improvements, schema updates), keeping it current ensures your workflow stays complete.

Upgrading the npm package alone does not deploy new commands or skills to an existing project β€” they live in the shipped payload/

directory and must be written to your project's .opencode/

(or .claude/

, .codex/

) structure by opsx update

.

npm update -g @davidpv/opsx

npm update @davidpv/opsx

npx @davidpv/opsx update

Both steps are required. Step 1 updates the code that npx

resolves; step 2 writes the new payload files (commands, skills, agents, templates) into your project. If you skip step 2, new capabilities like /opsx:verify

won't appear even though the package is the latest version.

The command compares the shipped payload against what's already on disk in your project:

Scenario What happens
New file (e.g., opsx-verify.md added in a newer opsx version)
Created automatically
Changed file, not user-modified
Silently overwritten
Changed file, user-modified
Kept as-is (use opsx update --force to overwrite)
AGENTS.md managed block
Only the <!-- OPSX:START --> block is refreshed; your surrounding custom rules are preserved
opencode.json / settings.json
New top-level keys merged in; your overrides stay

To see exactly what changed on the last run, check the output β€” it lists Added:

, Updated:

, and Kept (not touched):

files.

npm outdated -g @davidpv/opsx     # check if a newer npm version exists
npx @davidpv/opsx --version       # show installed version
npx @davidpv/opsx doctor          # verify tooling and project state

New opsx versions are published to npm. Check the CHANGELOG for breaking changes before updating.

── more in #developer-tools 4 stories Β· sorted by recency
── more on @openspec 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/cli-that-enforces-sp…] indexed:0 read:11min 2026-06-18 Β· β€”