pincer-workflow 0.7.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -193
- package/bin/pincer.js +189 -166
- package/package.json +4 -3
- package/template/.agents/skills/pincer-blueprint/SKILL.md +70 -0
- package/template/.agents/skills/pincer-status/SKILL.md +29 -55
- package/template/.claude/commands/pincer-blueprint.md +7 -0
- package/template/.claude/commands/pincer-status.md +4 -59
- package/template/.claude/hooks/hook-policy.cjs +60 -25
- package/template/.claude/hooks/ticket-guard.sh +6 -3
- package/template/.claude/pincer/library/agents/accessibility-reviewer.md +27 -0
- package/template/.claude/pincer/library/agents/code-quality-reviewer.md +25 -0
- package/template/.claude/pincer/library/agents/codebase-explorer.md +23 -0
- package/template/.claude/pincer/library/agents/fix-agent.md +23 -0
- package/template/.claude/pincer/library/agents/history-reviewer.md +26 -0
- package/template/.claude/pincer/library/agents/implementer.md +24 -0
- package/template/.claude/pincer/library/agents/security-reviewer.md +26 -0
- package/template/.claude/pincer/library/agents/silent-failure-hunter.md +27 -0
- package/template/.claude/pincer/library/agents/standards-reviewer.md +25 -0
- package/template/.claude/pincer/library/agents/test-coverage-reviewer.md +26 -0
- package/template/.claude/pincer/library/agents-skeleton.md +89 -0
- package/template/.claude/pincer/library/agreement.md +46 -0
- package/template/.claude/pincer/library/models.json +6 -0
- package/template/.claude/pincer/library/stages/blueprint/SKILL.md +69 -0
- package/template/.claude/pincer/library/stages/blueprint/formats/harness.md +82 -0
- package/template/.claude/pincer/library/stages/blueprint/formats/roster-rules.md +61 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/confirm-kind.md +19 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/first-job.md +18 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-handoffs.md +21 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-models.md +20 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-pipeline.md +27 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-repo.md +21 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-responsibility.md +30 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-roster.md +20 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-simplify.md +26 -0
- package/template/.claude/pincer/library/stages/blueprint/prompts/q-systems.md +28 -0
- package/template/.claude/pincer/library/stages/code/SKILL.md +59 -0
- package/template/.claude/pincer/library/stages/code/formats/pr.md +55 -0
- package/template/.claude/pincer/library/stages/code/prompts/implement.md +25 -0
- package/template/.claude/pincer/library/stages/evaluate/SKILL.md +51 -0
- package/template/.claude/pincer/library/stages/evaluate/formats/evidence.md +49 -0
- package/template/.claude/pincer/library/stages/evaluate/formats/finding-report.md +72 -0
- package/template/.claude/pincer/library/stages/evaluate/formats/finding.md +35 -0
- package/template/.claude/pincer/library/stages/evaluate/prompts/review.md +25 -0
- package/template/.claude/pincer/library/stages/narrow/SKILL.md +47 -0
- package/template/.claude/pincer/library/stages/narrow/formats/ticket.md +126 -0
- package/template/.claude/pincer/library/stages/narrow/prompts/explore.md +19 -0
- package/template/.claude/pincer/library/stages/plan/SKILL.md +46 -0
- package/template/.claude/pincer/library/stages/plan/formats/spec.md +100 -0
- package/template/.claude/pincer/library/stages/plan/prompts/explore.md +20 -0
- package/template/.claude/pincer/library/stages/release/SKILL.md +51 -0
- package/template/.claude/pincer/library/stages/status/SKILL.md +37 -0
- package/template/.claude/skills/pincer-blueprint/SKILL.md +69 -0
- package/template/.claude/skills/pincer-blueprint/formats/harness.md +82 -0
- package/template/.claude/skills/pincer-blueprint/formats/roster-rules.md +61 -0
- package/template/.claude/skills/pincer-blueprint/prompts/confirm-kind.md +19 -0
- package/template/.claude/skills/pincer-blueprint/prompts/first-job.md +18 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-handoffs.md +21 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-models.md +20 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-pipeline.md +27 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-repo.md +21 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-responsibility.md +30 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-roster.md +20 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-simplify.md +26 -0
- package/template/.claude/skills/pincer-blueprint/prompts/q-systems.md +28 -0
- package/template/.claude/skills/pincer-status/SKILL.md +37 -0
- package/template/.codex/README.md +39 -35
- package/template/.github/copilot-instructions.md +19 -14
- package/template/.github/prompts/pincer-blueprint.prompt.md +70 -0
- package/template/.github/prompts/pincer-status.prompt.md +29 -55
- package/template/AGENTS.md +34 -67
- package/template/docs/engine.md +249 -0
- package/template/docs/release-checklist.md +27 -28
- package/template/scripts/pincer/blueprint.cjs +137 -0
- package/template/scripts/pincer/cli.cjs +233 -0
- package/template/scripts/pincer/evidence.cjs +142 -0
- package/template/scripts/pincer/generate.cjs +375 -0
- package/template/scripts/pincer/harness.cjs +269 -0
- package/template/scripts/pincer/models.cjs +121 -0
- package/template/scripts/{pincer-runtime → pincer}/parse.cjs +83 -133
- package/template/scripts/pincer/recon.cjs +205 -0
- package/template/scripts/pincer/status.cjs +173 -0
- package/template/scripts/pincer/ticket.cjs +204 -0
- package/template/scripts/pincer/util.cjs +104 -0
- package/template/scripts/pincer-blueprint.cjs +7 -0
- package/template/scripts/pincer-evidence.cjs +5 -73
- package/template/scripts/pincer-status.sh +10 -12
- package/template/scripts/pincer-ticket.sh +15 -18
- package/template/scripts/sync-prompts.sh +2 -50
- package/template/.agents/skills/pincer-code/SKILL.md +0 -240
- package/template/.agents/skills/pincer-evaluate/SKILL.md +0 -178
- package/template/.agents/skills/pincer-narrow/SKILL.md +0 -148
- package/template/.agents/skills/pincer-plan/SKILL.md +0 -134
- package/template/.agents/skills/pincer-release/SKILL.md +0 -82
- package/template/.claude/agents/code-quality-reviewer.md +0 -38
- package/template/.claude/agents/codebase-explorer.md +0 -23
- package/template/.claude/commands/pincer-code.md +0 -238
- package/template/.claude/commands/pincer-evaluate.md +0 -176
- package/template/.claude/commands/pincer-narrow.md +0 -146
- package/template/.claude/commands/pincer-plan.md +0 -132
- package/template/.claude/commands/pincer-release.md +0 -80
- package/template/.claude/references/prd-template.md +0 -108
- package/template/.claude/references/ticket-template.md +0 -86
- package/template/.github/prompts/pincer-code.prompt.md +0 -240
- package/template/.github/prompts/pincer-evaluate.prompt.md +0 -178
- package/template/.github/prompts/pincer-narrow.prompt.md +0 -148
- package/template/.github/prompts/pincer-plan.prompt.md +0 -134
- package/template/.github/prompts/pincer-release.prompt.md +0 -82
- package/template/docs/dry-run-checklist.md +0 -264
- package/template/docs/runtime-contracts.md +0 -1769
- package/template/scripts/pincer-runtime/adopt.cjs +0 -132
- package/template/scripts/pincer-runtime/agreement.cjs +0 -240
- package/template/scripts/pincer-runtime/authorization.cjs +0 -167
- package/template/scripts/pincer-runtime/changes.cjs +0 -517
- package/template/scripts/pincer-runtime/checks.cjs +0 -48
- package/template/scripts/pincer-runtime/coverage.cjs +0 -361
- package/template/scripts/pincer-runtime/dispositions.cjs +0 -95
- package/template/scripts/pincer-runtime/evidence.cjs +0 -676
- package/template/scripts/pincer-runtime/fsutil.cjs +0 -37
- package/template/scripts/pincer-runtime/gates.cjs +0 -73
- package/template/scripts/pincer-runtime/identity.cjs +0 -163
- package/template/scripts/pincer-runtime/impact.cjs +0 -177
- package/template/scripts/pincer-runtime/io.cjs +0 -41
- package/template/scripts/pincer-runtime/lifecycle.cjs +0 -311
- package/template/scripts/pincer-runtime/locator.cjs +0 -158
- package/template/scripts/pincer-runtime/migrate.cjs +0 -204
- package/template/scripts/pincer-runtime/phases.cjs +0 -245
- package/template/scripts/pincer-runtime/readiness.cjs +0 -102
- package/template/scripts/pincer-runtime/requirements.cjs +0 -255
- package/template/scripts/pincer-runtime/resume.cjs +0 -273
- package/template/scripts/pincer-runtime/routing.cjs +0 -54
- package/template/scripts/pincer-runtime/runner.cjs +0 -242
- package/template/scripts/pincer-runtime/sanitize.cjs +0 -63
- package/template/scripts/pincer-runtime/scaffold.cjs +0 -254
- package/template/scripts/pincer-runtime/source.cjs +0 -129
- package/template/scripts/pincer-runtime/state.cjs +0 -314
- package/template/scripts/pincer-runtime/status.cjs +0 -514
- package/template/scripts/pincer-runtime/transaction.cjs +0 -200
- package/template/scripts/pincer-runtime/transitions.cjs +0 -134
- package/template/scripts/pincer-runtime.cjs +0 -686
package/bin/pincer.js
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// pincer — installer for
|
|
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
|
|
5
|
-
// npx pincer-workflow update refresh an
|
|
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
|
|
9
|
-
//
|
|
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
|
|
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
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
//
|
|
37
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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
|
|
121
|
-
// null means
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
-
|
|
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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
|
|
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
|
-
|
|
173
|
-
|
|
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
|
-
|
|
176
|
-
const
|
|
177
|
-
console.log(`
|
|
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: [
|
|
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 [
|
|
254
|
+
if (value === 'all') return [...PLATFORMS];
|
|
195
255
|
const list = value.split(',').map((s) => s.trim());
|
|
196
|
-
if (list.every((p) =>
|
|
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
|
-
|
|
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
|
-
//
|
|
222
|
-
// way to
|
|
223
|
-
const baseline = manifest.schema
|
|
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 (
|
|
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
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
333
|
-
|
|
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.
|
|
4
|
-
"description": "PINCER — a
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "PINCER — 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/
|
|
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.
|