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.
- package/README.md +1 -1
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/audit/basal-cost.js +92 -9
- package/src/audit/deterministic-summary.js +200 -0
- package/src/audit/osv-findings.js +162 -0
- package/src/audit/semgrep-findings.js +117 -0
- package/src/audit/sonar-findings.js +81 -0
- package/src/audit/webperf-input.js +90 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +3 -3
- package/src/checks/ci.js +2 -2
- package/src/checks/config-files.js +6 -6
- package/src/checks/dir-setup.js +4 -0
- package/src/checks/mcp-health.js +6 -0
- package/src/checks/ports.js +91 -29
- package/src/checks/rtk.js +1 -1
- package/src/checks/skills.js +5 -1
- package/src/checks/sonar.js +2 -2
- package/src/checks/system.js +3 -3
- package/src/checks/tokens.js +1 -1
- package/src/checks/types.js +0 -12
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +218 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +265 -30
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/doctor.js +1 -1
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +78 -0
- package/src/config.js +30 -632
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +11 -11
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +7 -3
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +301 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +431 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/feedback-queue.js +0 -1
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2077
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +81 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +20 -7
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/role-output-compressor.js +0 -1
- package/src/orchestrator/session-journal.js +4 -41
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +84 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -17
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -28
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +37 -6
- package/src/prompts/audit.js +206 -6
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/prompts/rtk-snippet.js +1 -1
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/audit-role.js +125 -13
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +2 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/display/event-handlers.js +1 -1
- package/src/utils/display/header.js +1 -1
- package/src/utils/display/session-summary.js +3 -3
- package/src/utils/display/solomon.js +1 -1
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/role-assigner.js +1 -1
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -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
|
-
*
|
|
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
|
-
|
|
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
|
|
56
|
-
const
|
|
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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
|
|
181
|
-
const { config } = await
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
64
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/mcp/preflight.js
CHANGED
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
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
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
];
|
|
@@ -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
|
|
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
|
|
33
|
-
for (const c of pushResult.commits)
|
|
34
|
+
const tracker = getIntegration("tracker");
|
|
35
|
+
for (const c of pushResult.commits) tracker?.onCommit?.(session, c);
|
|
34
36
|
|
|
35
|
-
|
|
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
|
|
52
|
-
for (const c of earlyPr.commits)
|
|
53
|
+
const tracker = getIntegration("tracker");
|
|
54
|
+
for (const c of earlyPr.commits) tracker?.onCommit?.(session, c);
|
|
53
55
|
|
|
54
|
-
session.
|
|
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}`,
|
|
@@ -86,7 +86,7 @@ export async function handleCiEarlyPrOrPush({ ciEnabled, config, session, emitte
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
function formatBlockingIssues(issues) {
|
|
90
90
|
return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
91
91
|
}
|
|
92
92
|
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
32
|
-
|
|
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
|
|
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 {
|
|
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
|
-
|
|
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"],
|
|
@@ -186,4 +190,4 @@ export function getAllowedActionTypes() {
|
|
|
186
190
|
return Object.keys(ACTION_HANDLERS);
|
|
187
191
|
}
|
|
188
192
|
|
|
189
|
-
export {
|
|
193
|
+
export { isCommandAllowed };
|