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.
Files changed (165) 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/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 +85 -31
  18. package/src/checks/sonar.js +1 -1
  19. package/src/checks/system.js +1 -1
  20. package/src/cli/_shared.js +30 -0
  21. package/src/cli/register-meta.js +204 -0
  22. package/src/cli/register-pipeline.js +212 -0
  23. package/src/cli/register-plan.js +152 -0
  24. package/src/cli/register-roles-skills.js +38 -0
  25. package/src/cli/register-sonar.js +28 -0
  26. package/src/cli.js +83 -468
  27. package/src/commands/agents.js +18 -6
  28. package/src/commands/architect.js +29 -2
  29. package/src/commands/audit.js +27 -3
  30. package/src/commands/board.js +160 -30
  31. package/src/commands/clean.js +83 -0
  32. package/src/commands/code.js +7 -2
  33. package/src/commands/discover.js +7 -2
  34. package/src/commands/init.js +2 -2
  35. package/src/commands/plan/_shared.js +40 -0
  36. package/src/commands/plan/add-hu.js +21 -0
  37. package/src/commands/plan/delete.js +9 -0
  38. package/src/commands/plan/generate.js +284 -0
  39. package/src/commands/plan/index.js +14 -0
  40. package/src/commands/plan/list.js +21 -0
  41. package/src/commands/plan/ready.js +46 -0
  42. package/src/commands/plan/remove-hu.js +17 -0
  43. package/src/commands/plan/show.js +81 -0
  44. package/src/commands/plan/validate.js +20 -0
  45. package/src/commands/plan.js +14 -291
  46. package/src/commands/researcher.js +7 -2
  47. package/src/commands/resume.js +24 -1
  48. package/src/commands/review.js +7 -2
  49. package/src/commands/run.js +52 -1
  50. package/src/commands/status.js +1 -1
  51. package/src/commands/sync.js +75 -0
  52. package/src/commands/triage.js +7 -2
  53. package/src/commands/undo.js +1 -1
  54. package/src/config/defaults.js +213 -0
  55. package/src/config/loader.js +137 -0
  56. package/src/config/overrides.js +199 -0
  57. package/src/config/role-resolver.js +159 -0
  58. package/src/config/test-harness.js +79 -0
  59. package/src/config.js +30 -632
  60. package/src/git/automation.js +2 -2
  61. package/src/guards/output-guard.js +0 -2
  62. package/src/hu/acceptance-runner.js +52 -10
  63. package/src/hu/store.js +11 -0
  64. package/src/mcp/handlers/direct-handlers.js +2 -5
  65. package/src/mcp/handlers/management-handlers.js +48 -4
  66. package/src/mcp/handlers/run-handler.js +9 -8
  67. package/src/mcp/preflight.js +32 -13
  68. package/src/mcp/server-handlers.js +4 -3
  69. package/src/mcp/suggest-handler.js +3 -2
  70. package/src/mcp/tools.js +15 -0
  71. package/src/orchestrator/agent-fallback.js +1 -1
  72. package/src/orchestrator/ci-integration.js +10 -10
  73. package/src/orchestrator/config-init.js +25 -7
  74. package/src/orchestrator/direct-actions.js +6 -2
  75. package/src/orchestrator/drivers/error-recovery.js +287 -0
  76. package/src/orchestrator/drivers/init-context.js +222 -0
  77. package/src/orchestrator/drivers/iteration-loop.js +311 -0
  78. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  79. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  80. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  81. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  82. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  83. package/src/orchestrator/drivers/post-loop.js +370 -0
  84. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  85. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  86. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  87. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  88. package/src/orchestrator/drivers/pre-loop.js +434 -0
  89. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  90. package/src/orchestrator/flow-control.js +4 -3
  91. package/src/orchestrator/flow-runner.js +121 -2077
  92. package/src/orchestrator/fs-leak-detector.js +156 -0
  93. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  94. package/src/orchestrator/integrations.js +88 -0
  95. package/src/orchestrator/post-loop-stages.js +35 -8
  96. package/src/orchestrator/preflight-checks.js +20 -7
  97. package/src/orchestrator/repair/repair-detector.js +117 -0
  98. package/src/orchestrator/repair/repair-runner.js +142 -0
  99. package/src/orchestrator/reviewer-fallback.js +1 -1
  100. package/src/orchestrator/solomon-escalation.js +3 -2
  101. package/src/orchestrator/solomon-rules.js +7 -3
  102. package/src/orchestrator/stages/architect-stage.js +12 -11
  103. package/src/orchestrator/stages/coder-stage.js +56 -7
  104. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  105. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  106. package/src/orchestrator/stages/planner-stage.js +1 -1
  107. package/src/orchestrator/stages/research-stage.js +1 -1
  108. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  109. package/src/orchestrator/stages/sonar-stage.js +94 -10
  110. package/src/orchestrator/stages/stage-classes.js +86 -0
  111. package/src/orchestrator/stages/stage-executor.js +1 -1
  112. package/src/orchestrator/stages/triage-stage.js +1 -1
  113. package/src/orchestrator/standby.js +3 -2
  114. package/src/orchestrator/verification-gate.js +20 -12
  115. package/src/plan/adr-generator.js +170 -0
  116. package/src/plan/adr-loader.js +60 -0
  117. package/src/plan/plan-executor.js +19 -1
  118. package/src/plan/plan-hu-ops.js +170 -1
  119. package/src/plan/plan-reviewer.js +199 -0
  120. package/src/plan/plan-schema.js +69 -1
  121. package/src/plan/plan-store.js +40 -3
  122. package/src/plan/spec-detector.js +61 -0
  123. package/src/plan/tests-synthesizer.js +116 -0
  124. package/src/planning-game/client.js +3 -14
  125. package/src/planning-game/decomposition.js +1 -1
  126. package/src/planning-game/pipeline-adapter.js +36 -5
  127. package/src/prompts/coder.js +172 -1
  128. package/src/prompts/hu-reviewer.js +0 -1
  129. package/src/prompts/planner.js +81 -4
  130. package/src/review/diff-generator.js +35 -22
  131. package/src/review/snapshot-diff.js +1 -1
  132. package/src/roles/coder-role.js +25 -3
  133. package/src/roles/planner-role.js +42 -1
  134. package/src/roles/repairer-role.js +181 -0
  135. package/src/roles/tester-role.js +70 -4
  136. package/src/session/mutators.js +304 -0
  137. package/src/session/runtime-overrides.js +59 -0
  138. package/src/{session-store.js → session/store.js} +2 -2
  139. package/src/skills/semantic-detector.js +10 -5
  140. package/src/skills/skill-detector.js +1 -1
  141. package/src/sonar/cloud-scanner.js +0 -1
  142. package/src/sonar/credentials.js +14 -6
  143. package/src/sonar/discovery.js +123 -0
  144. package/src/sonar/manager.js +18 -0
  145. package/src/sync/detect-drift.js +113 -0
  146. package/src/tracker-bootstrap.js +44 -0
  147. package/src/utils/agent-detect.js +15 -11
  148. package/src/utils/board-prompt-bridge.js +109 -0
  149. package/src/utils/cli-progress.js +161 -0
  150. package/src/utils/cwd-confirm.js +39 -0
  151. package/src/utils/derive-project-name-from-cwd.js +119 -0
  152. package/src/utils/garbage-collector.js +396 -0
  153. package/src/utils/git.js +16 -4
  154. package/src/utils/locale.js +1 -1
  155. package/src/utils/paths.js +21 -1
  156. package/src/utils/process.js +1 -1
  157. package/src/utils/prompt-project-name.js +35 -0
  158. package/src/utils/run-context.js +75 -0
  159. package/src/utils/run-log.js +12 -0
  160. package/src/utils/task-file.js +1 -1
  161. package/src/utils/telemetry.js +13 -1
  162. package/src/webperf/devtools-detect.js +1 -1
  163. package/templates/coder-rules.md +13 -0
  164. package/templates/roles/coder.md +34 -0
  165. package/templates/roles/repairer.md +94 -0
@@ -37,35 +37,77 @@ async function runSingleTest(cmd, cwd, timeoutMs = 30000) {
37
37
 
38
38
  /**
39
39
  * Run all acceptance tests for an HU.
40
- * @param {string[]} tests - Array of shell commands
40
+ *
41
+ * Accepts two shapes:
42
+ * - Legacy: plain string shell command.
43
+ * - v2.7.5: { type: "shell" | "gherkin", content, file? }
44
+ *
45
+ * Shell entries execute immediately (exit code 0 = pass). Gherkin entries
46
+ * cannot be executed directly — that's Phase 3 of tests-first where the
47
+ * tester role translates them into real code tests. Until then, we report
48
+ * them as "pending" and skip rather than failing the whole gate.
49
+ *
50
+ * @param {Array<string|object>} tests - Array of shell commands OR structured entries
41
51
  * @param {string} cwd - Working directory
42
- * @returns {Promise<{allPassed: boolean, results: object[], summary: string, diagnostics: string|null}>}
52
+ * @returns {Promise<{allPassed: boolean, results: object[], summary: string, diagnostics: string|null, pending: number}>}
43
53
  */
44
54
  export async function runAcceptanceTests(tests, cwd) {
45
55
  if (!tests || tests.length === 0) {
46
- return { allPassed: false, results: [], summary: "No acceptance tests defined", diagnostics: null };
56
+ return { allPassed: false, results: [], summary: "No acceptance tests defined", diagnostics: null, pending: 0 };
47
57
  }
48
58
 
49
59
  const results = [];
50
- for (const cmd of tests) {
60
+ let pending = 0;
61
+ for (const entry of tests) {
62
+ let type = "shell";
63
+ let cmd;
64
+ if (typeof entry === "string") {
65
+ cmd = entry;
66
+ } else if (entry && typeof entry === "object" && typeof entry.content === "string") {
67
+ type = entry.type === "gherkin" ? "gherkin" : "shell";
68
+ cmd = entry.content;
69
+ } else {
70
+ results.push({ cmd: JSON.stringify(entry), passed: false, output: "Malformed acceptance_test entry", exitCode: -2, type: "invalid" });
71
+ continue;
72
+ }
73
+ if (type === "gherkin") {
74
+ // Gherkin entries need a translator (Phase 3). Mark them pending
75
+ // so the coder/tester sees they haven't been validated yet —
76
+ // better than silently passing (would lie) or hard-failing
77
+ // (would block the whole pipeline).
78
+ pending += 1;
79
+ results.push({
80
+ cmd,
81
+ passed: false,
82
+ output: "Gherkin test not yet executable (awaits tester translation — Phase 3)",
83
+ exitCode: 0,
84
+ type: "gherkin",
85
+ pending: true,
86
+ });
87
+ continue;
88
+ }
51
89
  const result = await runSingleTest(cmd, cwd);
52
- results.push(result);
90
+ results.push({ ...result, type: "shell" });
53
91
  }
54
92
 
55
- const passed = results.filter(r => r.passed);
56
- const failed = results.filter(r => !r.passed);
93
+ const executedResults = results.filter((r) => !r.pending);
94
+ const passed = executedResults.filter((r) => r.passed);
95
+ const failed = executedResults.filter((r) => !r.passed);
57
96
  const allPassed = failed.length === 0;
58
97
 
59
- const summary = `${passed.length}/${results.length} acceptance tests passed`;
98
+ const shellSummary = `${passed.length}/${executedResults.length} shell tests passed`;
99
+ const summary = pending > 0
100
+ ? `${shellSummary}, ${pending} gherkin test(s) pending translation`
101
+ : shellSummary;
60
102
 
61
103
  let diagnostics = null;
62
104
  if (!allPassed) {
63
- diagnostics = failed.map(f =>
105
+ diagnostics = failed.map((f) =>
64
106
  `FAIL: ${f.cmd}\n exit=${f.exitCode}\n output: ${f.output.trim().split("\n").slice(-5).join("\n ")}`
65
107
  ).join("\n\n");
66
108
  }
67
109
 
68
- return { allPassed, results, summary, diagnostics };
110
+ return { allPassed, results, summary, diagnostics, pending };
69
111
  }
70
112
 
71
113
  /**
package/src/hu/store.js CHANGED
@@ -80,12 +80,23 @@ export async function loadHuBatch(sessionId) {
80
80
 
81
81
  /**
82
82
  * Save a batch back to disk.
83
+ *
84
+ * Ensures the target directory exists before writing — the previous
85
+ * implementation assumed createHuBatch had already done the mkdir,
86
+ * which holds for the auto-HU path but NOT for plan-driven runs:
87
+ * `kj run --plan <id>` derives a batchSessionId from the plan id,
88
+ * loads the (non-existent) batch.json, falls back to in-memory, then
89
+ * crashes here with ENOENT on the first save attempt because no one
90
+ * created `~/.karajan/hu-stories/<sessionId>/`. The fix is one
91
+ * mkdir -p; cheap and idempotent.
92
+ *
83
93
  * @param {string} sessionId - The session identifier.
84
94
  * @param {object} batch - The batch object to persist.
85
95
  * @returns {Promise<void>}
86
96
  */
87
97
  export async function saveHuBatch(sessionId, batch) {
88
98
  const dir = path.join(getHuDir(), sessionId);
99
+ await fs.mkdir(dir, { recursive: true });
89
100
  batch.updated_at = new Date().toISOString();
90
101
  await fs.writeFile(path.join(dir, "batch.json"), JSON.stringify(batch, null, 2));
91
102
  }
@@ -3,7 +3,6 @@
3
3
  * Extracted from server-handlers.js for maintainability.
4
4
  */
5
5
 
6
- import { EventEmitter } from "node:events";
7
6
  import fs from "node:fs/promises";
8
7
  import { resolveRole } from "../../config.js";
9
8
  import { createLogger } from "../../utils/logger.js";
@@ -28,6 +27,7 @@ import {
28
27
  assertNotOnBaseBranch,
29
28
  buildConfig,
30
29
  buildDirectEmitter,
30
+ ensureTaskFromFile,
31
31
  } from "../shared-helpers.js";
32
32
 
33
33
  /**
@@ -56,7 +56,6 @@ export async function handlePlanDirect(a, server, extra) {
56
56
  await assertAgentsAvailable([plannerRole.provider]);
57
57
 
58
58
  const projectDir = await resolveProjectDir(server, a.projectDir);
59
- const { ensureTaskFromFile } = await import("../shared-helpers.js");
60
59
  await ensureTaskFromFile(a, projectDir);
61
60
  const runLog = createRunLog(projectDir);
62
61
  const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
@@ -154,7 +153,7 @@ export async function handlePlanDirect(a, server, extra) {
154
153
  sendTrackerLog(server, "planner", "running", plannerRole.provider);
155
154
  runLog.logText(`[planner] agent launched, waiting for response...`);
156
155
  let result;
157
- let plannerStats = null;
156
+ let plannerStats;
158
157
  try {
159
158
  result = await planner.runTask({
160
159
  prompt,
@@ -212,7 +211,6 @@ export async function handleCodeDirect(a, server, extra) {
212
211
  await assertAgentsAvailable([coderRole.provider]);
213
212
 
214
213
  const projectDir = await resolveProjectDir(server, a.projectDir);
215
- const { ensureTaskFromFile } = await import("../shared-helpers.js");
216
214
  await ensureTaskFromFile(a, projectDir);
217
215
  const runLog = createRunLog(projectDir);
218
216
  runLog.logText(`[kj_code] started — provider=${coderRole.provider}`);
@@ -265,7 +263,6 @@ export async function handleReviewDirect(a, server, extra) {
265
263
  await assertAgentsAvailable([reviewerRole.provider]);
266
264
 
267
265
  const projectDir = await resolveProjectDir(server, a.projectDir);
268
- const { ensureTaskFromFile } = await import("../shared-helpers.js");
269
266
  await ensureTaskFromFile(a, projectDir);
270
267
  const runLog = createRunLog(projectDir);
271
268
  runLog.logText(`[kj_review] started — provider=${reviewerRole.provider}`);
@@ -6,7 +6,7 @@
6
6
  import { runKjCommand } from "../run-kj.js";
7
7
  import { loadConfig } from "../../config.js";
8
8
  import { readRunLog } from "../../utils/run-log.js";
9
- import { isPreflightAcked, ackPreflight, getSessionOverrides } from "../preflight.js";
9
+ import { ackPreflight, getSessionOverrides } from "../preflight.js";
10
10
  import { ensureBootstrap } from "../../bootstrap.js";
11
11
  import {
12
12
  resolveProjectDir,
@@ -15,7 +15,7 @@ import {
15
15
  buildConfig,
16
16
  } from "../shared-helpers.js";
17
17
  import { buildDashboardJson } from "../../utils/status-dashboard.js";
18
- import { loadMostRecentSession } from "../../session-store.js";
18
+ import { loadMostRecentSession } from "../../session/store.js";
19
19
 
20
20
  const AGENT_ROLES = new Set(["coder", "reviewer", "tester", "security", "solomon"]);
21
21
 
@@ -177,8 +177,8 @@ export async function handleScan(a, server) {
177
177
 
178
178
  export async function handleBoard(a) {
179
179
  const action = a.action || "status";
180
- const { loadConfig: lc } = await import("../../config.js");
181
- const { config } = await lc();
180
+ // TSK-0340: loadConfig is already statically imported at the top.
181
+ const { config } = await loadConfig();
182
182
  const port = a.port || config.hu_board?.port || 4000;
183
183
  const { startBoard, stopBoard, boardStatus } = await import("../../commands/board.js");
184
184
  switch (action) {
@@ -201,6 +201,50 @@ export async function handleUndo(a, server) {
201
201
  }
202
202
  }
203
203
 
204
+ /**
205
+ * Garbage-collect stale Karajan state. Routed here (not through the
206
+ * generic `runKjCommand`) because the user needs a structured result
207
+ * payload: MCP clients (Claude Desktop, Cursor, …) render lists, not
208
+ * ANSI-styled stdout. Shared implementation lives in
209
+ * `src/utils/garbage-collector.js` so CLI and MCP stay in sync.
210
+ */
211
+ export async function handleClean(a) {
212
+ const { runManualGC, nukeBoardDb, summarizeGC } = await import("../../utils/garbage-collector.js");
213
+
214
+ const dryRun = !a?.yes;
215
+ const retention = a?.nuke
216
+ ? { planRetentionDays: 0, draftRetentionDays: 0, sessionRetentionDays: 0, huRetentionDays: 0 }
217
+ : {
218
+ planRetentionDays: a?.planDays,
219
+ draftRetentionDays: a?.draftDays,
220
+ sessionRetentionDays: a?.sessionDays,
221
+ huRetentionDays: a?.huDays,
222
+ };
223
+
224
+ const result = await runManualGC({ ...retention, dryRun });
225
+ if (a?.nuke) {
226
+ const boardResult = await nukeBoardDb({ dryRun });
227
+ result.removed.push(...boardResult.removed);
228
+ result.bytesFreed += boardResult.bytesFreed;
229
+ result.errors.push(...boardResult.errors);
230
+ }
231
+
232
+ const byKind = {};
233
+ for (const r of result.removed) byKind[r.kind] = (byKind[r.kind] || 0) + 1;
234
+
235
+ return {
236
+ ok: true,
237
+ dryRun,
238
+ nuke: Boolean(a?.nuke),
239
+ removed: result.removed.length,
240
+ bytesFreed: result.bytesFreed,
241
+ breakdown: byKind,
242
+ items: result.removed.map((r) => ({ path: r.path, kind: r.kind, reason: r.reason })),
243
+ errors: result.errors,
244
+ summary: summarizeGC(result) || "[clean] nothing to clean — ~/.kj/ and ~/.karajan/ are tidy.",
245
+ };
246
+ }
247
+
204
248
  export async function buildPreflightRequiredResponse(toolName) {
205
249
  const { config } = await loadConfig();
206
250
  const { listAgents } = await import("../../commands/agents.js");
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { EventEmitter } from "node:events";
7
7
  import { runFlow, resumeFlow } from "../../orchestrator.js";
8
- import { loadConfig, applyRunOverrides, validateConfig, resolveRole } from "../../config.js";
8
+ import { loadConfig, resolveRole } from "../../config.js";
9
9
  import { createLogger } from "../../utils/logger.js";
10
10
  import { assertAgentsAvailable } from "../../agents/availability.js";
11
11
  import { validatePolicyCompliance } from "../../guards/policy-guard.js";
@@ -21,7 +21,7 @@ import {
21
21
  assertNotOnBaseBranch,
22
22
  buildConfig,
23
23
  buildAskQuestion,
24
- enrichedFailPayload,
24
+ ensureTaskFromFile,
25
25
  } from "../shared-helpers.js";
26
26
 
27
27
  const MAX_AUTO_RESUMES = 2;
@@ -34,7 +34,7 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
34
34
  if (NON_RECOVERABLE_CATEGORIES.has(category)) return null;
35
35
 
36
36
  // Find session ID from most recent session file
37
- const { loadMostRecentSession } = await import("../../session-store.js");
37
+ const { loadMostRecentSession } = await import("../../session/store.js");
38
38
  let session;
39
39
  try {
40
40
  session = await loadMostRecentSession();
@@ -60,8 +60,9 @@ async function attemptAutoResume({ err, config, logger, emitter, askQuestion, ru
60
60
  });
61
61
 
62
62
  // Increment counter and save before resuming
63
- const { saveSession } = await import("../../session-store.js");
64
- session.auto_resume_count = autoResumeCount + 1;
63
+ const { saveSession } = await import("../../session/store.js");
64
+ const { setRetryCount } = await import("../../session/mutators.js");
65
+ setRetryCount(session, "auto_resume", autoResumeCount + 1);
65
66
  await saveSession(session);
66
67
 
67
68
  try {
@@ -251,7 +252,7 @@ export async function handleResume(a, server, extra) {
251
252
  // Load session to get pause context for policy-aware validation
252
253
  let pauseContext;
253
254
  try {
254
- const { loadSession } = await import("../../session-store.js");
255
+ const { loadSession } = await import("../../session/store.js");
255
256
  const session = await loadSession(a.sessionId);
256
257
  const ctx = session?.paused_state?.context;
257
258
  pauseContext = typeof ctx === "string" ? ctx : ctx?.reason;
@@ -267,9 +268,9 @@ export async function handleResume(a, server, extra) {
267
268
  }
268
269
 
269
270
  export async function handleRun(a, server, extra) {
270
- const { ensureTaskFromFile, resolveProjectDir: _rpd } = await import("../shared-helpers.js");
271
+ // TSK-0340: was `await import(...)` — both helpers now come from the static import block.
271
272
  try {
272
- await ensureTaskFromFile(a, a.projectDir || (await _rpd(server, a.projectDir).catch(() => null)));
273
+ await ensureTaskFromFile(a, a.projectDir || (await resolveProjectDir(server, a.projectDir).catch(() => null)));
273
274
  } catch (err) {
274
275
  return failPayload(`taskFile read failed: ${err.message}`);
275
276
  }
@@ -1,9 +1,28 @@
1
1
  /**
2
- * Session-scoped preflight state.
3
- * Lives in memory — dies when the MCP server restarts.
2
+ * MCP-scoped preflight state (handshake flag + session overrides facade).
3
+ *
4
+ * Two pieces of state:
5
+ * - `preflightAcked`: whether the user/host has completed the
6
+ * `kj_preflight` handshake in the current MCP session. Stays local
7
+ * because it is strictly MCP-protocol state (no CLI equivalent).
8
+ * - Session overrides: since v2.7.5 these live in
9
+ * `src/session/runtime-overrides.js` — a neutral module that both
10
+ * CLI commands and MCP handlers can depend on without creating a
11
+ * layer violation. This file re-exports the same functions under
12
+ * their historical names for back-compat with existing MCP handlers.
13
+ *
14
+ * CLI code must NOT import from this file — it should go straight to
15
+ * `src/session/runtime-overrides.js`. Enforced by
16
+ * `tests/architecture/layer-boundaries.test.js`.
4
17
  */
18
+
19
+ import {
20
+ getRuntimeOverrides,
21
+ setRuntimeOverride,
22
+ clearRuntimeOverrides,
23
+ } from "../session/runtime-overrides.js";
24
+
5
25
  let preflightAcked = false;
6
- let sessionOverrides = {};
7
26
 
8
27
  export function isPreflightAcked() {
9
28
  return preflightAcked;
@@ -11,18 +30,18 @@ export function isPreflightAcked() {
11
30
 
12
31
  export function ackPreflight(overrides = {}) {
13
32
  preflightAcked = true;
14
- sessionOverrides = { ...overrides };
15
- }
16
-
17
- export function getSessionOverrides() {
18
- return { ...sessionOverrides };
19
- }
20
-
21
- export function setSessionOverride(key, value) {
22
- sessionOverrides[key] = value;
33
+ clearRuntimeOverrides();
34
+ for (const [k, v] of Object.entries(overrides || {})) {
35
+ setRuntimeOverride(k, v);
36
+ }
23
37
  }
24
38
 
25
39
  export function resetPreflight() {
26
40
  preflightAcked = false;
27
- sessionOverrides = {};
41
+ clearRuntimeOverrides();
28
42
  }
43
+
44
+ // Back-compat aliases for MCP handlers that still import these names.
45
+ // New code should import from `src/session/runtime-overrides.js` directly.
46
+ export const getSessionOverrides = getRuntimeOverrides;
47
+ export const setSessionOverride = setRuntimeOverride;
@@ -22,7 +22,7 @@ export {
22
22
  // ── Sub-module re-exports ────────────────────────────────────────────
23
23
  export { handleRunDirect, handleResumeDirect, validateResumeAnswer, handleRun, handleResume } from "./handlers/run-handler.js";
24
24
  export { handlePlanDirect, handleCodeDirect, handleReviewDirect, handleDiscoverDirect, handleTriageDirect, handleResearcherDirect, handleAuditDirect, handleArchitectDirect, handleCode, handleReview, handlePlan, handleDiscover, handleTriage, handleResearcher, handleArchitect, handleAudit } from "./handlers/direct-handlers.js";
25
- export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, handleUndo, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
25
+ export { handleStatus, handleAgents, handlePreflight, handleRoles, handleReport, handleInit, handleDoctor, handleConfig, handleScan, handleBoard, handleUndo, handleClean, buildPreflightRequiredResponse } from "./handlers/management-handlers.js";
26
26
  export { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
27
27
 
28
28
  // ── Handler dispatch ─────────────────────────────────────────────────
@@ -36,7 +36,7 @@ import {
36
36
  import {
37
37
  handleStatus, handleAgents, handlePreflight, handleRoles,
38
38
  handleReport, handleInit, handleDoctor, handleConfig,
39
- handleScan, handleBoard, handleUndo
39
+ handleScan, handleBoard, handleUndo, handleClean
40
40
  } from "./handlers/management-handlers.js";
41
41
  import { handleHu, handleSkills, handleSuggest } from "./handlers/hu-handlers.js";
42
42
 
@@ -66,7 +66,8 @@ export async function handleToolCall(name, args, server, extra) {
66
66
  kj_hu: (a, server) => handleHu(a, server),
67
67
  kj_suggest: (a) => handleSuggest(a),
68
68
  kj_skills: (a) => handleSkills(a),
69
- kj_undo: (a, server) => handleUndo(a, server)
69
+ kj_undo: (a, server) => handleUndo(a, server),
70
+ kj_clean: (a) => handleClean(a),
70
71
  }[name];
71
72
  if (handler) {
72
73
  return handler(a, server, extra);
@@ -6,6 +6,7 @@
6
6
  import fs from "node:fs/promises";
7
7
  import path from "node:path";
8
8
  import { getSessionRoot } from "../utils/paths.js";
9
+ import { setSuggestions, touch } from "../session/mutators.js";
9
10
 
10
11
  /**
11
12
  * Find the most recent running session for a given projectDir.
@@ -65,7 +66,7 @@ export async function handleSuggestion({ suggestion, context, projectDir }) {
65
66
 
66
67
  // Initialize suggestions array if not present
67
68
  if (!Array.isArray(session.suggestions)) {
68
- session.suggestions = [];
69
+ setSuggestions(session, []);
69
70
  }
70
71
 
71
72
  const entry = {
@@ -79,7 +80,7 @@ export async function handleSuggestion({ suggestion, context, projectDir }) {
79
80
  // Persist the updated session
80
81
  const sessionRoot = getSessionRoot();
81
82
  const sessionFile = path.join(sessionRoot, session.id, "session.json");
82
- session.updated_at = new Date().toISOString();
83
+ touch(session);
83
84
  await fs.writeFile(sessionFile, JSON.stringify(session, null, 2), "utf8");
84
85
 
85
86
  return {
package/src/mcp/tools.js CHANGED
@@ -385,5 +385,20 @@ export const tools = [
385
385
  projectDir: { type: "string", description: "Absolute path to the project directory" }
386
386
  }
387
387
  }
388
+ },
389
+ {
390
+ name: "kj_clean",
391
+ description: "Garbage-collect stale Karajan state: orphan plans (project dir no longer exists), finalised plans/sessions past their retention window, and HU story batches. Dry-run by default; pass yes=true to actually delete. Use nuke=true to wipe everything (retention=0 on every bucket + drop the HU board SQLite DB).",
392
+ inputSchema: {
393
+ type: "object",
394
+ properties: {
395
+ yes: { type: "boolean", description: "If true, actually delete. Default false (dry-run: returns the list of items that would be removed without touching disk)." },
396
+ nuke: { type: "boolean", description: "If true, retention=0 on every bucket + wipe the HU board DB (stops the board first if running). The \"I want it all gone right now\" button." },
397
+ planDays: { type: "number", description: "Keep finalised plans (approved/rejected/executed) for this many days. Default: 30." },
398
+ draftDays: { type: "number", description: "Keep draft plans for this many days. Default: 60." },
399
+ sessionDays: { type: "number", description: "Keep finalised sessions for this many days. Default: 7." },
400
+ huDays: { type: "number", description: "Keep HU story batches for this many days. Default: 14." }
401
+ }
402
+ }
388
403
  }
389
404
  ];
@@ -1,5 +1,5 @@
1
1
  import { createAgent } from "../agents/index.js";
2
- import { addCheckpoint } from "../session-store.js";
2
+ import { addCheckpoint } from "../session/store.js";
3
3
  import { detectRateLimit } from "../utils/rate-limit-detector.js";
4
4
 
5
5
  /**
@@ -2,11 +2,13 @@
2
2
  * CI/CD integration — early PR creation, incremental push, review dispatch.
3
3
  * Extracted from orchestrator.js — self-contained feature, only active when ci.enabled.
4
4
  */
5
- import { saveSession } from "../session-store.js";
5
+ import { saveSession } from "../session/store.js";
6
+ import { setCiEarlyPr, appendCiCommits } from "../session/mutators.js";
6
7
  import { earlyPrCreation, incrementalPush } from "../git/automation.js";
7
8
  import { emitProgress, makeEvent } from "../utils/events.js";
9
+ import { getIntegration } from "./integrations.js";
8
10
 
9
- export async function tryCiComment({ config, session, logger, agent, body }) {
11
+ export async function tryCiComment({ config, session, logger: _logger, agent, body }) {
10
12
  if (!config.ci?.enabled || !session.ci_pr_number) return;
11
13
  try {
12
14
  const { dispatchComment } = await import("../ci/dispatch.js");
@@ -29,10 +31,10 @@ async function ciIncrementalPush({ config, session, gitCtx, task, logger, repo,
29
31
  const pushResult = await incrementalPush({ gitCtx, task, logger, session });
30
32
  if (!pushResult) return;
31
33
 
32
- const { accumulateCommit } = await import("../planning-game/pipeline-adapter.js");
33
- for (const c of pushResult.commits) accumulateCommit(session, c);
34
+ const tracker = getIntegration("tracker");
35
+ for (const c of pushResult.commits) tracker?.onCommit?.(session, c);
34
36
 
35
- session.ci_commits = [...(session.ci_commits ?? []), ...pushResult.commits];
37
+ appendCiCommits(session, pushResult.commits);
36
38
  await saveSession(session);
37
39
 
38
40
  if (!repo) return;
@@ -48,12 +50,10 @@ async function ciCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, ta
48
50
  const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
49
51
  if (!earlyPr) return;
50
52
 
51
- const { accumulateCommit } = await import("../planning-game/pipeline-adapter.js");
52
- for (const c of earlyPr.commits) accumulateCommit(session, c);
53
+ const tracker = getIntegration("tracker");
54
+ for (const c of earlyPr.commits) tracker?.onCommit?.(session, c);
53
55
 
54
- session.ci_pr_number = earlyPr.prNumber;
55
- session.ci_pr_url = earlyPr.prUrl;
56
- session.ci_commits = earlyPr.commits;
56
+ setCiEarlyPr(session, { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl, commits: earlyPr.commits });
57
57
  await saveSession(session);
58
58
  emitProgress(emitter, makeEvent("ci:pr-created", { ...eventBase, stage: "ci" }, {
59
59
  message: `Early PR created: #${earlyPr.prNumber}`,
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
- import { computeBaseRef } from "../review/diff-generator.js";
7
+ import { computeBaseRef, setSnapshot } from "../review/diff-generator.js";
8
8
  import { buildCoderPrompt } from "../prompts/coder.js";
9
9
  import { buildReviewerPrompt } from "../prompts/reviewer.js";
10
10
  import { resolveRole } from "../config.js";
@@ -12,9 +12,11 @@ import { emitProgress, makeEvent } from "../utils/events.js";
12
12
  import { BudgetTracker, extractUsageMetrics } from "../utils/budget.js";
13
13
  import { computeKjComparison } from "../budget/comparison.js";
14
14
  import { resolveRoleMdPath, loadFirstExisting } from "../roles/base-role.js";
15
+ import { projectSlug } from "../plan/plan-store.js";
15
16
  import { applyPolicies } from "../guards/policy-resolver.js";
16
17
  import { resolveReviewProfile } from "../review/profiles.js";
17
- import { createSession } from "../session-store.js";
18
+ import { createSession } from "../session/store.js";
19
+ import { setResolvedPolicies } from "../session/mutators.js";
18
20
  import { exists, ensureDir } from "../utils/fs.js";
19
21
 
20
22
  /**
@@ -26,10 +28,13 @@ export async function autoInit(projectDir, logger) {
26
28
  // Ensure git repo exists — without git, diff/reviewer/commit won't work
27
29
  const gitDir = path.join(projectDir, ".git");
28
30
  if (!(await exists(gitDir))) {
29
- const { execSync } = await import("node:child_process");
31
+ // Audit follow-up: was execSync with constant strings. Inputs are
32
+ // hardcoded, so injection isn't possible, but the project standard
33
+ // since #555 is execFileSync everywhere. Migrated for consistency.
34
+ const { execFileSync } = await import("node:child_process");
30
35
  try {
31
- execSync("git init", { cwd: projectDir, stdio: "pipe" });
32
- execSync("git commit --allow-empty -m 'initial commit'", { cwd: projectDir, stdio: "pipe" });
36
+ execFileSync("git", ["init"], { cwd: projectDir, stdio: "pipe" });
37
+ execFileSync("git", ["commit", "--allow-empty", "-m", "initial commit"], { cwd: projectDir, stdio: "pipe" });
33
38
  logger.info("Initialized git repository with empty initial commit");
34
39
  } catch (err) {
35
40
  logger.warn(`Failed to init git repo: ${err.message}`);
@@ -342,14 +347,27 @@ export async function initializeSession({ task, config, flags, pgTaskId, pgProje
342
347
  const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
343
348
 
344
349
  if (baseRef === "__snapshot__") {
350
+ // TSK-0340: `setSnapshot` now static; `takeSnapshot` kept dynamic because
351
+ // snapshot-diff is ~600 LOC of dependency-free fs walking that most
352
+ // runs don't need (only the greenfield/no-git path reaches here).
345
353
  const { takeSnapshot } = await import("../review/snapshot-diff.js");
346
- const { setSnapshot } = await import("../review/diff-generator.js");
347
354
  const snapshot = await takeSnapshot(config.projectDir || process.cwd());
348
355
  setSnapshot(snapshot);
349
356
  }
350
357
 
358
+ // PR-F: stamp project_id on every session derived from the project
359
+ // directory (slug = same algorithm the board uses to bucket plans).
360
+ // Without this, sessions created by `kj run` from a terminal land
361
+ // on disk with `project_id: null`. The board sync sees an orphan
362
+ // and (used to) promote it to a phantom "Orphan sessions" project.
363
+ // Now that PR-B blocks orphans on the board side, we must also fix
364
+ // the source so the session can attach to a real project.
365
+ const projectDir = config.projectDir || process.cwd();
366
+ const project_id = projectSlug(projectDir);
367
+
351
368
  const sessionInit = {
352
369
  task,
370
+ project_id,
353
371
  config_snapshot: config,
354
372
  base_ref: baseRef,
355
373
  session_start_sha: baseRef,
@@ -422,7 +440,7 @@ export function resolvePipelinePolicies({ flags, config, stageResults, emitter,
422
440
  taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
423
441
  policies: config.policies,
424
442
  });
425
- session.resolved_policies = resolvedPolicies;
443
+ setResolvedPolicies(session, resolvedPolicies);
426
444
 
427
445
  let updatedConfig = config;
428
446
  if (!resolvedPolicies.tdd) {
@@ -1,7 +1,7 @@
1
1
  // Direct actions: commands Karajan Brain can execute without invoking a full role.
2
2
  // Keeps the action catalog small, auditable, and safe.
3
3
 
4
- import { execSync, execFileSync } from "node:child_process";
4
+ import { execFileSync } from "node:child_process";
5
5
  import fs from "node:fs/promises";
6
6
  import path from "node:path";
7
7
 
@@ -48,7 +48,11 @@ async function runCommand({ cmd, cwd }) {
48
48
  }
49
49
 
50
50
  try {
51
- const output = execSync(cmd, {
51
+ // Audit follow-up: was using execSync, which routes through /bin/sh.
52
+ // Even though `cmd` is allow-listed, defense-in-depth: switched to
53
+ // execFileSync with a tokenised arg array so no shell expansion at all.
54
+ const [program, ...args] = cmd.trim().split(/\s+/);
55
+ const output = execFileSync(program, args, {
52
56
  cwd: cwd || process.cwd(),
53
57
  encoding: "utf8",
54
58
  stdio: ["pipe", "pipe", "pipe"],