karajan-code 2.7.4 → 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 +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/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 +85 -31
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- 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 -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 +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 -2077
- 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 +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/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} +2 -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
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
---
|
|
25
25
|
|
|
26
|
-
> **v2.
|
|
26
|
+
> **v2.8.0 released** — Audit-driven hardening release. The 2026-04-30 self-audit (`kj audit`) flagged 13 issues across security, code quality, performance, architecture and testing; this release closes all of them in 16 PRs with 0 user-visible API changes. Highlights: (1) **Security**: every `child_process` call in `src/` now uses tokenised arg arrays (no shell expansion) — closed 7 sites that took template strings. (2) **FASE 1 e2e suite**: 7 scenarios + fake-coder/sonar fixtures cover the 5-bug class from the 2026-04-27 demo regression (zombie-HU, saveSession-missing, Repairer unfixable, zombi-status, audit smoke). 23 e2e tests in 6s. (3) **File splits**: `cli.js` 699→113 LOC, `commands/plan.js` 549→14 LOC (shim) + sub-commands, `iteration-loop.js` 513→311 LOC, `pre-loop.js` 626→435 LOC. Each big driver now under the 600-LOC ceiling. (4) **ESLint ratcheted**: 57 warnings closed in `src/`, then `no-unused-vars` / `no-useless-assignment` / `no-useless-escape` / `preserve-caught-error` switched warn→error so regressions fail CI. New rules ban `console.*` outside CLI paths and `globalThis.__KJ_*` outside `test-harness.js`. (5) **BREAKING**: `engines.node` 18→20.10.0 (Node 18 LTS hit EOL 2025-04-30). See [CHANGELOG.md](./CHANGELOG.md) for the full punch list and [MIGRATION-v2.md](./MIGRATION-v2.md) for v1→v2 breaking changes.
|
|
27
27
|
|
|
28
28
|
You describe what you want to build. Karajan orchestrates multiple AI agents to plan it, implement it, test it, review it with SonarQube, and iterate. No babysitting required.
|
|
29
29
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "karajan-code",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Local multi-agent coding orchestrator with TDD, SonarQube, and code review pipeline",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "AGPL-3.0",
|
|
@@ -24,7 +24,13 @@
|
|
|
24
24
|
"cli"
|
|
25
25
|
],
|
|
26
26
|
"engines": {
|
|
27
|
-
"node": ">=
|
|
27
|
+
"node": ">=20.10.0"
|
|
28
|
+
},
|
|
29
|
+
"imports": {
|
|
30
|
+
"#utils/*": "./src/utils/*",
|
|
31
|
+
"#session/*": "./src/session/*",
|
|
32
|
+
"#hu/*": "./src/hu/*",
|
|
33
|
+
"#skills/*": "./src/skills/*"
|
|
28
34
|
},
|
|
29
35
|
"files": [
|
|
30
36
|
"src/",
|
|
@@ -46,11 +52,19 @@
|
|
|
46
52
|
"setup": "node scripts/install.js",
|
|
47
53
|
"test": "vitest run",
|
|
48
54
|
"test:watch": "vitest",
|
|
49
|
-
"lint": "
|
|
55
|
+
"lint": "eslint src/",
|
|
56
|
+
"lint:fix": "eslint src/ --fix",
|
|
57
|
+
"lint:syntax": "((command -v rg >/dev/null 2>&1 && rg --files src tests | rg '\\.js$') || find src tests -type f -name '*.js') | while IFS= read -r f; do node --check \"$f\"; done",
|
|
50
58
|
"typecheck": "npx -p typescript@5 tsc --noEmit -p tsconfig.json",
|
|
51
|
-
"validate": "npm run lint && npm test",
|
|
59
|
+
"validate": "npm run lint && npm run lint:syntax && npm test",
|
|
52
60
|
"mcp": "node src/mcp/server.js"
|
|
53
61
|
},
|
|
62
|
+
"simple-git-hooks": {
|
|
63
|
+
"pre-commit": "npx lint-staged"
|
|
64
|
+
},
|
|
65
|
+
"lint-staged": {
|
|
66
|
+
"src/**/*.js": "eslint --max-warnings=999"
|
|
67
|
+
},
|
|
54
68
|
"dependencies": {
|
|
55
69
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
56
70
|
"commander": "^14.0.0",
|
|
@@ -60,9 +74,15 @@
|
|
|
60
74
|
},
|
|
61
75
|
"devDependencies": {
|
|
62
76
|
"@changesets/cli": "^2.30.0",
|
|
77
|
+
"@eslint/js": "^10.0.1",
|
|
63
78
|
"@vitest/coverage-v8": "^4.0.18",
|
|
64
79
|
"esbuild": "^0.28.0",
|
|
80
|
+
"eslint": "^10.2.1",
|
|
81
|
+
"eslint-plugin-import-x": "^4.16.2",
|
|
82
|
+
"globals": "^17.5.0",
|
|
83
|
+
"lint-staged": "^16.4.0",
|
|
65
84
|
"postject": "^1.0.0-alpha.6",
|
|
85
|
+
"simple-git-hooks": "^2.13.1",
|
|
66
86
|
"vitest": "^4.0.0"
|
|
67
87
|
}
|
|
68
88
|
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test Diet — baseline snapshot generator.
|
|
4
|
+
*
|
|
5
|
+
* Captures the *current* state of the unit test suite so subsequent diet
|
|
6
|
+
* commits (HU plan-20260429094214-0srv_002 .. _008) can prove they did not
|
|
7
|
+
* blow past the guardrails (wall-clock budget and branch-coverage drop).
|
|
8
|
+
*
|
|
9
|
+
* Output file: tests/_diet/baseline.json
|
|
10
|
+
*
|
|
11
|
+
* Schema:
|
|
12
|
+
* {
|
|
13
|
+
* capturedAt: ISO-8601 timestamp,
|
|
14
|
+
* locTotal: total wc -l across every tests/...test.js file,
|
|
15
|
+
* wallClockSec: number, // `time npm test` wall-clock
|
|
16
|
+
* branchCoveragePct: number, // src/ branch coverage % from vitest --coverage
|
|
17
|
+
* fileCount: number, // files.length
|
|
18
|
+
* files: [ { path, loc, architecture } ]
|
|
19
|
+
* }
|
|
20
|
+
*
|
|
21
|
+
* The script is intentionally *measurement-only*: it never edits source
|
|
22
|
+
* or test files. Re-running it overwrites the snapshot — but the diet
|
|
23
|
+
* compares against the committed copy, not the latest run.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
27
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { dirname, relative, resolve } from "node:path";
|
|
29
|
+
import { fileURLToPath } from "node:url";
|
|
30
|
+
|
|
31
|
+
const REPO_ROOT = resolve(fileURLToPath(import.meta.url), "..", "..");
|
|
32
|
+
const OUTPUT_PATH = resolve(REPO_ROOT, "tests/_diet/baseline.json");
|
|
33
|
+
const ARCH_PREFIX = "tests/architecture/";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Enumerate every committed `tests/**\/*.test.js` file via `git ls-files`.
|
|
37
|
+
* Using git rather than fs walk keeps the snapshot reproducible: untracked
|
|
38
|
+
* scratch files in someone's worktree never leak into the baseline.
|
|
39
|
+
*
|
|
40
|
+
* @returns {string[]} repo-relative paths
|
|
41
|
+
*/
|
|
42
|
+
function listTestFiles() {
|
|
43
|
+
const out = execFileSync(
|
|
44
|
+
"git",
|
|
45
|
+
["ls-files", "tests"],
|
|
46
|
+
{ cwd: REPO_ROOT, encoding: "utf8" }
|
|
47
|
+
);
|
|
48
|
+
return out
|
|
49
|
+
.split("\n")
|
|
50
|
+
.map((line) => line.trim())
|
|
51
|
+
.filter((line) => line.endsWith(".test.js"));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Count lines in a file. Uses readFileSync because the files are small and
|
|
56
|
+
* spawning `wc -l` 300+ times is wasteful.
|
|
57
|
+
*
|
|
58
|
+
* @param {string} absPath
|
|
59
|
+
* @returns {number}
|
|
60
|
+
*/
|
|
61
|
+
function countLines(absPath) {
|
|
62
|
+
const content = readFileSync(absPath, "utf8");
|
|
63
|
+
if (content.length === 0) return 0;
|
|
64
|
+
// wc -l counts newlines; mimic that so the JSON matches `wc -l` output.
|
|
65
|
+
let lines = 0;
|
|
66
|
+
for (let i = 0; i < content.length; i += 1) {
|
|
67
|
+
if (content.charCodeAt(i) === 10) lines += 1;
|
|
68
|
+
}
|
|
69
|
+
return lines;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Run `npm test` once and report wall-clock seconds. The suite has a
|
|
74
|
+
* pre-existing architecture-budget failure (tests/architecture/dynamic-imports);
|
|
75
|
+
* its exit code is irrelevant for the wall-clock baseline.
|
|
76
|
+
*
|
|
77
|
+
* @returns {number} seconds, rounded to 2 decimals
|
|
78
|
+
*/
|
|
79
|
+
function measureWallClock() {
|
|
80
|
+
const started = process.hrtime.bigint();
|
|
81
|
+
spawnSync("npm", ["test", "--silent"], {
|
|
82
|
+
cwd: REPO_ROOT,
|
|
83
|
+
stdio: "ignore"
|
|
84
|
+
});
|
|
85
|
+
const elapsedNs = process.hrtime.bigint() - started;
|
|
86
|
+
return Math.round(Number(elapsedNs) / 1e7) / 100;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run vitest with v8 coverage and parse the `text-summary` reporter output
|
|
91
|
+
* for the `Branches` percentage of the src/ tree.
|
|
92
|
+
*
|
|
93
|
+
* The architecture-budget test fails before the coverage summary is emitted,
|
|
94
|
+
* so we exclude that single file from this measurement run only. It scans
|
|
95
|
+
* source files statically and does not exercise src/ runtime code, so its
|
|
96
|
+
* absence does not move branch coverage in any meaningful way.
|
|
97
|
+
*
|
|
98
|
+
* @returns {number} branch coverage % (e.g. 17.23)
|
|
99
|
+
*/
|
|
100
|
+
function measureBranchCoverage() {
|
|
101
|
+
const result = spawnSync(
|
|
102
|
+
"npx",
|
|
103
|
+
[
|
|
104
|
+
"vitest",
|
|
105
|
+
"run",
|
|
106
|
+
"--coverage",
|
|
107
|
+
"--coverage.provider=v8",
|
|
108
|
+
"--coverage.reporter=text-summary",
|
|
109
|
+
"--exclude=tests/architecture/dynamic-imports.test.js"
|
|
110
|
+
],
|
|
111
|
+
{ cwd: REPO_ROOT, encoding: "utf8" }
|
|
112
|
+
);
|
|
113
|
+
const out = `${result.stdout}\n${result.stderr}`;
|
|
114
|
+
const match = out.match(/Branches\s*:\s*([\d.]+)%/);
|
|
115
|
+
if (!match) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
"Could not parse branch coverage from vitest output:\n" +
|
|
118
|
+
out.slice(-2000)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
return Number(match[1]);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Build the files[] array with metadata for every test file.
|
|
126
|
+
*
|
|
127
|
+
* @param {string[]} relativePaths
|
|
128
|
+
* @returns {Array<{path: string, loc: number, architecture: boolean}>}
|
|
129
|
+
*/
|
|
130
|
+
function buildFileEntries(relativePaths) {
|
|
131
|
+
return relativePaths
|
|
132
|
+
.map((rel) => ({
|
|
133
|
+
path: rel,
|
|
134
|
+
loc: countLines(resolve(REPO_ROOT, rel)),
|
|
135
|
+
architecture: rel.startsWith(ARCH_PREFIX)
|
|
136
|
+
}))
|
|
137
|
+
.sort((a, b) => a.path.localeCompare(b.path));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function main() {
|
|
141
|
+
const relPaths = listTestFiles();
|
|
142
|
+
const files = buildFileEntries(relPaths);
|
|
143
|
+
const locTotal = files.reduce((sum, f) => sum + f.loc, 0);
|
|
144
|
+
|
|
145
|
+
// Order matters: cheap measurements first, expensive ones last, so a
|
|
146
|
+
// wedge in the slow ones is easy to bisect via Ctrl-C.
|
|
147
|
+
const wallClockSec = measureWallClock();
|
|
148
|
+
const branchCoveragePct = measureBranchCoverage();
|
|
149
|
+
|
|
150
|
+
const baseline = {
|
|
151
|
+
capturedAt: new Date().toISOString(),
|
|
152
|
+
locTotal,
|
|
153
|
+
wallClockSec,
|
|
154
|
+
branchCoveragePct,
|
|
155
|
+
fileCount: files.length,
|
|
156
|
+
files
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
|
|
160
|
+
writeFileSync(OUTPUT_PATH, `${JSON.stringify(baseline, null, 2)}\n`, "utf8");
|
|
161
|
+
|
|
162
|
+
process.stdout.write(
|
|
163
|
+
`Wrote ${relative(REPO_ROOT, OUTPUT_PATH)} — ` +
|
|
164
|
+
`${baseline.fileCount} files, ${baseline.locTotal} LOC, ` +
|
|
165
|
+
`${baseline.wallClockSec}s wall-clock, ` +
|
|
166
|
+
`${baseline.branchCoveragePct}% branch coverage.\n`
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
main();
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Regenerate the numeric stats in docs/ARCHITECTURE.md from the current tree.
|
|
3
|
+
#
|
|
4
|
+
# Motivation: docs/ARCHITECTURE.md accumulates staleness. The v2.7.4 audit
|
|
5
|
+
# found it claimed "28k LOC, 234 files" when reality was 35.9k LOC / 260
|
|
6
|
+
# files (+28% LOC, +11% files), and pointed at `src/orchestrator.js` as the
|
|
7
|
+
# main pipeline (~1400 LOC) when the real orchestrator is
|
|
8
|
+
# `src/orchestrator/flow-runner.js` (2241 LOC) and `src/orchestrator.js` is
|
|
9
|
+
# a 22-line barrel.
|
|
10
|
+
#
|
|
11
|
+
# This script rewrites the specific lines that contain numeric stats
|
|
12
|
+
# in-place; narrative paragraphs are left alone.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# scripts/regen-arch-stats.sh # rewrite and exit 0
|
|
16
|
+
# scripts/regen-arch-stats.sh --check # exit 1 if the file would change
|
|
17
|
+
#
|
|
18
|
+
# Add to the release checklist: run without --check, commit any diff.
|
|
19
|
+
set -euo pipefail
|
|
20
|
+
|
|
21
|
+
MODE="${1:-}"
|
|
22
|
+
DOC="docs/ARCHITECTURE.md"
|
|
23
|
+
|
|
24
|
+
if [[ ! -f "$DOC" ]]; then
|
|
25
|
+
echo "ARCHITECTURE.md not found at $DOC — run from the repo root." >&2
|
|
26
|
+
exit 2
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# ── Compute current stats ──────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
SRC_LOC=$(find src -name "*.js" -not -path "*/node_modules/*" | xargs wc -l 2>/dev/null | tail -1 | awk '{print $1}')
|
|
32
|
+
SRC_FILES=$(find src -name "*.js" -not -path "*/node_modules/*" | wc -l)
|
|
33
|
+
TEST_COUNT=$(find tests -name "*.test.js" -not -path "*/worktrees/*" -not -path "*/.kj/*" | wc -l)
|
|
34
|
+
|
|
35
|
+
# Format LOC with thousands separator ("36038" → "36k" for narrative, raw for table)
|
|
36
|
+
SRC_LOC_SHORT=$(awk -v n="$SRC_LOC" 'BEGIN{ printf "%dk", int(n/1000) }')
|
|
37
|
+
|
|
38
|
+
# Orchestrator pipeline stats
|
|
39
|
+
FLOW_RUNNER_LOC=$(wc -l < src/orchestrator/flow-runner.js 2>/dev/null | awk '{print $1}')
|
|
40
|
+
BARREL_LOC=$(wc -l < src/orchestrator.js 2>/dev/null | awk '{print $1}')
|
|
41
|
+
|
|
42
|
+
# ── Build a sed program ────────────────────────────────────────────────
|
|
43
|
+
# Each line targets a specific claim. Patterns are narrow enough to miss
|
|
44
|
+
# unrelated mentions but wide enough to absorb small formatting drifts.
|
|
45
|
+
|
|
46
|
+
SED_SCRIPT=$(cat <<EOF
|
|
47
|
+
s|Source code ([0-9kKmM.]\\+ LOC, [0-9]\\+ files)|Source code (${SRC_LOC_SHORT} LOC, ${SRC_FILES} files)|g
|
|
48
|
+
s|Test suite ([0-9]\\+ tests)|Test suite (${TEST_COUNT} test files)|g
|
|
49
|
+
s|\`src/orchestrator.js\` (~[0-9]\\+ LOC)|\`src/orchestrator.js\` (${BARREL_LOC}-line barrel) → delegates to \`src/orchestrator/flow-runner.js\` (${FLOW_RUNNER_LOC} LOC)|g
|
|
50
|
+
EOF
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
if [[ "$MODE" == "--check" ]]; then
|
|
54
|
+
if sed "$SED_SCRIPT" "$DOC" | diff -q "$DOC" - >/dev/null; then
|
|
55
|
+
echo "docs/ARCHITECTURE.md stats are up to date."
|
|
56
|
+
exit 0
|
|
57
|
+
fi
|
|
58
|
+
echo "docs/ARCHITECTURE.md stats are STALE. Run scripts/regen-arch-stats.sh to rewrite." >&2
|
|
59
|
+
sed "$SED_SCRIPT" "$DOC" | diff -u "$DOC" - || true
|
|
60
|
+
exit 1
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# Real rewrite, in place, with backup we immediately delete to keep the
|
|
64
|
+
# working tree clean.
|
|
65
|
+
sed -i.bak "$SED_SCRIPT" "$DOC"
|
|
66
|
+
rm -f "${DOC}.bak"
|
|
67
|
+
|
|
68
|
+
echo "docs/ARCHITECTURE.md updated with:"
|
|
69
|
+
echo " Source code: ${SRC_LOC_SHORT} LOC, ${SRC_FILES} files"
|
|
70
|
+
echo " Tests: ${TEST_COUNT} test files"
|
|
71
|
+
echo " Orchestrator: src/orchestrator.js ${BARREL_LOC}-line barrel → flow-runner.js ${FLOW_RUNNER_LOC} LOC"
|
|
@@ -4,16 +4,22 @@ import { getAgentMeta } from "./index.js";
|
|
|
4
4
|
|
|
5
5
|
export async function assertAgentsAvailable(agentNames = []) {
|
|
6
6
|
const unique = [...new Set(agentNames.filter(Boolean))];
|
|
7
|
-
const missing = [];
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
// PR-J (audit quick win): probe every agent CLI in parallel via
|
|
9
|
+
// Promise.all instead of awaiting in a for-of loop. Each probe is
|
|
10
|
+
// an independent fork+exec with no shared state, so serialising
|
|
11
|
+
// them was wasted preflight latency: O(n) shell-outs becomes O(1)
|
|
12
|
+
// (max of all probes). On 4 agents at ~80ms each this drops the
|
|
13
|
+
// wait from ~320ms to ~80ms.
|
|
14
|
+
const probes = await Promise.all(
|
|
15
|
+
unique.map(async (name) => {
|
|
16
|
+
const meta = getAgentMeta(name);
|
|
17
|
+
if (!meta?.bin) return null;
|
|
18
|
+
const res = await runCommand(resolveBin(meta.bin), ["--version"]);
|
|
19
|
+
return res.exitCode !== 0 ? { name, ...meta } : null;
|
|
20
|
+
})
|
|
21
|
+
);
|
|
22
|
+
const missing = probes.filter(Boolean);
|
|
17
23
|
|
|
18
24
|
if (missing.length === 0) return;
|
|
19
25
|
|
|
@@ -72,10 +72,10 @@ function collectAssistantText(obj) {
|
|
|
72
72
|
* Returns an object with tokens_in, tokens_out, cost_usd, model or null if not found.
|
|
73
73
|
*/
|
|
74
74
|
function extractUsageFromStreamJson(raw) {
|
|
75
|
-
const
|
|
76
|
-
for (let i =
|
|
77
|
-
const obj =
|
|
78
|
-
if (
|
|
75
|
+
const events = parseClaudeEvents(raw);
|
|
76
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
77
|
+
const obj = events[i];
|
|
78
|
+
if (obj.type !== "result") continue;
|
|
79
79
|
|
|
80
80
|
const tokens_in = obj.usage?.input_tokens ?? 0;
|
|
81
81
|
const tokens_out = obj.usage?.output_tokens ?? 0;
|
|
@@ -89,25 +89,54 @@ function extractUsageFromStreamJson(raw) {
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
92
|
+
* Normalize Claude's output payload to a flat array of event objects.
|
|
93
|
+
*
|
|
94
|
+
* Claude 2.x supports two on-disk shapes depending on the output-format flag:
|
|
95
|
+
*
|
|
96
|
+
* --output-format stream-json → NDJSON, one event JSON per line
|
|
97
|
+
* --output-format json → a single JSON array with every event
|
|
98
|
+
*
|
|
99
|
+
* The legacy path assumed NDJSON and silently dropped the `json` variant
|
|
100
|
+
* (split("\n") returned one unparseable "line" containing the whole array,
|
|
101
|
+
* extract returned empty, caller fell back to the raw string — which made
|
|
102
|
+
* `kj plan` save the whole stream as `approach` with zero HUs). Handle both
|
|
103
|
+
* shapes here so callers see a uniform list of events regardless of flag.
|
|
104
|
+
*/
|
|
105
|
+
function parseClaudeEvents(raw) {
|
|
106
|
+
const trimmed = (raw || "").trim();
|
|
107
|
+
if (!trimmed) return [];
|
|
108
|
+
// `--output-format json` → single JSON array.
|
|
109
|
+
if (trimmed.startsWith("[")) {
|
|
110
|
+
try {
|
|
111
|
+
const arr = JSON.parse(trimmed);
|
|
112
|
+
if (Array.isArray(arr)) return arr.filter((o) => o && typeof o === "object");
|
|
113
|
+
} catch { /* fall through to NDJSON */ }
|
|
114
|
+
}
|
|
115
|
+
// `--output-format stream-json` → NDJSON, one event per line.
|
|
116
|
+
const events = [];
|
|
117
|
+
for (const line of trimmed.split("\n")) {
|
|
118
|
+
const obj = tryParseJson(line);
|
|
119
|
+
if (obj && typeof obj === "object") events.push(obj);
|
|
120
|
+
}
|
|
121
|
+
return events;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract the final text result from Claude's output (either stream-json
|
|
126
|
+
* NDJSON or single-array json — see parseClaudeEvents). We look for the
|
|
127
|
+
* `result` message first (holds the final text); if missing, concatenate
|
|
128
|
+
* every assistant text block encountered.
|
|
95
129
|
*/
|
|
96
130
|
function extractTextFromStreamJson(raw) {
|
|
97
|
-
const
|
|
98
|
-
//
|
|
99
|
-
for (let i =
|
|
100
|
-
const
|
|
101
|
-
if (!obj) continue;
|
|
102
|
-
const result = extractResultText(obj);
|
|
131
|
+
const events = parseClaudeEvents(raw);
|
|
132
|
+
// Walk from the end — the final `result` event is the last one.
|
|
133
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
134
|
+
const result = extractResultText(events[i]);
|
|
103
135
|
if (result) return result;
|
|
104
136
|
}
|
|
105
|
-
// Fallback: accumulate
|
|
137
|
+
// Fallback: accumulate every assistant text block in order.
|
|
106
138
|
const parts = [];
|
|
107
|
-
for (const
|
|
108
|
-
const obj = tryParseJson(line);
|
|
109
|
-
if (obj) parts.push(...collectAssistantText(obj));
|
|
110
|
-
}
|
|
139
|
+
for (const obj of events) parts.push(...collectAssistantText(obj));
|
|
111
140
|
return parts.join("") || raw;
|
|
112
141
|
}
|
|
113
142
|
|
|
@@ -138,36 +167,105 @@ function summarizeToolCall(name, input = {}) {
|
|
|
138
167
|
}
|
|
139
168
|
}
|
|
140
169
|
|
|
141
|
-
|
|
170
|
+
/**
|
|
171
|
+
* Translate a Claude stream-json event into a human-readable summary.
|
|
172
|
+
*
|
|
173
|
+
* Returns a `{ line, kind }` object for the cli-progress reporter to
|
|
174
|
+
* display, OR `null` to suppress (the event is structural noise).
|
|
175
|
+
*
|
|
176
|
+
* Whitelist semantics on purpose: pre-v2.7.5 the filter ONLY recognised
|
|
177
|
+
* `assistant` + `result` and forwarded everything else as raw JSON,
|
|
178
|
+
* which dumped 6 KB of `system/init` + `rate_limit_event` to the user
|
|
179
|
+
* on every `kj plan`. Now we recognise every event type Claude can
|
|
180
|
+
* emit and either condense it to one line or drop it. Unknown types
|
|
181
|
+
* are dropped silently unless DEBUG_KJ_AGENT is set.
|
|
182
|
+
*/
|
|
183
|
+
export function translateStreamJsonEvent(obj) {
|
|
184
|
+
switch (obj.type) {
|
|
185
|
+
case "system": {
|
|
186
|
+
// The init frame announces session id + model. Useful as a single
|
|
187
|
+
// status line on first connect; the rate_limit subtype on a
|
|
188
|
+
// healthy session is pure noise — suppress.
|
|
189
|
+
if (obj.subtype === "init") {
|
|
190
|
+
const model = typeof obj.model === "string" ? obj.model : "?";
|
|
191
|
+
return { line: `Claude session ready (model: ${model.replace(/\[.*\]/, "")})`, kind: "text" };
|
|
192
|
+
}
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
case "rate_limit_event": {
|
|
196
|
+
const status = obj.rate_limit_info?.status;
|
|
197
|
+
// Only surface when actually throttled — "allowed" is the happy
|
|
198
|
+
// path and surfaces every few seconds during normal runs.
|
|
199
|
+
if (status && status !== "allowed") {
|
|
200
|
+
return { line: `Rate limit: ${status}`, kind: "text" };
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
204
|
+
case "user": {
|
|
205
|
+
// Echoes of the previous tool's output coming back into the
|
|
206
|
+
// conversation. Adding a line per echo would double the noise of
|
|
207
|
+
// the corresponding tool_use line, which we already render.
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
case "assistant": {
|
|
211
|
+
const content = obj.message?.content;
|
|
212
|
+
if (!Array.isArray(content)) return null;
|
|
213
|
+
// Each content block becomes a separate line — we return them
|
|
214
|
+
// through the caller's emit-many path below.
|
|
215
|
+
return { _multi: true, blocks: content };
|
|
216
|
+
}
|
|
217
|
+
case "result": {
|
|
218
|
+
const summary = typeof obj.result === "string"
|
|
219
|
+
? obj.result.split("\n")[0].slice(0, 120)
|
|
220
|
+
: "result received";
|
|
221
|
+
return { line: `done: ${summary}` };
|
|
222
|
+
}
|
|
223
|
+
default: {
|
|
224
|
+
if (process.env.DEBUG_KJ_AGENT) {
|
|
225
|
+
return { line: `[debug] unknown event: ${obj.type}` };
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function createStreamJsonFilter(onOutput) {
|
|
142
233
|
if (!onOutput) return null;
|
|
143
234
|
return ({ stream, line }) => {
|
|
235
|
+
let obj;
|
|
144
236
|
try {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
onOutput({ stream, line: summarizeToolCall(block.name, block.input), kind: "tool" });
|
|
155
|
-
} else if (block.type === "thinking" && block.thinking) {
|
|
156
|
-
onOutput({ stream, line: `thinking: ${block.thinking.slice(0, 80).replace(/\s+/g, " ")}…`, kind: "thinking" });
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
return;
|
|
237
|
+
obj = JSON.parse(line);
|
|
238
|
+
} catch {
|
|
239
|
+
// Not JSON — forward verbatim ONLY if it's short and not a partial
|
|
240
|
+
// JSON chunk (some agents print stderr diagnostics through the
|
|
241
|
+
// same channel). Anything that smells like JSON-debris gets
|
|
242
|
+
// dropped to keep the CLI clean.
|
|
243
|
+
const trimmed = line.trim();
|
|
244
|
+
if (trimmed && !trimmed.startsWith("{") && !trimmed.startsWith("[") && trimmed.length < 200) {
|
|
245
|
+
onOutput({ stream, line: trimmed });
|
|
160
246
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const summary = translateStreamJsonEvent(obj);
|
|
251
|
+
if (!summary) return;
|
|
252
|
+
|
|
253
|
+
// Multi-block assistant payload: one line per content block.
|
|
254
|
+
if (summary._multi) {
|
|
255
|
+
for (const block of summary.blocks) {
|
|
256
|
+
if (block.type === "text" && block.text) {
|
|
257
|
+
const firstLine = block.text.split("\n")[0].trim().slice(0, 120);
|
|
258
|
+
if (firstLine) onOutput({ stream, line: firstLine, kind: "text" });
|
|
259
|
+
} else if (block.type === "tool_use") {
|
|
260
|
+
onOutput({ stream, line: summarizeToolCall(block.name, block.input), kind: "tool" });
|
|
261
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
262
|
+
onOutput({ stream, line: `thinking: ${block.thinking.slice(0, 80).replace(/\s+/g, " ")}…`, kind: "thinking" });
|
|
263
|
+
}
|
|
168
264
|
}
|
|
169
|
-
|
|
170
|
-
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
onOutput({ stream, line: summary.line, kind: summary.kind });
|
|
171
269
|
};
|
|
172
270
|
}
|
|
173
271
|
|
|
@@ -190,7 +288,7 @@ function createStreamJsonFilter(onOutput) {
|
|
|
190
288
|
* response data.
|
|
191
289
|
*/
|
|
192
290
|
function cleanExecaOpts(extra = {}) {
|
|
193
|
-
const { CLAUDECODE, ...env } = process.env;
|
|
291
|
+
const { CLAUDECODE: _CLAUDECODE, ...env } = process.env;
|
|
194
292
|
return { env, stdin: "ignore", ...extra };
|
|
195
293
|
}
|
|
196
294
|
|
|
@@ -235,7 +333,7 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
235
333
|
return result;
|
|
236
334
|
}
|
|
237
335
|
|
|
238
|
-
async _runTaskExec(task, model,
|
|
336
|
+
async _runTaskExec(task, model, _role) {
|
|
239
337
|
const args = ["-p", task.prompt, "--allowedTools", ...ALLOWED_TOOLS];
|
|
240
338
|
if (model) args.push("--model", model);
|
|
241
339
|
|
|
@@ -45,7 +45,7 @@ export function getModelInfo(name) {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export function getRegisteredModels() {
|
|
48
|
-
return [...modelRegistry.entries()].map(([
|
|
48
|
+
return [...modelRegistry.entries()].map(([_name, entry]) => ({
|
|
49
49
|
name: entry.name,
|
|
50
50
|
provider: entry.provider,
|
|
51
51
|
pricing: { ...entry.pricing },
|