karajan-code 2.7.4 → 2.8.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/README.md +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +85 -31
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
package/src/prompts/coder.js
CHANGED
|
@@ -33,8 +33,155 @@ const SERENA_INSTRUCTIONS = [
|
|
|
33
33
|
"Fall back to reading files only when Serena tools are not sufficient."
|
|
34
34
|
].join("\n");
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
/**
|
|
37
|
+
* Render structured acceptance_tests (v2.7.5) as a Markdown section the
|
|
38
|
+
* coder reads as its contract. Legacy plain-string entries and
|
|
39
|
+
* structured { type, content, file? } objects are both accepted.
|
|
40
|
+
*
|
|
41
|
+
* The layout is intentional:
|
|
42
|
+
* - Gherkin blocks preserve their newlines so the coder sees the
|
|
43
|
+
* Given/When/Then scenarios as-is.
|
|
44
|
+
* - Shell blocks are fenced so the coder can copy-paste to verify.
|
|
45
|
+
* - The header says "MUST pass" because this is the gate the tester
|
|
46
|
+
* role runs (phase 3) and refuses to approve without.
|
|
47
|
+
*
|
|
48
|
+
* @param {Array} tests
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
function renderAcceptanceTestsSection(tests) {
|
|
52
|
+
if (!Array.isArray(tests) || tests.length === 0) return "";
|
|
53
|
+
const lines = [
|
|
54
|
+
"## Acceptance Tests — MUST pass before the HU can be approved",
|
|
55
|
+
"",
|
|
56
|
+
"These are the concrete contract for this task. The tester stage will execute them as the pass/fail gate — if any one fails, the HU is rejected. Write your implementation so each test passes.",
|
|
57
|
+
"",
|
|
58
|
+
];
|
|
59
|
+
tests.forEach((entry, i) => {
|
|
60
|
+
// Normalise: accept legacy strings and structured objects.
|
|
61
|
+
let type = "shell";
|
|
62
|
+
let content;
|
|
63
|
+
let file = null;
|
|
64
|
+
if (typeof entry === "string") {
|
|
65
|
+
content = entry;
|
|
66
|
+
} else if (entry && typeof entry === "object") {
|
|
67
|
+
type = entry.type === "gherkin" ? "gherkin" : "shell";
|
|
68
|
+
content = typeof entry.content === "string" ? entry.content : JSON.stringify(entry);
|
|
69
|
+
file = typeof entry.file === "string" && entry.file.trim() ? entry.file.trim() : null;
|
|
70
|
+
} else {
|
|
71
|
+
content = String(entry ?? "");
|
|
72
|
+
}
|
|
73
|
+
const heading = file
|
|
74
|
+
? `### Test ${i + 1} · \`${type}\` · target: \`${file}\``
|
|
75
|
+
: `### Test ${i + 1} · \`${type}\``;
|
|
76
|
+
lines.push(heading);
|
|
77
|
+
if (type === "gherkin") {
|
|
78
|
+
lines.push("```gherkin", content, "```", "");
|
|
79
|
+
} else {
|
|
80
|
+
lines.push("```bash", content, "```", "");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
lines.push(
|
|
84
|
+
"If a test is ambiguous, implement what makes it pass AND add the clarifying assertion to the PR description — do NOT silently rewrite the test."
|
|
85
|
+
);
|
|
86
|
+
return lines.join("\n");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Render the active ADRs (PR F) as a "MUST respect" preamble. Each
|
|
91
|
+
* ADR is shown verbatim — they're already in Nygard format
|
|
92
|
+
* (Context · Decision · Consequences) and the coder benefits from
|
|
93
|
+
* the full text, not a summary. Keeps the prompt size sane: caps at
|
|
94
|
+
* 8 ADRs (the planner shouldn't realistically emit more for one
|
|
95
|
+
* plan, but bound it just in case).
|
|
96
|
+
*/
|
|
97
|
+
function renderAdrSection(adrs) {
|
|
98
|
+
if (!Array.isArray(adrs) || adrs.length === 0) return "";
|
|
99
|
+
const capped = adrs.slice(0, 8);
|
|
100
|
+
const lines = [
|
|
101
|
+
"## Architecture Decision Records (active)",
|
|
102
|
+
"",
|
|
103
|
+
"These ADRs were committed by the architect role. Your implementation MUST respect them. If the SPEC asks for something incompatible with one of these, surface the conflict in your PR description rather than silently overriding the ADR.",
|
|
104
|
+
"",
|
|
105
|
+
];
|
|
106
|
+
for (const adr of capped) {
|
|
107
|
+
lines.push(adr.markdown.trim(), "", "---", "");
|
|
108
|
+
}
|
|
109
|
+
if (adrs.length > capped.length) {
|
|
110
|
+
lines.push(`_(…${adrs.length - capped.length} more ADRs not shown — see ~/.kj/plans/<slug>/<planId>/adrs/.)_`, "");
|
|
111
|
+
}
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* SPEC-section pointer (PR F). One short line. Lets the coder put
|
|
117
|
+
* "implements §X.Y of SPEC.md" in the PR description for traceability.
|
|
118
|
+
*/
|
|
119
|
+
function renderSpecSectionLine(specSection) {
|
|
120
|
+
if (!specSection) return "";
|
|
121
|
+
return [
|
|
122
|
+
"## SPEC reference",
|
|
123
|
+
"",
|
|
124
|
+
`This HU implements **§${specSection}** of the source SPEC. Cite this section in your PR description so the reviewer can trace coverage end-to-end.`,
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Plan-reviewer findings relevant to this HU (PR F). The reviewer's
|
|
130
|
+
* output is plan-wide; we filter to entries that mention the
|
|
131
|
+
* current HU id so the coder gets only what's actionable for them.
|
|
132
|
+
*/
|
|
133
|
+
function renderReviewerFindingsSection(findings, huId) {
|
|
134
|
+
if (!findings || !huId) return "";
|
|
135
|
+
const items = [];
|
|
136
|
+
for (const md of findings.missing_dependencies || []) {
|
|
137
|
+
if (md.from === huId) items.push(`- This HU should depend on **${md.on}** — ${md.rationale}`);
|
|
138
|
+
else if (md.on === huId) items.push(`- HU **${md.from}** should depend on this one — ${md.rationale}`);
|
|
139
|
+
}
|
|
140
|
+
for (const ov of findings.scope_overlaps || []) {
|
|
141
|
+
if (Array.isArray(ov.between) && ov.between.includes(huId)) {
|
|
142
|
+
const others = ov.between.filter((id) => id !== huId);
|
|
143
|
+
items.push(`- Scope overlap with **${others.join(", ")}** — ${ov.rationale}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const oi of findings.order_issues || []) {
|
|
147
|
+
if (Array.isArray(oi.hus) && oi.hus.includes(huId)) {
|
|
148
|
+
items.push(`- Order issue (${oi.issue}): ${oi.rationale}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (items.length === 0) return "";
|
|
152
|
+
return [
|
|
153
|
+
"## Reviewer findings about this HU",
|
|
154
|
+
"",
|
|
155
|
+
"The plan reviewer flagged the following — consider them while implementing:",
|
|
156
|
+
"",
|
|
157
|
+
...items,
|
|
158
|
+
].join("\n");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSummary = null, coderRules = null, methodology = "tdd", serenaEnabled = false, rtkAvailable = false, deferredContext = null, productContext = null, domainContext = null, plan = null, projectDir = null, language = "en", provider = null, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null }) {
|
|
37
162
|
const langInstruction = getLanguageInstruction(language);
|
|
163
|
+
// KJC-BUG-0032 (PR-I): hard rule about file paths. Real bug: a HU
|
|
164
|
+
// titled "Initialize project skeleton: create assistant/ directory…"
|
|
165
|
+
// had Claude run `cd /home/manu/assistant && pnpm init …` and 36 MB
|
|
166
|
+
// of code landed in $HOME instead of inside projectDir. Reinforce
|
|
167
|
+
// the constraint at the prompt level (soft fix) AND enforce it
|
|
168
|
+
// post-coder via fs-leak-detector.js (hard fix).
|
|
169
|
+
const projectDirRule = projectDir
|
|
170
|
+
? [
|
|
171
|
+
"## CRITICAL — Project directory boundary",
|
|
172
|
+
`The project root is **${projectDir}**. EVERY file you create, write, edit, mkdir or cd to MUST stay inside this directory.`,
|
|
173
|
+
"",
|
|
174
|
+
"Forbidden, regardless of what the task says:",
|
|
175
|
+
"- Absolute paths outside the project root (e.g. `/home/USER/something`, `/tmp/x`, `/etc/y`).",
|
|
176
|
+
"- `cd` to any directory outside the project root inside Bash commands.",
|
|
177
|
+
"- Creating sibling top-level directories under `$HOME`.",
|
|
178
|
+
"",
|
|
179
|
+
"If the task or HU title mentions a directory like `assistant/` or `app/`, treat it as RELATIVE to the project root. NEVER as an absolute path under $HOME.",
|
|
180
|
+
"",
|
|
181
|
+
"Karajan runs a deterministic post-coder guard that detects writes outside the project root and ABORTS the HU. If you ignore this rule the run will fail and your work will be discarded."
|
|
182
|
+
].join("\n")
|
|
183
|
+
: null;
|
|
184
|
+
|
|
38
185
|
const sections = [
|
|
39
186
|
serenaEnabled ? SUBAGENT_PREAMBLE_SERENA : SUBAGENT_PREAMBLE,
|
|
40
187
|
...(langInstruction ? [langInstruction] : []),
|
|
@@ -47,6 +194,7 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
47
194
|
"No console.log — use a structured logger. No 'any' types — use JSDoc annotations.",
|
|
48
195
|
SUBPROCESS_CONSTRAINTS
|
|
49
196
|
];
|
|
197
|
+
if (projectDirRule) sections.push(projectDirRule);
|
|
50
198
|
|
|
51
199
|
if (serenaEnabled) {
|
|
52
200
|
sections.push(SERENA_INSTRUCTIONS);
|
|
@@ -69,6 +217,29 @@ export async function buildCoderPrompt({ task, reviewerFeedback = null, sonarSum
|
|
|
69
217
|
sections.push(`## Implementation Plan (from planner)\nFollow these steps:\n${plan}`);
|
|
70
218
|
}
|
|
71
219
|
|
|
220
|
+
// PR F context injections — order matters:
|
|
221
|
+
// 1. ADRs first (they constrain HOW you implement),
|
|
222
|
+
// 2. SPEC reference (what section we're satisfying),
|
|
223
|
+
// 3. Reviewer findings about THIS HU (warnings the coder needs
|
|
224
|
+
// to consider while writing code),
|
|
225
|
+
// 4. Acceptance tests (the gate the implementation must pass).
|
|
226
|
+
// All four sit BEFORE coder rules / TDD policy so the policy
|
|
227
|
+
// frames them, not the other way around.
|
|
228
|
+
const adrSection = renderAdrSection(adrs);
|
|
229
|
+
if (adrSection) sections.push(adrSection);
|
|
230
|
+
|
|
231
|
+
const specSectionLine = renderSpecSectionLine(specSection);
|
|
232
|
+
if (specSectionLine) sections.push(specSectionLine);
|
|
233
|
+
|
|
234
|
+
const reviewerSection = renderReviewerFindingsSection(reviewerFindings, huId);
|
|
235
|
+
if (reviewerSection) sections.push(reviewerSection);
|
|
236
|
+
|
|
237
|
+
// Tests-first contract: the gate the implementation must pass.
|
|
238
|
+
const testsSection = renderAcceptanceTestsSection(acceptanceTests);
|
|
239
|
+
if (testsSection) {
|
|
240
|
+
sections.push(testsSection);
|
|
241
|
+
}
|
|
242
|
+
|
|
72
243
|
if (coderRules) {
|
|
73
244
|
sections.push(`Coder rules (MUST follow):\n${coderRules}`);
|
|
74
245
|
}
|
package/src/prompts/planner.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { detectSpecSections } from "../plan/spec-detector.js";
|
|
2
|
+
|
|
1
3
|
function extractStepText(line) {
|
|
2
4
|
const numberedStep = /^\d+[).:-]\s*(.+)$/.exec(line);
|
|
3
5
|
if (numberedStep) return numberedStep[1].trim();
|
|
@@ -64,7 +66,68 @@ function formatArchitectContext(architectContext) {
|
|
|
64
66
|
return lines.length > 1 ? lines.join("\n") : null;
|
|
65
67
|
}
|
|
66
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Render the SPEC-section guidance block for the planner.
|
|
71
|
+
*
|
|
72
|
+
* Two modes:
|
|
73
|
+
* • the task contains numbered headings (`## 1.`, `### 2.1`, `§5`) →
|
|
74
|
+
* spec_section becomes a HARD requirement and the detected
|
|
75
|
+
* sections are echoed back as a checklist. LLMs (Sonnet, Codex)
|
|
76
|
+
* consistently skipped the soft "preferred" wording, so the only
|
|
77
|
+
* reliable lever is to (a) escalate the rule to MUST and (b) hand
|
|
78
|
+
* them the literal list of section refs they're allowed to cite.
|
|
79
|
+
* • no numbered headings detected → spec_section stays optional
|
|
80
|
+
* and the prompt simply explains the field for completeness.
|
|
81
|
+
*/
|
|
82
|
+
function renderSpecSectionRules(specInfo) {
|
|
83
|
+
if (!specInfo.hasSpec) {
|
|
84
|
+
return [
|
|
85
|
+
"### `spec_section` (optional)",
|
|
86
|
+
"The task does not look like a structured SPEC, so this field may be `null`. If you do cite a section, use a short ref like `\"5.3\"` or `\"§5 Initial Scope\"`.",
|
|
87
|
+
].join("\n");
|
|
88
|
+
}
|
|
89
|
+
const checklist = specInfo.sectionList
|
|
90
|
+
.map(({ section, title }) => ` - "${section}" — ${title}`)
|
|
91
|
+
.join("\n");
|
|
92
|
+
return [
|
|
93
|
+
"### `spec_section` (REQUIRED — the task is a structured SPEC)",
|
|
94
|
+
`Detected ${specInfo.sections.length} numbered SPEC heading(s) in the task. Every \`step\` MUST set \`spec_section\` to one of the refs below — string, exactly as listed. NEVER \`null\`, NEVER omitted, NEVER a free-form paraphrase.`,
|
|
95
|
+
"",
|
|
96
|
+
"Allowed values:",
|
|
97
|
+
checklist,
|
|
98
|
+
"",
|
|
99
|
+
"If a step covers multiple sections, pick the most specific one (e.g. prefer `\"2.1\"` over `\"2\"`). If a step does not map to any section, refactor the step so it does — the SPEC is the source of truth for what work belongs in this plan.",
|
|
100
|
+
].join("\n");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Render the acceptance_tests guidance block — same contract as
|
|
105
|
+
* planner-role.js (the orchestrator path) so both entry points
|
|
106
|
+
* produce comparable plans.
|
|
107
|
+
*/
|
|
108
|
+
function renderAcceptanceTestsRules() {
|
|
109
|
+
return [
|
|
110
|
+
"### `acceptance_tests` (REQUIRED — at least one test per step)",
|
|
111
|
+
"Each step MUST include 2-4 executable tests. Mix `gherkin` (observable behaviour) and `shell` (concrete commands that exit 0).",
|
|
112
|
+
"",
|
|
113
|
+
"Shape:",
|
|
114
|
+
"```json",
|
|
115
|
+
'"acceptance_tests": [',
|
|
116
|
+
' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
|
|
117
|
+
' { "type": "shell", "content": "<bash command that exits 0 on success>" }',
|
|
118
|
+
"]",
|
|
119
|
+
"```",
|
|
120
|
+
"",
|
|
121
|
+
"Rules:",
|
|
122
|
+
"- Tests are written BEFORE the step is implemented — they should fail until the step is done, pass afterwards.",
|
|
123
|
+
"- Cover at least one happy path and one edge case / failure mode.",
|
|
124
|
+
"- Do NOT use the placeholder `\"npx vitest run\"` — every test must assert something specific to the step.",
|
|
125
|
+
].join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
67
128
|
export function buildPlannerPrompt({ task, context, architectContext, productContext = null, domainContext = null }) {
|
|
129
|
+
const specInfo = detectSpecSections(task);
|
|
130
|
+
|
|
68
131
|
const parts = [
|
|
69
132
|
"You are an expert software architect. Create an implementation plan for the following task.",
|
|
70
133
|
"",
|
|
@@ -93,10 +156,24 @@ export function buildPlannerPrompt({ task, context, architectContext, productCon
|
|
|
93
156
|
parts.push(
|
|
94
157
|
"## Output format",
|
|
95
158
|
"Respond with a JSON object containing:",
|
|
96
|
-
"- `approach`: A concise paragraph describing the overall strategy",
|
|
97
|
-
"- `steps`: An array of objects
|
|
98
|
-
"- `risks`: An array of strings describing potential risks or challenges",
|
|
99
|
-
"- `outOfScope`: An array of strings listing what is explicitly NOT included",
|
|
159
|
+
"- `approach`: A concise paragraph describing the overall strategy.",
|
|
160
|
+
"- `steps`: An array of step objects (see shape below). Each step = one commit.",
|
|
161
|
+
"- `risks`: An array of strings describing potential risks or challenges.",
|
|
162
|
+
"- `outOfScope`: An array of strings listing what is explicitly NOT included.",
|
|
163
|
+
"",
|
|
164
|
+
"Each step in `steps` has this shape:",
|
|
165
|
+
"```json",
|
|
166
|
+
"{",
|
|
167
|
+
' "description": "<what this step achieves, one sentence>",',
|
|
168
|
+
' "commit": "<conventional commit message>",',
|
|
169
|
+
' "spec_section": "<see rules below>",',
|
|
170
|
+
' "acceptance_tests": [ /* see rules below */ ]',
|
|
171
|
+
"}",
|
|
172
|
+
"```",
|
|
173
|
+
"",
|
|
174
|
+
renderSpecSectionRules(specInfo),
|
|
175
|
+
"",
|
|
176
|
+
renderAcceptanceTestsRules(),
|
|
100
177
|
"",
|
|
101
178
|
"Respond ONLY with valid JSON, no markdown fences or extra text."
|
|
102
179
|
);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { runCommand } from "../utils/process.js";
|
|
2
2
|
import { generateSnapshotDiff } from "./snapshot-diff.js";
|
|
3
|
+
import { getRunContext } from "../utils/run-context.js";
|
|
4
|
+
|
|
5
|
+
// Module-scope back-compat state for callers outside a `withRunContext`
|
|
6
|
+
// scope. When inside a pipeline, `getRunContext()` provides per-run
|
|
7
|
+
// isolation (TSK-0338) — the module-level values below are only consulted
|
|
8
|
+
// as a last resort.
|
|
3
9
|
|
|
4
10
|
/** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
|
|
5
11
|
let _runner = null;
|
|
@@ -10,35 +16,39 @@ let _snapshot = null;
|
|
|
10
16
|
/** @type {string|null} — project directory scope for git diff (prevents leaking unrelated changes) */
|
|
11
17
|
let _projectDir = null;
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
* Set the project directory scope. When set, all diffs are scoped to this directory.
|
|
15
|
-
* Call once at pipeline start when projectDir differs from repo root.
|
|
16
|
-
* @param {string|null} dir
|
|
17
|
-
*/
|
|
19
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ projectDir })` for isolation. */
|
|
18
20
|
export function setProjectDir(dir) {
|
|
19
21
|
_projectDir = dir;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* Store a filesystem snapshot for git-free diff fallback.
|
|
24
|
-
* Call before the coder runs. If git is available, this is unused.
|
|
25
|
-
* @param {Map<string, string>} snapshot
|
|
26
|
-
*/
|
|
24
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ snapshot })` for isolation. */
|
|
27
25
|
export function setSnapshot(snapshot) {
|
|
28
26
|
_snapshot = snapshot;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
* Inject an RTK-aware runner so all git operations in this module benefit from token savings.
|
|
33
|
-
* Call once at pipeline start when RTK is available.
|
|
34
|
-
* @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
|
|
35
|
-
*/
|
|
29
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ runner })` for isolation. */
|
|
36
30
|
export function setRunner(runner) {
|
|
37
31
|
_runner = runner;
|
|
38
32
|
}
|
|
39
33
|
|
|
34
|
+
function resolveRunner() {
|
|
35
|
+
return getRunContext()?.runner || _runner || runCommand;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveProjectDir() {
|
|
39
|
+
const ctx = getRunContext();
|
|
40
|
+
if (ctx && "projectDir" in ctx) return ctx.projectDir;
|
|
41
|
+
return _projectDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSnapshot() {
|
|
45
|
+
const ctx = getRunContext();
|
|
46
|
+
if (ctx && "snapshot" in ctx) return ctx.snapshot;
|
|
47
|
+
return _snapshot;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function run(command, args, ...rest) {
|
|
41
|
-
return (
|
|
51
|
+
return resolveRunner()(command, args, ...rest);
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
@@ -61,8 +71,9 @@ export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export async function generateDiff({ baseRef, stageNewFiles = false, projectDir = null }) {
|
|
64
|
-
// Auto-resolve projectDir from
|
|
65
|
-
const scopeDir = projectDir ||
|
|
74
|
+
// Auto-resolve projectDir from run context (TSK-0338) or module fallback
|
|
75
|
+
const scopeDir = projectDir || resolveProjectDir() || null;
|
|
76
|
+
const snapshot = resolveSnapshot();
|
|
66
77
|
|
|
67
78
|
// Try git diff first
|
|
68
79
|
try {
|
|
@@ -80,14 +91,16 @@ export async function generateDiff({ baseRef, stageNewFiles = false, projectDir
|
|
|
80
91
|
} catch { /* git not available or failed */ }
|
|
81
92
|
|
|
82
93
|
// Fallback: snapshot-based diff (no git needed)
|
|
83
|
-
if (
|
|
94
|
+
if (snapshot) {
|
|
84
95
|
const dir = projectDir || process.cwd();
|
|
85
|
-
return generateSnapshotDiff(
|
|
96
|
+
return generateSnapshotDiff(snapshot, null, dir);
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
// No git and no snapshot — generate diff of all files as new
|
|
99
|
+
// No git and no snapshot — generate diff of all files as new.
|
|
100
|
+
// TSK-0340: dropped a stray `const { takeSnapshot } = await import(...)`
|
|
101
|
+
// that was never used in this branch (we synthesize an empty Map and
|
|
102
|
+
// let generateSnapshotDiff do the file walk itself).
|
|
89
103
|
const dir = projectDir || process.cwd();
|
|
90
|
-
const { takeSnapshot } = await import("./snapshot-diff.js");
|
|
91
104
|
const emptySnapshot = new Map();
|
|
92
105
|
return generateSnapshotDiff(emptySnapshot, null, dir);
|
|
93
106
|
}
|
|
@@ -67,7 +67,7 @@ export async function generateSnapshotDiff(before, after, rootDir) {
|
|
|
67
67
|
const lines = [];
|
|
68
68
|
|
|
69
69
|
// New files (in after but not before)
|
|
70
|
-
for (const [path,
|
|
70
|
+
for (const [path, _hash] of after) {
|
|
71
71
|
if (!before.has(path)) {
|
|
72
72
|
const content = await safeRead(join(rootDir, path));
|
|
73
73
|
if (content !== null) {
|
package/src/roles/coder-role.js
CHANGED
|
@@ -22,19 +22,41 @@ export class CoderRole extends AgentRole {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
extractInput(input) {
|
|
25
|
-
if (typeof input === "string")
|
|
25
|
+
if (typeof input === "string") {
|
|
26
|
+
return {
|
|
27
|
+
task: input,
|
|
28
|
+
reviewerFeedback: null, sonarSummary: null, deferredContext: null,
|
|
29
|
+
acceptanceTests: null,
|
|
30
|
+
adrs: null, specSection: null, reviewerFindings: null, huId: null,
|
|
31
|
+
onOutput: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
26
34
|
return {
|
|
27
35
|
task: input?.task || this.context?.task || "",
|
|
28
36
|
reviewerFeedback: input?.reviewerFeedback || null,
|
|
29
37
|
sonarSummary: input?.sonarSummary || null,
|
|
30
38
|
deferredContext: input?.deferredContext || null,
|
|
39
|
+
// Tests-first (v2.7.5): the coder reads these as the gate it
|
|
40
|
+
// must satisfy. Only the plan-backed HU path populates this;
|
|
41
|
+
// the standard single-task `kj run` flow leaves it null.
|
|
42
|
+
acceptanceTests: input?.acceptanceTests || null,
|
|
43
|
+
// PR F (v2.7.5): per-HU planning context. ADRs constrain HOW,
|
|
44
|
+
// specSection ties this HU to a SPEC heading, reviewerFindings
|
|
45
|
+
// are filtered to entries that mention this HU. All four are
|
|
46
|
+
// null for non-plan-backed runs, which keeps the legacy flow
|
|
47
|
+
// byte-for-byte identical.
|
|
48
|
+
adrs: Array.isArray(input?.adrs) ? input.adrs : null,
|
|
49
|
+
specSection: input?.specSection || null,
|
|
50
|
+
reviewerFindings: input?.reviewerFindings || null,
|
|
51
|
+
huId: input?.huId || null,
|
|
31
52
|
onOutput: input?.onOutput || null
|
|
32
53
|
};
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext }) {
|
|
56
|
+
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId }) {
|
|
36
57
|
const prompt = await buildCoderPrompt({
|
|
37
|
-
task, reviewerFeedback, sonarSummary, deferredContext,
|
|
58
|
+
task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests,
|
|
59
|
+
adrs, specSection, reviewerFindings, huId,
|
|
38
60
|
coderRules: this.instructions,
|
|
39
61
|
methodology: this.config?.development?.methodology || "tdd",
|
|
40
62
|
serenaEnabled: Boolean(this.config?.serena?.enabled),
|
|
@@ -65,7 +65,48 @@ export class PlannerRole extends AgentRole {
|
|
|
65
65
|
|
|
66
66
|
const sections = [];
|
|
67
67
|
if (this.instructions) sections.push(this.instructions, "");
|
|
68
|
-
|
|
68
|
+
// Tests-first planning (v2.7.5):
|
|
69
|
+
// The planner MUST emit structured acceptance tests for every step.
|
|
70
|
+
// These are the contract the coder is handed (phase 2 of the
|
|
71
|
+
// tests-first flow); the tester role later runs them as the
|
|
72
|
+
// pass/fail gate. Without them the coder has no executable spec.
|
|
73
|
+
sections.push(
|
|
74
|
+
"Create an implementation plan for this task as structured JSON.",
|
|
75
|
+
"",
|
|
76
|
+
"Output MUST be a JSON object with this exact shape:",
|
|
77
|
+
"",
|
|
78
|
+
"```json",
|
|
79
|
+
"{",
|
|
80
|
+
' "approach": "<2-3 sentence high-level plan>",',
|
|
81
|
+
' "risks": ["<risk 1>", "<risk 2>"],',
|
|
82
|
+
' "outOfScope": ["<excluded item>"],',
|
|
83
|
+
' "steps": [',
|
|
84
|
+
" {",
|
|
85
|
+
' "description": "<what this step achieves, one sentence>",',
|
|
86
|
+
' "spec_section": "<short reference to the SPEC section this implements, e.g. \\"5.3\\" or \\"§5 Initial Scope\\". Optional but strongly preferred.>",',
|
|
87
|
+
' "acceptance_tests": [',
|
|
88
|
+
' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
|
|
89
|
+
' { "type": "shell", "content": "<bash command that must exit 0>" }',
|
|
90
|
+
" ]",
|
|
91
|
+
" }",
|
|
92
|
+
" ]",
|
|
93
|
+
"}",
|
|
94
|
+
"```",
|
|
95
|
+
"",
|
|
96
|
+
"Rules for acceptance_tests (CRITICAL — this is the contract the coder must satisfy):",
|
|
97
|
+
'- EVERY step MUST have at least one acceptance_test. No empty arrays.',
|
|
98
|
+
'- "gherkin" tests describe observable behaviour in natural language. Use them for end-to-end or user-facing assertions.',
|
|
99
|
+
'- "shell" tests are bash commands that exit 0 on success. Use them for concrete verifications (file exists, lint passes, specific exit code, HTTP status).',
|
|
100
|
+
'- Write tests BEFORE the step is implemented — they should fail until the step is done, pass afterwards.',
|
|
101
|
+
'- Prefer 2-4 tests per step: one happy-path, at least one edge case / failure path.',
|
|
102
|
+
'- Do NOT use the generic fallback "npx vitest run" (pre-v2.7.5 placeholder). Each test must assert something specific to the step.',
|
|
103
|
+
"",
|
|
104
|
+
"spec_section: when the task includes a structured SPEC (numbered headings like '## 5. Initial Scope'), every step MUST cite the section it implements. Becomes traceability metadata for the coder and reviewer.",
|
|
105
|
+
"",
|
|
106
|
+
"Steps must be ordered so that each depends only on previous steps (the first step has no prerequisites).",
|
|
107
|
+
"Return ONLY the JSON — no prose, no markdown fences around it.",
|
|
108
|
+
""
|
|
109
|
+
);
|
|
69
110
|
if (this.config?.productContext) sections.push("## Product Context", this.config.productContext, "");
|
|
70
111
|
if (this.config?.domainContext) sections.push("## Domain Context", this.config.domainContext, "");
|
|
71
112
|
appendDecompositionSection(sections, triageDecomposition);
|