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.
- package/README.md +6 -6
- package/bin/pincer.js +17 -1
- package/package.json +3 -3
- package/template/.agents/skills/pincer-code/SKILL.md +85 -14
- package/template/.agents/skills/pincer-evaluate/SKILL.md +42 -13
- package/template/.agents/skills/pincer-narrow/SKILL.md +43 -12
- package/template/.agents/skills/pincer-plan/SKILL.md +12 -4
- package/template/.agents/skills/pincer-release/SKILL.md +18 -0
- package/template/.agents/skills/pincer-status/SKILL.md +12 -2
- package/template/.claude/commands/pincer-code.md +85 -14
- package/template/.claude/commands/pincer-evaluate.md +42 -13
- package/template/.claude/commands/pincer-narrow.md +43 -12
- package/template/.claude/commands/pincer-plan.md +12 -4
- package/template/.claude/commands/pincer-release.md +18 -0
- package/template/.claude/commands/pincer-status.md +12 -2
- package/template/.claude/hooks/hook-policy.cjs +13 -6
- package/template/.claude/references/prd-template.md +11 -4
- package/template/.codex/README.md +1 -1
- package/template/.github/prompts/pincer-code.prompt.md +85 -14
- package/template/.github/prompts/pincer-evaluate.prompt.md +42 -13
- package/template/.github/prompts/pincer-narrow.prompt.md +43 -12
- package/template/.github/prompts/pincer-plan.prompt.md +12 -4
- package/template/.github/prompts/pincer-release.prompt.md +18 -0
- package/template/.github/prompts/pincer-status.prompt.md +12 -2
- package/template/AGENTS.md +17 -1
- package/template/docs/dry-run-checklist.md +30 -3
- package/template/docs/release-checklist.md +3 -1
- package/template/docs/runtime-contracts.md +1342 -96
- package/template/scripts/pincer-evidence.cjs +9 -7
- package/template/scripts/pincer-runtime/adopt.cjs +132 -0
- package/template/scripts/pincer-runtime/agreement.cjs +240 -0
- package/template/scripts/pincer-runtime/authorization.cjs +167 -0
- package/template/scripts/pincer-runtime/changes.cjs +517 -0
- package/template/scripts/pincer-runtime/checks.cjs +48 -0
- package/template/scripts/pincer-runtime/coverage.cjs +361 -0
- package/template/scripts/pincer-runtime/dispositions.cjs +95 -0
- package/template/scripts/pincer-runtime/evidence.cjs +303 -18
- package/template/scripts/pincer-runtime/gates.cjs +73 -0
- package/template/scripts/pincer-runtime/identity.cjs +21 -4
- package/template/scripts/pincer-runtime/impact.cjs +177 -0
- package/template/scripts/pincer-runtime/io.cjs +41 -0
- package/template/scripts/pincer-runtime/lifecycle.cjs +34 -12
- package/template/scripts/pincer-runtime/locator.cjs +158 -0
- package/template/scripts/pincer-runtime/migrate.cjs +140 -63
- package/template/scripts/pincer-runtime/parse.cjs +20 -1
- package/template/scripts/pincer-runtime/phases.cjs +245 -0
- package/template/scripts/pincer-runtime/readiness.cjs +9 -1
- package/template/scripts/pincer-runtime/requirements.cjs +255 -0
- package/template/scripts/pincer-runtime/resume.cjs +205 -0
- package/template/scripts/pincer-runtime/routing.cjs +54 -0
- package/template/scripts/pincer-runtime/runner.cjs +24 -6
- package/template/scripts/pincer-runtime/state.cjs +29 -7
- package/template/scripts/pincer-runtime/status.cjs +178 -22
- package/template/scripts/pincer-runtime/transaction.cjs +200 -0
- package/template/scripts/pincer-runtime/transitions.cjs +134 -0
- package/template/scripts/pincer-runtime.cjs +387 -76
- package/template/scripts/pincer-status.sh +1 -1
- 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
|
|
5
|
-
only writer of ticket lifecycle state, verification attempts, change
|
|
6
|
-
|
|
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
|
|
12
|
-
|
|
13
|
-
|
|
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
|
|
18
|
-
|
|
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
|
|
21
|
-
| --- | --- | --- |
|
|
22
|
-
| How recognized | no `.prd/changes/*.json`
|
|
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
|
-
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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` |
|
|
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` |
|
|
50
|
-
| `verify` | `T-NN` | legacy: ticket receipts; migrated: an attempt | always creates a new attempt
|
|
51
|
-
| `done` | `T-NN` | ticket `status`, `finished` (once) | legacy re-runs the check; migrated
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
105
|
-
|
|
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
|
-
|
|
121
|
-
never an untracked change.
|
|
122
|
-
for another PRD
|
|
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`).
|
|
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
|
|
164
|
-
|
|
165
|
-
|
|
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
|
|
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)
|
|
307
|
-
release. Lifecycle `done` and verification readiness
|
|
308
|
-
can be stale or failed without its `finished` date
|
|
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
|
|
313
|
-
| `MIGRATION_REQUIRED` | legacy receipts exist and the command needs
|
|
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
|
|
327
|
-
| `
|
|
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
|
|
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
|
|
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
|
|
381
|
-
|
|
382
|
-
|
|
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
|
|
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
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
|
428
|
-
|
|
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
|
|
436
|
-
and any platform outside the matrix is untested.
|
|
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.
|