@renzrollon/interlock 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.
- package/.claude-plugin/marketplace.json +19 -0
- package/.claude-plugin/plugin.json +58 -0
- package/CHANGELOG.md +380 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/agents/ping.md +8 -0
- package/agents/worker.md +8 -0
- package/bin/interlock +2655 -0
- package/bin/interlock-graph +6 -0
- package/bin/interlock-run +508 -0
- package/bin/interlock-ship-acp +39 -0
- package/hooks/_shared.mjs +162 -0
- package/hooks/guard-commit.mjs +77 -0
- package/hooks/guard-tasks.mjs +107 -0
- package/hooks/guard-tests.mjs +63 -0
- package/hooks/preflight.mjs +107 -0
- package/lib/artifacts.mjs +316 -0
- package/lib/autonomy.mjs +149 -0
- package/lib/conformance.mjs +132 -0
- package/lib/doctor.mjs +1176 -0
- package/lib/drift.mjs +409 -0
- package/lib/eval-history.mjs +304 -0
- package/lib/evals-calibrate.mjs +317 -0
- package/lib/evals-capture.mjs +437 -0
- package/lib/evals-promote.mjs +257 -0
- package/lib/evals-triage.mjs +216 -0
- package/lib/findings.mjs +340 -0
- package/lib/graph/cli.mjs +266 -0
- package/lib/graph/extract/config-json.mjs +74 -0
- package/lib/graph/extract/create-extractor.mjs +35 -0
- package/lib/graph/extract/memory.mjs +99 -0
- package/lib/graph/extract/openspec.mjs +80 -0
- package/lib/graph/extract/python.mjs +137 -0
- package/lib/graph/extract/regex-fallback.mjs +187 -0
- package/lib/graph/extract/shell.mjs +71 -0
- package/lib/graph/extract/typescript.mjs +217 -0
- package/lib/graph/extract/walk.mjs +272 -0
- package/lib/graph/format.mjs +51 -0
- package/lib/graph/graph/build.mjs +374 -0
- package/lib/graph/graph/query.mjs +249 -0
- package/lib/graph/graph/report.mjs +122 -0
- package/lib/graph/graph/update.mjs +47 -0
- package/lib/graph/paths.mjs +66 -0
- package/lib/graph/retrieve/context.mjs +86 -0
- package/lib/graph/retrieve/docs-index.mjs +116 -0
- package/lib/graph/retrieve/markdown.mjs +246 -0
- package/lib/host/acp.mjs +551 -0
- package/lib/host/claude-cli.mjs +285 -0
- package/lib/host/codex.mjs +231 -0
- package/lib/host/model-map.mjs +129 -0
- package/lib/host/qwen.mjs +176 -0
- package/lib/host/registry.mjs +213 -0
- package/lib/host.mjs +393 -0
- package/lib/ledger.mjs +444 -0
- package/lib/limits.mjs +525 -0
- package/lib/locators.mjs +99 -0
- package/lib/merge-lanes.mjs +148 -0
- package/lib/metrics.mjs +99 -0
- package/lib/notify.mjs +229 -0
- package/lib/outcomes.mjs +560 -0
- package/lib/plan-fingerprint.mjs +570 -0
- package/lib/project-slug.mjs +18 -0
- package/lib/prompts/commit.mjs +26 -0
- package/lib/prompts/dimensions.mjs +70 -0
- package/lib/prompts/handoff.mjs +86 -0
- package/lib/prompts/implementer.mjs +150 -0
- package/lib/prompts/index.mjs +250 -0
- package/lib/prompts/planner.mjs +70 -0
- package/lib/prompts/remediate.mjs +137 -0
- package/lib/prompts/replan.mjs +23 -0
- package/lib/prompts/review.mjs +107 -0
- package/lib/prompts/schemas.mjs +206 -0
- package/lib/prompts/stage.mjs +48 -0
- package/lib/prompts/verify.mjs +88 -0
- package/lib/ready.mjs +812 -0
- package/lib/receipt.mjs +434 -0
- package/lib/remediate.mjs +275 -0
- package/lib/report-html.mjs +623 -0
- package/lib/report.mjs +1124 -0
- package/lib/review-core.mjs +713 -0
- package/lib/risk.mjs +370 -0
- package/lib/run-log.mjs +1004 -0
- package/lib/run-paths.mjs +239 -0
- package/lib/run.mjs +2921 -0
- package/lib/ship-stage.mjs +223 -0
- package/lib/spill.mjs +150 -0
- package/lib/surface.mjs +195 -0
- package/lib/verify.mjs +1074 -0
- package/lib/waves.mjs +2661 -0
- package/package.json +40 -0
- package/shared/CONTEXT-HYGIENE.md +84 -0
- package/shared/DECISION-LEDGER.md +141 -0
- package/shared/EXPLORE-BRIEF.md +96 -0
- package/shared/INVARIANT-SWEEP.md +57 -0
- package/shared/SESSION-SHAPES.md +261 -0
- package/shared/SKILL-CANDIDATE-BRIEF.md +106 -0
- package/shared/TEST-PROFILE.md +204 -0
- package/shared/TOOL-ECONOMY.md +155 -0
- package/skills/bootstrap/SKILL.md +226 -0
- package/skills/commit/SKILL.md +99 -0
- package/skills/dispatch/SKILL.md +78 -0
- package/skills/docs-digest/SKILL.md +109 -0
- package/skills/evals/SKILL.md +160 -0
- package/skills/explain-code/SKILL.md +42 -0
- package/skills/explain-code/commit-teachin.md +136 -0
- package/skills/explain-code/module-walkthrough.md +358 -0
- package/skills/explore/SKILL.md +159 -0
- package/skills/fix-tests/SKILL.md +120 -0
- package/skills/graph/SKILL.md +136 -0
- package/skills/manual-test-plan/SKILL.md +121 -0
- package/skills/mr/SKILL.md +170 -0
- package/skills/report/SKILL.md +121 -0
- package/skills/review-artifacts/SKILL.md +127 -0
- package/skills/review-code/REVIEW.template.md +49 -0
- package/skills/review-code/SKILL.md +156 -0
- package/skills/review-code/dimensions/architecture.md +10 -0
- package/skills/review-code/dimensions/devops.md +12 -0
- package/skills/review-code/dimensions/language.md +12 -0
- package/skills/review-code/dimensions/qa.md +12 -0
- package/skills/review-code/dimensions/security.md +12 -0
- package/skills/review-code/dimensions/technical-lead.md +19 -0
- package/skills/ship/SKILL.md +67 -0
- package/skills/spec/SKILL.md +264 -0
- package/skills/spec/continuity.md +66 -0
- package/workflows/ship.js +565 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// The handoff briefing — manual test plan, code explanation, spec
|
|
2
|
+
// conformance and learnings capture, in that order.
|
|
3
|
+
//
|
|
4
|
+
// Moved from `workflows/ship.js` (the handoff section) as part of
|
|
5
|
+
// `emit-strict-tail-from-cli`. The only change is where the two decisions
|
|
6
|
+
// come from (design D5): the retired text told the agent to run
|
|
7
|
+
// `interlock surface` and, when conformance was on, `interlock conformance`,
|
|
8
|
+
// itself. `run judge --context final` now runs both in-process before this
|
|
9
|
+
// briefing is assembled and inlines what they decided — whether a manual
|
|
10
|
+
// test plan is needed (with the reason when it is not) and the scenario
|
|
11
|
+
// checklist to answer. The agent still writes every artifact and reports
|
|
12
|
+
// the counts; nothing about what it is asked to do changed.
|
|
13
|
+
//
|
|
14
|
+
// `needsManualTestPlan` is `null` when the manifest's `handoff` flag is off
|
|
15
|
+
// (a conformance-only run): the manual-test-plan and code-explanation
|
|
16
|
+
// artifacts, and the learnings capture, belong to `handoff`, not to
|
|
17
|
+
// `conformance`, in the retired text (`workflows/ship.js:2295-2328`), so a
|
|
18
|
+
// `null` here skips that whole section rather than guessing which way a
|
|
19
|
+
// boolean should default.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @param {{id: string, artifact: string, line: number, title: string}} scenario
|
|
23
|
+
* @returns {string} one checklist line, matching `lib/conformance.mjs`'s own `formatChecklist`
|
|
24
|
+
*/
|
|
25
|
+
function renderScenario(scenario) {
|
|
26
|
+
const s = scenario || {}
|
|
27
|
+
const loc = s.artifact ? `${s.artifact}:${s.line}` : null
|
|
28
|
+
return loc ? ` ${s.id} ${s.title}\n ${loc}` : ` ${s.id} ${s.title}`
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Assemble the handoff briefing.
|
|
33
|
+
*
|
|
34
|
+
* @param {object} input
|
|
35
|
+
* @param {string} input.change
|
|
36
|
+
* @param {boolean|null} input.needsManualTestPlan from `interlock surface`; `null` when `handoff` is off
|
|
37
|
+
* @param {string} [input.testPlanReason] why no plan is needed, when `needsManualTestPlan` is `false`
|
|
38
|
+
* @param {Array<{id: string, artifact: string, line: number, title: string}>|null} input.conformance
|
|
39
|
+
* the scenario checklist from `interlock conformance`, or `null` when `conformance` is off
|
|
40
|
+
* @param {boolean} input.learnings whether to ask for the learnings-capture paragraph
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
export function assembleHandoffPrompt({ change, needsManualTestPlan, testPlanReason, conformance, learnings }) {
|
|
44
|
+
const wantsHandoffArtifacts = needsManualTestPlan !== null && needsManualTestPlan !== undefined
|
|
45
|
+
const scenarios = Array.isArray(conformance) ? conformance : null
|
|
46
|
+
|
|
47
|
+
const parts = [`Produce the handoff artifacts for change "${change}".\n`]
|
|
48
|
+
|
|
49
|
+
if (wantsHandoffArtifacts) {
|
|
50
|
+
parts.push(
|
|
51
|
+
needsManualTestPlan
|
|
52
|
+
? `Write openspec/changes/${change}/manual-test-plan.md covering every touched file, with ` +
|
|
53
|
+
`spec scenarios and tasks mapped to numbered cases.\n`
|
|
54
|
+
: `Skip the manual test plan and say why: ${testPlanReason || 'no UI-testable surface was touched'} ` +
|
|
55
|
+
`— a backend-only change does not get a UI test plan.\n`
|
|
56
|
+
)
|
|
57
|
+
parts.push(
|
|
58
|
+
`Then write openspec/changes/${change}/code-explanation.md as a commit teach-in: why each ` +
|
|
59
|
+
`file changed, what changed, the blast radius, and what would break if it were left out.\n`
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (scenarios !== null) {
|
|
64
|
+
const list = scenarios.length ? scenarios.map(renderScenario).join('\n') : ' (no scenarios listed)'
|
|
65
|
+
parts.push(
|
|
66
|
+
`Spec conformance — for each scenario below, read the implementation and answer whether the ` +
|
|
67
|
+
`described behaviour was actually built, citing file:line. Write the answers to ` +
|
|
68
|
+
`openspec/changes/${change}/conformance.md with one section per scenario id. A scenario you ` +
|
|
69
|
+
`cannot confirm is recorded as unconfirmed with what you looked at — never as satisfied, and ` +
|
|
70
|
+
`never omitted. If the checklist is empty, say so and move on.\n\n${list}\n\n` +
|
|
71
|
+
`This never halts the run: a prose scenario matched to code is a judgement, and a judgement ` +
|
|
72
|
+
`that stopped a ship run would be a gate built on a guess. Report it and let a person read it.\n`
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (learnings) {
|
|
77
|
+
parts.push(
|
|
78
|
+
`Finally, capture at most three learnings from fixes made during this run — recurring failure ` +
|
|
79
|
+
`modes under .claude/memory/failure-modes/, module coupling under .claude/memory/coupling/ — ` +
|
|
80
|
+
`each one small file, indexed in .claude/memory/MEMORY.md. Only genuinely recurring patterns; ` +
|
|
81
|
+
`write nothing if nothing recurred. This is silent.`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return parts.join('\n')
|
|
86
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// The text a wave implementer is handed is a contract, not a convenience.
|
|
2
|
+
//
|
|
3
|
+
// It used to live inline in `workflows/ship.js`, marked so a test could eval it
|
|
4
|
+
// out of the script's source, because the Workflow runtime rejects a script that
|
|
5
|
+
// loads modules at all. Now the CLI assembles every briefing and the script only
|
|
6
|
+
// carries the path and the hash of the file it wrote (design D2), so this text
|
|
7
|
+
// can live where every other piece of policy lives: in `lib/`, imported, read by
|
|
8
|
+
// one party.
|
|
9
|
+
//
|
|
10
|
+
// The fixtures under `test/fixtures/prompts/` are the contract. Changing the
|
|
11
|
+
// assembled text has to be a deliberate act that updates a file, not a side
|
|
12
|
+
// effect of editing nearby control flow.
|
|
13
|
+
|
|
14
|
+
export function assembleImplementerPrompt({ change, lane, task, previousHandoffs, isolateWaves, solo }) {
|
|
15
|
+
// A lane is the unit now; a bare `task` is still accepted and means a lane of
|
|
16
|
+
// one. That is not politeness to old callers — a one-task lane MUST assemble
|
|
17
|
+
// byte-identically to the pre-lane prompt, and sharing one code path is the
|
|
18
|
+
// only way that stays true as this text is edited.
|
|
19
|
+
const tasks = (Array.isArray(lane) && lane.length ? lane : [task]).filter(
|
|
20
|
+
t => t && typeof t === 'object'
|
|
21
|
+
)
|
|
22
|
+
const first = tasks[0] || { id: '(unknown)', description: '', tier: 1 }
|
|
23
|
+
// The lane runs on its hardest task's ladder: one agent must be briefed for
|
|
24
|
+
// everything it is about to do, and a tier is a floor, not an average.
|
|
25
|
+
//
|
|
26
|
+
// A solo lane is the whole change, so it is briefed at the full-read ladder
|
|
27
|
+
// however cheap its hardest task looks: the agent has no other wave to inherit
|
|
28
|
+
// context from and no sibling to compare notes with. This raises the BRIEFING
|
|
29
|
+
// only — the tasks keep the tiers the classifier gave them, because tier is
|
|
30
|
+
// what effort and the promotion report are read from and rewriting it here
|
|
31
|
+
// would misreport both (design D7).
|
|
32
|
+
const soloLane = solo === true
|
|
33
|
+
const laneTier =
|
|
34
|
+
tasks.reduce((m, t) => (Number.isInteger(t.tier) && t.tier > m ? t.tier : m), 0) || 1
|
|
35
|
+
const tier = soloLane ? Math.max(laneTier, 4) : laneTier
|
|
36
|
+
|
|
37
|
+
const packets = (Array.isArray(previousHandoffs) ? previousHandoffs : []).filter(
|
|
38
|
+
h => h && typeof h === 'object'
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
// Empty renders nothing at all, so a first wave's prompt is byte-identical to
|
|
42
|
+
// what it was before handoffs existed and its snapshot stays stable.
|
|
43
|
+
const previous = packets.length
|
|
44
|
+
? `\nPREVIOUS WAVE (schema-validated; do not re-derive from git):\n` +
|
|
45
|
+
packets
|
|
46
|
+
.map(
|
|
47
|
+
h =>
|
|
48
|
+
`- [${h.taskId} ${h.status}] ${h.summary}\n` +
|
|
49
|
+
` evidence: ${(Array.isArray(h.evidence) ? h.evidence : []).join(', ')}\n` +
|
|
50
|
+
` next: ${h.next}` +
|
|
51
|
+
(h.blocker ? `\n blocker: ${h.blocker}` : '')
|
|
52
|
+
)
|
|
53
|
+
.join('\n') +
|
|
54
|
+
`\n`
|
|
55
|
+
: ''
|
|
56
|
+
|
|
57
|
+
// A lane of one renders exactly the text it rendered before lanes existed —
|
|
58
|
+
// the single-task fixtures are the pin that keeps that honest.
|
|
59
|
+
const single = tasks.length === 1
|
|
60
|
+
|
|
61
|
+
const list =
|
|
62
|
+
tasks.map((t, i) => `TASK ${i + 1}/${tasks.length} — ${t.id}: ${t.description}`).join('\n') +
|
|
63
|
+
`\n\n`
|
|
64
|
+
|
|
65
|
+
// The multi-task heading used to say the tasks "edit the same files". That was
|
|
66
|
+
// true when a lane could only be a path-collision component; a cohesion lane
|
|
67
|
+
// packs path-DISJOINT tasks, so the sentence became a false statement handed
|
|
68
|
+
// to every implementer. What replaced it is true of every lane there is: one
|
|
69
|
+
// agent owns it, and nothing else is writing what it claims.
|
|
70
|
+
const heading = single
|
|
71
|
+
? `Implement exactly one task from OpenSpec change "${change}".\n\n` +
|
|
72
|
+
`TASK ${first.id}: ${first.description}\n\n`
|
|
73
|
+
: soloLane
|
|
74
|
+
? `Implement OpenSpec change "${change}" end to end — all ${tasks.length} of its tasks, IN ` +
|
|
75
|
+
`THIS ORDER. You are the only implementer on this change: you own every task listed below, ` +
|
|
76
|
+
`including its test tasks, and no other agent is touching this repository while you work.\n\n` +
|
|
77
|
+
list
|
|
78
|
+
: `Implement ${tasks.length} tasks from OpenSpec change "${change}", IN THIS ORDER. They are ` +
|
|
79
|
+
`one lane run by you alone — no other agent touches the files they claim while you ` +
|
|
80
|
+
`work.\n\n` +
|
|
81
|
+
list
|
|
82
|
+
|
|
83
|
+
const scope = single
|
|
84
|
+
? `- Implement ONLY this task. Do not modify files outside its scope.\n`
|
|
85
|
+
: `- Implement ONLY these tasks, in the order listed. Do not modify files outside their scope.\n` +
|
|
86
|
+
`- Finish a task before starting the next one. Do not skip ahead, and do not do a later ` +
|
|
87
|
+
`task's work under an earlier task's name.\n` +
|
|
88
|
+
`- STOP at the first task you cannot complete. Report it as failed and report every task ` +
|
|
89
|
+
`after it as not-attempted — do not continue past a failure.\n`
|
|
90
|
+
|
|
91
|
+
const handoff = single
|
|
92
|
+
? `\nHANDOFF — the next wave reads your packet instead of reconstructing your work ` +
|
|
93
|
+
`from git, so every result MUST carry one:\n` +
|
|
94
|
+
` { "schema": "interlock.wave-handoff/1", "taskId": "${first.id}", ` +
|
|
95
|
+
`"status": "ok" | "blocked" | "partial",\n` +
|
|
96
|
+
` "summary": "...", "evidence": ["path:12-40"], "next": "...", "blocker": null }\n`
|
|
97
|
+
: `\nHANDOFF — the next wave reads your packets instead of reconstructing your work ` +
|
|
98
|
+
`from git. Report an outcome for EVERY task you were given:\n` +
|
|
99
|
+
` { "tasks": [ { "id": "<task id>", "outcome": "ok" | "failed" | "not-attempted",\n` +
|
|
100
|
+
` "handoff": { "schema": "interlock.wave-handoff/1", "taskId": "<same id>", ` +
|
|
101
|
+
`"status": "ok" | "blocked" | "partial",\n` +
|
|
102
|
+
` "summary": "...", "evidence": ["path:12-40"], "next": "...", "blocker": null } } ] }\n` +
|
|
103
|
+
`- One packet per task you ATTEMPTED, keyed by its own id. One packet cannot stand in for ` +
|
|
104
|
+
`several tasks.\n` +
|
|
105
|
+
`- A "not-attempted" task carries no packet. That absence is expected, not an error.\n` +
|
|
106
|
+
`- A result that omits a task you were given fails every task in this lane, so report all ` +
|
|
107
|
+
`${tasks.length}.\n` +
|
|
108
|
+
`- Do not pass a packet between your own tasks — you already know what you just did.\n`
|
|
109
|
+
|
|
110
|
+
// Rendered only under --isolate-waves. Absent, this function must produce
|
|
111
|
+
// the exact prompt it produced before worktree isolation existed — that
|
|
112
|
+
// byte-identity is what the implementer-prompt fixtures pin.
|
|
113
|
+
const isolation = isolateWaves
|
|
114
|
+
? `\nISOLATION — you are running in your own git worktree, not the shared tree. Before you ` +
|
|
115
|
+
`report your result, run \`pwd\` and report its output as "worktreePath" in your result, ` +
|
|
116
|
+
`exactly as printed. The orchestrator has no other way to find your worktree, and folds your ` +
|
|
117
|
+
`writes back into the shared tree using that path, so it must be the real one, not a guess.\n`
|
|
118
|
+
: ''
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
heading +
|
|
122
|
+
`CONTEXT — read only what your tier needs:\n` +
|
|
123
|
+
` tier 1: the task description alone\n` +
|
|
124
|
+
` tier 2+: the relevant section of openspec/changes/${change}/design.md\n` +
|
|
125
|
+
` tier 3+: the relevant file under openspec/changes/${change}/specs/\n` +
|
|
126
|
+
` tier 4+: design.md and the specs in full\n` +
|
|
127
|
+
`Your tier is ${tier}.\n\n` +
|
|
128
|
+
`RULES:\n` +
|
|
129
|
+
scope +
|
|
130
|
+
`- Do not fix unrelated problems you notice; report them instead.\n` +
|
|
131
|
+
`- Run typecheck and lint on what you changed.\n` +
|
|
132
|
+
`- Do not commit, and do not edit tasks.md — the orchestrator owns both.\n` +
|
|
133
|
+
`- If .claude/graph/graph.json exists, interlock-graph query / consumers before grep.\n` +
|
|
134
|
+
`- Locate (graph or grep) then Read spans. Do not re-read a file unless it changed.\n` +
|
|
135
|
+
`- Return the schema only. No narrative.\n` +
|
|
136
|
+
(tier <= 2
|
|
137
|
+
? `- If your tier is 1 or 2: after typecheck/lint pass, stop. Do not refactor or polish.\n`
|
|
138
|
+
: '') +
|
|
139
|
+
previous +
|
|
140
|
+
isolation +
|
|
141
|
+
handoff +
|
|
142
|
+
`- status "ok" means blocker is null; "blocked" and "partial" need a non-empty blocker.\n` +
|
|
143
|
+
`- evidence is at most 8 locators (path, path:line, path:start-end) — never file bodies.\n` +
|
|
144
|
+
`- Keep it terse. A packet over the character cap \`interlock limits\` publishes fails the ` +
|
|
145
|
+
`task, and it is never truncated for you.\n\n` +
|
|
146
|
+
(single
|
|
147
|
+
? `Report ok:false if you could not complete the task, with what blocked you.`
|
|
148
|
+
: `Report outcome "failed" for the task that blocked you, with what blocked you.`)
|
|
149
|
+
)
|
|
150
|
+
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// The registry of every briefing the run program can assemble.
|
|
2
|
+
//
|
|
3
|
+
// Coverage of assembled prompts is ENUMERATED, never sampled: the prompt-
|
|
4
|
+
// integrity suite iterates this table, and a module under `lib/prompts/` that
|
|
5
|
+
// exports an assembler and is not registered here fails that suite by name. The
|
|
6
|
+
// alternative — a suite that checks the prompts it happens to reach — is how the
|
|
7
|
+
// classifier's tier ladder stayed corrupted while every test passed.
|
|
8
|
+
//
|
|
9
|
+
// `inputs` are the sample inputs the suite assembles with. They are not
|
|
10
|
+
// fixtures: a fixture pins bytes (that is `test/fixtures/prompts/`), while these
|
|
11
|
+
// only have to be well-formed enough to exercise every branch of the assembler.
|
|
12
|
+
|
|
13
|
+
import { assembleImplementerPrompt } from './implementer.mjs'
|
|
14
|
+
import { assemblePlannerPrompt } from './planner.mjs'
|
|
15
|
+
import { assembleVerifyPrompt } from './verify.mjs'
|
|
16
|
+
import { assembleReplanPrompt } from './replan.mjs'
|
|
17
|
+
import { assembleCommitPrompt } from './commit.mjs'
|
|
18
|
+
import { assembleReviewPrompt } from './review.mjs'
|
|
19
|
+
import { assembleRemediatePrompt, assembleVerdictPrompt } from './remediate.mjs'
|
|
20
|
+
import { assembleHandoffPrompt } from './handoff.mjs'
|
|
21
|
+
import { publishStageLine } from './stage.mjs'
|
|
22
|
+
|
|
23
|
+
export { assembleImplementerPrompt } from './implementer.mjs'
|
|
24
|
+
export { assemblePlannerPrompt } from './planner.mjs'
|
|
25
|
+
export { assembleVerifyPrompt } from './verify.mjs'
|
|
26
|
+
export { assembleReplanPrompt } from './replan.mjs'
|
|
27
|
+
export { assembleCommitPrompt } from './commit.mjs'
|
|
28
|
+
export { assembleReviewPrompt } from './review.mjs'
|
|
29
|
+
export { assembleRemediatePrompt, assembleVerdictPrompt } from './remediate.mjs'
|
|
30
|
+
export { assembleHandoffPrompt } from './handoff.mjs'
|
|
31
|
+
export { selectDimensions, ALWAYS_ON_DIMENSIONS } from './dimensions.mjs'
|
|
32
|
+
export { publishStageLine } from './stage.mjs'
|
|
33
|
+
export * from './schemas.mjs'
|
|
34
|
+
|
|
35
|
+
const CHANGE = 'demo-change'
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* name → { module, assemble, inputs }. One entry per assembler; `inputs` is a
|
|
39
|
+
* list, so an assembler with a meaningfully different shape per branch (a lane
|
|
40
|
+
* of one vs a lane of several; inter-wave vs final verification) registers each.
|
|
41
|
+
*/
|
|
42
|
+
export const PROMPTS = Object.freeze({
|
|
43
|
+
implementer: {
|
|
44
|
+
module: 'implementer.mjs',
|
|
45
|
+
assemble: assembleImplementerPrompt,
|
|
46
|
+
inputs: [
|
|
47
|
+
{
|
|
48
|
+
change: CHANGE,
|
|
49
|
+
task: { id: '1.1', description: 'sessions table', tier: 3, model: 'sonnet' },
|
|
50
|
+
previousHandoffs: []
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
change: CHANGE,
|
|
54
|
+
lane: [
|
|
55
|
+
{ id: '1.1', description: 'sessions table', tier: 2, model: 'sonnet' },
|
|
56
|
+
{ id: '1.2', description: 'session index', tier: 4, model: 'sonnet' }
|
|
57
|
+
],
|
|
58
|
+
previousHandoffs: [
|
|
59
|
+
{
|
|
60
|
+
taskId: '0.1',
|
|
61
|
+
status: 'ok',
|
|
62
|
+
summary: 'schema landed',
|
|
63
|
+
evidence: ['lib/db.mjs:12-40'],
|
|
64
|
+
next: 'wire the reader',
|
|
65
|
+
blocker: null
|
|
66
|
+
}
|
|
67
|
+
],
|
|
68
|
+
isolateWaves: true
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
change: CHANGE,
|
|
72
|
+
lane: [{ id: '1.1', description: 'the whole change', tier: 2, model: 'sonnet' }],
|
|
73
|
+
solo: true,
|
|
74
|
+
previousHandoffs: []
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
planner: {
|
|
79
|
+
module: 'planner.mjs',
|
|
80
|
+
assemble: assemblePlannerPrompt,
|
|
81
|
+
inputs: [{ change: CHANGE, classifiedPath: '.claude/ship/classified.json' }]
|
|
82
|
+
},
|
|
83
|
+
verify: {
|
|
84
|
+
module: 'verify.mjs',
|
|
85
|
+
assemble: assembleVerifyPrompt,
|
|
86
|
+
inputs: [
|
|
87
|
+
{
|
|
88
|
+
change: CHANGE,
|
|
89
|
+
context: 'inter-wave',
|
|
90
|
+
steps: [{ kind: 'unit', command: 'npm test' }],
|
|
91
|
+
runId: 'run-1',
|
|
92
|
+
statePath: '.claude/ship/state.json'
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
change: CHANGE,
|
|
96
|
+
context: 'final',
|
|
97
|
+
steps: [
|
|
98
|
+
{ kind: 'unit', command: 'npm test' },
|
|
99
|
+
{ kind: 'e2e', command: 'npm run e2e' }
|
|
100
|
+
],
|
|
101
|
+
runId: null,
|
|
102
|
+
statePath: '.claude/ship/state.json'
|
|
103
|
+
},
|
|
104
|
+
// The fix branch: a retry carries what the previous attempt reported, and
|
|
105
|
+
// the integrity sweep must see that branch rendered too.
|
|
106
|
+
{
|
|
107
|
+
change: CHANGE,
|
|
108
|
+
context: 'inter-wave',
|
|
109
|
+
steps: [{ kind: 'unit', command: 'npm test' }],
|
|
110
|
+
runId: 'run-1',
|
|
111
|
+
statePath: '.claude/ship/state.json',
|
|
112
|
+
fixAttempt: 1,
|
|
113
|
+
fixAttemptsRemaining: 1,
|
|
114
|
+
errors: ['unit suite is red (1 failing, 1 root-cause cluster(s))']
|
|
115
|
+
}
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
replan: {
|
|
119
|
+
module: 'replan.mjs',
|
|
120
|
+
assemble: assembleReplanPrompt,
|
|
121
|
+
inputs: [{ change: CHANGE, replanPath: '.claude/ship/replan.json' }]
|
|
122
|
+
},
|
|
123
|
+
commit: {
|
|
124
|
+
module: 'commit.mjs',
|
|
125
|
+
assemble: assembleCommitPrompt,
|
|
126
|
+
inputs: [{ change: CHANGE, stageIndex: 7 }]
|
|
127
|
+
},
|
|
128
|
+
review: {
|
|
129
|
+
module: 'review.mjs',
|
|
130
|
+
assemble: assembleReviewPrompt,
|
|
131
|
+
inputs: [
|
|
132
|
+
{
|
|
133
|
+
change: CHANGE,
|
|
134
|
+
dimensions: [
|
|
135
|
+
{ name: 'language', rubric: 'Check idiomatic usage, type safety and error handling.' },
|
|
136
|
+
{ name: 'devops', rubric: null }
|
|
137
|
+
],
|
|
138
|
+
policyProse: 'Blockers must cite evidence; treat purely cosmetic nits as suggestions, not warnings.',
|
|
139
|
+
findingsPath: '.claude/ship/demo-change/review/findings.json',
|
|
140
|
+
verdictsPath: '.claude/ship/demo-change/review/verdicts.json',
|
|
141
|
+
stageLine: publishStageLine('review', CHANGE, 5)
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
change: CHANGE,
|
|
145
|
+
dimensions: [{ name: 'qa', rubric: 'Are edge cases and error paths covered by a test?' }],
|
|
146
|
+
policyProse: '',
|
|
147
|
+
findingsPath: '.claude/ship/demo-change/review/findings.json',
|
|
148
|
+
verdictsPath: '.claude/ship/demo-change/review/verdicts.json',
|
|
149
|
+
stageLine: publishStageLine('review', CHANGE, 5)
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
remediate: {
|
|
154
|
+
module: 'remediate.mjs',
|
|
155
|
+
assemble: assembleRemediatePrompt,
|
|
156
|
+
inputs: [
|
|
157
|
+
{
|
|
158
|
+
change: CHANGE,
|
|
159
|
+
round: 1,
|
|
160
|
+
plan: {
|
|
161
|
+
fix: {
|
|
162
|
+
byFile: [
|
|
163
|
+
{
|
|
164
|
+
file: 'lib/db.mjs',
|
|
165
|
+
findings: [
|
|
166
|
+
{
|
|
167
|
+
severity: 'blocker',
|
|
168
|
+
file: 'lib/db.mjs',
|
|
169
|
+
line: 42,
|
|
170
|
+
title: 'missing null check',
|
|
171
|
+
description: 'a null session can reach the query builder unchecked',
|
|
172
|
+
suggestion: 'guard before the query is built'
|
|
173
|
+
}
|
|
174
|
+
]
|
|
175
|
+
}
|
|
176
|
+
],
|
|
177
|
+
unscoped: [
|
|
178
|
+
{
|
|
179
|
+
severity: 'warning',
|
|
180
|
+
file: null,
|
|
181
|
+
title: 'naming drift',
|
|
182
|
+
description: 'the same concept is named two different ways across the diff'
|
|
183
|
+
}
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
reReviewDimensions: ['language']
|
|
187
|
+
},
|
|
188
|
+
dimensions: [{ name: 'language', rubric: 'Check idiomatic usage, type safety and error handling.' }],
|
|
189
|
+
policyProse: 'Blockers must cite evidence; treat purely cosmetic nits as suggestions, not warnings.',
|
|
190
|
+
stageLine: publishStageLine('remediation', CHANGE, 6)
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
verdict: {
|
|
195
|
+
module: 'remediate.mjs',
|
|
196
|
+
assemble: assembleVerdictPrompt,
|
|
197
|
+
inputs: [{ change: CHANGE, round: 3 }]
|
|
198
|
+
},
|
|
199
|
+
handoff: {
|
|
200
|
+
module: 'handoff.mjs',
|
|
201
|
+
assemble: assembleHandoffPrompt,
|
|
202
|
+
inputs: [
|
|
203
|
+
{
|
|
204
|
+
change: CHANGE,
|
|
205
|
+
needsManualTestPlan: true,
|
|
206
|
+
testPlanReason: undefined,
|
|
207
|
+
conformance: [
|
|
208
|
+
{ id: 'S1', artifact: 'openspec/specs/demo-change/spec.md', line: 12, title: 'rejects an expired session' }
|
|
209
|
+
],
|
|
210
|
+
learnings: true
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
change: CHANGE,
|
|
214
|
+
needsManualTestPlan: false,
|
|
215
|
+
testPlanReason: 'a backend-only change touches no UI surface',
|
|
216
|
+
conformance: null,
|
|
217
|
+
learnings: false
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
change: CHANGE,
|
|
221
|
+
needsManualTestPlan: null,
|
|
222
|
+
testPlanReason: undefined,
|
|
223
|
+
conformance: [],
|
|
224
|
+
learnings: false
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
},
|
|
228
|
+
stage: {
|
|
229
|
+
module: 'stage.mjs',
|
|
230
|
+
assemble: ({ stage, change, index }) => publishStageLine(stage, change, index),
|
|
231
|
+
inputs: [
|
|
232
|
+
{ stage: 'implement', change: CHANGE, index: 1 },
|
|
233
|
+
{ stage: 'fix-tests', change: CHANGE, index: 2 },
|
|
234
|
+
{ stage: 'commit', change: CHANGE, index: 3 }
|
|
235
|
+
]
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
/** Every registered assembler's module filename, for the unregistered-module check. */
|
|
240
|
+
export const REGISTERED_MODULES = Object.freeze(
|
|
241
|
+
Object.values(PROMPTS).map(entry => entry.module)
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Modules under `lib/prompts/` that hold no assembler and are not expected in
|
|
246
|
+
* the registry. `dimensions.mjs` selects which review dimensions run — a
|
|
247
|
+
* `{ dimensions, reasons }` decision, never a string handed to an agent — so
|
|
248
|
+
* it has no briefing to register or check for coercion artifacts.
|
|
249
|
+
*/
|
|
250
|
+
export const NON_ASSEMBLER_MODULES = Object.freeze(['index.mjs', 'schemas.mjs', 'dimensions.mjs'])
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// The wave-classification briefing.
|
|
2
|
+
//
|
|
3
|
+
// This text lived in both drivers, worded differently in each — the one place
|
|
4
|
+
// classifier policy demonstrably drifted, since the same task could classify
|
|
5
|
+
// differently depending on which host ran it. A cross-driver parity test
|
|
6
|
+
// compared the extracted tier boundaries after the fact. There is nothing left
|
|
7
|
+
// to compare: the CLI assembles this once and hands it to whichever host is
|
|
8
|
+
// running (design D7).
|
|
9
|
+
//
|
|
10
|
+
// The form is the ACP driver's — the model writes `classified.json` and reports
|
|
11
|
+
// whether it did. It never runs the interlock CLI; `interlock run classified`
|
|
12
|
+
// does coverage, waves, the plan file and `wave-state create` afterwards. The
|
|
13
|
+
// rules themselves are the Workflow driver's fuller wording, verbatim.
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {{ change: string, classifiedPath: string }} input
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
export function assemblePlannerPrompt({ change, classifiedPath }) {
|
|
20
|
+
return (
|
|
21
|
+
`For OpenSpec change "${change}": read proposal.md, design.md, tasks.md and specs/**/*.md in full — ` +
|
|
22
|
+
`this is the artifact leash and is not subject to bounded retrieval.\n\n` +
|
|
23
|
+
`Classify every UNCHECKED task with: id, group (wave number), description, tier 1-5, model, ` +
|
|
24
|
+
`isTestTask, paths, and dependsOn.\n\n` +
|
|
25
|
+
`GROUPING — four rules, in order:\n` +
|
|
26
|
+
` 1. Default group to the numbered tasks.md section (1.x → group 1, 2.x → group 2).\n` +
|
|
27
|
+
` 2. A shared file is NOT a reason for a new group. Put the predicted edit paths in \`paths\` ` +
|
|
28
|
+
`and let the planner fold colliding tasks into one LANE of the SAME wave — an ordered task ` +
|
|
29
|
+
`list run by a single agent. Inventing a new group to avoid a file clash costs a verification ` +
|
|
30
|
+
`cycle; naming the path costs nothing and saves a spawn.\n` +
|
|
31
|
+
` 3. A dependency on a task editing a DIFFERENT file is a reason for a \`dependsOn\` EDGE, ` +
|
|
32
|
+
`not a reason to increment \`group\`. Incrementing \`group\` to order one cross-file dependency ` +
|
|
33
|
+
`serializes every task in the new group that is independent of it; an edge orders only the ` +
|
|
34
|
+
`dependent task, so its independent siblings keep sharing a batch. Prefer the edge.\n` +
|
|
35
|
+
` 4. Only add a group for a LATER NUMBERED SECTION that needs an earlier section's output ` +
|
|
36
|
+
`to already exist. The next sequential slice of the same file is NOT a new group — it stays ` +
|
|
37
|
+
`in that file's section group and becomes a later batch. Groups run sequentially; tasks in a ` +
|
|
38
|
+
`group are otherwise independent.\n\n` +
|
|
39
|
+
`\`paths\` is your best prediction of the repo-relative files the task will edit. Predict what you ` +
|
|
40
|
+
`can and OMIT the field when you genuinely cannot — an invented path serializes a batch for ` +
|
|
41
|
+
`nothing, while an omitted one only leaves things as they were.\n\n` +
|
|
42
|
+
`\`dependsOn\` is the array of ids of EARLIER tasks whose output this task needs — the file it ` +
|
|
43
|
+
`imports, the type it consumes, the helper it calls. Populate it whenever that is true, and ` +
|
|
44
|
+
`omit it otherwise. Every id must name a task in this same classification, must not point at ` +
|
|
45
|
+
`a later numbered section, and must not point at a test task; the edges must not form a cycle. ` +
|
|
46
|
+
`A dangling id, a backward edge or a cycle FAILS the plan rather than being dropped, so declare ` +
|
|
47
|
+
`only dependencies you can point at.\n\n` +
|
|
48
|
+
`MODE — beside the task array, emit a top-level "recommendedMode" of "solo" or "waves" and a ` +
|
|
49
|
+
`one-line "modeReason" saying why. Solo means one agent implements this whole change by ` +
|
|
50
|
+
`itself, task by task in the planned order, with no parallel waves and no isolation between ` +
|
|
51
|
+
`tasks; waves means the ordinary parallel plan. Recommend solo when the change is small and ` +
|
|
52
|
+
`tightly coupled enough that one agent holding all of it beats several agents each holding a ` +
|
|
53
|
+
`slice, and waves otherwise. Judge the shape only: the planner enforces the published envelope ` +
|
|
54
|
+
`on how large a change may ship solo and will refuse a recommendation above it, so do not ` +
|
|
55
|
+
`reason about the size bound and do not state one.\n\n` +
|
|
56
|
+
// A unary `+` once sat before this operand and coerced it to NaN, so tiers
|
|
57
|
+
// 1-3 and the haiku routing rule never reached the classifier while the
|
|
58
|
+
// sentence stayed intact in the source bytes. test/spine/prompts.test.mjs
|
|
59
|
+
// asserts the ASSEMBLED string for exactly that reason.
|
|
60
|
+
`Tier 1 trivial one-file edit → haiku. Tier 2 single-concern change. Tier 3 new logic in one ` +
|
|
61
|
+
`domain. Tier 4 cross-file work following existing patterns — a mechanical refactor across many ` +
|
|
62
|
+
`files is tier 4 sonnet, because breadth is not depth. Tier 5 only for genuinely novel ` +
|
|
63
|
+
`architecture, and only tier 5 may be opus. When unsure, sonnet.\n\n` +
|
|
64
|
+
`Write the classification as { "tasks": [...] } to ${classifiedPath}. ` +
|
|
65
|
+
`Do NOT run the interlock CLI yourself — the orchestrator runs the planner, the coverage check ` +
|
|
66
|
+
`and the state machine. Report only whether the file was written and how many tasks it holds.\n\n` +
|
|
67
|
+
`The planner is authoritative: it clamps over-eager opus, orders the waves, defers test tasks ` +
|
|
68
|
+
`and splits wide waves into batches. Do not re-derive or override any of it.`
|
|
69
|
+
)
|
|
70
|
+
}
|