karajan-code 2.7.3 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- 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/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 +1 -1
- package/src/checks/ports.js +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -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 +27 -3
- 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/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 +79 -0
- package/src/config.js +30 -609
- 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 +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- 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 +311 -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 +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2019
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +30 -3
- 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/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 +86 -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 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- 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} +8 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.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/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/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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas → flat task string adapter.
|
|
3
|
+
*
|
|
4
|
+
* Renders a validated Canvas object as a single markdown blob the
|
|
5
|
+
* existing planner can consume verbatim. Preserves every section
|
|
6
|
+
* with its original heading so downstream steps (planner prompt,
|
|
7
|
+
* coder prompt, plan JSON `task` field) see the same shape.
|
|
8
|
+
*
|
|
9
|
+
* The point: Canvas users get planner-time validation (catching
|
|
10
|
+
* broken acceptance tests before they're persisted) AND benefit from
|
|
11
|
+
* every existing planner / coder pipeline without those pipelines
|
|
12
|
+
* needing Canvas awareness. Norms + Safeguards explicitly bracketed
|
|
13
|
+
* so prompts that scan for them can find them with a regex.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} canvas — output of validateCanvas (.canvas)
|
|
18
|
+
* @returns {string} markdown task ready for the planner
|
|
19
|
+
*/
|
|
20
|
+
export function canvasToTask(canvas) {
|
|
21
|
+
const lines = [`# ${canvas.title}`, ""];
|
|
22
|
+
|
|
23
|
+
// Requirements
|
|
24
|
+
lines.push("## Requirements", canvas.requirements.problem);
|
|
25
|
+
if (canvas.requirements.done && canvas.requirements.done.length > 0) {
|
|
26
|
+
lines.push("", "Definition of done:");
|
|
27
|
+
for (const d of canvas.requirements.done) lines.push(`- ${d}`);
|
|
28
|
+
}
|
|
29
|
+
lines.push("");
|
|
30
|
+
|
|
31
|
+
// Entities (optional)
|
|
32
|
+
if (canvas.entities && canvas.entities.length > 0) {
|
|
33
|
+
lines.push("## Entities");
|
|
34
|
+
for (const e of canvas.entities) {
|
|
35
|
+
const fields = e.fields && e.fields.length > 0 ? ` — fields: ${e.fields.join(", ")}` : "";
|
|
36
|
+
lines.push(`- ${e.name}${fields}${e.notes ? `. ${e.notes}` : ""}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Approach
|
|
42
|
+
lines.push("## Approach", canvas.approach, "");
|
|
43
|
+
|
|
44
|
+
// Structure (optional content)
|
|
45
|
+
lines.push("## Structure");
|
|
46
|
+
if (canvas.structure?.touches?.length > 0) lines.push(`- touches: ${canvas.structure.touches.join(", ")}`);
|
|
47
|
+
if (canvas.structure?.depends_on?.length > 0) lines.push(`- depends_on: ${canvas.structure.depends_on.join(", ")}`);
|
|
48
|
+
if (canvas.structure?.contracts?.length > 0) lines.push(`- contracts: ${canvas.structure.contracts.join(", ")}`);
|
|
49
|
+
lines.push("");
|
|
50
|
+
|
|
51
|
+
// Operations — each becomes one HU. Acceptance tests inline so the
|
|
52
|
+
// planner sees the contract from turn 1.
|
|
53
|
+
lines.push("## Operations");
|
|
54
|
+
canvas.operations.forEach((op, i) => {
|
|
55
|
+
lines.push(`${i + 1}. ${op.title}`);
|
|
56
|
+
if (op.file_hint) lines.push(` file: ${op.file_hint}`);
|
|
57
|
+
if (op.blocked_by && op.blocked_by.length > 0) lines.push(` depends on: ${op.blocked_by.join(", ")}`);
|
|
58
|
+
lines.push(" Acceptance:");
|
|
59
|
+
for (const t of op.acceptance) {
|
|
60
|
+
// Render shell as one-liner, gherkin as fenced block so newlines survive.
|
|
61
|
+
if (t.type === "gherkin") {
|
|
62
|
+
lines.push(" - gherkin:", " ```gherkin", ...t.content.split("\n").map((l) => ` ${l}`), " ```");
|
|
63
|
+
} else {
|
|
64
|
+
lines.push(` - shell: \`${t.content}\``);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
lines.push("");
|
|
69
|
+
|
|
70
|
+
// Norms (optional) — bracketed so prompts can find them.
|
|
71
|
+
if (canvas.norms && canvas.norms.length > 0) {
|
|
72
|
+
lines.push("## Norms (per-task engineering standards — coder MUST follow)");
|
|
73
|
+
for (const n of canvas.norms) lines.push(`- ${n}`);
|
|
74
|
+
lines.push("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Safeguards (optional) — bracketed similarly.
|
|
78
|
+
if (canvas.safeguards && canvas.safeguards.length > 0) {
|
|
79
|
+
lines.push("## Safeguards (non-negotiable invariants — code MUST hold these)");
|
|
80
|
+
for (const s of canvas.safeguards) lines.push(`- ${s}`);
|
|
81
|
+
lines.push("");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return lines.join("\n");
|
|
85
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* REASONS Canvas validator.
|
|
3
|
+
*
|
|
4
|
+
* Schema validation + planner-time sanity checks for acceptance tests.
|
|
5
|
+
* The schema is enforced via Valibot in canvas/schema.js. This module
|
|
6
|
+
* adds the bug-class fix: parse-check every shell acceptance test
|
|
7
|
+
* BEFORE it gets persisted into a plan and BEFORE any LLM is called.
|
|
8
|
+
*
|
|
9
|
+
* Catches the 2026-04-29 incident class:
|
|
10
|
+
* `length as $n | .filesOver300LOC == $n` → exits non-zero with a
|
|
11
|
+
* descriptive error pointing at the offending operation + test
|
|
12
|
+
* index, BEFORE the user has paid for any agent run.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { spawnSync } from "node:child_process";
|
|
16
|
+
import { safeParseCanvas } from "./schema.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Run a single shell acceptance test through `bash -n` (parse-only,
|
|
20
|
+
* doesn't execute) to catch shell-level syntax errors. Then, if the
|
|
21
|
+
* test invokes `jq`, also probe the jq query through `jq -n <query>`
|
|
22
|
+
* with empty input — jq errors there iff the query has bad syntax.
|
|
23
|
+
*
|
|
24
|
+
* Trade-off accepted: bash -n won't catch every command-substitution
|
|
25
|
+
* pitfall (it parses but doesn't expand), and jq -n with empty input
|
|
26
|
+
* may pass queries that fail at runtime against real input. The point
|
|
27
|
+
* is to catch the OBVIOUS bugs cheaply, not to be airtight.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} cmd
|
|
30
|
+
* @returns {{ ok: true } | { ok: false, kind: string, error: string }}
|
|
31
|
+
*/
|
|
32
|
+
export function validateShellTest(cmd) {
|
|
33
|
+
if (typeof cmd !== "string" || cmd.trim().length === 0) {
|
|
34
|
+
return { ok: false, kind: "empty", error: "shell test is empty" };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Layer 1: bash -n parse-check on the whole command.
|
|
38
|
+
const bash = spawnSync("bash", ["-n", "-c", cmd], { encoding: "utf8", timeout: 5000 });
|
|
39
|
+
if (bash.status !== 0) {
|
|
40
|
+
const err = (bash.stderr || "").trim() || "bash -n rejected the command";
|
|
41
|
+
return { ok: false, kind: "shell-syntax", error: err };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Layer 2: every `jq '...'` invocation gets its query parse-checked.
|
|
45
|
+
// Permissive regex: catches `jq -e 'X'`, `jq "X"`, `| jq 'X'`. False
|
|
46
|
+
// negatives are fine — better to skip a real jq than misinterpret a
|
|
47
|
+
// shell-quoting edge case.
|
|
48
|
+
//
|
|
49
|
+
// We distinguish COMPILE errors (the bug we want to catch — invalid
|
|
50
|
+
// syntax, e.g. `length as $n | .field` over an array) from RUNTIME
|
|
51
|
+
// errors (the query is well-formed but fails on null input from
|
|
52
|
+
// `-n`, like `.foo[]` on null). Only compile errors fail validation;
|
|
53
|
+
// runtime errors against synthetic input are expected because real
|
|
54
|
+
// input isn't available at plan-time.
|
|
55
|
+
const jqRe = /\bjq\b\s+(?:-[a-zA-Z]+\s+)*(?:'([^']+)'|"([^"]+)")/g;
|
|
56
|
+
for (const m of cmd.matchAll(jqRe)) {
|
|
57
|
+
const query = m[1] || m[2];
|
|
58
|
+
if (!query) continue;
|
|
59
|
+
|
|
60
|
+
// Static pattern detector for the 2026-04-29 as-binding misuse:
|
|
61
|
+
// `[expr] | length as $X | .field` and similar. The `as $X`
|
|
62
|
+
// binds the value to a name but does NOT change the value
|
|
63
|
+
// flowing through the pipe. The next `.field` is therefore
|
|
64
|
+
// applied to the array tail (or whatever the previous stage
|
|
65
|
+
// returned), not to the original root. Always wrong.
|
|
66
|
+
//
|
|
67
|
+
// Detection: a pipeline whose tail is `length as $X | .X` (or
|
|
68
|
+
// any `<terminal-operator> as $X | .field`) is the smell.
|
|
69
|
+
const asBindingMisuse = /\b(?:length|keys|values|min|max|add|first|last)\s+as\s+\$[A-Za-z_][\w]*\s*\|\s*\.[A-Za-z_]/;
|
|
70
|
+
if (asBindingMisuse.test(query)) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
kind: "jq-syntax",
|
|
74
|
+
error: "Likely as-binding misuse: `length as $n | .field` does not change the value flowing through; `.field` is applied to the previous stage's output, not the root. Use `. as $r | <transform> | $r.field` instead.",
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const jq = spawnSync("jq", ["-n", query], { encoding: "utf8", timeout: 5000 });
|
|
79
|
+
if (jq.status !== 0) {
|
|
80
|
+
const err = (jq.stderr || "").trim() || "jq rejected the query";
|
|
81
|
+
// jq distinguishes compile errors with these tokens. Anything
|
|
82
|
+
// else (e.g. "Cannot index null with string") is a runtime error
|
|
83
|
+
// against the empty input, NOT a syntax bug.
|
|
84
|
+
const isCompileError = /\b(compile error|syntax error|unexpected|jq: error \(at <stdin>:\d+\): ?$)/i.test(err);
|
|
85
|
+
if (isCompileError) {
|
|
86
|
+
return { ok: false, kind: "jq-syntax", error: err };
|
|
87
|
+
}
|
|
88
|
+
// Special case: `length as $n | .field` returns "Cannot index
|
|
89
|
+
// array/number/string with string" on -n input because the as-
|
|
90
|
+
// binding misuse means `.field` is applied to the wrong thing.
|
|
91
|
+
// This is exactly the 2026-04-29 bug class. Treat it as syntax-
|
|
92
|
+
// adjacent: the query is "compilable" but semantically broken
|
|
93
|
+
// by misuse of as-binding.
|
|
94
|
+
if (/Cannot index (array|number|string) with string/i.test(err)) {
|
|
95
|
+
return { ok: false, kind: "jq-syntax", error: err };
|
|
96
|
+
}
|
|
97
|
+
// Otherwise let it pass — runtime error against synthetic input
|
|
98
|
+
// is fine. jq parsed the query successfully.
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return { ok: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Validate every operation's acceptance tests in a parsed Canvas.
|
|
107
|
+
* Returns an array of issues; empty array means everything passed.
|
|
108
|
+
*
|
|
109
|
+
* @param {object} canvas
|
|
110
|
+
* @returns {Array<{ operationIndex: number, operationTitle: string, testIndex: number, type: string, content: string, kind: string, error: string }>}
|
|
111
|
+
*/
|
|
112
|
+
export function validateAcceptanceTests(canvas) {
|
|
113
|
+
const issues = [];
|
|
114
|
+
if (!canvas || !Array.isArray(canvas.operations)) return issues;
|
|
115
|
+
canvas.operations.forEach((op, opIdx) => {
|
|
116
|
+
(op.acceptance || []).forEach((t, tIdx) => {
|
|
117
|
+
if (t.type === "shell") {
|
|
118
|
+
const r = validateShellTest(t.content);
|
|
119
|
+
if (!r.ok) {
|
|
120
|
+
issues.push({
|
|
121
|
+
operationIndex: opIdx,
|
|
122
|
+
operationTitle: op.title,
|
|
123
|
+
testIndex: tIdx,
|
|
124
|
+
type: t.type,
|
|
125
|
+
content: t.content,
|
|
126
|
+
kind: r.kind,
|
|
127
|
+
error: r.error,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
} else if (t.type === "gherkin") {
|
|
131
|
+
// Minimal gherkin sanity: must contain at least one Given AND
|
|
132
|
+
// one Then. When/And/But are optional. This catches "blank
|
|
133
|
+
// gherkin" without trying to parse the language.
|
|
134
|
+
if (!/\bGiven\b/i.test(t.content) || !/\bThen\b/i.test(t.content)) {
|
|
135
|
+
issues.push({
|
|
136
|
+
operationIndex: opIdx,
|
|
137
|
+
operationTitle: op.title,
|
|
138
|
+
testIndex: tIdx,
|
|
139
|
+
type: t.type,
|
|
140
|
+
content: t.content,
|
|
141
|
+
kind: "gherkin-incomplete",
|
|
142
|
+
error: "Gherkin scenario must contain at least one Given and one Then.",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
return issues;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Top-level validator. Runs schema + acceptance-test parse-checks.
|
|
153
|
+
*
|
|
154
|
+
* @param {unknown} canvas
|
|
155
|
+
* @returns {{ valid: true, canvas: object } | { valid: false, schemaIssues?: object[], testIssues?: object[] }}
|
|
156
|
+
*/
|
|
157
|
+
export function validateCanvas(canvas) {
|
|
158
|
+
const schema = safeParseCanvas(canvas);
|
|
159
|
+
if (!schema.success) return { valid: false, schemaIssues: schema.issues };
|
|
160
|
+
const testIssues = validateAcceptanceTests(schema.output);
|
|
161
|
+
if (testIssues.length > 0) return { valid: false, testIssues };
|
|
162
|
+
return { valid: true, canvas: schema.output };
|
|
163
|
+
}
|
package/src/checks/binaries.js
CHANGED
|
@@ -88,7 +88,7 @@ export function createSerenaCheck() {
|
|
|
88
88
|
applies: (config) => config.serena?.enabled === true,
|
|
89
89
|
describe: "Install Serena via: uvx --from git+https://github.com/oraios/serena serena",
|
|
90
90
|
async detect() {
|
|
91
|
-
let ok
|
|
91
|
+
let ok;
|
|
92
92
|
try {
|
|
93
93
|
const res = await runCommand("serena", ["--version"]);
|
|
94
94
|
ok = res.exitCode === 0;
|
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;
|
|
@@ -22,47 +24,110 @@ function extractPortFromHost(host, fallback) {
|
|
|
22
24
|
return m ? Number(m[1]) : fallback;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
/**
|
|
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.
|
|
29
|
-
*/
|
|
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
27
|
export function createSonarPortCheck() {
|
|
37
28
|
return {
|
|
38
29
|
name: "port:sonar",
|
|
39
30
|
label: "SonarQube port",
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
// AUTO since v2.7.5 — Karajan now resolves SonarQube discovery on
|
|
32
|
+
// its own. If a Sonar container is running anywhere (any name, any
|
|
33
|
+
// image tag), point Karajan at it. If port 9000 is occupied by
|
|
34
|
+
// someone non-Sonar, rebind to the next free port. Asking the user
|
|
35
|
+
// to "stop the unknown process" was busywork.
|
|
36
|
+
strategy: STRATEGY.AUTO,
|
|
37
|
+
applies: (config) => {
|
|
38
|
+
if (config?.sonarqube?.external === true) {
|
|
39
|
+
return { applies: false, reason: "sonarqube.external is true — port managed outside Karajan" };
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
},
|
|
43
|
+
describe: "Discover or rebind SonarQube port (no user action needed)",
|
|
42
44
|
async detect({ config }) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
+
const configuredPort = extractPortFromHost(config?.sonarqube?.host, DEFAULT_SONAR_PORT);
|
|
46
|
+
|
|
47
|
+
// Step 1 — is there ANY running SonarQube container we can reuse?
|
|
48
|
+
// Container name doesn't matter; we just need a published host port
|
|
49
|
+
// that responds to /api/system/status. This avoids "port 9000 is
|
|
50
|
+
// taken by an unknown process" errors when the unknown process is
|
|
51
|
+
// actually a perfectly good SonarQube from another project.
|
|
52
|
+
const discovered = await discoverRunningSonar();
|
|
53
|
+
if (discovered.found && discovered.reachable) {
|
|
54
|
+
// If the discovered host matches what's already configured,
|
|
55
|
+
// there's nothing to fix — happy path.
|
|
56
|
+
if (discovered.port === configuredPort) {
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
severity: "info",
|
|
60
|
+
detail: `Reusing running SonarQube container "${discovered.containerName}" on port ${discovered.port}`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
// Otherwise we want to rewrite the host config at runtime.
|
|
64
|
+
return {
|
|
65
|
+
ok: false,
|
|
66
|
+
severity: "warn",
|
|
67
|
+
detail: `Found running SonarQube container "${discovered.containerName}" on port ${discovered.port} (config points at ${configuredPort})`,
|
|
68
|
+
extra: { remap: { host: discovered.host, port: discovered.port, container: discovered.containerName } },
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Step 2 — no running container. Is the configured port even free?
|
|
73
|
+
const free = await isPortAvailable(configuredPort);
|
|
45
74
|
if (free) {
|
|
46
|
-
return {
|
|
75
|
+
return {
|
|
76
|
+
ok: true,
|
|
77
|
+
severity: "info",
|
|
78
|
+
detail: `Port ${configuredPort} is free (Sonar container will be started on demand)`,
|
|
79
|
+
};
|
|
47
80
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
|
|
82
|
+
// Step 3 — port is busy. Maybe the occupant IS Sonar but not
|
|
83
|
+
// discovered by `docker ps` (rare: bare-metal Sonar, host-network
|
|
84
|
+
// container, etc.). HTTP-probe to find out.
|
|
85
|
+
const probeHost = `http://localhost:${configuredPort}`;
|
|
86
|
+
if (await isSonarReachable(probeHost)) {
|
|
51
87
|
return {
|
|
52
88
|
ok: true,
|
|
53
89
|
severity: "info",
|
|
54
|
-
detail: `Port ${
|
|
90
|
+
detail: `Port ${configuredPort} held by a Sonar instance (HTTP /api/system/status responds) — reusing it`,
|
|
55
91
|
};
|
|
56
92
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
93
|
+
|
|
94
|
+
// Step 4 — really not Sonar. Find an alternate free port and tell
|
|
95
|
+
// the runtime to use it. The compose template's published port is
|
|
96
|
+
// hardcoded at 9000 so we can't auto-rebind the container itself,
|
|
97
|
+
// but we CAN warn loudly with a clear, fixable message.
|
|
98
|
+
const occupant = await getPortOccupant(configuredPort);
|
|
99
|
+
const alt = await findAvailablePort(configuredPort + 1, 20);
|
|
100
|
+
if (alt) {
|
|
101
|
+
return {
|
|
102
|
+
ok: false,
|
|
103
|
+
severity: "warn",
|
|
104
|
+
detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}). Will rebind Karajan-managed Sonar to ${alt}.`,
|
|
105
|
+
extra: { remap: { host: `http://localhost:${alt}`, port: alt, fromPort: configuredPort } },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
// Last-resort failure (no free port in the next 20).
|
|
60
109
|
return {
|
|
61
110
|
ok: false,
|
|
62
111
|
severity: "fail",
|
|
63
|
-
detail: `Port ${
|
|
64
|
-
fix:
|
|
65
|
-
|
|
112
|
+
detail: `Port ${configuredPort} occupied by ${occupant?.command || "unknown"} (PID ${occupant?.pid ?? "?"}) and no free port in ${configuredPort + 1}..${configuredPort + 20}`,
|
|
113
|
+
fix: "Stop the conflicting process or set sonarqube.host in kj.config.yml.",
|
|
114
|
+
};
|
|
115
|
+
},
|
|
116
|
+
async remediate({ extra }) {
|
|
117
|
+
if (!extra?.remap) {
|
|
118
|
+
return { fixed: false, detail: "No remap available" };
|
|
119
|
+
}
|
|
120
|
+
const { host, container, fromPort } = extra.remap;
|
|
121
|
+
const detail = container
|
|
122
|
+
? `Pointed Karajan at running SonarQube ${container} (${host})`
|
|
123
|
+
: `Rebound Karajan-managed Sonar from port ${fromPort} to ${host}`;
|
|
124
|
+
return {
|
|
125
|
+
fixed: true,
|
|
126
|
+
detail,
|
|
127
|
+
// The runner merges this into the runtime config so every
|
|
128
|
+
// downstream Sonar caller (scanner, manager, MCP handler) sees
|
|
129
|
+
// the new host without the user touching kj.config.yml.
|
|
130
|
+
changes: { sonarqube: { host } },
|
|
66
131
|
};
|
|
67
132
|
},
|
|
68
133
|
};
|
|
@@ -73,7 +138,12 @@ export function createHuBoardPortCheck() {
|
|
|
73
138
|
name: "port:hu-board",
|
|
74
139
|
label: "HU Board port",
|
|
75
140
|
strategy: STRATEGY.AUTO,
|
|
76
|
-
applies: (config) =>
|
|
141
|
+
applies: (config) => {
|
|
142
|
+
if (config?.hu_board?.enabled === false) {
|
|
143
|
+
return { applies: false, reason: "hu_board.enabled is false in kj.config.yml — HU Board dashboard skipped" };
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
77
147
|
describe: "Rebind HU Board to the next free port",
|
|
78
148
|
async detect({ config }) {
|
|
79
149
|
const port = config?.hu_board?.port || DEFAULT_HU_BOARD_PORT;
|
package/src/checks/runner.js
CHANGED
|
@@ -55,19 +55,35 @@ export async function runChecks(checks, ctx, options = {}) {
|
|
|
55
55
|
const overrides = {};
|
|
56
56
|
|
|
57
57
|
// Build applicable entries (skipped checks noted up front).
|
|
58
|
+
// A check.applies(config) may return:
|
|
59
|
+
// - true / false — boolean form (back-compat).
|
|
60
|
+
// - an object { applies: boolean, reason?: string } — when false, `reason`
|
|
61
|
+
// is shown verbatim so users see WHY (e.g. "sonarqube.enabled is false
|
|
62
|
+
// in kj.config.yml") instead of the opaque "Not applicable" message
|
|
63
|
+
// that confused users debugging their config.
|
|
58
64
|
for (const check of checks) {
|
|
59
|
-
if (typeof check.applies
|
|
60
|
-
entries.push({
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
} else {
|
|
65
|
+
if (typeof check.applies !== "function") {
|
|
66
|
+
entries.push({ check, status: null, detail: null, runMs: 0, detectResult: null, remediated: false });
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const verdict = check.applies(config);
|
|
70
|
+
const isObj = verdict && typeof verdict === "object" && "applies" in verdict;
|
|
71
|
+
const applies = isObj ? Boolean(verdict.applies) : Boolean(verdict);
|
|
72
|
+
if (applies) {
|
|
69
73
|
entries.push({ check, status: null, detail: null, runMs: 0, detectResult: null, remediated: false });
|
|
74
|
+
continue;
|
|
70
75
|
}
|
|
76
|
+
const reason = (isObj && typeof verdict.reason === "string" && verdict.reason.trim())
|
|
77
|
+
? verdict.reason.trim()
|
|
78
|
+
: "Not applicable for current configuration";
|
|
79
|
+
entries.push({
|
|
80
|
+
check,
|
|
81
|
+
status: STATUS.SKIPPED,
|
|
82
|
+
detail: reason,
|
|
83
|
+
runMs: 0,
|
|
84
|
+
detectResult: null,
|
|
85
|
+
remediated: false,
|
|
86
|
+
});
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
// Phase 1: detect in parallel with timeout.
|
package/src/checks/sonar.js
CHANGED
|
@@ -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
package/src/checks/tokens.js
CHANGED
|
@@ -1,41 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Provider CLI availability checks (preflight).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* active role
|
|
4
|
+
* IMPORTANT — Karajan does NOT call provider APIs directly. There is no
|
|
5
|
+
* `@anthropic-ai/sdk`, no `openai`, no `@google/generative-ai` in
|
|
6
|
+
* package.json. Every active role spawns its agent CLI as a subprocess
|
|
7
|
+
* (`claude`, `codex`, `gemini`, `opencode`, `aider`). The CLI handles its
|
|
8
|
+
* own auth, typically via OAuth + on-disk token (Claude Code, Codex CLI,
|
|
9
|
+
* Gemini CLI). The user does NOT need to set ANTHROPIC_API_KEY etc. for
|
|
10
|
+
* Karajan to work.
|
|
7
11
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* Pre-v2.7.4 this module checked for ANTHROPIC_API_KEY / OPENAI_API_KEY /
|
|
13
|
+
* GEMINI_API_KEY env vars and FAILed preflight when missing — a false
|
|
14
|
+
* positive that blocked every user running Karajan as an MCP child of
|
|
15
|
+
* Claude Code (where `apiKeySource: "none"` is the norm). Replaced with a
|
|
16
|
+
* CLI-availability check that mirrors what the orchestrator actually does.
|
|
17
|
+
*
|
|
18
|
+
* GH_TOKEN is still checked when `git.auto_pr=true`, because that flow uses
|
|
19
|
+
* `git push` directly with the env var.
|
|
14
20
|
*/
|
|
15
21
|
|
|
16
22
|
import { checkBinary } from "../utils/agent-detect.js";
|
|
17
|
-
import { getRequiredProviderEnvs,
|
|
23
|
+
import { getRequiredProviderEnvs, normalizeProvider } from "../utils/provider-env.js";
|
|
18
24
|
import { STRATEGY } from "./types.js";
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
|
-
*
|
|
27
|
+
* Map canonical provider name → the CLI binary Karajan actually spawns.
|
|
28
|
+
* Keep in sync with src/agents/index.js.
|
|
22
29
|
*/
|
|
23
|
-
|
|
30
|
+
const PROVIDER_TO_CLI = Object.freeze({
|
|
31
|
+
anthropic: "claude",
|
|
32
|
+
openai: "codex",
|
|
33
|
+
google: "gemini",
|
|
34
|
+
opencode: "opencode",
|
|
35
|
+
aider: "aider",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build a CLI-availability check for one provider.
|
|
40
|
+
*
|
|
41
|
+
* The check runs `<cli> --version`. If the binary is on PATH and exits 0,
|
|
42
|
+
* preflight passes — Karajan can spawn it. If the CLI is missing, preflight
|
|
43
|
+
* fails with the install URL we already track in PROVIDER_ENV.
|
|
44
|
+
*
|
|
45
|
+
* Note: we do NOT verify that the CLI is *authenticated*. Doing so would
|
|
46
|
+
* require provider-specific introspection (`claude doctor`, `codex login
|
|
47
|
+
* status`, ...), each with its own quirks and exit codes. If the CLI is
|
|
48
|
+
* present but not logged in, the agent will fail at the first request with
|
|
49
|
+
* a clear error — and the orchestrator surfaces that error already.
|
|
50
|
+
*/
|
|
51
|
+
function createProviderCliCheck(entry) {
|
|
52
|
+
const cli = PROVIDER_TO_CLI[entry.provider];
|
|
24
53
|
return {
|
|
25
|
-
name: `
|
|
26
|
-
label:
|
|
54
|
+
name: `cli:${entry.provider}`,
|
|
55
|
+
label: `${entry.provider} CLI (${cli})`,
|
|
27
56
|
strategy: STRATEGY.MANUAL,
|
|
28
57
|
async detect() {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
58
|
+
if (!cli) {
|
|
59
|
+
return {
|
|
60
|
+
ok: false,
|
|
61
|
+
severity: "warn",
|
|
62
|
+
detail: `Unknown CLI for provider ${entry.provider}`,
|
|
63
|
+
extra: { provider: entry.provider },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const result = await checkBinary(cli);
|
|
67
|
+
if (result.ok) {
|
|
68
|
+
return {
|
|
69
|
+
ok: true,
|
|
70
|
+
severity: "info",
|
|
71
|
+
detail: `${cli} on PATH (${result.version || "version unknown"})`,
|
|
72
|
+
extra: { provider: entry.provider, cli, version: result.version, path: result.path },
|
|
73
|
+
};
|
|
32
74
|
}
|
|
33
75
|
return {
|
|
34
76
|
ok: false,
|
|
35
77
|
severity: entry.optional ? "warn" : "fail",
|
|
36
|
-
detail: `${
|
|
37
|
-
fix: `
|
|
38
|
-
extra: { provider: entry.provider,
|
|
78
|
+
detail: `${cli} not found on PATH`,
|
|
79
|
+
fix: `Install the ${cli} CLI: see ${entry.howToGetUrl}`,
|
|
80
|
+
extra: { provider: entry.provider, cli, howToGetUrl: entry.howToGetUrl },
|
|
39
81
|
};
|
|
40
82
|
},
|
|
41
83
|
};
|
|
@@ -43,6 +85,8 @@ function createProviderTokenCheck(entry) {
|
|
|
43
85
|
|
|
44
86
|
/**
|
|
45
87
|
* GH token check + auto-remediation via `gh auth login` when it's installed.
|
|
88
|
+
* Kept from the previous version: this one IS legitimate because Karajan
|
|
89
|
+
* uses `gh` (or raw `git push`) directly when auto_pr is on.
|
|
46
90
|
*/
|
|
47
91
|
function createGhTokenCheck() {
|
|
48
92
|
return {
|
|
@@ -82,15 +126,24 @@ function createGhTokenCheck() {
|
|
|
82
126
|
}
|
|
83
127
|
|
|
84
128
|
/**
|
|
85
|
-
* Build the list of token checks for the active config.
|
|
86
|
-
*
|
|
129
|
+
* Build the list of CLI/token checks for the active config.
|
|
130
|
+
* - One CLI availability check per active provider (claude / codex / gemini / ...).
|
|
131
|
+
* - One GH token check, conditional on git.auto_pr.
|
|
132
|
+
*
|
|
133
|
+
* Function name kept as `getTokenChecks` for backward compat with
|
|
134
|
+
* preflight-checks.js, even though the bulk of the checks no longer touch
|
|
135
|
+
* tokens at all. Tracked: rename to `getProviderChecks` in a follow-up
|
|
136
|
+
* once consumers are updated.
|
|
87
137
|
*
|
|
88
138
|
* @param {Object} config
|
|
89
139
|
* @returns {import("./types.js").Check[]}
|
|
90
140
|
*/
|
|
91
141
|
export function getTokenChecks(config) {
|
|
92
142
|
const required = getRequiredProviderEnvs(config);
|
|
93
|
-
const checks = required.map(
|
|
143
|
+
const checks = required.map(createProviderCliCheck);
|
|
94
144
|
checks.push(createGhTokenCheck());
|
|
95
145
|
return checks;
|
|
96
146
|
}
|
|
147
|
+
|
|
148
|
+
// Re-export for tests that need to introspect the mapping.
|
|
149
|
+
export { PROVIDER_TO_CLI, normalizeProvider };
|
|
@@ -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
|
+
}
|