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,233 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // PINCER engine — argv dispatch for the entry points (docs/engine.md,
4
+ // "Commands and exit codes"). Exit codes: 0 ok, 1 refused, 2 usage,
5
+ // 3 no agreement or stage off, 130 interrupted.
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+ const util = require('./util.cjs');
9
+ const harness = require('./harness.cjs');
10
+ const ticket = require('./ticket.cjs');
11
+ const status = require('./status.cjs');
12
+ const evidence = require('./evidence.cjs');
13
+ const recon = require('./recon.cjs');
14
+ const writer = require('./blueprint.cjs');
15
+ const generate = require('./generate.cjs');
16
+
17
+ const USAGE = `usage: node scripts/pincer/cli.cjs [--root <dir>] <command> ...
18
+ ticket <start|verify|done|accept> <T-NN> [--by "<name>" --words "<what they said>"]
19
+ status [--json] [--stage <name>]
20
+ evidence validate <manifest> [--candidate <sha>] | digest <file>...
21
+ recon [--json] [--network]
22
+ write [--answers <file>]
23
+ generate [--harness <file>] [--platform <claude,codex,copilot|all>] [--overwrite]
24
+ `;
25
+ const GENERATE_USAGE = `usage: scripts/sync-prompts.sh [--harness <file>] [--platform <claude,codex,copilot|all>] [--overwrite]
26
+ `;
27
+ const BLUEPRINT_USAGE = `usage: scripts/pincer-blueprint.cjs recon [--json] [--network]
28
+ scripts/pincer-blueprint.cjs write [--answers <file>]
29
+ scripts/pincer-blueprint.cjs generate [--harness <file>] [--platform <list>] [--overwrite]
30
+ `;
31
+ const BLUEPRINT_VERBS = ['recon', 'write', 'generate'];
32
+ const EVIDENCE_USAGE = `usage: scripts/pincer-evidence.cjs validate <manifest> [--candidate <sha>]
33
+ scripts/pincer-evidence.cjs digest <file>...
34
+ `;
35
+ const STATUS_USAGE = `usage: scripts/pincer-status.sh [--json] [--stage <name>]
36
+ `;
37
+ const TICKET_USAGE = `usage: scripts/pincer-ticket.sh <verb> <T-NN>
38
+ start T-NN open -> in_progress
39
+ verify T-NN run the Verification block, stamp the receipt
40
+ done T-NN close on a current passing check, all criteria ticked
41
+ accept T-NN --by "<name>" --words "<their words>" record the person's acceptance
42
+ `;
43
+ const TICKET_VERBS = ['start', 'verify', 'done', 'accept'];
44
+
45
+ // The project root: --root, else the nearest directory from the working
46
+ // directory up that holds harness.json, else the working directory.
47
+ function resolveRoot(argv) {
48
+ const args = [];
49
+ let root = null;
50
+ for (let i = 0; i < argv.length; i++) {
51
+ if (argv[i] === '--root') { root = argv[++i]; if (root === undefined) return { problem: '--root needs a directory' }; }
52
+ else if (argv[i].startsWith('--root=')) root = argv[i].slice('--root='.length);
53
+ else args.push(argv[i]);
54
+ }
55
+ if (root !== null) return { root: path.resolve(root), args };
56
+ let dir = process.cwd();
57
+ for (;;) {
58
+ if (fs.existsSync(path.join(dir, harness.HARNESS_FILE))) return { root: dir, args };
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) return { root: process.cwd(), args };
61
+ dir = parent;
62
+ }
63
+ }
64
+
65
+ // --name value pairs; unknown flags and bare words are usage problems.
66
+ function flags(rest, allowed) {
67
+ const out = {};
68
+ for (let i = 0; i < rest.length; i++) {
69
+ const arg = rest[i];
70
+ const eq = arg.startsWith('--') ? arg.indexOf('=') : -1;
71
+ const name = eq === -1 ? arg.slice(2) : arg.slice(2, eq);
72
+ if (!arg.startsWith('--') || !allowed.includes(name)) return { problem: `unexpected argument ${arg}` };
73
+ const value = eq === -1 ? rest[++i] : arg.slice(eq + 1);
74
+ if (value === undefined) return { problem: `--${name} needs a value` };
75
+ out[name] = value;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ async function ticketCommand(root, args, io) {
81
+ const [verb, id, ...rest] = args;
82
+ if (!TICKET_VERBS.includes(verb) || !id) { io.err(TICKET_USAGE); return ticket.EXIT.USAGE; }
83
+ const options = flags(rest, verb === 'accept' ? ['by', 'words'] : []);
84
+ if (options.problem) { io.err(`pincer-ticket: ${options.problem}\n${TICKET_USAGE}`); return ticket.EXIT.USAGE; }
85
+ if (verb === 'accept' && (!('by' in options) || !('words' in options))) { io.err(`pincer-ticket: accept needs --by and --words\n${TICKET_USAGE}`); return ticket.EXIT.USAGE; }
86
+ if (verb === 'start') { const r = ticket.start(root, id); io.out(r.out); return r.exit; }
87
+ if (verb === 'accept') { const r = ticket.accept(root, id, options); io.out(r.out); return r.exit; }
88
+ return (await ticket[verb](root, id, io)).exit;
89
+ }
90
+
91
+ function statusCommand(root, args, io) {
92
+ let json = false, stage = null;
93
+ for (let i = 0; i < args.length; i++) {
94
+ const arg = args[i];
95
+ if (arg === '--json') json = true;
96
+ else if (arg === '--stage') { stage = args[++i]; if (stage === undefined) { io.err(STATUS_USAGE); return 2; } }
97
+ else if (arg.startsWith('--stage=')) stage = arg.slice('--stage='.length);
98
+ else { io.err(`pincer-status: unexpected argument ${arg}\n${STATUS_USAGE}`); return 2; }
99
+ }
100
+ if (stage !== null) {
101
+ const gate = status.stageGate(root, stage);
102
+ (gate.exit === 0 ? io.out : io.err)(`${gate.message}\n`);
103
+ return gate.exit;
104
+ }
105
+ const report = status.gather(root);
106
+ io.out(json ? `${JSON.stringify(report, null, 2)}\n` : status.render(report));
107
+ if (report.harnessProblem) { io.err(`${harness.HARNESS_FILE}: ${report.harnessProblem}\n`); return 3; }
108
+ return 0;
109
+ }
110
+
111
+ // Paths typed on the command line resolve against the working directory.
112
+ const fromCwd = (file) => (path.isAbsolute(file) ? file : path.resolve(process.cwd(), file));
113
+
114
+ function evidenceCommand(root, args, io) {
115
+ const [verb, ...rest] = args;
116
+ if (verb === 'validate' && rest.length) {
117
+ const [file, ...more] = rest;
118
+ const options = flags(more, ['candidate']);
119
+ if (options.problem) { io.err(`pincer-evidence: ${options.problem}\n${EVIDENCE_USAGE}`); return 2; }
120
+ const abs = fromCwd(file);
121
+ const problems = evidence.validate(root, abs, { candidate: options.candidate });
122
+ if (problems.length) { for (const p of problems) io.err(`evidence: ${file}: ${p}\n`); return 1; }
123
+ io.out(`ok ${util.readJson(abs).candidate}\n`);
124
+ return 0;
125
+ }
126
+ if (verb === 'digest' && rest.length) {
127
+ let failed = false;
128
+ for (const file of rest) {
129
+ try { io.out(`${evidence.digestFile(fromCwd(file))} ${file}\n`); } catch (error) { io.err(`evidence: ${file}: ${error.message}\n`); failed = true; }
130
+ }
131
+ return failed ? 1 : 0;
132
+ }
133
+ io.err(EVIDENCE_USAGE);
134
+ return 2;
135
+ }
136
+
137
+ function reconCommand(root, args, io) {
138
+ let json = false, network = false;
139
+ for (const arg of args) {
140
+ if (arg === '--json') json = true;
141
+ else if (arg === '--network') network = true;
142
+ else { io.err(`pincer-blueprint: unexpected argument ${arg}\n${BLUEPRINT_USAGE}`); return 2; }
143
+ }
144
+ if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) { io.err(`pincer-blueprint: ${root} is not a directory\n`); return 1; }
145
+ const result = recon.recon(root, { network });
146
+ io.out(json ? `${JSON.stringify(result, null, 2)}\n` : recon.render(result));
147
+ return 0;
148
+ }
149
+
150
+ // write: answers (default harness.json itself) -> harness.json, the agreement
151
+ // in AGENTS.md, the project copies. Problems go to stderr, one per line.
152
+ function writeCommand(root, args, io) {
153
+ const options = flags(args, ['answers']);
154
+ if (options.problem) { io.err(`pincer-blueprint: ${options.problem}\n${BLUEPRINT_USAGE}`); return 2; }
155
+ const given = options.answers;
156
+ const result = writer.write(root, given === undefined ? {} : { answers: fromCwd(given), label: given });
157
+ if (!result.ok) { for (const p of result.problems) io.err(`pincer-blueprint: ${p}\n`); return result.exit; }
158
+ for (const file of result.written) io.out(`wrote ${file}\n`);
159
+ reportGenerated(result.generated, io, 'pincer-blueprint');
160
+ return 0;
161
+ }
162
+
163
+ // What the library or its table could not provide, as one sentence.
164
+ function missingText(name) {
165
+ if (name === 'models.json') return 'library has no models.json; run npx pincer-workflow update';
166
+ if (name.startsWith('models.json:')) return `${name} — fix the library table; after an update the upstream copy is beside it as models.json.new`;
167
+ if (name.startsWith('models/')) { const [platform, tier] = name.slice('models/'.length).split('/'); return `library table has no ${platform} value for tier ${tier}; set models.map.${platform}.${tier} in harness.json`; }
168
+ if (name.startsWith('agents/')) return `library has no agent ${name.slice('agents/'.length)}`;
169
+ return `library has no stage ${name}`;
170
+ }
171
+ // The generator's report as lines: what was written, kept (with or without a
172
+ // sidecar), removed, and what the library or the manifest could not provide.
173
+ function reportGenerated(report, io, who) {
174
+ for (const file of report.written) io.out(`wrote ${file}\n`);
175
+ for (const file of report.conflicted) io.out(`kept ${file} (edited; fresh copy in ${file}.new)\n`);
176
+ for (const file of report.retained) io.out(`kept ${file} (edited; not removed)\n`);
177
+ for (const file of report.removed) io.out(`removed ${file}\n`);
178
+ for (const name of report.missing) io.err(`${who}: warning: ${missingText(name)}\n`);
179
+ for (const rel of report.unsafe) io.err(`${who}: warning: ignored unsafe baseline path ${rel} in ${generate.MANIFEST}\n`);
180
+ }
181
+
182
+ // generate: the project copies, Codex skills and Copilot prompts from the
183
+ // library and the agreement; edited copies are kept and get a .new sidecar.
184
+ function generateCommand(root, args, io) {
185
+ let overwrite = false;
186
+ const rest = args.filter((arg) => { if (arg === '--overwrite') { overwrite = true; return false; } return true; });
187
+ const options = flags(rest, ['harness', 'platform']);
188
+ if (options.problem) { io.err(`pincer-generate: ${options.problem}\n${GENERATE_USAGE}`); return 2; }
189
+ let platforms;
190
+ if (options.platform !== undefined) {
191
+ platforms = options.platform === 'all' ? [...generate.PLATFORMS] : options.platform.split(',').map((p) => p.trim()).filter(Boolean);
192
+ if (!platforms.length || platforms.some((p) => !generate.PLATFORMS.includes(p))) { io.err(`pincer-generate: --platform takes ${generate.PLATFORMS.join(', ')} or all\n${GENERATE_USAGE}`); return 2; }
193
+ }
194
+ const loaded = harness.load(root, options.harness === undefined ? undefined : fromCwd(options.harness));
195
+ if (!loaded.ok) { io.err(`${options.harness === undefined ? harness.HARNESS_FILE : options.harness}: ${loaded.message}\n`); return 3; }
196
+ const report = generate.generate(root, { harness: loaded.harness, platforms, overwrite });
197
+ reportGenerated(report, io, 'pincer-generate');
198
+ io.out(`generated: ${report.written.length} written, ${report.skipped.length} unchanged, ${report.conflicted.length + report.retained.length} kept, ${report.removed.length} removed\n`);
199
+ return 0;
200
+ }
201
+
202
+ // scripts/pincer-blueprint.cjs: the blueprint verbs only, same root rules.
203
+ async function blueprint(argv = process.argv.slice(2), io = { out: util.out, err: util.err }) {
204
+ const resolved = resolveRoot(argv);
205
+ if (resolved.problem || !BLUEPRINT_VERBS.includes(resolved.args[0])) { io.err(BLUEPRINT_USAGE); return 2; }
206
+ return main(argv, io);
207
+ }
208
+
209
+ async function main(argv = process.argv.slice(2), io = { out: util.out, err: util.err }) {
210
+ const resolved = resolveRoot(argv);
211
+ if (resolved.problem) { io.err(`pincer: ${resolved.problem}\n${USAGE}`); return 2; }
212
+ const { root, args } = resolved;
213
+ const [command, ...rest] = args;
214
+ try {
215
+ if (command === 'ticket') return await ticketCommand(root, rest, io);
216
+ if (command === 'status') return statusCommand(root, rest, io);
217
+ if (command === 'evidence') return evidenceCommand(root, rest, io);
218
+ if (command === 'recon') return reconCommand(root, rest, io);
219
+ if (command === 'write') return writeCommand(root, rest, io);
220
+ if (command === 'generate') return generateCommand(root, rest, io);
221
+ io.err(USAGE);
222
+ return 2;
223
+ } catch (error) {
224
+ if (error instanceof ticket.Refusal) { io.err(`${error.prefix ? `${error.prefix}: ` : ''}${error.message}\n`); return error.exit; }
225
+ io.err(`pincer: ${error.message}\n`);
226
+ if (process.env.PINCER_DEBUG) io.err(`${error.stack}\n`);
227
+ return 1;
228
+ }
229
+ }
230
+
231
+ if (require.main === module) main().then((code) => { process.exitCode = code; });
232
+
233
+ module.exports = { main, blueprint, resolveRoot, flags, USAGE, TICKET_USAGE, STATUS_USAGE, EVIDENCE_USAGE, BLUEPRINT_USAGE, GENERATE_USAGE };
@@ -0,0 +1,142 @@
1
+ 'use strict';
2
+ // PINCER engine — the evidence manifest, schema 1 (docs/engine.md, "Evidence
3
+ // manifest schema 1"): which checks ran for a candidate, with their logs, and
4
+ // the digests that prove the logs are the ones recorded. Validation reads the
5
+ // manifest's directory and the spec it names; it never runs a check.
6
+ const fs = require('node:fs');
7
+ const path = require('node:path');
8
+ const crypto = require('node:crypto');
9
+
10
+ const SCHEMA = 1;
11
+ const HEX40 = /^[0-9a-f]{40}$/;
12
+ const SHA256 = /^[0-9a-f]{64}$/;
13
+ const ISO_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/;
14
+ const CHECK_ID = /^C-[0-9]{2,6}$/;
15
+ const TOP_KEYS = ['schema', 'candidate', 'base', 'created', 'spec', 'environment', 'checks', 'visual_review', 'artifacts'];
16
+ const ENV_KEYS = ['tools', 'limitations'];
17
+ const CHECK_KEYS = ['id', 'kind', 'required', 'result', 'command', 'log', 'note'];
18
+ const VISUAL_KEYS = ['applicable', 'reason'];
19
+ const KINDS = ['command', 'review', 'visual'];
20
+ const RESULTS = ['passed', 'failed', 'unverified'];
21
+ const MAX_TEXT = 2000; // guard against pasted environment dumps
22
+
23
+ const isObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
24
+ const shortText = (v) => typeof v === 'string' && v.length <= MAX_TEXT;
25
+ const nonempty = (v) => shortText(v) && v.trim() !== '';
26
+
27
+ const digestFile = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
28
+
29
+ // Why a relative POSIX path is unsafe, or null when it is acceptable.
30
+ function unsafePath(p) {
31
+ if (p.startsWith('/')) return 'absolute paths are not allowed';
32
+ if (/^[A-Za-z]:/.test(p)) return 'drive-letter paths are not allowed';
33
+ if (p.includes('\\')) return 'backslashes are not allowed; use relative POSIX paths';
34
+ if (p.split('/').some((s) => s === '' || s === '.' || s === '..')) return 'path must be normalized and relative (no "..", "." or empty segments)';
35
+ return null;
36
+ }
37
+ // Is `p`, relative to `dir`, inside `dir`?
38
+ function inside(dir, p) {
39
+ if (p.startsWith('/') || /^[A-Za-z]:/.test(p)) return false;
40
+ const target = path.resolve(dir, p);
41
+ return target.startsWith(dir + path.sep);
42
+ }
43
+
44
+ // Every problem with the manifest at manifestPath (absolute, or relative to
45
+ // root); [] when it is valid. `candidate`, when given, must equal the
46
+ // manifest's.
47
+ function validate(root, manifestPath, { candidate } = {}) {
48
+ const abs = path.isAbsolute(manifestPath) ? manifestPath : path.join(root, manifestPath);
49
+ const dir = path.dirname(abs);
50
+ let raw;
51
+ try { raw = fs.readFileSync(abs, 'utf8'); } catch (error) {
52
+ return [error.code === 'ENOENT' ? 'manifest does not exist — run /pincer-evaluate to write evidence for this candidate' : `cannot read the manifest (${error.message})`];
53
+ }
54
+ let doc;
55
+ try { doc = JSON.parse(raw); } catch (error) { return [`malformed JSON (${error.message})`]; }
56
+ if (!isObject(doc)) return ['the manifest must be a JSON object'];
57
+ if (doc.schema !== SCHEMA) return [`unknown evidence schema ${JSON.stringify(doc.schema)} — this engine validates schema ${SCHEMA}`];
58
+
59
+ const problems = [];
60
+ const problem = (message) => problems.push(message);
61
+ for (const key of Object.keys(doc)) if (!TOP_KEYS.includes(key)) problem(`unknown top-level key "${key}"`);
62
+ for (const key of TOP_KEYS) if (!(key in doc)) problem(`missing "${key}"`);
63
+
64
+ // The candidate: a commit, the directory the manifest sits in, and the one asked for.
65
+ const candidateOk = typeof doc.candidate === 'string' && HEX40.test(doc.candidate);
66
+ if (!candidateOk) problem('candidate must be a full 40-hex commit ID');
67
+ else if (path.basename(dir) !== doc.candidate) problem(`candidate ${doc.candidate} is not the manifest's directory name (${path.basename(dir)}) — the manifest lives at <evidence home>/<candidate>/manifest.json`);
68
+ if (candidateOk && candidate && doc.candidate !== candidate) problem(`candidate ${doc.candidate} is not the expected candidate ${candidate}`);
69
+ if (typeof doc.base !== 'string' || !HEX40.test(doc.base)) problem('base must be a full 40-hex commit ID');
70
+ if (typeof doc.created !== 'string' || !ISO_UTC.test(doc.created)) problem('created must be an ISO UTC timestamp (YYYY-MM-DDTHH:MM:SSZ)');
71
+
72
+ // The spec, when the agreement has one.
73
+ if (doc.spec !== null && doc.spec !== undefined) {
74
+ if (!nonempty(doc.spec)) problem('spec must be a repository-relative path or null');
75
+ else {
76
+ const why = unsafePath(doc.spec);
77
+ if (why) problem(`spec ${doc.spec}: ${why}`);
78
+ else if (!fs.existsSync(path.join(root, doc.spec))) problem(`spec ${doc.spec} does not exist`);
79
+ }
80
+ }
81
+
82
+ if (!isObject(doc.environment)) problem('environment must be an object {tools, limitations}');
83
+ else {
84
+ for (const key of ENV_KEYS) if (!Array.isArray(doc.environment[key]) || !doc.environment[key].every(shortText)) problem(`environment.${key} must be a list of strings`);
85
+ for (const key of Object.keys(doc.environment)) if (!ENV_KEYS.includes(key)) problem(`environment.${key} is not allowed`);
86
+ }
87
+
88
+ // Artifacts: regular files inside the manifest's directory with matching digests.
89
+ const artifacts = new Set();
90
+ if (!isObject(doc.artifacts)) problem('artifacts must be an object {<path>: <sha256>} of files inside the evidence directory');
91
+ else {
92
+ for (const [p, digest] of Object.entries(doc.artifacts)) {
93
+ if (!inside(dir, p)) { problem(`artifact ${p}: outside the evidence directory ${path.basename(dir)}/`); continue; }
94
+ const why = unsafePath(p);
95
+ if (why) { problem(`artifact ${p}: ${why}`); continue; }
96
+ if (p === 'manifest.json') { problem('artifact manifest.json: the manifest cannot list itself'); continue; }
97
+ artifacts.add(p);
98
+ if (typeof digest !== 'string' || !SHA256.test(digest)) { problem(`artifact ${p}: sha256 must be a full 64-hex digest`); continue; }
99
+ const target = path.join(dir, p);
100
+ let stat;
101
+ try { stat = fs.statSync(target); } catch { problem(`artifact ${p}: missing — no such file under the evidence directory`); continue; }
102
+ if (!stat.isFile()) { problem(`artifact ${p}: not a regular file`); continue; }
103
+ if (digestFile(target) !== digest) problem(`artifact ${p}: digest mismatch — the file changed after the evidence was recorded`);
104
+ }
105
+ }
106
+
107
+ // Checks: C-NN ids, a kind, a result, a command for command checks, a listed log; required ones passed.
108
+ if (!Array.isArray(doc.checks) || doc.checks.length === 0) problem('checks must be a non-empty array');
109
+ else {
110
+ const ids = new Set();
111
+ doc.checks.forEach((check, index) => {
112
+ const name = isObject(check) && typeof check.id === 'string' ? check.id : `#${index + 1}`;
113
+ if (!isObject(check)) { problem(`check ${name}: must be an object`); return; }
114
+ for (const key of Object.keys(check)) if (!CHECK_KEYS.includes(key)) problem(`check ${name}: key "${key}" is not allowed`);
115
+ if (typeof check.id !== 'string' || !CHECK_ID.test(check.id)) problem(`check ${name}: id must be C-NN`);
116
+ else if (ids.has(check.id)) problem(`duplicate check id ${check.id}`);
117
+ else ids.add(check.id);
118
+ if (!KINDS.includes(check.kind)) problem(`check ${name}: kind must be ${KINDS.join(', ')}`);
119
+ if (typeof check.required !== 'boolean') problem(`check ${name}: required must be true or false`);
120
+ if (!RESULTS.includes(check.result)) problem(`check ${name}: result must be ${RESULTS.join(', ')}`);
121
+ if (check.kind === 'command') { if (!nonempty(check.command)) problem(`check ${name}: command must be the command line a command check ran`); }
122
+ else if (!(check.command === null || check.command === undefined || shortText(check.command))) problem(`check ${name}: command must be a string or null`);
123
+ if (check.log !== null && check.log !== undefined) {
124
+ if (!nonempty(check.log)) problem(`check ${name}: log must be a path inside the evidence directory, or null`);
125
+ else if (!artifacts.has(check.log)) problem(`check ${name}: log ${check.log} is not listed in artifacts`);
126
+ }
127
+ if ('note' in check && !shortText(check.note)) problem(`check ${name}: note must be a short string`);
128
+ if (check.required === true && check.result !== 'passed') problem(`required check ${name} is ${check.result} — the candidate is not ready until it passes`);
129
+ });
130
+ }
131
+
132
+ if (!isObject(doc.visual_review)) problem('visual_review must be an object {applicable, reason?}');
133
+ else {
134
+ const visual = doc.visual_review;
135
+ for (const key of Object.keys(visual)) if (!VISUAL_KEYS.includes(key)) problem(`visual_review.${key} is not allowed`);
136
+ if (typeof visual.applicable !== 'boolean') problem('visual_review.applicable must be true or false');
137
+ else if (visual.applicable === false && !nonempty(visual.reason)) problem('visual_review.reason is required when visual review is not applicable (say why)');
138
+ }
139
+ return problems;
140
+ }
141
+
142
+ module.exports = { SCHEMA, validate, digestFile, unsafePath };