specwright-roadmap

Project-level planning above OpenSpec changes: strategy, architecture baseline (foundational ADRs) and a milestone roadmap. Modes: init (turn a big project idea or brief into strategy, architecture an

SKILL.md

GitHub

Specwright Roadmap

Three project files sit above OpenSpec changes. Paths come from project: in openspec/specwright.yaml; defaults are below. Templates are in this skill's references/.

File Default Holds Changes
Strategy openspec/strategy.md Purpose, users, boundaries, metrics Rarely
Architecture openspec/architecture.md System shape + index of in-force ADRs (docs/adr/) When an ADR is added or superseded
Roadmap openspec/roadmap.md Milestones: outcome, exit criteria, changes At every milestone close

Status is derived, never stored. A milestone is done when all its changes are done and every exit criterion has passed. A change's status depends on where its planning lives (Planning repo below):

  • Main branches. In pr mode PRs merge on GitHub, so local <main> lags. In each repo (store-backed: the code repo and the store) read the fetch URL with git remote get-url origin: when it is on github.com (any scheme, github.com. included, whatever its push URLs; decided by that fetch URL, not by pr-pair.sh identity), run [vcs] fetch-main --login <run login> and read the ref it prints (origin/<main> on Git, a commit id on jj); for any other host run [vcs] fetch-main, which runs plain. [vcs] <sub> is bash <specwright-pr>/scripts/vcs.sh <sub> --repo <toplevel> --role code, and for the store --repo "<store toplevel>" --role store --code "<code toplevel>": it works on Git and on a colocated jj repo alike and prints one JSON document (ok, backend, and on a stop error and message; exit 1 stops, exit 3 is a network or fence refusal). [as] is bash <specwright-pr>/scripts/as.sh <run login>, where <specwright-pr> is that skill's folder and the run login is the one handed over (run login: <login>; a handed run login: none means local refs only, with no --resolve and no fetch of a github.com remote), else github.login, else the one bash <specwright-pr>/scripts/as.sh --resolve prints once for this user-started step; that is the code repo's run login, and in the store [as] takes the store login, planning_store.login, else that run login. Where there is no origin, the fetch is refused (exit 3, including a failed --resolve: no gh account) or the fetch fails, read that repo's local <main> and say its status may be stale (local refs only (no gh account), or store status may be stale: fetch failed). Local mode reads local <main>.

  • Archive. git -C <planning toplevel> ls-tree -d --name-only <planning main> <P>/changes/archive/, matched by the archive name (Archive name below: a change named YYYY-MM-DD-... is matched as is, never prefixed twice).

  • Repo-local: a change is done when the main branch holds its archive (in pr mode that means its PR merged, not just that archive ran on the branch); otherwise it is not done.

  • Store-backed: the archive on the store's main shows only that the planning merged. The change is done only when the store's main holds its archive and one of these proves the code side merged as a whole:

    • the archive directory on the store's main holds specwright-change.yaml with code_changes: none (a planning-only change; git -C <store toplevel> show <store main>:<P>/changes/archive/<archived-name>/specwright-change.yaml);
    • local mode: git -C <code toplevel> log --first-parent --format=%s <code main> has the exact subject merge: <change-name>;
    • pr mode: a merged code PR from the code repo's own <prefix>/<change-name> branch into the code main, <prefix> derived from the change name exactly as specwright-branch does (step 6). Find it with complete discovery, run from the code checkout (through [as]): [as] gh api --paginate "repos/<owner>/<repo>/pulls?state=closed&base=<code main>&per_page=100" --jq '.[] | select(.merged_at != null and .head.repo.full_name == "<owner>/<repo>" and .head.ref == "<prefix>/<change-name>") | .number', with <owner>/<repo> from the code repo's origin and GH_REPO unset. A PR merged into another base, from a fork's same-named branch, or from another prefix's branch (docs/<change-name> for a feat change) is not proof.

    A subject that merely contains (<change-name>) is not proof: a cherry-picked task commit has it while the rest of the change is unmerged. Without proof the change is planning merged, code pending. In pr mode, when gh is not available it is planning merged, code unverified, and when the discovery call fails (non-zero exit) it is planning merged, code state unknown. None of these counts as done, for status, next or close.

Committing project files (init and close): start from a clean main ([vcs] state: on_main, clean; in pr mode, [vcs] pull --remote-branch --login <run login> first, which fences the pull when origin's fetch URL is on github.com (or github.com.) and runs plain for any other host; a refusal, a failed --resolve or a non-zero pull stops before any branch) and [vcs] branch-create --branch <branch> before writing anything, commit the files you wrote by name ([vcs] commit --branch <branch> --message-file <msgfile> -- <files>) with a docs(<branch-name>): ... subject, then finish per finish in openspec/specwright.yaml: local → [vcs] switch --branch <main> and [vcs] merge --branch <branch> --message-file <msgfile> (a --no-ff merge with subject merge: <branch-name>), [vcs] delete-branch --branch <branch>, never push; pr → specwright-pr ship with the run login. There is no change to archive, so specwright-finish does not apply.

Store-backed (see the Planning repo section of specwright-commit): the store's gate lock, the same specwright-gate.lock that specwright-branch takes for a change, guards the branch, so a roadmap branch and a change branch never pass their gates together. Run this gate before any branch or write in either repo; which repo holds each project file does not change. Every command that uses the lock starts with cd "<store toplevel>" && L="$(git rev-parse --path-format=absolute --git-common-dir)/specwright-gate.lock" && (shell variables do not survive between calls).

  1. Lock, in one call: ... && mkdir "$L" && printf 'time: %s\nchange: %s\ncode: %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "<branch>" "<code toplevel>" > "$L/owner". If the lock is held (mkdir errors because it exists): stop before any branch or write in either repo, show owner (when, for which change or branch, from which checkout) and say another session may be in its gate for this store. When owner's change: starts with specwright-update, a store update holds the lock: say an update from the checkout on its code: line holds the store, tell the user to rerun the update from <code> (that code: line), and do not offer removal. Never remove a lock another session took: only the user can confirm it is stale.
  2. Check the store: it must be clean and on its main (planning_store.main_branch, else main, else master). On a branch other than its main, the store is busy with that branch: stop. Dirty: stop and show the dirty_paths of [vcs] state on the store. <branch> already exists in the store: stop. When project files are written in the code repo too, check the code repo the same way before either repo branches: it must be clean and on its main, and <branch> must not already exist there; otherwise stop. Each of these stops releases the lock first (step 4).
  3. Branch: [vcs] branch-create --branch <branch> in the store, and from the clean code main in the code repo when files are written there too. If any branch-create errors, roll back before anything else; a failing post-checkout hook makes it error after the branch was created and switched to, so never assume nothing was made. In each repo where this gate ran branch-create and <branch> exists: on jj (state shows backend jj), switch nothing: branch-create only added a bookmark at main's commit, which state reports as the current branch until it is gone, so delete it with [vcs] delete-branch --branch <branch> and judge it by state (on_main true). On Git, when [vcs] state shows the repo on <branch>, run [vcs] switch --branch <main> and ignore its exit status (the same hook may fail again), then judge the switch by state (on_main), not by the exit status; once it is on main, delete the branch with [vcs] delete-branch --branch <branch> (nothing is committed on it). A repo still on <branch> keeps it: delete nothing there, but still roll back the other repo. Then release the lock (step 4) and stop, showing the original error; when a repo could not switch back, name that repo, <branch> and its main.
  4. Release only the lock you took, right after the branch exists, in one call that sets L again: ... && rm -r "$L".

Then, before each step that writes project files in the store, run [vcs] state on the store (branch must be <branch>), and commit store files in one call: [vcs] commit --branch <branch> --message-file <msgfile> -- <files> in its store form. When the check fails (the store is on another branch), stop with no further write or commit in either repo, and name both branches: <branch> expected, and the store's current branch found.

init

Input: the user's project idea (any size), plus the repo if code exists. Work on branch docs/project-baseline (see Committing project files).

  1. Adopt before writing. If strategy, architecture or planning documents already exist, point the project: settings at them or extract from them; do not rewrite what is there.
  2. Interview, briefly. List the gaps that would change the architecture or the first milestone: users, platform, scope and non-goals, hard constraints, data and its sensitivity, success measures. Ask them in at most two batched rounds, each question with a recommended default. Everything else becomes a stated assumption.
  3. Strategy (references/strategy.md), one page.
  4. Architecture baseline (references/architecture.md). Run the design triage (T1-T6 from the schema's design instruction) for the whole system, not one change. Write the component diagram, state ownership, boundaries and contracts, resource bounds and failure visibility at system level - brief, a few lines per row. Record the foundational decisions as ADRs (process and isolate model, storage and formats, IPC and contracts, state management, external integrations), each with a rejected alternative and its reversal cost, written with Status: proposed. A proposed ADR is still a draft: when the review asks to change a decision, edit the ADR in place, never by a superseding ADR. Ground in the code if it exists; otherwise check claims about the stack against its docs.
  5. Review the strategy, architecture and ADRs the same way a FULL design is reviewed, with the reviewer chosen by the specwright-review routes (<specwright-review>/scripts/review-run.sh select, its output saved to a file as the select output): a reviewer CLI through review-run.sh run --kind design --route <route> --prompt-file <request file> --out openspec/architecture-review.md --change baseline --selection <select output> (store-backed: --store <store toplevel>, so the Claude route can read the planning files), or the specwright-reviewer agent in baseline mode: get a scratch result path with review-run.sh record --kind design --change baseline --scratch (the script prints scratch), have the agent write its review to that file, then review-run.sh record --kind design --change baseline --result <scratch> --out openspec/architecture-review.md --route subagent --selection <select output>, which writes the review file only when it validates (on exit 3 an earlier openspec/architecture-review.md stays as it was). The request names those file paths and the installed project skills that apply, each by name with the path of its installed SKILL.md, or none; never a specwright-* workflow skill. The review goes into openspec/architecture-review.md; an exit 3 is a failed review, never a fallthrough to another route. Rounds follow the schema's review rule: each full review is a new round; after 2 consecutive VERDICT: REVISE rounds, stop and ask the user, and write no roadmap until they decide (VERDICT: USER_OVERRIDE if they choose to proceed). There is no other round cap, and a pass ends a run of REVISE rounds. A pass covers only the content reviewed: any later edit to the strategy, architecture or ADRs voids it and gets a new round without asking the user, except the acceptance edit below. This is the one place a review always runs: early architecture mistakes are the most expensive ones. Gate: do not write the roadmap until the review passes: VERDICT: APPROVE; VERDICT: APPROVE_WITH_CHANGES with CHANGES_APPLIED: yes set by the reviewer; or VERDICT: USER_OVERRIDE after escalation. DEFERRED, WAIVED and SKIPPED_LIGHT never pass the baseline, the one place a review always runs: write no roadmap, leave every ADR Status: proposed and tell the user the baseline needs a review. When it passes, accept the ADRs: set each reviewed ADR's Status: proposed to Status: accepted with today's date and add its row to the architecture's in-force index, which lists accepted ADRs only. This acceptance edit is part of passing the gate and does not void the verdict. Only then write the roadmap. An accepted ADR not yet on main (a PR fix round on this branch) may still be edited, but the edit voids the review like any other; once on main it is never edited.
  6. Roadmap (references/roadmap.md):
    • M1 is a walking skeleton: the thinnest end-to-end slice through the architecture, which proves the riskiest assumptions first.
    • Each milestone has a demoable outcome and exit criteria that can be checked, each marked agent- or user-verified.
    • Only the current milestone is split into changes (2-6, each about one PR). Next gets a paragraph, Later one line each.
    • Exception: when the author explicitly agrees a split for Next, list Next's changes in the same Changes format and add a **Split agreed:** <YYYY-MM-DD> line (today's date) above them, so close keeps that split. Never add the line for a split the author did not agree.
  7. Commit and finish (see Committing project files). Then offer next.

next

  1. Read the roadmap's Now section. Run openspec list --json for active changes, and derive done changes from the main branch as above.

  2. Read each active change's review.md. One holding a whole line VERDICT: DEFERRED is parked: never offer to resume it, and continue to step 3 as if it were not active. Every other active change, including one with no review.md, is offered for resume instead. On a parking change's branch (the current branch is a parked change's own branch), say the parking change is on its own branch and that next starts from main once it has merged; start nothing: no branch, no change.

  3. Otherwise, check every change listed in the Now milestone first. A listed change whose active review.md holds a whole line VERDICT: DEFERRED is a parking change listed in error: report it as parked (listed in error), neither started nor counted done, and say that removing it from Changes is the user's planning edit. Then stop: start nothing (no branch, no change), and never edit the roadmap for it.

    Then the milestone end comes first. When every change of the Now milestone is done (a change that is planning merged, code pending, unverified or unknown is not done), the milestone is at its end: unless an exit criterion was shown failing by close or reported failing by the user, report milestone end: run close, offer close and stop. Start no change, create no branch and edit no file.

    next starts only changes of the Now milestone, never one under Next, even when Next already lists an agreed split: close promotes it first. Before the milestone end, pick the first planned change of Now that has no archive and whose prerequisites are all done (a prerequisite that is planning merged, code pending, unverified or unknown is not done). An archived change whose code is pending is never started again: report it and what it is waiting for (its code PR or merge). When no planned change is ready to start (each one left waits on a prerequisite that is not done), report what it waits for (each such prerequisite and its status) and start nothing.

    At the milestone end with an exit criterion shown failing by close or reported failing by the user, propose a new change that closes the gap, without editing any file yet: the roadmap entry is written in step 4, on the change's branch. Never propose a gap-closing change for criteria nobody has shown failing: offer close, which walks them.

  4. Start /opsx:propose <change-name> (specwright-branch runs first, handed this run's login: run login: <login>, or run login: none as this step was handed it; with no run login yet, as in local mode, resolve it first as this user-started step does, github.login, else bash <specwright-pr>/scripts/as.sh --resolve, once; when that fails (no gh account), hand run login: none and the gate uses local refs only). For a gap-closing change from step 3, as soon as specwright-branch has created <prefix>/<change-name> and before any planning artifact is written: add the change to the current milestone's Changes, then commit only the roadmap file by name in the repo that holds it, in one call with [vcs] commit --branch <prefix>/<change-name> --message-file <msgfile> -- <roadmap file> (it refuses on another branch), subject docs(<change-name>): add <change-name> to the roadmap (store-backed: also run the store branch check from Committing project files first). Then propose continues. If the gate stops, nothing was edited: leave the roadmap untouched. The entry merges with the change. Give the proposal its context: the milestone, the exit criteria this change serves, and the ADRs that constrain it. The proposal names its milestone.

close

  1. Every change of the milestone done (on main, as derived above)? If not, list what is left and stop. A listed change whose active review.md holds a whole line VERDICT: DEFERRED is reported as parked (listed in error), neither started nor counted done, so the milestone does not close; say that removing it from Changes is the user's planning edit, then stop, and never edit the roadmap for it.

  2. Walk the exit criteria. Run agent-verifiable checks yourself. Ask the user to run user-verified ones and record their results; anything skipped is recorded as unverified, never passed.

  3. Any criterion fails → propose the change that fixes it (via next); do not close. Any criterion unverified → list it and stop; the milestone stays in Now until it has a passing result. 3b. Releases, only with release.enabled true (otherwise list no versions and offer no release): list each version in main's first-parent history of the primary version file that has no tag on origin, as [as] bash <specwright-release>/scripts/release.sh untagged --main <ref> lists them (specwright-release publish step 2: fenced with the run login, since it reads origin's tags; a version is published when its tag is on origin, and a local tag not on origin is always listed), and offer to publish them oldest first. Publish nothing without the user's yes; a refused or failed publish is reported and never blocks the close.

  4. Close on branch docs/close-<milestone> (see Committing project files): move the milestone to Done with the date and one line on what was learned.

  5. Re-plan with what the milestone taught: promote Next to Now and revise Later. How Next becomes Now depends on its split:

    • Next has a **Split agreed:** <date> line: keep its changes, their names and their order, and drop the line. When the finished milestone suggests an amendment (a change no longer needed, a new one, another order), propose it to the user and apply it only if they agree. Never re-plan an agreed split silently.
    • Next lists changes without that line: ask the user whether that split is agreed. On yes, keep it as above (no line is written); on no, split it as below.
    • Next has no changes: split it into changes (2-6, each about one PR).

    The new Next gets a paragraph, unless the author explicitly agrees its split now: then list its changes in the same Changes format and add **Split agreed:** <today> above them.

    If an architecture assumption broke, write a new ADR with Status: proposed and Supersedes: <number>, review it as init step 5 reviews the baseline (a new round in openspec/architecture-review.md, same escalation rule), and accept it only after that gate passes (Status: accepted with the date, its index row in, the superseded ADR's row out). Never edit an accepted ADR: the superseded ADR's file stays unchanged.

  6. Commit and finish like init.

status

One answer, no edits: current milestone, its changes (done / planning merged, code pending / code unverified / code state unknown / active / parked / planned, derived as above, one status per change with its proof; a change whose review.md holds VERDICT: DEFERRED is listed as parked with its Deferred to: targets, never as active; one listed in the milestone's Changes is parked (listed in error) instead, neither started nor counted done, with the line that removing it from Changes is the user's planning edit), the stale note when a fetch failed or was refused (local refs only (no gh account)), exit criteria state, and what next would do: the change it would start, what it waits for when no change is ready to start (as next step 3 reports it), or milestone end: run close when every change of the Now milestone is done (never while one is parked (listed in error): then report that change, as next step 3 does), unless an exit criterion was shown failing by close or reported failing by the user: then the gap-closing change next would propose. On a parking change's branch, say the parking change is on its own branch and that next starts from main once it has merged.

Rules

  • Keep it small: the strategy is one page, the roadmap one or two, the architecture a few pages plus ADRs. Detail lives in OpenSpec changes, not here.
  • The roadmap is a plan, not a contract. Change it whenever a milestone teaches something; record why in the Done line.
  • Never mark a user-verified criterion passed on the user's behalf.
  • A parking change (a backlog for later milestones, never applied or archived) has its review recorded as VERDICT: DEFERRED only by the user's decision. It is never listed in a milestone's Changes, since it is never archived and so never done; next, close and status report one listed there as parked (listed in error). The user merges its branch: local by hand with --no-ff, pr on GitHub (watch reports it parked). next moves a milestone's part into its own change, which gets its own full review. Removing an emptied parking change is the user's planning edit, never automated.

Planning repo

OpenSpec decides where planning lives: in this repo, in a folder nested in it, or in a store (a separate git repo). Resolve it at the start of every run, before any write; never assume ./openspec/.

  1. Root: openspec list --json from the code checkout, plus --store <id> when the session selected a store. It needs no change name, writes nothing and still works after archive. root null with an error whose message starts with Declared in or Invalid store declaration in → stop before any git or file write and show that message and fix.
  2. Repo, in one call: git rev-parse --path-format=absolute --git-common-dir --show-toplevel && git -C "<root.path>" rev-parse --path-format=absolute --git-common-dir --show-toplevel.
    • The second fails (root not in a git work tree) → stop and ask: initialise git there, or abort.
    • Different common dir → store-backed: the planning repo is the store checkout at its toplevel.
    • Same common dir and toplevel → repo-local (including a root nested in this checkout, such as planning/): one repo, as before.
    • Same common dir, different toplevel → the root is in another worktree of this repo, where a commit would land on that worktree's branch. Stop before any write, name both worktrees (path and branch) and ask.
  3. Announce once: Code repo: <toplevel>; planning repo: <toplevel> (store <root.store_id>) or (same repo).

Paths. <P> is <root.path>/openspec made relative to the planning repo's toplevel (openspec in the usual layout, planning/openspec for a nested root). Stage and ls-tree planning files as <P>/... in the planning repo.

Where commands run. Git, gh, as.sh and vcs.sh commands on the store run in one shell call that starts with cd "<store toplevel>" && (as.sh only fences the repo it starts in). openspec templates and openspec schema validate have no root selection: run them with cd "<root.path>" &&. git -C is for read-only queries only (rev-parse, status, log, ls-tree).

Archive name. Use archivedAs from the archive output when you have it. Otherwise it is the change name if that starts with YYYY-MM-DD-, else YYYY-MM-DD-<change-name>: <P>/changes/archive/<archived-name>/.

Referenced stores. A references: list in the root's config.yaml does not move the planning root: a repo with its own root stays repo-local. Referenced stores are read-only: never branch, commit, stash, push, open a PR or write a file in one. Read them only through openspec context --json (each members entry with role: referenced_store gives its path and a fetch command such as openspec show <spec-id> --type spec --store <id>). A member whose status has reference_unresolved (the store is not registered on this machine) is named in the report as unresolved reference: <id> with its fix, and the step continues without it; never guess its path.