karajan-code 2.7.4 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -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 (_runner || runCommand)(command, args, ...rest);
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 config if not passed explicitly
65
- const scopeDir = projectDir || _projectDir || null;
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 (_snapshot) {
94
+ if (snapshot) {
84
95
  const dir = projectDir || process.cwd();
85
- return generateSnapshotDiff(_snapshot, null, dir);
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, hash] of after) {
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) {
@@ -1,6 +1,11 @@
1
1
  import { AgentRole } from "./agent-role.js";
2
2
  import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
3
3
  import { measureBasalCost, loadPreviousAudit, saveAuditSnapshot, computeGrowthDelta } from "../audit/basal-cost.js";
4
+ import { detectProjectStack } from "../utils/stack-detect.js";
5
+ import { collectSonarFindings } from "../audit/sonar-findings.js";
6
+ import { collectWebPerfInput } from "../audit/webperf-input.js";
7
+ import { collectOsvFindings } from "../audit/osv-findings.js";
8
+ import { collectSemgrepFindings } from "../audit/semgrep-findings.js";
4
9
 
5
10
  function parseDimensions(dimensionsStr) {
6
11
  if (!dimensionsStr || dimensionsStr === "all") return null;
@@ -26,37 +31,98 @@ export class AuditRole extends AgentRole {
26
31
  super({ ...opts, name: "audit" });
27
32
  }
28
33
 
29
- async execute(input) {
30
- const task = typeof input === "string" ? input : input?.task || this.context?.task || "";
31
- const onOutput = typeof input === "string" ? null : input?.onOutput || null;
32
- const rawDimensions = typeof input === "object" ? input?.dimensions || null : null;
33
- const context = typeof input === "object" ? input?.context || null : null;
34
- const dimensions = typeof rawDimensions === "string" ? parseDimensions(rawDimensions) : rawDimensions;
35
-
34
+ /**
35
+ * Run the deterministic side of an audit — basalCost, stack detection,
36
+ * Sonar issues, WebPerf input — without invoking the LLM. Cheap (no
37
+ * tokens, no API call). KJC-TSK-0364 splits the original execute() into
38
+ * this collector + executeWithDeterministic() so the CLI can show
39
+ * deterministic findings first and ask the user before paying for the
40
+ * LLM phase.
41
+ *
42
+ * @param {object|string} input - same shape execute() accepts
43
+ * @returns {Promise<object>} deterministic context (the same data
44
+ * execute() would gather internally)
45
+ */
46
+ async collectDeterministic(input) {
47
+ const noSonar = typeof input === "object" ? Boolean(input?.noSonar) : false;
48
+ const noOsv = typeof input === "object" ? Boolean(input?.noOsv) : false;
49
+ const noSemgrep = typeof input === "object" ? Boolean(input?.noSemgrep) : false;
36
50
  const projectDir = this.config?.projectDir || process.cwd();
37
51
  let basalCost = null;
38
52
  let growthDelta = null;
53
+ let stack = null;
54
+ let sonarFindings = null;
55
+ let webperf = null;
56
+ let osvFindings = null;
57
+ let semgrepFindings = null;
39
58
  try {
40
59
  basalCost = await measureBasalCost(projectDir);
41
60
  const previous = await loadPreviousAudit(projectDir);
42
61
  growthDelta = computeGrowthDelta(basalCost, previous);
43
62
  } catch { /* basal cost is best-effort */ }
63
+ try {
64
+ stack = await detectProjectStack(projectDir);
65
+ } catch { /* stack detect is best-effort */ }
66
+ if (!noSonar) {
67
+ try {
68
+ sonarFindings = await collectSonarFindings(this.config, this.logger);
69
+ } catch { /* sonar fetch is best-effort */ }
70
+ }
71
+ try {
72
+ webperf = collectWebPerfInput(stack, this.config);
73
+ } catch { /* webperf input is best-effort */ }
74
+ // OSV vulnerabilities — KJC-TSK-0365. Best-effort: missing
75
+ // osv-scanner binary returns available:false and the audit
76
+ // continues without the section.
77
+ if (!noOsv) {
78
+ try {
79
+ osvFindings = await collectOsvFindings(projectDir, this.logger);
80
+ } catch { /* osv-scanner is best-effort */ }
81
+ }
82
+ // Semgrep SAST — KJC-TSK-0366. Best-effort: missing semgrep binary
83
+ // returns available:false and the audit continues without the section.
84
+ if (!noSemgrep) {
85
+ try {
86
+ semgrepFindings = await collectSemgrepFindings(projectDir, this.logger);
87
+ } catch { /* semgrep is best-effort */ }
88
+ }
89
+ return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings };
90
+ }
91
+
92
+ /**
93
+ * Run the LLM phase of an audit using a pre-collected deterministic
94
+ * context. KJC-TSK-0364: separated from execute() so the CLI can run
95
+ * collectDeterministic() first, show findings, prompt, and only then
96
+ * call this if the user wants to spend tokens.
97
+ */
98
+ async executeWithDeterministic(input, deterministicCtx) {
99
+ const task = typeof input === "string" ? input : input?.task || this.context?.task || "";
100
+ const onOutput = typeof input === "string" ? null : input?.onOutput || null;
101
+ const rawDimensions = typeof input === "object" ? input?.dimensions || null : null;
102
+ const context = typeof input === "object" ? input?.context || null : null;
103
+ const dimensions = typeof rawDimensions === "string" ? parseDimensions(rawDimensions) : rawDimensions;
104
+
105
+ const { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings } = deterministicCtx;
44
106
 
45
107
  const provider = this.resolveProvider();
46
108
  const agent = this.createAgentInstance(provider);
47
- const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context, basalCost, growthDelta });
109
+ const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings });
48
110
  const runArgs = { prompt, role: "audit" };
49
111
  if (onOutput) runArgs.onOutput = onOutput;
112
+ const startedAt = Date.now();
50
113
  const result = await agent.runTask(runArgs);
114
+ const durationMs = Date.now() - startedAt;
115
+
116
+ const usage = extractUsage(result, { provider, durationMs });
51
117
 
52
118
  if (!result.ok) {
53
- return { ok: false, result: { error: result.error || result.output || "Audit failed", provider }, summary: `Audit failed: ${result.error || "unknown error"}`, usage: result.usage };
119
+ return { ok: false, result: { error: result.error || result.output || "Audit failed", provider }, summary: `Audit failed: ${result.error || "unknown error"}`, usage };
54
120
  }
55
121
 
56
122
  try {
57
123
  const parsed = parseAuditOutput(result.output);
58
124
  if (!parsed) {
59
- return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage: result.usage };
125
+ return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage };
60
126
  }
61
127
  if (basalCost) { try { await saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ } }
62
128
 
@@ -65,13 +131,59 @@ export class AuditRole extends AgentRole {
65
131
  result: {
66
132
  summary: parsed.summary, dimensions: parsed.dimensions,
67
133
  topRecommendations: parsed.topRecommendations,
68
- basalCost: basalCost || undefined, growthDelta: growthDelta || undefined, provider
134
+ textSummary: parsed.textSummary || undefined,
135
+ basalCost: basalCost || undefined, growthDelta: growthDelta || undefined,
136
+ stack: stack || undefined,
137
+ sonarFindings: sonarFindings?.available ? sonarFindings : undefined,
138
+ webperf: webperf?.available ? webperf : undefined,
139
+ osvFindings: osvFindings?.available ? osvFindings : undefined,
140
+ semgrepFindings: semgrepFindings?.available ? semgrepFindings : undefined,
141
+ provider
69
142
  },
70
143
  summary: buildSummary(parsed),
71
- usage: result.usage
144
+ usage
72
145
  };
73
146
  } catch {
74
- return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage: result.usage };
147
+ return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage };
75
148
  }
76
149
  }
150
+
151
+ /**
152
+ * Convenience: run both phases in one call. Identical to the previous
153
+ * (single-phase) execute() so MCP and pipeline callers don't change.
154
+ */
155
+ async execute(input) {
156
+ const deterministicCtx = await this.collectDeterministic(input);
157
+ return this.executeWithDeterministic(input, deterministicCtx);
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Consolidate the usage telemetry the agent spreads across its top-level
163
+ * result fields. Returns a single object so consumers (CLI formatter,
164
+ * report-file writer, MCP) don't need to know about the spread shape.
165
+ *
166
+ * @param {object} result - raw agent.runTask() return
167
+ * @param {{provider: string, durationMs: number}} ctx
168
+ * @returns {{available: boolean, provider: string, model?: string, tokens_in?: number, tokens_out?: number, total_tokens?: number, cost_usd?: number, durationMs: number, reason?: string}}
169
+ */
170
+ function extractUsage(result, { provider, durationMs }) {
171
+ if (!result || typeof result !== "object") {
172
+ return { available: false, provider, durationMs, reason: "no agent result" };
173
+ }
174
+ const tokens_in = Number.isFinite(result.tokens_in) ? result.tokens_in : null;
175
+ const tokens_out = Number.isFinite(result.tokens_out) ? result.tokens_out : null;
176
+ if (tokens_in === null && tokens_out === null) {
177
+ return { available: false, provider, model: result.model, durationMs, reason: `provider "${provider}" did not report token usage` };
178
+ }
179
+ return {
180
+ available: true,
181
+ provider,
182
+ model: result.model,
183
+ tokens_in: tokens_in ?? 0,
184
+ tokens_out: tokens_out ?? 0,
185
+ total_tokens: (tokens_in ?? 0) + (tokens_out ?? 0),
186
+ cost_usd: typeof result.cost_usd === "number" ? result.cost_usd : null,
187
+ durationMs,
188
+ };
77
189
  }
@@ -22,19 +22,41 @@ export class CoderRole extends AgentRole {
22
22
  }
23
23
 
24
24
  extractInput(input) {
25
- if (typeof input === "string") return { task: input, reviewerFeedback: null, sonarSummary: null, deferredContext: null, onOutput: null };
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
- sections.push("Create an implementation plan for this task.", "Return concise numbered steps focused on execution order and risk.", "");
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);
@@ -0,0 +1,181 @@
1
+ /**
2
+ * RepairerRole — repairs broken acceptance tests at runtime.
3
+ *
4
+ * The Brain calls this when an HU's acceptance test has failed N
5
+ * consecutive iterations with the same error signature, signalling
6
+ * that the test ITSELF may be the bug (not the implementation).
7
+ * Originated from the 2026-04-29 incident where the planner emitted
8
+ * a syntactically-broken jq query in HU 002's acceptance_tests; the
9
+ * coder iterated 3× trying to make an irreparable test pass; HU 002
10
+ * → failed → 7 dependents blocked.
11
+ *
12
+ * The role takes the failing test + its error output + HU intent and
13
+ * returns either a corrected test (`verdict: "fixable"` with
14
+ * `fixed_test`) or an escalation (`verdict: "unfixable"` with reason).
15
+ *
16
+ * Behaviour notes:
17
+ * - The role MUST NOT soften assertions to make them pass — the test
18
+ * defines the contract; this role's job is to make the test ASK
19
+ * its question correctly, not weakly.
20
+ * - The role MUST NOT rewrite a test to pass against the current
21
+ * code. Code is measured against the test, not the other way.
22
+ * - One bug per repair. If the test is layered with problems, return
23
+ * the most blocking fix first; subsequent invocations handle the
24
+ * rest.
25
+ */
26
+
27
+ import { AgentRole } from "./agent-role.js";
28
+ import { extractFirstJson } from "../utils/json-extract.js";
29
+ import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
30
+
31
+ const SUBAGENT_PREAMBLE = [
32
+ "IMPORTANT: You are running as a Karajan sub-agent.",
33
+ "Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
34
+ "Do NOT use any MCP tools. Focus only on diagnosing and (if possible) repairing a single broken acceptance test.",
35
+ ].join(" ");
36
+
37
+ export class RepairerRole extends AgentRole {
38
+ constructor(opts) {
39
+ super({ ...opts, name: "repairer" });
40
+ }
41
+
42
+ resolveProvider() {
43
+ // Default to the same provider family the coder uses — keeps
44
+ // the repair attempt cheap and consistent. Per-role override
45
+ // works as for every other role.
46
+ return this.config?.roles?.repairer?.provider
47
+ || this.config?.roles?.coder?.provider
48
+ || this.config?.coder
49
+ || "claude";
50
+ }
51
+
52
+ /**
53
+ * Inputs:
54
+ * - failingTest: the test object (`{ type, content }` or string).
55
+ * - errorOutput: stdout+stderr from the last test run (truncated to
56
+ * ~2KB to keep the prompt small).
57
+ * - huTitle, huScope: intent context.
58
+ * - siblingTests: the HU's other acceptance_tests (for consistency).
59
+ * - iteration: how many coder iterations have already failed on this.
60
+ */
61
+ extractInput(input) {
62
+ if (typeof input === "string") {
63
+ return {
64
+ failingTest: input, errorOutput: "", huTitle: "", huScope: "",
65
+ siblingTests: [], iteration: 0, onOutput: null,
66
+ };
67
+ }
68
+ return {
69
+ failingTest: input?.failingTest ?? null,
70
+ errorOutput: input?.errorOutput ?? "",
71
+ huTitle: input?.huTitle ?? "",
72
+ huScope: input?.huScope ?? "",
73
+ siblingTests: Array.isArray(input?.siblingTests) ? input.siblingTests : [],
74
+ iteration: Number.isFinite(input?.iteration) ? input.iteration : 0,
75
+ onOutput: input?.onOutput ?? null,
76
+ };
77
+ }
78
+
79
+ async buildPrompt({ failingTest, errorOutput, huTitle, huScope, siblingTests, iteration }) {
80
+ const sections = [SUBAGENT_PREAMBLE];
81
+ if (this.instructions) sections.push(this.instructions);
82
+
83
+ const testContent = (failingTest && typeof failingTest === "object")
84
+ ? failingTest.content
85
+ : String(failingTest || "");
86
+ const testType = (failingTest && typeof failingTest === "object" && failingTest.type)
87
+ ? failingTest.type
88
+ : "shell";
89
+
90
+ sections.push(
91
+ "You are repairing a broken acceptance test. The coder has failed to satisfy it across multiple iterations,",
92
+ "and the Brain suspects the test ITSELF is the bug — not the implementation.",
93
+ "",
94
+ "## HU intent",
95
+ `Title: ${huTitle || "(unknown)"}`,
96
+ `Scope: ${huScope || "(unknown)"}`,
97
+ "",
98
+ `## Failing test (type: ${testType}, after ${iteration} iteration(s))`,
99
+ "```",
100
+ testContent,
101
+ "```",
102
+ "",
103
+ "## Last error output",
104
+ "```",
105
+ String(errorOutput || "").slice(0, 2048),
106
+ "```",
107
+ );
108
+
109
+ if (siblingTests.length > 0) {
110
+ sections.push("## HU's other acceptance tests (for consistency — do NOT modify these)", "```");
111
+ for (const t of siblingTests) {
112
+ const c = typeof t === "object" ? t.content : t;
113
+ sections.push(`- ${c}`);
114
+ }
115
+ sections.push("```");
116
+ }
117
+
118
+ sections.push(
119
+ "",
120
+ "## Your job",
121
+ "Decide whether the failing test is **fixable** (you can correct a syntactic / structural bug)",
122
+ "or **unfixable** (test is fundamentally impossible, or the test is correct and the code must change).",
123
+ "",
124
+ "Return ONLY a single valid JSON object:",
125
+ '{"verdict":"fixable"|"unfixable","fixed_test":{"type":"shell"|"gherkin","content":string}|null,"reason":string,"diagnostics":[string]}',
126
+ "",
127
+ "- When verdict=fixable, fixed_test is REQUIRED and reason describes the bug class.",
128
+ "- When verdict=unfixable, fixed_test is null and reason explains why.",
129
+ "- NEVER soften an assertion. NEVER rewrite to pass against current code.",
130
+ );
131
+
132
+ const projectDir = this.config?.projectDir || process.cwd();
133
+ const skills = await loadAvailableSkills(projectDir);
134
+ const skillSection = buildSkillSection(skills, {
135
+ provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null),
136
+ role: "repairer",
137
+ });
138
+ if (skillSection) sections.push(skillSection);
139
+
140
+ return { prompt: sections.join("\n\n") };
141
+ }
142
+
143
+ parseOutput(raw) { return extractFirstJson(raw); }
144
+
145
+ isSuccessful(parsed) {
146
+ if (!parsed || typeof parsed !== "object") return false;
147
+ if (parsed.verdict === "fixable") {
148
+ // Must produce a complete fixed_test object.
149
+ const ft = parsed.fixed_test;
150
+ return Boolean(ft && typeof ft === "object" && typeof ft.content === "string" && ft.content.trim());
151
+ }
152
+ if (parsed.verdict === "unfixable") {
153
+ // Unfixable is a valid OUTCOME (escalation), not a role failure.
154
+ return Boolean(parsed.reason && String(parsed.reason).trim());
155
+ }
156
+ return false;
157
+ }
158
+
159
+ buildSuccessResult(parsed, provider) {
160
+ return {
161
+ verdict: parsed.verdict,
162
+ fixed_test: parsed.fixed_test || null,
163
+ reason: parsed.reason || "",
164
+ diagnostics: Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [],
165
+ provider,
166
+ };
167
+ }
168
+
169
+ buildSummary(parsed) {
170
+ if (!parsed) return "Repairer: no parseable output";
171
+ if (parsed.verdict === "fixable") {
172
+ const reason = (parsed.reason || "").slice(0, 120);
173
+ return `Repairer: fixable — ${reason}`;
174
+ }
175
+ if (parsed.verdict === "unfixable") {
176
+ const reason = (parsed.reason || "").slice(0, 120);
177
+ return `Repairer: unfixable — ${reason}`;
178
+ }
179
+ return `Repairer: unexpected verdict "${parsed.verdict}"`;
180
+ }
181
+ }