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
@@ -15,6 +15,10 @@ const REQUIRED_DIRS = [
15
15
  "logs",
16
16
  ];
17
17
 
18
+ /**
19
+ * @internal Exported for dynamic import from tests/checks/dir-setup.test.js
20
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
21
+ */
18
22
  export function createKarajanDirsCheck() {
19
23
  return {
20
24
  name: "karajan-dirs",
@@ -86,6 +86,9 @@ async function pingMcpStdio(command, args) {
86
86
  /**
87
87
  * karajan-mcp health — stdio server bundled with this package.
88
88
  * Strategy: auto (we can re-spawn it ourselves).
89
+ *
90
+ * @internal Exported for dynamic import from tests/checks/mcp-health.test.js.
91
+ * Knip false positive.
89
92
  */
90
93
  export function createKarajanMcpCheck() {
91
94
  return {
@@ -121,6 +124,9 @@ export function createKarajanMcpCheck() {
121
124
  /**
122
125
  * Serena MCP — external tool the user installs themselves. Warn only when
123
126
  * enabled in config. Strategy: manual (we don't manage its lifecycle).
127
+ *
128
+ * @internal Exported for dynamic import from tests/checks/mcp-health.test.js.
129
+ * Knip false positive.
124
130
  */
125
131
  export function createSerenaMcpCheck() {
126
132
  return {
@@ -12,6 +12,8 @@
12
12
 
13
13
  import { isPortAvailable, findAvailablePort } from "../utils/port-check.js";
14
14
  import { getPortOccupant } from "../utils/port-occupant.js";
15
+ import { discoverRunningSonar } from "../sonar/discovery.js";
16
+ import { isSonarReachable } from "../sonar/manager.js";
15
17
  import { STRATEGY } from "./types.js";
16
18
 
17
19
  const DEFAULT_SONAR_PORT = 9000;
@@ -23,62 +25,122 @@ function extractPortFromHost(host, fallback) {
23
25
  }
24
26
 
25
27
  /**
26
- * Whether the occupant looks like the Karajan-managed Sonar container.
27
- * Docker Desktop's proxy is typically `com.docker.backend` / `com.docker.vmnetd`
28
- * on macOS and `docker-proxy` on Linux. We treat those as OK.
28
+ * @internal Exported for dynamic import from tests/checks/ports.test.js
29
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
29
30
  */
30
- function isKarajanSonarOccupant(occupant) {
31
- if (!occupant?.command) return false;
32
- const cmd = occupant.command.toLowerCase();
33
- return cmd.includes("docker") || cmd.includes("sonar");
34
- }
35
-
36
31
  export function createSonarPortCheck() {
37
32
  return {
38
33
  name: "port:sonar",
39
34
  label: "SonarQube port",
40
- strategy: STRATEGY.MANUAL,
41
- // Since v2.7.4 Sonar is intrinsic to Karajan for code tasks, so the
42
- // port check almost always applies. The only legitimate skip is when
43
- // the user runs their own external Sonar instance (`external: true`),
44
- // in which case the port is managed outside Karajan. The legacy
45
- // `sonarqube.enabled` field is ignored (deprecation warning emitted
46
- // at config load time — see emitConfigDeprecations in flow-runner.js).
35
+ // AUTO since v2.7.5 — Karajan now resolves SonarQube discovery on
36
+ // its own. If a Sonar container is running anywhere (any name, any
37
+ // image tag), point Karajan at it. If port 9000 is occupied by
38
+ // someone non-Sonar, rebind to the next free port. Asking the user
39
+ // to "stop the unknown process" was busywork.
40
+ strategy: STRATEGY.AUTO,
47
41
  applies: (config) => {
48
42
  if (config?.sonarqube?.external === true) {
49
43
  return { applies: false, reason: "sonarqube.external is true — port managed outside Karajan" };
50
44
  }
51
45
  return true;
52
46
  },
47
+ describe: "Discover or rebind SonarQube port (no user action needed)",
53
48
  async detect({ config }) {
54
- const port = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
55
- const free = await isPortAvailable(port);
49
+ const configuredPort = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
50
+
51
+ // Step 1 — is there ANY running SonarQube container we can reuse?
52
+ // Container name doesn't matter; we just need a published host port
53
+ // that responds to /api/system/status. This avoids "port 9000 is
54
+ // taken by an unknown process" errors when the unknown process is
55
+ // actually a perfectly good SonarQube from another project.
56
+ const discovered = await discoverRunningSonar();
57
+ if (discovered.found && discovered.reachable) {
58
+ // If the discovered host matches what's already configured,
59
+ // there's nothing to fix — happy path.
60
+ if (discovered.port === configuredPort) {
61
+ return {
62
+ ok: true,
63
+ severity: "info",
64
+ detail: `Reusing running SonarQube container "${discovered.containerName}" on port ${discovered.port}`,
65
+ };
66
+ }
67
+ // Otherwise we want to rewrite the host config at runtime.
68
+ return {
69
+ ok: false,
70
+ severity: "warn",
71
+ detail: `Found running SonarQube container "${discovered.containerName}" on port ${discovered.port} (config points at ${configuredPort})`,
72
+ extra: { remap: { host: discovered.host, port: discovered.port, container: discovered.containerName } },
73
+ };
74
+ }
75
+
76
+ // Step 2 — no running container. Is the configured port even free?
77
+ const free = await isPortAvailable(configuredPort);
56
78
  if (free) {
57
- return { ok: true, severity: "info", detail: `Port ${port} is free (Sonar container not yet running)` };
79
+ return {
80
+ ok: true,
81
+ severity: "info",
82
+ detail: `Port ${configuredPort} is free (Sonar container will be started on demand)`,
83
+ };
58
84
  }
59
- // Busy — is it our container?
60
- const occupant = await getPortOccupant(port);
61
- if (occupant && isKarajanSonarOccupant(occupant)) {
85
+
86
+ // Step 3 — port is busy. Maybe the occupant IS Sonar but not
87
+ // discovered by `docker ps` (rare: bare-metal Sonar, host-network
88
+ // container, etc.). HTTP-probe to find out.
89
+ const probeHost = `http://localhost:${configuredPort}`;
90
+ if (await isSonarReachable(probeHost)) {
62
91
  return {
63
92
  ok: true,
64
93
  severity: "info",
65
- detail: `Port ${port} held by Karajan-managed Sonar (${occupant.command} PID ${occupant.pid})`,
94
+ detail: `Port ${configuredPort} held by a Sonar instance (HTTP /api/system/status responds) — reusing it`,
66
95
  };
67
96
  }
68
- const who = occupant
69
- ? `${occupant.command || "unknown"} (PID ${occupant.pid ?? "?"})`
70
- : "unknown process";
97
+
98
+ // Step 4 — really not Sonar. Find an alternate free port and tell
99
+ // the runtime to use it. The compose template's published port is
100
+ // hardcoded at 9000 so we can't auto-rebind the container itself,
101
+ // but we CAN warn loudly with a clear, fixable message.
102
+ const occupant = await getPortOccupant(configuredPort);
103
+ const alt = await findAvailablePort(configuredPort + 1, 20);
104
+ if (alt) {
105
+ return {
106
+ ok: false,
107
+ severity: "warn",
108
+ detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}). Will rebind Karajan-managed Sonar to ${alt}.`,
109
+ extra: { remap: { host: `http://localhost:${alt}`, port: alt, fromPort: configuredPort } },
110
+ };
111
+ }
112
+ // Last-resort failure (no free port in the next 20).
71
113
  return {
72
114
  ok: false,
73
115
  severity: "fail",
74
- detail: `Port ${port} occupied by ${who}`,
75
- fix: `Stop the process holding port ${port} or change sonarqube.host in kj.config.yml.`,
76
- extra: { port, occupant },
116
+ detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}) and no free port in ${configuredPort + 1}..${configuredPort + 20}`,
117
+ fix: "Stop the conflicting process or set sonarqube.host in kj.config.yml.",
118
+ };
119
+ },
120
+ async remediate({ extra }) {
121
+ if (!extra?.remap) {
122
+ return { fixed: false, detail: "No remap available" };
123
+ }
124
+ const { host, container, fromPort } = extra.remap;
125
+ const detail = container
126
+ ? `Pointed Karajan at running SonarQube ${container} (${host})`
127
+ : `Rebound Karajan-managed Sonar from port ${fromPort} to ${host}`;
128
+ return {
129
+ fixed: true,
130
+ detail,
131
+ // The runner merges this into the runtime config so every
132
+ // downstream Sonar caller (scanner, manager, MCP handler) sees
133
+ // the new host without the user touching kj.config.yml.
134
+ changes: { sonarqube: { host } },
77
135
  };
78
136
  },
79
137
  };
80
138
  }
81
139
 
140
+ /**
141
+ * @internal Exported for dynamic import from tests/checks/ports.test.js
142
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
143
+ */
82
144
  export function createHuBoardPortCheck() {
83
145
  return {
84
146
  name: "port:hu-board",
package/src/checks/rtk.js CHANGED
@@ -8,7 +8,7 @@ import { getInstallCommand } from "../utils/os-detect.js";
8
8
  import { withDocLink } from "../utils/doc-links.js";
9
9
  import { STRATEGY } from "./types.js";
10
10
 
11
- export function createRtkCheck() {
11
+ function createRtkCheck() {
12
12
  return {
13
13
  name: "rtk",
14
14
  label: "RTK (Rust Token Killer)",
@@ -8,10 +8,14 @@ import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
8
8
  import { runCommand } from "../utils/process.js";
9
9
  import { STRATEGY } from "./types.js";
10
10
 
11
- export function skillsEnabled(config) {
11
+ function skillsEnabled(config) {
12
12
  return config?.skills?.enabled !== false;
13
13
  }
14
14
 
15
+ /**
16
+ * @internal Exported for dynamic import from tests/checks/skills.test.js
17
+ * and tests/checks/auto-remediation-e2e.test.js. Knip false positive.
18
+ */
15
19
  export function createOpenSkillsCheck() {
16
20
  return {
17
21
  name: "openskills",
@@ -26,7 +26,7 @@ function sonarHost(config) {
26
26
  * Informative check: Sonar enabled/disabled status.
27
27
  * Produces an OK result with disabled description when Sonar is off.
28
28
  */
29
- export function createSonarStatusCheck() {
29
+ function createSonarStatusCheck() {
30
30
  return {
31
31
  name: "sonarqube",
32
32
  label: "SonarQube",
@@ -36,7 +36,7 @@ export function createSonarStatusCheck() {
36
36
  if (!sonarEnabled(config)) {
37
37
  return { ok: true, severity: "info", detail: "Disabled in config" };
38
38
  }
39
- let reachable = false;
39
+ let reachable;
40
40
  try {
41
41
  reachable = await isSonarReachable(host);
42
42
  } catch {
@@ -14,7 +14,7 @@ function getPackageVersion() {
14
14
  return JSON.parse(readFileSync(pkgPath, "utf8")).version;
15
15
  }
16
16
 
17
- export function createKarajanVersionCheck() {
17
+ function createKarajanVersionCheck() {
18
18
  return {
19
19
  name: "karajan",
20
20
  label: "Karajan Code",
@@ -26,13 +26,13 @@ export function createKarajanVersionCheck() {
26
26
  };
27
27
  }
28
28
 
29
- export function createGitRepoCheck() {
29
+ function createGitRepoCheck() {
30
30
  return {
31
31
  name: "git",
32
32
  label: "Git repository",
33
33
  strategy: STRATEGY.MANUAL,
34
34
  async detect() {
35
- let gitOk = false;
35
+ let gitOk;
36
36
  try {
37
37
  gitOk = await ensureGitRepo();
38
38
  } catch {
@@ -146,4 +146,4 @@ export function getTokenChecks(config) {
146
146
  }
147
147
 
148
148
  // Re-export for tests that need to introspect the mapping.
149
- export { PROVIDER_TO_CLI, normalizeProvider };
149
+ export { normalizeProvider };
@@ -91,15 +91,3 @@ export const STRATEGY = Object.freeze({
91
91
  NONE: "none",
92
92
  });
93
93
 
94
- export const SEVERITY = Object.freeze({
95
- FAIL: "fail",
96
- WARN: "warn",
97
- INFO: "info",
98
- });
99
-
100
- /**
101
- * Whether the final status blocks the pipeline (for preflight).
102
- */
103
- export function isBlocking(status) {
104
- return status === STATUS.FAIL || status === STATUS.TIMEOUT;
105
- }
@@ -0,0 +1,30 @@
1
+ import { applyRunOverrides, loadConfig, validateConfig } from "../config.js";
2
+ import { createLogger } from "../utils/logger.js";
3
+
4
+ /**
5
+ * Wraps a CLI action so it loads + validates config, builds the logger,
6
+ * and fires the anonymous cli_command telemetry event before invoking
7
+ * the real command body. Shared across every register-*.js module so
8
+ * each one doesn't need to re-import the config plumbing.
9
+ *
10
+ * @param {string} pkgVersion Current kj version (used for telemetry).
11
+ * @param {string} commandName The CLI command name being executed.
12
+ * @param {object} flags Flags parsed by Commander.
13
+ * @param {(ctx: { config: object, logger: object, flags: object }) => Promise<any>} fn
14
+ * Body of the command — receives the merged config + logger.
15
+ */
16
+ export async function withConfig(pkgVersion, commandName, flags, fn) {
17
+ const { config } = await loadConfig();
18
+ const merged = applyRunOverrides(config, flags || {});
19
+ validateConfig(merged, commandName);
20
+ const logger = createLogger(merged.output.log_level);
21
+
22
+ // Telemetry: anonymous cli_command event (non-blocking).
23
+ import("../utils/telemetry.js")
24
+ .then(({ sendTelemetryEvent }) =>
25
+ sendTelemetryEvent("cli_command", { command: commandName, version: pkgVersion }, merged)
26
+ )
27
+ .catch(() => {});
28
+
29
+ return fn({ config: merged, logger, flags });
30
+ }
@@ -0,0 +1,218 @@
1
+ import { discoverCommand } from "../commands/discover.js";
2
+ import { triageCommand } from "../commands/triage.js";
3
+ import { researcherCommand } from "../commands/researcher.js";
4
+ import { architectCommand } from "../commands/architect.js";
5
+ import { auditCommand } from "../commands/audit.js";
6
+ import { resumeCommand } from "../commands/resume.js";
7
+ import { boardCommand } from "../commands/board.js";
8
+ import { undoCommand } from "../commands/undo.js";
9
+ import { syncCommand } from "../commands/sync.js";
10
+ import { cleanCommand } from "../commands/clean.js";
11
+ import { withConfig } from "./_shared.js";
12
+
13
+ /**
14
+ * Register the "meta" / single-role / housekeeping commands: pre-pipeline
15
+ * analysis (discover, triage, researcher, architect, audit), session
16
+ * management (resume, board, undo) and maintenance (update, sync, clean).
17
+ *
18
+ * Note: `triage` lives here (not in pipeline) because it's a single-role
19
+ * analysis command in the same family as discover / researcher /
20
+ * architect / audit, none of which run the full agent loop.
21
+ */
22
+ export function registerMeta(program, { pkgVersion }) {
23
+ program
24
+ .command("discover")
25
+ .description("Analyze task for gaps, ambiguities and missing info")
26
+ .argument("[task]", "Task description (or use --task-file)")
27
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
28
+ .option("--mode <name>", "Discovery mode: gaps|momtest|wendel|classify|jtbd", "gaps")
29
+ .option("--discover <name>", "Override discover agent")
30
+ .option("--discover-model <name>", "Override discover model")
31
+ .option("--json", "Output raw JSON")
32
+ .action(async (task, flags) => {
33
+ await withConfig(pkgVersion, "discover", flags, async ({ config, logger }) => {
34
+ const { resolveTaskInput } = await import("../utils/task-file.js");
35
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
36
+ await discoverCommand({ task: resolvedTask, config, logger, mode: flags.mode, json: flags.json });
37
+ });
38
+ });
39
+
40
+ program
41
+ .command("triage")
42
+ .description("Classify task complexity and recommend pipeline roles")
43
+ .argument("[task]", "Task description (or use --task-file)")
44
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
45
+ .option("--triage <name>", "Override triage agent")
46
+ .option("--triage-model <name>", "Override triage model")
47
+ .option("--json", "Output raw JSON")
48
+ .action(async (task, flags) => {
49
+ await withConfig(pkgVersion, "triage", flags, async ({ config, logger }) => {
50
+ const { resolveTaskInput } = await import("../utils/task-file.js");
51
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
52
+ await triageCommand({ task: resolvedTask, config, logger, json: flags.json });
53
+ });
54
+ });
55
+
56
+ program
57
+ .command("researcher")
58
+ .description("Research codebase for a task (files, patterns, constraints)")
59
+ .argument("[task]", "Task description (or use --task-file)")
60
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
61
+ .option("--researcher <name>", "Override researcher agent")
62
+ .option("--researcher-model <name>", "Override researcher model")
63
+ .action(async (task, flags) => {
64
+ await withConfig(pkgVersion, "researcher", flags, async ({ config, logger }) => {
65
+ const { resolveTaskInput } = await import("../utils/task-file.js");
66
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
67
+ await researcherCommand({ task: resolvedTask, config, logger });
68
+ });
69
+ });
70
+
71
+ program
72
+ .command("architect")
73
+ .description("Design solution architecture (layers, patterns, contracts)")
74
+ .argument("[task]", "Task description (or use --task-file)")
75
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
76
+ .option("--architect <name>", "Override architect agent")
77
+ .option("--architect-model <name>", "Override architect model")
78
+ .option("--context <text>", "Additional context (e.g. researcher output)")
79
+ .option("--json", "Output raw JSON")
80
+ .action(async (task, flags) => {
81
+ await withConfig(pkgVersion, "architect", flags, async ({ config, logger }) => {
82
+ const { resolveTaskInput } = await import("../utils/task-file.js");
83
+ const resolvedTask = await resolveTaskInput({ task, taskFile: flags.taskFile, projectDir: config.projectDir, logger });
84
+ await architectCommand({ task: resolvedTask, config, logger, context: flags.context, json: flags.json });
85
+ });
86
+ });
87
+
88
+ program
89
+ .command("audit")
90
+ .description("Analyze codebase health (read-only)")
91
+ .argument("[task]", "Task description. If absent, defaults to a full-codebase analysis. Use --task-file to point at a .md.")
92
+ .option("--task-file <path>", "Read the task from a file (e.g. .md)")
93
+ .option("--dimensions <list>", "Comma-separated: security,quality,performance,architecture,testing,accessibility (default: all; accessibility auto-skipped on detected backend-only projects)", "all")
94
+ .option("--json", "Output raw JSON")
95
+ .option("--agent-readiness", "Score the repo for AI-agent readability (llms.txt, SKILL.md coverage, page token budgets, robots allowlist, heading hierarchy). LLM-free; uses [path] or cwd as the audit target. See issue #542.")
96
+ .option("--path <dir>", "Path to audit (used with --agent-readiness; defaults to cwd)")
97
+ .option("--no-sonar", "Skip the SonarQube findings collector (faster, less context). Sonar findings are also skipped automatically when SonarQube is unreachable.")
98
+ .option("--no-osv", "Skip the OSV-Scanner vulnerability collector. Findings are also skipped automatically when osv-scanner is not installed.")
99
+ .option("--no-semgrep", "Skip the Semgrep SAST collector. Findings are also skipped automatically when semgrep is not installed (install via 'pipx install semgrep' or 'brew install semgrep').")
100
+ .option("--report-file <path>", "Write the audit report to disk in addition to stdout. <path> may be a file (extension drives format: .md or .json) or a directory (creates audit-<ISO>.<md|json> inside). $KJ_AUDIT_REPORT_DIR env var is used as default directory if no --report-file is given.")
101
+ .option("--deterministic-only", "Skip the LLM analysis entirely. Print/persist only the deterministic findings (basalCost, sonar, stack, growth-delta, webperf). Zero tokens spent. Compatible with --report-file and --json.")
102
+ .option("-y, --yes", "Auto-confirm the 'Continue with LLM analysis?' prompt. Useful in scripts that want the full audit non-interactively. CI/non-TTY paths already auto-confirm without this flag.")
103
+ .action(async (task, flags) => {
104
+ await withConfig(pkgVersion, "audit", flags, async ({ config, logger }) => {
105
+ let resolvedTask = task;
106
+ if (flags.taskFile) {
107
+ const { readTaskFile } = await import("../utils/task-file.js");
108
+ resolvedTask = await readTaskFile(flags.taskFile, { projectDir: config.projectDir });
109
+ }
110
+ // commander resolves --no-sonar to flags.sonar=false (negation flag).
111
+ // We translate to the explicit `noSonar: true` shape AuditRole expects.
112
+ await auditCommand({
113
+ task: resolvedTask || "Analyze the full codebase",
114
+ config, logger,
115
+ dimensions: flags.dimensions, json: flags.json,
116
+ agentReadiness: Boolean(flags.agentReadiness),
117
+ path: flags.path,
118
+ noSonar: flags.sonar === false,
119
+ noOsv: flags.osv === false,
120
+ noSemgrep: flags.semgrep === false,
121
+ reportFile: flags.reportFile || null,
122
+ deterministicOnly: Boolean(flags.deterministicOnly),
123
+ yes: Boolean(flags.yes),
124
+ });
125
+ });
126
+ });
127
+
128
+ program
129
+ .command("resume")
130
+ .description("Resume a paused session")
131
+ .argument("<sessionId>")
132
+ .option("--answer <text>", "Answer to the question that caused the pause")
133
+ .option("--json", "Output JSON only")
134
+ .action(async (sessionId, flags) => {
135
+ await withConfig(pkgVersion, "resume", flags, async ({ config, logger }) => {
136
+ await resumeCommand({ sessionId, answer: flags.answer, config, logger, flags });
137
+ });
138
+ });
139
+
140
+ program
141
+ .command("update")
142
+ .description("Update karajan-code to the latest version from npm")
143
+ .action(async () => {
144
+ // Audit follow-up: was execaCommand (shell parsing). Inputs are
145
+ // constants today, but the project standard since #555 is execa
146
+ // with arg arrays (no shell). Migrated for consistency.
147
+ const { execa } = await import("execa");
148
+ console.log(`Current version: ${pkgVersion}`);
149
+ console.log("Checking for updates...");
150
+ try {
151
+ const { stdout } = await execa("npm", ["view", "karajan-code", "version"]);
152
+ const latest = stdout.trim();
153
+ if (latest === pkgVersion) {
154
+ console.log(`Already on the latest version (${pkgVersion}).`);
155
+ return;
156
+ }
157
+ console.log(`Updating ${pkgVersion} → ${latest}...`);
158
+ await execa("npm", ["install", "-g", "karajan-code@latest"], { stdio: "inherit" });
159
+ console.log(`Updated to ${latest}. Restart Claude to pick up the new MCP server.`);
160
+ } catch (err) {
161
+ console.error(`Update failed: ${err.message}`);
162
+ process.exit(1);
163
+ }
164
+ });
165
+
166
+ program
167
+ .command("board [action]")
168
+ .description("Manage HU Board (start|stop|status|open)")
169
+ .option("--port <number>", "Port (default: 4000)", "4000")
170
+ .action(async (action = "start", opts) => {
171
+ await withConfig(pkgVersion, "board", opts, async ({ config, logger }) => {
172
+ const port = Number(opts.port) || config.hu_board?.port || 4000;
173
+ await boardCommand({ action, port, logger });
174
+ });
175
+ });
176
+
177
+ program
178
+ .command("undo")
179
+ .description("Revert last pipeline run")
180
+ .option("--hard", "Discard all changes (default: soft reset, keeps changes staged)")
181
+ .action(async (flags) => {
182
+ await withConfig(pkgVersion, "undo", flags, async ({ logger }) => {
183
+ const result = await undoCommand({ hard: !!flags.hard, logger });
184
+ if (!result.ok) process.exit(1);
185
+ });
186
+ });
187
+
188
+ program
189
+ .command("sync")
190
+ .description("Detect drift between code and the latest plan. Read-only report (issue #540 MVP).")
191
+ .option("--plan <planId>", "Sync against a specific plan instead of the latest")
192
+ .option("--json", "Emit machine-readable JSON instead of human output")
193
+ .action(async (flags) => {
194
+ await withConfig(pkgVersion, "sync", flags, async ({ config, logger }) => {
195
+ await syncCommand({ config, logger, planId: flags.plan, json: flags.json });
196
+ });
197
+ });
198
+
199
+ program
200
+ .command("clean")
201
+ .description("Garbage-collect stale plans, sessions and HU batches (dry-run by default)")
202
+ .option("--yes", "Actually delete — without this flag, only prints what would be removed")
203
+ .option("--nuke", "Retention=0 everywhere + wipe HU board DB. \"I want it all gone\"")
204
+ .option("--plan-days <n>", "Keep finalised plans (approved/rejected/executed) for N days (default: 30)")
205
+ .option("--draft-days <n>", "Keep draft plans for N days (default: 60)")
206
+ .option("--session-days <n>", "Keep finalised sessions for N days (default: 7)")
207
+ .option("--hu-days <n>", "Keep HU story batches for N days (default: 14)")
208
+ .action(async (flags) => {
209
+ await cleanCommand({
210
+ yes: Boolean(flags.yes),
211
+ nuke: Boolean(flags.nuke),
212
+ planDays: flags.planDays ? Number(flags.planDays) : undefined,
213
+ draftDays: flags.draftDays ? Number(flags.draftDays) : undefined,
214
+ sessionDays: flags.sessionDays ? Number(flags.sessionDays) : undefined,
215
+ huDays: flags.huDays ? Number(flags.huDays) : undefined,
216
+ });
217
+ });
218
+ }