openspec-shipper
v0.1.35
Published
Ship OpenSpec changes through an agent-backed queue.
Readme
openspec-shipper
openspec-shipper is a small CLI that runs an OpenSpec delivery queue through
AI executors. OpenCode is the stable v1 provider; Codex CLI and Claude Code are
experimental providers that share the same queue and delivery contract.
The package is npm-first and repo-local by default:
npm install -D openspec-shipper
npx openspec-shipper init
npx openspec-shipper doctorThere is no postinstall mutation. init is the command that installs project
assets, and it writes state only under .openspec-shipper/ plus provider assets
such as .opencode/.
Requirements
gitgh, authenticated with GitHub CLI- OpenCode, Codex CLI, or Claude Code for the selected executor provider
- The package manager configured during
init
gh is used by the runner to create pull requests after branch push and to
reconcile PR state before spending tokens. That is how push becomes
waiting_for_merge, and how waiting_for_merge becomes sync_main after a PR
has been merged.
Before running the queue, the configured base branch (main by default) should be clean except for ignored shipper
runtime state such as .openspec-shipper/queue.md, logs, lock files, and
worktrees/. doctor fails when it sees non-runtime changes because the native
prepare_worktree phase creates feature worktrees from the base branch checkout.
Commands
openspec-shipper init
openspec-shipper doctor
openspec-shipper update
openspec-shipper queue add <change-name>
openspec-shipper queue next
openspec-shipper queue run
openspec-shipper queue status
openspec-shipper queue dry-run
openspec-shipper queue stop
openspec-shipper queue statsTop-level aliases are also available:
openspec-shipper add <change-name>
openspec-shipper next
openspec-shipper run
openspec-shipper status
openspec-shipper dry-run
openspec-shipper stop
openspec-shipper statsThe old Bun scripts are kept as transition helpers in this repository, but new docs should use the CLI above.
State And Env
Runtime state lives in the target repository:
.openspec-shipper/
config.json
.env
.env.example
queue.md
runs/
tmp/
installed.json
shipper.lock
stopopenspec-shipper never loads the target app's .env. It only reads
.openspec-shipper/.env, or the file passed with --env-file.
Config precedence is:
CLI flags > process.env OPENSPEC_SHIPPER_* > .openspec-shipper/.env > .openspec-shipper/config.json > defaultsUseful variables:
OPENSPEC_SHIPPER_PROJECT_DIR=/absolute/path/to/repo
OPENSPEC_SHIPPER_QUEUE_PATH=/absolute/path/to/repo/.openspec-shipper/queue.md
OPENSPEC_SHIPPER_PROVIDER=opencode
OPENSPEC_SHIPPER_OPENCODE_BIN=opencode
OPENSPEC_SHIPPER_OPENCODE_MODEL=opencode-go/deepseek-v4-pro
OPENSPEC_SHIPPER_CODEX_BIN=codex
OPENSPEC_SHIPPER_CODEX_MODEL=gpt-5.5
OPENSPEC_SHIPPER_CODEX_REASONING_EFFORT=low
OPENSPEC_SHIPPER_CLAUDE_BIN=claude
OPENSPEC_SHIPPER_CLAUDE_MODEL=sonnet
OPENSPEC_SHIPPER_CLAUDE_EFFORT=low
OPENSPEC_SHIPPER_CLAUDE_PERMISSION_MODE=dontAsk
OPENSPEC_SHIPPER_CLAUDE_MAX_TURNS=
OPENSPEC_SHIPPER_CLAUDE_MAX_BUDGET_USD=
OPENSPEC_SHIPPER_ALLOW_ACTIVE_EXECUTOR=2
OPENSPEC_SHIPPER_PRINT_LOGS=1
OPENSPEC_SHIPPER_LOG_LEVEL=ERROR
OPENSPEC_SHIPPER_STATS=1OPENSPEC_SHIPPER_ALLOW_ACTIVE_EXECUTOR is a numeric allowance. The default is
2, so having Codex/ChatGPT, Claude Code, or OpenCode open in another window does not block
the queue. Set it to 0 for strict single-executor mode.
init adds these ignored entries:
# OpenSpec Shipper local state
.openspec-shipper/.env
.openspec-shipper/queue.md
.openspec-shipper/shipper.lock
.openspec-shipper/stop
.openspec-shipper/runs/
.openspec-shipper/tmp/
worktrees/Init
Interactive mode:
npx openspec-shipper initNon-interactive mode:
npx openspec-shipper init --yes --provider opencode --package-manager npm
npx openspec-shipper init --yes --provider claude-code --model sonnet --effort lowCurrent implementation still uses the previous profile flag while the interactive wizard is being expanded:
npx openspec-shipper init --profile node-npminit installs:
.openspec-shipper/config.jsonand.openspec-shipper/.env.example.openspec-shipper/README.mdand.openspec-shipper/queue.example.md.openspec-shipper/openspec-config.example.yamlas optional OpenSpec config guidance.openspec-shipper/scripts/with shipper-owned validation helpers- provider assets:
- OpenCode:
.opencode/commands,.opencode/agents,.opencode/rules - Codex CLI:
.openspec-shipper/codex/ - Claude Code:
.openspec-shipper/claude/
- OpenCode:
- GitHub CLI (
gh) based pull request creation after branch push - package scripts and missing dev dependencies
.gitignoreentries for shipper state and worktrees
The installer does not overwrite the target repo's root README.md; that file
belongs to the application. A repo-local usage guide is installed at
.openspec-shipper/README.md.
Commit the installed project assets on the configured base branch (main by default)
before running the queue. The native prepare_worktree phase creates feature worktrees from HEAD; if the base branch is dirty after
init, the new worktree would miss the freshly installed scripts,
provider commands, and package changes. Local queue state remains ignored.
OpenSpec Shipper uses GitHub CLI to create and inspect pull requests. Authenticate once before running the queue:
gh auth login
openspec-shipper doctorgit status --short
git add <installed files you want to track>
git commit -m "chore: install openspec shipper"update refreshes installed assets using .openspec-shipper/installed.json.
Locally changed files are reported as drifted instead of overwritten; use
--force only when replacing local edits intentionally.
Queue
queue add creates the queue if needed and avoids duplicates:
npx openspec-shipper queue add add-name-greeting
npx openspec-shipper queue add openspec/changes/add-spanish-greeting
npx openspec-shipper queue add add-shouting-greeting --depends-on add-spanish-greetingQueue format:
- [ ] deliver add-name-greeting
- [ ] deliver add-spanish-greeting <!-- depends_on: add-name-greeting -->deliver is the only public queue action. OpenSpec Shipper stores its current
phase as metadata on that task and advances it through:
prepare_worktree -> implement -> push -> waiting_for_merge -> sync_main -> archive -> cleanup_worktreesequenceDiagram
participant Human
participant Shipper as openspec-shipper
participant OpenSpec
Human->>OpenSpec: Create change
Human->>Shipper: Add change to queue.md
Shipper->>Shipper: prepare_worktree
Shipper->>Shipper: implement in worktree
Shipper->>Shipper: push branch and open PR with gh
Human->>Shipper: Review and merge PR
Shipper->>Shipper: sync_main
Shipper->>OpenSpec: archive merged change
Shipper->>Shipper: cleanup_worktree
Shipper-->>Human: Mark task done and start next task
alt any phase blocks
Shipper-->>Human: Write blocker reason and retry hint
Human->>Shipper: Fix cause and change [!] back to [ ]
Shipper->>Shipper: Reconcile again before retrying
endprepare_worktree is native runner logic: it creates or reconnects
worktrees/<change-name> and the deterministic implementation branch before
any AI executor is called. implement then spends model tokens only on
implementation inside that prepared workspace.
waiting_for_merge is intentionally not runnable. The runner uses gh to
notice when the PR has merged and then reconciles the task to phase: sync_main, so
the shipper can synchronize the configured base branch, archive safely, and
clean local artifacts.
Reverse State Inference
queue.md is the human-facing source of truth, but it is reconciled before each
queue command. Reconciliation infers the most advanced observable state first,
instead of trusting the phase comment blindly:
archived and locally clean -> done
archived but local work remains -> cleanup_worktree
merged PR -> sync_main
open PR -> waiting_for_merge
remote branch without PR -> push
local work complete -> push
local work incomplete -> implement
active change without local work -> prepare_worktree
nothing found -> blockedThis backwards inference avoids false blockers when an earlier phase can no
longer see its original inputs because a later phase already happened. For
example, after openspec archive succeeds, openspec/changes/<change-name>/
disappears. The reconciler therefore checks openspec/changes/archive/ before
concluding that an archive task is broken.
Explicit waiting phases are not regressed just because a transient external
check returns no data. For example, waiting_for_merge remains waiting unless
the runner positively observes a merged PR, archived change, or completed
cleanup_worktree.
When a task blocks, the queue includes a human retry hint below it:
- [!] deliver add-name-greeting <!-- phase: archive; reason: ... -->
> Fixed? Change `[!]` to `[ ]` and run `openspec-shipper queue run` again.After fixing the cause, change only [!] to [ ]. The next queue command will
remove the hint, reconcile the task from repository evidence, and retry or move
it to the correct phase.
The archive phase keeps the intelligent work narrow: the provider validates
and reconciles OpenSpec archive/spec files, then the runner owns the deterministic
Git finalization: staging only OpenSpec paths, committing, rebasing, and pushing
to the configured base branch. The cleanup_worktree phase is OpenSpec Shipper
housekeeping: it removes a clean local worktrees/<change-name> worktree and
deletes the merged local branch with git branch -d when safe. If there is
nothing left to clean, cleanup succeeds as a no-op.
Providers
OpenCode
OpenCode is the stable v1 provider. It keeps the provider command filenames for the phases that still need an agent. Git/GitHub housekeeping phases are native runner logic and do not call OpenCode:
opencode run --command openspec-apply-worktree <change>
opencode run --command openspec-archive-merged <change>With config enabled, it also adds:
--print-logs --log-level ERROR --model <model>Codex CLI
Codex CLI is experimental. It does not install .opencode assets; it installs
phase prompts under .openspec-shipper/codex/:
npx openspec-shipper init --provider codex-cli{
"executor": {
"provider": "codex-cli",
"codex": {
"bin": "codex",
"model": "gpt-5.5",
"reasoningEffort": "low"
}
}
}Dry-run will produce command specs like:
codex exec -C <projectDir> --sandbox workspace-write -c 'approval_policy="never"' --model <model> -c 'model_reasoning_effort="low"' <prompt>Claude Code
Claude Code support is experimental. It runs claude -p non-interactively for
the intelligent implement and archive phases, while Shipper continues to own
worktrees, push, pull requests, base-branch sync, archive finalization, and
cleanup.
npm install -g @anthropic-ai/claude-code
claude auth login
npx openspec-shipper init --provider claude-code
npx openspec-shipper doctorProvider assets are installed under .openspec-shipper/claude/. Shipper does
not create or modify .claude/, so existing project skills, agents, hooks, and
settings remain application-owned. Each phase receives an explicit prompt,
strict sandbox settings, a restricted tool surface, and a structured completion
contract.
{
"executor": {
"provider": "claude-code",
"claude": {
"bin": "claude",
"model": "sonnet",
"effort": "low",
"permissionMode": "dontAsk"
}
}
}The strict Claude sandbox is supported on macOS, Linux, and WSL2. Native Windows support remains experimental because Claude Code does not provide its sandbox there.
Local And External Modes
Default local mode runs inside the target repo:
cd /path/to/target-repo
npx openspec-shipper queue dry-run
npx openspec-shipper queue runExternal mode is still supported:
npx openspec-shipper \
--project /path/to/target-repo \
--queue /path/to/target-repo/.openspec-shipper/queue.md \
queue dry-runThis keeps the hybrid option open while making the npm-installed local workflow the normal path.
Demo
The demo repo is at:
/Users/javigomez/Documents/projects/openspec-demoSuggested walkthrough for a GIF:
git clone <demo-url> openspec-demo-gif
cd openspec-demo-gif
npm install
npm install -D openspec-shipper
npx openspec-shipper init --profile node-npm
npx openspec-shipper doctor
npx openspec-shipper queue add add-name-greeting
npx openspec-shipper queue add add-spanish-greeting --depends-on add-name-greeting
npx openspec-shipper queue add add-shouting-greeting --depends-on add-spanish-greeting
npx openspec-shipper queue status
npx openspec-shipper queue dry-run
npx openspec-shipper queue runFor local tarball testing before publish:
npm run build
npm pack
cd /Users/javigomez/Documents/projects/openspec-demo
npm install -D /path/to/openspec-shipper-0.1.0.tgz
npx openspec-shipper doctor
npx openspec-shipper queue dry-runPublishing Checklist
Run these before publishing:
bun test
npm run build
npm_config_cache=/private/tmp/openspec-shipper-npm-cache npm pack --dry-runThen test the generated tarball in openspec-demo. Publish only after the
manual OpenCode demo works:
npm publish --access public