pincer-workflow 0.6.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/README.md +157 -106
  2. package/bin/pincer.js +189 -166
  3. package/package.json +4 -3
  4. package/template/.agents/skills/pincer-blueprint/SKILL.md +70 -0
  5. package/template/.agents/skills/pincer-status/SKILL.md +29 -41
  6. package/template/.claude/commands/pincer-blueprint.md +7 -0
  7. package/template/.claude/commands/pincer-status.md +4 -45
  8. package/template/.claude/hooks/hook-policy.cjs +60 -25
  9. package/template/.claude/hooks/ticket-guard.sh +6 -3
  10. package/template/.claude/pincer/library/agents/accessibility-reviewer.md +27 -0
  11. package/template/.claude/pincer/library/agents/code-quality-reviewer.md +25 -0
  12. package/template/.claude/pincer/library/agents/codebase-explorer.md +23 -0
  13. package/template/.claude/pincer/library/agents/fix-agent.md +23 -0
  14. package/template/.claude/pincer/library/agents/history-reviewer.md +26 -0
  15. package/template/.claude/pincer/library/agents/implementer.md +24 -0
  16. package/template/.claude/pincer/library/agents/security-reviewer.md +26 -0
  17. package/template/.claude/pincer/library/agents/silent-failure-hunter.md +27 -0
  18. package/template/.claude/pincer/library/agents/standards-reviewer.md +25 -0
  19. package/template/.claude/pincer/library/agents/test-coverage-reviewer.md +26 -0
  20. package/template/.claude/pincer/library/agents-skeleton.md +89 -0
  21. package/template/.claude/pincer/library/agreement.md +46 -0
  22. package/template/.claude/pincer/library/models.json +6 -0
  23. package/template/.claude/pincer/library/stages/blueprint/SKILL.md +69 -0
  24. package/template/.claude/pincer/library/stages/blueprint/formats/harness.md +82 -0
  25. package/template/.claude/pincer/library/stages/blueprint/formats/roster-rules.md +61 -0
  26. package/template/.claude/pincer/library/stages/blueprint/prompts/confirm-kind.md +19 -0
  27. package/template/.claude/pincer/library/stages/blueprint/prompts/first-job.md +18 -0
  28. package/template/.claude/pincer/library/stages/blueprint/prompts/q-handoffs.md +21 -0
  29. package/template/.claude/pincer/library/stages/blueprint/prompts/q-models.md +20 -0
  30. package/template/.claude/pincer/library/stages/blueprint/prompts/q-pipeline.md +27 -0
  31. package/template/.claude/pincer/library/stages/blueprint/prompts/q-repo.md +21 -0
  32. package/template/.claude/pincer/library/stages/blueprint/prompts/q-responsibility.md +30 -0
  33. package/template/.claude/pincer/library/stages/blueprint/prompts/q-roster.md +20 -0
  34. package/template/.claude/pincer/library/stages/blueprint/prompts/q-simplify.md +26 -0
  35. package/template/.claude/pincer/library/stages/blueprint/prompts/q-systems.md +28 -0
  36. package/template/.claude/pincer/library/stages/code/SKILL.md +59 -0
  37. package/template/.claude/pincer/library/stages/code/formats/pr.md +55 -0
  38. package/template/.claude/pincer/library/stages/code/prompts/implement.md +25 -0
  39. package/template/.claude/pincer/library/stages/evaluate/SKILL.md +51 -0
  40. package/template/.claude/pincer/library/stages/evaluate/formats/evidence.md +49 -0
  41. package/template/.claude/pincer/library/stages/evaluate/formats/finding-report.md +72 -0
  42. package/template/.claude/pincer/library/stages/evaluate/formats/finding.md +35 -0
  43. package/template/.claude/pincer/library/stages/evaluate/prompts/review.md +25 -0
  44. package/template/.claude/pincer/library/stages/narrow/SKILL.md +47 -0
  45. package/template/.claude/pincer/library/stages/narrow/formats/ticket.md +126 -0
  46. package/template/.claude/pincer/library/stages/narrow/prompts/explore.md +19 -0
  47. package/template/.claude/pincer/library/stages/plan/SKILL.md +46 -0
  48. package/template/.claude/pincer/library/stages/plan/formats/spec.md +100 -0
  49. package/template/.claude/pincer/library/stages/plan/prompts/explore.md +20 -0
  50. package/template/.claude/pincer/library/stages/release/SKILL.md +48 -0
  51. package/template/.claude/pincer/library/stages/status/SKILL.md +37 -0
  52. package/template/.claude/skills/pincer-blueprint/SKILL.md +69 -0
  53. package/template/.claude/skills/pincer-blueprint/formats/harness.md +82 -0
  54. package/template/.claude/skills/pincer-blueprint/formats/roster-rules.md +61 -0
  55. package/template/.claude/skills/pincer-blueprint/prompts/confirm-kind.md +19 -0
  56. package/template/.claude/skills/pincer-blueprint/prompts/first-job.md +18 -0
  57. package/template/.claude/skills/pincer-blueprint/prompts/q-handoffs.md +21 -0
  58. package/template/.claude/skills/pincer-blueprint/prompts/q-models.md +20 -0
  59. package/template/.claude/skills/pincer-blueprint/prompts/q-pipeline.md +27 -0
  60. package/template/.claude/skills/pincer-blueprint/prompts/q-repo.md +21 -0
  61. package/template/.claude/skills/pincer-blueprint/prompts/q-responsibility.md +30 -0
  62. package/template/.claude/skills/pincer-blueprint/prompts/q-roster.md +20 -0
  63. package/template/.claude/skills/pincer-blueprint/prompts/q-simplify.md +26 -0
  64. package/template/.claude/skills/pincer-blueprint/prompts/q-systems.md +28 -0
  65. package/template/.claude/skills/pincer-status/SKILL.md +37 -0
  66. package/template/.codex/README.md +39 -35
  67. package/template/.github/copilot-instructions.md +19 -14
  68. package/template/.github/prompts/pincer-blueprint.prompt.md +70 -0
  69. package/template/.github/prompts/pincer-status.prompt.md +29 -41
  70. package/template/AGENTS.md +34 -67
  71. package/template/docs/engine.md +236 -0
  72. package/template/docs/release-checklist.md +22 -28
  73. package/template/scripts/pincer/blueprint.cjs +137 -0
  74. package/template/scripts/pincer/cli.cjs +233 -0
  75. package/template/scripts/pincer/evidence.cjs +142 -0
  76. package/template/scripts/pincer/generate.cjs +373 -0
  77. package/template/scripts/pincer/harness.cjs +269 -0
  78. package/template/scripts/pincer/models.cjs +121 -0
  79. package/template/scripts/{pincer-runtime → pincer}/parse.cjs +83 -133
  80. package/template/scripts/pincer/recon.cjs +205 -0
  81. package/template/scripts/pincer/status.cjs +161 -0
  82. package/template/scripts/pincer/ticket.cjs +203 -0
  83. package/template/scripts/pincer/util.cjs +104 -0
  84. package/template/scripts/pincer-blueprint.cjs +7 -0
  85. package/template/scripts/pincer-evidence.cjs +5 -73
  86. package/template/scripts/pincer-status.sh +10 -12
  87. package/template/scripts/pincer-ticket.sh +15 -18
  88. package/template/scripts/sync-prompts.sh +2 -50
  89. package/template/.agents/skills/pincer-code/SKILL.md +0 -237
  90. package/template/.agents/skills/pincer-evaluate/SKILL.md +0 -178
  91. package/template/.agents/skills/pincer-narrow/SKILL.md +0 -138
  92. package/template/.agents/skills/pincer-plan/SKILL.md +0 -134
  93. package/template/.agents/skills/pincer-release/SKILL.md +0 -82
  94. package/template/.claude/agents/code-quality-reviewer.md +0 -38
  95. package/template/.claude/agents/codebase-explorer.md +0 -23
  96. package/template/.claude/commands/pincer-code.md +0 -235
  97. package/template/.claude/commands/pincer-evaluate.md +0 -176
  98. package/template/.claude/commands/pincer-narrow.md +0 -136
  99. package/template/.claude/commands/pincer-plan.md +0 -132
  100. package/template/.claude/commands/pincer-release.md +0 -80
  101. package/template/.claude/references/prd-template.md +0 -108
  102. package/template/.claude/references/ticket-template.md +0 -86
  103. package/template/.github/prompts/pincer-code.prompt.md +0 -237
  104. package/template/.github/prompts/pincer-evaluate.prompt.md +0 -178
  105. package/template/.github/prompts/pincer-narrow.prompt.md +0 -138
  106. package/template/.github/prompts/pincer-plan.prompt.md +0 -134
  107. package/template/.github/prompts/pincer-release.prompt.md +0 -82
  108. package/template/docs/dry-run-checklist.md +0 -264
  109. package/template/docs/runtime-contracts.md +0 -1683
  110. package/template/scripts/pincer-runtime/adopt.cjs +0 -132
  111. package/template/scripts/pincer-runtime/agreement.cjs +0 -240
  112. package/template/scripts/pincer-runtime/authorization.cjs +0 -167
  113. package/template/scripts/pincer-runtime/changes.cjs +0 -517
  114. package/template/scripts/pincer-runtime/checks.cjs +0 -48
  115. package/template/scripts/pincer-runtime/coverage.cjs +0 -361
  116. package/template/scripts/pincer-runtime/dispositions.cjs +0 -95
  117. package/template/scripts/pincer-runtime/evidence.cjs +0 -676
  118. package/template/scripts/pincer-runtime/fsutil.cjs +0 -37
  119. package/template/scripts/pincer-runtime/gates.cjs +0 -73
  120. package/template/scripts/pincer-runtime/identity.cjs +0 -163
  121. package/template/scripts/pincer-runtime/impact.cjs +0 -177
  122. package/template/scripts/pincer-runtime/io.cjs +0 -41
  123. package/template/scripts/pincer-runtime/lifecycle.cjs +0 -311
  124. package/template/scripts/pincer-runtime/locator.cjs +0 -158
  125. package/template/scripts/pincer-runtime/migrate.cjs +0 -204
  126. package/template/scripts/pincer-runtime/phases.cjs +0 -245
  127. package/template/scripts/pincer-runtime/readiness.cjs +0 -97
  128. package/template/scripts/pincer-runtime/requirements.cjs +0 -255
  129. package/template/scripts/pincer-runtime/resume.cjs +0 -205
  130. package/template/scripts/pincer-runtime/routing.cjs +0 -54
  131. package/template/scripts/pincer-runtime/runner.cjs +0 -242
  132. package/template/scripts/pincer-runtime/sanitize.cjs +0 -63
  133. package/template/scripts/pincer-runtime/source.cjs +0 -129
  134. package/template/scripts/pincer-runtime/state.cjs +0 -314
  135. package/template/scripts/pincer-runtime/status.cjs +0 -514
  136. package/template/scripts/pincer-runtime/transaction.cjs +0 -200
  137. package/template/scripts/pincer-runtime/transitions.cjs +0 -134
  138. package/template/scripts/pincer-runtime.cjs +0 -661
@@ -0,0 +1,161 @@
1
+ 'use strict';
2
+ // PINCER engine — where the work stands (docs/engine.md, "Stage gate"), read
3
+ // from harness.json and the artifacts on disk. Read-only: nothing here writes
4
+ // a file or runs a check. Every stage skill runs the gate first.
5
+ const fs = require('node:fs');
6
+ const path = require('node:path');
7
+ const parse = require('./parse.cjs');
8
+ const harnessModule = require('./harness.cjs');
9
+ const models = require('./models.cjs');
10
+ const generate = require('./generate.cjs');
11
+
12
+ const SCRIPT = 'scripts/pincer-ticket.sh';
13
+ const { STAGES, ALWAYS_ON, HARNESS_FILE } = harnessModule;
14
+
15
+ // Is a ticket's receipt current? Lifted from 0.7: the wording is what a done
16
+ // ticket that needs attention has always shown.
17
+ function readiness(text, fields) {
18
+ const receipt = fields.verified || '';
19
+ const attempt = fields.last_check || '';
20
+ const hash = parse.blockHash(text);
21
+ const fail = (message) => ({ ready: false, message });
22
+ if (!attempt) return fail('missing latest verification outcome — re-run verify');
23
+ const parts = attempt.split(/[ \t]+/);
24
+ if (!(parts.length === 3 && parse.TIMESTAMP.test(parts[0]) && parts[1] === 'passed' && /^[a-f0-9]{12}$/.test(parts[2])) || parts[2] !== hash) {
25
+ return fail(`latest verification: ${attempt} — re-run verify`);
26
+ }
27
+ if (!receipt) return fail('done without a verification receipt — re-run verify');
28
+ const rparts = receipt.split(/[ \t]+/);
29
+ if (!(rparts.length === 2 && parse.TIMESTAMP.test(rparts[0]) && /^[a-f0-9]{12}$/.test(rparts[1])) || rparts[1] !== hash) {
30
+ return fail('stale or malformed verification receipt — re-run verify');
31
+ }
32
+ if (parse.unticked(text).length) return fail('unticked acceptance criteria — complete and re-run verify');
33
+ return { ready: true, message: null };
34
+ }
35
+
36
+ const idOf = (file) => (path.basename(file).match(/^T-[0-9]+/) || [''])[0];
37
+ const readText = (root, file) => { try { return fs.readFileSync(path.join(root, file), 'utf8'); } catch (error) { return null; } };
38
+
39
+ function specEntries(root, specsDir) {
40
+ const dir = path.join(root, specsDir);
41
+ if (!fs.existsSync(dir)) return [];
42
+ return fs.readdirSync(dir).filter((name) => name.endsWith('.md')).sort().map((name) => {
43
+ const file = path.posix.join(specsDir, name);
44
+ const text = readText(root, file) || '';
45
+ return { file, status: parse.frontmatterField(text, 'status') || null };
46
+ });
47
+ }
48
+ function ticketEntry(root, file) {
49
+ const text = readText(root, file);
50
+ const entry = { id: idOf(file), file, status: null, started: null, verified: null, lastCheck: null, accepted: null, acceptedBy: null, ready: false, problem: null, depends: [] };
51
+ if (text === null) { entry.problem = 'cannot read the file'; return entry; }
52
+ const v = parse.validateTicket(file, text);
53
+ const fields = v.fields;
54
+ entry.status = fields.status || null;
55
+ entry.started = fields.started || null;
56
+ entry.verified = fields.verified || null;
57
+ entry.lastCheck = fields.last_check || null;
58
+ entry.accepted = fields.accepted || null;
59
+ entry.acceptedBy = fields.accepted_by || null;
60
+ entry.depends = parse.dependencies(fields);
61
+ if (!v.ok) { entry.problem = v.problems.join('; '); return entry; }
62
+ entry.id = fields.ticket;
63
+ const r = readiness(text, fields);
64
+ entry.ready = r.ready;
65
+ entry.readiness = r.message;
66
+ return entry;
67
+ }
68
+ function manifests(root, evidenceDir) {
69
+ const dir = path.join(root, evidenceDir);
70
+ if (!fs.existsSync(dir)) return [];
71
+ return fs.readdirSync(dir).sort()
72
+ .map((name) => path.posix.join(evidenceDir, name, 'manifest.json'))
73
+ .filter((file) => fs.existsSync(path.join(root, file)));
74
+ }
75
+
76
+ // The whole picture, as data. `harness` is null without a valid agreement;
77
+ // `harnessProblem` says why when the file exists but does not validate.
78
+ function gather(root) {
79
+ const report = { name: path.basename(root), harness: null, harnessProblem: null, stages: {}, models: null, specs: [], tickets: [], evidence: [], warnings: [], next: '' };
80
+ const loaded = harnessModule.load(root);
81
+ let homes = harnessModule.homes(null);
82
+ if (loaded.ok) {
83
+ report.harness = loaded.harness;
84
+ if (loaded.harness.project.name) report.name = loaded.harness.project.name;
85
+ for (const name of STAGES) report.stages[name] = harnessModule.stageOn(loaded.harness, name) ? 'on' : 'off';
86
+ report.models = models.effective(loaded.harness, generate.libraryDir(root));
87
+ homes = harnessModule.homes(loaded.harness);
88
+ } else if (!loaded.missing) {
89
+ report.harnessProblem = loaded.message;
90
+ report.warnings.push(`${HARNESS_FILE}: ${loaded.message}`);
91
+ }
92
+ report.specs = specEntries(root, homes.specs);
93
+ report.tickets = parse.ticketFiles(root, homes.tickets).map((file) => ticketEntry(root, file));
94
+ report.evidence = manifests(root, homes.evidence);
95
+ for (const t of report.tickets) {
96
+ if (t.problem) { report.warnings.push(`${t.file}: ${t.problem}`); continue; }
97
+ if (t.status === 'done' && !t.ready) report.warnings.push(`${t.id} done: ${t.readiness}`);
98
+ if (t.status === 'done' && !t.accepted) report.warnings.push(`${t.id} done without acceptance`);
99
+ }
100
+ report.next = nextCommand(report);
101
+ return report;
102
+ }
103
+
104
+ // The one thing to do next, first rule that matches.
105
+ function nextCommand(report) {
106
+ if (!report.harness) return '/pincer-blueprint';
107
+ const tickets = report.tickets;
108
+ const broken = tickets.find((t) => t.problem);
109
+ if (broken) return `fix ${broken.file}`;
110
+ if (report.stages.plan === 'on' && report.specs.length === 0 && tickets.length === 0) return '/pincer-plan <brief>';
111
+ if (tickets.length === 0) return '/pincer-narrow';
112
+ const inProgress = tickets.find((t) => t.status === 'in_progress');
113
+ if (inProgress) return `${SCRIPT} verify ${inProgress.id}`;
114
+ if (tickets.some((t) => t.status === 'open')) return '/pincer-code';
115
+ const stale = tickets.find((t) => t.status === 'done' && !t.ready);
116
+ if (stale) return `${SCRIPT} verify ${stale.id}`;
117
+ if (report.stages.evaluate === 'on' && report.harness.stages.evaluate.evidence && report.evidence.length === 0) return '/pincer-evaluate';
118
+ const unaccepted = tickets.find((t) => t.status === 'done' && !t.accepted);
119
+ if (unaccepted) {
120
+ // Acceptance is the floor even with release off: then the next step is the accept call itself.
121
+ if (harnessModule.stageOn(report.harness, 'release')) return '/pincer-release';
122
+ return `${SCRIPT} accept ${unaccepted.id} --by "<name>" --words "<their words>"`;
123
+ }
124
+ return 'all tickets done and accepted';
125
+ }
126
+
127
+ function ticketLine(t) {
128
+ if (t.problem) return `${t.id || t.file} invalid: ${t.problem}`;
129
+ let line = `${t.id} ${t.status}`;
130
+ if (t.status === 'in_progress' && t.started) line += ` started ${t.started}`;
131
+ if (t.verified) line += ` verified ${t.verified.split(/\s+/)[0]}`;
132
+ if (t.acceptedBy) line += ` accepted by ${t.acceptedBy}`;
133
+ return line;
134
+ }
135
+ function render(report) {
136
+ const lines = [];
137
+ const h = report.harness;
138
+ lines.push(h ? `PINCER — ${report.name} (${h.project.kind})` : 'PINCER — no agreement yet');
139
+ if (h) lines.push(`Stages: ${STAGES.map((name) => `${name} ${report.stages[name]}`).join(', ')}`);
140
+ const tiers = report.models ? models.statusLine(report.models) : '';
141
+ if (tiers) lines.push(`Models: ${tiers}`);
142
+ if (report.specs.length) lines.push(`Specs: ${report.specs.map((s) => `${s.file} (${s.status || 'no status'})`).join(', ')}`);
143
+ for (const t of report.tickets) lines.push(ticketLine(t));
144
+ if (report.warnings.length) { lines.push('Warnings:'); for (const w of report.warnings) lines.push(` ${w}`); }
145
+ lines.push(`Next: ${report.next}`);
146
+ return `${lines.join('\n')}\n`;
147
+ }
148
+
149
+ // The gate every stage runs first. Exit 0 and "on", or exit 3 and the reason;
150
+ // exit 2 for a name that is not a stage. The always-on stages pass without an
151
+ // agreement, because the blueprint is what writes it.
152
+ function stageGate(root, name) {
153
+ if (!STAGES.includes(name)) return { exit: 2, message: `${name} is not a stage (${STAGES.join(', ')})` };
154
+ if (ALWAYS_ON.includes(name)) return { exit: 0, message: 'on' };
155
+ const loaded = harnessModule.load(root);
156
+ if (!loaded.ok) return { exit: 3, message: loaded.missing ? loaded.message : `${HARNESS_FILE}: ${loaded.message}` };
157
+ if (!harnessModule.stageOn(loaded.harness, name)) return { exit: 3, message: `stage ${name} is off in ${HARNESS_FILE}` };
158
+ return { exit: 0, message: 'on' };
159
+ }
160
+
161
+ module.exports = { readiness, gather, nextCommand, render, stageGate };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+ // PINCER engine — the ticket lifecycle (docs/engine.md, "Ticket fields and
3
+ // receipts"): start, verify, done, accept. The only writer of a ticket's
4
+ // lifecycle fields. `verify` runs the Verification block and stamps a receipt
5
+ // only on exit 0; `done` re-runs the check and refuses on unticked criteria or
6
+ // a stale receipt; `accept` records a named person's words on a done ticket.
7
+ const fs = require('node:fs');
8
+ const path = require('node:path');
9
+ const { spawn } = require('node:child_process');
10
+ const parse = require('./parse.cjs');
11
+ const harnessModule = require('./harness.cjs');
12
+ const { nowIso, atomicWrite } = require('./util.cjs');
13
+
14
+ const EXIT = { OK: 0, REFUSED: 1, USAGE: 2, HARNESS: 3, INTERRUPTED: 130 };
15
+ const GRACE_MS = 5000;
16
+ const SCRIPT = 'scripts/pincer-ticket.sh';
17
+
18
+ class Refusal extends Error {
19
+ constructor(message, { exit = EXIT.REFUSED, prefix = 'pincer-ticket' } = {}) { super(message); this.exit = exit; this.prefix = prefix; }
20
+ }
21
+ const refuse = (message, options) => { throw new Refusal(message, options); };
22
+
23
+ // --- The agreement ---------------------------------------------------------
24
+ // Every verb needs an agreement with the code stage on; tickets live in its home.
25
+ function agreement(root) {
26
+ const loaded = harnessModule.load(root);
27
+ if (!loaded.ok) refuse(`${harnessModule.HARNESS_FILE}: ${loaded.message}`, { exit: EXIT.HARNESS, prefix: '' });
28
+ if (!harnessModule.stageOn(loaded.harness, 'code')) refuse(`stage code is off in ${harnessModule.HARNESS_FILE}`, { exit: EXIT.HARNESS, prefix: '' });
29
+ return { harness: loaded.harness, tickets: harnessModule.homes(loaded.harness).tickets };
30
+ }
31
+
32
+ // --- Tickets ---------------------------------------------------------------
33
+ const readTicket = (root, file) => fs.readFileSync(path.join(root, file), 'utf8');
34
+ const writeTicket = (root, file, text) => atomicWrite(path.join(root, file), text);
35
+
36
+ function loadTicket(root, file, id) {
37
+ const text = readTicket(root, file);
38
+ const v = parse.validateTicket(file, text);
39
+ if (!v.ok) refuse(v.problems.map((p) => `${file}: ${p}`).join('\n'));
40
+ return { id: id || v.fields.ticket, file, text, fields: v.fields, timeout: v.timeout };
41
+ }
42
+ function resolve(root, ticketsDir, input) {
43
+ const tf = parse.ticketFile(root, ticketsDir, input);
44
+ if (tf.problem) refuse(tf.problem);
45
+ return loadTicket(root, tf.file, tf.id);
46
+ }
47
+
48
+ // Why a ticket's receipt is not current, or null when it is: a receipt exists,
49
+ // its hash matches the Verification block as it stands, and the latest check
50
+ // of that block passed.
51
+ function receiptProblem(text, fields) {
52
+ const receipt = fields.verified || '';
53
+ if (!receipt) return 'no verification receipt';
54
+ const hash = parse.blockHash(text);
55
+ const receiptHash = receipt.split(/\s+/).pop();
56
+ if (receiptHash !== hash) return `receipt hash ${receiptHash} does not match the current Verification block (${hash})`;
57
+ const last = (fields.last_check || '').split(/\s+/);
58
+ if (last.length === 3 && last[2] === hash && last[1] !== 'passed') return `last check ${last[1]}`;
59
+ return null;
60
+ }
61
+
62
+ // --- start -----------------------------------------------------------------
63
+ function startLoaded(root, ticketsDir, t) {
64
+ if (t.fields.status === 'in_progress') return { exit: EXIT.OK, out: `${t.id} already in progress (started ${t.fields.started || ''})\n` };
65
+ if (t.fields.status === 'done') refuse(`${t.id} is already done`);
66
+ for (const dep of parse.dependencies(t.fields)) {
67
+ const df = parse.ticketFile(root, ticketsDir, dep);
68
+ if (df.problem) refuse(`${t.id} depends on ${dep}: ${df.problem}`);
69
+ const d = loadTicket(root, df.file, dep);
70
+ if (d.fields.status !== 'done') refuse(`${t.id} depends on ${dep}, which is '${d.fields.status}' — finish ${dep} first (or fix depends_on in ${t.file})`);
71
+ const problem = receiptProblem(d.text, d.fields);
72
+ if (problem) refuse(`${t.id} depends on ${dep}: ${problem} — run '${SCRIPT} verify ${dep}' again`);
73
+ }
74
+ let text = parse.fmSet(t.text, 'status', 'in_progress');
75
+ if (!t.fields.started) text = parse.fmSet(text, 'started', nowIso());
76
+ writeTicket(root, t.file, text);
77
+ return { exit: EXIT.OK, out: `▶ ${t.id} started ${parse.frontmatterField(text, 'started')} — ${t.file}\n` };
78
+ }
79
+ function start(root, input) {
80
+ const { tickets } = agreement(root);
81
+ return startLoaded(root, tickets, resolve(root, tickets, input));
82
+ }
83
+
84
+ // --- verify ----------------------------------------------------------------
85
+ // The block runs under bash with inherited stdio in its own process group;
86
+ // SIGINT/SIGTERM to the engine terminate it and are reported as interrupted, and
87
+ // so is the ticket's timeout (seconds) when the block outlives it.
88
+ function execute(root, block, timeoutSeconds) {
89
+ return new Promise((done) => {
90
+ const child = spawn('bash', ['-eo', 'pipefail', '-c', block], { cwd: root, detached: true, stdio: 'inherit', env: process.env });
91
+ let interrupted = null, timedOut = false;
92
+ const terminate = () => {
93
+ try { process.kill(-child.pid, 'SIGTERM'); } catch { try { child.kill('SIGTERM'); } catch { /* gone */ } }
94
+ setTimeout(() => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* gone */ } }, GRACE_MS).unref();
95
+ };
96
+ const onSignal = (signal) => { interrupted = signal; terminate(); };
97
+ const timer = timeoutSeconds > 0 ? setTimeout(() => { timedOut = true; terminate(); }, timeoutSeconds * 1000) : null;
98
+ const release = () => { process.off('SIGINT', onSignal); process.off('SIGTERM', onSignal); if (timer) clearTimeout(timer); };
99
+ process.on('SIGINT', onSignal);
100
+ process.on('SIGTERM', onSignal);
101
+ child.on('error', (error) => { release(); done({ code: 127, signal: null, interrupted, timedOut, error: error.message }); });
102
+ child.on('exit', (code, signal) => { release(); done({ code, signal, interrupted, timedOut }); });
103
+ });
104
+ }
105
+
106
+ async function runCheck(root, t, io) {
107
+ const { id, file } = t;
108
+ if (t.fields.status === 'done') io.out(`${id} is done — re-running its check and updating the latest outcome\n`);
109
+ const commands = parse.verificationCommands(t.text);
110
+ const block = parse.blockText(t.text);
111
+ const hash = parse.blockHash(t.text);
112
+ let text = parse.fmUnset(t.text, 'verified');
113
+ text = parse.fmSet(text, 'last_check', `${nowIso()} running ${hash}`);
114
+ writeTicket(root, file, text);
115
+ if (!commands.some((c) => !/^[ \t]*(#.*)?$/.test(c))) refuse(`no runnable command in the Verification block of ${file}`);
116
+ io.out(`── ${id} verification ──\n`);
117
+ for (const c of commands) io.out(` $ ${c}\n`);
118
+ const timeout = parse.effectiveTimeout(t.fields);
119
+ const result = await execute(root, block, timeout);
120
+ const stamp = (outcome) => writeTicket(root, file, parse.fmSet(readTicket(root, file), 'last_check', `${nowIso()} ${outcome} ${hash}`));
121
+ if (result.timedOut) {
122
+ stamp('interrupted');
123
+ io.err(`✗ ${id} verification TIMED OUT after ${timeout}s — recorded as interrupted in last_check of ${file}. Raise timeout: in the frontmatter or fix the check, then re-run verify.\n`);
124
+ return { exit: EXIT.INTERRUPTED };
125
+ }
126
+ if (result.interrupted) { stamp('interrupted'); return { exit: EXIT.INTERRUPTED }; }
127
+ const rc = result.code === null ? 1 : result.code;
128
+ if (rc !== 0) {
129
+ stamp('failed');
130
+ io.err(`✗ ${id} verification FAILED (exit ${rc}) — failure recorded in last_check of ${file}; any prior successful receipt was revoked. Fix, then re-run verify.\n`);
131
+ return { exit: EXIT.REFUSED };
132
+ }
133
+ const after = readTicket(root, file);
134
+ if (parse.blockHash(after) !== hash) { stamp('failed'); refuse('Verification block changed during execution — re-run verify'); }
135
+ if (!parse.validateTicket(file, after).ok) { stamp('failed'); refuse('ticket became invalid during verification — fix it and re-run verify'); }
136
+ let passed = parse.fmSet(after, 'last_check', `${nowIso()} passed ${hash}`);
137
+ passed = parse.fmSet(passed, 'verified', `${nowIso()} ${hash}`);
138
+ writeTicket(root, file, passed);
139
+ io.out(`✓ ${id} verified — receipt: ${parse.frontmatterField(passed, 'verified')}\n`);
140
+ return { exit: EXIT.OK };
141
+ }
142
+
143
+ const defaultIo = () => ({ out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) });
144
+
145
+ async function verify(root, input, io = defaultIo()) {
146
+ const { tickets } = agreement(root);
147
+ const t0 = resolve(root, tickets, input);
148
+ if (t0.fields.status === 'open') io.out(startLoaded(root, tickets, t0).out);
149
+ return runCheck(root, loadTicket(root, t0.file, t0.id), io);
150
+ }
151
+
152
+ // --- done ------------------------------------------------------------------
153
+ const slugWords = (file) => path.basename(file, '.md').replace(/^T-[0-9]+-/, '').replace(/-/g, ' ');
154
+
155
+ async function done(root, input, io = defaultIo()) {
156
+ const { tickets } = agreement(root);
157
+ const t = resolve(root, tickets, input);
158
+ const { id, file } = t;
159
+ const status = t.fields.status;
160
+ if (status !== 'in_progress' && status !== 'done') refuse(`${id} is '${status}' — run '${SCRIPT} verify ${id}' first`);
161
+ const receipt = t.fields.verified || '';
162
+ if (!receipt) refuse(`no verification receipt on ${id} — run '${SCRIPT} verify ${id}' and get a green check first`);
163
+ const current = parse.blockHash(t.text);
164
+ const receiptHash = receipt.split(/\s+/).pop();
165
+ if (receiptHash !== current) refuse(`receipt hash ${receiptHash} does not match the current Verification block (${current}): the check changed after it passed — run '${SCRIPT} verify ${id}' again`);
166
+ const unticked = parse.unticked(t.text);
167
+ if (unticked.length) refuse(`unticked acceptance criteria on ${id}:\n${unticked.join('\n')}\nTick each verified criterion; a criterion that was cut is a scope change to record in the spec, not a box to skip.`);
168
+ const result = await runCheck(root, t, io);
169
+ if (result.exit !== EXIT.OK) return result;
170
+ const after = loadTicket(root, file, id);
171
+ const still = parse.unticked(after.text);
172
+ if (still.length) refuse(`unticked acceptance criteria on ${id} after verification:\n${still.join('\n')}`);
173
+ if (status === 'done') { io.out(`${id} already done — current check passed\n`); return { exit: EXIT.OK }; }
174
+ let text = parse.fmSet(after.text, 'status', 'done');
175
+ text = parse.fmSet(text, 'finished', nowIso());
176
+ writeTicket(root, file, text);
177
+ io.out(`✓ ${id} done. Inspect staged work, stage only this ticket's paths, review git diff --cached, then commit: ${id}: ${slugWords(file)}\n`);
178
+ return { exit: EXIT.OK };
179
+ }
180
+
181
+ // --- accept ----------------------------------------------------------------
182
+ // The floor's second half: a named person, from the agreement's acceptors,
183
+ // and their words, on one line. Run only after the person said so.
184
+ function accept(root, input, { by, words } = {}) {
185
+ const { harness, tickets } = agreement(root);
186
+ const t = resolve(root, tickets, input);
187
+ if (t.fields.status !== 'done') refuse(`${t.id} is '${t.fields.status}' — acceptance is recorded on a done ticket; run verify and done first`);
188
+ const stale = receiptProblem(t.text, t.fields);
189
+ if (stale) refuse(`${t.id}: ${stale} — run '${SCRIPT} verify ${t.id}' and get a green check before recording acceptance`);
190
+ const people = harnessModule.acceptors(harness);
191
+ const name = String(by || '').trim();
192
+ if (!people.includes(name)) refuse(`${name ? `'${name}'` : 'nobody'} is not an acceptor in ${harnessModule.HARNESS_FILE} — the people who accept are: ${people.join(', ')}`);
193
+ const said = String(words || '').replace(/\s*\r?\n\s*/g, ' ').trim();
194
+ if (!said) refuse(`${t.id}: --words must quote what ${name} said when accepting`);
195
+ const previous = t.fields.accepted ? `${t.id} already accepted by ${t.fields.accepted_by} at ${t.fields.accepted} — overwriting\n` : '';
196
+ let text = parse.fmSet(t.text, 'accepted', nowIso());
197
+ text = parse.fmSet(text, 'accepted_by', name);
198
+ text = parse.fmSet(text, 'accepted_words', said);
199
+ writeTicket(root, t.file, text);
200
+ return { exit: EXIT.OK, out: `${previous}✓ ${t.id} accepted by ${name}: ${said}\n` };
201
+ }
202
+
203
+ module.exports = { EXIT, Refusal, receiptProblem, start, verify, done, accept };
@@ -0,0 +1,104 @@
1
+ 'use strict';
2
+ // PINCER engine — small helpers shared by the engine modules: time, atomic
3
+ // writes, JSON, git, and the output sanitizer. Dependency-free.
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const crypto = require('node:crypto');
7
+ const { execFileSync } = require('node:child_process');
8
+
9
+ // UTC to the second, the timestamp format every ticket field uses.
10
+ const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, 'Z');
11
+
12
+ // Write via a temporary file in the same directory and rename into place, so a
13
+ // reader never sees a partial file and a crash leaves the old file or the new one.
14
+ function atomicWrite(file, text) {
15
+ const dir = path.dirname(file);
16
+ fs.mkdirSync(dir, { recursive: true });
17
+ const temp = path.join(dir, `.${path.basename(file)}.${process.pid}.${crypto.randomBytes(4).toString('hex')}.tmp`);
18
+ fs.writeFileSync(temp, text);
19
+ fs.renameSync(temp, file);
20
+ }
21
+
22
+ // The parsed file, or null when it is missing or not JSON. Callers that need
23
+ // to tell the two apart read the file themselves.
24
+ function readJson(file) {
25
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
26
+ }
27
+
28
+ // Run git in `root`; stdout without its trailing newline. Throws on a non-zero
29
+ // exit (the error carries stderr).
30
+ function git(root, args) {
31
+ return execFileSync('git', ['-C', root, '-c', 'core.quotePath=false', ...args], {
32
+ encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 64 * 1024 * 1024,
33
+ }).trimEnd();
34
+ }
35
+ function tryGit(root, args) {
36
+ try { return git(root, args); } catch { return null; }
37
+ }
38
+
39
+ // Output sanitizer: documented secret patterns are replaced by [redacted]
40
+ // before anything captured is persisted. A safety net, not a guarantee.
41
+ const PATTERNS = [
42
+ // Bearer tokens first, so an `Authorization: Bearer <token>` header keeps the
43
+ // token, not the word Bearer, as the redacted value.
44
+ { re: /((?:bearer|basic|token|digest)\s+)[A-Za-z0-9\-._~+/=]+/gi, replace: '$1[redacted]' },
45
+ // key/secret/password/token/authorization assignments and JSON fields; a
46
+ // quoted value is redacted up to its closing quote, an unquoted one to the
47
+ // next whitespace or separator. Bounded quantifiers: an unbounded prefix would
48
+ // backtrack quadratically on long lines.
49
+ { re: /((?:^|[^A-Za-z0-9_.-])[A-Za-z0-9_.-]{0,64}?(?:key|secret|password|passwd|token|authorization)[A-Za-z0-9_.-]{0,64}["']?[ \t]{0,8}[:=][ \t]{0,8})(?:"([^"\n]{0,512})"|'([^'\n]{0,512})'|([^\s"',;]+))/gi, replace: (m, head, dq, sq) => `${head}${dq !== undefined ? '"[redacted]"' : sq !== undefined ? "'[redacted]'" : '[redacted]'}` },
50
+ { re: /AKIA[0-9A-Z]{16}/g, replace: '[redacted]' },
51
+ { re: /gh[pousr]_[A-Za-z0-9]{20,}/g, replace: '[redacted]' },
52
+ ];
53
+ const PEM_BEGIN = /-----BEGIN [A-Z ]*PRIVATE KEY-----/;
54
+ const PEM_END = /-----END [A-Z ]*PRIVATE KEY-----/;
55
+
56
+ // Stateful: PEM private key blocks span lines. `state` is a plain object the
57
+ // caller keeps per stream.
58
+ function sanitizeLine(line, state = {}) {
59
+ if (state.inPem) {
60
+ if (PEM_END.test(line)) state.inPem = false;
61
+ return '[redacted]';
62
+ }
63
+ if (PEM_BEGIN.test(line)) {
64
+ state.inPem = !PEM_END.test(line);
65
+ return '[redacted]';
66
+ }
67
+ let text = line;
68
+ for (const { re, replace } of PATTERNS) {
69
+ text = text.replace(re, (...args) => (typeof replace === 'function' ? replace(...args) : replace.replace(/\$(\d)/g, (_, n) => args[Number(n)])));
70
+ }
71
+ return text;
72
+ }
73
+ function sanitizeText(text) {
74
+ const state = {};
75
+ return String(text).split('\n').map((line) => sanitizeLine(line, state)).join('\n');
76
+ }
77
+
78
+ // Complete output on every exit path. process.stdout is asynchronous when it
79
+ // is a pipe, so a report larger than the pipe buffer is cut short by an early
80
+ // exit; writing the descriptor synchronously puts the bytes in the pipe before
81
+ // the call returns. A reader that went away (EPIPE) or a closed descriptor
82
+ // (EBADF) stays silent and keeps the command's own exit code.
83
+ const IDLE = new Int32Array(new SharedArrayBuffer(4));
84
+ function writeFd(fd, text) {
85
+ if (!text) return;
86
+ const buffer = Buffer.from(String(text), 'utf8');
87
+ let offset = 0;
88
+ while (offset < buffer.length) {
89
+ try {
90
+ offset += fs.writeSync(fd, buffer, offset, buffer.length - offset);
91
+ } catch (error) {
92
+ if (error.code === 'EAGAIN' || error.code === 'EINTR') { Atomics.wait(IDLE, 0, 0, 1); continue; }
93
+ if (error.code === 'EPIPE' || error.code === 'EBADF') return;
94
+ throw error;
95
+ }
96
+ }
97
+ }
98
+ const out = (text) => writeFd(1, text);
99
+ const err = (text) => writeFd(2, text);
100
+
101
+ // A URL without the credentials that may sit before its host (https://user:token@host/...).
102
+ const stripUserinfo = (url) => (typeof url === 'string' ? url.replace(/^([a-z][a-z0-9+.-]*:\/\/)[^/@]*@/i, '$1') : url);
103
+
104
+ module.exports = { nowIso, atomicWrite, readJson, git, tryGit, sanitizeLine, sanitizeText, stripUserinfo, writeFd, out, err };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ // Entry point for the blueprint stage's scripts: recon prints the classification
3
+ // with its reasons and the answers it could pre-fill (offline unless --network);
4
+ // write turns an answers file into harness.json, the agreement in AGENTS.md and
5
+ // the project copies.
6
+ 'use strict';
7
+ require('./pincer/cli.cjs').blueprint(process.argv.slice(2)).then((code) => { process.exitCode = code; });
@@ -1,76 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
- // PINCER evidence — read-only validator for candidate evidence manifests.
4
- // Dependency-free; runs under `node` on any platform. The implementation lives
5
- // in scripts/pincer-runtime/evidence.cjs (shared with status and release).
3
+ // PINCER evidence — validate an evidence manifest, or digest files for one
4
+ // (docs/engine.md, "Evidence manifest schema 1"). Read-only.
6
5
  //
7
- // node scripts/pincer-evidence.cjs validate .prd/evidence/prd-vN/<candidate>/manifest.json \
8
- // [--candidate <sha>] [--base <sha>] [--prd .prd/prd-vN.md] [--files]
9
- // node scripts/pincer-evidence.cjs digest <file>...
10
- //
11
- // `validate` exits 0 and prints `ok <candidate>` when the manifest is
12
- // consistent; with --files it also lists the manifest and every artifact path
13
- // (repository-relative, one per line). Otherwise it prints one
14
- // `evidence: <manifest>: <reason>` line per problem to stderr and exits 1.
15
- // Usage errors exit 2. Nothing is ever written.
16
- //
17
- // What validation establishes: that the locally authored record is internally
18
- // consistent — schema, references, candidate association, required results,
19
- // artifact existence and digests, repository containment. It is NOT independent
20
- // attestation that the recorded commands ran or that images depict the stated
21
- // application; that judgment stays with the reviewer.
22
- const fs = require('node:fs');
23
- const path = require('node:path');
24
- const { HEX40, PRD_REF, validate, digestFile } = require('./pincer-runtime/evidence.cjs');
25
- const io = require('./pincer-runtime/io.cjs');
26
-
27
- function usage(message) {
28
- if (message) io.err(`pincer-evidence: ${message}\n`);
29
- io.err('usage: pincer-evidence.cjs validate <manifest> [--candidate <sha>] [--base <sha>] [--prd .prd/prd-vN.md] [--files]\n pincer-evidence.cjs digest <file>...\n');
30
- process.exit(2);
31
- }
32
-
33
- function main(argv) {
34
- const [command, ...rest] = argv;
35
- if (command === 'validate') {
36
- const opts = { files: false };
37
- let manifest = null;
38
- for (let i = 0; i < rest.length; i++) {
39
- const arg = rest[i];
40
- if (arg === '--files') opts.files = true;
41
- else if (arg === '--candidate' || arg === '--base' || arg === '--prd') {
42
- const value = rest[++i];
43
- if (value === undefined) usage(`${arg} requires a value`);
44
- opts[arg.slice(2)] = value;
45
- } else if (arg.startsWith('--')) usage(`unknown option ${arg}`);
46
- else if (manifest === null) manifest = arg;
47
- else usage('validate takes exactly one manifest path');
48
- }
49
- if (manifest === null) usage('validate requires a manifest path');
50
- if (opts.candidate !== undefined && !HEX40.test(opts.candidate)) usage('--candidate must be a full 40-hex commit ID');
51
- if (opts.base !== undefined && !HEX40.test(opts.base)) usage('--base must be a full 40-hex commit ID');
52
- if (opts.prd !== undefined && !PRD_REF.test(opts.prd)) usage('--prd must be of the form .prd/prd-vN.md');
53
- const problems = validate(manifest, opts);
54
- if (problems.length > 0) {
55
- for (const p of problems) io.err(`evidence: ${manifest}: ${p}\n`);
56
- process.exit(1);
57
- }
58
- const doc = JSON.parse(fs.readFileSync(path.resolve(manifest), 'utf8'));
59
- io.out(`ok ${doc.candidate}${doc.schema >= 2 ? ` schema ${doc.schema}` : ''}\n`);
60
- for (const l of opts.limitations || []) io.err(`evidence: limitation: ${l}\n`);
61
- if (opts.files) for (const p of opts.list) io.out(`${p}\n`);
62
- return;
63
- }
64
- if (command === 'digest') {
65
- if (rest.length === 0) usage('digest requires at least one file');
66
- for (const file of rest) {
67
- let digest;
68
- try { digest = digestFile(file); } catch (error) { io.err(`pincer-evidence: ${file}: ${error.code === 'ENOENT' ? 'missing' : error.message}\n`); process.exit(1); }
69
- io.out(`${digest} ${file}\n`);
70
- }
71
- return;
72
- }
73
- usage(command ? `unknown command ${command}` : undefined);
74
- }
75
-
76
- main(process.argv.slice(2));
6
+ // scripts/pincer-evidence.cjs validate <manifest> [--candidate <sha>] prints "ok <candidate>", or one problem per line and exits 1
7
+ // scripts/pincer-evidence.cjs digest <file>... prints "<sha256> <file>" per line
8
+ require('./pincer/cli.cjs').main(['evidence', ...process.argv.slice(2)]).then((code) => { process.exitCode = code; });
@@ -1,17 +1,15 @@
1
1
  #!/bin/bash
2
- # PINCER status — where the workflow stands, read from the artifacts on disk.
3
- # Read-only. Every playbook runs this first; /pincer-status wraps it.
2
+ # PINCER status — where the work stands, read from harness.json and the
3
+ # artifacts on disk (docs/engine.md, "Stage gate"). Read-only. Every stage
4
+ # skill runs the gate first; /pincer-status wraps the report.
4
5
  #
5
- # scripts/pincer-status.sh [--json]
6
+ # scripts/pincer-status.sh stages, specs, every ticket, warnings, and the next command
7
+ # scripts/pincer-status.sh --json the same report as JSON
8
+ # scripts/pincer-status.sh --stage <name> the gate: prints "on" and exits 0, or the reason and exits 3
6
9
  #
7
- # Compatibility wrapper: the report is computed by scripts/pincer-runtime.cjs
8
- # (`status`), which prints the PRD state and profile, every ticket with its
9
- # state and clock-based elapsed time, the wall-clock elapsed build line while a
10
- # ticket is in progress or PINCER_BUILD_BUDGET_MIN is set, the evidence verdict,
11
- # readiness warnings, and the next command. Exit codes follow
12
- # docs/runtime-contracts.md (0 inspected, 4 invalid input).
10
+ # Exit codes: 0 ok, 2 usage, 3 no agreement, invalid harness.json or stage off.
13
11
  command -v node >/dev/null 2>&1 || {
14
- echo 'pincer-status: Node.js 22+ is required — the runtime is scripts/pincer-runtime.cjs' >&2
15
- exit 4
12
+ echo 'pincer-status: Node.js 22+ is required — the engine is scripts/pincer/cli.cjs' >&2
13
+ exit 1
16
14
  }
17
- exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pincer-runtime.cjs" status "$@"
15
+ exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pincer/cli.cjs" status "$@"
@@ -1,25 +1,22 @@
1
1
  #!/bin/bash
2
- # PINCER ticket state machine — the ONLY writer of a ticket's lifecycle fields.
3
- # Compatibility wrapper: every call is executed by scripts/pincer-runtime.cjs
4
- # (docs/runtime-contracts.md). On Claude Code a PreToolUse hook
5
- # (.claude/hooks/ticket-guard.sh) blocks hand edits of those fields so "done"
6
- # can only be reached through a passing check.
2
+ # PINCER ticket lifecycle — the ONLY writer of a ticket's lifecycle fields
3
+ # (docs/engine.md, "Ticket fields and receipts"). On Claude Code a PreToolUse
4
+ # hook (.claude/hooks/ticket-guard.sh) blocks hand edits of those fields, so
5
+ # "done" is reached only through a passing check and acceptance only through
6
+ # a named person's words.
7
7
  #
8
- # scripts/pincer-ticket.sh start T-03 open -> in_progress; refuses while a depends_on ticket isn't ready
9
- # scripts/pincer-ticket.sh verify T-03 runs the ticket's Verification block and records the outcome
10
- # scripts/pincer-ticket.sh done T-03 closes the ticket on a current passing check with all criteria ticked
11
- # scripts/pincer-ticket.sh bind T-03 .prd/prd-vN.md associates a legacy ticket with its PRD
8
+ # scripts/pincer-ticket.sh start T-03 open -> in_progress; refuses while a depends_on ticket is not done and verified
9
+ # scripts/pincer-ticket.sh verify T-03 runs the ticket's Verification block and stamps the receipt on exit 0
10
+ # scripts/pincer-ticket.sh done T-03 closes the ticket on a current passing check with every criterion ticked
11
+ # scripts/pincer-ticket.sh accept T-03 --by "<name>" --words "<what they said>" records the person's acceptance
12
12
  #
13
- # Unmigrated (legacy) projects keep the v0.4.1 receipts: `verify` writes
14
- # `last_check` and `verified` into the ticket and `done` re-runs the check.
15
- # Migrated projects record attempts under .pincer/runtime/ and `done` consumes
16
- # the current passing attempt. Exit codes follow docs/runtime-contracts.md.
13
+ # Exit codes: 0 ok, 1 refused, 2 usage, 3 no agreement or stage off, 130 interrupted.
17
14
  command -v node >/dev/null 2>&1 || {
18
- echo 'pincer-ticket: Node.js 22+ is required — the runtime is scripts/pincer-runtime.cjs' >&2
19
- exit 4
15
+ echo 'pincer-ticket: Node.js 22+ is required — the engine is scripts/pincer/cli.cjs' >&2
16
+ exit 1
20
17
  }
21
18
  case "${1:-}" in
22
- start|verify|done|bind) ;;
23
- *) sed -n '8,11p' "$0" | sed 's/^# \{0,1\}//'; exit 2 ;;
19
+ start|verify|done|accept) ;;
20
+ *) sed -n '8,11p' "$0" | sed 's/^# \{0,1\}//' >&2; exit 2 ;;
24
21
  esac
25
- exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pincer-runtime.cjs" "$@"
22
+ exec node "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/pincer/cli.cjs" ticket "$@"