karajan-code 2.7.3 → 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.
Files changed (167) hide show
  1. package/README.md +4 -4
  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/bootstrap.js +26 -1
  10. package/src/brain/decision-tracker.js +3 -1
  11. package/src/brain/decisor.js +1 -1
  12. package/src/canvas/parse-md.js +238 -0
  13. package/src/canvas/schema.js +101 -0
  14. package/src/canvas/to-flat.js +85 -0
  15. package/src/canvas/validate.js +163 -0
  16. package/src/checks/binaries.js +1 -1
  17. package/src/checks/ports.js +97 -27
  18. package/src/checks/runner.js +26 -10
  19. package/src/checks/sonar.js +1 -1
  20. package/src/checks/system.js +1 -1
  21. package/src/checks/tokens.js +77 -24
  22. package/src/cli/_shared.js +30 -0
  23. package/src/cli/register-meta.js +204 -0
  24. package/src/cli/register-pipeline.js +212 -0
  25. package/src/cli/register-plan.js +152 -0
  26. package/src/cli/register-roles-skills.js +38 -0
  27. package/src/cli/register-sonar.js +28 -0
  28. package/src/cli.js +83 -468
  29. package/src/commands/agents.js +18 -6
  30. package/src/commands/architect.js +29 -2
  31. package/src/commands/audit.js +27 -3
  32. package/src/commands/board.js +160 -30
  33. package/src/commands/clean.js +83 -0
  34. package/src/commands/code.js +7 -2
  35. package/src/commands/discover.js +7 -2
  36. package/src/commands/init.js +2 -2
  37. package/src/commands/plan/_shared.js +40 -0
  38. package/src/commands/plan/add-hu.js +21 -0
  39. package/src/commands/plan/delete.js +9 -0
  40. package/src/commands/plan/generate.js +284 -0
  41. package/src/commands/plan/index.js +14 -0
  42. package/src/commands/plan/list.js +21 -0
  43. package/src/commands/plan/ready.js +46 -0
  44. package/src/commands/plan/remove-hu.js +17 -0
  45. package/src/commands/plan/show.js +81 -0
  46. package/src/commands/plan/validate.js +20 -0
  47. package/src/commands/plan.js +14 -291
  48. package/src/commands/researcher.js +7 -2
  49. package/src/commands/resume.js +24 -1
  50. package/src/commands/review.js +7 -2
  51. package/src/commands/run.js +52 -1
  52. package/src/commands/status.js +1 -1
  53. package/src/commands/sync.js +75 -0
  54. package/src/commands/triage.js +7 -2
  55. package/src/commands/undo.js +1 -1
  56. package/src/config/defaults.js +213 -0
  57. package/src/config/loader.js +137 -0
  58. package/src/config/overrides.js +199 -0
  59. package/src/config/role-resolver.js +159 -0
  60. package/src/config/test-harness.js +79 -0
  61. package/src/config.js +30 -609
  62. package/src/git/automation.js +2 -2
  63. package/src/guards/output-guard.js +0 -2
  64. package/src/hu/acceptance-runner.js +52 -10
  65. package/src/hu/store.js +11 -0
  66. package/src/mcp/handlers/direct-handlers.js +2 -5
  67. package/src/mcp/handlers/management-handlers.js +48 -4
  68. package/src/mcp/handlers/run-handler.js +9 -8
  69. package/src/mcp/preflight.js +32 -13
  70. package/src/mcp/server-handlers.js +4 -3
  71. package/src/mcp/suggest-handler.js +3 -2
  72. package/src/mcp/tools.js +15 -0
  73. package/src/orchestrator/agent-fallback.js +1 -1
  74. package/src/orchestrator/ci-integration.js +10 -10
  75. package/src/orchestrator/config-init.js +25 -7
  76. package/src/orchestrator/direct-actions.js +6 -2
  77. package/src/orchestrator/drivers/error-recovery.js +287 -0
  78. package/src/orchestrator/drivers/init-context.js +222 -0
  79. package/src/orchestrator/drivers/iteration-loop.js +311 -0
  80. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  81. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  82. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  83. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  84. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  85. package/src/orchestrator/drivers/post-loop.js +370 -0
  86. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  87. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  88. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  89. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  90. package/src/orchestrator/drivers/pre-loop.js +434 -0
  91. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  92. package/src/orchestrator/flow-control.js +4 -3
  93. package/src/orchestrator/flow-runner.js +121 -2019
  94. package/src/orchestrator/fs-leak-detector.js +156 -0
  95. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  96. package/src/orchestrator/integrations.js +88 -0
  97. package/src/orchestrator/post-loop-stages.js +35 -8
  98. package/src/orchestrator/preflight-checks.js +30 -3
  99. package/src/orchestrator/repair/repair-detector.js +117 -0
  100. package/src/orchestrator/repair/repair-runner.js +142 -0
  101. package/src/orchestrator/reviewer-fallback.js +1 -1
  102. package/src/orchestrator/solomon-escalation.js +3 -2
  103. package/src/orchestrator/solomon-rules.js +7 -3
  104. package/src/orchestrator/stages/architect-stage.js +12 -11
  105. package/src/orchestrator/stages/coder-stage.js +56 -7
  106. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  107. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  108. package/src/orchestrator/stages/planner-stage.js +1 -1
  109. package/src/orchestrator/stages/research-stage.js +1 -1
  110. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  111. package/src/orchestrator/stages/sonar-stage.js +94 -10
  112. package/src/orchestrator/stages/stage-classes.js +86 -0
  113. package/src/orchestrator/stages/stage-executor.js +1 -1
  114. package/src/orchestrator/stages/triage-stage.js +1 -1
  115. package/src/orchestrator/standby.js +3 -2
  116. package/src/orchestrator/verification-gate.js +20 -12
  117. package/src/plan/adr-generator.js +170 -0
  118. package/src/plan/adr-loader.js +60 -0
  119. package/src/plan/plan-executor.js +19 -1
  120. package/src/plan/plan-hu-ops.js +170 -1
  121. package/src/plan/plan-reviewer.js +199 -0
  122. package/src/plan/plan-schema.js +69 -1
  123. package/src/plan/plan-store.js +40 -3
  124. package/src/plan/spec-detector.js +61 -0
  125. package/src/plan/tests-synthesizer.js +116 -0
  126. package/src/planning-game/client.js +3 -14
  127. package/src/planning-game/decomposition.js +1 -1
  128. package/src/planning-game/pipeline-adapter.js +36 -5
  129. package/src/prompts/coder.js +172 -1
  130. package/src/prompts/hu-reviewer.js +0 -1
  131. package/src/prompts/planner.js +81 -4
  132. package/src/review/diff-generator.js +35 -22
  133. package/src/review/snapshot-diff.js +1 -1
  134. package/src/roles/coder-role.js +25 -3
  135. package/src/roles/planner-role.js +42 -1
  136. package/src/roles/repairer-role.js +181 -0
  137. package/src/roles/tester-role.js +70 -4
  138. package/src/session/mutators.js +304 -0
  139. package/src/session/runtime-overrides.js +59 -0
  140. package/src/{session-store.js → session/store.js} +8 -2
  141. package/src/skills/semantic-detector.js +10 -5
  142. package/src/skills/skill-detector.js +1 -1
  143. package/src/sonar/cloud-scanner.js +0 -1
  144. package/src/sonar/credentials.js +14 -6
  145. package/src/sonar/discovery.js +123 -0
  146. package/src/sonar/manager.js +18 -0
  147. package/src/sync/detect-drift.js +113 -0
  148. package/src/tracker-bootstrap.js +44 -0
  149. package/src/utils/agent-detect.js +15 -11
  150. package/src/utils/board-prompt-bridge.js +109 -0
  151. package/src/utils/cli-progress.js +161 -0
  152. package/src/utils/cwd-confirm.js +39 -0
  153. package/src/utils/derive-project-name-from-cwd.js +119 -0
  154. package/src/utils/garbage-collector.js +396 -0
  155. package/src/utils/git.js +16 -4
  156. package/src/utils/locale.js +1 -1
  157. package/src/utils/paths.js +21 -1
  158. package/src/utils/process.js +1 -1
  159. package/src/utils/prompt-project-name.js +35 -0
  160. package/src/utils/run-context.js +75 -0
  161. package/src/utils/run-log.js +12 -0
  162. package/src/utils/task-file.js +1 -1
  163. package/src/utils/telemetry.js +13 -1
  164. package/src/webperf/devtools-detect.js +1 -1
  165. package/templates/coder-rules.md +13 -0
  166. package/templates/roles/coder.md +34 -0
  167. package/templates/roles/repairer.md +94 -0
@@ -16,7 +16,6 @@ import { emitProgress, makeEvent } from "../utils/events.js";
16
16
  import { msg, getLang } from "../utils/messages.js";
17
17
  import {
18
18
  resolveSonarHost,
19
- resolveSonarToken,
20
19
  resolveSonarTokenAsync,
21
20
  resolveSonarCredentials,
22
21
  } from "../sonar/config-resolver.js";
@@ -30,6 +29,7 @@ import { getPortChecks } from "../checks/ports.js";
30
29
  import { getTokenChecks } from "../checks/tokens.js";
31
30
  import { getMcpHealthChecks } from "../checks/mcp-health.js";
32
31
  import { getSkillsChecks } from "../checks/skills.js";
32
+ import { resolveTestHarness } from "../config/test-harness.js";
33
33
 
34
34
  function parseJsonSafe(text) {
35
35
  try {
@@ -168,7 +168,32 @@ async function checkSecurityAgent(config) {
168
168
  * @returns {{ ok: boolean, checks: object[], remediations: string[], configOverrides: object, warnings: string[], errors: object[] }}
169
169
  */
170
170
  export async function runPreflightChecks({ config, logger, emitter, eventBase, resolvedPolicies, securityEnabled }) {
171
- const sonarEnabled = Boolean(config.sonarqube?.enabled) && resolvedPolicies.sonar !== false;
171
+ // Defensive test-harness resolution for direct callers that bypass loadConfig()
172
+ // (notably tests/preflight-checks.test.js and tests/sonar-token-flow.test.js
173
+ // which construct raw configs). Production callers go through loadConfig
174
+ // which already populates config.testHarness once via resolveTestHarness().
175
+ // Idempotent — no-op when already present.
176
+ if (config && !config.testHarness) {
177
+ config.testHarness = resolveTestHarness(config.testHarness);
178
+ }
179
+ // Sonar is intrinsic to Karajan for code tasks (sw/refactor/add-tests).
180
+ // Since v2.7.4 it is NOT toggleable via `config.sonarqube.enabled` —
181
+ // that field is ignored (deprecation warning emitted at config load).
182
+ // The taskType policy (resolved_policies.sonar from DEFAULT_POLICIES in
183
+ // src/guards/policy-resolver.js) is the single source of truth: sw /
184
+ // refactor / add-tests run Sonar, audit / doc / infra / analysis /
185
+ // no-code skip it. Solomon may decide to skip a single iteration via
186
+ // rule alerts — that's a runtime decision, not a config option.
187
+ //
188
+ // Test-harness escape hatch via config.testHarness.disableSonarStage
189
+ // — production code reads `config?.testHarness?.disableSonarStage`,
190
+ // never `globalThis.*`. The legacy `globalThis.__KJ_DISABLE_SONAR_STAGE`
191
+ // shape is documented (and exclusively read) in
192
+ // src/config/test-harness.js, where the loader translates it into
193
+ // the typed config slot above. ESLint rule (#557) blocks any
194
+ // re-introduction outside that one file.
195
+ const sonarStageDisabledForTest = config?.testHarness?.disableSonarStage === true;
196
+ const sonarEnabled = !sonarStageDisabledForTest && resolvedPolicies.sonar !== false;
172
197
  const isExternalSonar = Boolean(config.sonarqube?.external);
173
198
  const sonarHost = resolveSonarHost(config.sonarqube?.host);
174
199
 
@@ -182,7 +207,9 @@ export async function runPreflightChecks({ config, logger, emitter, eventBase, r
182
207
  };
183
208
 
184
209
  // Resolve extended preflight opt-in early so the short-circuit below can see it.
185
- const extendedDefault = globalThis.__KJ_DEFAULT_PREFLIGHT_EXTENDED ?? true;
210
+ // Post-v2.7.5 this no longer reads globalThis directly — config.testHarness
211
+ // is populated by the loader from the global or the default.
212
+ const extendedDefault = config?.testHarness?.defaultPreflightExtended ?? true;
186
213
  const extendedEnabled = config?.preflight?.extended ?? extendedDefault;
187
214
 
188
215
  // Preserve legacy short-circuit: if nothing at all needs checking, skip
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Heuristics for classifying an acceptance-test failure as "possibly
3
+ * infeasible" — i.e. the test itself looks broken, not the code.
4
+ *
5
+ * Used by the HU sub-pipeline to decide when to invoke the Repairer
6
+ * role instead of feeding the failure back to the coder for ANOTHER
7
+ * iteration. Each iteration costs real money + time; classifying
8
+ * misroutes is the difference between a 3-minute repair and a
9
+ * 30-minute budget burn.
10
+ *
11
+ * The classifier is deliberately CONSERVATIVE: false negatives
12
+ * (treating a broken test as just-failed) cost an extra iteration;
13
+ * false positives (treating a real failure as infeasible) waste a
14
+ * Repairer call AND give the coder a spec change it shouldn't
15
+ * receive. We err toward false negatives.
16
+ */
17
+
18
+ /**
19
+ * Signatures that strongly suggest the TEST is broken, not the code.
20
+ * Each is a regex applied to the failing command's stderr+stdout.
21
+ */
22
+ const INFEASIBLE_SIGNATURES = [
23
+ // jq: `length as $n | .field` after a transform — the binding doesn't
24
+ // change the value flowing through, so `.field` is applied to the wrong
25
+ // thing. Real-world example from 2026-04-29: "Cannot index array with
26
+ // string \"filesOver300LOC\"".
27
+ { kind: "jq-as-binding-misuse", re: /Cannot index array with string/i },
28
+ // bash -n / shell parse errors before the command even runs.
29
+ { kind: "shell-parse-error", re: /syntax error|unexpected token|unexpected EOF/i },
30
+ // jq: invalid query syntax detected by jq itself.
31
+ { kind: "jq-syntax", re: /jq: error \(at .*\): (?!Cannot index)/i },
32
+ // command not found = test references a tool that doesn't exist in
33
+ // this environment. Could be a fixable typo or a real env mismatch.
34
+ { kind: "missing-tool", re: /command not found|: not found$|No such file or directory.*\.(sh|mjs|js|py)/im },
35
+ // jq fails to parse the JSON input — the input file is wrong, the
36
+ // path is wrong, or the assumed schema is off.
37
+ { kind: "jq-parse-input", re: /jq: error: Could not (open|parse)/i },
38
+ ];
39
+
40
+ /**
41
+ * Compute a stable signature for a single failing test outcome — the
42
+ * test command + a normalized error fingerprint. Used to detect when
43
+ * the SAME test is failing the SAME way across iterations; that's the
44
+ * pattern that argues for a Repairer call (vs. a coder retry).
45
+ *
46
+ * @param {string} cmd
47
+ * @param {string} errorOutput
48
+ * @returns {string}
49
+ */
50
+ export function failureSignature(cmd, errorOutput) {
51
+ const cmdNorm = String(cmd || "").trim().slice(0, 200);
52
+ const errNorm = String(errorOutput || "")
53
+ .replace(/\s+/g, " ") // collapse whitespace
54
+ .replace(/\d+/g, "N") // numbers tend to drift; line numbers, counts
55
+ .replace(/\/[^\s]+/g, "/PATH") // absolute paths drift across runs
56
+ .trim()
57
+ .slice(0, 300);
58
+ return `${cmdNorm}|||${errNorm}`;
59
+ }
60
+
61
+ /**
62
+ * Classify a single test failure as `infeasible` or `regular`.
63
+ *
64
+ * @param {object} args
65
+ * @param {string} args.cmd — the command that ran
66
+ * @param {string} args.errorOutput — stdout + stderr from the failing run
67
+ * @param {number} args.consecutive — how many iterations in a row this
68
+ * same signature has failed
69
+ * @param {number} [args.threshold=2] — min consecutive failures before
70
+ * infeasibility is even considered.
71
+ * Below this, we always say "regular"
72
+ * (let the coder try once more).
73
+ * @returns {{infeasible: boolean, kind: string|null, signature: string}}
74
+ */
75
+ export function classifyFailure({ cmd, errorOutput, consecutive, threshold = 2 }) {
76
+ const signature = failureSignature(cmd, errorOutput);
77
+ if (!Number.isFinite(consecutive) || consecutive < threshold) {
78
+ return { infeasible: false, kind: null, signature };
79
+ }
80
+ for (const sig of INFEASIBLE_SIGNATURES) {
81
+ if (sig.re.test(errorOutput || "")) {
82
+ return { infeasible: true, kind: sig.kind, signature };
83
+ }
84
+ }
85
+ return { infeasible: false, kind: null, signature };
86
+ }
87
+
88
+ /**
89
+ * Track consecutive identical failures across iterations of the same HU.
90
+ * Caller passes the same instance across iterations; the tracker
91
+ * remembers the last signature per command.
92
+ */
93
+ export function createFailureTracker() {
94
+ // Map<cmd, { signature, count }>
95
+ const state = new Map();
96
+ return {
97
+ /**
98
+ * Record a failure and return how many consecutive iterations have
99
+ * produced the same signature for this cmd.
100
+ * @returns {number}
101
+ */
102
+ record(cmd, errorOutput) {
103
+ const signature = failureSignature(cmd, errorOutput);
104
+ const prev = state.get(cmd);
105
+ if (prev && prev.signature === signature) {
106
+ prev.count += 1;
107
+ return prev.count;
108
+ }
109
+ state.set(cmd, { signature, count: 1 });
110
+ return 1;
111
+ },
112
+ /** Reset tracker for a cmd that just passed (stops false positives). */
113
+ reset(cmd) { state.delete(cmd); },
114
+ /** Diagnostic: dump current state. */
115
+ snapshot() { return Object.fromEntries(state); },
116
+ };
117
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Repair runner — orchestrates the Repairer role end-to-end.
3
+ *
4
+ * Called from run-hu-batch when an acceptance test failure has been
5
+ * classified as `infeasible` by repair-detector. The runner:
6
+ * 1. Builds the prompt context (failing test, error output, HU
7
+ * intent, sibling tests).
8
+ * 2. Invokes RepairerRole.
9
+ * 3. If verdict=fixable: patches the plan JSON's acceptance_tests
10
+ * AND the in-memory batch story so the next coder iteration sees
11
+ * the corrected test. Saves both. Returns { repaired: true }.
12
+ * 4. If verdict=unfixable: returns { repaired: false, escalate: true,
13
+ * reason } so the caller can hand off to Solomon / FDE.
14
+ * 5. If RepairerRole itself fails (agent error): returns
15
+ * { repaired: false, escalate: false, error } so the caller
16
+ * decides whether to retry coder or escalate.
17
+ */
18
+
19
+ import { RepairerRole } from "../../roles/repairer-role.js";
20
+ import { saveHuBatch } from "../../hu/store.js";
21
+ import { emitProgress, makeEvent } from "../../utils/events.js";
22
+
23
+ /**
24
+ * @param {object} args
25
+ * @param {object} args.story — the HU story object (in-memory batch slice). MUTATED.
26
+ * @param {number} args.testIndex — index in story.acceptance_tests of the failing test.
27
+ * @param {string} args.errorOutput — last failing run's stderr+stdout.
28
+ * @param {string} args.batchSessionId
29
+ * @param {object} args.batch — full batch object. MUTATED (story is a slice).
30
+ * @param {object} args.config
31
+ * @param {object} args.logger
32
+ * @param {object} args.emitter
33
+ * @param {object} args.eventBase
34
+ * @param {string} args.failureKind — repair-detector's `kind` (e.g. "jq-as-binding-misuse").
35
+ * @param {Function} [args.savePlanPatch] — async (huId, fieldPatch) => void; if provided,
36
+ * patches the plan JSON on disk too.
37
+ * @returns {Promise<{repaired: boolean, escalate?: boolean, reason?: string, error?: string}>}
38
+ */
39
+ export async function runRepair({
40
+ story, testIndex, errorOutput, batchSessionId, batch,
41
+ config, logger, emitter, eventBase, failureKind, savePlanPatch,
42
+ }) {
43
+ if (!story || !Array.isArray(story.acceptance_tests) || !story.acceptance_tests[testIndex]) {
44
+ return { repaired: false, escalate: false, error: "repair-runner: invalid story or testIndex" };
45
+ }
46
+
47
+ const failingTest = story.acceptance_tests[testIndex];
48
+ const siblingTests = story.acceptance_tests.filter((_, i) => i !== testIndex);
49
+
50
+ emitProgress(emitter, makeEvent("repair:start", { ...eventBase, stage: "repair" }, {
51
+ message: `Repairer invoked for HU ${story.id} test #${testIndex} (kind=${failureKind})`,
52
+ detail: {
53
+ huId: story.id,
54
+ testIndex,
55
+ testContent: typeof failingTest === "object" ? failingTest.content : failingTest,
56
+ kind: failureKind,
57
+ },
58
+ }));
59
+
60
+ const repairer = new RepairerRole({ config, logger, emitter });
61
+ await repairer.init({ task: story.scope || story.title || story.id, sessionId: batchSessionId, iteration: 0 });
62
+
63
+ let result;
64
+ try {
65
+ result = await repairer.run({
66
+ failingTest,
67
+ errorOutput,
68
+ huTitle: story.title || story.id,
69
+ huScope: story.scope || "",
70
+ siblingTests,
71
+ iteration: 0,
72
+ });
73
+ } catch (err) {
74
+ logger.warn(`Repairer threw: ${err.message}`);
75
+ emitProgress(emitter, makeEvent("repair:end", { ...eventBase, stage: "repair" }, {
76
+ status: "fail",
77
+ message: `Repairer error: ${err.message}`,
78
+ detail: { huId: story.id, testIndex },
79
+ }));
80
+ return { repaired: false, escalate: false, error: err.message };
81
+ }
82
+
83
+ if (!result?.ok) {
84
+ logger.warn(`Repairer failed: ${result?.summary || "unknown"}`);
85
+ emitProgress(emitter, makeEvent("repair:end", { ...eventBase, stage: "repair" }, {
86
+ status: "fail",
87
+ message: `Repairer could not produce a verdict: ${result?.summary || "unknown"}`,
88
+ detail: { huId: story.id, testIndex },
89
+ }));
90
+ return { repaired: false, escalate: false, error: result?.summary || "no verdict" };
91
+ }
92
+
93
+ const { verdict, fixed_test: fixedTest, reason } = result.result;
94
+
95
+ if (verdict === "unfixable") {
96
+ logger.info(`Repairer: HU ${story.id} test #${testIndex} unfixable — ${reason}`);
97
+ emitProgress(emitter, makeEvent("repair:end", { ...eventBase, stage: "repair" }, {
98
+ status: "escalate",
99
+ message: `Repairer: unfixable — ${reason}`,
100
+ detail: { huId: story.id, testIndex, reason },
101
+ }));
102
+ return { repaired: false, escalate: true, reason };
103
+ }
104
+
105
+ if (verdict !== "fixable" || !fixedTest) {
106
+ logger.warn(`Repairer returned unexpected verdict: ${verdict}`);
107
+ return { repaired: false, escalate: false, error: `unexpected verdict: ${verdict}` };
108
+ }
109
+
110
+ // Apply the fix in-memory and persist when we have the batch handle.
111
+ // Some callers can't pass `batch` from their scope (closure depth);
112
+ // the in-memory mutation of `story.acceptance_tests` is enough for
113
+ // the next iteration of the same HU loop because every acceptance
114
+ // run reads the live story object. Disk persistence is a "next run
115
+ // sees the fix too" optimisation; PR #544's reconcile keeps the
116
+ // plan JSON consistent across runs in either case.
117
+ story.acceptance_tests[testIndex] = fixedTest;
118
+ if (batch && batchSessionId) {
119
+ await saveHuBatch(batchSessionId, batch);
120
+ }
121
+
122
+ if (typeof savePlanPatch === "function") {
123
+ try {
124
+ await savePlanPatch(story.id, { acceptance_tests: story.acceptance_tests });
125
+ } catch (err) {
126
+ logger.warn(`Repairer: in-memory + batch fixed but plan-patch failed: ${err.message}`);
127
+ }
128
+ }
129
+
130
+ logger.info(`Repairer: HU ${story.id} test #${testIndex} REPAIRED — ${reason}`);
131
+ emitProgress(emitter, makeEvent("repair:end", { ...eventBase, stage: "repair" }, {
132
+ status: "ok",
133
+ message: `Repairer fixed HU ${story.id} test #${testIndex}: ${reason}`,
134
+ detail: {
135
+ huId: story.id,
136
+ testIndex,
137
+ reason,
138
+ fixedContent: typeof fixedTest === "object" ? fixedTest.content : fixedTest,
139
+ },
140
+ }));
141
+ return { repaired: true, reason };
142
+ }
@@ -1,6 +1,6 @@
1
1
  import { ReviewerRole } from "../roles/reviewer-role.js";
2
2
  import { createAgent } from "../agents/index.js";
3
- import { addCheckpoint } from "../session-store.js";
3
+ import { addCheckpoint } from "../session/store.js";
4
4
 
5
5
  export async function runReviewerWithFallback({ reviewerName, config, logger, emitter, reviewInput, session, iteration, onAttemptResult }) {
6
6
  const fallbackReviewer = config.reviewer_options?.fallback_reviewer;
@@ -1,5 +1,6 @@
1
1
  import { SolomonRole } from "../roles/solomon-role.js";
2
- import { addCheckpoint, pauseSession, saveSession } from "../session-store.js";
2
+ import { addCheckpoint, pauseSession, saveSession } from "../session/store.js";
3
+ import { setSuggestions } from "../session/mutators.js";
3
4
  import { emitProgress, makeEvent } from "../utils/events.js";
4
5
  import { msg, getLang } from "../utils/messages.js";
5
6
 
@@ -158,7 +159,7 @@ export async function invokeSolomon({ config, logger, emitter, eventBase, stage,
158
159
 
159
160
  // Clear processed suggestions from session
160
161
  if (pendingSuggestions) {
161
- session.suggestions = [];
162
+ setSuggestions(session, []);
162
163
  try { await saveSession(session); } catch { /* best-effort */ }
163
164
  }
164
165
 
@@ -118,11 +118,15 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
118
118
 
119
119
  // Count files changed via git
120
120
  try {
121
- const { execaCommand } = await import("execa");
121
+ // Audit follow-up: was using execaCommand with template-string
122
+ // interpolation of `baseRef`. baseRef comes from session state, not
123
+ // user input, but the shape is a known shell-injection vector.
124
+ // Switched to execa with arg arrays so no shell expansion happens.
125
+ const { execa } = await import("execa");
122
126
  const baseRef = session.session_start_sha || "HEAD~1";
123
127
 
124
128
  // Files changed
125
- const diffResult = await execaCommand(`git diff --name-only ${baseRef}`, { reject: false });
129
+ const diffResult = await execa("git", ["diff", "--name-only", String(baseRef)], { reject: false });
126
130
  if (diffResult.stdout) {
127
131
  const files = diffResult.stdout.split("\n").filter(Boolean);
128
132
  context.filesChanged = files.length;
@@ -142,7 +146,7 @@ export async function buildRulesContext({ session, task, iteration, blockingIssu
142
146
  // Detect new dependencies
143
147
  if (files.includes("package.json")) {
144
148
  try {
145
- const pkgDiff = await execaCommand(`git diff ${baseRef} -- package.json`, { reject: false });
149
+ const pkgDiff = await execa("git", ["diff", String(baseRef), "--", "package.json"], { reject: false });
146
150
  const addedDeps = (pkgDiff.stdout || "").split("\n")
147
151
  .filter(line => line.startsWith("+") && line.includes('"') && !line.startsWith("+++"))
148
152
  .map(line => {
@@ -5,10 +5,10 @@
5
5
 
6
6
  import { ArchitectRole } from "../../roles/architect-role.js";
7
7
  import { createAgent } from "../../agents/index.js";
8
- import { createArchitectADRs } from "../../planning-game/architect-adrs.js";
9
- import { addCheckpoint } from "../../session-store.js";
8
+ import { addCheckpoint } from "../../session/store.js";
10
9
  import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
11
10
  import { createStallDetector } from "../../utils/stall-detector.js";
11
+ import { getIntegration } from "../integrations.js";
12
12
 
13
13
  async function handleArchitectClarification({ architectOutput, askQuestion, config, logger, emitter, eventBase, session, architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget }) {
14
14
  if (!architectOutput.ok
@@ -140,9 +140,10 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
140
140
 
141
141
  const architectContext = architectOutput.ok ? architectOutput.result : null;
142
142
 
143
- // TODO: Move ADR creation to planning-game/pipeline-adapter.js (PG coupling still here because
144
- // stageResult.adrs is consumed synchronously within runArchitectStage's return value).
145
- // Generate ADRs from architect tradeoffs when PG is linked
143
+ // TSK-0339: ADR creation is now a tracker hook. The orchestrator ships the
144
+ // tradeoffs; the registered adapter decides whether/how to persist them
145
+ // (PG creates ADR cards, another tracker might create Jira issues, an
146
+ // unregistered tracker is a no-op).
146
147
  const tradeoffs = architectOutput.result?.architecture?.tradeoffs;
147
148
  if (architectOutput.ok
148
149
  && architectOutput.result?.verdict === "ready"
@@ -150,17 +151,17 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
150
151
  && session.pg_task_id
151
152
  && session.pg_project_id) {
152
153
  try {
153
- const pgClient = await import("../../planning-game/client.js");
154
- const adrResult = await createArchitectADRs({
154
+ const adrResult = await getIntegration("tracker")?.createAdrsFromTradeoffs?.({
155
155
  tradeoffs,
156
156
  pgTaskId: session.pg_task_id,
157
157
  pgProject: session.pg_project_id,
158
158
  taskTitle: session.task,
159
- mcpClient: pgClient
160
159
  });
161
- stageResult.adrs = adrResult;
162
- if (adrResult.created > 0) {
163
- logger.info(`Architect: created ${adrResult.created} ADR(s) in Planning Game`);
160
+ if (adrResult) {
161
+ stageResult.adrs = adrResult;
162
+ if (adrResult.created > 0) {
163
+ logger.info(`Architect: created ${adrResult.created} ADR(s) in tracker`);
164
+ }
164
165
  }
165
166
  } catch (err) {
166
167
  logger.warn(`Architect: failed to create ADRs: ${err.message}`);
@@ -6,7 +6,10 @@
6
6
  import { createAgent } from "../../agents/index.js";
7
7
  import { CoderRole } from "../../roles/coder-role.js";
8
8
  import { RefactorerRole } from "../../roles/refactorer-role.js";
9
- import { addCheckpoint, markSessionStatus, saveSession } from "../../session-store.js";
9
+ import { addCheckpoint, markSessionStatus, saveSession } from "../../session/store.js";
10
+ import {
11
+ setReviewerFeedback, incrementRetryCount, resetRetryCount,
12
+ } from "../../session/mutators.js";
10
13
  import { generateDiff, getUntrackedFiles } from "../../review/diff-generator.js";
11
14
  import { evaluateTddPolicy } from "../../review/tdd-policy.js";
12
15
  import { buildDeferredContext } from "../../review/scope-filter.js";
@@ -15,8 +18,9 @@ import { runCoderWithFallback } from "../agent-fallback.js";
15
18
  import { invokeSolomon } from "../solomon-escalation.js";
16
19
  import { detectRateLimit } from "../../utils/rate-limit-detector.js";
17
20
  import { createStallDetector } from "../../utils/stall-detector.js";
21
+ import { snapshotHomeTopLevel, detectNewHomeEntries, formatLeakMessage, verifyLeaksAgainstTranscript } from "../fs-leak-detector.js";
18
22
 
19
- export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx }) {
23
+ export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null }) {
20
24
  logger.setContext({ iteration, stage: "coder" });
21
25
  emitProgress(
22
26
  emitter,
@@ -42,6 +46,12 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
42
46
  onOutput: coderOnOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
43
47
  });
44
48
  const coderStart = Date.now();
49
+ // KJC-BUG-0032 (PR-I): snapshot $HOME's top-level entries BEFORE
50
+ // the coder runs so we can detect leaks afterwards. The bug: a HU
51
+ // titled "Initialize project skeleton: create assistant/…" had
52
+ // Claude run `cd /home/manu/assistant && pnpm init …` and 36 MB
53
+ // of code landed outside projectDir, outside any git repo.
54
+ const homeSnapshotBeforeCoder = snapshotHomeTopLevel();
45
55
  let coderExecResult;
46
56
  try {
47
57
  coderExecResult = await coderRoleInstance.execute({
@@ -49,6 +59,13 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
49
59
  reviewerFeedback,
50
60
  sonarSummary: session.last_sonar_summary,
51
61
  deferredContext: buildDeferredContext(session.deferred_issues),
62
+ // Tests-first contract flows through to buildCoderPrompt which
63
+ // renders the "Acceptance Tests — MUST pass" section.
64
+ acceptanceTests,
65
+ // PR F (v2.7.5): plan-aware context. ADRs apply to every HU on
66
+ // the plan, the rest are scoped to this HU. All four pass through
67
+ // untouched when the caller omits them (legacy single-task runs).
68
+ adrs, specSection, reviewerFindings, huId,
52
69
  onOutput: coderStall.onOutput
53
70
  });
54
71
  } finally {
@@ -129,6 +146,38 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
129
146
  throw new Error(`Coder failed: ${details}`);
130
147
  }
131
148
 
149
+ // KJC-BUG-0032 (PR-I): scan $HOME for new top-level entries the
150
+ // coder created OUTSIDE projectDir. Stop the HU loudly with a
151
+ // plain-Spanish message instead of letting the user discover by
152
+ // accident hours later that 36 MB of code is in the wrong place.
153
+ // Runs BEFORE the success-path verification so a leaked HU doesn't
154
+ // get marked as "Coder applied changes".
155
+ const projectDirAbs = config.projectDir || process.cwd();
156
+ const candidateLeaks = detectNewHomeEntries(homeSnapshotBeforeCoder, projectDirAbs);
157
+ // Issue #546: snapshot-diff alone produces false positives when other
158
+ // processes write to $HOME during the iteration window. Filter the
159
+ // detected entries against the coder's transcript: only flag those
160
+ // the coder demonstrably referenced.
161
+ const coderTranscript = coderExecResult.result?.output || "";
162
+ const fsLeaks = verifyLeaksAgainstTranscript(candidateLeaks, coderTranscript);
163
+ if (candidateLeaks.length > 0 && fsLeaks.length === 0) {
164
+ logger.warn(`fs-leak-detector: ${candidateLeaks.length} new $HOME entr(y/ies) detected but none referenced in the coder transcript — likely a concurrent host-side write, not flagging (#546).`);
165
+ }
166
+ if (fsLeaks.length > 0) {
167
+ const msg = formatLeakMessage(fsLeaks, projectDirAbs);
168
+ logger.error(msg);
169
+ emitProgress(
170
+ emitter,
171
+ makeEvent("coder:fs-leak", { ...eventBase, stage: "coder" }, {
172
+ status: "fail",
173
+ message: `Coder escribió fuera del projectDir: ${fsLeaks.length} ruta(s) detectada(s)`,
174
+ detail: { provider: coderRole.provider, leaks: fsLeaks, projectDir: projectDirAbs }
175
+ })
176
+ );
177
+ await markSessionStatus(session, "failed");
178
+ throw new Error(msg);
179
+ }
180
+
132
181
  // Measure files changed so stale detection (solomon-rules) has accurate data
133
182
  let filesChanged = 0;
134
183
  try {
@@ -252,7 +301,7 @@ function handleSolomonAction(solomonResult, session, contextPrefix) {
252
301
  async function handleSolomonContinue(solomonResult, session, counterField, brainCtx) {
253
302
  if (solomonResult.action !== "continue") return false;
254
303
  if (solomonResult.humanGuidance) {
255
- session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
304
+ setReviewerFeedback(session, `${session.last_reviewer_feedback ?? ""}\nUser guidance: ${solomonResult.humanGuidance}`);
256
305
  // Brain: also push user guidance into feedback queue when enabled
257
306
  if (brainCtx?.enabled) {
258
307
  const { processRoleOutput } = await import("../brain-coordinator.js");
@@ -265,13 +314,13 @@ async function handleSolomonContinue(solomonResult, session, counterField, brain
265
314
  }
266
315
 
267
316
  async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion, task, brainCtx }) {
268
- session.last_reviewer_feedback = tddEval.message;
317
+ setReviewerFeedback(session, tddEval.message);
269
318
  // Brain: push TDD failure into feedback queue when enabled
270
319
  if (brainCtx?.enabled) {
271
320
  const { processRoleOutput } = await import("../brain-coordinator.js");
272
321
  processRoleOutput(brainCtx, { roleName: "tdd", output: { verdict: "fail", summary: tddEval.message }, iteration });
273
322
  }
274
- session.repeated_issue_count += 1;
323
+ incrementRetryCount(session, "repeated_issue");
275
324
  await saveSession(session);
276
325
 
277
326
  if (session.repeated_issue_count < config.session.fail_fast_repeats) {
@@ -285,7 +334,7 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
285
334
  message: `TDD sub-loop limit reached (${session.repeated_issue_count}/${config.session.fail_fast_repeats}) — Brain handling`,
286
335
  detail: { subloop: "tdd", retryCount: session.repeated_issue_count, reason: tddEval.reason }
287
336
  }));
288
- session.repeated_issue_count = 0;
337
+ resetRetryCount(session, "repeated_issue");
289
338
  await saveSession(session);
290
339
  return { action: "continue" };
291
340
  }
@@ -320,7 +369,7 @@ async function handleTddFailure({ tddEval, config, logger, emitter, eventBase, s
320
369
  return { action: "continue" };
321
370
  }
322
371
 
323
- export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration, askQuestion, task, brainCtx }) {
372
+ export async function runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget: _trackBudget, iteration, askQuestion, task, brainCtx }) {
324
373
  logger.setContext({ iteration, stage: "tdd" });
325
374
  let tddDiff, untrackedFiles;
326
375
  try {