pincer-workflow 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -31,15 +31,80 @@ On Codex CLI the commands are skills, invoked by mention rather than slash:
31
31
  `$pincer-plan <brief>` → `$pincer-narrow` → `$pincer-code` → `$pincer-evaluate`
32
32
  → `$pincer-release`, and `$pincer-status`.
33
33
 
34
+ ## Your first change, end to end
35
+
36
+ The five commands above are the shape of the work. This is the whole journey for one
37
+ change on a project that wants strict requirement coverage — the path most of the
38
+ runtime exists to support. Every step is a real command; none of them is optional
39
+ ceremony, and the two read-only reports (`coverage scaffold`, `resume --brief`) write
40
+ nothing.
41
+
42
+ ```bash
43
+ # 1. Install, then plan and break down the work.
44
+ npx pincer-workflow init
45
+ # /pincer-plan <brief> → .prd/prd-v1.md with R-NN requirements and S-NN scenarios
46
+ # /pincer-narrow → tickets/T-*.md, one per unit of work
47
+
48
+ # 2. Register the change and select it in this worktree.
49
+ node scripts/pincer-runtime.cjs register --prd .prd/prd-v1.md
50
+ node scripts/pincer-runtime.cjs change select prd-v1
51
+
52
+ # 3. Choose your coverage. Default is fine: tickets, checks and evidence work without
53
+ # this step. Strict coverage additionally binds every S-NN to a ticket and a check.
54
+ # Start the map from the draft — it lists every scenario and ticket for you:
55
+ node scripts/pincer-runtime.cjs coverage scaffold --change prd-v1
56
+
57
+ # 4. Author .prd/coverage/prd-v1.json yourself from that draft, then review the links.
58
+ # The draft never invents a link, a check command or a scope disposition, and it is
59
+ # not a map: `coverage adopt` refuses it. Working through its Unresolved list is the
60
+ # authoring step.
61
+
62
+ # 5. Adopt strict coverage explicitly, then record the user's actual approval.
63
+ node scripts/pincer-runtime.cjs coverage adopt --preview --change prd-v1
64
+ node scripts/pincer-runtime.cjs coverage adopt --apply --change prd-v1
65
+ node scripts/pincer-runtime.cjs change authorize prd-v1 --agreement <digest> \
66
+ --reference "<where the user said it>" --excerpt "<the user's approval, quoted>"
67
+ node scripts/pincer-runtime.cjs change activate prd-v1
68
+
69
+ # 6. Implement. One commit per ticket; `done` refuses without a current passing check.
70
+ # /pincer-code
71
+
72
+ # 7. Come back tomorrow, or in a fresh session with no memory of any of this:
73
+ node scripts/pincer-runtime.cjs resume --brief
74
+ # → one screen: the change, the authorization verdict, the coverage label, the
75
+ # blocker categories with counts, and the exact next command. `resume` (no
76
+ # --brief) prints every row; the brief names the command that shows them.
77
+
78
+ # 8. Evaluate and audit.
79
+ # /pincer-evaluate → validated evidence under .prd/evidence/
80
+ # /pincer-release → read-only pass/fail audit
81
+ ```
82
+
83
+ Adoption grants no approval, and approval is never inferred: `coverage adopt` records
84
+ the agreement, `change authorize` records what the user actually said about it, and a
85
+ generic "continue" authorizes no revised scope. Edit the PRD, the tickets or the map
86
+ afterwards and the agreement changes — the runtime reports `AGREEMENT_CHANGED` and
87
+ waits for a fresh decision rather than carrying the old approval forward.
88
+
89
+ ### Pinning a management kit
90
+
91
+ The kit under `scripts/` is code that the workflow also runs *on* the project. When you
92
+ are changing that code, manage the work with a pinned released copy from outside the
93
+ tree rather than with the version you are editing — extract the scripts from a release
94
+ tag into a directory outside the repository, record its digest, and run the lifecycle
95
+ commands from there. A project that only uses PINCER never needs this; a project that
96
+ develops it does.
97
+
34
98
  ### Claude Code
35
99
 
36
- Works immediately: the commands, the two subagents, the `.env` deny rules and
100
+ Installs complete: the commands, the two subagents, the `.env` deny rules and
37
101
  the destructive-command and ticket-guard hooks install to `.claude/`. Start
38
- `claude` in the repo and run `/pincer-plan <brief>`.
102
+ `claude` in the repo and run `/pincer-plan <brief>`. See
103
+ [platform support](#platform-support) for what that install has been observed to do.
39
104
 
40
105
  ### Codex CLI
41
106
 
42
- Works immediately as well: `AGENTS.md` loads natively, and the six commands
107
+ Installs complete as well: `AGENTS.md` loads natively, and the six commands
43
108
  install as skills under `.agents/skills/`, which Codex discovers from the repo
44
109
  (no copying into your home directory — Codex removed custom prompts and
45
110
  `~/.codex/prompts/` in early 2026). Start `codex` in the repo, then type
@@ -61,6 +126,28 @@ Full notes in `.codex/README.md`.
61
126
  Enable `"chat.promptFiles": true` in VS Code settings, then run `/pincer-plan`
62
127
  in chat. `.github/copilot-instructions.md` is wired to `AGENTS.md`.
63
128
 
129
+ ## Platform support
130
+
131
+ Two different claims, kept apart on purpose. **Installation-tested** means the packed
132
+ artifact installs and its files land where they should, proven by the suites in this
133
+ repository on every push. **Live-observed** means a real agent session carried a real
134
+ change through the workflow on that surface and the artifacts were reviewed. An
135
+ installation check is not a platform trial, and packaged parity is not live success.
136
+
137
+ | Surface | Installation-tested | Live-observed |
138
+ | --- | --- | --- |
139
+ | Claude Code (`npx pincer-workflow init`) | yes — packed install, layout and generated parity | not for this release |
140
+ | Codex CLI (`.agents/skills/`) | yes — packed install and skill layout | not for this release |
141
+ | GitHub Copilot (VS Code prompts) | yes — packed install and prompt layout | no |
142
+ | Claude Code plugin (marketplace) | yes — plugin build parity | no |
143
+ | Windows (native, no POSIX shell) | no | no |
144
+
145
+ No live platform trial has been observed for the current release. The work that would
146
+ produce one — a full strict journey on Claude Code and on Codex, plus a real cross-agent
147
+ handoff resumed from files — is open, and until it reports, every "live-observed" cell
148
+ above stays as it reads. Earlier releases have their own recorded trials under `docs/`;
149
+ those describe the versions they were run against, not this one.
150
+
64
151
  ## Claude Code plugin (alternative)
65
152
 
66
153
  Claude users can install PINCER as a plugin instead — commands arrive
@@ -80,17 +167,6 @@ and Copilot). Installing both gives you duplicate commands. Plugin users who
80
167
  want the repo-side rules too can copy `AGENTS.md` from the
81
168
  [template](template/AGENTS.md).
82
169
 
83
- ## Update
84
-
85
- ```bash
86
- npx pincer-workflow@latest update
87
- ```
88
-
89
- Files you never touched are refreshed in place. (Installs older than v0.2.0 gain the ticket state machine, the status report and the ticket-guard hook on update; `.claude/settings.json` conflicts if you edited it — merge the new hook entry from the `.new` file. v0.2.2 replaces the Codex adapter: the commands are now skills in `.agents/skills/` invoked as `$pincer-*`, since Codex no longer loads `~/.codex/prompts/` — you can delete the copies there. v0.2.3 ships the playbooks, rubrics and templates under `.claude/` on every platform, which Codex- and Copilot-only installs were missing. v0.3.0 makes trust revocable: every verification attempt is recorded, `done` re-runs the check, tickets carry `prd:`, the release audit is read-only, and `.pincer.json` moves to schema 2 — an install from 0.2.x is treated as an untrusted baseline, so on the first update every changed file arrives as a `.new` proposal once; hooks now need Node 18+. v0.4.0 carries requirement IDs and `Proves:` checks from the PRD to evaluation, adds `profile: small|standard`, and saves candidate evidence under `.prd/evidence/`; the update is additive, but an existing `NOTES.md` without an `evidence:` manifest reads as stale until `/pincer-evaluate` is re-run. v0.4.1 is playbook wording only: the code playbook gains one recovery exception (a tree back at the evaluated candidate is restored by the user and nothing is committed), evaluate records one check per command, and plan asks only the open part of a partly answered question. v0.5.0 adds the runtime: `scripts/pincer-runtime.cjs` with its modules under `scripts/pincer-runtime/` now implements the ticket lifecycle, status, migration and evidence export, and `pincer-ticket.sh` / `pincer-status.sh` delegate to it, so every command needs Node.js 18+; unmigrated projects keep their receipts and behavior. Run `node scripts/pincer-runtime.cjs migrate --preview --prd .prd/prd-vN.md` to see what migration would change; after `--apply`, `verify` records attempts with captured logs under the ignored `.pincer/runtime/` and writes no receipt into the ticket, `done` consumes the current passing attempt against the current source, and `/pincer-evaluate` exports evidence schema 2 from runtime attempts. `docs/runtime-contracts.md` is the contract; `pincer doctor` tells you when a migration is available and never migrates on its own. The next version keeps several changes side by side: `register` writes a change record (schema 2) that retains every earlier change, the worktree selects the change it works on (`change select`), the user's approval is recorded against the reviewed agreement (`change authorize`), the lifecycle is explicit (`change activate|pause|resume|complete|reopen|cancel|supersede`), and `resume` prints where to continue from files alone. A v0.5.0 binding converts with the same `migrate --preview` / `--apply`; the converted change is planned with no authorization, its old attempts and evaluation are history until verified again, and the old free-text authorization never authorizes execution. Selection and local attempts are per worktree; records are tracked and shared through git; completed means ready for evaluation, not released. The next version also raises the floor to Node.js 22+, because 18 and 20 are past end of life. It separately fixes a defect that was never version-specific: a command whose output exceeded one pipe buffer (65,536 bytes) lost the tail when the process exited and still exited 0, on every version including 22 and 24. Every write is now synchronous, so piped `--json` is complete. Updating the kit inside a migrated project changes tracked runtime files, so every done ticket's attempt reads `SOURCE_CHANGED` until it is verified again; update between changes, not mid-ticket. An update from 0.4.x leaves the retired `scripts/pincer-ticket-lib.sh` on disk; `pincer doctor` names it and it is safe to delete.) Files you edited are left
90
- alone — the new version lands next to them as `<file>.new` for a manual merge.
91
- `npx pincer-workflow doctor` checks the health of an install (hook executable,
92
- `.gitignore` covering `.env*`, no unmerged `*.new` files, version current).
93
-
94
170
  ## What you get
95
171
 
96
172
  | Piece | Purpose |
@@ -110,6 +186,17 @@ alone — the new version lands next to them as `<file>.new` for a manual merge.
110
186
  | `docs/release-checklist.md` | General, read-only candidate audit used by `/pincer-release` |
111
187
  | `docs/dry-run-checklist.md` | Separate manual platform trial for the Pincer kit |
112
188
 
189
+ ## Update
190
+
191
+ ```bash
192
+ npx pincer-workflow@latest update
193
+ ```
194
+
195
+ Files you never touched are refreshed in place. (Installs older than v0.2.0 gain the ticket state machine, the status report and the ticket-guard hook on update; `.claude/settings.json` conflicts if you edited it — merge the new hook entry from the `.new` file. v0.2.2 replaces the Codex adapter: the commands are now skills in `.agents/skills/` invoked as `$pincer-*`, since Codex no longer loads `~/.codex/prompts/` — you can delete the copies there. v0.2.3 ships the playbooks, rubrics and templates under `.claude/` on every platform, which Codex- and Copilot-only installs were missing. v0.3.0 makes trust revocable: every verification attempt is recorded, `done` re-runs the check, tickets carry `prd:`, the release audit is read-only, and `.pincer.json` moves to schema 2 — an install from 0.2.x is treated as an untrusted baseline, so on the first update every changed file arrives as a `.new` proposal once; hooks now need Node 18+. v0.4.0 carries requirement IDs and `Proves:` checks from the PRD to evaluation, adds `profile: small|standard`, and saves candidate evidence under `.prd/evidence/`; the update is additive, but an existing `NOTES.md` without an `evidence:` manifest reads as stale until `/pincer-evaluate` is re-run. v0.4.1 is playbook wording only: the code playbook gains one recovery exception (a tree back at the evaluated candidate is restored by the user and nothing is committed), evaluate records one check per command, and plan asks only the open part of a partly answered question. v0.5.0 adds the runtime: `scripts/pincer-runtime.cjs` with its modules under `scripts/pincer-runtime/` now implements the ticket lifecycle, status, migration and evidence export, and `pincer-ticket.sh` / `pincer-status.sh` delegate to it, so every command needs Node.js 18+; unmigrated projects keep their receipts and behavior. Run `node scripts/pincer-runtime.cjs migrate --preview --prd .prd/prd-vN.md` to see what migration would change; after `--apply`, `verify` records attempts with captured logs under the ignored `.pincer/runtime/` and writes no receipt into the ticket, `done` consumes the current passing attempt against the current source, and `/pincer-evaluate` exports evidence schema 2 from runtime attempts. `docs/runtime-contracts.md` is the contract; `pincer doctor` tells you when a migration is available and never migrates on its own. v0.6.0 keeps several changes side by side: `register` writes a change record (schema 2) that retains every earlier change, the worktree selects the change it works on (`change select`), the user's approval is recorded against the reviewed agreement (`change authorize`), the lifecycle is explicit (`change activate|pause|resume|complete|reopen|cancel|supersede`), and `resume` prints where to continue from files alone. A v0.5.0 binding converts with the same `migrate --preview` / `--apply`; the converted change is planned with no authorization, its old attempts and evaluation are history until verified again, and the old free-text authorization never authorizes execution. Selection and local attempts are per worktree; records are tracked and shared through git; completed means ready for evaluation, not released. v0.6.0 also raises the floor to Node.js 22+, because 18 and 20 are past end of life. It separately fixes a defect that was never version-specific: a command whose output exceeded one pipe buffer (65,536 bytes) lost the tail when the process exited and still exited 0, on every version including 22 and 24. Every write is now synchronous, so piped `--json` is complete. Updating the kit inside a migrated project changes tracked runtime files, so every done ticket's attempt reads `SOURCE_CHANGED` until it is verified again; update between changes, not mid-ticket. An update from 0.4.x leaves the retired `scripts/pincer-ticket-lib.sh` on disk; `pincer doctor` names it and it is safe to delete.) Files you edited are left
196
+ alone — the new version lands next to them as `<file>.new` for a manual merge.
197
+ `npx pincer-workflow doctor` checks the health of an install (hook executable,
198
+ `.gitignore` covering `.env*`, no unmerged `*.new` files, version current).
199
+
113
200
  ## Design principles
114
201
 
115
202
  - **Approval gates scale with decision cost** — a human owns architecture, scope,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pincer-workflow",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "PINCER — a PRD-driven agentic delivery workflow for Claude Code, Codex CLI, and GitHub Copilot. Plan · Investigate · Narrow · Code · Evaluate · Release.",
5
5
  "bin": {
6
6
  "pincer": "bin/pincer.js"
@@ -14,7 +14,7 @@
14
14
  "node": ">=22"
15
15
  },
16
16
  "scripts": {
17
- "test": "node test/smoke.test.js && node test/installer.test.js && node test/ticket.test.js && node test/validation.test.js && node test/verification.test.js && node test/behavioral-verification.test.js && node test/evidence.test.js && node test/candidate.test.js && node test/recovery.test.js && node test/hooks.test.js && node test/runtime-parse.test.js && node test/coverage-inventory.test.js && node test/coverage-map.test.js && node test/coverage-agreement.test.js && node test/coverage-adoption.test.js && node test/coverage-readiness.test.js && node test/coverage-impact.test.js && node test/coverage-checks.test.js && node test/coverage-evidence.test.js && node test/coverage-reports.test.js && node test/runtime-identity.test.js && node test/change-registry.test.js && node test/change-selection.test.js && node test/change-agreement.test.js && node test/change-authorization.test.js && node test/change-lifecycle.test.js && node test/change-command-gates.test.js && node test/change-evidence-context.test.js && node test/change-evaluations.test.js && node test/change-resume.test.js && node test/change-trial-record.test.js && node test/change-review-packet.test.js && node test/runtime-state.test.js && node test/change-transactions.test.js && node test/runtime-status.test.js && node test/runtime-output.test.js && node test/runtime-runner.test.js && node test/runtime-lifecycle.test.js && node test/runtime-migrate.test.js && node test/change-migration.test.js && node test/runtime-evidence.test.js && node test/workflow.test.js && node test/contracts.test.js && node test/change-contracts.test.js && node test/coverage-contracts.test.js && node test/distribution.test.js && node test/change-distribution.test.js && node test/coverage-distribution.test.js && node test/delivery-benchmark.test.js && node test/coverage-trial-record.test.js && node test/coverage-review-packet.test.js"
17
+ "test": "node test/smoke.test.js && node test/installer.test.js && node test/strict-onboarding.test.js && node test/ticket.test.js && node test/validation.test.js && node test/verification.test.js && node test/behavioral-verification.test.js && node test/evidence.test.js && node test/candidate.test.js && node test/recovery.test.js && node test/hooks.test.js && node test/runtime-parse.test.js && node test/coverage-inventory.test.js && node test/coverage-map.test.js && node test/coverage-scaffold.test.js && node test/coverage-agreement.test.js && node test/coverage-adoption.test.js && node test/coverage-readiness.test.js && node test/coverage-impact.test.js && node test/coverage-checks.test.js && node test/coverage-evidence.test.js && node test/coverage-reports.test.js && node test/runtime-identity.test.js && node test/change-registry.test.js && node test/change-selection.test.js && node test/change-agreement.test.js && node test/change-authorization.test.js && node test/change-lifecycle.test.js && node test/change-command-gates.test.js && node test/change-evidence-context.test.js && node test/change-evaluations.test.js && node test/change-resume.test.js && node test/resume-brief.test.js && node test/change-trial-record.test.js && node test/change-review-packet.test.js && node test/runtime-state.test.js && node test/change-transactions.test.js && node test/runtime-status.test.js && node test/runtime-output.test.js && node test/runtime-runner.test.js && node test/runtime-lifecycle.test.js && node test/runtime-migrate.test.js && node test/change-migration.test.js && node test/runtime-evidence.test.js && node test/workflow.test.js && node test/improvement-contracts.test.js && node test/contracts.test.js && node test/change-contracts.test.js && node test/coverage-contracts.test.js && node test/distribution.test.js && node test/change-distribution.test.js && node test/coverage-distribution.test.js && node test/effort-records.test.js && node test/execution-freeze.test.js && node test/delivery-benchmark.test.js && node test/delivery-benchmark-v7.test.js && node test/benchmark-orchestrator.test.js && node test/strict-pilot-records.test.js && node test/platform-trial-records.test.js && node test/improvement-trial-records.test.js && node test/coverage-trial-record.test.js && node test/coverage-review-packet.test.js && node test/improvement-review-packet.test.js && node test/readiness-contracts.test.js && node test/benchmark-effective-inputs.test.js && node test/release-preparation.test.js && node test/benchmark-run-claims.test.js && node test/benchmark-environment.test.js && node test/benchmark-browser.test.js && node test/benchmark-restart.test.js && node test/benchmark-usage-completeness.test.js && node test/benchmark-terminal-records.test.js && node test/study-readiness.test.js && node test/benchmark-allocation.test.js && node test/benchmark-prepared-bases.test.js && node test/benchmark-study-launch.test.js"
18
18
  },
19
19
  "keywords": [
20
20
  "claude-code",
@@ -40,7 +40,10 @@ previously authorized work. Read the `Runtime` line before the first ticket. `ch
40
40
  `.prd/changes/`) → run `node scripts/pincer-runtime.cjs resume` and follow its `Next`
41
41
  line: it names the selected change, its lifecycle state, the agreement and the
42
42
  authorization verdict, the blockers in order and the exact next command, all from the
43
- files on disk. Select the change to work on (`node scripts/pincer-runtime.cjs change
43
+ files on disk. `resume --brief` is the same report projected to counts and one next
44
+ action — the same verdict and the same `Next` line, with every blocker category kept
45
+ and its `Detail` line naming the command that prints the rows it grouped. Prefer it
46
+ when you are orienting, and read the full report when a blocker needs its detail. Select the change to work on (`node scripts/pincer-runtime.cjs change
44
47
  select <id>`; selection is local metadata and touches no source), activate it
45
48
  (`change activate <id>`; refused until the user's authorization is recorded with
46
49
  `change authorize` and no consequential decision is open) and resume a paused change
@@ -39,7 +39,17 @@ discovered consequential choice is surfaced before implementation.
39
39
  in the ticket Objective. Resolve missing coverage and conflicting criteria with
40
40
  the user before implementation; do not start with an unmapped required scenario. On a change with change records, author the map once as
41
41
  `.prd/coverage/<change id>.json` (coverage map schema 1, "Coverage map" in
42
- `docs/runtime-contracts.md`): one `scenarios` row per `S-NN` naming its
42
+ `docs/runtime-contracts.md`). Start from
43
+ `node scripts/pincer-runtime.cjs coverage scaffold --change <change id>`: it prints
44
+ a read-only draft listing every live scenario exactly once with its requirement,
45
+ every ticket with its objective, its own `Implements:` claim and its Verification
46
+ text, and an `Unresolved` list of what is still unauthored. It writes nothing,
47
+ adopts nothing and decides nothing — it removes the transcription, not the
48
+ judgment, so a ticket's claim is material to check rather than a link, and no check
49
+ command, ticket role or scope disposition is ever invented. The draft is not a map:
50
+ it carries no `schema` key and `coverage adopt` refuses it. You author the real map
51
+ yourself, working through the draft's unresolved entries:
52
+ one `scenarios` row per `S-NN` naming its
43
53
  implementing tickets and declared checks, every ticket of the change in
44
54
  `tickets` as `implements` or `enables` (with a rationale), each check declared
45
55
  once in `checks` with its kind, `required` flag and, for a command, the exact
@@ -12,7 +12,19 @@ start of a session. Read-only: change nothing.
12
12
 
13
13
  ## Steps
14
14
 
15
- 1. Run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`, `tickets/`,
15
+ 1. On a project with change records, start with
16
+ `node scripts/pincer-runtime.cjs resume --brief`. It is one read that answers the
17
+ question this command exists for: the selected change and its lifecycle, the
18
+ authorization verdict, the coverage label, ticket and attempt counts, every blocking
19
+ reason category with its count, and the exact next command — the full report's own,
20
+ copied, never recomputed. Reading the full `resume`, `status` and `coverage` reports
21
+ in sequence to find one next action costs several kilobytes to answer in a line, and
22
+ on a large change the full report grows with the work while the answer does not.
23
+ Read further only when the brief gives you a reason to: it ends with the `Detail`
24
+ line naming the command that prints every row it grouped.
25
+ 2. When you need the detail — a blocker you must act on, a ticket list, an attempt
26
+ history — run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`,
27
+ `tickets/`,
16
28
  `NOTES.md`) and prints the PRD state and profile, every ticket with its state and
17
29
  clock-based elapsed time, what is blocked, wall-clock build time while a ticket is in
18
30
  progress or against an explicit user budget, the `Runtime` line (legacy receipts or
@@ -20,8 +32,8 @@ start of a session. Read-only: change nothing.
20
32
  candidate, any warnings (each readiness problem once), and the next command to run.
21
33
  `scripts/pincer-status.sh --json` prints one status object with reason codes and the
22
34
  next action for tooling; `node scripts/pincer-runtime.cjs ready [T-NN]` is the
23
- read-only gate. On a project with change records (`Runtime changes …`) run
24
- `node scripts/pincer-runtime.cjs resume` as well: it reports the selected change, its
35
+ read-only gate. On a project with change records (`Runtime changes …`)
36
+ `node scripts/pincer-runtime.cjs resume` is the full report: it reports the selected change, its
25
37
  lifecycle, agreement and authorization, decisions, references, tickets, attempts,
26
38
  candidate, the authored handoff note and the next command, and never writes;
27
39
  `change list` shows every retained change, `status --change <id>` and
@@ -30,12 +42,14 @@ start of a session. Read-only: change nothing.
30
42
  `strict` or `unverified`; on a strict change `node scripts/pincer-runtime.cjs coverage`
31
43
  (and `impact` after an edit) name the exact scenario, ticket, check or decision
32
44
  that is next — quote them rather than inferring coverage from the ticket list.
33
- 2. Report in three lines: where the workflow is, what is in progress or blocked, and the
45
+ On a legacy project there is no change record to summarize, so `pincer-status.sh`
46
+ is the first read.
47
+ 3. Report in three lines: where the workflow is, what is in progress or blocked, and the
34
48
  next command. Quote the `Next` line as-is. When the `Runtime` line says `legacy`,
35
49
  add the register or migrate command it names as the step that precedes the next
36
50
  ticket (fresh project → `register`, legacy receipts → `migrate --preview`, a v0.5.0
37
51
  binding → `migrate --preview`, no selection → `change select <id>`).
38
- 3. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
52
+ 4. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
39
53
  `$pincer-code T-{NN}`. If the script printed a warning, surface it — a done ticket
40
54
  without a receipt was marked by hand and needs `scripts/pincer-ticket.sh verify T-{NN}`.
41
55
  Never restore a ticket file from git to clear a warning; a failed attempt is a record.
@@ -38,7 +38,10 @@ previously authorized work. Read the `Runtime` line before the first ticket. `ch
38
38
  `.prd/changes/`) → run `node scripts/pincer-runtime.cjs resume` and follow its `Next`
39
39
  line: it names the selected change, its lifecycle state, the agreement and the
40
40
  authorization verdict, the blockers in order and the exact next command, all from the
41
- files on disk. Select the change to work on (`node scripts/pincer-runtime.cjs change
41
+ files on disk. `resume --brief` is the same report projected to counts and one next
42
+ action — the same verdict and the same `Next` line, with every blocker category kept
43
+ and its `Detail` line naming the command that prints the rows it grouped. Prefer it
44
+ when you are orienting, and read the full report when a blocker needs its detail. Select the change to work on (`node scripts/pincer-runtime.cjs change
42
45
  select <id>`; selection is local metadata and touches no source), activate it
43
46
  (`change activate <id>`; refused until the user's authorization is recorded with
44
47
  `change authorize` and no consequential decision is open) and resume a paused change
@@ -37,7 +37,17 @@ discovered consequential choice is surfaced before implementation.
37
37
  in the ticket Objective. Resolve missing coverage and conflicting criteria with
38
38
  the user before implementation; do not start with an unmapped required scenario. On a change with change records, author the map once as
39
39
  `.prd/coverage/<change id>.json` (coverage map schema 1, "Coverage map" in
40
- `docs/runtime-contracts.md`): one `scenarios` row per `S-NN` naming its
40
+ `docs/runtime-contracts.md`). Start from
41
+ `node scripts/pincer-runtime.cjs coverage scaffold --change <change id>`: it prints
42
+ a read-only draft listing every live scenario exactly once with its requirement,
43
+ every ticket with its objective, its own `Implements:` claim and its Verification
44
+ text, and an `Unresolved` list of what is still unauthored. It writes nothing,
45
+ adopts nothing and decides nothing — it removes the transcription, not the
46
+ judgment, so a ticket's claim is material to check rather than a link, and no check
47
+ command, ticket role or scope disposition is ever invented. The draft is not a map:
48
+ it carries no `schema` key and `coverage adopt` refuses it. You author the real map
49
+ yourself, working through the draft's unresolved entries:
50
+ one `scenarios` row per `S-NN` naming its
41
51
  implementing tickets and declared checks, every ticket of the change in
42
52
  `tickets` as `implements` or `enables` (with a rationale), each check declared
43
53
  once in `checks` with its kind, `required` flag and, for a command, the exact
@@ -10,7 +10,19 @@ start of a session. Read-only: change nothing.
10
10
 
11
11
  ## Steps
12
12
 
13
- 1. Run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`, `tickets/`,
13
+ 1. On a project with change records, start with
14
+ `node scripts/pincer-runtime.cjs resume --brief`. It is one read that answers the
15
+ question this command exists for: the selected change and its lifecycle, the
16
+ authorization verdict, the coverage label, ticket and attempt counts, every blocking
17
+ reason category with its count, and the exact next command — the full report's own,
18
+ copied, never recomputed. Reading the full `resume`, `status` and `coverage` reports
19
+ in sequence to find one next action costs several kilobytes to answer in a line, and
20
+ on a large change the full report grows with the work while the answer does not.
21
+ Read further only when the brief gives you a reason to: it ends with the `Detail`
22
+ line naming the command that prints every row it grouped.
23
+ 2. When you need the detail — a blocker you must act on, a ticket list, an attempt
24
+ history — run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`,
25
+ `tickets/`,
14
26
  `NOTES.md`) and prints the PRD state and profile, every ticket with its state and
15
27
  clock-based elapsed time, what is blocked, wall-clock build time while a ticket is in
16
28
  progress or against an explicit user budget, the `Runtime` line (legacy receipts or
@@ -18,8 +30,8 @@ start of a session. Read-only: change nothing.
18
30
  candidate, any warnings (each readiness problem once), and the next command to run.
19
31
  `scripts/pincer-status.sh --json` prints one status object with reason codes and the
20
32
  next action for tooling; `node scripts/pincer-runtime.cjs ready [T-NN]` is the
21
- read-only gate. On a project with change records (`Runtime changes …`) run
22
- `node scripts/pincer-runtime.cjs resume` as well: it reports the selected change, its
33
+ read-only gate. On a project with change records (`Runtime changes …`)
34
+ `node scripts/pincer-runtime.cjs resume` is the full report: it reports the selected change, its
23
35
  lifecycle, agreement and authorization, decisions, references, tickets, attempts,
24
36
  candidate, the authored handoff note and the next command, and never writes;
25
37
  `change list` shows every retained change, `status --change <id>` and
@@ -28,12 +40,14 @@ start of a session. Read-only: change nothing.
28
40
  `strict` or `unverified`; on a strict change `node scripts/pincer-runtime.cjs coverage`
29
41
  (and `impact` after an edit) name the exact scenario, ticket, check or decision
30
42
  that is next — quote them rather than inferring coverage from the ticket list.
31
- 2. Report in three lines: where the workflow is, what is in progress or blocked, and the
43
+ On a legacy project there is no change record to summarize, so `pincer-status.sh`
44
+ is the first read.
45
+ 3. Report in three lines: where the workflow is, what is in progress or blocked, and the
32
46
  next command. Quote the `Next` line as-is. When the `Runtime` line says `legacy`,
33
47
  add the register or migrate command it names as the step that precedes the next
34
48
  ticket (fresh project → `register`, legacy receipts → `migrate --preview`, a v0.5.0
35
49
  binding → `migrate --preview`, no selection → `change select <id>`).
36
- 3. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
50
+ 4. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
37
51
  `/pincer-code T-{NN}`. If the script printed a warning, surface it — a done ticket
38
52
  without a receipt was marked by hand and needs `scripts/pincer-ticket.sh verify T-{NN}`.
39
53
  Never restore a ticket file from git to clear a warning; a failed attempt is a record.
@@ -40,7 +40,10 @@ previously authorized work. Read the `Runtime` line before the first ticket. `ch
40
40
  `.prd/changes/`) → run `node scripts/pincer-runtime.cjs resume` and follow its `Next`
41
41
  line: it names the selected change, its lifecycle state, the agreement and the
42
42
  authorization verdict, the blockers in order and the exact next command, all from the
43
- files on disk. Select the change to work on (`node scripts/pincer-runtime.cjs change
43
+ files on disk. `resume --brief` is the same report projected to counts and one next
44
+ action — the same verdict and the same `Next` line, with every blocker category kept
45
+ and its `Detail` line naming the command that prints the rows it grouped. Prefer it
46
+ when you are orienting, and read the full report when a blocker needs its detail. Select the change to work on (`node scripts/pincer-runtime.cjs change
44
47
  select <id>`; selection is local metadata and touches no source), activate it
45
48
  (`change activate <id>`; refused until the user's authorization is recorded with
46
49
  `change authorize` and no consequential decision is open) and resume a paused change
@@ -39,7 +39,17 @@ discovered consequential choice is surfaced before implementation.
39
39
  in the ticket Objective. Resolve missing coverage and conflicting criteria with
40
40
  the user before implementation; do not start with an unmapped required scenario. On a change with change records, author the map once as
41
41
  `.prd/coverage/<change id>.json` (coverage map schema 1, "Coverage map" in
42
- `docs/runtime-contracts.md`): one `scenarios` row per `S-NN` naming its
42
+ `docs/runtime-contracts.md`). Start from
43
+ `node scripts/pincer-runtime.cjs coverage scaffold --change <change id>`: it prints
44
+ a read-only draft listing every live scenario exactly once with its requirement,
45
+ every ticket with its objective, its own `Implements:` claim and its Verification
46
+ text, and an `Unresolved` list of what is still unauthored. It writes nothing,
47
+ adopts nothing and decides nothing — it removes the transcription, not the
48
+ judgment, so a ticket's claim is material to check rather than a link, and no check
49
+ command, ticket role or scope disposition is ever invented. The draft is not a map:
50
+ it carries no `schema` key and `coverage adopt` refuses it. You author the real map
51
+ yourself, working through the draft's unresolved entries:
52
+ one `scenarios` row per `S-NN` naming its
43
53
  implementing tickets and declared checks, every ticket of the change in
44
54
  `tickets` as `implements` or `enables` (with a rationale), each check declared
45
55
  once in `checks` with its kind, `required` flag and, for a command, the exact
@@ -12,7 +12,19 @@ start of a session. Read-only: change nothing.
12
12
 
13
13
  ## Steps
14
14
 
15
- 1. Run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`, `tickets/`,
15
+ 1. On a project with change records, start with
16
+ `node scripts/pincer-runtime.cjs resume --brief`. It is one read that answers the
17
+ question this command exists for: the selected change and its lifecycle, the
18
+ authorization verdict, the coverage label, ticket and attempt counts, every blocking
19
+ reason category with its count, and the exact next command — the full report's own,
20
+ copied, never recomputed. Reading the full `resume`, `status` and `coverage` reports
21
+ in sequence to find one next action costs several kilobytes to answer in a line, and
22
+ on a large change the full report grows with the work while the answer does not.
23
+ Read further only when the brief gives you a reason to: it ends with the `Detail`
24
+ line naming the command that prints every row it grouped.
25
+ 2. When you need the detail — a blocker you must act on, a ticket list, an attempt
26
+ history — run `scripts/pincer-status.sh`. It reads the artifacts on disk (`.prd/`,
27
+ `tickets/`,
16
28
  `NOTES.md`) and prints the PRD state and profile, every ticket with its state and
17
29
  clock-based elapsed time, what is blocked, wall-clock build time while a ticket is in
18
30
  progress or against an explicit user budget, the `Runtime` line (legacy receipts or
@@ -20,8 +32,8 @@ start of a session. Read-only: change nothing.
20
32
  candidate, any warnings (each readiness problem once), and the next command to run.
21
33
  `scripts/pincer-status.sh --json` prints one status object with reason codes and the
22
34
  next action for tooling; `node scripts/pincer-runtime.cjs ready [T-NN]` is the
23
- read-only gate. On a project with change records (`Runtime changes …`) run
24
- `node scripts/pincer-runtime.cjs resume` as well: it reports the selected change, its
35
+ read-only gate. On a project with change records (`Runtime changes …`)
36
+ `node scripts/pincer-runtime.cjs resume` is the full report: it reports the selected change, its
25
37
  lifecycle, agreement and authorization, decisions, references, tickets, attempts,
26
38
  candidate, the authored handoff note and the next command, and never writes;
27
39
  `change list` shows every retained change, `status --change <id>` and
@@ -30,12 +42,14 @@ start of a session. Read-only: change nothing.
30
42
  `strict` or `unverified`; on a strict change `node scripts/pincer-runtime.cjs coverage`
31
43
  (and `impact` after an edit) name the exact scenario, ticket, check or decision
32
44
  that is next — quote them rather than inferring coverage from the ticket list.
33
- 2. Report in three lines: where the workflow is, what is in progress or blocked, and the
45
+ On a legacy project there is no change record to summarize, so `pincer-status.sh`
46
+ is the first read.
47
+ 3. Report in three lines: where the workflow is, what is in progress or blocked, and the
34
48
  next command. Quote the `Next` line as-is. When the `Runtime` line says `legacy`,
35
49
  add the register or migrate command it names as the step that precedes the next
36
50
  ticket (fresh project → `register`, legacy receipts → `migrate --preview`, a v0.5.0
37
51
  binding → `migrate --preview`, no selection → `change select <id>`).
38
- 3. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
52
+ 4. If a ticket is `in_progress`, read it and `git status`, then offer to resume it with
39
53
  `/pincer-code T-{NN}`. If the script printed a warning, surface it — a done ticket
40
54
  without a receipt was marked by hand and needs `scripts/pincer-ticket.sh verify T-{NN}`.
41
55
  Never restore a ticket file from git to clear a warning; a failed attempt is a record.
@@ -893,8 +893,8 @@ it selects nothing, completes nothing, runs nothing and writes nothing.
893
893
 
894
894
  ## Resume report
895
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
896
+ `resume [--change <id>] [--brief] [--json]` is read-only inspection of the selected
897
+ (or named) change for a fresh session; it is distinct from `change resume`, the lifecycle
898
898
  operation. It launches no check, records no approval, changes no selection and writes
899
899
  no file; repeated runs are byte-identical apart from `generated`. Resume JSON schema 1:
900
900
 
@@ -938,6 +938,40 @@ command to run):
938
938
  7. `completed` without a current evaluation → `/pincer-evaluate`;
939
939
  8. evaluated and current → `/pincer-release` (read-only audit).
940
940
 
941
+ ### Brief resume
942
+
943
+ `resume --brief [--change <id>] [--json]` is a **projection of the report `resume`
944
+ already computes**, for a fresh session that needs the next action without paying for
945
+ every row to find it. It is not a second policy engine: `next` is the full report's
946
+ own object, copied, and no verdict, readiness value or precedence rule is recomputed.
947
+ It composes with `--change` and `--json`, keeps the full report's exit code, and
948
+ writes nothing.
949
+
950
+ Brief JSON envelope — `brief: 1` with `of` naming the schema it projects, so nothing
951
+ reading resume JSON ever sees a new shape:
952
+
953
+ ```
954
+ { brief: 1, kind: "resume-brief", of: 2, generated, root, mode,
955
+ selection: { change | null, problem | null },
956
+ change: { id, prd, base, lifecycle } | null,
957
+ agreement: { current, verdict, authorized: { id, disposition } | null } | null,
958
+ coverage: { label, strict, structure, implementation } | null,
959
+ tickets: { total, by_status: { open, in_progress, done }, not_ready },
960
+ attempts: { total, running, current_failed },
961
+ candidate: { notes, candidate, evidence } | null,
962
+ blockers: { total, categories: [ { code, count } ] },
963
+ next: <the full report's next, verbatim>,
964
+ detail: { command, prd, tickets: [ paths ], omitted } }
965
+ ```
966
+
967
+ Grouping may collapse **repetition**; it may never collapse a **category**. Every
968
+ distinct blocker code of the full report appears in `blockers.categories` with its
969
+ exact count, and `blockers.total`, `tickets.total` and `attempts.total` equal the full
970
+ report's own lengths. `detail.omitted` states how many rows the brief did not print
971
+ and `detail.command` is the exact command that prints them, so nothing is hidden —
972
+ only deferred. The default `resume` human output and resume JSON schema 2 are
973
+ unchanged.
974
+
941
975
  ## Migration and rollback
942
976
 
943
977
  `migrate --preview --prd .prd/prd-vN.md` prints the plan and writes nothing. It exits 0
@@ -1556,6 +1590,7 @@ are unchanged.
1556
1590
  | --- | --- | --- | --- |
1557
1591
  | `coverage` | `[--change <id>] [--json]` | nothing | the phase-specific coverage report of the selected (or named) change; exit 0 when the report was computed (complete or not), 4 when the inputs cannot be read |
1558
1592
  | `impact` | `[--change <id>] [--from G-NN \| A-NN] [--json]` | nothing | structural differences between the current authored inputs and a retained agreement; exit 0 when computed (`unchanged`, `changed` or `unavailable`), 4 on invalid input |
1593
+ | `coverage scaffold` | `--change <id> [--json]` | nothing | the read-only coverage draft ("Coverage draft"); exit 0 when a draft was produced, 4 when the inputs cannot be read |
1559
1594
  | `coverage adopt` | `--preview \| --apply --change <id> [--agreement <digest>]` | apply: the backup, the schema 3 record, the adoption snapshot | preview writes nothing; exit 0 / 1 (conflict) / 4 (invalid state) |
1560
1595
  | `check` | `C-NN --candidate <sha>` (strict) | an attempt | the declared command; `--timeout` and `-- <command>` are `CHECK_UNDECLARED` in a strict change |
1561
1596
 
@@ -1563,6 +1598,57 @@ are unchanged.
1563
1598
  write no file; repeated runs are byte-identical apart from `generated`. Their human
1564
1599
  output and their JSON name the same IDs, codes and next action.
1565
1600
 
1601
+ ### Coverage draft
1602
+
1603
+ `coverage scaffold --change <id> [--json]` projects the validated inventory, the
1604
+ change's tickets and any authored map into one reviewable **draft**. It exists to
1605
+ remove transcription, not judgment: authoring `.prd/coverage/<id>.json` means copying
1606
+ every live scenario, every ticket role and every check declaration out of documents
1607
+ the runtime has already parsed, and that copying is all this removes.
1608
+
1609
+ The draft **decides nothing**. It never invents a link, a check command, a ticket role
1610
+ or a scope disposition; an entry it cannot resolve from authored content stays
1611
+ `unresolved`. A ticket's own `Implements:`/`Scenarios:` claim and its Verification
1612
+ text are reported under `candidates` with the ticket's file, as material to read —
1613
+ never promoted into `scenarios` or `checks`. IDs on those lines that the inventory
1614
+ does not define are not reported at all, because a ticket naming other tickets is not
1615
+ a scenario claim.
1616
+
1617
+ Draft envelope — `draft: 1`, and deliberately **no `schema` key**, so `readMap`
1618
+ refuses it with `COVERAGE_INVALID: unsupported coverage map schema undefined`:
1619
+
1620
+ ```
1621
+ { draft: 1, kind: "coverage-draft", change, prd,
1622
+ inventory: { digest, requirements, scenarios },
1623
+ authored: { map: <path> | null, digest | null },
1624
+ scenarios: { "S-NN": { requirement, state, tickets: [], checks: [] } },
1625
+ scope: { "S-NN": { disposition, decision, prior, note, state } },
1626
+ tickets: { "T-NN": { role, rationale, state } },
1627
+ checks: { "C-NN": { kind, required, command, timeout, cwd, obligation, note, state } },
1628
+ candidates: { tickets: { "T-NN": { file, objective, implements: [], scenarios: [], verification } } },
1629
+ unresolved: [ { code, id, detail } ],
1630
+ next: { action, command } }
1631
+ ```
1632
+
1633
+ `state` is `authored` for content read from the existing map and `unresolved`
1634
+ otherwise. Every live inventory scenario appears exactly once across `scenarios` and
1635
+ `scope`. Authored rows the inventory no longer defines are **preserved and flagged**
1636
+ (`SCENARIO_STALE`), never dropped: removing an obligation is a decision with its own
1637
+ disposition and authorization. Unresolved codes are `SCENARIO_UNLINKED`,
1638
+ `CHECK_UNDECLARED`, `TICKET_UNCLASSIFIED`, `SCENARIO_STALE` and `TICKET_FOREIGN`.
1639
+
1640
+ The draft body carries no timestamp, so two calls on identical authored inputs are
1641
+ byte-identical. Scaffolding writes no file, launches no check, changes no selection,
1642
+ adopts nothing and records no approval; a malformed, unsupported, symlinked or
1643
+ out-of-root map is refused with exit 4 before any draft is printed, because silently
1644
+ dropping authored content is the one failure this command must not have.
1645
+
1646
+ A draft is **not a coverage map and confers no readiness**. The route is unchanged:
1647
+ author `.prd/coverage/<id>.json` by hand, review the links, `coverage` validates it,
1648
+ `coverage adopt --preview|--apply` adopts it, and `change authorize` records the
1649
+ user's instruction covering the new agreement. Editing the inputs afterwards still
1650
+ yields `AGREEMENT_CHANGED`.
1651
+
1566
1652
  Coverage JSON schema 1:
1567
1653
 
1568
1654
  ```
@@ -70,7 +70,12 @@ function migratedTicketReadiness({ text, fields, timeout, attempt, legacyReceipt
70
70
  default: return { ready: false, reasons: [reason('ATTEMPT_ERROR', `attempt ${id} has unknown outcome ${JSON.stringify(attempt.outcome)}`, 'verify')] };
71
71
  }
72
72
  if (current.prdRevision && attempt.context && attempt.context.prd_revision !== current.prdRevision) {
73
- reasons.push(reason('REVISION_CHANGED', 'the PRD revision changed since the passing attempt', 'register --rebind, then verify'));
73
+ // The remedy is mode-specific. `register --rebind` is the v0.4.1/v0.5.0 repair and
74
+ // changes mode refuses it outright (AGREEMENT_CHANGED: "--rebind is not supported
75
+ // for change records"), so naming it there sends the reader to a command the
76
+ // runtime will not run — the defect class T-80 closed for the coverage report.
77
+ reasons.push(reason('REVISION_CHANGED', 'the PRD revision changed since the passing attempt',
78
+ mode === 'changes' ? 'change revise, record its authorization, then verify' : 'register --rebind, then verify'));
74
79
  }
75
80
  if (attempt.check && attempt.check.digest !== parse.checkDigest(text, timeout)) {
76
81
  reasons.push(reason('CHECK_CHANGED', 'the Verification block or timeout changed since the passing attempt', 'verify'));
@@ -202,4 +202,72 @@ function render(r) {
202
202
  return `${lines.join('\n')}\n`;
203
203
  }
204
204
 
205
- module.exports = { SCHEMA, build, decide, render };
205
+ // --- The brief projection (docs/runtime-contracts.md, "Brief resume") ----------------
206
+ // A fresh session reads the full report to answer one question — what do I do next —
207
+ // and pays for every ticket row, attempt row and repeated blocker detail to get it.
208
+ // The brief keeps the answer and the counts and drops the repetition.
209
+ //
210
+ // It is a projection, not a second report: `next` is the full report's own object,
211
+ // copied, and every distinct blocker code survives with its exact count. Grouping may
212
+ // collapse repetition; it may never collapse a category, because a category is the
213
+ // reason a gate will refuse. Nothing here recomputes a verdict, caches readiness or
214
+ // decides an action, and `detail` names the command that prints every omitted row.
215
+ const BRIEF = 1;
216
+ const BRIEF_KIND = 'resume-brief';
217
+
218
+ function brief(r) {
219
+ const counts = {};
220
+ for (const b of r.blockers) counts[b.code] = (counts[b.code] || 0) + 1;
221
+ const byStatus = { open: 0, in_progress: 0, done: 0 };
222
+ let notReady = 0;
223
+ for (const t of r.tickets) {
224
+ if (t.status in byStatus) byStatus[t.status]++;
225
+ if (!t.readiness.ready) notReady++;
226
+ }
227
+ const running = r.attempts.filter(a => a.outcome === 'running').length;
228
+ const currentFailed = r.attempts.filter(a => a.current && a.outcome !== 'passed').length;
229
+ const cov = r.coverage;
230
+ return {
231
+ brief: BRIEF, kind: BRIEF_KIND, of: SCHEMA,
232
+ generated: r.generated, root: r.root, mode: r.mode,
233
+ selection: r.selection,
234
+ change: r.change ? { id: r.change.id, prd: r.change.prd, base: r.change.base, lifecycle: r.change.lifecycle.state } : null,
235
+ agreement: r.agreement ? { current: r.agreement.current, verdict: r.agreement.verdict, authorized: r.agreement.authorized ? { id: r.agreement.authorized.id, disposition: r.agreement.authorized.disposition } : null } : null,
236
+ coverage: cov ? { label: cov.label, strict: cov.strict, structure: cov.strict && cov.structure ? cov.structure.complete : null, implementation: cov.strict && cov.implementation ? cov.implementation.scenarios : null } : null,
237
+ tickets: { total: r.tickets.length, by_status: byStatus, not_ready: notReady },
238
+ attempts: { total: r.attempts.length, running, current_failed: currentFailed },
239
+ candidate: r.candidate ? { notes: r.candidate.notes, candidate: r.candidate.candidate, evidence: r.candidate.evidence ? r.candidate.evidence.verdict : null } : null,
240
+ blockers: { total: r.blockers.length, categories: Object.keys(counts).map(code => ({ code, count: counts[code] })) },
241
+ next: r.next,
242
+ detail: {
243
+ command: `${RUNTIME} resume${r.change ? ` --change ${r.change.id}` : ''}`,
244
+ prd: r.references ? r.references.prd.path : null,
245
+ tickets: r.references ? r.references.tickets.map(t => t.file) : [],
246
+ omitted: r.tickets.length + r.attempts.length + Math.max(0, r.blockers.length - Object.keys(counts).length),
247
+ },
248
+ };
249
+ }
250
+
251
+ function renderBrief(b) {
252
+ const lines = [];
253
+ const short = s => (typeof s === 'string' ? s.slice(0, 12) : '—');
254
+ lines.push(`PINCER resume --brief · ${b.generated} · ${b.root}`);
255
+ if (!b.change) {
256
+ lines.push(`Selection ${b.selection && b.selection.change ? b.selection.change : 'none'}${b.selection && b.selection.problem ? ` · ${b.selection.problem.code}: ${b.selection.problem.detail}` : ''}`);
257
+ } else {
258
+ const c = b.change, a = b.agreement;
259
+ lines.push(`Change ${c.id} · ${c.prd} · base ${c.base.slice(0, 7)} · ${c.lifecycle}`);
260
+ lines.push(`Agreement ${a.current ? short(a.current) : 'unavailable'} · ${a.verdict}${a.authorized ? ` · authorized ${a.authorized.id} (${a.authorized.disposition})` : ' · not authorized'}`);
261
+ if (b.coverage) lines.push(`Coverage ${b.coverage.label}${b.coverage.strict ? ` · structure ${b.coverage.structure ? 'complete' : 'incomplete'} · implementation ${b.coverage.implementation.complete}/${b.coverage.implementation.total - b.coverage.implementation.dispositioned} scenarios` : ''}`);
262
+ const t = b.tickets;
263
+ lines.push(`Tickets ${t.total} · ${t.by_status.done} done · ${t.by_status.in_progress} in progress · ${t.by_status.open} open · ${t.not_ready} not ready`);
264
+ lines.push(`Attempts ${b.attempts.total}${b.attempts.running ? ` · ${b.attempts.running} running` : ''}${b.attempts.current_failed ? ` · ${b.attempts.current_failed} current not passed` : ''}`);
265
+ if (b.candidate) lines.push(`Candidate ${b.candidate.notes === 'current' ? `current (${String(b.candidate.candidate).slice(0, 7)})` : b.candidate.notes}${b.candidate.evidence ? ` · evidence ${b.candidate.evidence}` : ''}`);
266
+ }
267
+ lines.push(`Blockers ${b.blockers.total ? b.blockers.categories.map(x => `${x.code}${x.count > 1 ? ` ×${x.count}` : ''}`).join(' · ') : 'none'}`);
268
+ lines.push(`Next ${b.next.action}: ${b.next.command}`);
269
+ lines.push(`Detail ${b.detail.command}${b.detail.omitted ? ` (${b.detail.omitted} row(s) not shown)` : ''}${b.detail.prd ? ` · ${b.detail.prd}` : ''}`);
270
+ return `${lines.join('\n')}\n`;
271
+ }
272
+
273
+ module.exports = { SCHEMA, BRIEF, BRIEF_KIND, build, decide, render, brief, renderBrief };
@@ -0,0 +1,254 @@
1
+ 'use strict';
2
+ // PINCER runtime — the coverage draft (docs/runtime-contracts.md, "Coverage draft").
3
+ // `coverage scaffold --change <id>` projects the validated PRD inventory, the change's
4
+ // tickets and any authored map into one reviewable draft. It exists because authoring
5
+ // `.prd/coverage/<id>.json` means transcribing every live scenario, every ticket role
6
+ // and every check declaration out of documents the runtime has already parsed; that
7
+ // transcription is the measured cost this removes (docs/prd-v7-pilots.md).
8
+ //
9
+ // What it must never do is decide. The draft carries no semantic judgment: an
10
+ // unresolved scenario stays unresolved, a check command is never invented, a ticket
11
+ // role is never guessed, and a scope disposition is never chosen. Candidate tickets
12
+ // and their existing verification text are listed as *material to read*, with their
13
+ // file provenance, never promoted into a link or a declaration.
14
+ //
15
+ // The envelope is `draft: 1` with no `schema` key, so `coverage.validateMap` refuses
16
+ // it: a draft written to disk is not a coverage map and cannot become one by being
17
+ // saved. The real map is authored by hand, validated by `coverage`, adopted by
18
+ // `coverage adopt` and authorized by `change authorize`, exactly as before.
19
+ //
20
+ // Pure and read-only: nothing here writes a file, launches a check, changes a
21
+ // selection or records an approval. The draft body carries no timestamp, so two calls
22
+ // on identical authored inputs produce byte-identical output.
23
+ const fs = require('node:fs');
24
+ const path = require('node:path');
25
+ const parse = require('./parse.cjs');
26
+ const requirements = require('./requirements.cjs');
27
+ const coverage = require('./coverage.cjs');
28
+
29
+ const DRAFT = 1;
30
+ const KIND = 'coverage-draft';
31
+ const RUNTIME = 'node scripts/pincer-runtime.cjs';
32
+ // Unresolved codes, in report order. These describe the draft, not the runtime state:
33
+ // they say what a human still has to author, never what a gate will refuse.
34
+ const UNRESOLVED_ORDER = ['SCENARIO_UNLINKED', 'CHECK_UNDECLARED', 'TICKET_UNCLASSIFIED', 'SCENARIO_STALE', 'TICKET_FOREIGN'];
35
+
36
+ const AUTHORED = 'authored';
37
+ const UNRESOLVED = 'unresolved';
38
+
39
+ // The first line under a ticket heading, used as the objective a reviewer reads.
40
+ function firstLine(text, heading) {
41
+ const rows = parse.lines(text);
42
+ const i = rows.findIndex(l => l.trim() === heading);
43
+ if (i === -1) return null;
44
+ const next = rows.slice(i + 1).find(l => l.trim() !== '');
45
+ return next ? next.trim() : null;
46
+ }
47
+ // The IDs a ticket's own Context section claims ("- Implements: R-04." and
48
+ // "- Scenarios: S-10, S-11."). Classified against the live inventory rather than by
49
+ // prefix, because tickets name other tickets on the same lines — T-03 of the strict
50
+ // fixture says "Implements: none (enables T-01, T-02)", and reading that as two
51
+ // scenario claims would be the draft inventing a link out of prose. An ID the
52
+ // inventory does not define is not reported at all.
53
+ const CLAIM_LINE = /^[ \t]*[-+*][ \t]+(?:Implements|Scenarios|Requirements):/;
54
+ function claimsOf(text, inventory) {
55
+ const ids = new Set();
56
+ for (const row of parse.lines(text)) {
57
+ if (!CLAIM_LINE.test(row)) continue;
58
+ for (const id of row.match(new RegExp(requirements.ID, 'g')) || []) ids.add(id);
59
+ }
60
+ const claimed = [...ids];
61
+ return {
62
+ requirements: requirements.sortIds(claimed.filter(id => inventory.requirements[id])),
63
+ scenarios: requirements.sortIds(claimed.filter(id => inventory.scenarios[id])),
64
+ };
65
+ }
66
+ // The ticket's own Verification fence, verbatim, as provenance for a reviewer. It is
67
+ // quoted, never turned into a check declaration: whether it proves the scenario is a
68
+ // judgment the draft does not make.
69
+ function verificationOf(text) {
70
+ const commands = parse.verificationCommands(text);
71
+ return commands && commands.length ? commands.join('\n') : null;
72
+ }
73
+
74
+ // build(root, record) → { ok, code, problems, draft }
75
+ // `code` is set only when the inputs cannot be read; a missing map is the normal case
76
+ // this command exists for, not an error.
77
+ function build(root, record) {
78
+ const fail = (code, problems) => ({ ok: false, code, problems, draft: null });
79
+ const inv = requirements.readInventory(root, record.prd);
80
+ if (!inv.ok) return fail(inv.code, inv.problems);
81
+ const inventory = inv.inventory;
82
+
83
+ // An absent map is expected; an unreadable one is not — silently dropping authored
84
+ // content would be the one way this command could destroy work.
85
+ const rel = coverage.file(record.change);
86
+ const real = coverage.realFile(root, rel);
87
+ let authored = null;
88
+ if (!real) {
89
+ const m = coverage.readMap(root, record);
90
+ if (!m.ok) return fail(m.code, m.problems);
91
+ authored = m;
92
+ } else if (!real.endsWith(': missing')) {
93
+ return fail('COVERAGE_INVALID', [`${rel}: ${real.slice(rel.length + 2)}`]);
94
+ }
95
+
96
+ const t = coverage.ticketsOf(root, record);
97
+ if (!t.ok) return fail(t.code, t.problems);
98
+
99
+ const map = authored ? authored.map : null;
100
+ const unresolved = [];
101
+ const note = (code, id, detail) => unresolved.push({ code, id, detail });
102
+
103
+ // --- scenarios: every live scenario exactly once, authored rows preserved ----------
104
+ const scenarios = {};
105
+ const scope = {};
106
+ for (const id of requirements.sortIds(Object.keys(inventory.scenarios))) {
107
+ const live = inventory.scenarios[id];
108
+ const authoredScope = map && map.scope ? map.scope[id] : null;
109
+ if (authoredScope) {
110
+ scope[id] = { ...authoredScope, state: AUTHORED };
111
+ continue;
112
+ }
113
+ const row = map && map.scenarios ? map.scenarios[id] : null;
114
+ const tickets = row ? [...row.tickets] : [];
115
+ const checks = row ? [...row.checks] : [];
116
+ const resolved = Boolean(row) && tickets.length > 0 && checks.length > 0;
117
+ scenarios[id] = { requirement: live.requirement, state: resolved ? AUTHORED : UNRESOLVED, tickets, checks };
118
+ if (!row) note('SCENARIO_UNLINKED', id, `${id} (${live.requirement}) has no row in scenarios or scope`);
119
+ else if (!tickets.length) note('SCENARIO_UNLINKED', id, `${id} is linked to no ticket`);
120
+ else if (!checks.length) note('CHECK_UNDECLARED', id, `${id} is linked to no check`);
121
+ }
122
+ // A map row naming a scenario the inventory no longer defines is authored content:
123
+ // it is preserved and flagged, never deleted, because removing an obligation is a
124
+ // decision with its own disposition and authorization.
125
+ if (map) {
126
+ for (const id of requirements.sortIds(Object.keys(map.scenarios))) {
127
+ if (scenarios[id] || scope[id]) continue;
128
+ scenarios[id] = { requirement: null, state: UNRESOLVED, tickets: [...map.scenarios[id].tickets], checks: [...map.scenarios[id].checks] };
129
+ note('SCENARIO_STALE', id, `${id} has a row but is not a scenario of ${record.prd}`);
130
+ }
131
+ for (const id of requirements.sortIds(Object.keys(map.scope))) {
132
+ if (scope[id] || inventory.scenarios[id]) continue;
133
+ scope[id] = { ...map.scope[id], state: UNRESOLVED };
134
+ note('SCENARIO_STALE', id, `${id} has a scope row but is not a scenario of ${record.prd}`);
135
+ }
136
+ }
137
+
138
+ // --- tickets: authored roles preserved; unclassified tickets named, not classified --
139
+ const tickets = {};
140
+ for (const id of requirements.sortIds(Object.keys(t.tickets))) {
141
+ const row = map && map.tickets ? map.tickets[id] : null;
142
+ if (row) tickets[id] = { role: row.role, rationale: row.rationale, state: AUTHORED };
143
+ else {
144
+ tickets[id] = { role: null, rationale: null, state: UNRESOLVED };
145
+ note('TICKET_UNCLASSIFIED', id, `${id} has no role; classify it as implements or enables (an enabling ticket needs a rationale)`);
146
+ }
147
+ }
148
+ if (map) {
149
+ for (const id of requirements.sortIds(Object.keys(map.tickets))) {
150
+ if (tickets[id]) continue;
151
+ tickets[id] = { role: map.tickets[id].role, rationale: map.tickets[id].rationale, state: UNRESOLVED };
152
+ note(t.others[id] ? 'TICKET_FOREIGN' : 'SCENARIO_STALE', id,
153
+ t.others[id] ? `${id} is a ticket of ${t.others[id]}, not of ${record.prd}` : `${id} has a row but no ticket file is associated with ${record.prd}`);
154
+ }
155
+ }
156
+
157
+ // --- checks: authored declarations preserved verbatim; none invented ---------------
158
+ const checks = {};
159
+ if (map) for (const id of Object.keys(map.checks).sort()) checks[id] = { ...map.checks[id], state: AUTHORED };
160
+ // Every check a scenario links to must be declared; an undeclared one is named.
161
+ for (const [sid, row] of Object.entries(scenarios)) {
162
+ for (const c of row.checks) {
163
+ if (checks[c]) continue;
164
+ note('CHECK_UNDECLARED', c, `${c} is linked from ${sid} but has no declaration`);
165
+ }
166
+ }
167
+
168
+ // --- candidates: material to read, with provenance. Never a link. -----------------
169
+ const candidates = { tickets: {} };
170
+ for (const id of requirements.sortIds(Object.keys(t.tickets))) {
171
+ const entry = t.tickets[id];
172
+ const claims = claimsOf(entry.text, inventory);
173
+ candidates.tickets[id] = {
174
+ file: entry.file,
175
+ objective: firstLine(entry.text, '## Objective'),
176
+ implements: claims.requirements,
177
+ scenarios: claims.scenarios,
178
+ verification: verificationOf(entry.text),
179
+ };
180
+ }
181
+
182
+ unresolved.sort((a, b) => (UNRESOLVED_ORDER.indexOf(a.code) - UNRESOLVED_ORDER.indexOf(b.code)) || requirements.compareIds(a.id, b.id));
183
+ const draft = {
184
+ draft: DRAFT,
185
+ kind: KIND,
186
+ change: record.change,
187
+ prd: record.prd,
188
+ inventory: { digest: inventory.digest, requirements: Object.keys(inventory.requirements).length, scenarios: Object.keys(inventory.scenarios).length },
189
+ authored: { map: authored ? authored.file : null, digest: authored ? authored.digest : null },
190
+ scenarios, scope, tickets, checks, candidates, unresolved,
191
+ next: nextAction(record, unresolved, authored),
192
+ };
193
+ return { ok: true, code: null, problems: [], draft };
194
+ }
195
+
196
+ // What the reader does next. A complete draft does not adopt anything: it says the map
197
+ // is ready to be reviewed and validated, which is a different claim from "correct".
198
+ function nextAction(record, unresolved, authored) {
199
+ if (unresolved.length) {
200
+ const first = unresolved[0];
201
+ return { action: `author the unresolved entries (${unresolved.length}), starting with ${first.id}`, command: `edit ${coverage.file(record.change)} — ${first.detail}` };
202
+ }
203
+ if (!authored) return { action: 'author the map from this draft, then validate it', command: `edit ${coverage.file(record.change)}, then ${RUNTIME} coverage --change ${record.change}` };
204
+ return { action: 'review the authored map, then preview adoption', command: `${RUNTIME} coverage adopt --preview --change ${record.change}` };
205
+ }
206
+
207
+ function render(d) {
208
+ const lines = [];
209
+ const count = o => Object.keys(o).length;
210
+ lines.push(`PINCER coverage draft · change ${d.change} · ${d.prd}`);
211
+ lines.push(`Inventory ${d.inventory.requirements} requirement(s), ${d.inventory.scenarios} scenario(s) · digest ${d.inventory.digest.slice(0, 12)}`);
212
+ lines.push(`Authored ${d.authored.map ? `${d.authored.map} · digest ${d.authored.digest.slice(0, 12)}` : 'no map yet'}`);
213
+ lines.push('');
214
+ lines.push(`Scenarios ${count(d.scenarios)} linked row(s), ${count(d.scope)} scope row(s)`);
215
+ for (const [id, s] of Object.entries(d.scenarios)) {
216
+ const marker = s.state === AUTHORED ? ' ' : '?';
217
+ lines.push(` ${marker} ${id} ${s.requirement || '(not in the inventory)'} tickets ${s.tickets.length ? s.tickets.join(', ') : '—'} checks ${s.checks.length ? s.checks.join(', ') : '—'}`);
218
+ }
219
+ for (const [id, s] of Object.entries(d.scope)) {
220
+ lines.push(` ${s.state === AUTHORED ? ' ' : '?'} ${id} scope: ${s.disposition}${s.decision ? ` (${s.decision})` : ''}${s.note ? ` — ${s.note}` : ''}`);
221
+ }
222
+ lines.push('');
223
+ lines.push(`Tickets ${count(d.tickets)}`);
224
+ for (const [id, t] of Object.entries(d.tickets)) {
225
+ const c = d.candidates.tickets[id];
226
+ lines.push(` ${t.state === AUTHORED ? ' ' : '?'} ${id} role ${t.role || '—'}${t.rationale ? ` (${t.rationale})` : ''}${c && c.objective ? ` · ${c.objective}` : ''}`);
227
+ if (t.state !== AUTHORED && c) {
228
+ const claimed = [...c.implements, ...c.scenarios];
229
+ if (claimed.length) lines.push(` the ticket says it implements ${claimed.join(', ')} (${c.file}) — a statement to check, not a link`);
230
+ if (c.verification) lines.push(` its Verification block runs: ${c.verification.split('\n')[0]}${c.verification.includes('\n') ? ' …' : ''}`);
231
+ }
232
+ }
233
+ lines.push('');
234
+ lines.push(`Checks ${count(d.checks)}`);
235
+ for (const [id, c] of Object.entries(d.checks)) {
236
+ lines.push(` ${c.state === AUTHORED ? ' ' : '?'} ${id} ${c.kind}${c.required ? ', required' : ''} ${c.kind === 'command' ? c.command : c.obligation}`);
237
+ }
238
+ if (!count(d.checks)) lines.push(' none declared — a check is authored, never derived from a ticket');
239
+ lines.push('');
240
+ if (d.unresolved.length) {
241
+ lines.push(`Unresolved ${d.unresolved.length}`);
242
+ for (const u of d.unresolved) lines.push(` ${u.code} ${u.detail}`);
243
+ } else {
244
+ lines.push('Unresolved none — every scenario has a row, every ticket a role, every linked check a declaration');
245
+ }
246
+ lines.push('');
247
+ lines.push('This is a draft, not a coverage map: it carries no schema, and `coverage`,');
248
+ lines.push('`coverage adopt` and the agreement digest do not accept it. Author');
249
+ lines.push(`${coverage.file(d.change)} yourself, review the links, then validate and adopt it.`);
250
+ lines.push(`Next ${d.next.action}: ${d.next.command}`);
251
+ return `${lines.join('\n')}\n`;
252
+ }
253
+
254
+ module.exports = { DRAFT, KIND, UNRESOLVED_ORDER, AUTHORED, UNRESOLVED, build, render, nextAction };
@@ -16,8 +16,8 @@
16
16
  // node scripts/pincer-runtime.cjs change authorize <id> --agreement <digest> (--reference <text> --excerpt <text> | --delegated --basis A-NN --explanation <text>) [--decision D-NN]...
17
17
  // node scripts/pincer-runtime.cjs change decide <id> --summary <text> [--id D-NN] | --resolve D-NN --reference <text> --excerpt <text>
18
18
  // node scripts/pincer-runtime.cjs change activate|pause|resume|complete|reopen|cancel|supersede <id> [--reason <text>] [--note <text>] [--decision D-NN] [--with <id>]
19
- // node scripts/pincer-runtime.cjs resume [--change <id>] [--json]
20
- // node scripts/pincer-runtime.cjs coverage [--change <id>] [--json] · coverage adopt --preview|--apply --change <id> [--agreement <digest>]
19
+ // node scripts/pincer-runtime.cjs resume [--change <id>] [--brief] [--json]
20
+ // node scripts/pincer-runtime.cjs coverage [--change <id>] [--json] · coverage scaffold --change <id> [--json] · coverage adopt --preview|--apply --change <id> [--agreement <digest>]
21
21
  // node scripts/pincer-runtime.cjs impact [--change <id>] [--from G-NN|A-NN] [--json]
22
22
  //
23
23
  // Exit codes: 0 ok · 1 failed/not ready/refused · 2 usage · 3 state busy ·
@@ -77,8 +77,8 @@ function usage(message) {
77
77
  ' pincer-runtime.cjs change decide <id> --summary <text> [--id D-NN] | --resolve D-NN --reference <text> --excerpt <text>\n' +
78
78
  ' pincer-runtime.cjs change activate|resume|complete <id> · change pause <id> --reason <text> [--note <text>] · change reopen <id> --reason <text>\n' +
79
79
  ' pincer-runtime.cjs change cancel <id> --decision D-NN --reason <text> · change supersede <id> --with <id> --decision D-NN\n' +
80
- ' pincer-runtime.cjs resume [--change <id>] [--json] (the read-only report; `change resume` is the lifecycle operation)\n' +
81
- ' pincer-runtime.cjs coverage [--change <id>] [--json] · coverage adopt --preview|--apply --change <id> [--agreement <digest>]\n' +
80
+ ' pincer-runtime.cjs resume [--change <id>] [--brief] [--json] (the read-only report; `change resume` is the lifecycle operation)\n' +
81
+ ' pincer-runtime.cjs coverage [--change <id>] [--json] · coverage scaffold --change <id> [--json] · coverage adopt --preview|--apply --change <id> [--agreement <digest>]\n' +
82
82
  ' pincer-runtime.cjs impact [--change <id>] [--from G-NN|A-NN] [--json]\n');
83
83
  process.exit(EXIT.USAGE);
84
84
  }
@@ -422,6 +422,22 @@ function cmdRegister(root, args) {
422
422
  // entry into strict coverage (docs/runtime-contracts.md, "Adoption and rollback").
423
423
  function cmdCoverage(root, args) {
424
424
  const [sub, ...rest] = args;
425
+ // coverage scaffold --change <id> [--json] — the read-only coverage draft
426
+ // (docs/runtime-contracts.md, "Coverage draft"). Writes nothing and launches nothing;
427
+ // the draft it prints is not a map and `coverage adopt` does not accept it.
428
+ if (sub === 'scaffold') {
429
+ const o = parseOptions(rest, { switches: ['--json'], valued: ['--change'] });
430
+ if (o.positional.length) usage(`unexpected argument ${o.positional[0]} (coverage scaffold takes --change <id> [--json])`);
431
+ if (!o.change) usage('coverage scaffold requires --change <id>');
432
+ const scaffold = require('./pincer-runtime/scaffold.cjs');
433
+ const resolved = changes.resolveSelected(root, { change: o.change });
434
+ if (resolved.code) fail('pincer', `${resolved.code}: ${resolved.problem}`, exitForCode(resolved.code));
435
+ const result = scaffold.build(root, resolved.record);
436
+ if (!result.ok) fail('pincer', `${result.code}: ${result.problems[0]}`, exitForCode(result.code));
437
+ if (o.json) io.out(`${JSON.stringify(result.draft, null, 2)}\n`);
438
+ else io.out(scaffold.render(result.draft));
439
+ process.exit(EXIT.OK);
440
+ }
425
441
  if (sub !== 'adopt') {
426
442
  // The read-only coverage report (docs/runtime-contracts.md, "Coverage and impact commands").
427
443
  const o = parseOptions(args, { switches: ['--json'], valued: ['--change'] });
@@ -476,9 +492,18 @@ function cmdImpact(root, args) {
476
492
 
477
493
  // resume [--change <id>] [--json] — the read-only resume report (never the lifecycle operation).
478
494
  function cmdResume(root, args) {
479
- const o = parseOptions(args, { switches: ['--json'], valued: ['--change'] });
495
+ const o = parseOptions(args, { switches: ['--json', '--brief'], valued: ['--change'] });
480
496
  if (o.positional.length) usage(`unexpected argument ${o.positional[0]}`);
481
497
  const result = resume.build(root, { change: o.change || null });
498
+ // --brief is a projection of the same computed report: same verdict, same next
499
+ // action, same blocker categories, fewer bytes (docs/runtime-contracts.md, "Brief
500
+ // resume"). It never recomputes a decision and never changes the exit code.
501
+ if (o.brief) {
502
+ const brief = resume.brief(result.json);
503
+ if (o.json) io.out(`${JSON.stringify(brief, null, 2)}\n`);
504
+ else io.out(resume.renderBrief(brief));
505
+ process.exit(result.exit);
506
+ }
482
507
  if (o.json) io.out(`${JSON.stringify(result.json, null, 2)}\n`);
483
508
  else io.out(result.text);
484
509
  process.exit(result.exit);