pincer-workflow 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +106 -19
  2. package/bin/pincer.js +17 -1
  3. package/package.json +3 -3
  4. package/template/.agents/skills/pincer-code/SKILL.md +88 -14
  5. package/template/.agents/skills/pincer-evaluate/SKILL.md +42 -13
  6. package/template/.agents/skills/pincer-narrow/SKILL.md +53 -12
  7. package/template/.agents/skills/pincer-plan/SKILL.md +12 -4
  8. package/template/.agents/skills/pincer-release/SKILL.md +18 -0
  9. package/template/.agents/skills/pincer-status/SKILL.md +29 -5
  10. package/template/.claude/commands/pincer-code.md +88 -14
  11. package/template/.claude/commands/pincer-evaluate.md +42 -13
  12. package/template/.claude/commands/pincer-narrow.md +53 -12
  13. package/template/.claude/commands/pincer-plan.md +12 -4
  14. package/template/.claude/commands/pincer-release.md +18 -0
  15. package/template/.claude/commands/pincer-status.md +29 -5
  16. package/template/.claude/hooks/hook-policy.cjs +13 -6
  17. package/template/.claude/references/prd-template.md +11 -4
  18. package/template/.codex/README.md +1 -1
  19. package/template/.github/prompts/pincer-code.prompt.md +88 -14
  20. package/template/.github/prompts/pincer-evaluate.prompt.md +42 -13
  21. package/template/.github/prompts/pincer-narrow.prompt.md +53 -12
  22. package/template/.github/prompts/pincer-plan.prompt.md +12 -4
  23. package/template/.github/prompts/pincer-release.prompt.md +18 -0
  24. package/template/.github/prompts/pincer-status.prompt.md +29 -5
  25. package/template/AGENTS.md +17 -1
  26. package/template/docs/dry-run-checklist.md +30 -3
  27. package/template/docs/release-checklist.md +3 -1
  28. package/template/docs/runtime-contracts.md +1428 -96
  29. package/template/scripts/pincer-evidence.cjs +9 -7
  30. package/template/scripts/pincer-runtime/adopt.cjs +132 -0
  31. package/template/scripts/pincer-runtime/agreement.cjs +240 -0
  32. package/template/scripts/pincer-runtime/authorization.cjs +167 -0
  33. package/template/scripts/pincer-runtime/changes.cjs +517 -0
  34. package/template/scripts/pincer-runtime/checks.cjs +48 -0
  35. package/template/scripts/pincer-runtime/coverage.cjs +361 -0
  36. package/template/scripts/pincer-runtime/dispositions.cjs +95 -0
  37. package/template/scripts/pincer-runtime/evidence.cjs +303 -18
  38. package/template/scripts/pincer-runtime/gates.cjs +73 -0
  39. package/template/scripts/pincer-runtime/identity.cjs +21 -4
  40. package/template/scripts/pincer-runtime/impact.cjs +177 -0
  41. package/template/scripts/pincer-runtime/io.cjs +41 -0
  42. package/template/scripts/pincer-runtime/lifecycle.cjs +34 -12
  43. package/template/scripts/pincer-runtime/locator.cjs +158 -0
  44. package/template/scripts/pincer-runtime/migrate.cjs +140 -63
  45. package/template/scripts/pincer-runtime/parse.cjs +20 -1
  46. package/template/scripts/pincer-runtime/phases.cjs +245 -0
  47. package/template/scripts/pincer-runtime/readiness.cjs +15 -2
  48. package/template/scripts/pincer-runtime/requirements.cjs +255 -0
  49. package/template/scripts/pincer-runtime/resume.cjs +273 -0
  50. package/template/scripts/pincer-runtime/routing.cjs +54 -0
  51. package/template/scripts/pincer-runtime/runner.cjs +24 -6
  52. package/template/scripts/pincer-runtime/scaffold.cjs +254 -0
  53. package/template/scripts/pincer-runtime/state.cjs +29 -7
  54. package/template/scripts/pincer-runtime/status.cjs +178 -22
  55. package/template/scripts/pincer-runtime/transaction.cjs +200 -0
  56. package/template/scripts/pincer-runtime/transitions.cjs +134 -0
  57. package/template/scripts/pincer-runtime.cjs +412 -76
  58. package/template/scripts/pincer-status.sh +1 -1
  59. package/template/scripts/pincer-ticket.sh +1 -1
@@ -1,76 +1,121 @@
1
1
  # PINCER Runtime Contracts
2
2
 
3
3
  The runtime is `scripts/pincer-runtime.cjs` with its modules under
4
- `scripts/pincer-runtime/`. It is dependency-free CommonJS for Node.js 18+ and is the
5
- only writer of ticket lifecycle state, verification attempts, change bindings and
6
- exported candidate evidence. The shell entry points `scripts/pincer-ticket.sh` and
4
+ `scripts/pincer-runtime/`. It is dependency-free CommonJS for Node.js 22+ and is the
5
+ only writer of ticket lifecycle state, verification attempts, change records and
6
+ their lifecycle, the local selection, evaluation locators and exported candidate
7
+ evidence. The shell entry points `scripts/pincer-ticket.sh` and
7
8
  `scripts/pincer-status.sh` are compatibility wrappers that delegate to it. This
8
9
  document is the contract the runtime implements; tests pin it, and a change to a
9
10
  contract updates this file in the same commit as the code.
10
11
 
11
- Schema numbers in this document (`schema: 1` on bindings, attempts, manifests and
12
- status; evidence `schema: 2`) are independent of the package version. The runtime
13
- records its contract version as `runtime: 1`.
12
+ Schema numbers in this document are independent of the package version. The v0.5.0
13
+ records are `schema: 1` (bindings, attempts, manifests, index, status) with evidence
14
+ `schema: 2`; the runtime recorded its contract version as `runtime: 1`. The change
15
+ lifecycle introduced by PRD v5 uses change records `schema: 2`, attempts `schema: 2`,
16
+ status JSON `schema: 2`, and the runtime records its contract version as `runtime: 2`.
17
+ Selection, agreement snapshot, evaluation locator, transaction manifest and resume
18
+ JSON records start at `schema: 1`. A record whose schema this runtime does not read is
19
+ refused as `UNSUPPORTED_SCHEMA`; an older runtime refuses the newer schemas the same
20
+ way and never interprets several change records through its single-binding logic.
21
+ Strict coverage (PRD v6, "Strict coverage") adds change records `schema: 3`, attempts
22
+ `schema: 3`, evidence `schema: 3`, status JSON `schema: 3`, resume JSON `schema: 2`,
23
+ agreement projection version 2 with snapshots `schema: 2`, the coverage map `schema: 1`
24
+ and the runtime contract version `runtime: 3`; a v0.5.0 or PRD v5 runtime refuses every
25
+ one of them, and this runtime keeps reading the older schemas unchanged.
14
26
 
15
27
  ## Modes
16
28
 
17
- A project is in one of two modes per PRD, decided by the presence of a change binding
18
- (`.prd/changes/<change-id>.json`, see below) whose `prd` names that PRD.
29
+ A project is in exactly one of three modes, decided by the files under
30
+ `.prd/changes/`. Nothing switches modes implicitly; `migrate --apply` is the only
31
+ transition (legacy → changes, migrated → changes).
19
32
 
20
- | | Legacy (no binding) | Migrated (binding present) |
21
- | --- | --- | --- |
22
- | How recognized | no `.prd/changes/*.json` names the PRD | exactly one binding names it |
23
- | Selected PRD for status | the highest-numbered valid `.prd/prd-vN.md` | the binding's PRD; a newer unregistered PRD is reported, never selected |
24
- | `start` | as v0.4.1: dependency and readiness checks, writes `status: in_progress` and `started` | same checks; a dependency is ready when its latest attempt passed against current inputs |
25
- | `verify` | runs the block, writes `last_check` and `verified` into the ticket | runs the block as an attempt under `.pincer/runtime/`; writes nothing to tracked files |
26
- | `done` | re-runs the check, then writes `status: done` and `finished` | consumes the latest passing attempt whose inputs are current; writes `status: done` and `finished` once; read-only and idempotent afterwards |
27
- | Readiness authority | `last_check`/`verified` receipts in the ticket | attempt records; `verified`/`last_check` are ignored and reported as `LEGACY_RECEIPT` |
28
- | Local state | none; `.pincer/` is never written | `.pincer/runtime/` (ignored) |
29
- | Evidence | schema 1, authored | schema 2, exported from attempts (schema 1 still validates with a legacy label) |
30
- | Status line | `Runtime legacy · no change binding · migrate with node scripts/pincer-runtime.cjs migrate --preview --prd <prd>` | `Runtime change <id> · revision <12 hex> · base <short sha>` |
31
-
32
- Legacy mode is recognizable in every output; nothing switches modes implicitly.
33
- Migration (`migrate --apply`) is the only transition.
33
+ | | Legacy (no record) | Migrated (one schema 1 binding) | Changes (schema 2 records) |
34
+ | --- | --- | --- | --- |
35
+ | How recognized | no `.prd/changes/*.json` | exactly one `.prd/changes/<id>.json` with `schema: 1` | every `.prd/changes/*.json` has `schema: 2` |
36
+ | Selected PRD for status | the highest-numbered valid `.prd/prd-vN.md` | the binding's PRD; a newer unregistered PRD is reported, never selected | the PRD of the locally selected change; without a selection, none is selected |
37
+ | `start` | as v0.4.1: dependency and readiness checks, writes `status: in_progress` and `started` | same checks; a dependency is ready when its latest attempt passed against current inputs | additionally requires the ticket's change to be selected, `active` and authorized (see "Command gates") |
38
+ | `verify` | runs the block, writes `last_check` and `verified` into the ticket | runs the block as an attempt under `.pincer/runtime/`; writes nothing to tracked files | as migrated, for a selected, authorized `active` or `completed` change; the attempt records the agreement |
39
+ | `done` | re-runs the check, then writes `status: done` and `finished` | consumes the latest passing attempt whose inputs are current; writes `status: done` and `finished` once; read-only and idempotent afterwards | as migrated, gated like `start` |
40
+ | Readiness authority | `last_check`/`verified` receipts in the ticket | attempt records; `verified`/`last_check` are ignored and reported as `LEGACY_RECEIPT` | schema 2 attempt records of the same change; a schema 1 attempt is `HISTORICAL_EVIDENCE` |
41
+ | Local state | none; `.pincer/` is never written | `.pincer/runtime/` (ignored) | `.pincer/runtime/` plus `selection.json` and transaction journal |
42
+ | Evidence | schema 1, authored | schema 2, exported from attempts (schema 1 still validates with a legacy label) | schema 2 plus the per-change evaluation locator |
43
+ | Candidate locator | `NOTES.md` | `NOTES.md` | `.prd/evidence/changes/<id>.json`; `NOTES.md` is a compatibility summary |
44
+ | Status line | `Runtime legacy · no change binding · migrate with node scripts/pincer-runtime.cjs migrate --preview --prd <prd>` | `Runtime change <id> · revision <12 hex> · base <short sha>` | `Runtime changes · selected <id> · <state> · agreement <12 hex> · authorization <current \| code> · base <short sha>`, or `Runtime changes · no selection · change select <id>` |
45
+
46
+ Migrated mode keeps the v0.5.0 contract word for word; the v5 sections below apply to
47
+ changes mode only. A `.prd/changes/` directory that mixes schema 1 and schema 2
48
+ files, or holds a file this runtime cannot read, is `INPUT_INVALID` for every command
49
+ except `migrate --preview` (which names the conflict) and `recover`; an unreadable
50
+ schema 2 record never makes the project legacy or migrated.
34
51
 
35
52
  ## Commands and exit codes
36
53
 
37
54
  All commands: `node scripts/pincer-runtime.cjs <command> [arguments]`. The project
38
55
  root is `CLAUDE_PROJECT_DIR`, else `git rev-parse --show-toplevel`, else the working
39
56
  directory. Diagnostics go to stderr, prefixed `pincer-ticket: ` for ticket input,
40
- `pincer: ` for PRD, NOTES and runtime state, `evidence: ` for manifests.
57
+ `pincer: ` for PRD, NOTES, change and runtime state, `evidence: ` for manifests.
41
58
 
42
59
  | Command | Arguments | Writes | Notes |
43
60
  | --- | --- | --- | --- |
44
61
  | `validate` | `<file>... [--digests]` | nothing | validates tickets, PRDs and NOTES; `--digests` prints `ticket`, `check` and `prd` digests |
45
- | `register` | `--prd .prd/prd-vN.md [--change <id>] [--authorization <text>] [--replace] [--rebind]` | `.prd/changes/<id>.json` | requires a git HEAD; `--replace` allows a binding for another PRD to be replaced; `--rebind` updates `prd_revision` after PRD content changed |
62
+ | `register` | `--prd .prd/prd-vN.md [--change <id>] [--authorization <text>] [--replace] [--rebind]` | legacy/changes: `.prd/changes/<id>.json` (schema 2), `.gitignore`; migrated: as v0.5.0 | changes mode: a new `planned` record; `--replace`, `--rebind` and `--authorization` are refused with the replacing command named (`change select`/`supersede`, `change revise`, `change authorize`); migrated mode: `MIGRATION_REQUIRED` for a second PRD |
46
63
  | `snapshot` | `[--json] [--store]` | `.pincer/runtime/manifests/<digest>.json` only with `--store` | prints the source digest and file count, or the manifest |
47
- | `status` | `[--json]` | nothing | human report, or one status JSON object on stdout |
48
- | `ready` | `[T-NN]` | nothing | read-only gate: exit 0 when ready, 1 when not, with reason codes |
49
- | `start` | `T-NN` | ticket `status`, `started`, `prd` | both modes |
50
- | `verify` | `T-NN` | legacy: ticket receipts; migrated: an attempt | always creates a new attempt in migrated mode |
51
- | `done` | `T-NN` | ticket `status`, `finished` (once) | legacy re-runs the check; migrated consumes the current pass |
64
+ | `status` | `[--json] [--change <id>]` | nothing | human report, or one status JSON object on stdout; `--change` inspects another record without selecting it |
65
+ | `ready` | `[T-NN] [--change <id>]` | nothing | read-only gate: exit 0 when ready, 1 when not, with reason codes; without a ticket, the release gate of the selected change |
66
+ | `start` | `T-NN` | ticket `status`, `started`, `prd` | all modes; changes mode gated |
67
+ | `verify` | `T-NN` | legacy: ticket receipts; migrated/changes: an attempt | always creates a new attempt outside legacy mode |
68
+ | `done` | `T-NN` | ticket `status`, `finished` (once) | legacy re-runs the check; migrated/changes consume the current pass |
52
69
  | `bind` | `T-NN .prd/prd-vN.md` | ticket `prd` | legacy association repair |
53
- | `recover` | | attempts, lock, journal | finalizes dead-owner `running` attempts as `interrupted`; never promotes to `passed` |
54
- | `migrate` | `--preview | --apply --prd .prd/prd-vN.md [--change <id>] [--authorization <text>]` | apply: backups, ticket receipt lines, `.gitignore`, binding | preview writes nothing |
55
- | `check` | `C-NN --candidate <sha> [--timeout <seconds>] -- <command...>` | an attempt | candidate-context check on a clean view of the committed candidate |
56
- | `evidence export` | `--candidate <sha> --base <sha> --prd .prd/prd-vN.md --draft <file>` | `.prd/evidence/prd-vN/<candidate>/` | schema 2 manifest and logs populated from attempts |
70
+ | `recover` | | attempts, lock, journal, incomplete transactions | finalizes dead-owner `running` attempts as `interrupted`; completes committed transactions and discards uncommitted staging; never promotes to `passed`, never writes a lifecycle event |
71
+ | `migrate` | `--preview \| --apply --prd .prd/prd-vN.md [--change <id>] [--authorization <text>]` | apply: backups, ticket receipt lines, `.gitignore`, the schema 2 record, `selection.json`, index pointers | preview writes nothing; from legacy or from a schema 1 binding |
72
+ | `check` | `C-NN --candidate <sha> [--timeout <seconds>] -- <command...>` | an attempt | candidate-context check on a clean view of the committed candidate; changes mode: selected `completed` change with current authorization |
73
+ | `evidence export` | `--candidate <sha> --base <sha> --prd .prd/prd-vN.md --draft <file>` | `.prd/evidence/prd-vN/<candidate>/`; changes mode also `.prd/evidence/changes/<id>.json` | schema 2 manifest and logs populated from attempts |
74
+ | `change list` | `[--json]` | nothing | every retained record with lifecycle state, selection mark and authorization verdict |
75
+ | `change show` | `<id> [--json]` | nothing | one record: agreements, authorizations, decisions, events, evaluations, the structural difference between the authorized and current agreement; never changes the selection |
76
+ | `change select` | `<id>` | `.pincer/runtime/selection.json` | local pointer only; refuses an unknown or unreadable record; never touches HEAD, the index or files |
77
+ | `change activate` | `<id>` | the record (event) | `planned` → `active`; preconditions in "Lifecycle" |
78
+ | `change pause` | `<id> --reason <text> [--note <text>]` | the record (event) | `active` → `paused`; `--note` is the handoff note |
79
+ | `change resume` | `<id>` | the record (event) | `paused` → `active`; the lifecycle operation, distinct from the `resume` report |
80
+ | `change complete` | `<id>` | the record (event) | `active` → `completed`; requires every ticket done and ready, no open decision, current authorization |
81
+ | `change reopen` | `<id> --reason <text>` | the record (event) | `completed` → `active`; history retained |
82
+ | `change cancel` | `<id> --decision D-NN --reason <text>` | the record (event) | `planned`/`active`/`paused` → `cancelled` |
83
+ | `change supersede` | `<id> --with <replacement id> --decision D-NN` | the record (event) | `planned`/`active`/`paused`/`completed` → `superseded`; the replacement must exist and must not be superseded by `<id>` transitively |
84
+ | `change revise` | `<id>` | the record (agreement entry, event) and `.prd/changes/<id>/agreements/G-NN.json` | records the current agreement when it differs from the latest recorded one; no-op otherwise; authorizes nothing |
85
+ | `change authorize` | `<id> --agreement <digest> --reference <text> --excerpt <text> [--constraints <text>] [--decision D-NN]...` | the record (authorization, event) and the agreement snapshot when not yet recorded | the "user" disposition; `<digest>` must equal the agreement computed now |
86
+ | `change authorize` | `<id> --agreement <digest> --delegated --basis A-NN --explanation <text> [--decision D-NN]...` | as above | the "delegated" disposition; `A-NN` must be an existing authorization of the same change |
87
+ | `change decide` | `<id> --summary <text> [--id D-NN]` | the record (decision, event) | raises an open consequential decision; blocks execution until resolved |
88
+ | `change decide` | `<id> --resolve D-NN --reference <text> --excerpt <text>` | the record (decision, event) | records the user's decision; the agreement digest changes and needs authorization |
89
+ | `resume` | `[--change <id>] [--json]` | nothing | the read-only resume report ("Resume report"); `--change` inspects without selecting |
90
+ | `coverage` | `[--change <id>] [--json]` | nothing | the phase-specific coverage report ("Strict coverage"); read-only |
91
+ | `impact` | `[--change <id>] [--from G-NN \| A-NN] [--json]` | nothing | structural differences against a retained agreement ("Strict coverage"); read-only |
92
+ | `coverage adopt` | `--preview \| --apply --change <id> [--agreement <digest>]` | apply: a backup, the schema 3 record, the adoption snapshot | the explicit entry into strict coverage ("Adoption and rollback"); authorizes nothing |
57
93
 
58
94
  Exit codes:
59
95
 
60
96
  | Code | Meaning |
61
97
  | --- | --- |
62
- | 0 | success; for `status`, inspection succeeded even when work is not ready |
98
+ | 0 | success; for `status`, `resume`, `change list` and `change show`, inspection succeeded even when work is not ready |
63
99
  | 1 | the check failed, the readiness gate is not ready, or a transition was refused |
64
100
  | 2 | usage error (unknown command or option, missing argument) |
65
101
  | 3 | state busy: the lock is held by a live owner after the bounded wait |
66
- | 4 | invalid input or unreadable state (malformed ticket, PRD, binding, index, manifest) |
102
+ | 4 | invalid input or unreadable state (malformed ticket, PRD, record, index, manifest; incomplete transaction) |
67
103
  | 124 | the check timed out |
68
104
  | 130 | the check was interrupted (SIGINT or SIGTERM received by the runtime) |
69
105
 
70
106
  Wrapper mappings: `scripts/pincer-ticket.sh <start|verify|done|bind> …` calls the
71
107
  command of the same name and returns its exit code; `scripts/pincer-status.sh` calls
72
108
  `status` (reading `PINCER_BUILD_BUDGET_MIN` from the environment) and returns its exit
73
- code. Both print a Node.js 18+ requirement message and exit 4 when `node` is absent.
109
+ code. Both print a Node.js 22+ requirement message and exit 4 when `node` is absent.
110
+
111
+ Argument grammar: `<id>` is a change ID; `D-NN`, `A-NN`, `G-NN` and `E-NN` are
112
+ two-to-six-digit decision, authorization, agreement and event IDs; `--reason`,
113
+ `--note`, `--summary`, `--reference`, `--excerpt`, `--explanation` and
114
+ `--constraints` take one string of at most 2000 characters that is stored verbatim
115
+ (never a transcript; the sanitizer's inline-secret rule applies and refuses a
116
+ secret-like literal). Every mutating `change` command takes the lock, validates
117
+ inputs and preconditions, and refuses before any file is written; refusals exit 1
118
+ with a reason code, invalid state exits 4.
74
119
 
75
120
  ## Supported grammar
76
121
 
@@ -98,11 +143,16 @@ mutation, with the diagnostics listed in `scripts/pincer-runtime/parse.cjs`.
98
143
  - **NOTES.md:** frontmatter `prd`, `base` and `candidate` (full 40-hex commit IDs),
99
144
  `evidence` (manifest path); NOTES without `evidence` is a legacy evaluation.
100
145
  - **Change ID:** `[a-z0-9][a-z0-9-]{0,63}`.
146
+ - **Ticket association:** a ticket belongs to the PRD its `prd` field names, or to
147
+ the only PRD in `.prd/` when the field is absent. Ticket IDs are unique across the
148
+ repository; a ticket resolves to exactly one change through its PRD, and a PRD is
149
+ owned by exactly one change record.
101
150
 
102
151
  ## Change binding
103
152
 
104
- `.prd/changes/<change-id>.json`, written only by `register` and `migrate --apply`,
105
- tracked in git, schema 1:
153
+ `.prd/changes/<change-id>.json` with `schema: 1` is the v0.5.0 binding. It is read
154
+ and written unchanged in migrated mode, written only by `register` (migrated mode)
155
+ and never by the changes-mode commands:
106
156
 
107
157
  | Field | Value |
108
158
  | --- | --- |
@@ -117,16 +167,305 @@ tracked in git, schema 1:
117
167
  | `legacy_receipts` | `{ "T-NN": { "verified": "...", "last_check": "..." } }` imported by migration, else `{}` |
118
168
 
119
169
  Registration (and migration) adds `.pincer/` to `.gitignore` before writing the
120
- binding, and the installer adds it on `init` and `update`, so local runtime state is
121
- never an untracked change. Exactly one binding may exist per worktree in this increment. A second file, a file
122
- for another PRD (without `--replace`), malformed JSON, an unsupported `schema`, or a
170
+ record, and the installer adds it on `init` and `update`, so local runtime state is
171
+ never an untracked change. In migrated mode exactly one binding may exist per worktree. A second file, a file
172
+ for another PRD, malformed JSON, an unsupported `schema`, or a
123
173
  `prd_revision` that no longer matches the PRD content are diagnosed before any child
124
174
  process starts (`AMBIGUOUS`, `CHANGE_REQUIRED`, `MALFORMED`, `UNSUPPORTED_SCHEMA`,
125
- `REVISION_CHANGED`). Copying a binding into another repository requires its `base`
175
+ `REVISION_CHANGED`). The v0.5.0 `register --replace` (delete the other PRD's binding)
176
+ is refused in migrated mode with `MIGRATION_REQUIRED`: migrate to changes mode, then
177
+ `change select` the change to work on or `change supersede` the one being replaced.
178
+ Copying a binding into another repository requires its `base`
126
179
  to exist there and its `prd` path to resolve; otherwise it is `UNSUPPORTED_INPUT`.
127
180
  Binding fields are recorded on every attempt separately; a binding is never hashed
128
181
  into the source manifest.
129
182
 
183
+ ## Change records
184
+
185
+ `.prd/changes/<change-id>.json` with `schema: 2` is a change record: the portable
186
+ identity, agreement history, authorization and decision records, lifecycle history and
187
+ evaluation references of one change. It is tracked in git and is
188
+ written only by `register` and `migrate --apply` (which create it) and the mutating
189
+ `change` commands (which append its history), always through a transaction
190
+ ("Transactions and recovery"). Several records may coexist; each PRD is
191
+ owned by exactly one record and each record owns exactly one PRD.
192
+
193
+ | Field | Owner | Value |
194
+ | --- | --- | --- |
195
+ | `schema`, `runtime` | register/migrate | `2`, `2` |
196
+ | `change` | register/migrate | the change ID; equals the filename |
197
+ | `prd` | register/migrate | `.prd/prd-vN.md`; unique across records |
198
+ | `base` | register/migrate | HEAD at registration, 40 hex |
199
+ | `registered` | register/migrate | ISO UTC timestamp |
200
+ | `sequence` | every transaction | the record revision: the number of events; each committed transaction appends exactly one event and increments it |
201
+ | `lifecycle` | lifecycle transactions | the projection `{ state, since, reason, note, superseded_by }`; validated against `events` on every read |
202
+ | `agreements` | `change revise`, `change authorize`, migrate | `[{ id: "G-NN", digest, prd_revision, breakdown, tickets: ["T-NN"], decisions: ["D-NN"], snapshot, recorded }]` |
203
+ | `authorizations` | `change authorize`, migrate | `[{ id: "A-NN", agreement: "G-NN", digest, disposition: "user" \| "delegated", reference, excerpt, constraints, basis, explanation, decisions: ["D-NN"], recorded }]` |
204
+ | `decisions` | `change decide` | `[{ id: "D-NN", status: "open" \| "resolved", summary, reference, excerpt, raised, resolved }]` |
205
+ | `events` | every transaction | `[{ sequence, kind, from, to, at, reason, agreement, authorization, decision, replacement, note }]` |
206
+ | `evaluations` | nobody in this schema | always `[]`; evaluation references live in the evaluation locator so that recording one does not change the candidate's source view |
207
+ | `legacy` | migrate | `{ receipts: { "T-NN": { verified, last_check } }, authorization_text: string \| null, migrated_from: "legacy" \| "binding" \| null, migrated: timestamp \| null }`; `authorization_text` is the v0.5.0 free-text `authorization`, retained as an unvalidated historical reference that never satisfies the authorization gate |
208
+
209
+ Every key above is required and no other key is allowed (`MALFORMED`). IDs `G-NN`,
210
+ `A-NN`, `D-NN` are allocated sequentially per record starting at `01`; `events[i].sequence`
211
+ is `i + 1`, contiguous, and `sequence` equals `events.length` (`HISTORY_INVALID`
212
+ otherwise). Event `kind` is one of `register`, `activate`, `pause`, `resume`,
213
+ `complete`, `reopen`, `cancel`, `supersede`, `agreement`, `authorize`, `decide`,
214
+ `resolve`, `migrate`; lifecycle kinds carry `from` and `to` as in the transition
215
+ table, the others carry `from = to =` the state at the time. Unused fields are `null`.
216
+ The lifecycle projection is recomputed by replaying the lifecycle events from
217
+ `planned`; a projection that disagrees, a `superseded_by` that names a missing record
218
+ or a supersession chain that returns to the record itself is `HISTORY_INVALID`. A file
219
+ that fails to parse (including git conflict markers) is `MALFORMED`; a git merge that
220
+ interleaves two histories is caught by the sequence rule and is repaired by hand, never
221
+ by last-writer-wins.
222
+
223
+ Example of a valid record (`prd-v2` registered, revised once, authorized, activated,
224
+ paused with a handoff note):
225
+
226
+ ```json
227
+ {
228
+ "schema": 2,
229
+ "runtime": 2,
230
+ "change": "prd-v2",
231
+ "prd": ".prd/prd-v2.md",
232
+ "base": "3f2a1c9d5b7e4a6f8c0d2e1b9a7c5e3d1f0b8a6c",
233
+ "registered": "2026-09-12T08:00:00Z",
234
+ "sequence": 4,
235
+ "lifecycle": { "state": "paused", "since": "2026-09-12T10:30:00Z", "reason": "waiting for the API contract", "note": "T-03 is half done; the failing case is in test/api.test.js", "superseded_by": null },
236
+ "agreements": [
237
+ { "id": "G-01", "digest": "5a1e0c9d3b7f2e4a6c8d0b1f3e5a7c9d1b3f5e7a9c1d3f5b7e9a1c3d5f7b9e1a", "prd_revision": "c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00", "breakdown": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "tickets": ["T-03", "T-04"], "decisions": [], "snapshot": ".prd/changes/prd-v2/agreements/G-01.json", "recorded": "2026-09-12T08:05:00Z" }
238
+ ],
239
+ "authorizations": [
240
+ { "id": "A-01", "agreement": "G-01", "digest": "5a1e0c9d3b7f2e4a6c8d0b1f3e5a7c9d1b3f5e7a9c1d3f5b7e9a1c3d5f7b9e1a", "disposition": "user", "reference": "session 2026-09-12 09:58, user message after /pincer-narrow", "excerpt": "go ahead with both tickets as written", "constraints": null, "basis": null, "explanation": null, "decisions": [], "recorded": "2026-09-12T10:00:00Z" }
241
+ ],
242
+ "decisions": [],
243
+ "events": [
244
+ { "sequence": 1, "kind": "register", "from": null, "to": "planned", "at": "2026-09-12T08:00:00Z", "reason": null, "agreement": null, "authorization": null, "decision": null, "replacement": null, "note": null },
245
+ { "sequence": 2, "kind": "authorize", "from": "planned", "to": "planned", "at": "2026-09-12T10:00:00Z", "reason": null, "agreement": "G-01", "authorization": "A-01", "decision": null, "replacement": null, "note": null },
246
+ { "sequence": 3, "kind": "activate", "from": "planned", "to": "active", "at": "2026-09-12T10:01:00Z", "reason": null, "agreement": "G-01", "authorization": "A-01", "decision": null, "replacement": null, "note": null },
247
+ { "sequence": 4, "kind": "pause", "from": "active", "to": "paused", "at": "2026-09-12T10:30:00Z", "reason": "waiting for the API contract", "agreement": "G-01", "authorization": null, "decision": null, "replacement": null, "note": "T-03 is half done; the failing case is in test/api.test.js" }
248
+ ],
249
+ "evaluations": [],
250
+ "legacy": { "receipts": {}, "authorization_text": null, "migrated_from": null, "migrated": null }
251
+ }
252
+ ```
253
+
254
+ (`change authorize` records the agreement entry `G-01` in the same transaction as
255
+ `A-01` when it was not recorded by an earlier `change revise`; that is why the example
256
+ carries no separate `agreement` event.)
257
+
258
+ Invalid, unknown-schema and conflicting records, all refused before any write and
259
+ never treated as legacy:
260
+
261
+ ```json
262
+ { "schema": 2, "runtime": 2, "change": "prd-v2", "prd": ".prd/prd-v2.md", "base": "3f2a1c9d5b7e4a6f8c0d2e1b9a7c5e3d1f0b8a6c", "registered": "2026-09-12T08:00:00Z", "sequence": 2, "lifecycle": { "state": "active", "since": "2026-09-12T08:00:00Z", "reason": null, "note": null, "superseded_by": null }, "agreements": [], "authorizations": [], "decisions": [], "events": [ { "sequence": 1, "kind": "register", "from": null, "to": "planned", "at": "2026-09-12T08:00:00Z", "reason": null, "agreement": null, "authorization": null, "decision": null, "replacement": null, "note": null } ], "evaluations": [], "legacy": { "receipts": {}, "authorization_text": null, "migrated_from": null, "migrated": null } }
263
+ ```
264
+
265
+ is `HISTORY_INVALID` twice over: `sequence` is 2 with one event, and the projection
266
+ says `active` while the history ends at `planned`.
267
+
268
+ ```json
269
+ { "schema": 3, "change": "prd-v2", "prd": ".prd/prd-v2.md" }
270
+ ```
271
+
272
+ is `UNSUPPORTED_SCHEMA` (schema 3 is unknown; the file is left untouched and the
273
+ project is neither legacy nor migrated).
274
+
275
+ ```json
276
+ { "schema": 2, "runtime": 2, "change": "feature-x", "prd": ".prd/prd-v2.md", "base": "3f2a1c9d5b7e4a6f8c0d2e1b9a7c5e3d1f0b8a6c", "registered": "2026-09-12T09:00:00Z", "sequence": 1, "lifecycle": { "state": "planned", "since": "2026-09-12T09:00:00Z", "reason": null, "note": null, "superseded_by": null }, "agreements": [], "authorizations": [], "decisions": [], "events": [ { "sequence": 1, "kind": "register", "from": null, "to": "planned", "at": "2026-09-12T09:00:00Z", "reason": null, "agreement": null, "authorization": null, "decision": null, "replacement": null, "note": null } ], "evaluations": [], "legacy": { "receipts": {}, "authorization_text": null, "migrated_from": null, "migrated": null } }
277
+ ```
278
+
279
+ is a conflicting record next to the valid example above (`.prd/changes/feature-x.json`
280
+ also owns `.prd/prd-v2.md`): `INPUT_INVALID` "duplicate PRD ownership" for the whole
281
+ directory until one of them is removed by hand.
282
+
283
+ ```text
284
+ { "schema": 2, "change": "prd-v2", "prd": ".prd/prd-v2.md", "sequence":
285
+ ```
286
+
287
+ is `MALFORMED` (truncated JSON); so is a file carrying `<<<<<<<` conflict markers.
288
+
289
+ ## Selection
290
+
291
+ The selected change of a worktree is `.pincer/runtime/selection.json`:
292
+
293
+ ```json
294
+ { "schema": 1, "change": "prd-v2", "selected": "2026-09-12T10:00:00Z" }
295
+ ```
296
+
297
+ written only by `change select` and `migrate --apply`, through the transaction
298
+ journal, and never copied between worktrees. Selection is metadata: it never checks
299
+ out, stashes, resets, commits or creates a worktree, and it succeeds on a dirty tree,
300
+ leaving HEAD, the index, tracked files and untracked files byte for byte unchanged.
301
+ Selecting a change grants no approval and activates nothing.
302
+
303
+ Resolution rules (changes mode): no file → `SELECTION_REQUIRED` even when only one
304
+ record exists (a fresh clone always selects explicitly); a file naming a record that is
305
+ missing or unreadable → `SELECTION_INVALID` (no fallback to another record, the
306
+ highest PRD or the newest record); a valid file → the selected change. Inspection
307
+ commands accept `--change <id>` to read another record without changing the file.
308
+ Execution commands ("Command gates") use the selection only: a ticket whose PRD belongs
309
+ to another record is `WRONG_CHANGE`.
310
+
311
+ Repository view: a selected change is compatible with the working tree when its PRD
312
+ exists and validates, its recorded `base` is an ancestor of HEAD (`git merge-base
313
+ --is-ancestor`), and HEAD exists. Anything else is `BASE_MISMATCH`, reported with HEAD,
314
+ the recorded base, the current branch name and the dirty paths. The branch name is a
315
+ hint printed for the developer; it is never identity, authorization or proof that the
316
+ right branch is checked out, and the runtime never claims otherwise.
317
+
318
+ ## Lifecycle
319
+
320
+ States: `planned`, `active`, `paused`, `completed`, `cancelled`, `superseded`. At most
321
+ one record among the records in the tree may be `active` at a time. Every transition
322
+ is one transaction that validates the whole precondition set under the lock, appends
323
+ one event and rewrites the projection; an invalid transition writes nothing.
324
+
325
+ | From | Operation | To | Preconditions (all checked under the lock, before any write) |
326
+ | --- | --- | --- | --- |
327
+ | (none) | `register` | `planned` | unique change ID; PRD valid and owned by no record; HEAD exists |
328
+ | `planned` | `change activate` | `active` | selected; repository view compatible; agreement computable; authorization `current`; no open decision; no other record `active` |
329
+ | `active` | `change pause --reason` | `paused` | selected; no `running` attempt of this change |
330
+ | `paused` | `change resume` | `active` | as `activate` |
331
+ | `active` | `change complete` | `completed` | selected; authorization `current`; no open decision; every ticket of the PRD is `done` and ready (checked criteria, current passing attempt of this change, no `SOURCE_CHANGED`/`CHECK_CHANGED`/`REVISION_CHANGED`); no `running` attempt of this change |
332
+ | `completed` | `change reopen --reason` | `active` | as `activate`; completion events stay in the history |
333
+ | `planned`, `active`, `paused` | `change cancel --decision D-NN --reason` | `cancelled` | selected or `--change`; `D-NN` resolved; no `running` attempt of this change |
334
+ | `planned`, `active`, `paused`, `completed` | `change supersede --with <id> --decision D-NN` | `superseded` | `D-NN` resolved; the replacement exists, is not this record, and is not (transitively) superseded by this record; no `running` attempt of this change |
335
+ | `cancelled`, `superseded` | any execution, `activate`, `resume`, `reopen`, `revise`, `authorize`, `decide` | refused | `LIFECYCLE_BLOCKED`; register a new change and reference the historical one |
336
+
337
+ Idempotence: requesting the state a record already has (`pause` when `paused`,
338
+ `resume`/`activate`/`reopen` when `active`, `complete` when `completed`, `cancel` when
339
+ `cancelled`, `supersede` when `superseded` by the same replacement) exits 0, prints the
340
+ current state and writes no event. Every other pair not in the table is
341
+ `LIFECYCLE_BLOCKED` with the current state and the permitted operations named.
342
+
343
+ `completed` means implementation complete and ready for evaluation. It is not
344
+ evaluated, release-ready, merged or published, and it is written before the candidate
345
+ is chosen: the `complete` transaction, the last authored documentation changes and any
346
+ lifecycle or agreement change are committed, and the resulting commit is the candidate.
347
+ A later failing recheck makes a completed change non-ready in status and release
348
+ without rewriting history; `reopen` when the implementation must change.
349
+
350
+ Pause is an execution hold: ticket state, every attempt, every authorization and the
351
+ reason are kept. Cancellation and supersession preserve artifacts and evidence as
352
+ inspectable history; they grant no permission to discard source or to mark unfinished
353
+ requirements delivered.
354
+
355
+ ## Agreements and authorization
356
+
357
+ An agreement is the reviewed content a user authorized: the change, the PRD's authored
358
+ revision, the breakdown, and the resolved consequential decisions. Its digest is
359
+ SHA-256 over the agreement projection, version 1, an exact text:
360
+
361
+ ```
362
+ pincer agreement 1
363
+ change <change id>
364
+ prd <.prd/prd-vN.md> <prd_revision>
365
+ ticket <T-NN> <ticket_digest> (one line per ticket of the PRD, ascending numeric ID)
366
+ decision <D-NN> <decision digest> (one line per resolved decision, ascending ID)
367
+ ```
368
+
369
+ each line terminated by `\n`. `prd_revision` and `ticket_digest` are the digests of
370
+ "Content revisions": lifecycle fields (`status`, `started`, `finished`, `verified`,
371
+ `last_check`) and checkbox marks are the only exclusions, so ticking a criterion,
372
+ starting or closing a ticket, recording an attempt or editing implementation source
373
+ never changes the agreement, while a change to a ticket's acceptance text,
374
+ `depends_on`, `size`, `timeout`, `prd` association or Verification block, a ticket
375
+ added to or removed from the PRD, and any PRD body edit under the same filename each
376
+ change it. The decision digest is SHA-256 over `<id>\n<summary>\n<reference>\n<excerpt>\n`
377
+ of a resolved decision; open decisions are not agreement inputs (they gate
378
+ execution instead). The breakdown digest recorded on an agreement entry is SHA-256
379
+ over the `ticket` lines alone. Authorizations, lifecycle events, evaluations, attempts,
380
+ the selection and the change record itself are never inputs.
381
+
382
+ Agreement entries `G-NN` are recorded by `change revise` (explicitly) and by
383
+ `change authorize` (when the digest it binds is not yet recorded), with a snapshot
384
+ `.prd/changes/<id>/agreements/G-NN.json`, tracked in git:
385
+
386
+ ```json
387
+ { "schema": 1, "change": "prd-v2", "agreement": "G-01", "digest": "<64 hex>", "projection": "pincer agreement 1\nchange prd-v2\n…", "prd": { "path": ".prd/prd-v2.md", "revision": "<64 hex>", "text": "<normalized PRD text>" }, "tickets": { "T-03": { "file": "tickets/T-03-slug.md", "digest": "<64 hex>", "text": "<normalized ticket text>" } }, "decisions": { "D-01": { "summary": "…", "reference": "…", "excerpt": "…" } }, "recorded": "2026-09-12T08:05:00Z" }
388
+ ```
389
+
390
+ The snapshot holds the normalized inputs, so an old agreement can be reviewed and its
391
+ digest recomputed from the file alone; a record whose snapshot is missing or whose
392
+ recomputed digest differs is `HISTORY_INVALID` for that agreement, and the runtime
393
+ refuses to present a digest-only history as reviewable. The structural difference
394
+ shown by `change show` and `resume` compares the latest authorized snapshot with the
395
+ current inputs: PRD changed or not, tickets added, removed and changed, and for each
396
+ changed ticket which parts differ (`frontmatter`, `acceptance`, `verification`,
397
+ `other`). It is a structural diff, never a semantic impact judgment.
398
+
399
+ Authorization records `A-NN` bind one agreement digest. Two dispositions:
400
+
401
+ - `user`: `--reference` names where the user's instruction lives (a session, a date, a
402
+ document, a ticket comment) and `--excerpt` is a short faithful quotation of it;
403
+ `--constraints` records limits or delegations the user stated. It records local
404
+ provenance and consistency, not authenticated identity, and it cannot be created by
405
+ the runtime from a PRD status, a ticket status, a passing check, a registration or
406
+ the v0.5.0 free text.
407
+ - `delegated`: `--basis A-NN` names an earlier authorization of the same record
408
+ (transitively ending in a `user` one) and `--explanation` says why the new agreement
409
+ stays within the delegation that authorization granted. The runtime validates the
410
+ chain; whether the delegation really covers the change is a reviewer's judgment and
411
+ is presented as such.
412
+
413
+ Both require `--agreement <digest>` equal to the agreement digest computed from the
414
+ current inputs at commit time (`AGREEMENT_CHANGED` when a prepared digest no longer
415
+ matches), refuse when the record is `cancelled` or `superseded`, and are idempotent: a
416
+ second identical record (same digest, disposition, reference, excerpt, constraints,
417
+ basis, explanation and decisions) writes nothing and exits 0. Every `--decision` named
418
+ must exist and be resolved.
419
+
420
+ The authorization verdict is one pure computation shared by gates, status and resume:
421
+
422
+ | Verdict | Condition |
423
+ | --- | --- |
424
+ | `current` | some authorization's `digest` equals the agreement digest computed now, and no decision is `open` |
425
+ | `AUTHORIZATION_REQUIRED` | the record has no authorization at all |
426
+ | `AGREEMENT_CHANGED` | authorizations exist but none matches the current digest (detail names the latest authorized agreement, the current digest and the structural difference) |
427
+ | `DECISION_REQUIRED` | a decision is `open` (detail names it) |
428
+
429
+ Decisions `D-NN` are the consequential choices that need the user: `change decide
430
+ --summary` raises one as `open` (any agent may do this when it discovers such a
431
+ choice), and `change decide --resolve D-NN --reference --excerpt` records the user's
432
+ actual decision. Resolving changes the agreement digest, so the next `change authorize`
433
+ names the decision and binds the new digest. A deferral is a resolved decision whose
434
+ excerpt says so; it is reported as a decision, never as delivery.
435
+
436
+ ## Command gates
437
+
438
+ Execution and mutation in changes mode pass one shared guard before any child process
439
+ is spawned or any lifecycle file written. The guard resolves the selection, loads and
440
+ validates the record, checks the lifecycle state, the repository view and the
441
+ authorization verdict, and refuses with the first failing code in this order:
442
+ `INPUT_INVALID`/`MALFORMED`/`UNSUPPORTED_SCHEMA`/`HISTORY_INVALID`/`STATE_INCOMPLETE`
443
+ → `SELECTION_REQUIRED`/`SELECTION_INVALID` → `WRONG_CHANGE` → `LIFECYCLE_BLOCKED` →
444
+ `BASE_MISMATCH` → `DECISION_REQUIRED` → `AUTHORIZATION_REQUIRED`/`AGREEMENT_CHANGED`.
445
+
446
+ | Command | Requires |
447
+ | --- | --- |
448
+ | `start`, `done` | selected change owns the ticket; state `active`; view compatible; authorization `current` |
449
+ | `verify` | selected change owns the ticket; state `active` or `completed`; view compatible; authorization `current` |
450
+ | `check`, `evidence export` | selected change owns the PRD; state `completed`; clean candidate view; authorization `current` |
451
+ | `change activate`, `change resume`, `change reopen` | as "Lifecycle" |
452
+ | `status`, `ready`, `resume`, `change list`, `change show`, `validate`, `snapshot` | nothing; read-only, and they report the same codes the gates would |
453
+ | `register`, `change select`, `change revise`, `change decide`, `change authorize`, `recover`, `migrate` | no execution authorization; each validates its own inputs |
454
+
455
+ Read-only readiness (`ready`, status, resume, release) reports `AUTHORIZATION_REQUIRED`,
456
+ `AGREEMENT_CHANGED` or `DECISION_REQUIRED` as non-ready reasons of the selected change
457
+ while still showing historical attempts and evaluations; history never asserts present
458
+ authorization. Refused executions print nothing on stdout, launch nothing and leave
459
+ every ticket, record, index and selection file unchanged.
460
+
461
+ For `verify` and `check` the guard runs twice: once before anything is prepared, and
462
+ again under the worktree lock immediately before the `running` attempt record is
463
+ written. The second evaluation is the one that counts: a transition, revision or
464
+ authorization committed between the first evaluation and the lock is seen there, so
465
+ the attempt is refused with that gate's code (nothing written, nothing launched) or
466
+ recorded against the agreement current under the lock. An attempt therefore never
467
+ runs for a change whose state or authorization no longer permits it.
468
+
130
469
  ## Content revisions
131
470
 
132
471
  Digests are SHA-256, hex, shown shortened to 12 characters in human output and in
@@ -138,6 +477,7 @@ full in records. Line endings are not normalized.
138
477
  | Ticket authored digest (`ticket_digest`) | the ticket file with the frontmatter `status`, `started`, `finished`, `verified` and `last_check` lines removed and every Acceptance Criteria checkbox mark normalized to `[ ]` |
139
478
  | Check digest (`check_digest`) | the Verification block text (the lines between the fences) followed by `\ntimeout=<effective seconds>` |
140
479
  | Source digest | the source manifest (next section) |
480
+ | Agreement digest | the agreement projection ("Agreements and authorization") |
141
481
 
142
482
  These are the only normalization exceptions. Acceptance text, `depends_on`, `size`,
143
483
  `timeout`, the Verification block and the PRD body always contribute. Validation of
@@ -160,9 +500,13 @@ PRDs (`.prd/prd-v*.md`) are hashed after the normalization above, so ticking a b
160
500
  a lifecycle transition does not change the digest.
161
501
 
162
502
  Fixed exclusions (never inputs): `.git/`, `.pincer/`, `NOTES.md`, `.prd/evidence/`,
163
- `.prd/changes/`. This is why a new evidence artifact or NOTES edit cannot invalidate
164
- the snapshot that produced it; the separate post-candidate commit policy (only
165
- NOTES.md and the manifest's listed files may follow the candidate) is unchanged.
503
+ `.prd/changes/`. This is why a new evidence artifact, a NOTES edit, a lifecycle event,
504
+ an agreement snapshot or an evaluation reference cannot invalidate the snapshot that
505
+ produced it. The separate post-candidate commit policy is unchanged in substance: only
506
+ `NOTES.md`, the listed files of validated manifests for that candidate and (changes
507
+ mode) valid evaluation locators may follow the candidate ("Evaluation locator"); a
508
+ change record edit after the candidate is a candidate change, which is why completion
509
+ precedes the candidate.
166
510
 
167
511
  Configured exclusions: the optional tracked file `.prd/source-exclude`, one pattern
168
512
  per line (`#` comments; `dir/` prefixes; `*`, `**` and `?` globs; a pattern without
@@ -182,7 +526,9 @@ that is not inside a git repository.
182
526
  Limitations recorded on every manifest and attempt: ignored paths (for example
183
527
  `node_modules/`, `dist/`) are listed by name as not part of the identity; external
184
528
  services and installed toolchains are not part of the identity; source equality does
185
- not prove the environment stayed healthy.
529
+ not prove the environment stayed healthy. Whole-workspace identity is deliberately
530
+ conservative: another change's edits invalidate this change's passing attempts, which
531
+ status and resume report as `SOURCE_CHANGED`; no dependency-scoped digest exists.
186
532
 
187
533
  ## Attempts
188
534
 
@@ -196,22 +542,27 @@ hand.
196
542
  | `attempts/<attempt id>.json` | one attempt record |
197
543
  | `attempts/<attempt id>/stdout.log`, `stderr.log` | sanitized captured output |
198
544
  | `manifests/<digest>.json` | content-addressed source manifests |
545
+ | `selection.json` | the selected change ("Selection"; changes mode) |
199
546
  | `lock/` | the exclusive lock directory; `lock/owner.json` is `{ pid, ppid, host, started, command }` |
200
- | `journal/` | temporary files for atomic replacement |
547
+ | `journal/` | temporary files for atomic replacement and transaction staging ("Transactions and recovery") |
201
548
 
202
- Context keys: `ticket:<change>:<T-NN>` and `candidate:<40 hex>:<C-NN>`. Attempt IDs
549
+ Context keys: `ticket:<change>:<T-NN>` and `candidate:<40 hex>:<C-NN>` in migrated
550
+ mode; `ticket:<change>:<T-NN>` and `candidate:<change>:<40 hex>:<C-NN>` in changes
551
+ mode, so two changes that share a check ID and a candidate commit never share a
552
+ pointer. Attempt IDs
203
553
  are `<sequence, 6 digits>-<UTC compact timestamp>-<6 hex>`; the `sequence` in
204
554
  `index.json` is the authority for ordering (timestamps alone never order attempts), and
205
555
  `index.current[key]` is the authority for the latest attempt: when the record it names
206
556
  is missing or unreadable, readiness is `EVIDENCE_MISSING`, never an older record.
207
557
 
208
- Attempt record schema 1:
558
+ Attempt record schema 1 (migrated mode) and schema 2 (changes mode; the same fields
559
+ plus `context.agreement`):
209
560
 
210
561
  | Field | Value |
211
562
  | --- | --- |
212
- | `schema`, `runtime` | `1`, `1` |
563
+ | `schema`, `runtime` | `1`, `1` (migrated) or `2`, `2` (changes) |
213
564
  | `id`, `sequence` | as above |
214
- | `context` | `{ kind: "ticket" \| "candidate", change, prd, prd_revision, base, ticket?, ticket_digest?, candidate?, check? }` |
565
+ | `context` | `{ kind: "ticket" \| "candidate", change, prd, prd_revision, base, ticket?, ticket_digest?, candidate?, check?, agreement? }`; `agreement` is the agreement digest at launch, required in schema 2 and absent in schema 1 |
215
566
  | `check` | `{ digest, display, timeout_seconds }` (display is the sanitized command text) |
216
567
  | `outcome` | `running \| passed \| failed \| interrupted \| timed_out \| error` |
217
568
  | `exit_code`, `signal` | the child's exit code or signal, else `null` |
@@ -254,7 +605,15 @@ an `interrupted` record without log digests. Readiness and export
254
605
  also compare each captured log with the digest the record carries: a log that was
255
606
  altered after the run is `EVIDENCE_MISSING` (the reason names the stream) and export
256
607
  refuses it; a missing log is `EVIDENCE_MISSING` as before. This detects mistakes and
257
- stale copies, not a deliberate rewrite of both the log and its record.
608
+ stale copies, not a deliberate rewrite of both the log and its record. In changes
609
+ mode a pointed-at record with `schema: 1` (recorded before migration) is
610
+ `HISTORICAL_EVIDENCE`: it stays inspectable with its original identity, never becomes
611
+ current evidence, and the next action is `verify`. A schema 2 record whose
612
+ `context.change` is not the change it is read for is `ATTEMPT_ERROR`; one whose
613
+ `context.prd_revision` differs from the current PRD revision is `REVISION_CHANGED`;
614
+ its `context.agreement` is recorded and exported but is not a readiness input, because
615
+ the conservative whole-source, ticket and check digests already invalidate what a
616
+ changed agreement can change.
258
617
 
259
618
  Lock: acquired by building `lock/` with its `owner.json` in a staging directory and
260
619
  renaming it into place (atomic; a waiter never sees an owner-less lock); waiters poll
@@ -283,6 +642,59 @@ relies on the group id not being reused while any member is alive; between the
283
642
  SIGTERM and SIGKILL attempts an empty group's id could in principle be reused by an
284
643
  unrelated process, which is accepted as a limit.
285
644
 
645
+ ## Transactions and recovery
646
+
647
+ Every write of a change record, an agreement snapshot, the selection, the evaluation
648
+ locator, a migration and a lifecycle event goes through one transaction API
649
+ (`scripts/pincer-runtime/transaction.cjs`), which callers use instead of writing the
650
+ files themselves:
651
+
652
+ 1. Acquire the worktree lock (bounded; `STATE_BUSY` after the wait).
653
+ 2. Read and validate every input the operation depends on: the records under
654
+ `.prd/changes/`, the selection, the index (for `running` attempts of the change),
655
+ the PRD and tickets when the agreement is needed. Compare the record's `sequence`
656
+ with the one the caller expects when it prepared the operation; a difference is
657
+ `STATE_CHANGED` and the operation is refused (a decision prepared against an
658
+ agreement that changed meanwhile is never applied).
659
+ 3. Compute the outcome in memory. A refusal returns here; nothing has been written.
660
+ 4. Stage every new file under `journal/txn-<UTC compact timestamp>-<6 hex>/` on
661
+ the same filesystem, then write the transaction manifest
662
+ `journal/txn-<UTC compact timestamp>-<6 hex>/manifest.json` last, atomically:
663
+
664
+ ```json
665
+ { "schema": 1, "id": "txn-20260912T100100Z-a1b2c3", "command": "change activate prd-v2", "started": "2026-09-12T10:01:00Z", "writes": [ { "target": ".prd/changes/prd-v2.json", "staged": "01-prd-v2.json" }, { "target": ".pincer/runtime/selection.json", "staged": "02-selection.json" } ] }
666
+ ```
667
+
668
+ 5. Rename each staged file onto its target in the listed order, remove the manifest,
669
+ remove the staging directory, release the lock.
670
+
671
+ The manifest is the commit point. A process killed before it exists leaves the old
672
+ state; the staging directory is discarded by the next transaction or `recover`.
673
+ A process killed after it exists leaves a committed transaction whose renames are
674
+ completed (each rename is idempotent: a staged file that is already gone was
675
+ renamed) by the next transaction or `recover` before anything else runs; a manifest
676
+ that cannot be read is left in place and named, never guessed at.
677
+ Read-only commands that find a manifest report `STATE_INCOMPLETE` with the command
678
+ named and refuse to interpret the half-applied files; they never repair. A record is
679
+ therefore always the state before a transition or the state after it with its event;
680
+ a projection without its event cannot be observed.
681
+
682
+ Running attempts: transitions that need the change idle (`pause`, `complete`,
683
+ `cancel`, `supersede`) read `index.running` under the lock and refuse with
684
+ `ATTEMPT_RUNNING` naming the attempt when one belongs to the change. They never
685
+ terminate it; `recover` finalizes a dead owner's attempt first, after which the
686
+ transition succeeds and the `interrupted` result stays visible.
687
+
688
+ Contention order: writers serialize on the lock in acquisition order; a `verify`
689
+ holds the lock only while it evaluates the gates a second time and writes its
690
+ `running` record, and again while it finalizes, so a transition can be refused
691
+ between them by the running check, never the other way round, and a transition
692
+ committed before the check's lock refuses the check (see "Command gates"). Two concurrent activations of different changes: the first commits, the
693
+ second reads the committed record and is refused (`LIFECYCLE_BLOCKED`: another change is
694
+ active). Two concurrent selections: the later one wins and the earlier is overwritten;
695
+ selection carries no history. No lock is held across user interaction: a command that
696
+ needs the user's answer refuses and exits; the answer arrives as a new command.
697
+
286
698
  ## Capture and sanitization
287
699
 
288
700
  stdout and stderr are captured separately as streams, each stored up to 1 MiB
@@ -298,42 +710,70 @@ guarantee: checks must avoid printing secrets, no raw unredacted log is exported
298
710
  the runtime never persists the environment. A Verification block that assigns a
299
711
  secret-like variable inline (`TOKEN=literal`, not a `$reference`) is refused before
300
712
  launch with the line number. If capture or sanitization fails, the attempt is `error`,
301
- never a pass claiming complete output.
713
+ never a pass claiming complete output. The `change` command texts (`--reason`,
714
+ `--note`, `--summary`, `--reference`, `--excerpt`, `--explanation`, `--constraints`)
715
+ pass the same inline-secret rule and are refused, not redacted, when they carry a
716
+ secret-like literal.
302
717
 
303
718
  ## Readiness and reason codes
304
719
 
305
720
  Readiness is one pure computation (`scripts/pincer-runtime/readiness.cjs`) consumed
306
- by the human status, the JSON status, `ready`, `done`, `start` (for dependencies) and
307
- release. Lifecycle `done` and verification readiness are distinct: an old done ticket
308
- can be stale or failed without its `finished` date changing.
721
+ by the human status, the JSON status, `ready`, `done`, `start` (for dependencies),
722
+ `resume`, `change complete` and release. Lifecycle `done` and verification readiness
723
+ are distinct: an old done ticket can be stale or failed without its `finished` date
724
+ changing, and a `completed` change can be non-ready without its history changing.
309
725
 
310
726
  | Code | Meaning | Next action |
311
727
  | --- | --- | --- |
312
- | `CHANGE_REQUIRED` | no change binding names this PRD | `register` or `migrate` |
313
- | `MIGRATION_REQUIRED` | legacy receipts exist and the command needs runtime state | `migrate --preview` |
314
- | `REVISION_CHANGED` | PRD content differs from the bound revision | `register --rebind` |
728
+ | `CHANGE_REQUIRED` | no change record names this PRD | `register` or `migrate` |
729
+ | `MIGRATION_REQUIRED` | legacy receipts or a schema 1 binding exist and the command needs changes-mode state | `migrate --preview` |
730
+ | `REVISION_CHANGED` | PRD content differs from the bound revision (migrated), or from the passing attempt's (changes) | `register --rebind` (migrated); `change revise`, then `verify` (changes) |
315
731
  | `CHECK_CHANGED` | the Verification block or timeout changed since the latest pass | `verify` |
316
732
  | `SOURCE_CHANGED` | the source digest differs from the verified one (paths named) | `verify` |
317
733
  | `CHECK_FAILED` | the latest attempt failed | fix, then `verify` |
318
734
  | `ATTEMPT_RUNNING` | an attempt is `running` | wait, or `recover` if its owner died |
319
735
  | `ATTEMPT_INTERRUPTED` | the latest attempt was interrupted | `verify` |
320
736
  | `ATTEMPT_TIMED_OUT` | the latest attempt timed out | fix or raise `timeout`, then `verify` |
321
- | `ATTEMPT_ERROR` | the latest attempt could not be recorded, is incomplete or malformed, belongs to another context, or mutated source | inspect the record, then `verify` |
737
+ | `ATTEMPT_ERROR` | the latest attempt could not be recorded, is incomplete or malformed, belongs to another context or change, or mutated source | inspect the record, then `verify` |
322
738
  | `EVIDENCE_MISSING` | no attempt, missing or altered log, or missing local state | `verify` |
323
739
  | `LEGACY_RECEIPT` | only a migrated legacy receipt exists | `verify` |
740
+ | `HISTORICAL_EVIDENCE` | the latest attempt or evaluation predates migration to changes mode (schema 1) | `verify` |
324
741
  | `CRITERIA_UNTICKED` | unticked acceptance criteria | tick verified criteria |
325
742
  | `DEPENDENCY_BLOCKED` | a `depends_on` ticket is not ready | finish the dependency |
326
- | `INPUT_INVALID` | malformed ticket, PRD or NOTES | repair the input |
327
- | `CANDIDATE_STALE` | NOTES/evidence do not describe HEAD (reason quoted) | `/pincer-evaluate` |
743
+ | `INPUT_INVALID` | malformed ticket, PRD, NOTES, or conflicting change records | repair the input |
744
+ | `MALFORMED` | a record, snapshot, selection, locator or manifest cannot be parsed or has the wrong shape | repair or remove the file by hand |
745
+ | `UNSUPPORTED_SCHEMA` | a record carries a schema this runtime does not read | update the kit, or restore the record |
746
+ | `HISTORY_INVALID` | a record's projection, event sequence, supersession chain or agreement snapshot disagrees with its history | repair the record by hand from its events and snapshots |
747
+ | `STATE_INCOMPLETE` | a committed transaction was not fully applied | `recover` |
748
+ | `STATE_CHANGED` | the record changed between preparing and committing an operation | inspect, then repeat the command against the current state |
749
+ | `SELECTION_REQUIRED` | no change is selected in this worktree | `change select <id>` |
750
+ | `SELECTION_INVALID` | the selected record is missing or unreadable | `change select <id>` or repair the record |
751
+ | `WRONG_CHANGE` | the ticket or PRD belongs to a change other than the selected one | `change select <id>` |
752
+ | `LIFECYCLE_BLOCKED` | the change's state does not permit the operation, or another change is active | the permitted operation is named |
753
+ | `BASE_MISMATCH` | the PRD is missing or the recorded base is not an ancestor of HEAD | check out the branch that carries the change, or register a new change |
754
+ | `AUTHORIZATION_REQUIRED` | the change has no authorization record | `change authorize` with the user's instruction |
755
+ | `AGREEMENT_CHANGED` | no authorization matches the current agreement | record the disposition: `change authorize` (user or `--delegated`) |
756
+ | `DECISION_REQUIRED` | a consequential decision is open | `change decide --resolve` with the user's decision |
757
+ | `CANDIDATE_STALE` | NOTES/evidence/locator do not describe HEAD (reason quoted) | `/pincer-evaluate` |
328
758
  | `STATE_BUSY` | the lock is held | retry, or `recover` |
329
759
  | `SECRET_PATH` | a secret file is in the source view | remove or ignore it |
330
760
  | `UNSUPPORTED_INPUT` | symlink, submodule, no git, or a refused exclusion | remove the input or change the configuration |
761
+ | `INVENTORY_INVALID` | the PRD's requirement and scenario definitions violate the inventory grammar (line named) | repair the PRD definitions |
762
+ | `COVERAGE_INVALID` | the coverage map cannot be interpreted: missing file, malformed or duplicate-key JSON, unknown or missing keys, wrong types or IDs, duplicate entries, unsafe path, wrong change or PRD | repair `.prd/coverage/<id>.json` |
763
+ | `COVERAGE_INCOMPLETE` | the map is readable but its graph is incomplete: a live scenario without a row, an invented or stale row, a scenario without work or checks, an unclassified or wrong-change ticket, an undeclared check | author the missing rows, then `change authorize` |
764
+ | `OBLIGATION_MISSING` | a scenario of the baseline inventory is defined neither in the PRD nor as a `removed` tombstone in the map | restore it, or record the decision and the tombstone |
765
+ | `SCOPE_UNAUTHORIZED` | a deferral or removal has no resolved decision naming the ID, or no applicable user authorization covers that decision | `change decide`, then `change authorize` |
766
+ | `CHECK_UNDECLARED` | the check is not declared in the map, the supplied command or timeout differs from the declaration, the declaration changed since the command was prepared, or a review obligation was invoked as a command | declare it in the map, or run the declared form |
767
+ | `REVIEW_MISSING` | a required review obligation has no candidate-bound artifact with a passed result | perform the review and record it in the evaluation draft |
768
+ | `ADEQUACY_REQUIRED` | the adequacy judgment is missing or `inadequate` | record the reviewer's judgment in the evaluation draft |
769
+ | `HISTORY_UNAVAILABLE` | impact has no baseline agreement with an inventory snapshot | authorize the adoption agreement, then compare |
770
+ | `COVERAGE_UNVERIFIED` | label only, never a blocker: strict coverage is not adopted (legacy, migrated or schema 2) | `coverage adopt --preview --change <id>` when strict coverage is wanted |
331
771
 
332
- Status JSON schema 1 (one object on stdout; diagnostics on stderr; no progress text,
333
- no secret values):
772
+ Status JSON schema 1 (legacy and migrated modes; one object on stdout; diagnostics on
773
+ stderr; no progress text, no secret values):
334
774
 
335
775
  ```
336
- { schema: 1, runtime: 1, generated, root, mode: "legacy" | "migrated",
776
+ { schema: 1, runtime: 1, generated, root, mode: "legacy" | "migrated" | "invalid",
337
777
  change: { id, prd, prd_revision, base } | null,
338
778
  prd: { path, status, profile, date } | null,
339
779
  tickets: [ { id, file, status, size, depends_on, started, finished,
@@ -348,6 +788,24 @@ no secret values):
348
788
  reasons: [ { code, detail } ], next }
349
789
  ```
350
790
 
791
+ Status JSON schema 2 (changes mode) keeps every schema 1 field with `mode: "changes"`
792
+ and `runtime: 2`, and adds:
793
+
794
+ ```
795
+ { schema: 2, …,
796
+ selection: { change | null, problem: { code, detail } | null },
797
+ changes: [ { id, prd, state, since, selected, authorization: "current" | code, agreement: <digest or null> } ],
798
+ change: { id, prd, prd_revision, base, sequence,
799
+ lifecycle: { state, since, reason, note, superseded_by },
800
+ agreement: { current, authorized: { id, agreement, digest, disposition, recorded } | null, verdict, open_decisions: [ "D-NN" ] },
801
+ view: { head, branch, base_is_ancestor, dirty: [ paths ] } } | null,
802
+ candidate: { …, locator: ".prd/evidence/changes/<id>.json" | null, evaluation: { candidate, base, manifest, recorded } | null } }
803
+ ```
804
+
805
+ `tickets` are the selected change's; without a selection `tickets` is `[]`, `change`
806
+ is `null` and `reasons` starts with `SELECTION_REQUIRED`. `mode: "invalid"` (with the
807
+ problem in `reasons`) is reported when `.prd/changes/` mixes schemas or holds a file
808
+ the runtime cannot read: such a project is never reported as legacy or migrated.
351
809
  `status` exits 0 when inspection succeeded; `ready` exits 1 for non-ready work; both
352
810
  exit 4 on invalid input.
353
811
 
@@ -355,7 +813,7 @@ exit 4 on invalid input.
355
813
 
356
814
  Schema 2 keeps every schema 1 field and rule and adds:
357
815
 
358
- - top-level `change`: `{ id, prd_revision, base }` copied from the binding; its `base`
816
+ - top-level `change`: `{ id, prd_revision, base }` copied from the change record; its `base`
359
817
  is HEAD at registration and may precede the evaluation `base` the manifest records;
360
818
  - `provenance` on every check: `runtime` for command checks populated from attempts,
361
819
  `authored` for review and visual checks and for a command check recorded as
@@ -374,12 +832,16 @@ required }`), refuses when any command stub has no attempt for the candidate, wr
374
832
  `checks/C-NN.log` (the command line, then stdout, then stderr, with truncation notes),
375
833
  fills the runtime fields, computes `artifacts` digests, writes `manifest.json` and
376
834
  validates it. It never invents a review transcript or converts a review judgment into a
377
- command result.
835
+ command result. In changes mode the attempts it consumes must be schema 2 records of
836
+ the selected change (a schema 1 record or another change's record is refused), and the
837
+ manifest's `change.prd_revision` is the PRD revision the attempts recorded.
378
838
 
379
839
  `check` refuses unless HEAD is `--candidate` (or a descendant that differs from it
380
- only in `NOTES.md` and `.prd/evidence/prd-vN/<candidate>/`, such as the evaluate
381
- commit), the binding is current, and `git status --porcelain --untracked-files=all`
382
- lists nothing outside those same paths; it never stashes, resets or commits.
840
+ only in `NOTES.md`, evidence directories of that candidate commit
841
+ (`.prd/evidence/prd-v*/<candidate>/`) and evaluation locators, such as the evaluate
842
+ commit of this or another change of the same candidate), the record is current, and
843
+ `git status --porcelain --untracked-files=all` lists nothing outside those same paths;
844
+ it never stashes, resets or commits.
383
845
 
384
846
  Validation: `node scripts/pincer-evidence.cjs validate <manifest> …` accepts schema 1
385
847
  and 2 and prints `ok <candidate>` for schema 1 (unchanged) and `ok <candidate>
@@ -389,49 +851,919 @@ legacy (schema 1, authored command results)` and schema 2 `provenance: runtime`.
389
851
  schema 1 manifest cannot satisfy a requirement for runtime evidence. Release
390
852
  readiness additionally requires that no newer local attempt for the same candidate and
391
853
  check with the same source digest is nonpassing; a fresh clone without
392
- `.pincer/runtime/` reports `local verification history unavailable; saved candidate
854
+ local attempt history (no `.pincer/runtime/index.json`; a selection alone is not
855
+ history) reports `local verification history unavailable; saved candidate
393
856
  evidence validated only`. Local capture establishes provenance and mistake detection,
394
857
  not tamper-proof attestation.
395
858
 
859
+ ## Evaluation locator
860
+
861
+ In changes mode the evaluations of a change are located by
862
+ `.prd/evidence/changes/<change-id>.json`, tracked in git, written only by
863
+ `evidence export` (appending) through a transaction:
864
+
865
+ ```json
866
+ { "schema": 1, "change": "prd-v2", "evaluations": [ { "candidate": "9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d", "base": "3f2a1c9d5b7e4a6f8c0d2e1b9a7c5e3d1f0b8a6c", "prd": ".prd/prd-v2.md", "prd_revision": "c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00c0ffee00", "agreement": "5a1e0c9d3b7f2e4a6c8d0b1f3e5a7c9d1b3f5e7a9c1d3f5b7e9a1c3d5f7b9e1a", "manifest": ".prd/evidence/prd-v2/9e8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d/manifest.json", "recorded": "2026-09-12T12:00:00Z" } ] }
867
+ ```
868
+
869
+ The latest entry is the change's evaluation. The locator lives under the fixed source
870
+ exclusion `.prd/evidence/`, is not listed in any manifest (so no digest refers to
871
+ itself), and is one of the paths allowed to differ from the candidate. The
872
+ candidate of a change is "current" when the latest entry's manifest validates for its
873
+ candidate, base and PRD and records this change, the candidate is an ancestor of HEAD,
874
+ and the diff from the candidate to HEAD plus the dirty tree contain nothing but the
875
+ candidate's followers; otherwise `CANDIDATE_STALE` with the reason quoted. The
876
+ followers are computed from validated content, never from a directory or filename
877
+ pattern: `NOTES.md`; every locator under `.prd/evidence/changes/` that parses and
878
+ validates for its own id; and, for each locator entry naming this candidate whose
879
+ manifest validates with file digests for its candidate, base and PRD, that manifest
880
+ and the files it lists (so two changes can evaluate one candidate in turn). Every
881
+ other path is a candidate change: an unlisted file inside an evidence directory, a
882
+ listed artifact whose digest no longer matches, a locator that does not parse or
883
+ names another id, and directories of other candidates. `check` and `evidence export`
884
+ additionally allow the PRD's own `.prd/evidence/prd-vN/<candidate>/` directory while
885
+ the evaluation is being assembled. An entry whose manifest is missing, whose `change`
886
+ differs from the locator's, or that names another change's candidate is `MALFORMED`
887
+ and never revives readiness. Root
888
+ `NOTES.md` remains the human summary of whichever change was evaluated last;
889
+ overwriting it for another change loses nothing. Release (`ready` without a ticket,
890
+ `/pincer-release`) reads the selected change's lifecycle (`completed` required), its
891
+ authorization verdict, the locator, the manifest and the latest applicable attempts;
892
+ it selects nothing, completes nothing, runs nothing and writes nothing.
893
+
894
+ ## Resume report
895
+
896
+ `resume [--change <id>] [--brief] [--json]` is read-only inspection of the selected
897
+ (or named) change for a fresh session; it is distinct from `change resume`, the lifecycle
898
+ operation. It launches no check, records no approval, changes no selection and writes
899
+ no file; repeated runs are byte-identical apart from `generated`. Resume JSON schema 1:
900
+
901
+ ```
902
+ { schema: 1, runtime: 2, generated, root, mode,
903
+ selection: { change | null, problem: { code, detail } | null },
904
+ change: { id, prd, prd_revision, base, registered, sequence,
905
+ lifecycle: { state, since, reason, note, superseded_by },
906
+ view: { head, branch, base_is_ancestor, dirty: [ paths ] } } | null,
907
+ agreement: { current: <digest> | null, authorized: { id, agreement, digest, disposition, reference, excerpt, constraints, recorded } | null,
908
+ verdict: "current" | code, difference: { prd_changed, tickets_added: [], tickets_removed: [], tickets_changed: [ { id, parts: [] } ] } | null,
909
+ decisions: { open: [ { id, summary, raised } ], resolved: [ { id, summary, reference, resolved } ] } },
910
+ references: { prd: { path, title }, snapshot: path | null, tickets: [ { id, file, objective } ] },
911
+ tickets: [ <status ticket entries> ],
912
+ attempts: [ { ticket | check, id, outcome, started, finished, current: boolean } ],
913
+ candidate: <status candidate object>,
914
+ handoff: { kind: "pause" | "reopen" | null, reason, note, since, authored: true } | null,
915
+ blockers: [ { code, detail } ],
916
+ next: { action, command, ticket | check | null } }
917
+ ```
918
+
919
+ `references` quotes only titles, paths and the first line of each ticket's Objective:
920
+ the agreed outcome and constraints are read from the authored artifacts the report
921
+ names (the PRD, the tickets, the agreement snapshot), not paraphrased by the runtime.
922
+ `handoff` is the authored pause or reopen reason and note, labeled `authored: true`; it
923
+ is displayed and never used as an input to `blockers`, `next` or any readiness value,
924
+ so a note claiming approval or success changes nothing computed. Human output prints
925
+ the same fields as labeled lines (`Change`, `Lifecycle`, `View`, `Agreement`,
926
+ `Authorization`, `Decisions`, `Tickets`, `Candidate`, `Handoff (authored)`,
927
+ `Blockers`, `Next`) with the raw reason codes and details kept verbatim.
928
+
929
+ Next-action precedence (the first matching rule wins; `next.command` is the exact
930
+ command to run):
931
+
932
+ 1. invalid or missing state (`INPUT_INVALID`, `MALFORMED`, `UNSUPPORTED_SCHEMA`, `HISTORY_INVALID`, `STATE_INCOMPLETE`) or no selection (`SELECTION_REQUIRED`, `SELECTION_INVALID`) → repair, `recover` or `change select`;
933
+ 2. an unresolved `running` attempt of the change → wait or `recover`;
934
+ 3. lifecycle or repository mismatch (`cancelled`/`superseded` → inspect or register a replacement, never execute; `BASE_MISMATCH` → check out the branch; `planned`/`paused` → `change activate`/`change resume` once 4 is satisfied);
935
+ 4. agreement, decision or coverage gap (`DECISION_REQUIRED`, `AUTHORIZATION_REQUIRED`, `AGREEMENT_CHANGED`; in a strict change also `COVERAGE_INVALID`, `COVERAGE_INCOMPLETE`, `OBLIGATION_MISSING`, `SCOPE_UNAUTHORIZED` — "Strict coverage") → `change decide --resolve` / `change authorize` / repair the map;
936
+ 5. failed or stale verification or unfinished work → `verify T-NN` naming the ticket (or `check C-NN`), or `start T-NN` for the next ready ticket;
937
+ 6. everything done and ready but not `completed` → `change complete`;
938
+ 7. `completed` without a current evaluation → `/pincer-evaluate`;
939
+ 8. evaluated and current → `/pincer-release` (read-only audit).
940
+
941
+ ### Brief resume
942
+
943
+ `resume --brief [--change <id>] [--json]` is a **projection of the report `resume`
944
+ already computes**, for a fresh session that needs the next action without paying for
945
+ every row to find it. It is not a second policy engine: `next` is the full report's
946
+ own object, copied, and no verdict, readiness value or precedence rule is recomputed.
947
+ It composes with `--change` and `--json`, keeps the full report's exit code, and
948
+ writes nothing.
949
+
950
+ Brief JSON envelope — `brief: 1` with `of` naming the schema it projects, so nothing
951
+ reading resume JSON ever sees a new shape:
952
+
953
+ ```
954
+ { brief: 1, kind: "resume-brief", of: 2, generated, root, mode,
955
+ selection: { change | null, problem | null },
956
+ change: { id, prd, base, lifecycle } | null,
957
+ agreement: { current, verdict, authorized: { id, disposition } | null } | null,
958
+ coverage: { label, strict, structure, implementation } | null,
959
+ tickets: { total, by_status: { open, in_progress, done }, not_ready },
960
+ attempts: { total, running, current_failed },
961
+ candidate: { notes, candidate, evidence } | null,
962
+ blockers: { total, categories: [ { code, count } ] },
963
+ next: <the full report's next, verbatim>,
964
+ detail: { command, prd, tickets: [ paths ], omitted } }
965
+ ```
966
+
967
+ Grouping may collapse **repetition**; it may never collapse a **category**. Every
968
+ distinct blocker code of the full report appears in `blockers.categories` with its
969
+ exact count, and `blockers.total`, `tickets.total` and `attempts.total` equal the full
970
+ report's own lengths. `detail.omitted` states how many rows the brief did not print
971
+ and `detail.command` is the exact command that prints them, so nothing is hidden —
972
+ only deferred. The default `resume` human output and resume JSON schema 2 are
973
+ unchanged.
974
+
396
975
  ## Migration and rollback
397
976
 
398
- `migrate --preview --prd .prd/prd-vN.md` prints the plan and writes nothing: the
399
- binding it would write, each ticket of that PRD whose `verified`/`last_check` lines
400
- would be removed and recorded as `legacy_receipts`, the `.gitignore` line it would
401
- add, and every conflict. It exits 0 when apply would proceed and 1 when a conflict
402
- would stop it. Conflicts (all fail closed, before the first write): a binding for
403
- another PRD, malformed tickets, an unsupported binding schema, a ticket of another PRD
404
- with the same ID. A partially applied earlier migration (binding present, receipts
405
- remaining) is not a conflict: preview names it and apply completes it, extending the
406
- existing binding's `legacy_receipts`.
407
-
408
- `migrate --apply` backs up every authored file it changes under
409
- `.pincer/backups/<UTC timestamp>/<original path>`, rewrites the tickets, adds
410
- `.pincer/` to `.gitignore`, then writes the binding last. Repeated apply reports
411
- `already migrated` and changes nothing. Imported receipts are history: a migrated done
412
- ticket reports `LEGACY_RECEIPT` until a runtime attempt exists, without changing
413
- `finished`. Installation and update deploy the runtime files and `doctor` reports when
414
- a migration is available; neither migrates.
415
-
416
- Rollback: restore the files from the backup directory (they are byte-identical
417
- originals), delete `.prd/changes/<id>.json`, and remove `.pincer/`. The project is
418
- then in legacy mode with its original receipts. An older runtime does not enforce the
977
+ `migrate --preview --prd .prd/prd-vN.md` prints the plan and writes nothing. It exits 0
978
+ when apply would proceed and 1 when a conflict would stop it. Conflicts (all fail
979
+ closed, before the first write): malformed or unsupported records, records mixing
980
+ schemas, a ticket of another PRD with the same ID, a PRD owned by another schema 2
981
+ record, an incomplete transaction. Two sources are migrated:
982
+
983
+ - from legacy: each ticket of that PRD whose `verified`/`last_check` lines would be
984
+ removed and recorded as `legacy.receipts`, the `.gitignore` line, the schema 2
985
+ record (`planned`, event 1 `migrate`, `legacy.migrated_from: "legacy"`), the local
986
+ selection it sets in this worktree;
987
+ - from a schema 1 binding: the record it becomes (same change ID, `prd`, `base` and
988
+ `registered`; `legacy.receipts` carried over; the binding's free-text `authorization`
989
+ stored as `legacy.authorization_text`; `migrated_from: "binding"`), the local index
990
+ pointers under `candidate:<sha>:<C-NN>` dropped — a schema 1 attempt record's own key
991
+ never carries the change segment, so no such pointer could resolve after the migration
992
+ and the check is simply run again; the attempt records themselves are untouched and
993
+ stay as history — and the selection it sets.
994
+
995
+ In both cases the preview states that the migrated change is `planned` with no
996
+ authorization, that its existing attempts and evaluations stay as history
997
+ (`HISTORICAL_EVIDENCE`) until verified again, and that `--authorization <text>` is
998
+ retained as unvalidated text only. An actual earlier instruction of the user is recorded
999
+ afterwards with `change authorize`, without asking the user again. A project without
1000
+ receipts or binding needs no migration: `register` writes a schema 2 record directly.
1001
+
1002
+ `migrate --apply` is one transaction: it backs up every authored file it changes
1003
+ (tickets, `.gitignore`, the schema 1 binding) under
1004
+ `.pincer/backups/<UTC timestamp>/<original path>`, stages the rewritten tickets,
1005
+ `.gitignore`, the record, the selection and the index, and commits them together, so
1006
+ an interruption leaves either the unmigrated project or the migrated one (`recover`
1007
+ completes a committed apply). Repeated apply reports `already migrated` and changes
1008
+ nothing; a partially applied v0.5.0 migration (binding present, receipts remaining)
1009
+ is completed in the same apply. Imported receipts are history: a migrated done ticket
1010
+ reports `HISTORICAL_EVIDENCE` or `LEGACY_RECEIPT` until a runtime attempt exists,
1011
+ without changing `finished`. Installation and update deploy the runtime files and
1012
+ `doctor` reports when a migration is available; neither migrates.
1013
+
1014
+ Rollback is one procedure per migration source; the backups are byte-identical
1015
+ originals, and neither procedure deletes a file it has just restored.
1016
+
1017
+ - Rollback from legacy: restore the backed-up tickets and `.gitignore` from
1018
+ `.pincer/backups/<timestamp>/`; delete the schema 2 record `.prd/changes/<id>.json`
1019
+ and, if present, its snapshot directory `.prd/changes/<id>/`; remove `.pincer/` (a
1020
+ legacy project has no runtime state to keep). The project is legacy again with its
1021
+ original receipts.
1022
+ - Rollback from a v0.5.0 binding: restore the backed-up binding to
1023
+ `.prd/changes/<id>.json` — it overwrites the schema 2 record, which lives at the same
1024
+ path, so nothing under `.prd/changes/` is deleted except, if present, the snapshot
1025
+ directory `.prd/changes/<id>/`; restore the backed-up `.pincer/runtime/index.json`
1026
+ (the migration dropped its candidate pointers) and remove
1027
+ `.pincer/runtime/selection.json`; keep the rest of `.pincer/runtime/` so the old
1028
+ attempts and stored manifests remain. The project is migrated (v0.5.0) again with
1029
+ its original binding and history.
1030
+
1031
+ The backup directory can be removed afterwards. An older runtime does not enforce the
419
1032
  runtime guarantees: it will accept the restored receipts as it did before.
420
1033
 
1034
+ ## Strict coverage
1035
+
1036
+ Strict coverage (PRD v6) is an explicit, retained capability of one change: the
1037
+ runtime derives the complete requirement and scenario inventory from the change's
1038
+ PRD, validates one authored coverage map against it, binds both into the reviewed
1039
+ agreement, derives structural, implementation and candidate coverage from that graph,
1040
+ explains structural impact, executes candidate checks from their declarations, and
1041
+ reconciles the exported evidence with the committed candidate's authored inputs.
1042
+ Nothing switches a change into strict coverage implicitly: `coverage adopt --apply`
1043
+ is the only entry, it is recorded in the change record (schema 3), and a project or
1044
+ change without it keeps the documented behavior above with its coverage labeled
1045
+ `unverified`. A report never implies that strict coverage was established where it
1046
+ was not adopted. Semantic adequacy — whether a check really establishes its scenario —
1047
+ remains a named reviewer judgment (`adequacy`) and is never computed.
1048
+
1049
+ Versions frozen by this section: change records `schema: 3` (`runtime: 3`), attempts
1050
+ `schema: 3` (`runtime: 3`), evidence manifests `schema: 3`, status JSON `schema: 3`
1051
+ (changes mode), resume JSON `schema: 2`, agreement projection `pincer agreement 2`,
1052
+ agreement snapshots `schema: 2`, coverage map `schema: 1`, inventory projection
1053
+ `pincer inventory 1`, evidence snapshots (`coverage/inventory.json`,
1054
+ `coverage/map.json`) `schema: 1`, coverage JSON `schema: 1`, impact JSON `schema: 1`.
1055
+ A v0.5.0 runtime (reads change binding schema 1) and a PRD v5 runtime (reads change
1056
+ records schema 2 and attempts schema 1 and 2, validates evidence schema 1 and 2)
1057
+ refuse every new schema as `UNSUPPORTED_SCHEMA` (records, evidence) or as an
1058
+ incomplete record (`ATTEMPT_ERROR` for attempts); neither ever interprets a strict
1059
+ change through the older logic. This runtime keeps reading schema 2 records, schema 2
1060
+ attempts and schema 2 evidence exactly as documented above.
1061
+
1062
+ ### Requirement inventory
1063
+
1064
+ The PRD of a strict change is parsed into an inventory: the complete set of
1065
+ requirement and scenario definitions. The PRD prose is authoritative — the coverage
1066
+ map cannot add, rename or remove a definition — and parsing produces a computed view,
1067
+ never a second editable source. The grammar is bounded on purpose; syntax outside it
1068
+ is either ignored as prose or refused as unsupported, and no partial inventory is
1069
+ ever treated as complete.
1070
+
1071
+ - **Identifiers:** `<PREFIX>-<digits>` matching `[A-Z][A-Z0-9]{0,7}-[0-9]{1,6}` (the
1072
+ template's `R-01`/`S-01`, or a supplied PRD's `REQ-1`/`AC-12`). Requirement and
1073
+ scenario IDs share one namespace within the PRD. IDs are kept verbatim: nothing is
1074
+ renumbered, padded or generated. What makes an ID a requirement or a scenario is
1075
+ the definition form, not the prefix.
1076
+ - **Fenced code:** a line whose first non-blank characters are three or more
1077
+ backticks opens a fence; the next such line closes it; every line in between is
1078
+ ignored (no definition, no reference). A fence still open at the end of the file is
1079
+ `INVENTORY_INVALID` (`unclosed fence opened at line N`). A tilde fence line is
1080
+ `INVENTORY_INVALID` (`tilde fences are unsupported (line N)`).
1081
+ - **Tables and quotes:** a line whose first non-blank character is `|` or `>` is a
1082
+ reference context: it is prose and defines nothing.
1083
+ - **Requirement definition:** an ATX heading of level 2, 3 or 4 whose text is an ID,
1084
+ a separator (` — `, ` – `, ` - ` or `: `) and a nonempty title:
1085
+ `### R-01 — Derive the complete authored inventory`. Its section runs to the next
1086
+ requirement definition, or to the next heading whose level is less than or equal
1087
+ to its own, whichever comes first. A heading whose text starts with an ID in any
1088
+ other shape (no separator, no title, a separator with nothing after it) is
1089
+ `INVENTORY_INVALID` (`unsupported requirement definition syntax at line N; use
1090
+ "### R-NN — Title"`).
1091
+ - **Scenario definition:** a list item inside a requirement section whose text is a
1092
+ bold ID, optionally followed by a colon inside or after the bold, then nonempty
1093
+ text: `- **S-01:** A PRD with two requirements …`. Markers `-`, `+`, `*` and
1094
+ indentation are supported; an optional checkbox mark (`[ ]`, `[x]`, `[X]`) may
1095
+ precede the bold ID. Lines that follow the item, are indented by at least two
1096
+ spaces or a tab, and are not themselves list items, headings, fences or table rows
1097
+ are the item's continuation lines. A blank line does not end the item: a scenario
1098
+ written as two or more paragraphs keeps all of them, and every paragraph of it
1099
+ belongs to the scenario rather than to its requirement, so editing any of them
1100
+ changes that scenario's digest. A list item whose text begins with an ID
1101
+ followed by `:` or `.` without the bold form, or a bold ID with nothing after it,
1102
+ is `INVENTORY_INVALID` (`unsupported scenario definition syntax at line N; use
1103
+ "- **S-NN:** text"` / `empty definition S-NN at line N`).
1104
+ - **References:** an ID anywhere else — in prose, in a list item that does not start
1105
+ with the ID, in a table row, inside a fence, in a heading that is not a definition
1106
+ form because the ID is not first — defines nothing and creates no error.
1107
+ - **Membership rules, each `INVENTORY_INVALID` with the line named:** a duplicate
1108
+ definition of an ID (requirement or scenario, in any combination); a scenario
1109
+ definition outside every requirement section (`orphan scenario S-NN at line N`);
1110
+ a requirement with no scenario (`requirement R-NN at line N has no scenario`);
1111
+ an empty requirement title; a PRD with no requirement definition at all (`no
1112
+ requirement definitions`). Every scenario therefore has exactly one owning
1113
+ requirement: the section it appears in.
1114
+ - **Source locations:** each definition records `{ line, end }` (1-based, inclusive):
1115
+ the section for a requirement, the item and its continuation lines for a scenario.
1116
+ - **Normalized content:** a requirement's text is its title, then the lines of its
1117
+ section outside scenario definitions and their continuations, each right-trimmed,
1118
+ with leading and trailing blank lines removed and runs of blank lines collapsed to
1119
+ one; a scenario's text is the item text after the bold ID (marker, checkbox mark
1120
+ and bold ID removed) followed by its continuation lines with their indentation
1121
+ removed, joined by `\n`, right-trimmed. The frontmatter is not part of any
1122
+ definition, so the PRD `status` line (already excluded from `prd_revision`) never
1123
+ reaches a definition either. Checkbox marks and whitespace-only edits therefore
1124
+ preserve identity; any change to a scenario's wording changes its digest.
1125
+ - **Digests:** requirement `SHA-256("requirement <ID>\n<text>\n")`, scenario
1126
+ `SHA-256("scenario <ID>\n<text>\n")`. The inventory projection is the exact text
1127
+
1128
+ ```
1129
+ pincer inventory 1
1130
+ prd <.prd/prd-vN.md>
1131
+ requirement <ID> <digest> (one line per requirement)
1132
+ scenario <ID> <owner ID> <digest> (one line per scenario)
1133
+ ```
1134
+
1135
+ each line terminated by `\n`, requirements and scenarios each sorted by ID (prefix
1136
+ in byte order, then numerically), and the inventory digest is SHA-256 over it.
1137
+ Reordering sections changes `prd_revision` but not the inventory digest; moving a
1138
+ scenario to another requirement changes the inventory digest (the owner line) but
1139
+ not the scenario digest.
1140
+
1141
+ Example of a valid PRD body (two requirements, three scenarios; the table row, the
1142
+ prose mention and the fenced block create no definitions; the checkbox mark on
1143
+ `S-02` is normalized away):
1144
+
1145
+ ````markdown
1146
+ ### R-01 — Parse the inventory
1147
+
1148
+ Every definition is derived from the PRD prose. S-01 and S-02 belong here.
1149
+
1150
+ - **S-01:** A PRD with two requirements and three scenarios yields exactly those
1151
+ IDs, parent links, content digests and source locations.
1152
+ - [x] **S-02:** Duplicate IDs cause an actionable diagnostic.
1153
+
1154
+ | Scenario | Ticket |
1155
+ | --- | --- |
1156
+ | S-01 | T-01 |
1157
+
1158
+ ```markdown
1159
+ - **S-99:** an example inside a fence is not a definition
1160
+ ```
1161
+
1162
+ ### R-02: Report impact
1163
+
1164
+ - **S-03** — Changing one scenario names that scenario and its requirement.
1165
+ ````
1166
+
1167
+ Its projection is `pincer inventory 1`, `prd .prd/prd-v1.md`, `requirement R-01 …`,
1168
+ `requirement R-02 …`, `scenario S-01 R-01 …`, `scenario S-02 R-01 …`,
1169
+ `scenario S-03 R-02 …`. The same body with `### R-01 — Parse the inventory` repeated
1170
+ is `INVENTORY_INVALID: duplicate definition R-01 at line N`; with `- **S-03** —`
1171
+ moved above the first heading, `orphan scenario S-03 at line N`; with `- S-04: text`
1172
+ under R-02, `unsupported scenario definition syntax at line N`; with `### R-03 —`
1173
+ and no scenario, `requirement R-03 at line N has no scenario`; with the closing
1174
+ fence deleted, `unclosed fence opened at line N`.
1175
+
1176
+ A PRD that is not a strict change's PRD (legacy or migrated mode, or a schema 2
1177
+ record) is never parsed for a verdict: status, resume and `coverage` label such a
1178
+ change `coverage: unverified`, and inventory diagnostics for it are informational,
1179
+ never blockers. Old PRDs stay readable by `validate` exactly as before.
1180
+
1181
+ ### Coverage map
1182
+
1183
+ `.prd/coverage/<change-id>.json` is the authored coverage map of a strict change,
1184
+ tracked in git and edited by hand. It owns links and planned scope dispositions and
1185
+ declares the candidate checks; it never owns requirement prose or observed outcomes.
1186
+ Coverage map schema 1, with every key required and no other key allowed:
1187
+
1188
+ ```json
1189
+ {
1190
+ "schema": 1,
1191
+ "change": "prd-v2",
1192
+ "prd": ".prd/prd-v2.md",
1193
+ "scenarios": {
1194
+ "S-01": { "tickets": ["T-03"], "checks": ["C-01"] },
1195
+ "S-02": { "tickets": ["T-03", "T-04"], "checks": ["C-01", "C-02"] }
1196
+ },
1197
+ "scope": {
1198
+ "S-03": { "disposition": "deferred", "decision": "D-01", "prior": null, "note": "deferred to the next change; see D-01" },
1199
+ "S-04": { "disposition": "removed", "decision": "D-02", "prior": "G-02", "note": null }
1200
+ },
1201
+ "tickets": {
1202
+ "T-03": { "role": "implements", "rationale": null },
1203
+ "T-04": { "role": "implements", "rationale": null },
1204
+ "T-05": { "role": "enables", "rationale": "shared fixture harness used by C-01 and C-02; implements no scenario" }
1205
+ },
1206
+ "checks": {
1207
+ "C-01": { "kind": "command", "required": true, "command": "npm test", "timeout": 600, "cwd": null, "obligation": null, "note": null },
1208
+ "C-02": { "kind": "review", "required": true, "command": null, "timeout": null, "cwd": null, "obligation": "read the error-state rendering of S-02 against the mock-ups", "note": null }
1209
+ }
1210
+ }
1211
+ ```
1212
+
1213
+ The map is parsed strictly: a JSON object with a duplicate key anywhere, a file
1214
+ larger than 1 MiB, a value of the wrong type, an unknown key, a missing key, an ID
1215
+ that does not match its grammar, a string longer than 2000 characters, an array
1216
+ with a duplicate entry, `schema` other than 1, `change` other than the record's ID
1217
+ (and the filename), `prd` other than the record's PRD, or an unsafe path is
1218
+ `COVERAGE_INVALID` (exit 4) before any mutation or launch. Paths (`prd`, `cwd`) are
1219
+ repository-relative POSIX paths without `..`, `.` or empty segments; the map file,
1220
+ the PRD and a `cwd` must be regular files or directories inside the repository, not
1221
+ symbolic links and not reached through one. Whole-file rules:
1222
+
1223
+ - `scenarios` keys and `scope` keys are scenario IDs. Every scenario of the live
1224
+ inventory appears exactly once, in `scenarios` or in `scope`. A `scenarios` key that
1225
+ is not a live scenario (an invented ID, a requirement ID, a scenario that the PRD no
1226
+ longer defines) and a `scope` key that is neither a live scenario nor a `removed`
1227
+ tombstone are `COVERAGE_INCOMPLETE`, as is a live scenario missing from both.
1228
+ - A `scenarios` entry has `tickets` (nonempty array of ticket IDs) and `checks`
1229
+ (nonempty array of check IDs); every ticket named must be a ticket of this change
1230
+ with role `implements`, every check named must be declared. A ticket that does not
1231
+ exist, belongs to another PRD (`WRONG_CHANGE` is reported inside the
1232
+ `COVERAGE_INCOMPLETE` detail), is listed as `enables`, or is unlisted, and a check
1233
+ that is not declared, are `COVERAGE_INCOMPLETE` naming the scenario and the ID.
1234
+ - `tickets` lists every ticket of the change (every valid ticket whose PRD is the
1235
+ record's PRD) exactly once with `role` `implements` (referenced by at least one
1236
+ `scenarios` entry) or `enables` (referenced by none, with a nonempty `rationale`).
1237
+ An unlisted ticket, a listed ID that is not a ticket of this change, an
1238
+ `implements` ticket no scenario references and an `enables` ticket some scenario
1239
+ references are `COVERAGE_INCOMPLETE`. `rationale` is `null` for `implements`.
1240
+ - `scope` entries carry `disposition` (`deferred` or `removed`), `decision` (`D-NN`),
1241
+ `prior` (`null` for `deferred`; for `removed` the agreement ID `G-NN` of this change
1242
+ whose inventory snapshot defines the ID — the tombstone's reference to the prior
1243
+ inventory) and `note` (`null` or text). Whether the decision exists, is resolved,
1244
+ names the ID and is covered by an applicable user authorization is decided by
1245
+ "Scope dispositions", not by the map syntax.
1246
+ - `checks` declares candidate checks by stable ID `C-NN`. `kind` is `command`,
1247
+ `review` or `visual`; `required` is a boolean. A `command` check has `command`
1248
+ (nonempty, at most 2000 characters, valid Bash syntax under `bash -n`, no inline
1249
+ secret-like literal — the sanitizer's rule), `timeout` (a positive integer number of
1250
+ seconds, at most 2147483), `cwd` (`null` or a safe directory path) and `obligation`
1251
+ `null`. A `review` or `visual` check has `obligation` (nonempty text: what the
1252
+ reviewer must establish) and `command`, `timeout`, `cwd` `null`. `note` is `null`
1253
+ or text. A declared check may be referenced by several scenarios or by none; a check
1254
+ referenced by a scenario must be declared. A declaration is never an observed
1255
+ result.
1256
+ - Definition digests: a `command` check's is `SHA-256("<command>\ntimeout=<timeout>\n")`
1257
+ — exactly the check digest an attempt records, so an attempt satisfies a declaration
1258
+ only when its `check.digest` equals it; a `review`/`visual` check's is
1259
+ `SHA-256("<kind>\n<obligation>\n")`.
1260
+ - The map digest is SHA-256 over the normalized map text: the parsed object
1261
+ serialized as JSON with object keys sorted, the ID arrays (`tickets`, `checks`)
1262
+ sorted in the inventory's ID order, no insignificant whitespace, one trailing
1263
+ newline. Reformatting or reordering the file changes nothing; every value edit
1264
+ changes the digest.
1265
+
1266
+ `COVERAGE_INVALID` means the file cannot be interpreted; `COVERAGE_INCOMPLETE` means
1267
+ it is interpretable but the graph it describes is incomplete or disagrees with the
1268
+ inventory and the tickets. Both refuse execution and export; the detail names every
1269
+ affected ID. The map module exposes one validated graph (inventory, links,
1270
+ declarations, dispositions) that every consumer — coverage, impact, gates,
1271
+ completion, `check`, export, release, status and resume — reads; no consumer keeps a
1272
+ policy copy, and validation writes no file.
1273
+
1274
+ ### Strict change records
1275
+
1276
+ A strict change's record is `.prd/changes/<id>.json` with `schema: 3` and
1277
+ `runtime: 3`: every schema 2 key with the same owners and rules, plus:
1278
+
1279
+ | Field | Owner | Value |
1280
+ | --- | --- | --- |
1281
+ | `coverage` | `coverage adopt --apply` | `{ map: ".prd/coverage/<id>.json", adopted: <timestamp>, agreement: "G-NN" }` — the capability, when it was recorded, and the adoption agreement (the reviewed starting inventory) |
1282
+
1283
+ Event kind `adopt` is added: it carries `from = to =` the state at the time and
1284
+ `agreement: "G-NN"`. Exactly one `adopt` event exists in a schema 3 record, its
1285
+ `agreement` equals `coverage.agreement`, and that agreement entry carries the
1286
+ projection version 2 digests. Agreement entries in a schema 3 record carry two
1287
+ more fields, `inventory` and `coverage` (the inventory and map digests, 64 hex;
1288
+ `null` on entries recorded before adoption, which are projection version 1). A
1289
+ schema 3 record without `coverage`, with `coverage: null`, or with no `adopt` event
1290
+ is `MALFORMED` (`the strict coverage capability cannot be removed by editing the
1291
+ record; restore the backed-up schema 2 record instead`); a schema 2 record carrying a
1292
+ `coverage` key or an `adopt` event is `MALFORMED` under this runtime and under the
1293
+ PRD v5 runtime alike. Schema 2 and schema 3 records coexist in one `.prd/changes/`
1294
+ directory (each change adopts on its own); a schema 1 binding next to either is
1295
+ `INPUT_INVALID` as before. A schema 3 record read by a v0.5.0 or PRD v5 runtime is
1296
+ `UNSUPPORTED_SCHEMA`.
1297
+
1298
+ Agreement projection version 2, computed for strict changes only (schema 2 records
1299
+ keep version 1):
1300
+
1301
+ ```
1302
+ pincer agreement 2
1303
+ change <change id>
1304
+ prd <.prd/prd-vN.md> <prd_revision>
1305
+ inventory <inventory digest>
1306
+ coverage <.prd/coverage/<id>.json> <map digest>
1307
+ ticket <T-NN> <ticket_digest> (one line per ticket of the PRD, ascending numeric ID)
1308
+ decision <D-NN> <decision digest> (one line per resolved decision, ascending ID)
1309
+ ```
1310
+
1311
+ each line terminated by `\n`. The inventory digest and the map digest are agreement
1312
+ inputs, so editing a scenario's text, a link, a declared command or timeout, a
1313
+ scope disposition or a tombstone changes the agreement and invalidates the
1314
+ authorization (`AGREEMENT_CHANGED`), while the PRD `status` line, ticket lifecycle
1315
+ fields and ticket checkbox marks, attempts, generated reports and evidence do not. A
1316
+ checkbox mark on a scenario line keeps the inventory digest but is a PRD body edit
1317
+ ("Content revisions"), so it changes `prd_revision` and with it the agreement. When the
1318
+ inventory or the map cannot be read the agreement cannot be computed:
1319
+ `INVENTORY_INVALID` or `COVERAGE_INVALID` replaces `INPUT_INVALID` in every place
1320
+ the contract says the agreement is computed (a missing map is `COVERAGE_INVALID`:
1321
+ `author .prd/coverage/<id>.json first`). An incomplete map still yields an agreement:
1322
+ completeness is a coverage gate ("Phase-specific coverage"), not an input error, so a
1323
+ user may authorize an incomplete map and strengthen it later.
1324
+
1325
+ Agreement snapshot schema 2 (strict changes; `.prd/changes/<id>/agreements/G-NN.json`)
1326
+ keeps every schema 1 key and adds:
1327
+
1328
+ ```json
1329
+ { "schema": 2, "change": "prd-v2", "agreement": "G-03", "digest": "<64 hex>", "projection": "pincer agreement 2\nchange prd-v2\n…", "prd": { "path": ".prd/prd-v2.md", "revision": "<64 hex>", "text": "<normalized PRD text>" }, "inventory": { "digest": "<64 hex>", "projection": "pincer inventory 1\nprd .prd/prd-v2.md\n…", "requirements": { "R-01": { "title": "Parse the inventory", "text": "<normalized text>", "digest": "<64 hex>", "line": 40, "end": 58, "scenarios": ["S-01", "S-02"] } }, "scenarios": { "S-01": { "requirement": "R-01", "text": "<normalized text>", "digest": "<64 hex>", "line": 44, "end": 45 } } }, "coverage": { "path": ".prd/coverage/prd-v2.json", "digest": "<64 hex>", "text": "<normalized map text>" }, "tickets": { "T-03": { "file": "tickets/T-03-slug.md", "digest": "<64 hex>", "text": "<normalized ticket text>" } }, "decisions": { "D-01": { "summary": "…", "reference": "…", "excerpt": "…" } }, "recorded": "2026-09-12T08:05:00Z" }
1330
+ ```
1331
+
1332
+ `readSnapshot` recomputes the inventory projection from the snapshot's own
1333
+ requirement and scenario digests, the map digest from the normalized map text, and
1334
+ the agreement digest from the projection; any disagreement with the record entry is
1335
+ `HISTORY_INVALID`, so an old agreement's inventory and map can always be reviewed
1336
+ from the file alone. Snapshot schema 1 entries (recorded before adoption) stay valid
1337
+ in a schema 3 record and are compared as "history unavailable" for inventory purposes.
1338
+
1339
+ Attempt schema 3 (strict changes): every schema 2 field, plus `context.inventory`
1340
+ and `context.coverage`, the inventory and map digests at launch (64 hex, required).
1341
+ The runtime writes schema 3 attempts for a strict change and reads schema 1, 2 and 3
1342
+ records; readiness validates the context fields as it validates `agreement`. In a
1343
+ strict change a pointed-at schema 2 record (recorded before adoption) is
1344
+ `HISTORICAL_EVIDENCE`: it stays inspectable, never becomes current evidence, and the
1345
+ next action is `verify`. A schema 3 record read for a schema 2 change is
1346
+ `ATTEMPT_ERROR`. Context keys are unchanged. For a candidate attempt of a strict
1347
+ change the recorded `check.digest` is the declaration's digest by construction (the
1348
+ runtime builds the command from the map), and export compares it with the
1349
+ declaration current on the candidate.
1350
+
1351
+ ### Scope dispositions
1352
+
1353
+ A scope disposition records that an obligation is not delivered by this change. It
1354
+ is never inferred from a failing check, a missing ticket, a `blocked` requirement or
1355
+ free text. Two dispositions exist, both authored in the map's `scope`:
1356
+
1357
+ - `deferred`: the scenario stays defined in the PRD and is not implemented by this
1358
+ change. Requires a resolved decision `D-NN` of this change whose `summary` or
1359
+ `excerpt` names the scenario ID as a whole token.
1360
+ - `removed`: the obligation is withdrawn. The entry is a tombstone: `decision` as
1361
+ above and `prior: "G-NN"`, an agreement entry of this change whose inventory
1362
+ snapshot defines the ID. The ID may still be defined in the PRD (withdrawn but
1363
+ documented) or absent from it; in both cases the tombstone is the only way an ID
1364
+ that was once reviewed can stop being an obligation.
1365
+
1366
+ A disposition is *authorized* when its decision exists on this record, is
1367
+ `resolved`, names the ID, and an applicable user authorization covers it: an
1368
+ authorization `A-NN` of this record with disposition `user` that lists the decision
1369
+ in its `decisions`, such that an authorization binding the current agreement (any
1370
+ one the `current` verdict accepts) is `A-NN` itself or a `delegated` authorization
1371
+ whose `basis` chain reaches `A-NN`. Anything else is `SCOPE_UNAUTHORIZED` naming the ID
1372
+ and the missing element (`no decision`, `decision D-NN is open`, `decision D-NN does
1373
+ not name S-03`, `no user authorization names D-NN`, `the current authorization A-04
1374
+ does not descend from A-02`). A delegated authorization can therefore bind a
1375
+ strengthened map (a stricter command, an added check, a new link) without a new
1376
+ user instruction, and can carry a user's earlier scope decision forward through its
1377
+ basis chain, but it can never create the scope decision: `authorized_by` free text
1378
+ and `--constraints` text are never consulted. The runtime checks reference
1379
+ integrity and agreement currency; whether the user's words really support the
1380
+ disposition is a reviewer's judgment, which status and coverage label as such
1381
+ (`decision D-01 "…" (reviewer judgment: the excerpt must support the deferral)`).
1382
+
1383
+ Deleted obligations are detected against the retained history, never against the
1384
+ current files alone. The *baseline* of a strict change is the union of the
1385
+ inventories of its retained agreement snapshots — the adoption agreement and every
1386
+ agreement recorded after it, authorized or not — so an obligation that was ever
1387
+ reviewed stays one. Every scenario of the baseline that is neither defined in the
1388
+ live inventory nor a `removed` tombstone in the map is `OBLIGATION_MISSING` naming the
1389
+ IDs and the latest agreement that defined each: deleting the prose and the map row
1390
+ together erases nothing, and neither does authorizing the reduced agreement (a user
1391
+ or delegated authorization records approval of the inputs; only a decision with its
1392
+ tombstone withdraws an obligation). A first adoption can only establish its reviewed
1393
+ starting inventory: no baseline exists before the adoption agreement, so nothing
1394
+ predating it is ever reported as an omission. An
1395
+ open decision blocks execution (`DECISION_REQUIRED`) whatever the current digest is,
1396
+ so reverting the PRD and the map to an earlier authorized digest cannot bypass a
1397
+ retained open decision; and `change authorize`, `change decide --resolve` and
1398
+ `coverage adopt --apply` refuse with `AGREEMENT_CHANGED`/`STATE_CHANGED` when the
1399
+ inputs or the record changed after the operation was prepared.
1400
+
1401
+ ### Phase-specific coverage
1402
+
1403
+ Coverage is one pure computation over the validated graph (the inventory, the map,
1404
+ the record, the tickets and their readiness, the evaluation), consumed by `coverage`,
1405
+ `change complete`, `evidence export`, `ready`, status and resume. It has three
1406
+ separate fields; none implies another, and a linked check, a passing syntax check
1407
+ or a done ticket never implies candidate delivery:
1408
+
1409
+ | Field | Complete when | Blocking codes |
1410
+ | --- | --- | --- |
1411
+ | `structure` | the inventory and the map are valid; every live scenario is linked (tickets and checks) or dispositioned; every ticket is classified; every disposition is authorized; no baseline obligation is missing | `INVENTORY_INVALID`, `COVERAGE_INVALID`, `COVERAGE_INCOMPLETE`, `SCOPE_UNAUTHORIZED`, `OBLIGATION_MISSING` |
1412
+ | `implementation` | `structure` is complete and every ticket of the change is `done` and ready under the v5 rules (checked criteria; current passing schema 3 attempt of this change; no `SOURCE_CHANGED`, `CHECK_CHANGED`, `REVISION_CHANGED`, `HISTORICAL_EVIDENCE`) | the structure codes, then the ticket's readiness code prefixed by its scenario (`S-02: T-03 CHECK_FAILED …`) |
1413
+ | `candidate` | the change's latest evaluation is schema 3 evidence that validates and reconciles with the candidate; every in-scope scenario is `delivered`; `adequacy.verdict` is `adequate` | the structure codes, `EVIDENCE_MISSING`, `CANDIDATE_STALE`, `CHECK_FAILED`/`ATTEMPT_*` (a check), `REVIEW_MISSING`, `ADEQUACY_REQUIRED`, plus the v5 candidate reasons |
1414
+
1415
+ Per scenario the report carries `scope` (`in-scope`, `deferred`, `removed`),
1416
+ `implementation` (`complete`, `unfinished` — a linked ticket is not `done`,
1417
+ `unverified` — a linked ticket is done but not ready, with the code, `not applicable`
1418
+ for dispositioned scenarios) and `candidate` (`delivered`, `blocked` with the failing
1419
+ check or review, `deferred`, `removed`, `not evaluated`). `change complete` of a
1420
+ strict change requires `structure` and `implementation` complete and refuses,
1421
+ writing nothing, with the first blocking code in the table order; it never demands
1422
+ candidate evidence, which cannot exist before the candidate. `evidence export`
1423
+ requires `structure` complete. Release (`ready` without a ticket) requires
1424
+ `candidate` complete. Coverage output labels the absence of candidate evidence and
1425
+ the absence of an adequacy judgment explicitly (`candidate: not evaluated`,
1426
+ `adequacy: not recorded`) and never prints `delivered` or `release-ready` for a
1427
+ change whose evidence is missing, stale or judged inadequate.
1428
+
1429
+ ### Declared candidate checks
1430
+
1431
+ In a strict change `check C-NN --candidate <sha>` runs the declaration: the map's
1432
+ `command`, `timeout` and `cwd` for `C-NN`. A `--timeout` or a `-- <command>` on the
1433
+ command line, an ID that is not declared, and a `review`/`visual` declaration
1434
+ (recorded in the evidence draft, never run) are refused with `CHECK_UNDECLARED`
1435
+ (exit 4) before anything is prepared; an arbitrary supplied command therefore cannot
1436
+ become evidence for a declared check by reusing its ID. The old forms stay supported
1437
+ unchanged in legacy, migrated and schema 2 changes mode. The guard for strict `check`
1438
+ and `evidence export` is the v5 guard plus a valid inventory and map (gate order:
1439
+ `INPUT_INVALID`/`INVENTORY_INVALID`/`COVERAGE_INVALID`/`MALFORMED`/… first, the rest
1440
+ as documented). Under the attempt lock the guard runs again as for every attempt,
1441
+ the map is re-read and re-validated, and the declaration of `C-NN` is recomputed: a
1442
+ map or agreement change committed since the pre-launch evaluation refuses the
1443
+ attempt with that gate's code, and a changed declaration whose agreement was
1444
+ re-authorized meanwhile refuses with `CHECK_UNDECLARED` (`the declaration of C-NN
1445
+ changed since the command was prepared`) — the attempt either launches the command
1446
+ it validated against the inputs it recorded, or launches nothing and writes nothing.
1447
+ The attempt's `context.agreement`, `context.inventory` and `context.coverage` are the
1448
+ values validated under the lock. Changing a declaration stales every prior attempt
1449
+ for that check (their `check.digest` no longer equals the declaration), and a
1450
+ declaration's results are keyed by change and candidate (`candidate:<change>:<40
1451
+ hex>:<C-NN>`), so another change's `C-01` is never borrowed.
1452
+
1453
+ Review obligations (`review` and `visual` declarations) are recorded in the
1454
+ evaluation draft with an explicit `result` and at least one artifact saved under the
1455
+ candidate's evidence directory (a passed `visual` check needs an image, as in schema
1456
+ 1). A required review obligation that is missing from the draft, `unverified`,
1457
+ `failed` or without an artifact is `REVIEW_MISSING` and blocks export and release.
1458
+ Passing every command check creates no review result and no adequacy judgment.
1459
+
1460
+ ### Evidence schema 3
1461
+
1462
+ Schema 3 keeps every schema 2 field and rule and adds the complete reconciled
1463
+ coverage of the candidate:
1464
+
1465
+ - `coverage`: `{ agreement, authorization, inventory, map, snapshots: { inventory, map } }`
1466
+ — the agreement digest (projection 2) the evaluation ran under, the ID of the
1467
+ authorization that covered it (`A-NN`), the inventory and map digests, and the two
1468
+ snapshot artifacts `<evidence dir>/coverage/inventory.json` (`{ schema: 1, prd,
1469
+ digest, projection, requirements, scenarios }`, the inventory computed from the
1470
+ candidate's PRD, in the snapshot schema 2 shape) and `<evidence dir>/coverage/map.json`
1471
+ (`{ schema: 1, path, digest, map }`, the parsed map). Both are listed in
1472
+ `artifacts` with their file digests like every artifact; neither carries a digest
1473
+ of the manifest, so no digest refers to itself.
1474
+ - `scenarios`: one row per scenario of the inventory snapshot plus one per `removed`
1475
+ tombstone, `{ id, requirement, disposition, tickets, checks, decision, authorization,
1476
+ note }` with disposition `delivered`, `deferred`, `removed` or `blocked`.
1477
+ - `requirements` rows are `{ id, disposition, tickets, checks, scenarios, decision,
1478
+ authorization, note }` (`authorized_by` is gone): one per requirement of the
1479
+ inventory snapshot; `delivered` when every scenario is delivered, `deferred` or
1480
+ `removed` when every non-delivered scenario carries that disposition (`removed`
1481
+ only when all do), `blocked` otherwise.
1482
+ - `adequacy`: `{ verdict: "adequate" | "inadequate", note }`, authored in the draft:
1483
+ the reviewer's judgment that the delivered checks establish their scenarios.
1484
+ `note` is nonempty.
1485
+ - `delivery`: `{ original, agreed }` — `original` is true when every scenario of the
1486
+ inventory snapshot is `delivered`; `agreed` when every scenario is `delivered`,
1487
+ `deferred` or `removed` with an authorized disposition. A report shows both, so
1488
+ delivery with authorized scope dispositions is never presented as delivery of
1489
+ every original obligation.
1490
+ - each check row carries `declared` (the definition digest from the map snapshot).
1491
+
1492
+ Dispositions are derived, never authored: a scenario is `delivered` when every
1493
+ required check it links passed (command checks with `runtime` provenance and
1494
+ `attempt.check_digest` equal to `declared`; review checks with a passed result and a
1495
+ candidate-bound artifact), `deferred`/`removed` when its map disposition is
1496
+ authorized (the row names the decision and the user authorization), `blocked`
1497
+ otherwise. Validation of a schema 3 manifest establishes, in addition to the schema
1498
+ 2 rules: the snapshots are listed artifacts whose content recomputes to
1499
+ `coverage.inventory` and `coverage.map`; the scenario and requirement rows are
1500
+ exactly the inventory snapshot's set plus tombstones, each once, with the links the
1501
+ map snapshot gives; every check the map declares appears in `checks`, every check in
1502
+ `checks` is declared with the same kind and `required`, and `declared` equals the
1503
+ declaration; every disposition follows the rule above; `delivery` recomputes; a
1504
+ required check that did not pass, a required review obligation that is not passed
1505
+ with an artifact, a `blocked` row and an `inadequate` adequacy each make the manifest
1506
+ invalid with the reason named. When the repository is available (export, status,
1507
+ release, and `validate` run inside the repository) validation also reconciles
1508
+ independently with the committed candidate: the inventory recomputed from `git show
1509
+ <candidate>:<prd>` and the map digest from `git show <candidate>:.prd/coverage/<id>.json`
1510
+ must equal `coverage.inventory` and `coverage.map`, and `git show
1511
+ <candidate>:.prd/changes/<id>.json` must be a schema 3 record whose agreement entry
1512
+ with digest `coverage.agreement` exists and is bound by authorization
1513
+ `coverage.authorization`; a manifest whose own lists are consistent but disagree with
1514
+ the candidate is invalid (`the candidate's PRD defines S-05, which the manifest omits`).
1515
+ Outside a repository (`pincer-evidence.cjs validate` on copied files) that
1516
+ reconciliation is skipped and printed as a limitation, never claimed. A schema 3
1517
+ manifest is `UNSUPPORTED_SCHEMA`-refused by the v0.5.0 and PRD v5 validators
1518
+ (`unknown evidence schema 3`).
1519
+
1520
+ `evidence export` of a strict change reads a draft with the keys `environment`,
1521
+ `coverage_review`, `adequacy`, `checks` and `visual_review`; a draft `requirements`
1522
+ key is refused (`dispositions are derived from the map and the outcomes`). Every
1523
+ declared check appears in `checks` exactly once: a `command` entry is `{ id }` (a
1524
+ `kind`, `required` or `command` given must equal the declaration), a `review` or
1525
+ `visual` entry carries `result`, `artifacts` and the schema 1 fields; a declared
1526
+ check missing from the draft (`an unused failing required check cannot be omitted`)
1527
+ and an undeclared entry are refused naming the ID. Export requires `structure`
1528
+ complete, populates command checks from the attempts as in schema 2, writes the two
1529
+ snapshots and `checks/C-NN.log`, computes every row and `delivery`, writes
1530
+ `manifest.json`, validates it with the reconciliation above, and appends the
1531
+ locator entry (schema 1, unchanged; its `agreement` is the projection 2 digest).
1532
+ Release reads the selected strict change's lifecycle (`completed`), its verdict, the
1533
+ locator, the schema 3 manifest with reconciliation and the latest applicable
1534
+ attempts (a newer nonpassing local attempt on the same source blocks, as in schema
1535
+ 2); a fresh clone validates the saved record with the v5 provenance limit; release
1536
+ writes nothing. The post-candidate allowlist is unchanged: the snapshots are listed
1537
+ artifacts of a validated manifest and therefore followers; an unlisted file under
1538
+ the evidence directory, an altered snapshot and a malformed locator remain
1539
+ candidate changes. Two changes evaluated on one candidate keep distinct locators,
1540
+ manifests and attempt keys.
1541
+
1542
+ ### Adoption and rollback
1543
+
1544
+ `coverage adopt --preview --change <id>` prints the plan and writes nothing; it exits 0
1545
+ when apply would proceed (or when the change is already strict: `already adopted`)
1546
+ and 1 when a conflict stops it. Conflicts, all before the first write: not changes
1547
+ mode (`CHANGE_REQUIRED`/`MIGRATION_REQUIRED` — migrate first; migration never
1548
+ adopts), an unreadable record or directory, a `cancelled`/`superseded` change
1549
+ (`LIFECYCLE_BLOCKED`), a `running` attempt of the change (`ATTEMPT_RUNNING`), an
1550
+ incomplete transaction (`STATE_INCOMPLETE`), a PRD that does not parse strictly
1551
+ (`INVENTORY_INVALID`), a missing or invalid map (`COVERAGE_INVALID`), a map whose
1552
+ graph is incomplete (`COVERAGE_INCOMPLETE`: membership, classification and links must
1553
+ be complete before adoption; scope authorization is reported, not required, because
1554
+ the authorization that covers the map is recorded after adoption). The preview shows
1555
+ the inventory (counts, digest), the map digest, the agreement `G-NN` and digest apply
1556
+ would record, the attempts that become `HISTORICAL_EVIDENCE`, the backup path, and
1557
+ that adoption grants no authorization.
1558
+
1559
+ `coverage adopt --apply --change <id> [--agreement <digest>]` is one transaction:
1560
+ it recomputes the plan under the lock (refusing with the same codes), compares the
1561
+ agreement digest with `--agreement` when given (`AGREEMENT_CHANGED` when the inputs
1562
+ changed since the preview), backs up `.prd/changes/<id>.json` under
1563
+ `.pincer/backups/<UTC timestamp>/.prd/changes/<id>.json`, and stages the record as
1564
+ schema 3 (`runtime: 3`, `coverage: { map, adopted, agreement }`, the agreement entry
1565
+ `G-NN` with `inventory` and `coverage` digests, the `adopt` event) together with the
1566
+ snapshot `.prd/changes/<id>/agreements/G-NN.json` (schema 2). Nothing else changes:
1567
+ tickets, attempts, the index, the selection, the locator and the map are untouched;
1568
+ no authorization is created, inferred from the record's history, or copied from an
1569
+ earlier one — the user's instruction covering the new agreement is recorded
1570
+ afterwards with `change authorize` (user) or `--delegated --basis A-NN`, and until
1571
+ then the verdict is `AGREEMENT_CHANGED` (or `AUTHORIZATION_REQUIRED`). Repeated apply
1572
+ reports `already adopted` and writes nothing. A process killed during apply leaves
1573
+ the schema 2 record or the schema 3 record with its event and snapshot (`recover`
1574
+ completes a committed apply). Existing attempts of the change are history after
1575
+ adoption (`HISTORICAL_EVIDENCE`) until verified again.
1576
+
1577
+ Rollback from adoption restores the backed-up `.prd/changes/<id>.json` — it
1578
+ overwrites the schema 3 record at the same path, so nothing under `.prd/changes/` is
1579
+ deleted; the adoption snapshot `.prd/changes/<id>/agreements/G-NN.json` is no longer
1580
+ referenced by the restored record and may be removed or left in place. The map
1581
+ (authored), `.pincer/runtime/` (attempts, index, selection) and the evidence are kept.
1582
+ The change is a schema 2 record again with its history through the last
1583
+ pre-adoption event; attempts recorded as schema 3 during the strict period are then
1584
+ `ATTEMPT_ERROR` for that change until verified again. The migration rollbacks above
1585
+ are unchanged.
1586
+
1587
+ ### Coverage and impact commands
1588
+
1589
+ | Command | Arguments | Writes | Notes |
1590
+ | --- | --- | --- | --- |
1591
+ | `coverage` | `[--change <id>] [--json]` | nothing | the phase-specific coverage report of the selected (or named) change; exit 0 when the report was computed (complete or not), 4 when the inputs cannot be read |
1592
+ | `impact` | `[--change <id>] [--from G-NN \| A-NN] [--json]` | nothing | structural differences between the current authored inputs and a retained agreement; exit 0 when computed (`unchanged`, `changed` or `unavailable`), 4 on invalid input |
1593
+ | `coverage scaffold` | `--change <id> [--json]` | nothing | the read-only coverage draft ("Coverage draft"); exit 0 when a draft was produced, 4 when the inputs cannot be read |
1594
+ | `coverage adopt` | `--preview \| --apply --change <id> [--agreement <digest>]` | apply: the backup, the schema 3 record, the adoption snapshot | preview writes nothing; exit 0 / 1 (conflict) / 4 (invalid state) |
1595
+ | `check` | `C-NN --candidate <sha>` (strict) | an attempt | the declared command; `--timeout` and `-- <command>` are `CHECK_UNDECLARED` in a strict change |
1596
+
1597
+ `coverage` and `impact` launch no check, record no approval, change no selection and
1598
+ write no file; repeated runs are byte-identical apart from `generated`. Their human
1599
+ output and their JSON name the same IDs, codes and next action.
1600
+
1601
+ ### Coverage draft
1602
+
1603
+ `coverage scaffold --change <id> [--json]` projects the validated inventory, the
1604
+ change's tickets and any authored map into one reviewable **draft**. It exists to
1605
+ remove transcription, not judgment: authoring `.prd/coverage/<id>.json` means copying
1606
+ every live scenario, every ticket role and every check declaration out of documents
1607
+ the runtime has already parsed, and that copying is all this removes.
1608
+
1609
+ The draft **decides nothing**. It never invents a link, a check command, a ticket role
1610
+ or a scope disposition; an entry it cannot resolve from authored content stays
1611
+ `unresolved`. A ticket's own `Implements:`/`Scenarios:` claim and its Verification
1612
+ text are reported under `candidates` with the ticket's file, as material to read —
1613
+ never promoted into `scenarios` or `checks`. IDs on those lines that the inventory
1614
+ does not define are not reported at all, because a ticket naming other tickets is not
1615
+ a scenario claim.
1616
+
1617
+ Draft envelope — `draft: 1`, and deliberately **no `schema` key**, so `readMap`
1618
+ refuses it with `COVERAGE_INVALID: unsupported coverage map schema undefined`:
1619
+
1620
+ ```
1621
+ { draft: 1, kind: "coverage-draft", change, prd,
1622
+ inventory: { digest, requirements, scenarios },
1623
+ authored: { map: <path> | null, digest | null },
1624
+ scenarios: { "S-NN": { requirement, state, tickets: [], checks: [] } },
1625
+ scope: { "S-NN": { disposition, decision, prior, note, state } },
1626
+ tickets: { "T-NN": { role, rationale, state } },
1627
+ checks: { "C-NN": { kind, required, command, timeout, cwd, obligation, note, state } },
1628
+ candidates: { tickets: { "T-NN": { file, objective, implements: [], scenarios: [], verification } } },
1629
+ unresolved: [ { code, id, detail } ],
1630
+ next: { action, command } }
1631
+ ```
1632
+
1633
+ `state` is `authored` for content read from the existing map and `unresolved`
1634
+ otherwise. Every live inventory scenario appears exactly once across `scenarios` and
1635
+ `scope`. Authored rows the inventory no longer defines are **preserved and flagged**
1636
+ (`SCENARIO_STALE`), never dropped: removing an obligation is a decision with its own
1637
+ disposition and authorization. Unresolved codes are `SCENARIO_UNLINKED`,
1638
+ `CHECK_UNDECLARED`, `TICKET_UNCLASSIFIED`, `SCENARIO_STALE` and `TICKET_FOREIGN`.
1639
+
1640
+ The draft body carries no timestamp, so two calls on identical authored inputs are
1641
+ byte-identical. Scaffolding writes no file, launches no check, changes no selection,
1642
+ adopts nothing and records no approval; a malformed, unsupported, symlinked or
1643
+ out-of-root map is refused with exit 4 before any draft is printed, because silently
1644
+ dropping authored content is the one failure this command must not have.
1645
+
1646
+ A draft is **not a coverage map and confers no readiness**. The route is unchanged:
1647
+ author `.prd/coverage/<id>.json` by hand, review the links, `coverage` validates it,
1648
+ `coverage adopt --preview|--apply` adopts it, and `change authorize` records the
1649
+ user's instruction covering the new agreement. Editing the inputs afterwards still
1650
+ yields `AGREEMENT_CHANGED`.
1651
+
1652
+ Coverage JSON schema 1:
1653
+
1654
+ ```
1655
+ { schema: 1, runtime: 3, generated, root, mode, change: <id> | null,
1656
+ strict: boolean, label: "strict" | "unverified", reason: <why unverified> | null,
1657
+ inventory: { digest, requirements: [ { id, title, line, end, scenarios: [ids] } ], scenarios: [ { id, requirement, line, end } ] } | null,
1658
+ map: { path, digest, checks: [ { id, kind, required, declared } ] } | null,
1659
+ agreement: { current, reviewed: { agreement: "G-NN", authorization: "A-NN" | null, digest } | null, verdict },
1660
+ baseline: { agreement: "G-NN", authorization: "A-NN" | null, inventory: <digest> } | null,
1661
+ structure: { complete, problems: [ { code, detail, ids: [] } ] },
1662
+ implementation: { complete, scenarios: { "<id>": { scope, implementation, tickets: [ { id, status, ready, code } ], detail } } },
1663
+ candidate: { evaluated, candidate, manifest, delivery: { original, agreed } | null, adequacy: { verdict, note } | null,
1664
+ scenarios: { "<id>": { disposition, checks: [ { id, kind, required, result } ], detail } } } | null,
1665
+ blockers: [ { code, detail } ], next: { action, command, ticket | check | decision | null } }
1666
+ ```
1667
+
1668
+ Impact JSON schema 1 (the baseline is `--from`, else the latest authorization's
1669
+ agreement, else the latest retained agreement with an inventory snapshot):
1670
+
1671
+ ```
1672
+ { schema: 1, runtime: 3, generated, root, change,
1673
+ baseline: { agreement: "G-NN", authorization: "A-NN" | null, recorded, digest } | null,
1674
+ current: { digest, inventory, coverage },
1675
+ verdict: "unchanged" | "changed" | "unavailable", reason: <text> | null,
1676
+ requirements: { added: [ids], removed: [ids], changed: [ { id, parts: ["title" | "text" | "scenarios"] } ], unchanged: [ids] },
1677
+ scenarios: { added: [ids], removed: [ { id, tombstone: boolean } ], changed: [ { id, parts: ["text" | "requirement"] } ], unchanged: [ids] },
1678
+ links: { added: [ids], removed: [ids], changed: [ { id, tickets: { added, removed }, checks: { added, removed } } ] },
1679
+ scope: { added: [ { id, disposition } ], removed: [ids], changed: [ { id, from, to } ] },
1680
+ checks: { added: [ids], removed: [ids], changed: [ { id, parts: ["kind" | "command" | "timeout" | "cwd" | "obligation" | "required"] } ] },
1681
+ tickets: { added, removed, changed: [ { id, parts } ] },
1682
+ affected: { scenarios: [ { id, because: [reasons] } ], tickets: [ { id, because: [reasons] } ], checks: [ { id, because: [reasons] } ],
1683
+ dependents: [ { id, via: "T-NN", because: "depends_on" } ] },
1684
+ unscoped: { prd: boolean, detail } ,
1685
+ freshness: { note: "a narrow impact is not permission to reuse evidence whose source identity changed" } }
1686
+ ```
1687
+
1688
+ `affected` names every scenario whose text, owner, links or linked declarations
1689
+ changed, and every ticket and check linked from one of them, with the reason each is
1690
+ included; `dependents` lists tickets that `depends_on` an affected ticket, separately
1691
+ from direct links. `unscoped.prd` is true when `prd_revision` changed while every
1692
+ definition is unchanged (a constraint, the scope table, an architecture note): the
1693
+ verdict is then `changed` with the detail `unscoped PRD change requiring review`,
1694
+ never `unchanged`. `unavailable` is reported with its reason when the baseline
1695
+ agreement has no inventory snapshot (a pre-adoption or schema 1 snapshot, or none at
1696
+ all), when `--from` names a missing entry, or when the snapshot is `HISTORY_INVALID`;
1697
+ the runtime never reports "no impact" for history it cannot read. Impact is
1698
+ structural: it never judges semantics, and it does not touch evidence freshness —
1699
+ `SOURCE_CHANGED` keeps invalidating attempts by whole-source identity regardless of
1700
+ how narrow the report is.
1701
+
1702
+ Status JSON schema 3 (changes mode) keeps every schema 2 field and adds
1703
+ `coverage`: `{ strict, label, reason, structure: { complete, problems }, implementation:
1704
+ { complete, scenarios: { total, complete, unfinished, unverified, dispositioned } },
1705
+ candidate: { evaluated, delivery, adequacy } | null, next }`; legacy and migrated
1706
+ mode status JSON stays schema 1 with `coverage: { strict: false, label: "unverified",
1707
+ reason }`. The human status prints one `Coverage` line: `Coverage strict · agreement
1708
+ G-03 (A-02) · structure complete · implementation 4/6 scenarios · candidate not
1709
+ evaluated` or `Coverage unverified · strict coverage not adopted (node
1710
+ scripts/pincer-runtime.cjs coverage adopt --preview --change <id>)`. Resume JSON
1711
+ schema 2 keeps every schema 1 field and adds the same `coverage` object; its
1712
+ next-action precedence gains the coverage codes inside rule 4 ("agreement, decision
1713
+ or coverage gap": `COVERAGE_INVALID` → repair the map, `COVERAGE_INCOMPLETE` → author
1714
+ the missing rows, `OBLIGATION_MISSING` → restore the obligation or record the decision
1715
+ and tombstone, `SCOPE_UNAUTHORIZED` → `change decide --resolve` / `change authorize`),
1716
+ and rules 7 and 8 read the candidate coverage (`REVIEW_MISSING`, `ADEQUACY_REQUIRED`
1717
+ → `/pincer-evaluate`). The v5 blocker precedence is otherwise unchanged. Routine
1718
+ resume never records or requests authorization: it prints the exact `change
1719
+ authorize` command when and only when the verdict is not `current`.
1720
+
1721
+ A compact PRD (one requirement, one or two scenarios) with a map of one or two
1722
+ declarations passes the same validation; nothing requires an authored traceability
1723
+ table beyond the map. Playbooks author the map once (during `/pincer-narrow`) and
1724
+ read `coverage`/`impact` afterwards; a changed scope is dispositioned (decision,
1725
+ tombstone, authorization) before approval is recorded, and a generic "continue" is
1726
+ never an authorization of revised scope.
1727
+
421
1728
  ## Legacy compatibility
422
1729
 
423
1730
  For an unmigrated project nothing changes: the ticket and status commands keep their
424
1731
  names, arguments, output lines and diagnostics; receipts stay in the ticket; `done`
425
1732
  re-runs the check; `.pincer/` is never created; schema 1 evidence validates. The only
426
1733
  additions are the `Runtime legacy …` status line, the exit code 4 for invalid input,
427
- and the `--json` form of status. A project that already carries `.pincer/` from a
428
- migrated worktree is migrated; it cannot be half in each mode.
1734
+ and the `--json` form of status. A migrated (schema 1 binding) project keeps the
1735
+ v0.5.0 behavior in full until `migrate --apply`; its `register --replace` is the one
1736
+ command that now refuses, with `MIGRATION_REQUIRED`. A project that already carries
1737
+ `.pincer/` from a migrated worktree is migrated; it cannot be half in each mode. A
1738
+ schema 2 record that cannot be read never turns the project legacy or migrated.
1739
+
1740
+ ## Worktrees
1741
+
1742
+ Change records, agreement snapshots and evaluation locators are tracked files: each
1743
+ worktree sees the versions committed on its branch, and a lifecycle event committed
1744
+ in one worktree reaches another only through git. Local state (`.pincer/runtime/`,
1745
+ including the selection, attempts and the lock) belongs to one worktree and is never
1746
+ shared or copied: selecting a change in one worktree changes nothing in another, and
1747
+ attempts recorded in one are unavailable in the other (`EVIDENCE_MISSING`, the same as a
1748
+ fresh clone). The lock serializes writers inside one worktree only; two worktrees can
1749
+ each activate a different change on different branches, and the runtime does not
1750
+ claim to prevent two developers from working on the same change independently. When
1751
+ two branches carry divergent histories of the same record, the merge is reconciled by
1752
+ hand before execution: the merged file must satisfy the sequence and projection
1753
+ rules, or it is `HISTORY_INVALID`.
429
1754
 
430
1755
  ## Platform limits
431
1756
 
432
1757
  The check runner is a POSIX contract: `bash` in `PATH`, process groups
433
1758
  (`detached: true`, `kill(-pid)`), `SIGTERM`/`SIGKILL`. Native Windows is not
434
1759
  supported and not claimed. The CI matrix (`.github/workflows/ci.yml`: ubuntu and macOS
435
- × Node 18 and 22) is the target surface; a release claims only the runs it can cite,
436
- and any platform outside the matrix is untested. Sandbox and approval controls of the
1760
+ × Node 22 and 24) is the target surface; a release claims only the runs it can cite,
1761
+ and any platform outside the matrix is untested. Node 22 is the floor (`engines`)
1762
+ because 18 and 20 are past end of life, not because of any output defect: until
1763
+ T-79 a command whose stdout or stderr outgrew one pipe buffer lost the tail and
1764
+ still exited 0, on every version alike — the boundary was the pipe buffer, 65,536
1765
+ bytes, where 65,536 arrived and 65,537 did not. Every write now goes to the file
1766
+ descriptor synchronously (`scripts/pincer-runtime/io.cjs`), so a report is
1767
+ complete when the command exits however it is consumed; `test/runtime-output.test.js`
1768
+ compares a piped report against the same report redirected to a file. Sandbox and approval controls of the
437
1769
  host stay in force; the runtime never bypasses them.