This document is the authoritative description of the development lifecycle for
projects using this framework. AGENTS.md summarizes it; this file is the
source of truth. If the two disagree, this file wins and AGENTS.md should be
corrected.
A work item is one unit of change — a feature, a behavior change, or a
sizable bug. Each work item gets a directory under work/ and moves through six
phases. Each phase consumes the previous phase's artifact and produces its own.
Nothing is implicit: the repository's committed work/ directory plus the code
diff is the complete state of the work, visible to a fresh clone, a teammate,
and CI.
/spec ──▶ spec.md ──▶ /plan ──▶ design.md + tasks.md ──▶ /ship plan ──▶ merge ──▶ /build ──▶ code + [x] tasks
│
/ship ◀── review.md ◀── /review ◀── verify.md ──────┤ (/test)
└─ visual.md (/visual, optional, UI only)
A work item is done when its PR is open (or merged) and review.md records an
approving verdict. The lifecycle stops at "reviewable PR"; merging and
deployment are human decisions owned by the user and CI.
This section is the single authority for the recursive lifecycle model; other surfaces reference it and restate none of it. The model generalizes the single-level lifecycle above and the manifest spelling described in "Decomposing nodes and the manifest": the same six-phase cycle applies uniformly at every node, at any depth. The cycle itself is defined in "Phases"; the model introduces no second phase list, state model, or vocabulary.
The model states its vocabulary once, here:
- A work item is one unit of change that runs the lifecycle. Every directory
under
work/is a work item and an instance of the model. - A node is any work item in a recursive plan tree: a standalone item, a nested child, or an item that itself contains children. A node is addressed by its canonical reference (see "The work item directory").
- A leaf is a node with no children. A leaf is a valid terminal: a standalone item and a nested feature are both leaves until a planner decomposes them.
- A decomposition is a node's enumerated child set — the children a planning step records for the node.
- A plan is a work item's committed plan state: its requirements, design, and task artifacts together with its decomposition when it has one.
The legacy roadmap.md carrying phase: roadmap is a recognized
deprecated alias for the manifest: a node holding it is still a decomposing
node, derives the same container phase, and is never rewritten or migrated. The
old "roadmap" term is retained only for backward compatibility; new manifests
always use manifest.md with phase: manifest.
The standard six-phase cycle (see "Phases") applies uniformly at every node. The
model imposes no maximum recursion depth: any node may decompose into
children, and a decomposition may nest without limit. A one-segment standalone
item (NNNN-slug) and a nested child (NNNN-slug/MMMM-slug) are valid instances
of the same model; they differ in position, not in rule. Depth changes no node's
phase semantics — a node at any depth runs the same cycle.
A node's child set is authored and recorded by the planning step — /plan
(architect) — which is the recursive writer for a decomposition at any
depth, exposing the canonical signature /plan <item-ref or initiative>.
/plan <initiative> creates a brand-new top-level decomposing node (the next
top-level NNNN), and /plan <item-ref> revises an existing decomposition at
any depth.
For a decomposing node the writer authors the manifest
work/<item-ref>/manifest.md, whose feature: is the node's full canonical
reference and whose phase is manifest, and reserves each enumerated child as
work/<item-ref>/<MMMM-slug>/ with a .gitkeep. The manifest's shape and its
per-immediate-parent MMMM allocation are those of docs/artifact-conventions.md
→ "manifest.md — the manifest of a decomposing node" and "Sequence allocation"
(referenced, not restated here).
The writer writes no child phase artifact: each new child directory holds
only .gitkeep, and the child's own phases stay independently owned. It is
autonomous — it writes the manifest and child reservations with no pre-write
approval gate, and the user reviews the committed artifact afterward. A node that
is a genuine single feature is planned as before, producing design.md/tasks.md
and no manifest; a decomposing node always gets the manifest and MAY also carry
its own design.md/tasks.md when it has a deliverable of its own (a hybrid
node), which are never forced on a pure container.
A decomposing node's child set is committed plan state: it is versioned with
the node's other plan artifacts, so a fresh clone derives the same decomposition.
The handoff publishes the node's plan state (## Plan publication) and then
routes to /spec <child-ref> for each ready child. Revising an existing
decomposition is ### Revising a decomposition; the writer defines no second
revision or publication grammar.
A plan may decompose into a hybrid of tasks, features, initiatives, and
larger items. Fine-grained tasks.md entries remain a leaf's internal work list
and are not nodes: a planner creates a child by explicitly decomposing an
item, never by promoting a task. A node may carry its own phase artifacts and a
child set at the same time — the standard cycle applies to the node's own
artifacts independently of its children — and no node is required to be a pure
container.
The recursive model composes with the shipped backtracking and re-entry model
(## Phase reversal (backtracking)) per node, on the same unshipped work item:
the reverse edges, stale: marking, re-entry, recall, and the ownership
invariant are reused unchanged. A later phase that finds a node's decomposition
wrong takes the same reverse edge to the planning step, and the model introduces
no second state model, no new phase value, and no new reversal mechanism for a
decomposition.
Dependencies, readiness, and declared conflicts are likewise reused unchanged:
the model adds no dependency edge, conflict class, or state vocabulary.
Depends on and readiness keep their shipped definitions
(### Dependencies and readiness), and declared conflicts keep their shipped
grammar and reporting (### Declared conflicts (conflicts-with)) — both are
referenced, never restated. Ownership of their recursive generalization is
sibling-scoped: readiness, reporting, and derived state across levels are owned
by 0004-recursive-readiness, and the reference grammar, per-level sequence
allocation, and the parent chain are owned by 0002-recursive-schema.
Backward compatibility is preserved: a one-segment standalone work item
(NNNN-slug) behaves exactly as before with no forced migration, and existing
committed work/ content — including legacy roadmap.md parents and their
nested children — remains valid under the model.
work/
0001-add-dark-mode/ # standalone work item
spec.md # requirements (product)
design.md # technical design (architect)
tasks.md # ordered, checkable work (architect; builder ticks boxes)
verify.md # test evidence + gaps (tester)
review.md # findings + verdict (reviewer)
0002-billing/ # decomposing parent work item
manifest.md # initiative + children (manifest)
0001-model/ # nested child work item
spec.md # requirements (product)
...
0002-api/ # nested child work item
spec.md
...
This layout repeats at every level: any node may hold a manifest and child directories, so nesting continues to arbitrary depth.
NNNNis the next zero-padded sequence number across the wholework/tree. Allocate it by finding the highest existing directory name and adding one.slugis a short kebab-case handle derived from the request (e.g.add-dark-mode). Prefer 2–4 words. It is immutable once created.- A canonical reference is an item's path relative to
work/: a recursive path of one or more dash-slug segments —NNNN-slugfor a standalone item, then one further/<MMMM-slug>per nesting level, to arbitrary depth. The one-segment form is unchanged and the two-segment form is a special case, not a maximum; phase commands accept any depth, and a one-segment reference behaves exactly as before. The grammar is defined indocs/artifact-conventions.md→ "Work item references". - Workflow artifacts under
work/are committed working state: version-controlled so a fresh clone, a teammate, and CI derive the same phase. Onlyscratch/and opencode's generated state are ignored. - A phase may be skipped only by explicit user request. If skipped, say so in
the next artifact's frontmatter
notes.
A decomposing node is a parent work item that plans a broad, multi-feature
initiative before any child is specified. It lives in the same tree as ordinary
items: its directory work/<NNNN-slug>/ holds its manifest manifest.md and
one nested child work-item directory per feature,
work/<NNNN-slug>/<MMMM-slug>/. A pure container has no spec, design, tasks,
verification, or review of its own, and each child later runs the ordinary
per-feature lifecycle unchanged; a hybrid node that has a deliverable of its
own also carries its own phase artifacts alongside the manifest. Artifact shapes
and the reference grammar are defined in docs/artifact-conventions.md.
The manifest is the sole child-enumeration artifact of a decomposing node, at
every depth; its concrete spelling is manifest.md with phase: manifest. The
legacy roadmap.md carrying phase: roadmap is a recognized deprecated
alias (see ### Terms), never written for a new node.
The planning step (/plan, architect), exposing the canonical signature
/plan <item-ref or initiative>, authors the manifest and creates each child
directory containing only a .gitkeep, then stops. It writes no child specs and
performs no child phase work. Child numbering is local to the parent and
independent of the flat top-level sequence and of other nodes.
The Children table in a decomposing node's manifest (manifest.md) is the
single machine-readable source for the child set and the dependency graph, at
every depth. The Depends on cell holds another child's local id,
comma-separated when there are two or more, or — when the child has none. A
dependency may not name its own row, dependencies are sibling-only — a
Depends on value names only a sibling local id in the same Children table —
and the stored graph must be acyclic. An ancestor, descendant, cousin, or
cross-tree target is not a valid dependency edge; it is reported by the integrity
window as a DANGLING-DEP finding rather than accepted, and no new dependency
edge kind is introduced. If an initiative's dependencies are cyclic, the manifest
records the cycle under ## Open issues and leaves the stored graph acyclic.
Every decomposing node applies the same readiness rule to its own immediate children, independently at every level; a child resolves under the node's own canonical reference.
Readiness is derived live from files at status time; it is never stored:
satisfied(dep_local_id):
child_dir = work/<node-ref>/<dep_local_id>/
if child_dir does not exist -> dangling; not satisfied
if child_dir/ship.md exists
and it carries a `reopened:` marker -> not satisfied # recalled; shipped signal revoked
if child_dir/ship.md exists -> satisfied # shipped; presence is the sole shipped signal
if child_dir/review.md exists
and its verdict == "approve" -> satisfied # approved, even if unshipped
otherwise -> not satisfied
ready(child) = every dependency of child is satisfied AND child is not in a cycle
blocked_by(child) = [dep_local_id for each unsatisfied dependency]
A child with no dependencies is ready. A dependency is satisfied when its child
directory contains ship.md — its presence is the sole shipped signal, keyed on
presence rather than contents — or when its review.md verdict is approve, which
satisfies the dependency even if unshipped. ship.md presence takes precedence
over a request-changes verdict; a request-changes verdict or a missing
review.md (with no ship.md) is not satisfied.
A Depends on target that is itself a decomposing node is satisfied by its own
signal only — the ship.md/approve rule above applied to that node — and its
descendants are not consulted; readiness does not recurse through its children.
A non-recalled ship.md presence and an approve verdict satisfy exactly as
before. A recalled item — its ship.md carrying a reopened: marker — does
not satisfy the dependency, and its dependents are reported blocked, naming the
recalled item as the unsatisfied dependency, until it re-ships; the revocation is
defined by ### Shipped items and reopen.
A child in a cycle is never ready.
A planning-time conflict is a declared, committed claim by one plan that it
expects to collide with one or more targets. It is plan state recorded before
development, not a detected collision: it is distinct from the merge-time
## Merge conflicts contract, which acts on branches after they exist, and
distinct from Depends on, which is a readiness edge. A declaration is advisory —
it adds, removes, or reorders no Depends on edge and changes no child's
readiness; only Depends on gates readiness. A pair is a declared conflict when
at least one side names the other, so a declaration does not require a reciprocal
declaration.
The Children table carries the declaration in a conflicts-with column. The
same grammar defines the intended conflict set of a child or standalone work item;
that grammar is independent of where such an item's declaration is stored. A
manifest authored before the column existed has no conflicts-with column, and its
absence is treated as no declared conflicts (— for every row).
A child or standalone item's own declaration is stored in its design.md
frontmatter conflicts-with value; the field is optional and holds one
ConflictTargetList, the grammar defined below. Absence of the field or a —
value means no declared conflicts, and no separate container or artifact is
required. A decomposing child's declared set is the union of that design.md
value and its parent Children row's conflicts-with cell; when the two
disagree, the later read-only check reports the disagreement rather than silently
preferring one. A malformed or unresolved item value is likewise reported by the
later check, never dropped or auto-repaired. The declaration is advisory: it adds
no readiness edge, reorders no child, and changes no child's readiness.
A conflicts-with cell holds one ConflictTargetList:
ConflictTargetList ::= "—" # no declared conflicts
| ConflictTarget ("," ConflictTarget)*
ConflictTarget ::= SiblingOrItemRef | SurfacePath
SiblingOrItemRef ::= [0-9]{4}-[a-z0-9-]+( /[0-9]{4}-[a-z0-9-]+ )*
SurfacePath ::= repository-relative file or directory path
- Three target kinds. Each target is exactly one of: a sibling local id
(
MMMM-slug) naming a different row in the sameChildrentable; a canonical work-item reference — a top-levelNNNN-slug(including a decomposing parent), optionally followed by nested/<MMMM-slug>segments,NNNN-slug(/MMMM-slug)*, nesting to arbitrary depth — resolving towork/<ref>/; or a repository-relative surface path, an exact file or directory path (for exampledocs/workflow.md,tests/checks). A reference resolves from the repository root, independent of the declaring node, and may name an ancestor, a descendant, or an item in a different subtree: cross-level naming is permitted. Surface paths are exact: no glob metacharacters, no.., no absolute paths. - Separation and empty value. Targets are comma-separated and each is trimmed
of surrounding whitespace. Repository paths and references contain no comma, so
a comma always separates targets. A cell that is
—(em dash, the same convention asDepends on) declares no conflicts; an empty or whitespace-only cell is malformed, not equivalent to—. - Reference-vs-path discriminator. A target matching
^[0-9]{4}-[a-z0-9-]+(/[0-9]{4}-[a-z0-9-]+)*$— one or more segments — is a reference (a sibling local id, or a canonical work-item reference at arbitrary depth); any other target is a surface path. A reference containing/is root-relative and is never resolved against a sibling table. For a bare single-segment reference (no/), resolution precedence is the declaring entity's immediate parent'sChildrentable first, thenwork/<token>/at the repository root, so a bareMMMM-slugis deterministic when a sibling row and a top-level item could share the same text; an entity with no immediate parent (a standalone top-level item) has only the rootwork/<token>/fallback. - Well-formedness. A reference may not name the declaring entity's own local id or its own full canonical reference (both are unresolved self-references), and a list may not repeat a target after trimming.
- Unresolved declarations. A malformed cell, or a target that resolves to no
sibling row, no
work/<ref>/directory, and no existing path, is an unresolved declaration. It is reported by a later read-only check and never silently dropped or auto-repaired. - Reporting vocabulary is reused. A declared conflict, when reported, uses the
shipped
(a)–(d)class labels and the finding-line grammar defined in## Merge conflictsand.opencode/skill/merge-conflict/SKILL.md. There is no new class, finding code, or policy defined for declarations.
/status renders the decomposition tree recursively: each decomposing node
followed by its descendants, indented to arbitrary depth. Every node at every
level shows its derived phase and its readiness as ready or
blocked: <local ids> — the comma-separated local ids of the unsatisfied
siblings that block it. Readiness is derived live from committed files at status
time and is never stored.
A decomposing node's row shows <ready>/<total> ready across its immediate
children plus a distribution tally of those children across phases
(<phase> <n>, ...), so a decomposing node is never presented as a
single-feature item. Each blocked child names the specific siblings blocking it.
Status is read-only and modifies nothing.
Status also reports integrity findings rather than failing:
DANGLING-DEP— aDepends onlocal id with no child directory or no table row.MISSING-CHILD— a table row whose canonical reference/directory is absent.UNLISTED-CHILD— a child directory present under the parent but absent from the Children table (possible rename). When the parent's## Open issuesnames that directory as a withdrawn child, it is a deliberate withdrawal: reported report-only and never auto-repaired, not treated as an accidental graph fault.CYCLIC-DEP— a cycle in a manually edited graph; cycle members are never reported ready.DRIFT-FACT— a node holds bothmanifest.mdand a redundantroadmap.md, or a manifest's spelling andphasevalue disagree (manifest.mdwithphase: roadmap, orroadmap.mdwithphase: manifest).manifest.mdis authoritative and the inconsistency is reported, never silently merged, dropped, normalized, or auto-repaired.
When /spec is invoked for a nested child, the product agent resolves the parent
manifest.md and evaluates readiness first. If the child is blocked, it reports
the specific blocking children and stops before writing spec.md. It proceeds
only on an explicit user override, and then records the override and the blocking
dependencies in the new spec.md frontmatter notes. Refusing touches no
existing file, and status still reports the child's dependency state afterwards.
A decomposition is revised by the planning step (/plan, architect); the
command's /plan <item-ref> route revises an existing decomposition in place
at any depth — a top-level node or a nested child that itself decomposes.
<item-ref> must resolve to a node that has a manifest,
work/<item-ref>/manifest.md; a reference with no manifest is refused, because
the revise route revises an existing decomposition and never creates one. The
planning step (/plan, architect) is the sole writer of a manifest.md:
no other phase writes one. A child phase that discovers the decomposition is
wrong records a finding in its own committed backtracks.md (below, and
## Phase reversal (backtracking))
and recommends the revise route; it never edits the manifest. The revision writes
autonomously, with no pre-write approval gate, exactly as authoring does; the user
reviews the committed revision after the fact. A revision request that changes
nothing is a no-op and modifies no file.
A revision applies four operations to the loaded decomposition, in one pass, and validates the result after all four (never on an intermediate state):
- Re-scope — replace an existing row's
Titleand/orScopein place. The row'sLocal id, child directory, andCanonical referenceare preserved. - Add — append a row for a newly enumerated feature. Its
Local idis the next local number: the greatest 4-digitMMMMprefix ever committed underwork/<item-ref>/plus one, perdocs/artifact-conventions.md→ "Sequence allocation" applied within the node. A spent number is never reused. Creatework/<item-ref>/<MMMM-slug>/.gitkeep, and set the row'sCanonical referenceto resolve to that directory. - Withdraw — remove the row from the
Childrentable and from## Sequencing. The child's directory and spent local number are preserved (the number is never reused). Record the withdrawal and its rationale under the node's## Open issues, naming the retained directory; the record shape is indocs/artifact-conventions.md→ themanifest.mdtemplate. A withdrawal does not mark the child stale — it leaves the active set. - Re-sequence — edit
Depends oncells as required and rewrite## Sequencingas a topological order of the stored graph, so every child follows all of its dependencies.
After all four operations, the revision runs one validation predicate over the final state and stores no fault:
- every
Depends ontoken names another row'sLocal idin the sameChildrentable, and no row depends on itself; - the stored graph is acyclic;
- every row's
Canonical referenceresolves to an existing child directory, and every active child directory (all child directories except recorded withdrawals) appears as a row; ## Sequencinglists every active child after all of its dependencies;- the six-column
Childrenheader is unchanged —Depends onstays at pipe-field 5 andconflicts-withat pipe-field 6 — and everyconflicts-withcell is—or a well-formed target list.
If the intended dependencies would create a cycle, the revision stores no cyclic
edge, leaves the stored graph acyclic, and records the cycle under
## Open issues. If an operation would leave a dependency dangling — for example
withdrawing a child another child depends on — the revision does not silently
choose a resolution: it either applies the re-point or withdrawal that removes the
dangling edge in the same revision, or refuses and reports the ambiguous intent.
An existing unshipped child is affected when the revision changes any cell of
its Children row (Title, Scope, Depends on, conflicts-with) or
explicitly re-sequences it. For each affected existing unshipped child, the
revision marks each present phase artifact (spec.md, design.md, tasks.md,
verify.md, review.md) stale: manifest, per ## Phase reversal (backtracking);
nothing is deleted or silently rewritten, and the pre-revision content stays
recoverable from committed git history. stale: manifest maps to spec when the
child's phase is derived, so the child must re-run forward. A child holding only
.gitkeep marks nothing and simply derives not started; an unaffected child is
untouched.
A shipped child — its ship.md present — is excluded from a backtrack by the
model above. A revision that would re-scope or re-sequence a shipped child
refuses and surfaces the conflict for the user rather than rewriting the
shipped child's artifacts; the post-ship path is 0005-post-ship-pr-denial.
A revision records its provenance in committed state. The triggering child's
detecting phase appends a finding entry to work/<child-ref>/backtracks.md — a
target phase of manifest (the stale token), the affected
work/<item-ref>/manifest.md, observable evidence, and status open — and, when
the revision completes, the planning step (/plan, architect)
appends the matching resolution entry naming what changed. backtracks.md is
item-level backtrack state, not a phase artifact, so appending it does not breach
the ownership invariant; the planning step appends only this entry and writes no
child phase artifact. The node's manifest.md changes its frontmatter updated
date and appends a revision note under ## Open issues naming the date, the
triggering child, the operations applied, and the invalidated children. The
pre-revision manifest remains recoverable from committed git history. The entry
shapes are in docs/artifact-conventions.md → "backtracks.md" and the
manifest.md template.
Each phase below lists: Purpose, Entry criteria, Process, Exit criteria, Artifact, Next.
- Purpose: Convert an informal request into a precise, testable, implementation-free specification.
- Entry: A request from the user. No
spec.mdrequired. - Process: Recon the repo for domain context → surface ambiguities → ask the
user batched clarifying questions → write the spec. Details in the
productagent prompt and thespec-writingskill. - Exit:
spec.mdhas a problem statement, goals, non-goals, at least one user story, and numbered acceptance criteria in Given/When/Then form. Open questions are either resolved or explicitly marked as deferred. - Artifact:
work/<item-ref>/spec.md, frontmatterphase: spec. - Next:
/plan <item-ref>. If/planfinds this spec ambiguous or wrong, it takes the/plan→/specreverse edge (see "Phase reversal (backtracking)"); the finding's handoff routes the item back here to re-run/spec <item-ref>and revisespec.md.
-
Purpose: Decide how to satisfy the spec and decompose the work into independently verifiable tasks — or, for a node that decomposes, author or revise that node's child enumeration (its decomposition).
-
Entry: For a single-feature design,
spec.mdexists and is unambiguous. For a decomposition, the node is one that decomposes (an existingwork/<item-ref>/manifest.md, or the architect's planning judgment) and the node's ownspec.mdis not required — a.gitkeep-reserved child or a pure container has none, and the decomposition path writes none. A genuine single feature remains reachable only through the ordinary single-feature path, which requires itsspec.md. -
Process: Read
spec.mdwhen present → recon existing architecture and conventions → evaluate at least one alternative → then take one of two paths, selected from the node's committed state:- Single-feature path (unchanged): write
design.md→ decompose intotasks.md. Details in thearchitectagent prompt. - Decomposition path: author the node's manifest
work/<item-ref>/manifest.md(a node with no manifest) or revise it (an existing manifest), reserving each enumerated child aswork/<item-ref>/<MMMM-slug>/with a.gitkeep. Authoring and the four-operation revision are### Where decomposition is authored and recordedand### Revising a decomposition; the writer states no second grammar and writes no child phase artifact (each new child holds only.gitkeep). It is autonomous — no pre-write approval gate; the user reviews the committed artifact. A hybrid node also gets its owndesign.md/tasks.mdwhen it has a deliverable of its own; neither is ever forced on a pure container.
- Single-feature path (unchanged): write
-
Exit: For a single feature,
design.mdnames the approach, the alternatives considered, the interfaces/data model affected, and the risks; andtasks.mdis an ordered list where every task is small (roughly one focused commit), has its own verification, and declares dependencies, with every acceptance criterion inspec.mdmapping to at least one task. For a decomposition, the manifest and every child reservation exist and the manifest satisfies the manifest quality bar ofdocs/artifact-conventions.md→ "manifest.md— the manifest of a decomposing node" — six-columnChildren, everyDepends onnaming another row with no self-dependency and an acyclic stored graph, everyCanonical referenceresolving to its child directory, and everyconflicts-withcell—or well-formed (referenced, not restated). -
Artifact:
design.mdandtasks.md(single feature, frontmatterphase: designandphase: tasks) and/or the manifestmanifest.md(frontmatterphase: manifest) plus the child directory reservations, with the node's owndesign.md/tasks.mdwhen hybrid. -
Next:
/ship plan <item-ref>to publish the node's plan state (see "Plan publication"); then/buildonce the plan is merged — for a decomposing node the handoff also names/spec <child-ref>for each ready child. A denied or changes-requestedplan/<ref>pull request routes back here to re-run/plan <item-ref>and republish — never a force-push, and not a backtrack. If the spec is ambiguous or wrong, take the/plan→/specreverse edge and hand offNext: /spec <item-ref>(see "Phase reversal (backtracking)").
- Purpose: Implement the tasks.
- Entry:
tasks.mdexists with unchecked items, and the plan is published — see the/buildplan gate in "Plan publication". An unmergedplan/<ref>branch blocks development; an item with no publication recorded (historical or pre-flow) proceeds with a note. - Process: Read
spec.md,design.md,tasks.md→ select the requested task or the next unblocked unchecked task → implement following existing conventions → run the project's checks → tick the box intasks.md. - Exit: Each completed task's check box is
[x], its acceptance is demonstrably met, and the project's lint/typecheck/tests pass for the touched scope. Do not commit. - Artifact: source changes plus
tasks.mdwith updated check boxes. - Next:
/buildagain for more tasks, then/testwhen all boxes are checked. If the design is wrong, take the/build→/planreverse edge and hand offNext: /plan <item-ref>(see "Phase reversal (backtracking)"); the task stays unchecked and the design is revised by its owner.
- Purpose: Independently verify the implementation against the acceptance criteria and close coverage gaps.
- Entry: All tasks are checked, or the user asks for verification.
- Process: Read
spec.mdandtasks.md→ map every acceptance criterion to a test → run the suite → write missing tests → record results and residual risk inverify.md. - Exit: Every acceptance criterion is covered by at least one passing test or is explicitly flagged as manual/untestable with a reason. Failures are reported, not hidden.
- Artifact: test files plus
work/<item-ref>/verify.md, frontmatterphase: test. - Next:
/review. If defects were found, classify each one: an implementation defect routes to/buildto fix it first, while a defect that is really a spec or design fault is routed upstream through the backtrack model — take the/test→/planor/test→/specreverse edge (recordedbacktracks.mdfinding, downstreamstale:markers,Next: /plan <item-ref>orNext: /spec <item-ref>). If the classification is unresolved, escalate rather than guess and do not push the item at the wrong phase. - Optional visual pass: if the work item has a user-facing UI, run
/visual(or have the tester delegate to thevisualsubagent). It drives a real browser, produceswork/<item-ref>/visual.mdplus screenshots, and feeds its findings into review. It is optional so that non-UI projects never need a browser; when present, review must consider it.
- Purpose: A skeptical, read-only pass over the diff against the spec and project standards.
- Entry: Tests pass and
verify.mdexists. - Process: Determine the base ref → read the diff → check correctness,
security, tests, conventions, scope, and performance → write severity-ranked
findings with
file:linereferences and a verdict. - Exit:
review.mdstates a verdict ofapproveorrequest-changes, and every finding has a severity, a location, and an actionable recommendation. A finding of any severity —Blocker,Major,Minor, orNit— that survives scrutiny yieldsrequest-changes;approverequires that no finding of any severity survives. - Artifact:
work/<item-ref>/review.md, frontmatterphase: review. - Next:
/shipifapprove; otherwise/buildto address findings.
- Purpose: Turn a reviewed work item into a reviewable pull request.
- Entry:
review.mdexists with verdictapprove(or the user explicitly overrides), and the item is not challenged — an open challenge inchallenges.mdblocks it (see## Findings challenge and adjudication). - Process: Reconcile first — if the branch has fallen behind the default
branch, merge it forward and resolve conflicts per
## Merge conflicts→ confirm checks are green and the tree contains no secrets → create a branch → stage logical commits with conventional messages → (user-approved) push → open a PR with a structured body linking the artifacts → writeship.mdrecording the branch, commits, and PR (or "not created"), commit it (docs(work): record ship state for <item-ref>), and push so the shipped signal travels with the branch. - Recall mode:
/ship recall <item-ref> <phase>handles a post-ship denial by revoking the shipped signal and re-entering the named phase; see### Shipped items and reopen. - Exit: PR URL reported to the user, or — when
ghis unavailable — the local-commit path recorded inship.md; the branch carries a committedship.mdand the run leaves no uncommittedship.md. Nothing is merged by the agent. - Artifact: branch, commits, PR, and
ship.mdrecording the shipped signal (branch, commits, and PR URL). Presence ofship.mdis the shipped signal; see "Dependencies and readiness". - Next: Human review and merge.
/statuswill show the item as shipped.
A work item's plan is its committed plan state under work/<item-ref>/. For a
single-feature item that is its requirements and design artifacts — spec.md,
design.md, and tasks.md when present. For a decomposing node it is its
manifest manifest.md, its own spec.md/design.md/tasks.md when present, and
its child directory reservations. The plan is published to the shared default
branch before development begins so other maintainers can cross-reference
intended surfaces before any code exists. It is a process step, not a lifecycle
phase: it adds no artifact, no phase value, and no derived-state row.
Publication is a mode of /ship — /ship plan <item-ref> — performed by the
shipper, the only agent that writes git (see AGENTS.md → guardrails). Plan mode:
- Preconditions. The item has completed
/plan: eitherwork/<item-ref>/spec.mdanddesign.mdexist (a single-feature plan;tasks.mdis included when present), orwork/<item-ref>/manifest.mdexists (a decomposing node's plan, its ownspec.md/design.md/tasks.mdincluded when present). Thespec.mdanddesign.mdrequirement is not relaxed for an ordinary single-feature item. Noreview.mdis required — plan mode is a documented exception to the approved-work-item entry criterion, like fix mode. The plan diff contains no secrets. - Branch. A dedicated
plan/<ref>branch, where<ref>is the item's canonical reference flattened by the single collision-free reversible rule in.opencode/skill/conventional-commits/SKILL.md→ "Branch naming" (flatten(ref)joins the reference's/-separated segments with+; for exampleplan/0013-simultaneous-work-recursion+0004-parallel-publication). Every surface names the branch only by that rule and never restates it. Each item gets its own branch, so concurrent publications do not overwrite one another, and the plan branch is distinct from the item's final ship branch. - Commit and pull request. The node's own plan state under
work/<item-ref>/is committed as one conventional commit and pushed — for a decomposing node the manifest, its own phase artifacts when present, and its child.gitkeepreservations, but never a child's own in-flight phase artifacts (a child publishes its own plan). A pull request is opened with the plan description template. The PR links the plan artifacts by repository path and prints the item's declared conflicts, or—. - Approval and merge. At least one human approval is required before the plan pull request is merged (a process precondition the framework documents but does not verify offline). The shipper neither approves nor merges; the merge is a human action, and the repository may enforce approval via branch protection.
- No shipped state. Plan mode never writes
ship.md, never runs the ship pre-flight or reconcile, and never creates the final ship branch or pull request.ship.mdremains the sole shipped signal. - Revision. When a later
/planrevision changes the intended surfaces or declared targets, it is republished through the same flow: a commit is added to the existingplan/<ref>branch and its pull request updated (or a new one opened if the branch was pruned). A pushed branch is never force-pushed. - Denied or changes-requested pull request. When a
plan/<ref>pull request is denied, closed, or sent back for changes, the documented route is to re-run/plan <item-ref>to revise the plan and republish with/ship plan <item-ref>. Publication adds a commit to the existingplan/<ref>branch and updates its pull request (or opens a new one when the branch was pruned) — never a force-push. This round trip is the plan-publication revision flow, explicitly not a backtrack: it writes nothing tobacktracks.mdand moves no phase backward. - Idempotence. Re-invoking publication when the plan is already on the default branch and unchanged is a no-op: it reports that and creates nothing.
- No migration. An already-merged plan is read from the default branch and
stays valid. A pre-existing unmerged branch under the superseded
/→-joined name is not recognized: the gate derives only the collision-free name, so such a branch neither blocks development nor is read as a publication, and it falls to the existing "no publication recorded" case (see "The/buildplan gate"). Historical and shipped plans are never rewritten.
Development does not begin while a plan pull request is unmerged. Before
selecting a task, the builder runs an offline-first, git-only gate — it requires
no gh and never hard-fails:
plan_gate(item_ref):
ref = flatten(item_ref) # the rule in
# .opencode/skill/conventional-commits/SKILL.md → "Branch naming"
plan_branch = "plan/" + ref
default_ref = "origin/<default>" if origin exists, else "<default>"
# 0. Refresh refs best-effort (an offline fetch is ignored). Refresh the
# default branch, and the plan branch too when the remote advertises one,
# so the comparison below sees a later revision and a retained, already-
# merged branch is not mistaken for an unmerged one.
remote_plan = false
if origin exists:
git fetch origin <default>
remote_plan = git ls-remote --heads origin plan/<ref> returns a ref
if remote_plan: git fetch origin plan/<ref>
# Resolve the plan branch (remote first, then local) and flag it unmerged when
# its plan artifacts differ from the default branch — a first publication or a
# later revision. Branch existence alone is not enough, because a merged plan
# branch may be retained. A ref the remote advertises but that does not resolve
# locally cannot be compared, so it is refused rather than read as matching or
# differing.
plan_ref = none
unmerged_plan = false
unresolved_plan = false
if remote_plan and origin/plan/<ref> resolves:
plan_ref = "origin/plan/<ref>"
elif a local branch plan/<ref> exists:
plan_ref = "plan/<ref>"
elif remote_plan:
unresolved_plan = true
if plan_ref is not none:
unmerged_plan = git diff --quiet <plan_ref> <default_ref> -- work/<item_ref>/ is false
# 1. Published / merged? A plan is published when the default branch holds the
# item's design artifact or, for a decomposing node, its manifest. A
# differing plan branch is not yet published.
published = <default_ref> has work/<item_ref>/design.md
or <default_ref> has work/<item_ref>/manifest.md
if published and not unmerged_plan and not unresolved_plan -> PROCEED
# 2. An unmerged plan branch — or one the remote advertises but that cannot be
# resolved and compared — blocks development
if plan_ref is not none or unresolved_plan -> REFUSE
# 3. No publication recorded (historical / pre-flow item)
otherwise -> PROCEED (note it)
- Step 0 refreshes refs best-effort before the default-branch check: it fetches
origin/<default>, and theplan/<ref>branch too when the remote advertises one, so a plan merged since the last fetch is seen even when the localplan/<ref>branch was retained and a later revision is not hidden by a stale remote-tracking ref. The refresh is ignored whenoriginis unreachable. - An unmerged
plan/<ref>branch (case 2) refuses development: the builder stops before implementing and reports the plan branch or pull request that must be merged first. - Case 2 is content-aware, not mere branch existence: a
plan/<ref>branch whose plan artifacts differ from the default branch — a first publication or a later revision — refuses development; a retained branch whose plan artifacts already match the default branch (a merged publication) does not. An unmerged revision therefore blocks development even after the first plan merged. - An item with no plan pull request — historical, or created before this flow — is not blocked and needs no migration; the builder proceeds and notes that no plan publication was found (case 3).
- A pre-existing unmerged branch under the superseded
/→-joined name is not recognized: the gate derives only the collision-freeflattenname, so such a branch neither refuses development nor is read as a publication, and it falls to case 3 (no publication recorded). See "No migration" above. - A plan present on the default branch proceeds even if a merged
plan/<ref>branch is retained, because step 0 refreshes the ref and step 1 treats a branch whose plan artifacts already match the default branch as merged, not unmerged (case 2 is content-aware). - A
plan/<ref>branch the remote advertises but that has no resolvable local ref — for example when the best-effort fetch of it fails — cannot be compared, so the gate refuses rather than read it as matching or differing. - An unreachable
origindegrades to the best-effort result and never hard-fails the build.
The declaration remains advisory throughout: it adds no readiness edge, reorders
no child, and changes no child's readiness. The declared-conflict grammar and its
resolution rules are the single authority in "Declared conflicts
(conflicts-with)"; a plan's declaration home is the design.md frontmatter
conflicts-with value.
The conflicts-with declarations of the unshipped plans under work/ are
compared by one read-only check whose algorithm is stated once here. Every other
surface — the /conflicts command, the /status report, and the /build plan
gate — references this section and restates nothing. Like the merge-integrity
guard, the check is prompt behavior only: there is no committed checker,
script, helper, or executable tool for it and no committed fixture or
agreement area; fixture-based guards and mutation coverage are a separate work
item.
The check reads only committed work/ state and the local repository (for target
resolution). It walks the committed tree recursively and considers every
unshipped entity at any depth, deriving each one's declared set:
- Decomposing node — never an entity itself as a container; it is a
decomposition. Each row of its
Childrentable is an entity keyed by its full canonical reference (the node's own reference with the row's local id appended), and when that row's child directory itself holds a manifest (manifest.md, or the legacyroadmap.mdalias) the child is also a decomposition whose rows are recursed the same way — to arbitrary depth. A decomposition authored before theconflicts-withcolumn existed has no column, treated as—for every row. - Child entity — one entity per
Childrenrow, even when the child directory holds only.gitkeep(the parent cell still represents it). A child whosework/<child-ref>/ship.mdexists is excluded as a counterpart; a child with noship.mdis included.ownis the child'sdesign.mdfrontmatterconflicts-with(absent or—→ empty);cellis the row'sconflicts-withcell (absent column or—→ empty).- The child's declared set is
own ∪ cell, at every depth. - When
ownandcellare both present (neither—) and the two sets are not identical — a subset relationship still counts — the check reports the discrepancy (DRIFT-FACT, below) and prefers neither. A one-sided record is the union and is not a discrepancy.
- Standalone item — one entity per top-level item directory that is not a
decomposing node and has a
spec.md. Its declaration is itsdesign.mdfrontmatterconflicts-with. Aship.mdexcludes it.
An entity with an empty declared set — absent field, absent column, or — —
contributes no declared target of its own, so it yields no finding from its own
declaration and historical items remain valid with no migration. It is still an
unshipped entity: a declared target that names it is a counterpart under the
one-sided naming clause below, even though the named entity declares nothing. A
shipped entity still resolves as a target but is not a counterpart:
naming it produces neither a conflict nor an unresolved finding.
Each target token is resolved with the single algorithm in "Declared conflicts
(conflicts-with)" above — the reference-vs-path discriminator, the
immediate-parent-then-root precedence for a bare MMMM-slug, exact
repository-relative surface paths, and the prohibition on glob metacharacters,
.., and absolute paths. A reference token resolves to a canonical item
reference (a bare token's sibling row <parent>/<local-id>, or the root-relative
work/<ref>/ directory at arbitrary depth); a surface token resolves to its
repository-relative path. A token that is malformed (empty or whitespace-only, a
glob metacharacter, .., or absolute) or that resolves to no sibling row, no
work/<ref>/, and no existing repository path is an unresolved declaration:
it is reported (DANGLING-DEP, below) and never dropped or auto-repaired.
conflict(A, B) is evaluated between any two unshipped entities at any depth
A and B and is true exactly when one of:
- Shared target — some target of
Aand some target ofBare the same target. Two targets share identity when their trimmed repository-relative paths are equal (surface targets) or when both resolve to the same canonical item reference (reference targets). Equal trimmed tokens that resolve to different items — for example the sameMMMM-slugin two different decompositions — are not the same target. - One side names the other — a target of
Aresolves toB's canonical reference, or a target ofBresolves toA's. A one-sided declaration suffices; no reciprocity is required. The named entity need not declare anything itself: an unshipped entity with an empty declared set is still a counterpart under this clause, so a declaration that names it is reported even though that entity contributes no target of its own.
Each unordered pair is reported once: reciprocal naming, or a pair that both shares a target and names the other, yields exactly one finding. The finding list is never truncated; repeated runs on unchanged plans yield identical findings; concurrent runs take no lock and write nothing.
Findings use the shipped finding-line grammar
- [<CODE>] (<class>) <offender canonical reference(s)> — <detail> and a class
label from the shipped (a)–(d) set (## Merge conflicts and
.opencode/skill/merge-conflict/SKILL.md → "Finding grammar"); no new class,
code, or policy is introduced. The check reuses the shipped codes and broadens
their planning-time meaning:
| Situation | Code | Class | Offender / detail |
|---|---|---|---|
Two unshipped entities share a surface target outside work/ |
TEXTUAL-CONFLICT |
(a) |
the two entity refs; detail names the shared path |
Two unshipped entities share a work/ path or name each other |
TEXTUAL-CONFLICT |
(b) |
the two entity refs; detail names the shared target |
| A declared target is malformed or resolves to nothing | DANGLING-DEP |
(b) |
the declaring entity ref; detail names the target |
An entity's own declaration and its immediate parent's cell disagree (both non-—, sets unequal) |
DRIFT-FACT |
(d) |
the entity ref; detail shows both sets |
The shipped merge-time meanings of these codes are unchanged. The two notes that
scope TEXTUAL-CONFLICT to the dry-run-detected case remain true of that case,
while /status and /conflicts may additionally report a declared
TEXTUAL-CONFLICT without performing a dry-run merge.
The check is advisory, offline, local, and read-only. It performs no fetch,
remote read, merge, or dry-run merge; it takes no lock; and it modifies no file.
It operates on committed plan declarations only and does not perform or
reproduce the shipped pre-ship branch/textual detection of ## Merge conflicts:
that contract acts on branches at merge time, while this check compares
declarations at planning time. It is distinct from Depends on as well: a
declared conflict adds no readiness edge, reorders no child, and changes no
child's readiness, so no phase is blocked.
The check runs at three prompt-only enforcement points:
/conflicts [item-ref]— the dedicated read-only command. With no argument it reports every declared conflict, unresolved declaration, and declaration discrepancy across the wholework/tree. With an item-ref it reports only the findings that involve that item and names each counterpart./status— its existing integrity window reports the same findings, read-only and local-only.- The
/buildplan gate — after aPROCEEDoutcome the builder runs the focused check for the item and prints the findings before selecting a task; the findings never change the gate outcome and never stop the build. The check itself performs no fetch; the gate's own best-effort ref refresh is separate.
An empty compared set — no unshipped entities, no declarations, or all entities already shipped — reports no findings and does not error.
This section is the single authority for reversing the lifecycle. A backtrack
is a sanctioned reverse transition on the same unshipped work item, triggered
by a detected defect in an earlier phase's artifact or output. It is a step
inside the existing phases, not a new phase, command, or agent: it adds no
phase value and no artifact-presence row. It is explicitly not two things: not
normal forward progression, and not the plan-publication revision flow
(## Plan publication), which re-issues an item's own plan forward and never
moves a phase backward.
On an unshipped work item, a later phase may target an earlier phase, or — for a
nested child — its parent manifest.md. The sanctioned edges and what
each revises:
| Detecting phase | Target | Revises |
|---|---|---|
/build |
/plan |
design.md, tasks.md |
/plan |
/spec |
spec.md |
/test |
/build |
tasks.md + code (rework) |
/test |
/plan |
design.md, tasks.md |
/test |
/spec |
spec.md |
/review |
/build |
tasks.md + code (rework) |
| any child phase | parent manifest.md |
manifest.md (see ### Revising a decomposition) |
The general later→earlier rule and the table are the model; these exceptions restrict it:
- No self-target. A phase targeting itself is not a reversal and is refused.
- Shipped items are excluded. An item whose
ship.mdis present is out of scope for a backtrack; its reversal is the post-ship reopen case (below). - The target must be strictly earlier than the detecting phase.
- A parent-manifest revision is reachable only from a nested child, and only
against that child's own parent; the route that revises
manifest.mdis owned by0004-roadmap-revision. - A recorded finding precedes the edge. Every backtrack carries a committed finding before the reverse edge is taken; there is no unrecorded reversal.
- The detecting phase never edits the target artifact. It records the finding and hands control to the owner.
Control returns to the target phase, whose owning agent revises its own artifact or output. The detecting phase records its finding but never edits the target phase's artifact. The invariant "only the owning phase writes its artifact" is unchanged, and the rule that a phase may not rewrite another phase's artifact continues to hold in both directions.
Every backtrack is recorded in the item's committed, append-only
work/<item-ref>/backtracks.md record: a finding entry (detecting phase, target
phase, affected artifact or output, observable evidence, status open) and, when
the revision completes, a resolution entry. The record is not a phase artifact:
it carries no phase, is not an artifact-presence row, and does not by itself
determine the item's derived phase. Its template, and the frontmatter markers
below, are documented in docs/artifact-conventions.md.
When the reverse edge is taken, each artifact downstream of the target phase is
marked stale: <phase> in its frontmatter; nothing is deleted or silently
rewritten, and the pre-revision content stays recoverable from committed git
history. The item resumes at the target phase and re-runs forward through the
downstream phases. A stale artifact never satisfies a downstream prerequisite and
is never read as the item's current phase artifact.
When a detecting phase finds an earlier artifact wrong, it takes the reverse edge by performing five steps in order and then handing off. This procedure is the single operational statement of the edge; the agent prompts and commands reference it rather than restating it.
- Confirm the edge is sanctioned. The target is strictly earlier than the
detecting phase, the item is unshipped (
ship.mdabsent), and the edge is one the taking phase supports. A self-target, a parent-manifestedge, and a post-ship reopen are not taken here. The three/testedges (/test→/build//plan//spec) are supported: an implementation defect is the existing/test→/buildrework with no forced finding record orstale:marker, while a defect that is really a spec or design fault is routed with a recorded finding andstale:markers. - Record the finding. Append a
## Finding <n>entry towork/<item-ref>/backtracks.md, creating the record with its frontmatter (feature,record: backtracks,created,updated) when absent. The entry names the detecting phase, the target phase, the affected artifact(s), the observable evidence, andstatus: open; numbering is sequential and append-only. - Apply the
stale:markers. Mark every existing artifact strictly downstream of the target phase and not owned by the target phase withstale: <target-phase-label>. Downstream follows the artifact-presence order (spec.md<design.md<tasks.md<verify.md<review.md); the target's own artifact is revised by its owner, not marked. This is non-destructive metadata, not an authorship edit. - Never edit the target artifact. The detecting phase does not touch the file it is sending back; the target phase's owner revises it on re-entry.
- Hand off. End with the handoff for the target phase:
Next: /plan <item-ref>(from/buildor/test) orNext: /spec <item-ref>(from/planor/test).
The intra-item edges and their marker sets are concrete:
| Edge | Detecting phase | Finding entry (backtracks.md) |
Existing artifacts marked stale: |
Handoff |
|---|---|---|---|---|
/build→/plan |
builder | detecting /build; target /plan; affected design.md, tasks.md |
verify.md, review.md → stale: design |
Next: /plan <item-ref> |
/plan→/spec |
architect | detecting /plan; target /spec; affected spec.md |
design.md, tasks.md, verify.md, review.md → stale: spec |
Next: /spec <item-ref> |
/test→/plan |
tester | detecting /test; target /plan; affected design.md, tasks.md |
verify.md, review.md → stale: design |
Next: /plan <item-ref> |
/test→/spec |
tester | detecting /test; target /spec; affected spec.md |
design.md, tasks.md, verify.md, review.md → stale: spec |
Next: /spec <item-ref> |
/test→/build |
tester | none — the existing implementation-defect rework; no forced finding record | none — no forced stale: marker |
/build to fix (existing route) |
The marker's target-phase label uses the existing vocabulary: target /plan →
stale: design; target /spec → stale: spec. The /test→/build edge is the
pre-existing implementation-defect rework and forces no finding record or stale:
marker; the /test→/plan and /test→/spec edges are wired by the
reverse-phase routing. A parent-manifest revision and post-ship reopen follow
this same procedure when their items land, and their absence here is not a gap in
the model.
Every phase that owns a target reads for an open finding targeting its own phase
before doing forward work. A finding is open when backtracks.md contains an
entry whose target phase is the running phase and that has no matching
## Resolution entry.
- Open finding present. The owning agent reads the finding, revises the
affected artifact(s), appends a
## Resolution <n>entry (resolves: Finding <n>,revision: <what changed>), clears thestale:marker on any artifact it owns and has just re-run, and resumes the forward lifecycle from its phase as if the later phases had not run. The resolver is the target phase's owner; the detector never resolves its own finding. - No open finding. The phase behaves as ordinary forward
progression/revision: it records no finding and touches
backtracks.mdonly when one already exists for another phase.
Re-entry is invoked by re-running the target phase's existing command
(/build <item-ref>, /plan <item-ref>, or /spec <item-ref>); there is no
separate command.
A backtrack applies only to unshipped items. An item whose ship.md is present
is excluded: a reversal on it is the post-ship recall case, defined here and
owned by 0005-post-ship-pr-denial. The model changes neither the shipped signal
(ship.md presence) nor the Depends on/readiness contract except through the
one revocation below. The optional reopened: <phase> marker on ship.md names
the phase a recalled item re-enters and is documented in
docs/artifact-conventions.md.
/ship writes ship.md when it produces a reviewable PR, whether or not gh
was available. After that point an item is in one of:
- Pre-ship — no
ship.md. The item is anywhere fromspecthroughreview; a defect is handled by the reverse edges above, never a recall. - Shipped —
ship.mdpresent with noreopened:marker. The PR is open and awaiting human review or merged, or was never opened (for exampleghwas unavailable and only local commits exist), or its branch was abandoned. All four are the same state under the single shipped signal and stay shipped until an explicit recall; no automatic detection and no automatic revocation occurs. - Recalled / reopened —
ship.mdpresent with areopened: <phase>marker. The shipped signal is revoked: the item derives the named phase (reopened) and no longer satisfies its dependents (see "Dependencies and readiness"). It stays recalled until it re-ships.
A denied, closed, or changes-requested PR is not a state by itself: it is the triggering condition a maintainer reports when invoking a recall. The framework never reads a PR's state and never revokes a signal on its own.
The recall is a documented mode of the existing /ship command; it adds no
command, agent, skill, phase value, or state file:
/ship recall <item-ref> <phase>
<phase> ∈ { spec | design | build } # phase labels, strictly earlier than ship
spec -> re-enter /spec (record target phase `/spec`)
design -> re-enter /plan (record target phase `/plan`)
build -> re-enter /build (record target phase `/build`)
The argument is the phase label used by the reopened: and stale: markers
and by the derived-state table (spec | design | build); the recorded target
phase and the handoff use the corresponding command. The mode is refused when
work/<item-ref>/ship.md is absent (an unshipped item is an ordinary backtrack,
not a recall) and when <phase> is not strictly earlier than ship — test,
review, and ship are refused. It requires the user's explicit invocation,
which is the consent to record and commit the revocation.
The shipper performs the recall:
-
Record the finding. Append a
## Finding <n>entry towork/<item-ref>/backtracks.md, creating the record with its frontmatter (feature,record: backtracks,created,updated) when absent. The detecting phase is/ship; numbering is sequential and append-only:## Finding <n> — YYYY-MM-DD - detecting phase: `/ship` - target phase: `/build` | `/plan` | `/spec` - affected: `ship.md` (shipped signal revoked), plus the downstream artifacts marked `stale: <phase>` in step 3 - evidence: <triggering condition — PR denied / closed / changes-requested, PR never opened, or branch abandoned — and its observable evidence> - status: open
-
Revoke the shipped signal. Add the
reopened: <phase>field to the existingship.mdfrontmatter and refreshupdated; leave the recorded branch, commits, PR URL, and body intact. The marker is distinct from the frontmatterstatusand does not overload it. -
Mark the downstream artifacts
stale: <phase>. Reuse the non-destructive mechanical marking above (the detecting phase applies it; the target phase's own artifacts are revised, not marked). Whichever artifacts exist are marked; absent ones are skipped:Re-entry phase ship.mdmarkerArtifacts marked stale:build(/build)reopened: buildverify.md,review.md→stale: builddesign(/plan)reopened: designverify.md,review.md→stale: designspec(/spec)reopened: specdesign.md,tasks.md,verify.md,review.md→stale: spec -
Commit and report. Commit the revocation and the record on the item's branch (
docs(work): recall <item-ref>). When no branch or network is available the mode makes the local commits and reports the exact commands the user runs to push; recording and revoking never require the network. -
Hand off
Next: /build <item-ref>,/plan <item-ref>, or/spec <item-ref>per the phase.
A second recall (for example after a re-ship and a new denial) appends a new
finding and re-applies or updates the reopened: marker; no prior entry is
erased and the marker names the latest re-entry phase.
After the recall the item is no longer treated as shipped, so re-entry is the
### Re-entry procedure above, unchanged: the target phase's owner reads the
open finding targeting its phase, revises its own artifact(s), appends a
## Resolution <n> entry, clears the stale: markers on the artifacts it owns,
and resumes the forward lifecycle from its phase. The /plan and /spec targets
behave as 0002 wires them; the /build target reads the same open finding. The
target owner clears its own stale: markers but not the reopened: marker:
the item stays recalled, and its dependents stay blocked, until it re-ships.
Re-ship is the ordinary /ship <item-ref> work-item path. For a recalled item
(ship.md present with reopened: and a fresh review.md approve), the
shipper reuses the branch recorded in ship.md, adds commits, pushes without
force, updates the existing PR — or opens a new one when the branch was pruned —
never rebases a pushed branch, never force-pushes, never rewrites pushed history,
and never deletes or closes the denied PR. It then writes a fresh ship.md
without the reopened: marker (the shipper's own artifact), so the item
derives shipped and satisfies its dependents again. The recall stays auditable
in backtracks.md and in git history.
This section is the single authority for contesting a finding. A challenge is
a committed, append-only record that disputes a finding in review.md or a
defect reported in verify.md — the finding itself, its severity, or an
acceptance-criterion interpretation — without the challenger editing the artifact
owned by the phase that produced it. It is a step inside the existing phases, not
a new phase, command, or agent: it adds no phase value, no artifact-presence
row, and no state file. The existing severity scale
(Blocker/Major/Minor/Nit), finding format, and review verdict vocabulary
(approve/request-changes) are reused unchanged; no second severity scale,
finding grammar, or verdict is introduced.
A finding in review.md and a defect reported in verify.md are both
challengeable, including a claimed defect that is really a spec or design fault.
A challenge to a non-review finding is never silently dropped. An item with no
finding and no reported defect has nothing to challenge: no challenge record is
created and nothing blocks.
The challenger appends a ## Challenge <n> entry to the item's committed,
append-only work/<item-ref>/challenges.md record naming the challenged finding,
the challenge type (finding | severity | acceptance-criterion), the evidence,
and the rationale. Raising happens on the existing phase surface — a challenge to
a review.md finding or a verify.md defect is raised from the author's current
phase prompt — so no command is added. The challenger never edits the artifact
owned by the phase that produced the finding; it records state only. A challenge
hands control to the producing phase: Next: /review <item-ref> for a review
finding, Next: /test <item-ref> for a verify defect.
The record is not a phase artifact: it carries no phase, and by itself it does
not determine the item's derived phase (it determines the blocked
condition below). Its entry shapes are documented in
docs/artifact-conventions.md.
While challenges.md holds any challenge with no matching Response or
Withdrawal, the item is challenged: it does not advance to its next forward
phase, including /ship, and the open challenge escalates to the user. The
condition is the record's open entry plus the derived label challenged; it adds
no phase value, and its precedence in derived state is stated below. Nothing
resolves on its own: an open challenge that is never adjudicated or withdrawn
keeps the item challenged and keeps the escalation in place.
The phase that produced the challenged finding re-evaluates it first on
re-entry; if it cannot resolve the challenge, it escalates to the user on the
existing question surface, and the user decides. The challenger is never the
adjudicator of its own challenge, and no adjudication completes without a
Response entry recording the decision, its basis, and who made it
(adjudicator: the producing phase command, or user). A challenge may be
withdrawn by its author; the withdrawal is appended, the item is unblocked, and
the prior entry is preserved. A later decision that supersedes an earlier one is
appended as a Reversal entry; it never rewrites the prior entry.
A sustained challenge overturns the challenged finding or adjusts its
severity and, for a review finding, the review verdict is recomputed — approve
if and only if no finding of any severity (Blocker, Major, Minor, or Nit)
remains. A finding adjusted to Minor or Nit still blocks; only an overturn
removes it. The outcome and its evidence are
recorded, and the next command follows the recomputed verdict (/ship if
approve, /build if request-changes). If a sustained challenge shows the
acceptance criterion itself is wrong rather than the finding being mistaken, the
correction is routed upstream through ## Phase reversal (backtracking) —
revising spec.md or design.md by their owners — rather than written into
review.md. A rejected challenge leaves the challenged finding and the
verdict unchanged, records the rejection and its rationale, and resumes normal
routing — for a blocking review finding, the existing review.md verdict
request-changes → /build route.
The challenger/detector records state and never edits the artifact owned by the producing phase; the producing phase's owner revises its own artifact and appends the outcome. The invariant "only the owning phase writes its artifact" is unchanged, and the rule that a phase may not rewrite another phase's artifact continues to hold in both directions.
work/<item-ref>/challenges.md is committed, append-only, and numbered from 1;
entries are never edited, reordered, or removed. Its entry kinds (Challenge,
Response, Withdrawal, Reversal), fields, and the malformed out-of-order
rule are documented in docs/artifact-conventions.md. A malformed record — for
example a Response/Withdrawal/Reversal recorded before its Challenge — is
reported rather than reordered. An absent challenges.md means nothing is
challenged, and historical items need no migration.
There is no state file. /status derives each item's phase from artifacts and
content:
A node that has children derives its phase from its own phase artifacts
first: a node carrying its own phase artifacts derives its phase from them, and
only a node with a manifest (manifest.md, or the legacy roadmap.md alias)
and no own phase artifacts derives the container phase manifest. Own
artifacts take precedence over the manifest at every depth. This composes with the shipped precedence below — the structural
stale: and reopened: rows, then the unshipped open-challenge blocked overlay,
then the artifact-presence and verdict rows — and introduces no new phase value
and no second state model.
| Observed state | Phase |
|---|---|
earliest stale: marker among the item's artifacts names phase P |
P (backtracked) |
ship.md present with a reopened: marker naming phase P |
P (reopened) |
an open challenge in challenges.md (no matching Response/Withdrawal) on an item that has no ship.md |
challenged (blocked) |
a manifest present (manifest.md, or legacy roadmap.md) and no own phase artifacts (check before spec.md) |
manifest |
spec.md missing |
not started |
spec.md present, design.md missing |
spec |
design.md present, tasks.md missing |
design |
tasks.md present, some boxes unchecked |
build |
all boxes checked, verify.md missing |
test |
verify.md present, review.md missing |
review |
ship.md present |
shipped |
review.md verdict request-changes |
build (rework) |
review.md verdict approve, no ship.md |
ship |
visual.md present (optional; does not change the phase) |
review |
The first two rows are evaluated before the artifact-presence rows below
them. An earliest stale: marker names the backtrack target phase P; the item
derives P (backtracked), and the marked artifact is never read as the item's
current phase artifact nor counted as a satisfied downstream prerequisite. The
stale token manifest — used only for a phase→parent-manifest backtrack —
maps to spec when the item's phase is derived; a legacy stale: roadmap is
accepted as the deprecated equivalent of stale: manifest and maps to spec the
same way. A reopened: marker on
ship.md derives the named phase P (reopened); it is the one condition that
invalidates the ship.md presence row, and readiness consumes the same
revocation: a ship.md carrying a reopened: marker does not satisfy a
dependent until the item re-ships (see ### Shipped items and reopen). The
review.md verdict
request-changes → build (rework) row below is the pre-existing, already
rendered instance of the general backtrack model
(## Phase reversal (backtracking)): it is retained verbatim, defines no second
rework mechanism, and its routing literal and consumers are unchanged.
The challenged condition is a blocked overlay that applies to unshipped
items only; it is evaluated after the stale:/reopened: structural
derivations and before the forward-action and ship.md/verdict rows, so an
unshipped item with an open challenge is never read as ready to advance or ship
even when its artifact-presence and verdict rows would otherwise derive a forward
phase. While an unshipped item's challenges.md holds a Challenge <n> with no
matching Response n/Withdrawal n, the item derives challenged (blocked), the
next forward phase — including /ship — refuses and reports the open challenge(s),
and the open challenge escalates to the user; the condition reuses the 0001
marker model (the record's open entry is the marker, plus the derived label) and
introduces no new phase value. The unshipped precondition is what keeps the
overlay from masking a completed item: a ship.md-present item derives shipped
(or P (reopened) when it also carries a reopened: marker) and its open
challenge is out-of-scope for a challenge, because post-ship reversal is
recall's domain (0005, ### Shipped items and reopen); therefore no shipped
item is ever derived challenged. Its raising, adjudication, and outcome are
defined once in ## Findings challenge and adjudication.
/status reports the Phase column by mirroring this table's Phase cell — the
base phases plus P (backtracked), P (reopened), build (rework), and
challenged (blocked) — and adds a per-item Notes line for the derived states.
For a backtracked item the Notes name the target phase being revised and the
affected upstream artifact from the open backtracks.md finding, and list the
downstream artifacts the backtrack invalidated (the stale:-marked ones), or
none when no downstream artifact exists; for a reopened item they name the
recall and the re-entered phase; for an unshipped challenged item they name the
open challenge(s); and for a shipped item carrying an open challenge they surface
that challenge as out-of-scope. The report reuses this table and the readiness
authority as its single sources and restates neither.
A directory containing a manifest (manifest.md, or the legacy roadmap.md
alias) is a decomposing node at any depth — a pure container, or a hybrid
node that also carries its own artifacts — and, when it has a manifest and no own
phase artifacts, is derived as manifest before the single-feature rows; a
decomposing node with its own phase artifacts derives its own phase first,
independently of its children. A decomposing node's child is derived like any
other item: a child holding only its .gitkeep has no phase artifacts and is
not started.
/fix is the lightweight track for a small defect whose correct behavior is
already clear. It has no work item: no spec.md, design.md, tasks.md,
verify.md, or review.md, and no directory under work/. It consumes no
sequence number, so a fix cannot collide with or renumber a work item even when
both are in flight.
- Reproduce the defect with a failing test or an exact repro, find the root
cause, make the smallest change, and add a regression test that fails without
the change and passes with it. Fixes never introduce new behavior; a change
that needs new behavior routes to
/spec. - Run test, lint, and typecheck for the touched scope. A failed check, or a defect that could not be reproduced, blocks landing — the closing handoff omits the landing step and reports the failure as the blocker instead.
- Only the
shipperperforms git writes, and only on the user's explicit request. The builder and every other non-shipper agent never commit, push, or open a PR. - A verified fix lands through
/ship fix(with an optional short description). The shipper stages only the fix's files, creates afix/<short-description>branch, commits the change and its regression test as conventional commits, and opens a PR whose description carries the reproduction, root cause, change, and check results. - A landed fix creates no work item and no shipped-state record.
ship.mdremains the sole shipped signal for lifecycle work items, so a fix has no/statusphase and never appears underwork/.
/fix <bug>— for defects where the desired behavior is already clear and the change is small. Skips spec/design. Still: reproduce, fix, test, then report; a verified fix lands through/ship fixon the user's explicit request (see "The fix track"). Fixes never introduce new behavior.- Full lifecycle — new features, behavior changes, cross-cutting work, anything touching public interfaces, data, or security.
/ship recall <item-ref> <phase>— recall a shipped item whose PR was denied, closed, or sent back for changes (or whose PR was never opened, or whose branch was abandoned): record the finding, revoke the shipped signal without deleting the historicalship.md, and re-enterspec/design/build. Explicit and maintainer-invoked; see### Shipped items and reopen./plan <item-ref or initiative>— the planning step (/plan), which decomposes a broad, multi-feature initiative into a parent node and nested child work items, or revises an existing decomposition in place, sequencing them and recording sibling dependencies. Use before/specwhen a request spans several interdependent features; a single, self-contained feature still goes straight to/spec./status— when unsure where things stand./conflicts [item-ref]— to compare theconflicts-withdeclarations of unshipped plans before development and report overlaps in the shipped conflict classes. Advisory, offline, and read-only; it blocks no phase and names each counterpart for a focused item. See## Declared-conflict check./doctor— a framework-maintainer only, read-only consistency check of the framework's documented inventories, counts, permission blocks, and ignore rules. It reports drift and never edits; safe to run at any time, including before release./visual [url or item-ref]— to inspect a running user-facing frontend in a real browser for layout, interaction, responsiveness, and accessibility. Use for UI-bearing work; harmless to skip for backend-only work./bootstrap— once, when adopting the framework into a repository.
When a task's nature is ambiguous, ask the user which track to use.
Items are independent directories and may proceed in parallel. Keep them on
separate branches. tasks.md check boxes are per-item; never mix items in one
commit unless the user asks.
Artifacts are committed, so they travel with the branch: a work item's
work/<item-ref>/ directory merges alongside its code, and the artifact paths
linked from its pull request resolve for a reviewer who does not share the
author's working tree.
Because two branches can each allocate the same next number, a merge can leave
two items sharing a canonical reference without a filesystem conflict. Duplicate
4-digit prefixes are checked at every node of the recursive tree, not only
the top level plus one child layer; each decomposing node at any depth is checked
against its own immediate children, independently, and the top level is
checked against the top-level items. No node's check depends on another's, and
identity is the full canonical path, not the trailing segment — two nodes whose
local id text matches but whose parents differ are not a duplicate.
The two shipped codes name the two tree positions; a collision is reported under exactly one of them:
DUPLICATE-PREFIX— two top-levelNNNNprefixes are equal.DUPLICATE-CHILD— two same-parentMMMMprefixes are equal, at any depth.
A top-level collision is never reported as DUPLICATE-CHILD, and a deeper
collision is never reported as DUPLICATE-PREFIX. A DUPLICATE-CHILD finding
names both offenders by their full canonical reference; when the collision
is at depth ≥ 3 — for example two grandchildren, <parent>/<MMMM-slug>-a and
<parent>/<MMMM-slug>-b — it also names the shared parent and shared local
prefix in the detail, so the finding is unambiguous at any depth. Findings use
the class (c) label and the finding grammar of ## Merge conflicts; this rule
introduces no new code, class, or vocabulary.
On a collision, follow "Renumbering after a parallel merge" in
docs/artifact-conventions.md: renumber the unshipped item to the next number
from the allocation contract and update every reference in the same change
before it ships. A number is never reused. Duplicate sequence numbers are only
one class of conflict; for every other class — shared-surface textual conflicts,
work/ artifact conflicts, and derived-agreement drift — see
## Merge conflicts.
This section is the normative merge-conflict contract: it classifies the conflict
classes this workflow can encounter and states the sanctioned handling for each.
The merge-conflict skill restates the operational procedure; on any
disagreement, this section is the source of truth.
| Class | Conflict | Affected surface(s) |
|---|---|---|
| (a) shared-surface textual conflict | Both branches edit the same framework file, so Git emits conflict markers. | README.md, AGENTS.md, docs/*.md, .opencode/{agent,command,skill}/**, template/**, tests/checks/** |
(b) work/ artifact conflict |
Both branches edit the same committed work artifact. | Decomposition Children / Depends on tables, artifact frontmatter, nested-child intersections |
| (c) duplicate sequence number | Both branches allocate the same top-level NNNN, or the same local MMMM under the same parent at any node of the tree. |
Top-level work/<NNNN-slug>; and, for any decomposing node at any depth, its immediate children work/<node-ref>/<MMMM-slug> |
| (d) derived-agreement drift | A duplicated inventory or count fact merges cleanly but the two sides now disagree. | README Layout counts, README Skills table |
Reconciliation happens at two points in the lifecycle: a pre-ship reconcile
before the shipper performs any ship operation, and a post-merge integrity
pass after a merge to the default branch. The pre-ship reconcile merges the
default branch forward into the item branch and resolves any conflicts, so a
ship is never built on a stale merge base. The post-merge integrity pass is a
documented, shipper-owned checklist run on the merged tree: re-scan
every node of the recursive tree for duplicate 4-digit prefixes — the top
level against the top-level items, and each decomposing node at any depth
against its own immediate children, independently — re-check every Depends on
against its own node's Children table for dangling, missing, unlisted, or
cyclic references, and re-run the repository's own configured test command — the
Project profile Test: value — to surface derived-agreement drift.
When the branch is already up to date, both steps are no-ops and do not error.
The shipper is the single owner of reconciliation. Reconciliation is a step inside /ship and adds no new command or agent.
- Merge the default branch forward. Bring the default branch into the item branch with a merge. Never rebase a pushed branch and never force-push; the shipper's guardrails forbid both.
- Preserve both branches' intent. Keep both sides' records and changes rather than dropping one. A conflict whose resolution requires a judgment about competing intents is a semantic conflict: never silently accept it, and escalate it to the user for explicit approval before proceeding.
- Auto-resolve the mechanical. A conflict whose resolution is mechanical or structural — one that does not require choosing between competing intents — may be resolved automatically, subject to the re-verification below.
After any resolution, re-run the repository's own configured test command — the
Project profile Test: value — and the affected item's checks.
Both must be green before the resolved merge is recorded or shipped. A clean
merge is not evidence of correctness: even a merge that produced no conflict
markers still runs the suite, because derived-agreement drift is surfaced only
there.
The invariants this merge-conflict contract protects are stated once here. The guard is prompt behavior only: there is no committed checker, script, helper, or executable tool for it and no tests/ agreement area, and no separate checker exists. It runs at two prompt-only enforcement points and adds no separate guard:
/statusreports the invariants at every node, offline and read-only on demand, as the guard's read-only window.- The
/shippost-merge integrity pass runs at every node and after a merge to the default branch, as theshipper-owned checklist described under "Lifecycle placement".
The guard observes the same invariant set at both points, and neither defines a separate guard.
The invariant set is evaluated at every node of the recursive tree: the top level against the top-level items, and each decomposing node at any depth against its own immediate children, independently — no node's check depends on another's. Both enforcement points below observe the full invariant set at every node. The duplicate-sequence rows agree with the rule stated once in docs/workflow.md → "Multiple work items" and restate no competing duplicate rule.
The invariant set, with the finding code that names each invariant and the class it belongs to:
| Invariant | Code | Class |
|---|---|---|
no two top-level work/ items share a 4-digit NNNN prefix |
DUPLICATE-PREFIX |
(c) |
no two same-parent children of one decomposing node share a 4-digit MMMM, at any depth |
DUPLICATE-CHILD |
(c) |
every Depends on resolves to an existing Children row and child directory |
DANGLING-DEP |
(b) |
| the stored dependency graph is acyclic | CYCLIC-DEP |
(b) |
no child is missing — a Children row whose child directory is absent |
MISSING-CHILD |
(b) |
no child is unlisted — a child directory absent from the Children table |
UNLISTED-CHILD |
(b) |
| the duplicated inventory/count facts agree with disk | DRIFT-FACT |
(d) |
a node holds at most one manifest — no node holds both manifest.md and a redundant roadmap.md, and a manifest's spelling agrees with its phase value |
DRIFT-FACT |
(d) |
Findings use the merge-conflict skill's ### Finding grammar
(- [<CODE>] (<class>) <offender canonical reference(s)> — <detail>); this
subsection defines no second vocabulary and no second policy. Every
finding names its code, its class, and the offending canonical reference, and is
non-fatal: the list is never silently dropped and never truncated,
findings are not auto-repaired, and the guard modifies no file. An empty
work/ tree and an already-up-to-date branch with no collision are no-ops that
report no findings and does not error.
The guard's verification is portable: it names the repository's own
configured test command — the Project profile Test: value in
AGENTS.md — rather than a maintainer-only path, so an adopter runs their own
suite. The duplicated-inventory/count-fact invariant is nevertheless
observable offline through /status, without that command.
Duplicate sequence numbers — class (c) — are handled at any depth by
"Renumbering after a parallel merge" in docs/artifact-conventions.md. Those
steps apply identically to a collision at any node of the tree; this section
defers to and extends that rule and never defines a second renumbering rule.
Because state is file-based, resuming is just re-reading work/<item-ref>/. If a
session ends mid-phase, the next session reads the artifacts and continues. If
an artifact is stale relative to the code, reconcile before proceeding and note
what changed.
- A failed
/buildor/testleaves artifacts and code in place. Report the failure; do not paper over it. - Never rewrite another phase's artifact to hide a failure. If an upstream
artifact is wrong, say so and take the sanctioned reverse transition in
## Phase reversal (backtracking): record the finding and hand control to the phase that owns the wrong artifact. - Destructive git recovery (reset, revert, restore) requires user confirmation.