pincer-workflow 0.5.0 → 0.6.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 (58) hide show
  1. package/README.md +6 -6
  2. package/bin/pincer.js +17 -1
  3. package/package.json +3 -3
  4. package/template/.agents/skills/pincer-code/SKILL.md +85 -14
  5. package/template/.agents/skills/pincer-evaluate/SKILL.md +42 -13
  6. package/template/.agents/skills/pincer-narrow/SKILL.md +43 -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 +12 -2
  10. package/template/.claude/commands/pincer-code.md +85 -14
  11. package/template/.claude/commands/pincer-evaluate.md +42 -13
  12. package/template/.claude/commands/pincer-narrow.md +43 -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 +12 -2
  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 +85 -14
  20. package/template/.github/prompts/pincer-evaluate.prompt.md +42 -13
  21. package/template/.github/prompts/pincer-narrow.prompt.md +43 -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 +12 -2
  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 +1342 -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 +9 -1
  48. package/template/scripts/pincer-runtime/requirements.cjs +255 -0
  49. package/template/scripts/pincer-runtime/resume.cjs +205 -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/state.cjs +29 -7
  53. package/template/scripts/pincer-runtime/status.cjs +178 -22
  54. package/template/scripts/pincer-runtime/transaction.cjs +200 -0
  55. package/template/scripts/pincer-runtime/transitions.cjs +134 -0
  56. package/template/scripts/pincer-runtime.cjs +387 -76
  57. package/template/scripts/pincer-status.sh +1 -1
  58. 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,833 @@ 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>] [--json]` is read-only inspection of the selected (or named)
897
+ 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
+
396
941
  ## Migration and rollback
397
942
 
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
943
+ `migrate --preview --prd .prd/prd-vN.md` prints the plan and writes nothing. It exits 0
944
+ when apply would proceed and 1 when a conflict would stop it. Conflicts (all fail
945
+ closed, before the first write): malformed or unsupported records, records mixing
946
+ schemas, a ticket of another PRD with the same ID, a PRD owned by another schema 2
947
+ record, an incomplete transaction. Two sources are migrated:
948
+
949
+ - from legacy: each ticket of that PRD whose `verified`/`last_check` lines would be
950
+ removed and recorded as `legacy.receipts`, the `.gitignore` line, the schema 2
951
+ record (`planned`, event 1 `migrate`, `legacy.migrated_from: "legacy"`), the local
952
+ selection it sets in this worktree;
953
+ - from a schema 1 binding: the record it becomes (same change ID, `prd`, `base` and
954
+ `registered`; `legacy.receipts` carried over; the binding's free-text `authorization`
955
+ stored as `legacy.authorization_text`; `migrated_from: "binding"`), the local index
956
+ pointers under `candidate:<sha>:<C-NN>` dropped — a schema 1 attempt record's own key
957
+ never carries the change segment, so no such pointer could resolve after the migration
958
+ and the check is simply run again; the attempt records themselves are untouched and
959
+ stay as history — and the selection it sets.
960
+
961
+ In both cases the preview states that the migrated change is `planned` with no
962
+ authorization, that its existing attempts and evaluations stay as history
963
+ (`HISTORICAL_EVIDENCE`) until verified again, and that `--authorization <text>` is
964
+ retained as unvalidated text only. An actual earlier instruction of the user is recorded
965
+ afterwards with `change authorize`, without asking the user again. A project without
966
+ receipts or binding needs no migration: `register` writes a schema 2 record directly.
967
+
968
+ `migrate --apply` is one transaction: it backs up every authored file it changes
969
+ (tickets, `.gitignore`, the schema 1 binding) under
970
+ `.pincer/backups/<UTC timestamp>/<original path>`, stages the rewritten tickets,
971
+ `.gitignore`, the record, the selection and the index, and commits them together, so
972
+ an interruption leaves either the unmigrated project or the migrated one (`recover`
973
+ completes a committed apply). Repeated apply reports `already migrated` and changes
974
+ nothing; a partially applied v0.5.0 migration (binding present, receipts remaining)
975
+ is completed in the same apply. Imported receipts are history: a migrated done ticket
976
+ reports `HISTORICAL_EVIDENCE` or `LEGACY_RECEIPT` until a runtime attempt exists,
977
+ without changing `finished`. Installation and update deploy the runtime files and
978
+ `doctor` reports when a migration is available; neither migrates.
979
+
980
+ Rollback is one procedure per migration source; the backups are byte-identical
981
+ originals, and neither procedure deletes a file it has just restored.
982
+
983
+ - Rollback from legacy: restore the backed-up tickets and `.gitignore` from
984
+ `.pincer/backups/<timestamp>/`; delete the schema 2 record `.prd/changes/<id>.json`
985
+ and, if present, its snapshot directory `.prd/changes/<id>/`; remove `.pincer/` (a
986
+ legacy project has no runtime state to keep). The project is legacy again with its
987
+ original receipts.
988
+ - Rollback from a v0.5.0 binding: restore the backed-up binding to
989
+ `.prd/changes/<id>.json` — it overwrites the schema 2 record, which lives at the same
990
+ path, so nothing under `.prd/changes/` is deleted except, if present, the snapshot
991
+ directory `.prd/changes/<id>/`; restore the backed-up `.pincer/runtime/index.json`
992
+ (the migration dropped its candidate pointers) and remove
993
+ `.pincer/runtime/selection.json`; keep the rest of `.pincer/runtime/` so the old
994
+ attempts and stored manifests remain. The project is migrated (v0.5.0) again with
995
+ its original binding and history.
996
+
997
+ The backup directory can be removed afterwards. An older runtime does not enforce the
419
998
  runtime guarantees: it will accept the restored receipts as it did before.
420
999
 
1000
+ ## Strict coverage
1001
+
1002
+ Strict coverage (PRD v6) is an explicit, retained capability of one change: the
1003
+ runtime derives the complete requirement and scenario inventory from the change's
1004
+ PRD, validates one authored coverage map against it, binds both into the reviewed
1005
+ agreement, derives structural, implementation and candidate coverage from that graph,
1006
+ explains structural impact, executes candidate checks from their declarations, and
1007
+ reconciles the exported evidence with the committed candidate's authored inputs.
1008
+ Nothing switches a change into strict coverage implicitly: `coverage adopt --apply`
1009
+ is the only entry, it is recorded in the change record (schema 3), and a project or
1010
+ change without it keeps the documented behavior above with its coverage labeled
1011
+ `unverified`. A report never implies that strict coverage was established where it
1012
+ was not adopted. Semantic adequacy — whether a check really establishes its scenario —
1013
+ remains a named reviewer judgment (`adequacy`) and is never computed.
1014
+
1015
+ Versions frozen by this section: change records `schema: 3` (`runtime: 3`), attempts
1016
+ `schema: 3` (`runtime: 3`), evidence manifests `schema: 3`, status JSON `schema: 3`
1017
+ (changes mode), resume JSON `schema: 2`, agreement projection `pincer agreement 2`,
1018
+ agreement snapshots `schema: 2`, coverage map `schema: 1`, inventory projection
1019
+ `pincer inventory 1`, evidence snapshots (`coverage/inventory.json`,
1020
+ `coverage/map.json`) `schema: 1`, coverage JSON `schema: 1`, impact JSON `schema: 1`.
1021
+ A v0.5.0 runtime (reads change binding schema 1) and a PRD v5 runtime (reads change
1022
+ records schema 2 and attempts schema 1 and 2, validates evidence schema 1 and 2)
1023
+ refuse every new schema as `UNSUPPORTED_SCHEMA` (records, evidence) or as an
1024
+ incomplete record (`ATTEMPT_ERROR` for attempts); neither ever interprets a strict
1025
+ change through the older logic. This runtime keeps reading schema 2 records, schema 2
1026
+ attempts and schema 2 evidence exactly as documented above.
1027
+
1028
+ ### Requirement inventory
1029
+
1030
+ The PRD of a strict change is parsed into an inventory: the complete set of
1031
+ requirement and scenario definitions. The PRD prose is authoritative — the coverage
1032
+ map cannot add, rename or remove a definition — and parsing produces a computed view,
1033
+ never a second editable source. The grammar is bounded on purpose; syntax outside it
1034
+ is either ignored as prose or refused as unsupported, and no partial inventory is
1035
+ ever treated as complete.
1036
+
1037
+ - **Identifiers:** `<PREFIX>-<digits>` matching `[A-Z][A-Z0-9]{0,7}-[0-9]{1,6}` (the
1038
+ template's `R-01`/`S-01`, or a supplied PRD's `REQ-1`/`AC-12`). Requirement and
1039
+ scenario IDs share one namespace within the PRD. IDs are kept verbatim: nothing is
1040
+ renumbered, padded or generated. What makes an ID a requirement or a scenario is
1041
+ the definition form, not the prefix.
1042
+ - **Fenced code:** a line whose first non-blank characters are three or more
1043
+ backticks opens a fence; the next such line closes it; every line in between is
1044
+ ignored (no definition, no reference). A fence still open at the end of the file is
1045
+ `INVENTORY_INVALID` (`unclosed fence opened at line N`). A tilde fence line is
1046
+ `INVENTORY_INVALID` (`tilde fences are unsupported (line N)`).
1047
+ - **Tables and quotes:** a line whose first non-blank character is `|` or `>` is a
1048
+ reference context: it is prose and defines nothing.
1049
+ - **Requirement definition:** an ATX heading of level 2, 3 or 4 whose text is an ID,
1050
+ a separator (` — `, ` – `, ` - ` or `: `) and a nonempty title:
1051
+ `### R-01 — Derive the complete authored inventory`. Its section runs to the next
1052
+ requirement definition, or to the next heading whose level is less than or equal
1053
+ to its own, whichever comes first. A heading whose text starts with an ID in any
1054
+ other shape (no separator, no title, a separator with nothing after it) is
1055
+ `INVENTORY_INVALID` (`unsupported requirement definition syntax at line N; use
1056
+ "### R-NN — Title"`).
1057
+ - **Scenario definition:** a list item inside a requirement section whose text is a
1058
+ bold ID, optionally followed by a colon inside or after the bold, then nonempty
1059
+ text: `- **S-01:** A PRD with two requirements …`. Markers `-`, `+`, `*` and
1060
+ indentation are supported; an optional checkbox mark (`[ ]`, `[x]`, `[X]`) may
1061
+ precede the bold ID. Lines that follow the item, are indented by at least two
1062
+ spaces or a tab, and are not themselves list items, headings, fences or table rows
1063
+ are the item's continuation lines. A blank line does not end the item: a scenario
1064
+ written as two or more paragraphs keeps all of them, and every paragraph of it
1065
+ belongs to the scenario rather than to its requirement, so editing any of them
1066
+ changes that scenario's digest. A list item whose text begins with an ID
1067
+ followed by `:` or `.` without the bold form, or a bold ID with nothing after it,
1068
+ is `INVENTORY_INVALID` (`unsupported scenario definition syntax at line N; use
1069
+ "- **S-NN:** text"` / `empty definition S-NN at line N`).
1070
+ - **References:** an ID anywhere else — in prose, in a list item that does not start
1071
+ with the ID, in a table row, inside a fence, in a heading that is not a definition
1072
+ form because the ID is not first — defines nothing and creates no error.
1073
+ - **Membership rules, each `INVENTORY_INVALID` with the line named:** a duplicate
1074
+ definition of an ID (requirement or scenario, in any combination); a scenario
1075
+ definition outside every requirement section (`orphan scenario S-NN at line N`);
1076
+ a requirement with no scenario (`requirement R-NN at line N has no scenario`);
1077
+ an empty requirement title; a PRD with no requirement definition at all (`no
1078
+ requirement definitions`). Every scenario therefore has exactly one owning
1079
+ requirement: the section it appears in.
1080
+ - **Source locations:** each definition records `{ line, end }` (1-based, inclusive):
1081
+ the section for a requirement, the item and its continuation lines for a scenario.
1082
+ - **Normalized content:** a requirement's text is its title, then the lines of its
1083
+ section outside scenario definitions and their continuations, each right-trimmed,
1084
+ with leading and trailing blank lines removed and runs of blank lines collapsed to
1085
+ one; a scenario's text is the item text after the bold ID (marker, checkbox mark
1086
+ and bold ID removed) followed by its continuation lines with their indentation
1087
+ removed, joined by `\n`, right-trimmed. The frontmatter is not part of any
1088
+ definition, so the PRD `status` line (already excluded from `prd_revision`) never
1089
+ reaches a definition either. Checkbox marks and whitespace-only edits therefore
1090
+ preserve identity; any change to a scenario's wording changes its digest.
1091
+ - **Digests:** requirement `SHA-256("requirement <ID>\n<text>\n")`, scenario
1092
+ `SHA-256("scenario <ID>\n<text>\n")`. The inventory projection is the exact text
1093
+
1094
+ ```
1095
+ pincer inventory 1
1096
+ prd <.prd/prd-vN.md>
1097
+ requirement <ID> <digest> (one line per requirement)
1098
+ scenario <ID> <owner ID> <digest> (one line per scenario)
1099
+ ```
1100
+
1101
+ each line terminated by `\n`, requirements and scenarios each sorted by ID (prefix
1102
+ in byte order, then numerically), and the inventory digest is SHA-256 over it.
1103
+ Reordering sections changes `prd_revision` but not the inventory digest; moving a
1104
+ scenario to another requirement changes the inventory digest (the owner line) but
1105
+ not the scenario digest.
1106
+
1107
+ Example of a valid PRD body (two requirements, three scenarios; the table row, the
1108
+ prose mention and the fenced block create no definitions; the checkbox mark on
1109
+ `S-02` is normalized away):
1110
+
1111
+ ````markdown
1112
+ ### R-01 — Parse the inventory
1113
+
1114
+ Every definition is derived from the PRD prose. S-01 and S-02 belong here.
1115
+
1116
+ - **S-01:** A PRD with two requirements and three scenarios yields exactly those
1117
+ IDs, parent links, content digests and source locations.
1118
+ - [x] **S-02:** Duplicate IDs cause an actionable diagnostic.
1119
+
1120
+ | Scenario | Ticket |
1121
+ | --- | --- |
1122
+ | S-01 | T-01 |
1123
+
1124
+ ```markdown
1125
+ - **S-99:** an example inside a fence is not a definition
1126
+ ```
1127
+
1128
+ ### R-02: Report impact
1129
+
1130
+ - **S-03** — Changing one scenario names that scenario and its requirement.
1131
+ ````
1132
+
1133
+ Its projection is `pincer inventory 1`, `prd .prd/prd-v1.md`, `requirement R-01 …`,
1134
+ `requirement R-02 …`, `scenario S-01 R-01 …`, `scenario S-02 R-01 …`,
1135
+ `scenario S-03 R-02 …`. The same body with `### R-01 — Parse the inventory` repeated
1136
+ is `INVENTORY_INVALID: duplicate definition R-01 at line N`; with `- **S-03** —`
1137
+ moved above the first heading, `orphan scenario S-03 at line N`; with `- S-04: text`
1138
+ under R-02, `unsupported scenario definition syntax at line N`; with `### R-03 —`
1139
+ and no scenario, `requirement R-03 at line N has no scenario`; with the closing
1140
+ fence deleted, `unclosed fence opened at line N`.
1141
+
1142
+ A PRD that is not a strict change's PRD (legacy or migrated mode, or a schema 2
1143
+ record) is never parsed for a verdict: status, resume and `coverage` label such a
1144
+ change `coverage: unverified`, and inventory diagnostics for it are informational,
1145
+ never blockers. Old PRDs stay readable by `validate` exactly as before.
1146
+
1147
+ ### Coverage map
1148
+
1149
+ `.prd/coverage/<change-id>.json` is the authored coverage map of a strict change,
1150
+ tracked in git and edited by hand. It owns links and planned scope dispositions and
1151
+ declares the candidate checks; it never owns requirement prose or observed outcomes.
1152
+ Coverage map schema 1, with every key required and no other key allowed:
1153
+
1154
+ ```json
1155
+ {
1156
+ "schema": 1,
1157
+ "change": "prd-v2",
1158
+ "prd": ".prd/prd-v2.md",
1159
+ "scenarios": {
1160
+ "S-01": { "tickets": ["T-03"], "checks": ["C-01"] },
1161
+ "S-02": { "tickets": ["T-03", "T-04"], "checks": ["C-01", "C-02"] }
1162
+ },
1163
+ "scope": {
1164
+ "S-03": { "disposition": "deferred", "decision": "D-01", "prior": null, "note": "deferred to the next change; see D-01" },
1165
+ "S-04": { "disposition": "removed", "decision": "D-02", "prior": "G-02", "note": null }
1166
+ },
1167
+ "tickets": {
1168
+ "T-03": { "role": "implements", "rationale": null },
1169
+ "T-04": { "role": "implements", "rationale": null },
1170
+ "T-05": { "role": "enables", "rationale": "shared fixture harness used by C-01 and C-02; implements no scenario" }
1171
+ },
1172
+ "checks": {
1173
+ "C-01": { "kind": "command", "required": true, "command": "npm test", "timeout": 600, "cwd": null, "obligation": null, "note": null },
1174
+ "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 }
1175
+ }
1176
+ }
1177
+ ```
1178
+
1179
+ The map is parsed strictly: a JSON object with a duplicate key anywhere, a file
1180
+ larger than 1 MiB, a value of the wrong type, an unknown key, a missing key, an ID
1181
+ that does not match its grammar, a string longer than 2000 characters, an array
1182
+ with a duplicate entry, `schema` other than 1, `change` other than the record's ID
1183
+ (and the filename), `prd` other than the record's PRD, or an unsafe path is
1184
+ `COVERAGE_INVALID` (exit 4) before any mutation or launch. Paths (`prd`, `cwd`) are
1185
+ repository-relative POSIX paths without `..`, `.` or empty segments; the map file,
1186
+ the PRD and a `cwd` must be regular files or directories inside the repository, not
1187
+ symbolic links and not reached through one. Whole-file rules:
1188
+
1189
+ - `scenarios` keys and `scope` keys are scenario IDs. Every scenario of the live
1190
+ inventory appears exactly once, in `scenarios` or in `scope`. A `scenarios` key that
1191
+ is not a live scenario (an invented ID, a requirement ID, a scenario that the PRD no
1192
+ longer defines) and a `scope` key that is neither a live scenario nor a `removed`
1193
+ tombstone are `COVERAGE_INCOMPLETE`, as is a live scenario missing from both.
1194
+ - A `scenarios` entry has `tickets` (nonempty array of ticket IDs) and `checks`
1195
+ (nonempty array of check IDs); every ticket named must be a ticket of this change
1196
+ with role `implements`, every check named must be declared. A ticket that does not
1197
+ exist, belongs to another PRD (`WRONG_CHANGE` is reported inside the
1198
+ `COVERAGE_INCOMPLETE` detail), is listed as `enables`, or is unlisted, and a check
1199
+ that is not declared, are `COVERAGE_INCOMPLETE` naming the scenario and the ID.
1200
+ - `tickets` lists every ticket of the change (every valid ticket whose PRD is the
1201
+ record's PRD) exactly once with `role` `implements` (referenced by at least one
1202
+ `scenarios` entry) or `enables` (referenced by none, with a nonempty `rationale`).
1203
+ An unlisted ticket, a listed ID that is not a ticket of this change, an
1204
+ `implements` ticket no scenario references and an `enables` ticket some scenario
1205
+ references are `COVERAGE_INCOMPLETE`. `rationale` is `null` for `implements`.
1206
+ - `scope` entries carry `disposition` (`deferred` or `removed`), `decision` (`D-NN`),
1207
+ `prior` (`null` for `deferred`; for `removed` the agreement ID `G-NN` of this change
1208
+ whose inventory snapshot defines the ID — the tombstone's reference to the prior
1209
+ inventory) and `note` (`null` or text). Whether the decision exists, is resolved,
1210
+ names the ID and is covered by an applicable user authorization is decided by
1211
+ "Scope dispositions", not by the map syntax.
1212
+ - `checks` declares candidate checks by stable ID `C-NN`. `kind` is `command`,
1213
+ `review` or `visual`; `required` is a boolean. A `command` check has `command`
1214
+ (nonempty, at most 2000 characters, valid Bash syntax under `bash -n`, no inline
1215
+ secret-like literal — the sanitizer's rule), `timeout` (a positive integer number of
1216
+ seconds, at most 2147483), `cwd` (`null` or a safe directory path) and `obligation`
1217
+ `null`. A `review` or `visual` check has `obligation` (nonempty text: what the
1218
+ reviewer must establish) and `command`, `timeout`, `cwd` `null`. `note` is `null`
1219
+ or text. A declared check may be referenced by several scenarios or by none; a check
1220
+ referenced by a scenario must be declared. A declaration is never an observed
1221
+ result.
1222
+ - Definition digests: a `command` check's is `SHA-256("<command>\ntimeout=<timeout>\n")`
1223
+ — exactly the check digest an attempt records, so an attempt satisfies a declaration
1224
+ only when its `check.digest` equals it; a `review`/`visual` check's is
1225
+ `SHA-256("<kind>\n<obligation>\n")`.
1226
+ - The map digest is SHA-256 over the normalized map text: the parsed object
1227
+ serialized as JSON with object keys sorted, the ID arrays (`tickets`, `checks`)
1228
+ sorted in the inventory's ID order, no insignificant whitespace, one trailing
1229
+ newline. Reformatting or reordering the file changes nothing; every value edit
1230
+ changes the digest.
1231
+
1232
+ `COVERAGE_INVALID` means the file cannot be interpreted; `COVERAGE_INCOMPLETE` means
1233
+ it is interpretable but the graph it describes is incomplete or disagrees with the
1234
+ inventory and the tickets. Both refuse execution and export; the detail names every
1235
+ affected ID. The map module exposes one validated graph (inventory, links,
1236
+ declarations, dispositions) that every consumer — coverage, impact, gates,
1237
+ completion, `check`, export, release, status and resume — reads; no consumer keeps a
1238
+ policy copy, and validation writes no file.
1239
+
1240
+ ### Strict change records
1241
+
1242
+ A strict change's record is `.prd/changes/<id>.json` with `schema: 3` and
1243
+ `runtime: 3`: every schema 2 key with the same owners and rules, plus:
1244
+
1245
+ | Field | Owner | Value |
1246
+ | --- | --- | --- |
1247
+ | `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) |
1248
+
1249
+ Event kind `adopt` is added: it carries `from = to =` the state at the time and
1250
+ `agreement: "G-NN"`. Exactly one `adopt` event exists in a schema 3 record, its
1251
+ `agreement` equals `coverage.agreement`, and that agreement entry carries the
1252
+ projection version 2 digests. Agreement entries in a schema 3 record carry two
1253
+ more fields, `inventory` and `coverage` (the inventory and map digests, 64 hex;
1254
+ `null` on entries recorded before adoption, which are projection version 1). A
1255
+ schema 3 record without `coverage`, with `coverage: null`, or with no `adopt` event
1256
+ is `MALFORMED` (`the strict coverage capability cannot be removed by editing the
1257
+ record; restore the backed-up schema 2 record instead`); a schema 2 record carrying a
1258
+ `coverage` key or an `adopt` event is `MALFORMED` under this runtime and under the
1259
+ PRD v5 runtime alike. Schema 2 and schema 3 records coexist in one `.prd/changes/`
1260
+ directory (each change adopts on its own); a schema 1 binding next to either is
1261
+ `INPUT_INVALID` as before. A schema 3 record read by a v0.5.0 or PRD v5 runtime is
1262
+ `UNSUPPORTED_SCHEMA`.
1263
+
1264
+ Agreement projection version 2, computed for strict changes only (schema 2 records
1265
+ keep version 1):
1266
+
1267
+ ```
1268
+ pincer agreement 2
1269
+ change <change id>
1270
+ prd <.prd/prd-vN.md> <prd_revision>
1271
+ inventory <inventory digest>
1272
+ coverage <.prd/coverage/<id>.json> <map digest>
1273
+ ticket <T-NN> <ticket_digest> (one line per ticket of the PRD, ascending numeric ID)
1274
+ decision <D-NN> <decision digest> (one line per resolved decision, ascending ID)
1275
+ ```
1276
+
1277
+ each line terminated by `\n`. The inventory digest and the map digest are agreement
1278
+ inputs, so editing a scenario's text, a link, a declared command or timeout, a
1279
+ scope disposition or a tombstone changes the agreement and invalidates the
1280
+ authorization (`AGREEMENT_CHANGED`), while the PRD `status` line, ticket lifecycle
1281
+ fields and ticket checkbox marks, attempts, generated reports and evidence do not. A
1282
+ checkbox mark on a scenario line keeps the inventory digest but is a PRD body edit
1283
+ ("Content revisions"), so it changes `prd_revision` and with it the agreement. When the
1284
+ inventory or the map cannot be read the agreement cannot be computed:
1285
+ `INVENTORY_INVALID` or `COVERAGE_INVALID` replaces `INPUT_INVALID` in every place
1286
+ the contract says the agreement is computed (a missing map is `COVERAGE_INVALID`:
1287
+ `author .prd/coverage/<id>.json first`). An incomplete map still yields an agreement:
1288
+ completeness is a coverage gate ("Phase-specific coverage"), not an input error, so a
1289
+ user may authorize an incomplete map and strengthen it later.
1290
+
1291
+ Agreement snapshot schema 2 (strict changes; `.prd/changes/<id>/agreements/G-NN.json`)
1292
+ keeps every schema 1 key and adds:
1293
+
1294
+ ```json
1295
+ { "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" }
1296
+ ```
1297
+
1298
+ `readSnapshot` recomputes the inventory projection from the snapshot's own
1299
+ requirement and scenario digests, the map digest from the normalized map text, and
1300
+ the agreement digest from the projection; any disagreement with the record entry is
1301
+ `HISTORY_INVALID`, so an old agreement's inventory and map can always be reviewed
1302
+ from the file alone. Snapshot schema 1 entries (recorded before adoption) stay valid
1303
+ in a schema 3 record and are compared as "history unavailable" for inventory purposes.
1304
+
1305
+ Attempt schema 3 (strict changes): every schema 2 field, plus `context.inventory`
1306
+ and `context.coverage`, the inventory and map digests at launch (64 hex, required).
1307
+ The runtime writes schema 3 attempts for a strict change and reads schema 1, 2 and 3
1308
+ records; readiness validates the context fields as it validates `agreement`. In a
1309
+ strict change a pointed-at schema 2 record (recorded before adoption) is
1310
+ `HISTORICAL_EVIDENCE`: it stays inspectable, never becomes current evidence, and the
1311
+ next action is `verify`. A schema 3 record read for a schema 2 change is
1312
+ `ATTEMPT_ERROR`. Context keys are unchanged. For a candidate attempt of a strict
1313
+ change the recorded `check.digest` is the declaration's digest by construction (the
1314
+ runtime builds the command from the map), and export compares it with the
1315
+ declaration current on the candidate.
1316
+
1317
+ ### Scope dispositions
1318
+
1319
+ A scope disposition records that an obligation is not delivered by this change. It
1320
+ is never inferred from a failing check, a missing ticket, a `blocked` requirement or
1321
+ free text. Two dispositions exist, both authored in the map's `scope`:
1322
+
1323
+ - `deferred`: the scenario stays defined in the PRD and is not implemented by this
1324
+ change. Requires a resolved decision `D-NN` of this change whose `summary` or
1325
+ `excerpt` names the scenario ID as a whole token.
1326
+ - `removed`: the obligation is withdrawn. The entry is a tombstone: `decision` as
1327
+ above and `prior: "G-NN"`, an agreement entry of this change whose inventory
1328
+ snapshot defines the ID. The ID may still be defined in the PRD (withdrawn but
1329
+ documented) or absent from it; in both cases the tombstone is the only way an ID
1330
+ that was once reviewed can stop being an obligation.
1331
+
1332
+ A disposition is *authorized* when its decision exists on this record, is
1333
+ `resolved`, names the ID, and an applicable user authorization covers it: an
1334
+ authorization `A-NN` of this record with disposition `user` that lists the decision
1335
+ in its `decisions`, such that an authorization binding the current agreement (any
1336
+ one the `current` verdict accepts) is `A-NN` itself or a `delegated` authorization
1337
+ whose `basis` chain reaches `A-NN`. Anything else is `SCOPE_UNAUTHORIZED` naming the ID
1338
+ and the missing element (`no decision`, `decision D-NN is open`, `decision D-NN does
1339
+ not name S-03`, `no user authorization names D-NN`, `the current authorization A-04
1340
+ does not descend from A-02`). A delegated authorization can therefore bind a
1341
+ strengthened map (a stricter command, an added check, a new link) without a new
1342
+ user instruction, and can carry a user's earlier scope decision forward through its
1343
+ basis chain, but it can never create the scope decision: `authorized_by` free text
1344
+ and `--constraints` text are never consulted. The runtime checks reference
1345
+ integrity and agreement currency; whether the user's words really support the
1346
+ disposition is a reviewer's judgment, which status and coverage label as such
1347
+ (`decision D-01 "…" (reviewer judgment: the excerpt must support the deferral)`).
1348
+
1349
+ Deleted obligations are detected against the retained history, never against the
1350
+ current files alone. The *baseline* of a strict change is the union of the
1351
+ inventories of its retained agreement snapshots — the adoption agreement and every
1352
+ agreement recorded after it, authorized or not — so an obligation that was ever
1353
+ reviewed stays one. Every scenario of the baseline that is neither defined in the
1354
+ live inventory nor a `removed` tombstone in the map is `OBLIGATION_MISSING` naming the
1355
+ IDs and the latest agreement that defined each: deleting the prose and the map row
1356
+ together erases nothing, and neither does authorizing the reduced agreement (a user
1357
+ or delegated authorization records approval of the inputs; only a decision with its
1358
+ tombstone withdraws an obligation). A first adoption can only establish its reviewed
1359
+ starting inventory: no baseline exists before the adoption agreement, so nothing
1360
+ predating it is ever reported as an omission. An
1361
+ open decision blocks execution (`DECISION_REQUIRED`) whatever the current digest is,
1362
+ so reverting the PRD and the map to an earlier authorized digest cannot bypass a
1363
+ retained open decision; and `change authorize`, `change decide --resolve` and
1364
+ `coverage adopt --apply` refuse with `AGREEMENT_CHANGED`/`STATE_CHANGED` when the
1365
+ inputs or the record changed after the operation was prepared.
1366
+
1367
+ ### Phase-specific coverage
1368
+
1369
+ Coverage is one pure computation over the validated graph (the inventory, the map,
1370
+ the record, the tickets and their readiness, the evaluation), consumed by `coverage`,
1371
+ `change complete`, `evidence export`, `ready`, status and resume. It has three
1372
+ separate fields; none implies another, and a linked check, a passing syntax check
1373
+ or a done ticket never implies candidate delivery:
1374
+
1375
+ | Field | Complete when | Blocking codes |
1376
+ | --- | --- | --- |
1377
+ | `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` |
1378
+ | `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 …`) |
1379
+ | `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 |
1380
+
1381
+ Per scenario the report carries `scope` (`in-scope`, `deferred`, `removed`),
1382
+ `implementation` (`complete`, `unfinished` — a linked ticket is not `done`,
1383
+ `unverified` — a linked ticket is done but not ready, with the code, `not applicable`
1384
+ for dispositioned scenarios) and `candidate` (`delivered`, `blocked` with the failing
1385
+ check or review, `deferred`, `removed`, `not evaluated`). `change complete` of a
1386
+ strict change requires `structure` and `implementation` complete and refuses,
1387
+ writing nothing, with the first blocking code in the table order; it never demands
1388
+ candidate evidence, which cannot exist before the candidate. `evidence export`
1389
+ requires `structure` complete. Release (`ready` without a ticket) requires
1390
+ `candidate` complete. Coverage output labels the absence of candidate evidence and
1391
+ the absence of an adequacy judgment explicitly (`candidate: not evaluated`,
1392
+ `adequacy: not recorded`) and never prints `delivered` or `release-ready` for a
1393
+ change whose evidence is missing, stale or judged inadequate.
1394
+
1395
+ ### Declared candidate checks
1396
+
1397
+ In a strict change `check C-NN --candidate <sha>` runs the declaration: the map's
1398
+ `command`, `timeout` and `cwd` for `C-NN`. A `--timeout` or a `-- <command>` on the
1399
+ command line, an ID that is not declared, and a `review`/`visual` declaration
1400
+ (recorded in the evidence draft, never run) are refused with `CHECK_UNDECLARED`
1401
+ (exit 4) before anything is prepared; an arbitrary supplied command therefore cannot
1402
+ become evidence for a declared check by reusing its ID. The old forms stay supported
1403
+ unchanged in legacy, migrated and schema 2 changes mode. The guard for strict `check`
1404
+ and `evidence export` is the v5 guard plus a valid inventory and map (gate order:
1405
+ `INPUT_INVALID`/`INVENTORY_INVALID`/`COVERAGE_INVALID`/`MALFORMED`/… first, the rest
1406
+ as documented). Under the attempt lock the guard runs again as for every attempt,
1407
+ the map is re-read and re-validated, and the declaration of `C-NN` is recomputed: a
1408
+ map or agreement change committed since the pre-launch evaluation refuses the
1409
+ attempt with that gate's code, and a changed declaration whose agreement was
1410
+ re-authorized meanwhile refuses with `CHECK_UNDECLARED` (`the declaration of C-NN
1411
+ changed since the command was prepared`) — the attempt either launches the command
1412
+ it validated against the inputs it recorded, or launches nothing and writes nothing.
1413
+ The attempt's `context.agreement`, `context.inventory` and `context.coverage` are the
1414
+ values validated under the lock. Changing a declaration stales every prior attempt
1415
+ for that check (their `check.digest` no longer equals the declaration), and a
1416
+ declaration's results are keyed by change and candidate (`candidate:<change>:<40
1417
+ hex>:<C-NN>`), so another change's `C-01` is never borrowed.
1418
+
1419
+ Review obligations (`review` and `visual` declarations) are recorded in the
1420
+ evaluation draft with an explicit `result` and at least one artifact saved under the
1421
+ candidate's evidence directory (a passed `visual` check needs an image, as in schema
1422
+ 1). A required review obligation that is missing from the draft, `unverified`,
1423
+ `failed` or without an artifact is `REVIEW_MISSING` and blocks export and release.
1424
+ Passing every command check creates no review result and no adequacy judgment.
1425
+
1426
+ ### Evidence schema 3
1427
+
1428
+ Schema 3 keeps every schema 2 field and rule and adds the complete reconciled
1429
+ coverage of the candidate:
1430
+
1431
+ - `coverage`: `{ agreement, authorization, inventory, map, snapshots: { inventory, map } }`
1432
+ — the agreement digest (projection 2) the evaluation ran under, the ID of the
1433
+ authorization that covered it (`A-NN`), the inventory and map digests, and the two
1434
+ snapshot artifacts `<evidence dir>/coverage/inventory.json` (`{ schema: 1, prd,
1435
+ digest, projection, requirements, scenarios }`, the inventory computed from the
1436
+ candidate's PRD, in the snapshot schema 2 shape) and `<evidence dir>/coverage/map.json`
1437
+ (`{ schema: 1, path, digest, map }`, the parsed map). Both are listed in
1438
+ `artifacts` with their file digests like every artifact; neither carries a digest
1439
+ of the manifest, so no digest refers to itself.
1440
+ - `scenarios`: one row per scenario of the inventory snapshot plus one per `removed`
1441
+ tombstone, `{ id, requirement, disposition, tickets, checks, decision, authorization,
1442
+ note }` with disposition `delivered`, `deferred`, `removed` or `blocked`.
1443
+ - `requirements` rows are `{ id, disposition, tickets, checks, scenarios, decision,
1444
+ authorization, note }` (`authorized_by` is gone): one per requirement of the
1445
+ inventory snapshot; `delivered` when every scenario is delivered, `deferred` or
1446
+ `removed` when every non-delivered scenario carries that disposition (`removed`
1447
+ only when all do), `blocked` otherwise.
1448
+ - `adequacy`: `{ verdict: "adequate" | "inadequate", note }`, authored in the draft:
1449
+ the reviewer's judgment that the delivered checks establish their scenarios.
1450
+ `note` is nonempty.
1451
+ - `delivery`: `{ original, agreed }` — `original` is true when every scenario of the
1452
+ inventory snapshot is `delivered`; `agreed` when every scenario is `delivered`,
1453
+ `deferred` or `removed` with an authorized disposition. A report shows both, so
1454
+ delivery with authorized scope dispositions is never presented as delivery of
1455
+ every original obligation.
1456
+ - each check row carries `declared` (the definition digest from the map snapshot).
1457
+
1458
+ Dispositions are derived, never authored: a scenario is `delivered` when every
1459
+ required check it links passed (command checks with `runtime` provenance and
1460
+ `attempt.check_digest` equal to `declared`; review checks with a passed result and a
1461
+ candidate-bound artifact), `deferred`/`removed` when its map disposition is
1462
+ authorized (the row names the decision and the user authorization), `blocked`
1463
+ otherwise. Validation of a schema 3 manifest establishes, in addition to the schema
1464
+ 2 rules: the snapshots are listed artifacts whose content recomputes to
1465
+ `coverage.inventory` and `coverage.map`; the scenario and requirement rows are
1466
+ exactly the inventory snapshot's set plus tombstones, each once, with the links the
1467
+ map snapshot gives; every check the map declares appears in `checks`, every check in
1468
+ `checks` is declared with the same kind and `required`, and `declared` equals the
1469
+ declaration; every disposition follows the rule above; `delivery` recomputes; a
1470
+ required check that did not pass, a required review obligation that is not passed
1471
+ with an artifact, a `blocked` row and an `inadequate` adequacy each make the manifest
1472
+ invalid with the reason named. When the repository is available (export, status,
1473
+ release, and `validate` run inside the repository) validation also reconciles
1474
+ independently with the committed candidate: the inventory recomputed from `git show
1475
+ <candidate>:<prd>` and the map digest from `git show <candidate>:.prd/coverage/<id>.json`
1476
+ must equal `coverage.inventory` and `coverage.map`, and `git show
1477
+ <candidate>:.prd/changes/<id>.json` must be a schema 3 record whose agreement entry
1478
+ with digest `coverage.agreement` exists and is bound by authorization
1479
+ `coverage.authorization`; a manifest whose own lists are consistent but disagree with
1480
+ the candidate is invalid (`the candidate's PRD defines S-05, which the manifest omits`).
1481
+ Outside a repository (`pincer-evidence.cjs validate` on copied files) that
1482
+ reconciliation is skipped and printed as a limitation, never claimed. A schema 3
1483
+ manifest is `UNSUPPORTED_SCHEMA`-refused by the v0.5.0 and PRD v5 validators
1484
+ (`unknown evidence schema 3`).
1485
+
1486
+ `evidence export` of a strict change reads a draft with the keys `environment`,
1487
+ `coverage_review`, `adequacy`, `checks` and `visual_review`; a draft `requirements`
1488
+ key is refused (`dispositions are derived from the map and the outcomes`). Every
1489
+ declared check appears in `checks` exactly once: a `command` entry is `{ id }` (a
1490
+ `kind`, `required` or `command` given must equal the declaration), a `review` or
1491
+ `visual` entry carries `result`, `artifacts` and the schema 1 fields; a declared
1492
+ check missing from the draft (`an unused failing required check cannot be omitted`)
1493
+ and an undeclared entry are refused naming the ID. Export requires `structure`
1494
+ complete, populates command checks from the attempts as in schema 2, writes the two
1495
+ snapshots and `checks/C-NN.log`, computes every row and `delivery`, writes
1496
+ `manifest.json`, validates it with the reconciliation above, and appends the
1497
+ locator entry (schema 1, unchanged; its `agreement` is the projection 2 digest).
1498
+ Release reads the selected strict change's lifecycle (`completed`), its verdict, the
1499
+ locator, the schema 3 manifest with reconciliation and the latest applicable
1500
+ attempts (a newer nonpassing local attempt on the same source blocks, as in schema
1501
+ 2); a fresh clone validates the saved record with the v5 provenance limit; release
1502
+ writes nothing. The post-candidate allowlist is unchanged: the snapshots are listed
1503
+ artifacts of a validated manifest and therefore followers; an unlisted file under
1504
+ the evidence directory, an altered snapshot and a malformed locator remain
1505
+ candidate changes. Two changes evaluated on one candidate keep distinct locators,
1506
+ manifests and attempt keys.
1507
+
1508
+ ### Adoption and rollback
1509
+
1510
+ `coverage adopt --preview --change <id>` prints the plan and writes nothing; it exits 0
1511
+ when apply would proceed (or when the change is already strict: `already adopted`)
1512
+ and 1 when a conflict stops it. Conflicts, all before the first write: not changes
1513
+ mode (`CHANGE_REQUIRED`/`MIGRATION_REQUIRED` — migrate first; migration never
1514
+ adopts), an unreadable record or directory, a `cancelled`/`superseded` change
1515
+ (`LIFECYCLE_BLOCKED`), a `running` attempt of the change (`ATTEMPT_RUNNING`), an
1516
+ incomplete transaction (`STATE_INCOMPLETE`), a PRD that does not parse strictly
1517
+ (`INVENTORY_INVALID`), a missing or invalid map (`COVERAGE_INVALID`), a map whose
1518
+ graph is incomplete (`COVERAGE_INCOMPLETE`: membership, classification and links must
1519
+ be complete before adoption; scope authorization is reported, not required, because
1520
+ the authorization that covers the map is recorded after adoption). The preview shows
1521
+ the inventory (counts, digest), the map digest, the agreement `G-NN` and digest apply
1522
+ would record, the attempts that become `HISTORICAL_EVIDENCE`, the backup path, and
1523
+ that adoption grants no authorization.
1524
+
1525
+ `coverage adopt --apply --change <id> [--agreement <digest>]` is one transaction:
1526
+ it recomputes the plan under the lock (refusing with the same codes), compares the
1527
+ agreement digest with `--agreement` when given (`AGREEMENT_CHANGED` when the inputs
1528
+ changed since the preview), backs up `.prd/changes/<id>.json` under
1529
+ `.pincer/backups/<UTC timestamp>/.prd/changes/<id>.json`, and stages the record as
1530
+ schema 3 (`runtime: 3`, `coverage: { map, adopted, agreement }`, the agreement entry
1531
+ `G-NN` with `inventory` and `coverage` digests, the `adopt` event) together with the
1532
+ snapshot `.prd/changes/<id>/agreements/G-NN.json` (schema 2). Nothing else changes:
1533
+ tickets, attempts, the index, the selection, the locator and the map are untouched;
1534
+ no authorization is created, inferred from the record's history, or copied from an
1535
+ earlier one — the user's instruction covering the new agreement is recorded
1536
+ afterwards with `change authorize` (user) or `--delegated --basis A-NN`, and until
1537
+ then the verdict is `AGREEMENT_CHANGED` (or `AUTHORIZATION_REQUIRED`). Repeated apply
1538
+ reports `already adopted` and writes nothing. A process killed during apply leaves
1539
+ the schema 2 record or the schema 3 record with its event and snapshot (`recover`
1540
+ completes a committed apply). Existing attempts of the change are history after
1541
+ adoption (`HISTORICAL_EVIDENCE`) until verified again.
1542
+
1543
+ Rollback from adoption restores the backed-up `.prd/changes/<id>.json` — it
1544
+ overwrites the schema 3 record at the same path, so nothing under `.prd/changes/` is
1545
+ deleted; the adoption snapshot `.prd/changes/<id>/agreements/G-NN.json` is no longer
1546
+ referenced by the restored record and may be removed or left in place. The map
1547
+ (authored), `.pincer/runtime/` (attempts, index, selection) and the evidence are kept.
1548
+ The change is a schema 2 record again with its history through the last
1549
+ pre-adoption event; attempts recorded as schema 3 during the strict period are then
1550
+ `ATTEMPT_ERROR` for that change until verified again. The migration rollbacks above
1551
+ are unchanged.
1552
+
1553
+ ### Coverage and impact commands
1554
+
1555
+ | Command | Arguments | Writes | Notes |
1556
+ | --- | --- | --- | --- |
1557
+ | `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 |
1558
+ | `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 |
1559
+ | `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) |
1560
+ | `check` | `C-NN --candidate <sha>` (strict) | an attempt | the declared command; `--timeout` and `-- <command>` are `CHECK_UNDECLARED` in a strict change |
1561
+
1562
+ `coverage` and `impact` launch no check, record no approval, change no selection and
1563
+ write no file; repeated runs are byte-identical apart from `generated`. Their human
1564
+ output and their JSON name the same IDs, codes and next action.
1565
+
1566
+ Coverage JSON schema 1:
1567
+
1568
+ ```
1569
+ { schema: 1, runtime: 3, generated, root, mode, change: <id> | null,
1570
+ strict: boolean, label: "strict" | "unverified", reason: <why unverified> | null,
1571
+ inventory: { digest, requirements: [ { id, title, line, end, scenarios: [ids] } ], scenarios: [ { id, requirement, line, end } ] } | null,
1572
+ map: { path, digest, checks: [ { id, kind, required, declared } ] } | null,
1573
+ agreement: { current, reviewed: { agreement: "G-NN", authorization: "A-NN" | null, digest } | null, verdict },
1574
+ baseline: { agreement: "G-NN", authorization: "A-NN" | null, inventory: <digest> } | null,
1575
+ structure: { complete, problems: [ { code, detail, ids: [] } ] },
1576
+ implementation: { complete, scenarios: { "<id>": { scope, implementation, tickets: [ { id, status, ready, code } ], detail } } },
1577
+ candidate: { evaluated, candidate, manifest, delivery: { original, agreed } | null, adequacy: { verdict, note } | null,
1578
+ scenarios: { "<id>": { disposition, checks: [ { id, kind, required, result } ], detail } } } | null,
1579
+ blockers: [ { code, detail } ], next: { action, command, ticket | check | decision | null } }
1580
+ ```
1581
+
1582
+ Impact JSON schema 1 (the baseline is `--from`, else the latest authorization's
1583
+ agreement, else the latest retained agreement with an inventory snapshot):
1584
+
1585
+ ```
1586
+ { schema: 1, runtime: 3, generated, root, change,
1587
+ baseline: { agreement: "G-NN", authorization: "A-NN" | null, recorded, digest } | null,
1588
+ current: { digest, inventory, coverage },
1589
+ verdict: "unchanged" | "changed" | "unavailable", reason: <text> | null,
1590
+ requirements: { added: [ids], removed: [ids], changed: [ { id, parts: ["title" | "text" | "scenarios"] } ], unchanged: [ids] },
1591
+ scenarios: { added: [ids], removed: [ { id, tombstone: boolean } ], changed: [ { id, parts: ["text" | "requirement"] } ], unchanged: [ids] },
1592
+ links: { added: [ids], removed: [ids], changed: [ { id, tickets: { added, removed }, checks: { added, removed } } ] },
1593
+ scope: { added: [ { id, disposition } ], removed: [ids], changed: [ { id, from, to } ] },
1594
+ checks: { added: [ids], removed: [ids], changed: [ { id, parts: ["kind" | "command" | "timeout" | "cwd" | "obligation" | "required"] } ] },
1595
+ tickets: { added, removed, changed: [ { id, parts } ] },
1596
+ affected: { scenarios: [ { id, because: [reasons] } ], tickets: [ { id, because: [reasons] } ], checks: [ { id, because: [reasons] } ],
1597
+ dependents: [ { id, via: "T-NN", because: "depends_on" } ] },
1598
+ unscoped: { prd: boolean, detail } ,
1599
+ freshness: { note: "a narrow impact is not permission to reuse evidence whose source identity changed" } }
1600
+ ```
1601
+
1602
+ `affected` names every scenario whose text, owner, links or linked declarations
1603
+ changed, and every ticket and check linked from one of them, with the reason each is
1604
+ included; `dependents` lists tickets that `depends_on` an affected ticket, separately
1605
+ from direct links. `unscoped.prd` is true when `prd_revision` changed while every
1606
+ definition is unchanged (a constraint, the scope table, an architecture note): the
1607
+ verdict is then `changed` with the detail `unscoped PRD change requiring review`,
1608
+ never `unchanged`. `unavailable` is reported with its reason when the baseline
1609
+ agreement has no inventory snapshot (a pre-adoption or schema 1 snapshot, or none at
1610
+ all), when `--from` names a missing entry, or when the snapshot is `HISTORY_INVALID`;
1611
+ the runtime never reports "no impact" for history it cannot read. Impact is
1612
+ structural: it never judges semantics, and it does not touch evidence freshness —
1613
+ `SOURCE_CHANGED` keeps invalidating attempts by whole-source identity regardless of
1614
+ how narrow the report is.
1615
+
1616
+ Status JSON schema 3 (changes mode) keeps every schema 2 field and adds
1617
+ `coverage`: `{ strict, label, reason, structure: { complete, problems }, implementation:
1618
+ { complete, scenarios: { total, complete, unfinished, unverified, dispositioned } },
1619
+ candidate: { evaluated, delivery, adequacy } | null, next }`; legacy and migrated
1620
+ mode status JSON stays schema 1 with `coverage: { strict: false, label: "unverified",
1621
+ reason }`. The human status prints one `Coverage` line: `Coverage strict · agreement
1622
+ G-03 (A-02) · structure complete · implementation 4/6 scenarios · candidate not
1623
+ evaluated` or `Coverage unverified · strict coverage not adopted (node
1624
+ scripts/pincer-runtime.cjs coverage adopt --preview --change <id>)`. Resume JSON
1625
+ schema 2 keeps every schema 1 field and adds the same `coverage` object; its
1626
+ next-action precedence gains the coverage codes inside rule 4 ("agreement, decision
1627
+ or coverage gap": `COVERAGE_INVALID` → repair the map, `COVERAGE_INCOMPLETE` → author
1628
+ the missing rows, `OBLIGATION_MISSING` → restore the obligation or record the decision
1629
+ and tombstone, `SCOPE_UNAUTHORIZED` → `change decide --resolve` / `change authorize`),
1630
+ and rules 7 and 8 read the candidate coverage (`REVIEW_MISSING`, `ADEQUACY_REQUIRED`
1631
+ → `/pincer-evaluate`). The v5 blocker precedence is otherwise unchanged. Routine
1632
+ resume never records or requests authorization: it prints the exact `change
1633
+ authorize` command when and only when the verdict is not `current`.
1634
+
1635
+ A compact PRD (one requirement, one or two scenarios) with a map of one or two
1636
+ declarations passes the same validation; nothing requires an authored traceability
1637
+ table beyond the map. Playbooks author the map once (during `/pincer-narrow`) and
1638
+ read `coverage`/`impact` afterwards; a changed scope is dispositioned (decision,
1639
+ tombstone, authorization) before approval is recorded, and a generic "continue" is
1640
+ never an authorization of revised scope.
1641
+
421
1642
  ## Legacy compatibility
422
1643
 
423
1644
  For an unmigrated project nothing changes: the ticket and status commands keep their
424
1645
  names, arguments, output lines and diagnostics; receipts stay in the ticket; `done`
425
1646
  re-runs the check; `.pincer/` is never created; schema 1 evidence validates. The only
426
1647
  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.
1648
+ and the `--json` form of status. A migrated (schema 1 binding) project keeps the
1649
+ v0.5.0 behavior in full until `migrate --apply`; its `register --replace` is the one
1650
+ command that now refuses, with `MIGRATION_REQUIRED`. A project that already carries
1651
+ `.pincer/` from a migrated worktree is migrated; it cannot be half in each mode. A
1652
+ schema 2 record that cannot be read never turns the project legacy or migrated.
1653
+
1654
+ ## Worktrees
1655
+
1656
+ Change records, agreement snapshots and evaluation locators are tracked files: each
1657
+ worktree sees the versions committed on its branch, and a lifecycle event committed
1658
+ in one worktree reaches another only through git. Local state (`.pincer/runtime/`,
1659
+ including the selection, attempts and the lock) belongs to one worktree and is never
1660
+ shared or copied: selecting a change in one worktree changes nothing in another, and
1661
+ attempts recorded in one are unavailable in the other (`EVIDENCE_MISSING`, the same as a
1662
+ fresh clone). The lock serializes writers inside one worktree only; two worktrees can
1663
+ each activate a different change on different branches, and the runtime does not
1664
+ claim to prevent two developers from working on the same change independently. When
1665
+ two branches carry divergent histories of the same record, the merge is reconciled by
1666
+ hand before execution: the merged file must satisfy the sequence and projection
1667
+ rules, or it is `HISTORY_INVALID`.
429
1668
 
430
1669
  ## Platform limits
431
1670
 
432
1671
  The check runner is a POSIX contract: `bash` in `PATH`, process groups
433
1672
  (`detached: true`, `kill(-pid)`), `SIGTERM`/`SIGKILL`. Native Windows is not
434
1673
  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
1674
+ × Node 22 and 24) is the target surface; a release claims only the runs it can cite,
1675
+ and any platform outside the matrix is untested. Node 22 is the floor (`engines`)
1676
+ because 18 and 20 are past end of life, not because of any output defect: until
1677
+ T-79 a command whose stdout or stderr outgrew one pipe buffer lost the tail and
1678
+ still exited 0, on every version alike — the boundary was the pipe buffer, 65,536
1679
+ bytes, where 65,536 arrived and 65,537 did not. Every write now goes to the file
1680
+ descriptor synchronously (`scripts/pincer-runtime/io.cjs`), so a report is
1681
+ complete when the command exits however it is consumed; `test/runtime-output.test.js`
1682
+ compares a piped report against the same report redirected to a file. Sandbox and approval controls of the
437
1683
  host stay in force; the runtime never bypasses them.