karajan-code 2.7.4 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/README.md +1 -1
  2. package/package.json +24 -4
  3. package/scripts/diet-baseline.mjs +170 -0
  4. package/scripts/regen-arch-stats.sh +71 -0
  5. package/src/agents/availability.js +15 -9
  6. package/src/agents/claude-agent.js +143 -45
  7. package/src/agents/model-registry.js +1 -1
  8. package/src/audit/agent-readiness.js +240 -0
  9. package/src/audit/basal-cost.js +92 -9
  10. package/src/audit/deterministic-summary.js +200 -0
  11. package/src/audit/osv-findings.js +162 -0
  12. package/src/audit/semgrep-findings.js +117 -0
  13. package/src/audit/sonar-findings.js +81 -0
  14. package/src/audit/webperf-input.js +90 -0
  15. package/src/bootstrap.js +26 -1
  16. package/src/brain/decision-tracker.js +3 -1
  17. package/src/brain/decisor.js +1 -1
  18. package/src/canvas/parse-md.js +238 -0
  19. package/src/canvas/schema.js +101 -0
  20. package/src/canvas/to-flat.js +85 -0
  21. package/src/canvas/validate.js +163 -0
  22. package/src/checks/binaries.js +3 -3
  23. package/src/checks/ci.js +2 -2
  24. package/src/checks/config-files.js +6 -6
  25. package/src/checks/dir-setup.js +4 -0
  26. package/src/checks/mcp-health.js +6 -0
  27. package/src/checks/ports.js +91 -29
  28. package/src/checks/rtk.js +1 -1
  29. package/src/checks/skills.js +5 -1
  30. package/src/checks/sonar.js +2 -2
  31. package/src/checks/system.js +3 -3
  32. package/src/checks/tokens.js +1 -1
  33. package/src/checks/types.js +0 -12
  34. package/src/cli/_shared.js +30 -0
  35. package/src/cli/register-meta.js +218 -0
  36. package/src/cli/register-pipeline.js +212 -0
  37. package/src/cli/register-plan.js +152 -0
  38. package/src/cli/register-roles-skills.js +38 -0
  39. package/src/cli/register-sonar.js +28 -0
  40. package/src/cli.js +83 -468
  41. package/src/commands/agents.js +18 -6
  42. package/src/commands/architect.js +29 -2
  43. package/src/commands/audit.js +265 -30
  44. package/src/commands/board.js +160 -30
  45. package/src/commands/clean.js +83 -0
  46. package/src/commands/code.js +7 -2
  47. package/src/commands/discover.js +7 -2
  48. package/src/commands/doctor.js +1 -1
  49. package/src/commands/init.js +2 -2
  50. package/src/commands/plan/_shared.js +40 -0
  51. package/src/commands/plan/add-hu.js +21 -0
  52. package/src/commands/plan/delete.js +9 -0
  53. package/src/commands/plan/generate.js +284 -0
  54. package/src/commands/plan/index.js +14 -0
  55. package/src/commands/plan/list.js +21 -0
  56. package/src/commands/plan/ready.js +46 -0
  57. package/src/commands/plan/remove-hu.js +17 -0
  58. package/src/commands/plan/show.js +81 -0
  59. package/src/commands/plan/validate.js +20 -0
  60. package/src/commands/plan.js +14 -291
  61. package/src/commands/researcher.js +7 -2
  62. package/src/commands/resume.js +24 -1
  63. package/src/commands/review.js +7 -2
  64. package/src/commands/run.js +52 -1
  65. package/src/commands/status.js +1 -1
  66. package/src/commands/sync.js +75 -0
  67. package/src/commands/triage.js +7 -2
  68. package/src/commands/undo.js +1 -1
  69. package/src/config/defaults.js +213 -0
  70. package/src/config/loader.js +137 -0
  71. package/src/config/overrides.js +199 -0
  72. package/src/config/role-resolver.js +159 -0
  73. package/src/config/test-harness.js +78 -0
  74. package/src/config.js +30 -632
  75. package/src/git/automation.js +2 -2
  76. package/src/guards/output-guard.js +0 -2
  77. package/src/hu/acceptance-runner.js +52 -10
  78. package/src/hu/store.js +11 -0
  79. package/src/mcp/handlers/direct-handlers.js +2 -5
  80. package/src/mcp/handlers/management-handlers.js +48 -4
  81. package/src/mcp/handlers/run-handler.js +9 -8
  82. package/src/mcp/preflight.js +32 -13
  83. package/src/mcp/server-handlers.js +4 -3
  84. package/src/mcp/suggest-handler.js +3 -2
  85. package/src/mcp/tools.js +15 -0
  86. package/src/orchestrator/agent-fallback.js +1 -1
  87. package/src/orchestrator/ci-integration.js +11 -11
  88. package/src/orchestrator/config-init.js +25 -7
  89. package/src/orchestrator/direct-actions.js +7 -3
  90. package/src/orchestrator/drivers/error-recovery.js +287 -0
  91. package/src/orchestrator/drivers/init-context.js +222 -0
  92. package/src/orchestrator/drivers/iteration-loop.js +301 -0
  93. package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
  94. package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
  95. package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
  96. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
  97. package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
  98. package/src/orchestrator/drivers/post-loop.js +370 -0
  99. package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
  100. package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
  101. package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
  102. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
  103. package/src/orchestrator/drivers/pre-loop.js +431 -0
  104. package/src/orchestrator/drivers/run-hu-batch.js +367 -0
  105. package/src/orchestrator/feedback-queue.js +0 -1
  106. package/src/orchestrator/flow-control.js +4 -3
  107. package/src/orchestrator/flow-runner.js +121 -2077
  108. package/src/orchestrator/fs-leak-detector.js +156 -0
  109. package/src/orchestrator/hu-sub-pipeline.js +197 -9
  110. package/src/orchestrator/integrations.js +81 -0
  111. package/src/orchestrator/post-loop-stages.js +35 -8
  112. package/src/orchestrator/preflight-checks.js +20 -7
  113. package/src/orchestrator/repair/repair-detector.js +117 -0
  114. package/src/orchestrator/repair/repair-runner.js +142 -0
  115. package/src/orchestrator/reviewer-fallback.js +1 -1
  116. package/src/orchestrator/role-output-compressor.js +0 -1
  117. package/src/orchestrator/session-journal.js +4 -41
  118. package/src/orchestrator/solomon-escalation.js +3 -2
  119. package/src/orchestrator/solomon-rules.js +7 -3
  120. package/src/orchestrator/stages/architect-stage.js +12 -11
  121. package/src/orchestrator/stages/coder-stage.js +56 -7
  122. package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
  123. package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
  124. package/src/orchestrator/stages/planner-stage.js +1 -1
  125. package/src/orchestrator/stages/research-stage.js +1 -1
  126. package/src/orchestrator/stages/reviewer-stage.js +7 -7
  127. package/src/orchestrator/stages/sonar-stage.js +94 -10
  128. package/src/orchestrator/stages/stage-classes.js +84 -0
  129. package/src/orchestrator/stages/stage-executor.js +1 -1
  130. package/src/orchestrator/stages/triage-stage.js +1 -1
  131. package/src/orchestrator/standby.js +3 -2
  132. package/src/orchestrator/verification-gate.js +20 -12
  133. package/src/plan/adr-generator.js +170 -0
  134. package/src/plan/adr-loader.js +60 -0
  135. package/src/plan/plan-executor.js +19 -1
  136. package/src/plan/plan-hu-ops.js +170 -1
  137. package/src/plan/plan-reviewer.js +199 -0
  138. package/src/plan/plan-schema.js +69 -1
  139. package/src/plan/plan-store.js +40 -17
  140. package/src/plan/spec-detector.js +61 -0
  141. package/src/plan/tests-synthesizer.js +116 -0
  142. package/src/planning-game/client.js +3 -28
  143. package/src/planning-game/decomposition.js +1 -1
  144. package/src/planning-game/pipeline-adapter.js +37 -6
  145. package/src/prompts/audit.js +206 -6
  146. package/src/prompts/coder.js +172 -1
  147. package/src/prompts/hu-reviewer.js +0 -1
  148. package/src/prompts/planner.js +81 -4
  149. package/src/prompts/rtk-snippet.js +1 -1
  150. package/src/review/diff-generator.js +35 -22
  151. package/src/review/snapshot-diff.js +1 -1
  152. package/src/roles/audit-role.js +125 -13
  153. package/src/roles/coder-role.js +25 -3
  154. package/src/roles/planner-role.js +42 -1
  155. package/src/roles/repairer-role.js +181 -0
  156. package/src/roles/tester-role.js +70 -4
  157. package/src/session/mutators.js +304 -0
  158. package/src/session/runtime-overrides.js +59 -0
  159. package/src/{session-store.js → session/store.js} +2 -2
  160. package/src/skills/semantic-detector.js +10 -5
  161. package/src/skills/skill-detector.js +1 -1
  162. package/src/sonar/api.js +1 -1
  163. package/src/sonar/cloud-scanner.js +0 -1
  164. package/src/sonar/credentials.js +14 -6
  165. package/src/sonar/discovery.js +123 -0
  166. package/src/sonar/manager.js +18 -0
  167. package/src/sync/detect-drift.js +113 -0
  168. package/src/tracker-bootstrap.js +44 -0
  169. package/src/utils/agent-detect.js +15 -11
  170. package/src/utils/board-prompt-bridge.js +109 -0
  171. package/src/utils/cli-progress.js +161 -0
  172. package/src/utils/cwd-confirm.js +39 -0
  173. package/src/utils/derive-project-name-from-cwd.js +119 -0
  174. package/src/utils/display/event-handlers.js +1 -1
  175. package/src/utils/display/header.js +1 -1
  176. package/src/utils/display/session-summary.js +3 -3
  177. package/src/utils/display/solomon.js +1 -1
  178. package/src/utils/garbage-collector.js +396 -0
  179. package/src/utils/git.js +16 -4
  180. package/src/utils/locale.js +1 -1
  181. package/src/utils/paths.js +21 -1
  182. package/src/utils/process.js +1 -1
  183. package/src/utils/prompt-project-name.js +35 -0
  184. package/src/utils/role-assigner.js +1 -1
  185. package/src/utils/run-context.js +75 -0
  186. package/src/utils/run-log.js +12 -0
  187. package/src/utils/task-file.js +1 -1
  188. package/src/utils/telemetry.js +13 -1
  189. package/src/webperf/devtools-detect.js +1 -1
  190. package/templates/coder-rules.md +13 -0
  191. package/templates/roles/coder.md +34 -0
  192. package/templates/roles/repairer.md +94 -0
@@ -0,0 +1,83 @@
1
+ /**
2
+ * `kj clean` — manual garbage collector.
3
+ *
4
+ * Shows or deletes accumulated state under `~/.kj/plans/` and
5
+ * `~/.karajan/` (sessions, HU story batches). Dry-run by default so
6
+ * the user can eyeball the plan before committing to it.
7
+ *
8
+ * Usage:
9
+ * kj clean # dry-run, shows what would be removed
10
+ * kj clean --yes # actually delete
11
+ * kj clean --plan-days 60 # keep finalised plans for 60 days instead of 30
12
+ * kj clean --draft-days 90 # keep drafts for 90 days instead of 60
13
+ * kj clean --session-days 14 # keep finalised sessions for 14 days instead of 7
14
+ * kj clean --hu-days 30 # keep HU batches for 30 days instead of 14
15
+ *
16
+ * The auto-GC that runs at the start of every `kj run` / `kj plan`
17
+ * uses the same retention defaults; tweaking them here lets the user
18
+ * sweep more or less aggressively without editing config.
19
+ */
20
+
21
+ import { runManualGC, summarizeGC, nukeBoardDb } from "../utils/garbage-collector.js";
22
+
23
+ /**
24
+ * @param {Object} opts
25
+ * @param {boolean} [opts.yes=false] - when false, dry-run only
26
+ * @param {boolean} [opts.nuke=false] - retention=0 for every bucket + wipe board DB
27
+ * @param {number} [opts.planDays=30]
28
+ * @param {number} [opts.draftDays=60]
29
+ * @param {number} [opts.sessionDays=7]
30
+ * @param {number} [opts.huDays=14]
31
+ * @param {Object} [opts.logger]
32
+ */
33
+ export async function cleanCommand(opts = {}) {
34
+ const dryRun = !opts.yes;
35
+ // --nuke forces retention=0 everywhere so drafts / sessions / plans of
36
+ // any age are swept. Combined with wipeBoardDb this is the "I want it
37
+ // all gone right now" button.
38
+ const retention = opts.nuke
39
+ ? { planRetentionDays: 0, draftRetentionDays: 0, sessionRetentionDays: 0, huRetentionDays: 0 }
40
+ : {
41
+ planRetentionDays: opts.planDays,
42
+ draftRetentionDays: opts.draftDays,
43
+ sessionRetentionDays: opts.sessionDays,
44
+ huRetentionDays: opts.huDays,
45
+ };
46
+ const result = await runManualGC({ ...retention, dryRun });
47
+
48
+ if (opts.nuke) {
49
+ const boardResult = await nukeBoardDb({ dryRun });
50
+ result.removed.push(...boardResult.removed);
51
+ result.bytesFreed += boardResult.bytesFreed;
52
+ result.errors.push(...boardResult.errors);
53
+ }
54
+
55
+ if (!result.removed.length) {
56
+ console.log("[clean] nothing to clean — ~/.kj/ and ~/.karajan/ are tidy.");
57
+ return { ok: true, removed: 0 };
58
+ }
59
+
60
+ const verb = dryRun ? "would remove" : "removed";
61
+ console.log(`[clean] ${verb} ${result.removed.length} item(s):`);
62
+ for (const r of result.removed) {
63
+ console.log(` - ${r.kind.padEnd(10)} ${r.path}`);
64
+ console.log(` ${r.reason}`);
65
+ }
66
+
67
+ const kb = Math.round(result.bytesFreed / 1024);
68
+ if (kb) console.log(`[clean] ${verb} ${kb} KiB total`);
69
+
70
+ if (result.errors.length) {
71
+ console.log(`[clean] ${result.errors.length} error(s) (non-blocking):`);
72
+ for (const e of result.errors) console.log(` - ${e.path}: ${e.error}`);
73
+ }
74
+
75
+ if (dryRun) {
76
+ console.log("");
77
+ console.log("[clean] DRY-RUN — no files touched. Re-run with --yes to actually delete.");
78
+ } else {
79
+ console.log(summarizeGC(result));
80
+ }
81
+
82
+ return { ok: true, removed: result.removed.length, bytesFreed: result.bytesFreed };
83
+ }
@@ -4,6 +4,7 @@ import { assertAgentsAvailable } from "../agents/availability.js";
4
4
  import { buildCoderPrompt } from "../prompts/coder.js";
5
5
  import { resolveRole } from "../config.js";
6
6
  import { withCliRunLog } from "../utils/cli-run-log.js";
7
+ import { createCliProgressReporter } from "../utils/cli-progress.js";
7
8
 
8
9
  export async function codeCommand({ task, config, logger }) {
9
10
  return withCliRunLog("code", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
@@ -21,8 +22,12 @@ export async function codeCommand({ task, config, logger }) {
21
22
  }
22
23
  }
23
24
  const prompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology || "tdd" });
24
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
25
- const result = await coder.runTask({ prompt, onOutput, role: "coder" });
25
+ const progress = createCliProgressReporter({ role: "coder" });
26
+ let result;
27
+ try {
28
+ result = await coder.runTask({ prompt, onOutput: progress.onOutput, role: "coder" });
29
+ progress.finish(result.ok ? "done" : "failed");
30
+ } catch (err) { progress.finish("failed"); throw err; }
26
31
  if (!result.ok) {
27
32
  if (result.error) logger.error(result.error);
28
33
  throw new Error(result.error || result.output || `Coder failed (exit ${result.exitCode})`);
@@ -1,6 +1,7 @@
1
1
  import { createAgent } from "../agents/index.js";
2
2
  import { assertAgentsAvailable } from "../agents/availability.js";
3
3
  import { resolveRole } from "../config.js";
4
+ import { createCliProgressReporter } from "../utils/cli-progress.js";
4
5
  import { buildDiscoverPrompt, parseDiscoverOutput } from "../prompts/discover.js";
5
6
 
6
7
  function formatGaps(gaps, lines) {
@@ -71,8 +72,12 @@ export async function discoverCommand({ task, config, logger, mode, json }) {
71
72
 
72
73
  const agent = createAgent(discoverRole.provider, config, logger);
73
74
  const prompt = buildDiscoverPrompt({ task, mode: mode || "gaps" });
74
- const onOutput = ({ line }) => process.stdout.write(`${line}\n`);
75
- const result = await agent.runTask({ prompt, onOutput, role: "discover" });
75
+ const progress = createCliProgressReporter({ role: "discover" });
76
+ let result;
77
+ try {
78
+ result = await agent.runTask({ prompt, onOutput: progress.onOutput, role: "discover" });
79
+ progress.finish(result.ok ? "done" : "failed");
80
+ } catch (err) { progress.finish("failed"); throw err; }
76
81
 
77
82
  if (!result.ok) {
78
83
  throw new Error(result.error || result.output || "Discover failed");
@@ -77,7 +77,7 @@ function confirmViaTty(describe) {
77
77
  * @param {{ config: Object, checkOnly?: boolean, yes?: boolean, onConfirm?: Function, logger?: Object }} args
78
78
  * @returns {Promise<import("../checks/types.js").RunReport>}
79
79
  */
80
- export async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
80
+ async function runDoctor({ config, checkOnly = false, yes = false, onConfirm, logger }) {
81
81
  const checks = buildChecks(config);
82
82
  return runCheckPipeline(checks, { config }, {
83
83
  mode: checkOnly ? "check-only" : "fix",
@@ -408,10 +408,10 @@ export async function initCommand({ logger, flags = {} }) {
408
408
  // Telemetry: anonymous install event (non-blocking)
409
409
  const { sendTelemetryEvent } = await import("../utils/telemetry.js");
410
410
  const { readFileSync } = await import("node:fs");
411
- const { resolve, dirname } = await import("node:path");
412
411
  const { fileURLToPath } = await import("node:url");
413
412
  try {
414
- const pkgPath = resolve(dirname(fileURLToPath(import.meta.url)), "../../package.json");
413
+ // TSK-0340: use the already-static `path` default import for resolve/dirname.
414
+ const pkgPath = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../package.json");
415
415
  const version = JSON.parse(readFileSync(pkgPath, "utf8")).version;
416
416
  sendTelemetryEvent("install", { version }, config).catch(() => {});
417
417
  } catch { /* non-blocking */ }
@@ -0,0 +1,40 @@
1
+ // ---- Formatting helpers shared across plan sub-commands ----
2
+
3
+ export function formatPlan(plan) {
4
+ const lines = [];
5
+ if (plan.approach) lines.push("## Approach", plan.approach, "");
6
+ if (plan.steps?.length) {
7
+ lines.push("## Steps");
8
+ for (let i = 0; i < plan.steps.length; i++) {
9
+ const step = plan.steps[i];
10
+ const commit = step.commit ? ` → \`${step.commit}\`` : "";
11
+ lines.push(`${i + 1}. ${step.description}${commit}`);
12
+ }
13
+ lines.push("");
14
+ }
15
+ if (plan.risks?.length) {
16
+ lines.push("## Risks");
17
+ for (const risk of plan.risks) lines.push(`- ${risk}`);
18
+ lines.push("");
19
+ }
20
+ if (plan.outOfScope?.length) {
21
+ lines.push("## Out of scope");
22
+ for (const item of plan.outOfScope) lines.push(`- ${item}`);
23
+ lines.push("");
24
+ }
25
+ return lines.join("\n");
26
+ }
27
+
28
+ export function formatHuTable(hus) {
29
+ if (!hus?.length) return " (no HUs)";
30
+ const lines = [];
31
+ const maxTitle = Math.min(50, Math.max(...hus.map(h => h.title?.length || 0)));
32
+ lines.push(` ${"ID".padEnd(30)} ${"Title".padEnd(maxTitle)} ${"Type".padEnd(10)} ${"Status".padEnd(10)} Deps`);
33
+ lines.push(` ${"─".repeat(30)} ${"─".repeat(maxTitle)} ${"─".repeat(10)} ${"─".repeat(10)} ${"─".repeat(15)}`);
34
+ for (const hu of hus) {
35
+ const title = (hu.title || "").slice(0, maxTitle).padEnd(maxTitle);
36
+ const deps = (hu.blocked_by || []).join(", ") || "—";
37
+ lines.push(` ${hu.id.padEnd(30)} ${title} ${(hu.task_type || "sw").padEnd(10)} ${(hu.status || "?").padEnd(10)} ${deps}`);
38
+ }
39
+ return lines.join("\n");
40
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * kj plan add-hu <planId> --title "..." --type sw --deps hu_xxx
3
+ */
4
+ export async function planAddHuCommand({ config, planId, title, type, deps, scope }) {
5
+ const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
6
+ const { addHu } = await import("../../plan/plan-hu-ops.js");
7
+ const projectDir = config.projectDir || process.cwd();
8
+
9
+ const plan = await loadPlan(projectDir, planId);
10
+ if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
11
+
12
+ const hu = addHu(plan, {
13
+ title: title || "New HU",
14
+ task_type: type || "sw",
15
+ scope: scope || null,
16
+ blocked_by: deps ? deps.split(",").map(d => d.trim()) : []
17
+ });
18
+
19
+ await savePlan(projectDir, plan);
20
+ console.log(`Added: ${hu.id} — ${hu.title}`);
21
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * kj plan delete <planId>
3
+ */
4
+ export async function planDeleteCommand({ config, planId }) {
5
+ const { deletePlan } = await import("../../plan/plan-store.js");
6
+ const projectDir = config.projectDir || process.cwd();
7
+ const ok = await deletePlan(projectDir, planId);
8
+ console.log(ok ? `Deleted: ${planId}` : `Not found: ${planId}`);
9
+ }
@@ -0,0 +1,284 @@
1
+ import { createAgent } from "../../agents/index.js";
2
+ import { assertAgentsAvailable } from "../../agents/availability.js";
3
+ import { resolveRole } from "../../config.js";
4
+ import { buildPlannerPrompt } from "../../prompts/planner.js";
5
+ import { parseMaybeJsonString } from "../../review/parser.js";
6
+ import { createCliProgressReporter } from "../../utils/cli-progress.js";
7
+ import { runAutoGC, summarizeGC } from "../../utils/garbage-collector.js";
8
+ import { promptProjectName } from "../../utils/prompt-project-name.js";
9
+ import { deriveProjectNameFromCwd } from "../../utils/derive-project-name-from-cwd.js";
10
+ import { formatPlan, formatHuTable } from "./_shared.js";
11
+
12
+ /**
13
+ * kj plan "task" — generate plan + HUs
14
+ */
15
+ export async function planGenerateCommand({ task, config, logger, json, context, flags = {} }) {
16
+ const { withCliRunLog } = await import("../../utils/cli-run-log.js");
17
+ return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
18
+ return planGenerateImpl({ task, config, logger, json, context, runLog, flags });
19
+ });
20
+ }
21
+
22
+ async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {} }) {
23
+ // Auto-GC: prune orphan plans (project dir gone) + old finalised plans/
24
+ // sessions/HU batches before we start. Silent unless something was
25
+ // removed; one-liner summary on stderr in that case so --json stays
26
+ // untouched. Skipped in test env.
27
+ if (!process.env.VITEST && process.env.NODE_ENV !== "test") {
28
+ try {
29
+ const gcResult = await runAutoGC();
30
+ const line = summarizeGC(gcResult);
31
+ if (line && !json) process.stderr.write(`${line}\n`);
32
+ } catch (err) {
33
+ logger?.warn?.(`Auto-GC failed (non-blocking): ${err.message}`);
34
+ }
35
+ }
36
+
37
+ const plannerRole = resolveRole(config, "planner");
38
+ await assertAgentsAvailable([plannerRole.provider]);
39
+ runLog.logText(`[planner] provider=${plannerRole.provider}`);
40
+
41
+ const planner = createAgent(plannerRole.provider, config, logger);
42
+ const prompt = buildPlannerPrompt({ task, context });
43
+ const silenceTimeoutMs = Number(config?.session?.max_agent_silence_minutes) > 0
44
+ ? Math.round(Number(config.session.max_agent_silence_minutes) * 60 * 1000)
45
+ : undefined;
46
+ const timeoutMs = Number(config?.session?.max_planner_minutes) > 0
47
+ ? Math.round(Number(config.session.max_planner_minutes) * 60 * 1000)
48
+ : undefined;
49
+
50
+ // Live progress to stderr so the user sees what the planner is doing
51
+ // instead of staring at a silent shell for 30 s – 3 min. `--json` mode
52
+ // stays quiet so stdout remains a pure JSON document.
53
+ const progress = createCliProgressReporter({ role: "planner", quiet: Boolean(json) });
54
+ let result;
55
+ try {
56
+ result = await planner.runTask({
57
+ prompt, role: "planner", silenceTimeoutMs, timeoutMs,
58
+ onOutput: progress.onOutput,
59
+ });
60
+ progress.finish(result.ok ? "done" : "failed");
61
+ } catch (err) {
62
+ progress.finish("failed");
63
+ throw err;
64
+ }
65
+
66
+ if (!result.ok) throw new Error(result.error || result.output || "Planner failed");
67
+
68
+ const parsed = parseMaybeJsonString(result.output);
69
+
70
+ // Create v2 plan with HUs from planner steps
71
+ const { createPlanV2 } = await import("../../plan/plan-schema.js");
72
+ const { addHu } = await import("../../plan/plan-hu-ops.js");
73
+ const { savePlan } = await import("../../plan/plan-store.js");
74
+ const { deriveProjectName, classifyTaskType } = await import("../../hu/auto-generator.js");
75
+
76
+ const projectDir = config.projectDir || process.cwd();
77
+ const plan = createPlanV2(task);
78
+ // Default project name resolution, in priority order:
79
+ // 1. cwd signals — package.json "name" → git remote basename →
80
+ // directory basename. This is what the user actually expects:
81
+ // if they ran `git init` in `~/ws_demo/weather-dashboard` and
82
+ // then asked Karajan to plan, the default name should reflect
83
+ // THAT directory, not the words of the SPEC document.
84
+ // 2. Fallback: derive from the task text (legacy heuristic). Used
85
+ // when none of the cwd signals yields anything (e.g. running
86
+ // from a tmp dir, no git, no package.json — typical for
87
+ // ad-hoc planning sessions).
88
+ // The user is still prompted to confirm / override on TTY runs;
89
+ // non-interactive runs (--json, --yes, board-spawned subprocesses)
90
+ // accept the default silently.
91
+ const defaultName = deriveProjectNameFromCwd(projectDir) || deriveProjectName(task);
92
+ const canPrompt = process.stdin.isTTY && process.stdout.isTTY
93
+ && !json && !flags?.yes && flags?.interactive !== false;
94
+ plan.name = canPrompt ? await promptProjectName(defaultName) : defaultName;
95
+ plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
96
+ plan.risks = parsed?.risks || [];
97
+ plan.outOfScope = parsed?.outOfScope || [];
98
+
99
+ // Convert planner steps → HUs, carrying the structured acceptance_tests
100
+ // the LLM was asked to emit. If the LLM fell back to legacy behaviour
101
+ // (omitted tests or returned an empty array) we DO NOT inject a fake
102
+ // suite-wide placeholder — that was the pre-v2.7.5 lie. Instead we
103
+ // stamp `acceptance_tests: []` and the board / CLI flag the HU as
104
+ // "missing test contract" so the user can either regenerate or edit
105
+ // the tests manually before executing.
106
+ const { normaliseAcceptanceTests } = await import("../../plan/plan-schema.js");
107
+ const steps = parsed?.steps || [];
108
+ let prevId = null;
109
+ let stepsMissingTests = 0;
110
+ for (const step of steps) {
111
+ const desc = typeof step === "string" ? step : step.description || step.title || JSON.stringify(step);
112
+ const rawTests = typeof step === "object" ? step.acceptance_tests : null;
113
+ const tests = normaliseAcceptanceTests(rawTests);
114
+ if (tests.length === 0) stepsMissingTests += 1;
115
+ const hu = addHu(plan, {
116
+ title: desc.slice(0, 80),
117
+ task_type: classifyTaskType(desc),
118
+ scope: desc,
119
+ blocked_by: prevId ? [prevId] : [],
120
+ acceptance_tests: tests,
121
+ // Spec mapping (PR C): preserve the planner's section citation
122
+ // so the coder/reviewer / board can show "implements §5.3".
123
+ spec_section: typeof step === "object" && typeof step.spec_section === "string" ? step.spec_section.trim() || null : null,
124
+ });
125
+ prevId = hu.id;
126
+ }
127
+ // Tests-synthesizer pass (v2.7.5): the first planner call asks for
128
+ // acceptance_tests inline, but Claude / Codex frequently skip that
129
+ // sub-instruction on dense specs. If any HU came back without
130
+ // tests, run a focused second pass that ONLY synthesizes tests for
131
+ // those HUs. This is ON by default — the user is paying a CLI
132
+ // subscription, not per-token, so we optimise for plan quality, not
133
+ // round-trip cost. If `--no-tests-synth` (or `--quick`) is set, we
134
+ // skip and warn loudly that `kj run --plan` will refuse to execute.
135
+ const skipSynth = Boolean(flags?.noTestsSynth || flags?.quick);
136
+ if (stepsMissingTests > 0 && !skipSynth) {
137
+ runLog.logText(
138
+ `[planner] ${stepsMissingTests}/${steps.length} HUs missing acceptance_tests — running tests-synthesizer pass`
139
+ );
140
+ progress.onOutput?.({ line: `${stepsMissingTests} HU(s) missing tests — running synthesizer pass`, kind: "text" });
141
+ const { synthesizeMissingTests } = await import("../../plan/tests-synthesizer.js");
142
+ const husNeedingTests = plan.hus
143
+ .filter((h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0)
144
+ .map((h) => ({ id: h.id, title: h.title, scope: h.scope }));
145
+ const synthResult = await synthesizeMissingTests({
146
+ agent: planner,
147
+ task,
148
+ husNeedingTests,
149
+ onOutput: progress.onOutput,
150
+ silenceTimeoutMs,
151
+ timeoutMs,
152
+ });
153
+ if (synthResult.ok) {
154
+ let patched = 0;
155
+ for (const hu of plan.hus) {
156
+ const tests = synthResult.tests[hu.id];
157
+ if (tests && tests.length > 0) {
158
+ hu.acceptance_tests = tests;
159
+ hu.updatedAt = new Date().toISOString();
160
+ patched += 1;
161
+ }
162
+ }
163
+ stepsMissingTests = plan.hus.filter((h) => !h.acceptance_tests?.length).length;
164
+ runLog.logText(
165
+ `[planner] tests-synthesizer patched ${patched}/${husNeedingTests.length} HUs (${stepsMissingTests} still missing)`
166
+ );
167
+ } else {
168
+ runLog.logText(`[planner] tests-synthesizer failed: ${synthResult.error}`);
169
+ }
170
+ }
171
+
172
+ if (stepsMissingTests > 0) {
173
+ runLog.logText(
174
+ `[planner] WARN: ${stepsMissingTests}/${steps.length} HUs still have no acceptance_tests after the synthesizer pass. `
175
+ + `Edit them on the board (✎ Edit) or re-run \`kj plan\`. \`kj run --plan\` will refuse to execute these HUs.`
176
+ );
177
+ }
178
+
179
+ // Plan reviewer pass (PR D). Asks five closed-ended questions
180
+ // about the just-generated plan: missing HUs, missing deps, scope
181
+ // overlap, parallelisable groups, order issues. Output is purely
182
+ // advisory — we surface the findings to the user and they decide
183
+ // whether to apply them. Skipped when --no-plan-review or --quick.
184
+ const skipReview = Boolean(flags?.noPlanReview || flags?.quick);
185
+ if (plan.hus.length > 0 && !skipReview) {
186
+ runLog.logText("[planner] running plan reviewer pass");
187
+ progress.onOutput?.({ line: "Reviewing the plan for gaps / overlaps / order issues", kind: "text" });
188
+ const { reviewPlan, findingsCount } = await import("../../plan/plan-reviewer.js");
189
+ const review = await reviewPlan({
190
+ agent: planner,
191
+ task,
192
+ hus: plan.hus,
193
+ onOutput: progress.onOutput,
194
+ silenceTimeoutMs,
195
+ timeoutMs,
196
+ });
197
+ if (review.ok) {
198
+ plan.review = review.findings;
199
+ const count = findingsCount(review.findings);
200
+ runLog.logText(
201
+ count > 0
202
+ ? `[planner] reviewer surfaced ${count} item(s): ${review.findings.summary || "(see plan.review for details)"}`
203
+ : `[planner] reviewer found nothing actionable: ${review.findings.summary || "OK"}`
204
+ );
205
+ } else {
206
+ runLog.logText(`[planner] reviewer pass failed (non-blocking): ${review.error}`);
207
+ }
208
+ }
209
+
210
+ const planId = await savePlan(projectDir, plan);
211
+ const { plansDir } = await import("../../plan/plan-store.js");
212
+ const path = await import("node:path");
213
+ const planPath = path.join(plansDir(projectDir), `${planId}.json`);
214
+
215
+ runLog.logText(`[planner] finished — hus=${plan.hus.length} plan=${planId}`);
216
+
217
+ if (json) {
218
+ console.log(JSON.stringify(plan, null, 2));
219
+ return { ok: true };
220
+ }
221
+
222
+ if (parsed?.approach) {
223
+ console.log(formatPlan(parsed));
224
+ } else if (plan.approach) {
225
+ console.log(plan.approach);
226
+ }
227
+ console.log(`\n## HUs (${plan.hus.length})`);
228
+ console.log(formatHuTable(plan.hus));
229
+ // Tell the user what just happened and what their two real options are.
230
+ // Pre-v2.7.5 we printed Review / Approve / Execute as three lines, which
231
+ // confused everyone:
232
+ // - "Review" sounded like an action; `kj plan show` is read-only.
233
+ // - "Approve" exposed the internal `kj plan ready` gate that the board
234
+ // workflow now bypasses (the board's "Run plan" button calls
235
+ // `kj run --plan` directly and the tests-first gate is enforced there).
236
+ // Now we just say: inspect, then run. The board is the recommended UI.
237
+ console.log(`\nPlan saved: ${planId} (${plan.hus.length} HUs, status: ${plan.status})`);
238
+ console.log(` → ${planPath}`);
239
+ // Reviewer findings: print a short summary so the user knows
240
+ // at-a-glance whether the plan needs attention. Full details live
241
+ // in `plan.review` and `kj plan show <id>` renders them.
242
+ if (plan.review) {
243
+ const r = plan.review;
244
+ const counts = [
245
+ r.missing_hus?.length ? `${r.missing_hus.length} missing HU(s)` : null,
246
+ r.missing_dependencies?.length ? `${r.missing_dependencies.length} missing dep(s)` : null,
247
+ r.scope_overlaps?.length ? `${r.scope_overlaps.length} overlap(s)` : null,
248
+ r.order_issues?.length ? `${r.order_issues.length} order issue(s)` : null,
249
+ ].filter(Boolean);
250
+ if (counts.length === 0) {
251
+ console.log(`Reviewer: ✓ ${r.summary || "no issues"}`);
252
+ } else {
253
+ console.log(`Reviewer flagged: ${counts.join(", ")} — see \`kj plan show ${planId}\``);
254
+ }
255
+ }
256
+ console.log("");
257
+ console.log(`Inspect: kj plan show ${planId}`);
258
+ console.log(`Run: kj run --plan ${planId} "${task.slice(0, 40)}..."`);
259
+ console.log(` (or click ▶ Run plan on the board, if running)`);
260
+
261
+ // UX boost for HU-bearing plans: auto-start the HU Board (same pattern as
262
+ // `kj run` auto-HU generation) so the user can inspect the 12-HU plan in a
263
+ // browser instead of squinting at the ASCII table. The board reads
264
+ // ~/.kj/plans/ directly, so this plan is visible immediately. Never in
265
+ // vitest / test mode, and never when `--json` is active.
266
+ if (plan.hus.length > 0 && !process.env.VITEST && process.env.NODE_ENV !== "test") {
267
+ try {
268
+ const { startBoard, renderBoardBanner } = await import("../board.js");
269
+ const { projectSlug: slugFor } = await import("../../plan/plan-store.js");
270
+ const boardPort = config?.hu_board?.port ?? 4000;
271
+ // `projectSlug` scopes the board URL to this project's view
272
+ // (`/p/<slug>`) instead of the global "All projects" dashboard. The
273
+ // board UI hides multi-project chrome when it sees that prefix.
274
+ const boardResult = await startBoard(boardPort, { projectSlug: slugFor(projectDir) });
275
+ const status = boardResult.alreadyRunning ? "already running" : "started";
276
+ console.log(renderBoardBanner({ url: boardResult.url, status, projectName: plan.name }));
277
+ } catch (err) {
278
+ // Non-blocking: plan is saved, only the visualiser failed.
279
+ logger?.warn?.(`HU Board auto-start failed (non-blocking): ${err.message}`);
280
+ console.log(`\nVisualize: kj board start (http://localhost:${config?.hu_board?.port ?? 4000})`);
281
+ }
282
+ }
283
+ return { ok: true };
284
+ }
@@ -0,0 +1,14 @@
1
+ // Barrel: re-export all plan sub-command handlers.
2
+ import { planGenerateCommand } from "./generate.js";
3
+
4
+ export { planGenerateCommand } from "./generate.js";
5
+ export { planListCommand } from "./list.js";
6
+ export { planShowCommand } from "./show.js";
7
+ export { planReadyCommand } from "./ready.js";
8
+ export { planValidateCommand } from "./validate.js";
9
+ export { planDeleteCommand } from "./delete.js";
10
+ export { planAddHuCommand } from "./add-hu.js";
11
+ export { planRemoveHuCommand } from "./remove-hu.js";
12
+
13
+ // Keep backward compat — old callers import planCommand
14
+ export const planCommand = planGenerateCommand;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * kj plan list
3
+ */
4
+ export async function planListCommand({ config }) {
5
+ const { listPlans } = await import("../../plan/plan-store.js");
6
+ const projectDir = config.projectDir || process.cwd();
7
+ const plans = await listPlans(projectDir);
8
+
9
+ if (plans.length === 0) {
10
+ console.log("No plans found. Create one with: kj plan \"your task\"");
11
+ return;
12
+ }
13
+
14
+ console.log(`Plans for ${projectDir}:\n`);
15
+ for (const p of plans) {
16
+ const husInfo = p.huCount > 0 ? ` (${p.huCount} HUs)` : "";
17
+ const status = p.status !== "draft" ? ` [${p.status}]` : "";
18
+ console.log(` ${p.planId} ${(p.name || p.task || "").slice(0, 50)}${husInfo}${status}`);
19
+ console.log(` Created: ${p.createdAt}`);
20
+ }
21
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * kj plan ready <planId>
3
+ */
4
+ export async function planReadyCommand({ config, planId }) {
5
+ const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
6
+ const { certifyAllHus } = await import("../../plan/plan-hu-ops.js");
7
+ const { validatePlan } = await import("../../plan/plan-schema.js");
8
+ const projectDir = config.projectDir || process.cwd();
9
+
10
+ const plan = await loadPlan(projectDir, planId);
11
+ if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
12
+
13
+ const validation = validatePlan(plan);
14
+ if (!validation.valid) {
15
+ console.error("Plan validation failed:");
16
+ for (const err of validation.errors) console.error(` - ${err}`);
17
+ process.exitCode = 1;
18
+ return;
19
+ }
20
+
21
+ if (plan.hus.length === 0) {
22
+ console.error("Cannot approve a plan with 0 HUs.");
23
+ process.exitCode = 1;
24
+ return;
25
+ }
26
+
27
+ // Tests-first gate: refuse to certify HUs that have no acceptance_tests.
28
+ // A "ready" plan must have an executable test contract per HU, otherwise
29
+ // the coder has nothing to aim for. Flag each offender, then bail so the
30
+ // user can edit the plan (board ✎ Edit or `kj plan add-hu`) and retry.
31
+ const missing = plan.hus.filter((h) => !Array.isArray(h.acceptance_tests) || h.acceptance_tests.length === 0);
32
+ if (missing.length > 0) {
33
+ console.error(`Cannot mark ready: ${missing.length}/${plan.hus.length} HU(s) have no acceptance_tests:`);
34
+ for (const h of missing) console.error(` - ${h.id} ${(h.title || "").slice(0, 60)}`);
35
+ console.error("");
36
+ console.error("Tests-first flow requires a test contract per HU. Edit each HU on the board");
37
+ console.error("(http://localhost:4000) via the ✎ Edit button, or re-run `kj plan` to regenerate.");
38
+ process.exitCode = 1;
39
+ return;
40
+ }
41
+
42
+ const count = certifyAllHus(plan);
43
+ await savePlan(projectDir, plan);
44
+ console.log(`✅ ${count} HUs certified. Plan status: ready`);
45
+ console.log(`Run: kj run --plan ${planId} "${(plan.task || "").slice(0, 40)}..."`);
46
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * kj plan remove-hu <planId> <huId>
3
+ */
4
+ export async function planRemoveHuCommand({ config, planId, huId }) {
5
+ const { loadPlan, savePlan } = await import("../../plan/plan-store.js");
6
+ const { removeHu } = await import("../../plan/plan-hu-ops.js");
7
+ const projectDir = config.projectDir || process.cwd();
8
+
9
+ const plan = await loadPlan(projectDir, planId);
10
+ if (!plan) { console.error(`Plan not found: ${planId}`); process.exitCode = 1; return; }
11
+
12
+ const ok = removeHu(plan, huId);
13
+ if (!ok) { console.error(`HU not found: ${huId}`); process.exitCode = 1; return; }
14
+
15
+ await savePlan(projectDir, plan);
16
+ console.log(`Removed: ${huId}`);
17
+ }