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
package/src/checks/dir-setup.js
CHANGED
|
@@ -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",
|
package/src/checks/mcp-health.js
CHANGED
|
@@ -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 {
|
package/src/checks/ports.js
CHANGED
|
@@ -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
|
-
*
|
|
27
|
-
*
|
|
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
|
-
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
|
|
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
|
|
55
|
-
|
|
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 {
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 ${
|
|
94
|
+
detail: `Port ${configuredPort} held by a Sonar instance (HTTP /api/system/status responds) — reusing it`,
|
|
66
95
|
};
|
|
67
96
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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 ${
|
|
75
|
-
fix:
|
|
76
|
-
|
|
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
|
-
|
|
11
|
+
function createRtkCheck() {
|
|
12
12
|
return {
|
|
13
13
|
name: "rtk",
|
|
14
14
|
label: "RTK (Rust Token Killer)",
|
package/src/checks/skills.js
CHANGED
|
@@ -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
|
-
|
|
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",
|
package/src/checks/sonar.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
39
|
+
let reachable;
|
|
40
40
|
try {
|
|
41
41
|
reachable = await isSonarReachable(host);
|
|
42
42
|
} catch {
|
package/src/checks/system.js
CHANGED
|
@@ -14,7 +14,7 @@ function getPackageVersion() {
|
|
|
14
14
|
return JSON.parse(readFileSync(pkgPath, "utf8")).version;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
|
|
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
|
-
|
|
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
|
|
35
|
+
let gitOk;
|
|
36
36
|
try {
|
|
37
37
|
gitOk = await ensureGitRepo();
|
|
38
38
|
} catch {
|
package/src/checks/tokens.js
CHANGED
package/src/checks/types.js
CHANGED
|
@@ -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
|
+
}
|