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
package/bin/pincer.js CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
- // pincer — installer for the PINCER workflow (Plan · Investigate · Narrow · Code · Evaluate · Release)
2
+ // pincer — installer for PINCER, the harness builder
3
+ // (blueprint · plan · narrow · code · evaluate · release · status)
3
4
  //
4
- // npx pincer-workflow init install into the current repo (asks which platform)
5
- // npx pincer-workflow update refresh an existing install, preserving local edits
5
+ // npx pincer-workflow init install the kit into the current repo (asks which platforms)
6
+ // npx pincer-workflow update refresh an install, preserving local edits; regenerate the project copies
6
7
  // npx pincer-workflow doctor check the health of an install
7
8
  //
8
- // No dependencies by design. The kit is markdown files; this tool only copies
9
- // them carefully: it never overwrites a file you edited — you get a *.new file
10
- // and a warning instead.
9
+ // No dependencies by design. The kit is markdown plus a small Node core; this tool
10
+ // copies it carefully and never overwrites a file you edited: you get a *.new file
11
+ // and a warning instead. The install ends with one next step: run the blueprint,
12
+ // which interviews you and writes the agreement the other stages read.
11
13
 
12
14
  import fs from 'node:fs';
13
15
  import path from 'node:path';
@@ -16,67 +18,81 @@ import readline from 'node:readline';
16
18
  import { fileURLToPath } from 'node:url';
17
19
  import { createRequire } from 'node:module';
18
20
 
19
- const TEMPLATE = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'template');
21
+ const HERE = fileURLToPath(import.meta.url);
22
+ const TEMPLATE = path.join(path.dirname(HERE), '..', 'template');
20
23
  const VERSION = createRequire(import.meta.url)('../package.json').version;
21
- const MANIFEST = '.pincer.json';
22
- const MANIFEST_SCHEMA = 2;
23
-
24
- // The playbooks, subagent rubrics and PRD/ticket templates under .claude/ are
25
- // the canonical kit and are read by every platform's adapter (the Codex skills
26
- // and Copilot prompts point at them), so they ship everywhere; only Claude
27
- // Code's own wiring (CLAUDE.md, settings.json, hooks) is platform-specific.
28
- const PLATFORM_ROOTS = {
29
- common: ['AGENTS.md', 'docs/release-checklist.md', 'docs/dry-run-checklist.md', 'docs/runtime-contracts.md', 'scripts/sync-prompts.sh', 'scripts/pincer-ticket.sh', 'scripts/pincer-status.sh', 'scripts/pincer-evidence.cjs', 'scripts/pincer-runtime.cjs', 'scripts/pincer-runtime', '.claude/commands', '.claude/agents', '.claude/references'],
24
+ const requireAbsolute = createRequire(import.meta.url);
25
+ export const MANIFEST = '.pincer.json';
26
+ export const MANIFEST_SCHEMA = 3;
27
+ // Baselines recorded under these schemas were written by this installer and can be trusted.
28
+ const TRUSTED_SCHEMAS = [2, MANIFEST_SCHEMA];
29
+ export const PLATFORMS = ['claude', 'codex', 'copilot'];
30
+
31
+ // The .claude tree is the canonical, editable project copy and every platform's
32
+ // adapter points at it, so it ships everywhere; only Claude Code's own wiring
33
+ // (CLAUDE.md, settings.json, hooks) is platform-specific. Only the two always-on
34
+ // stages ship generated; the blueprint generates the stages it switches on.
35
+ export const PLATFORM_ROOTS = {
36
+ common: [
37
+ 'AGENTS.md', 'docs/engine.md', 'docs/release-checklist.md',
38
+ 'scripts/pincer-ticket.sh', 'scripts/pincer-status.sh', 'scripts/pincer-evidence.cjs', 'scripts/pincer-blueprint.cjs', 'scripts/sync-prompts.sh', 'scripts/pincer',
39
+ '.claude/pincer/library', '.claude/skills/pincer-blueprint', '.claude/skills/pincer-status', '.claude/commands/pincer-blueprint.md', '.claude/commands/pincer-status.md',
40
+ ],
30
41
  claude: ['CLAUDE.md', '.claude/settings.json', '.claude/hooks'],
31
- codex: ['.codex', '.agents'],
32
- copilot: ['.github'],
42
+ codex: ['.codex/README.md', '.agents/skills/pincer-blueprint', '.agents/skills/pincer-status'],
43
+ copilot: ['.github/copilot-instructions.md', '.github/prompts/pincer-blueprint.prompt.md', '.github/prompts/pincer-status.prompt.md'],
33
44
  };
34
- const EXECUTABLES = ['scripts/sync-prompts.sh', 'scripts/pincer-ticket.sh', 'scripts/pincer-status.sh', '.claude/hooks/block-dangerous.sh', '.claude/hooks/ticket-guard.sh'];
35
- const GITIGNORE_LINES = ['.env', '.env.*', '!.env.example'];
36
- // The runtime's local state (attempts, locks, backups) is never tracked.
37
- const RUNTIME_IGNORE = '.pincer/';
45
+ export const EXECUTABLES = ['scripts/sync-prompts.sh', 'scripts/pincer-ticket.sh', 'scripts/pincer-status.sh', 'scripts/pincer-blueprint.cjs', 'scripts/pincer-evidence.cjs', '.claude/hooks/block-dangerous.sh', '.claude/hooks/ticket-guard.sh'];
46
+ export const GITIGNORE_LINES = ['.env', '.env.*', '!.env.example'];
47
+ // Files an earlier kit installed that this version no longer ships. Never deleted
48
+ // here; doctor names them when neither the manifest nor the generator claims them.
49
+ const OBSOLETE = [
50
+ 'scripts/pincer-runtime.cjs', 'scripts/pincer-runtime', 'scripts/pincer-ticket-lib.sh', 'docs/runtime-contracts.md', 'docs/dry-run-checklist.md', '.claude/references',
51
+ '.claude/commands/pincer-plan.md', '.claude/commands/pincer-narrow.md', '.claude/commands/pincer-code.md', '.claude/commands/pincer-evaluate.md', '.claude/commands/pincer-release.md',
52
+ ];
38
53
 
39
54
  const sha = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
55
+ const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
40
56
 
41
57
  function walk(root, rel = '') {
42
58
  const abs = path.join(root, rel);
43
59
  if (!fs.statSync(abs).isDirectory()) return [rel];
44
- return fs
45
- .readdirSync(abs)
46
- .filter((n) => n !== '.DS_Store')
47
- .flatMap((n) => walk(root, rel ? path.join(rel, n) : n));
60
+ return fs.readdirSync(abs).filter((n) => n !== '.DS_Store').flatMap((n) => walk(root, rel ? path.join(rel, n) : n));
48
61
  }
49
62
 
50
- function filesFor(platforms) {
63
+ export function filesFor(platforms) {
51
64
  const roots = [...PLATFORM_ROOTS.common, ...platforms.flatMap((p) => PLATFORM_ROOTS[p])];
52
65
  return roots.flatMap((r) => walk(TEMPLATE, r)).sort();
53
66
  }
54
67
 
68
+ function fail(msg) {
69
+ console.error(`pincer: ${msg}`);
70
+ process.exit(1);
71
+ }
72
+
55
73
  function readManifest(dir) {
56
74
  const p = path.join(dir, MANIFEST);
57
75
  if (!fs.existsSync(p)) return null;
58
- try {
59
- const data = JSON.parse(fs.readFileSync(p, 'utf8'));
60
- if (!data || !Array.isArray(data.platforms) || !data.platforms.length ||
61
- !data.platforms.every(p => ['claude', 'codex', 'copilot'].includes(p)) ||
62
- !data.files || typeof data.files !== 'object' || Array.isArray(data.files) ||
63
- Object.keys(data.files).some(p => path.isAbsolute(p) || p.split(/[\\/]/).includes('..')) ||
64
- (data.schema !== undefined && data.schema !== MANIFEST_SCHEMA)) {
65
- fail(`${MANIFEST} has an unsupported or invalid schema — restore a valid manifest before updating.`);
66
- }
67
- return data;
68
- } catch {
69
- fail(`${MANIFEST} exists but is not valid JSON — fix or remove it first.`);
70
- }
76
+ let data;
77
+ try { data = JSON.parse(fs.readFileSync(p, 'utf8')); } catch { fail(`${MANIFEST} exists but is not valid JSON — fix or remove it first.`); }
78
+ const unsafe = (f) => path.isAbsolute(f) || /^[A-Za-z]:/.test(f) || f.split(/[\\/]/).includes('..');
79
+ const invalid = !data || typeof data !== 'object' || Array.isArray(data)
80
+ || !Array.isArray(data.platforms) || !data.platforms.length || !data.platforms.every((x) => PLATFORMS.includes(x))
81
+ || !data.files || typeof data.files !== 'object' || Array.isArray(data.files)
82
+ || Object.keys(data.files).some(unsafe)
83
+ || (data.generated !== undefined && (!isPlainObject(data.generated) || Object.keys(data.generated).some(unsafe)))
84
+ || (data.schema !== undefined && !TRUSTED_SCHEMAS.includes(data.schema));
85
+ if (invalid) fail(`${MANIFEST} has an unsupported or invalid schema — restore a valid manifest before updating.`);
86
+ return data;
71
87
  }
72
88
 
73
- function writeManifest(dir, platforms, hashes) {
74
- const data = { schema: MANIFEST_SCHEMA, version: VERSION, platforms, files: hashes };
89
+ function writeManifest(dir, platforms, hashes, generated) {
90
+ const data = { schema: MANIFEST_SCHEMA, version: VERSION, platforms, files: hashes, generated: isPlainObject(generated) ? generated : {} };
75
91
  fs.writeFileSync(path.join(dir, MANIFEST), JSON.stringify(data, null, 2) + '\n');
76
92
  }
77
93
 
78
- // Preserve in-progress merges too. Reuse an identical proposal, never replace
79
- // a different existing sidecar (including one left by an earlier release).
94
+ // Preserve in-progress merges too. Reuse an identical proposal, never replace a
95
+ // different existing sidecar (including one left by an earlier release).
80
96
  function writeProposal(dest, content) {
81
97
  const base = dest + '.new';
82
98
  let proposal = base;
@@ -90,91 +106,135 @@ function writeProposal(dest, content) {
90
106
  return proposal;
91
107
  }
92
108
 
93
- // Copies template files into dir. `baseline` (manifest hashes) tells an update
94
- // which existing files are unmodified and therefore safe to refresh.
95
- function install(dir, platforms, baseline) {
109
+ // Copies the template files into dir. `baseline` (the manifest's hashes) tells an
110
+ // update which existing files are unmodified and therefore safe to refresh.
111
+ function install(dir, platforms, baseline, generated) {
96
112
  const results = { written: [], skipped: [], conflicted: [] };
97
113
  const hashes = {};
98
-
99
114
  for (const rel of filesFor(platforms)) {
100
115
  const src = fs.readFileSync(path.join(TEMPLATE, rel));
101
116
  const dest = path.join(dir, rel);
102
117
  hashes[rel] = sha(src);
103
-
104
118
  if (!fs.existsSync(dest)) {
105
119
  fs.mkdirSync(path.dirname(dest), { recursive: true });
106
120
  fs.writeFileSync(dest, src);
107
121
  results.written.push(rel);
108
122
  continue;
109
123
  }
110
-
111
124
  const current = fs.readFileSync(dest);
112
125
  if (sha(current) === sha(src)) {
113
126
  results.skipped.push(rel);
114
127
  } else if (baseline && baseline[rel] === sha(current)) {
115
- // untouched since install — safe to refresh with the new template version
116
- fs.writeFileSync(dest, src);
128
+ fs.writeFileSync(dest, src); // untouched since install: safe to refresh
117
129
  results.written.push(rel);
118
130
  } else {
119
131
  const proposal = writeProposal(dest, src);
120
- // Only a version actually installed by us grants overwrite permission.
121
- // null means we have never established a trusted upstream baseline.
132
+ // Only a version this installer wrote grants overwrite permission later;
133
+ // null means no trusted upstream baseline was ever established.
122
134
  hashes[rel] = baseline?.[rel] ?? null;
123
135
  results.conflicted.push({ rel, proposal: path.relative(dir, proposal) });
124
136
  }
125
137
  }
126
-
127
138
  for (const rel of EXECUTABLES) {
128
139
  const p = path.join(dir, rel);
129
140
  if (fs.existsSync(p)) fs.chmodSync(p, 0o755);
130
141
  }
131
-
132
142
  ensureGitignore(dir);
133
- writeManifest(dir, platforms, hashes);
143
+ writeManifest(dir, platforms, hashes, generated);
134
144
  return results;
135
145
  }
136
146
 
137
147
  function ensureGitignore(dir) {
138
148
  const p = path.join(dir, '.gitignore');
139
- let existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
149
+ const existing = fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : '';
140
150
  const have = new Set(existing.split('\n').map((l) => l.trim()));
141
151
  const missing = GITIGNORE_LINES.filter((l) => !have.has(l));
142
- if (missing.length) {
143
- const lead = existing && !existing.endsWith('\n') ? '\n' : '';
144
- fs.appendFileSync(p, `${lead}${existing ? '\n' : ''}# secrets (added by pincer init)\n${missing.join('\n')}\n`);
145
- existing = fs.readFileSync(p, 'utf8');
146
- }
147
- if (![RUNTIME_IGNORE, '/.pincer/', '.pincer'].some((l) => have.has(l))) {
148
- const lead = existing && !existing.endsWith('\n') ? '\n' : '';
149
- fs.appendFileSync(p, `${lead}${existing ? '\n' : ''}# pincer runtime state (added by pincer init)\n${RUNTIME_IGNORE}\n`);
150
- }
152
+ if (!missing.length) return;
153
+ const lead = existing && !existing.endsWith('\n') ? '\n' : '';
154
+ fs.appendFileSync(p, `${lead}${existing ? '\n' : ''}# secrets (added by pincer init)\n${missing.join('\n')}\n`);
151
155
  }
152
156
 
153
157
  function report({ written, skipped, conflicted }) {
154
158
  if (written.length) console.log(` wrote ${written.length} file(s)`);
155
159
  if (skipped.length) console.log(` skipped ${skipped.length} file(s) already up to date`);
156
- for (const { rel, proposal } of conflicted) {
157
- console.log(` CONFLICT ${rel} — local content preserved; the new version is at ${proposal}`);
160
+ for (const { rel, proposal } of conflicted) console.log(` CONFLICT ${rel} — local content preserved; the new version is at ${proposal}`);
161
+ if (conflicted.length) console.log('\n Review and merge each reported proposal, then remove the resolved sidecars.');
162
+ }
163
+
164
+ // The installed engine, loaded from the project so an update uses the version it just wrote.
165
+ function installedEngine(dir) {
166
+ let models = null;
167
+ try { models = requireAbsolute(path.join(dir, 'scripts/pincer/models.cjs')); } catch { models = null; } // absent before 1.1
168
+ return {
169
+ harness: requireAbsolute(path.join(dir, 'scripts/pincer/harness.cjs')),
170
+ generate: requireAbsolute(path.join(dir, 'scripts/pincer/generate.cjs')),
171
+ models,
172
+ };
173
+ }
174
+ // What the library or its table could not provide, as one sentence.
175
+ function missingText(name) {
176
+ if (name === 'models.json') return 'the library has no models.json; run pincer update';
177
+ if (name.startsWith('models.json:')) return `${name} — fix the library table; after an update the upstream copy is beside it as models.json.new`;
178
+ if (name.startsWith('models/')) { const [platform, tier] = name.slice('models/'.length).split('/'); return `the library table has no ${platform} value for tier ${tier}; set models.map.${platform}.${tier} in harness.json`; }
179
+ if (name.startsWith('agents/')) return `the library has no agent ${name.slice('agents/'.length)}`;
180
+ return `the library has no stage ${name}`;
181
+ }
182
+ // The tiers in use and what each installed platform will run them on.
183
+ function reportModels(dir, engine, harness, platforms, check) {
184
+ const { models } = engine;
185
+ if (!models) { check(false, 'models checked', 'could not load scripts/pincer/models.cjs — run: pincer update'); return; }
186
+ const library = engine.generate.libraryDir(dir);
187
+ const read = models.readTable(library);
188
+ if (!read.ok) {
189
+ if (read.missing) check(false, 'library has models.json', 'run: pincer update');
190
+ else check(false, 'library table readable', `${read.problem} — fix it; after an update the upstream copy is beside it as models.json.new`);
191
+ return;
158
192
  }
159
- if (conflicted.length) {
160
- console.log('\n Review and merge each reported proposal, then remove the resolved sidecars.');
193
+ const table = models.overlay(read.table, harness.models.map);
194
+ const eff = models.effective(harness, library);
195
+ const inUse = new Set([...Object.values(eff.stages), ...Object.values(eff.agents)].map((entry) => entry.tier).filter((tier) => tier && tier !== 'inherit'));
196
+ const tiers = engine.harness.MAP_TIERS.filter((tier) => inUse.has(tier));
197
+ if (!tiers.length) console.log(" models no tier in use; the session's model everywhere");
198
+ for (const platform of tiers.length ? platforms : []) {
199
+ const pairs = tiers.map((tier) => [tier, models.modelFor(table, platform, tier)]);
200
+ console.log(` models ${platform}: ${pairs.filter(([, value]) => value).map(([tier, value]) => `${tier}=${value}`).join(', ')}`);
201
+ for (const [tier, value] of pairs) if (!value) check(false, `tier ${tier} has a ${platform} value`, `set models.map.${platform}.${tier} in harness.json or update the kit`);
161
202
  }
203
+ if (platforms.includes('codex') && fs.existsSync(path.join(dir, '.codex/agents'))) console.log(' note Codex loads .codex/ only in a trusted project');
162
204
  }
163
205
 
164
- function nextSteps(platforms) {
165
- console.log('\nNext steps:');
166
- if (platforms.includes('claude')) {
167
- console.log(' Claude Code start `claude` in this repo and run /pincer-plan <brief>');
168
- }
169
- if (platforms.includes('codex')) {
170
- console.log(' Codex CLI start `codex` in this repo and type $pincer-plan <brief> (skills load from .agents/skills/; posture notes in .codex/README.md)');
206
+ // With an agreement in place the project's stage copies come from the library just
207
+ // installed; the generator keeps edited copies and leaves .new sidecars beside them.
208
+ function regenerate(dir) {
209
+ let engine;
210
+ try { engine = installedEngine(dir); } catch (error) { console.log(` note could not load the installed engine (${error.message}); run scripts/sync-prompts.sh yourself`); return; }
211
+ const loaded = engine.harness.load(dir);
212
+ if (!loaded.ok) {
213
+ console.log(loaded.missing
214
+ ? ' note no harness.json yet — run /pincer-blueprint; it generates the project copies of the stages it switches on'
215
+ : ` note harness.json: ${loaded.message} — fix it, then run scripts/sync-prompts.sh to regenerate the project copies`);
216
+ return;
171
217
  }
172
- if (platforms.includes('copilot')) {
173
- console.log(' Copilot enable "chat.promptFiles": true in VS Code settings, then /pincer-plan in chat');
218
+ let result;
219
+ try { result = engine.generate.generate(dir, { harness: loaded.harness }); } catch (error) {
220
+ console.log(` note ${error.message} — fix the library file, then run scripts/sync-prompts.sh to regenerate the project copies`);
221
+ return;
174
222
  }
175
- console.log(' All rules live in AGENTS.md — fill in its Conventions section once you know the stack.');
176
- const statusCmd = platforms.every((p) => p === 'codex') ? '$pincer-status' : platforms.includes('codex') ? '/pincer-status, $pincer-status on Codex' : '/pincer-status';
177
- console.log(` Any session scripts/pincer-status.sh shows where the workflow stands (also ${statusCmd})`);
223
+ for (const rel of result.written) console.log(` generated ${rel}`);
224
+ for (const rel of result.conflicted) console.log(` kept ${rel} (edited; fresh copy in ${rel}.new)`);
225
+ for (const rel of result.retained) console.log(` kept ${rel} (edited; not removed)`);
226
+ for (const rel of result.removed) console.log(` removed ${rel}`);
227
+ for (const rel of result.unsafe) console.log(` note ignored unsafe baseline path ${rel} in ${MANIFEST}`);
228
+ for (const name of result.missing) console.log(` note ${missingText(name)}`);
229
+ console.log(` generated: ${result.written.length} written, ${result.skipped.length} unchanged, ${result.conflicted.length} kept, ${result.removed.length} removed`);
230
+ }
231
+
232
+ function nextSteps(platforms) {
233
+ console.log('\nNext: run the blueprint. It looks at the repository, interviews you, and writes the agreement (harness.json and the top of AGENTS.md).');
234
+ if (platforms.includes('claude')) console.log(' Claude Code start `claude` in this repo and run /pincer-blueprint');
235
+ if (platforms.includes('codex')) console.log(' Codex CLI start `codex` in this repo and type $pincer-blueprint (skills load from .agents/skills/; posture notes in .codex/README.md)');
236
+ if (platforms.includes('copilot')) console.log(' Copilot enable "chat.promptFiles": true in VS Code settings, then /pincer-blueprint in chat');
237
+ console.log(' Any session scripts/pincer-status.sh shows where the work stands and the next command');
178
238
  }
179
239
 
180
240
  async function askPlatforms() {
@@ -183,7 +243,7 @@ async function askPlatforms() {
183
243
  console.log('Which platform(s) do you use?\n 1) Claude Code\n 2) Codex CLI\n 3) GitHub Copilot\n 4) all of them');
184
244
  const answer = (await q('Choose [1-4, default 4]: ')).trim() || '4';
185
245
  rl.close();
186
- const map = { 1: ['claude'], 2: ['codex'], 3: ['copilot'], 4: ['claude', 'codex', 'copilot'] };
246
+ const map = { 1: ['claude'], 2: ['codex'], 3: ['copilot'], 4: [...PLATFORMS] };
187
247
  return map[answer] || fail(`invalid choice: ${answer}`);
188
248
  }
189
249
 
@@ -191,25 +251,18 @@ function parsePlatformFlag(args) {
191
251
  const i = args.indexOf('--platform');
192
252
  if (i === -1) return null;
193
253
  const value = args[i + 1] || '';
194
- if (value === 'all') return ['claude', 'codex', 'copilot'];
254
+ if (value === 'all') return [...PLATFORMS];
195
255
  const list = value.split(',').map((s) => s.trim());
196
- if (list.every((p) => ['claude', 'codex', 'copilot'].includes(p)) && list.length) return list;
256
+ if (list.length && list.every((p) => PLATFORMS.includes(p))) return [...new Set(list)];
197
257
  fail(`--platform must be claude, codex, copilot (comma-separated) or all — got "${value}"`);
198
258
  }
199
259
 
200
- function fail(msg) {
201
- console.error(`pincer: ${msg}`);
202
- process.exit(1);
203
- }
204
-
205
260
  async function cmdInit(args) {
206
261
  const dir = process.cwd();
207
- if (readManifest(dir)) {
208
- fail(`this repo already has PINCER installed (${MANIFEST} exists) — use \`pincer update\`.`);
209
- }
210
- const platforms = parsePlatformFlag(args) || (process.stdin.isTTY ? await askPlatforms() : ['claude', 'codex', 'copilot']);
262
+ if (readManifest(dir)) fail(`this repo already has PINCER installed (${MANIFEST} exists) — use \`pincer update\`.`);
263
+ const platforms = parsePlatformFlag(args) || (process.stdin.isTTY ? await askPlatforms() : [...PLATFORMS]);
211
264
  console.log(`\nInstalling PINCER v${VERSION} for: ${platforms.join(', ')}\n`);
212
- report(install(dir, platforms, null));
265
+ report(install(dir, platforms, null, {}));
213
266
  nextSteps(platforms);
214
267
  }
215
268
 
@@ -218,17 +271,19 @@ async function cmdUpdate() {
218
271
  const manifest = readManifest(dir);
219
272
  if (!manifest) fail(`no ${MANIFEST} here — run \`pincer init\` first.`);
220
273
  console.log(`\nUpdating PINCER ${manifest.version} -> ${VERSION} for: ${manifest.platforms.join(', ')}\n`);
221
- // v1 manifests could adopt a local edit as their baseline. There is no safe
222
- // way to distinguish those from upstream files after the fact.
223
- const baseline = manifest.schema === MANIFEST_SCHEMA ? manifest.files : null;
274
+ // A manifest without a schema could have adopted a local edit as its baseline;
275
+ // there is no safe way to tell those from upstream files after the fact.
276
+ const baseline = TRUSTED_SCHEMAS.includes(manifest.schema) ? manifest.files : null;
224
277
  if (!baseline) console.log(' Legacy baselines are untrusted; differing files will be preserved for review.');
225
- report(install(dir, manifest.platforms, baseline));
278
+ report(install(dir, manifest.platforms, baseline, manifest.generated));
279
+ regenerate(dir);
226
280
  }
227
281
 
228
282
  function cmdDoctor() {
229
283
  const dir = process.cwd();
230
284
  const manifest = readManifest(dir);
231
285
  if (!manifest) fail(`no ${MANIFEST} here — run \`pincer init\` first.`);
286
+ const generated = isPlainObject(manifest.generated) ? manifest.generated : {};
232
287
  let problems = 0;
233
288
  const check = (ok, label, hint) => {
234
289
  console.log(` ${ok ? 'ok ' : 'FAIL'} ${label}${ok || !hint ? '' : ` — ${hint}`}`);
@@ -238,78 +293,40 @@ function cmdDoctor() {
238
293
  console.log(`\nPINCER doctor (installed ${manifest.version}, this tool ${VERSION})\n`);
239
294
  const missing = Object.keys(manifest.files).filter((rel) => !fs.existsSync(path.join(dir, rel)));
240
295
  check(missing.length === 0, 'all installed files present', `missing: ${missing.join(', ')}`);
241
-
242
296
  for (const rel of EXECUTABLES) {
243
297
  const p = path.join(dir, rel);
244
- if (!fs.existsSync(p)) continue;
245
- check(!!(fs.statSync(p).mode & 0o100), `${rel} is executable`, `run: chmod +x ${rel}`);
246
- }
247
-
248
- const gi = fs.existsSync(path.join(dir, '.gitignore')) ? fs.readFileSync(path.join(dir, '.gitignore'), 'utf8') : '';
249
- check(GITIGNORE_LINES.every((l) => gi.split('\n').map((s) => s.trim()).includes(l)),
250
- '.gitignore covers .env files', 'add: .env / .env.* / !.env.example');
251
- if (![RUNTIME_IGNORE, '/.pincer/', '.pincer'].some((l) => gi.split('\n').map((s) => s.trim()).includes(l))) {
252
- console.log(` note .gitignore does not list ${RUNTIME_IGNORE} (runtime state); \`pincer update\`, \`register\` or \`migrate --apply\` adds it`);
298
+ if (fs.existsSync(p)) check(!!(fs.statSync(p).mode & 0o100), `${rel} is executable`, `run: chmod +x ${rel}`);
253
299
  }
254
-
300
+ const gi = fs.existsSync(path.join(dir, '.gitignore')) ? fs.readFileSync(path.join(dir, '.gitignore'), 'utf8').split('\n').map((s) => s.trim()) : [];
301
+ check(GITIGNORE_LINES.every((l) => gi.includes(l)), '.gitignore covers .env files', 'add: .env / .env.* / !.env.example');
255
302
  const stale = fs.existsSync(TEMPLATE) && manifest.version !== VERSION;
256
303
  check(!stale, `install is current (v${manifest.version})`, 'run: pincer update');
257
304
 
258
- // The runtime ships with every layout; a project with legacy receipts and no
259
- // change binding can migrate explicitly (never silently here).
260
- const runtimeFiles = Object.keys(manifest.files).filter((rel) => rel === 'scripts/pincer-runtime.cjs' || rel.startsWith('scripts/pincer-runtime/'));
261
- if (runtimeFiles.length) {
262
- const missingRuntime = runtimeFiles.filter((rel) => !fs.existsSync(path.join(dir, rel)));
263
- check(missingRuntime.length === 0, 'runtime files present', `missing: ${missingRuntime.join(', ')} — run: pincer update`);
264
- }
265
- // Files an earlier kit installed that this version no longer ships stay on disk
266
- // (the installer never deletes); name them so they can be removed by hand.
267
- const OBSOLETE = ['scripts/pincer-ticket-lib.sh'];
268
- const obsolete = OBSOLETE.filter((rel) => fs.existsSync(path.join(dir, rel)) && !(rel in manifest.files));
269
- if (obsolete.length) console.log(` note obsolete kit file(s) from an earlier version, safe to delete: ${obsolete.join(', ')}`);
270
-
271
- const ticketsDir = path.join(dir, 'tickets');
272
- const legacyReceipts = fs.existsSync(ticketsDir)
273
- ? fs.readdirSync(ticketsDir).filter((n) => /^T-\d+.*\.md$/.test(n)).filter((n) => /^(verified|last_check):/m.test(fs.readFileSync(path.join(ticketsDir, n), 'utf8')))
274
- : [];
275
- const bindingsDir = path.join(dir, '.prd', 'changes');
276
- const bindingFiles = fs.existsSync(bindingsDir) ? fs.readdirSync(bindingsDir).filter((n) => n.endsWith('.json')) : [];
277
- const hasBinding = bindingFiles.length > 0;
278
- // A v0.5.0 binding (schema 1) converts explicitly into a schema 2 change record; doctor only reports it.
279
- const oldBindings = bindingFiles.filter((n) => { try { return JSON.parse(fs.readFileSync(path.join(bindingsDir, n), 'utf8')).schema === 1; } catch { return false; } });
280
- for (const n of oldBindings) {
281
- let prd = '.prd/prd-vN.md';
282
- try { prd = JSON.parse(fs.readFileSync(path.join(bindingsDir, n), 'utf8')).prd || prd; } catch { /* reported above by the runtime */ }
283
- console.log(` note migration to change records available: .prd/changes/${n} is a v0.5.0 binding (schema 1) — preview with: node scripts/pincer-runtime.cjs migrate --preview --prd ${prd}`);
284
- }
285
- // A schema 2 change record beside an authored coverage map can adopt strict coverage explicitly; doctor only reports it.
286
- for (const n of bindingFiles) {
287
- let doc = null;
288
- try { doc = JSON.parse(fs.readFileSync(path.join(bindingsDir, n), 'utf8')); } catch { doc = null; }
289
- if (!doc || doc.schema !== 2) continue;
290
- const id = n.replace(/\.json$/, '');
291
- if (fs.existsSync(path.join(dir, '.prd', 'coverage', `${id}.json`))) console.log(` note strict coverage adoption available: .prd/coverage/${id}.json exists and .prd/changes/${n} is a schema 2 record — preview with: node scripts/pincer-runtime.cjs coverage adopt --preview --change ${id}`);
292
- }
293
- if (legacyReceipts.length && !hasBinding) {
294
- const prdDir = path.join(dir, '.prd');
295
- const prds = fs.existsSync(prdDir) ? fs.readdirSync(prdDir).map((n) => n.match(/^prd-v(\d+)\.md$/)).filter(Boolean).map((m) => Number(m[1])) : [];
296
- const latest = prds.length ? `.prd/prd-v${Math.max(...prds)}.md` : '.prd/prd-vN.md';
297
- console.log(` note migration available: ${legacyReceipts.length} ticket(s) carry legacy receipts and no change binding exists — preview with: node scripts/pincer-runtime.cjs migrate --preview --prd ${latest}`);
305
+ // The agreement, checked with the engine this install carries.
306
+ if (!fs.existsSync(path.join(dir, 'harness.json'))) {
307
+ console.log(' note no harness.json yet — run /pincer-blueprint');
308
+ } else {
309
+ try {
310
+ const engine = installedEngine(dir);
311
+ const loaded = engine.harness.load(dir);
312
+ check(loaded.ok, loaded.ok ? 'harness.json valid' : `harness.json: ${loaded.message}`);
313
+ if (loaded.ok) reportModels(dir, engine, loaded.harness, manifest.platforms, check);
314
+ } catch (error) {
315
+ check(false, 'harness.json checked', `could not load scripts/pincer/harness.cjs (${error.message}) — run: pincer update`);
316
+ }
298
317
  }
299
318
 
319
+ const obsolete = OBSOLETE.filter((rel) => fs.existsSync(path.join(dir, rel)) && !(rel in manifest.files) && !(rel in generated));
320
+ if (obsolete.length) console.log(` note obsolete kit file(s) from an earlier version, safe to delete: ${obsolete.join(', ')}`);
300
321
  const edited = Object.entries(manifest.files)
301
322
  .filter(([rel, h]) => fs.existsSync(path.join(dir, rel)) && sha(fs.readFileSync(path.join(dir, rel))) !== h)
302
323
  .map(([rel]) => rel);
303
- if (edited.length) {
304
- console.log(` note locally edited (kept as-is on update): ${edited.join(', ')}`);
305
- }
324
+ if (edited.length) console.log(` note locally edited (kept as-is on update): ${edited.join(', ')}`);
306
325
 
307
- const leftovers = [...new Set([...walk(TEMPLATE), ...Object.keys(manifest.files)])].flatMap(rel => {
326
+ const leftovers = [...new Set([...walk(TEMPLATE), ...Object.keys(manifest.files), ...Object.keys(generated)])].flatMap((rel) => {
308
327
  const parent = path.dirname(path.join(dir, rel));
309
328
  const base = path.basename(rel) + '.new';
310
- return fs.existsSync(parent) ? fs.readdirSync(parent)
311
- .filter(name => name === base || name.startsWith(base + '.'))
312
- .map(name => path.join(parent, name)) : [];
329
+ return fs.existsSync(parent) ? fs.readdirSync(parent).filter((name) => name === base || name.startsWith(base + '.')).map((name) => path.join(parent, name)) : [];
313
330
  });
314
331
  check(leftovers.length === 0, 'no unmerged *.new files', leftovers.map((p) => path.relative(dir, p)).join(', '));
315
332
 
@@ -317,18 +334,24 @@ function cmdDoctor() {
317
334
  process.exit(problems ? 1 : 0);
318
335
  }
319
336
 
320
- const [cmd, ...rest] = process.argv.slice(2);
321
- if (cmd === 'init') await cmdInit(rest);
322
- else if (cmd === 'update') await cmdUpdate();
323
- else if (cmd === 'doctor') cmdDoctor();
324
- else {
325
- console.log(`pincer-workflow v${VERSION} — PRD-driven agentic delivery workflow
337
+ function usage() {
338
+ console.log(`pincer-workflow v${VERSION} — a harness builder for AI coding agents
326
339
 
327
340
  Usage:
328
341
  npx pincer-workflow init [--platform claude|codex|copilot|all]
329
342
  npx pincer-workflow update
330
343
  npx pincer-workflow doctor
331
344
 
332
- Docs: https://github.com/orchestratedbyalex/pincer-workflow`);
333
- process.exit(cmd ? 1 : 0);
345
+ After init, run the blueprint (/pincer-blueprint or $pincer-blueprint): it writes the
346
+ agreement the other stages read. Docs: https://github.com/orchestratedbyalex/pincer-workflow`);
347
+ }
348
+
349
+ // Run the command only when this file is the program, not when a test imports it.
350
+ const invoked = process.argv[1] && (() => { try { return fs.realpathSync(process.argv[1]) === fs.realpathSync(HERE); } catch { return false; } })();
351
+ if (invoked) {
352
+ const [cmd, ...rest] = process.argv.slice(2);
353
+ if (cmd === 'init') await cmdInit(rest);
354
+ else if (cmd === 'update') await cmdUpdate();
355
+ else if (cmd === 'doctor') cmdDoctor();
356
+ else { usage(); process.exit(cmd ? 1 : 0); }
334
357
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pincer-workflow",
3
- "version": "0.6.0",
4
- "description": "PINCER — a PRD-driven agentic delivery workflow for Claude Code, Codex CLI, and GitHub Copilot. Plan · Investigate · Narrow · Code · Evaluate · Release.",
3
+ "version": "1.0.0",
4
+ "description": "PINCER \u2014 a harness builder for AI coding agents. It looks at your project, interviews you, writes the project agreement, and runs the pipeline that agreement describes. Claude Code, Codex CLI and GitHub Copilot.",
5
5
  "bin": {
6
6
  "pincer": "bin/pincer.js"
7
7
  },
@@ -14,7 +14,8 @@
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/run.js",
18
+ "generate": "node template/scripts/pincer/cli.cjs --root template generate --harness test/fixtures/always-on.harness.json --platform all --overwrite"
18
19
  },
19
20
  "keywords": [
20
21
  "claude-code",
@@ -0,0 +1,70 @@
1
+ ---
2
+ name: pincer-blueprint
3
+ description: "Look at the project, interview the person, write the harness agreement (harness.json and the top of AGENTS.md) and generate the project's stage copies"
4
+ ---
5
+ <!-- Generated by scripts/pincer/cli.cjs generate from .claude/skills/pincer-blueprint/SKILL.md — edit the project copy, not this file -->
6
+
7
+ # Blueprint
8
+
9
+ The first stage and the only one that writes the agreement. It is always on. It
10
+ reads the repository, decides greenfield or brownfield and says why, interviews
11
+ the person one question at a time through the six sections, writes `harness.json`
12
+ and the agreement at the top of `AGENTS.md`, and generates the project's copies of
13
+ the stages it switched on. Scripts do the reading and the writing so they can be
14
+ tested; the session does the asking.
15
+
16
+ Rules for the whole interview: one question at a time. Options listed, the
17
+ recommended one first, each explained in a sentence. Every answer recon could
18
+ pre-fill is presented as "I see X; is that right?", never assumed. `open` is always
19
+ an allowed answer and is written as open, never guessed. Record what the person
20
+ says; add no tool, step or role they did not choose.
21
+
22
+ ## Reads
23
+ - `harness.json`, if present: this is a re-run (see the last paragraph of Steps).
24
+ - The recon report from `node scripts/pincer-blueprint.cjs recon --json --network`:
25
+ `kind`, `reasons`, `facts`, `prefill`, `unavailable`.
26
+ - `AGENTS.md` as it stands: the standing rules and the conventions below the markers stay.
27
+ - The formats under `.claude/skills/pincer-blueprint/formats/`:
28
+ `.claude/skills/pincer-blueprint/formats/harness.md` (every field of `harness.json`)
29
+ and `.claude/skills/pincer-blueprint/formats/roster-rules.md` (how the roster follows the answers).
30
+ - The prompts under `.claude/skills/pincer-blueprint/prompts/`, one per question, named in the steps.
31
+ - `the text that follows the `$pincer-blueprint` mention, if any (when omitted, use the skill's documented default)`: notes the person gave up front. Treat them as pre-filled answers to confirm, not as answers.
32
+
33
+ ## Steps
34
+ 1. [session] Run `scripts/pincer-status.sh --stage blueprint` (always `on`; the plain report also says whether `harness.json` exists), then `node scripts/pincer-blueprint.cjs recon --json --network` and read the whole result: `kind`, `reasons`, `facts`, `prefill`, `unavailable`.
35
+ 2. [session] State the kind and the reasons in one paragraph with `.claude/skills/pincer-blueprint/prompts/confirm-kind.md`, naming what recon could not see. Ask the person to confirm or correct. Their word wins over recon.
36
+ 3. [session] Interview, one section at a time and one question at a time, in this order, each with its prompt: Systems (`.claude/skills/pincer-blueprint/prompts/q-systems.md`), Current pipeline (`.claude/skills/pincer-blueprint/prompts/q-pipeline.md`), Handoff artifacts (`.claude/skills/pincer-blueprint/prompts/q-handoffs.md`), Simplify (`.claude/skills/pincer-blueprint/prompts/q-simplify.md`), Responsibility (`.claude/skills/pincer-blueprint/prompts/q-responsibility.md`), Repo (`.claude/skills/pincer-blueprint/prompts/q-repo.md`). Fill each prompt's slots from recon and from earlier answers before asking.
37
+ 4. [session] Propose the roster from the answers and recon by applying `.claude/skills/pincer-blueprint/formats/roster-rules.md`; ask the person to confirm with `.claude/skills/pincer-blueprint/prompts/q-roster.md`.
38
+ 5. [session] Propose the model tier per role from the "Model tiers" section of `.claude/skills/pincer-blueprint/formats/roster-rules.md` and ask the person to confirm with `.claude/skills/pincer-blueprint/prompts/q-models.md`; record the answers under `models`.
39
+ 6. [session] Write the answers as `harness.json` in the shape `.claude/skills/pincer-blueprint/formats/harness.md` describes: start from recon's `prefill`, put every unanswered item in `open`, then run `node scripts/pincer-blueprint.cjs write`. On validation problems, fix the file and run it again. Never hand-edit `AGENTS.md` between the markers.
40
+ 7. [session] Show the agreement from `AGENTS.md` (the text between the markers) and ask for corrections. Apply them to `harness.json` and run `write` again. One round.
41
+ 8. [session] Name the first job with `.claude/skills/pincer-blueprint/prompts/first-job.md`: brownfield, the smallest real change that recon or the open items point at; greenfield, the setup job. Put it in `firstJob`, run `write` again, and say that the first run through the pipeline is the trace to revisit this agreement with.
42
+ 9. [session] Print the output of `scripts/pincer-status.sh` and stop. The next command is on its last line.
43
+
44
+ Re-run: when `harness.json` already exists, run recon, list what differs between the
45
+ file and what recon sees (kind, remote, CI, checks, people, merge style), ask only
46
+ about those and about anything the person wants to change, then continue from step 6.
47
+ When the file has no `models` key, propose the tiers once (step 5); otherwise ask only
48
+ when the person wants to change them.
49
+
50
+ ## Checks
51
+ - `node scripts/pincer-blueprint.cjs write` exits 0 and prints `wrote harness.json` and `wrote AGENTS.md`.
52
+ - The agreement between the markers has the six headings in order and names the project.
53
+ - Every pre-filled answer was confirmed or corrected by the person; no cell was invented to look complete. `open` is the honest cell.
54
+ - `scripts/pincer-status.sh` prints the project's name and a `Stages:` line that matches what was agreed.
55
+
56
+ ## Writes
57
+ - `harness.json` at the repository root, through `write`.
58
+ - The agreement between the markers at the top of `AGENTS.md`, through `write`; nothing below the end marker.
59
+ - The project copies of the stages that are on under `.claude/skills/pincer-<stage>/`, their commands, and the Codex and Copilot files for the installed platforms, through `write`.
60
+ - Nothing else. No CI workflow, no branch protection, no tool configuration: those belong to the first job when the person chose them.
61
+
62
+ ## Without subagents
63
+ This stage dispatches no agents and runs the same everywhere. The scripts need no
64
+ platform feature: `node scripts/pincer-blueprint.cjs` and `scripts/pincer-status.sh`
65
+ run in any shell. On a platform without slash commands, read this file and follow it.
66
+
67
+ ## When this stage is off
68
+ Never. Blueprint is always on; `harness.json` cannot switch it off (the validator
69
+ rejects a `stages.blueprint` entry). Without an agreement every other stage refuses
70
+ with `no agreement yet: run $pincer-blueprint`, and status points here.