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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { runCommand } from "../utils/process.js";
|
|
2
2
|
import { generateSnapshotDiff } from "./snapshot-diff.js";
|
|
3
|
+
import { getRunContext } from "../utils/run-context.js";
|
|
4
|
+
|
|
5
|
+
// Module-scope back-compat state for callers outside a `withRunContext`
|
|
6
|
+
// scope. When inside a pipeline, `getRunContext()` provides per-run
|
|
7
|
+
// isolation (TSK-0338) — the module-level values below are only consulted
|
|
8
|
+
// as a last resort.
|
|
3
9
|
|
|
4
10
|
/** @type {((command: string, args?: string[], options?: object) => Promise<object>)|null} */
|
|
5
11
|
let _runner = null;
|
|
@@ -10,35 +16,39 @@ let _snapshot = null;
|
|
|
10
16
|
/** @type {string|null} — project directory scope for git diff (prevents leaking unrelated changes) */
|
|
11
17
|
let _projectDir = null;
|
|
12
18
|
|
|
13
|
-
/**
|
|
14
|
-
* Set the project directory scope. When set, all diffs are scoped to this directory.
|
|
15
|
-
* Call once at pipeline start when projectDir differs from repo root.
|
|
16
|
-
* @param {string|null} dir
|
|
17
|
-
*/
|
|
19
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ projectDir })` for isolation. */
|
|
18
20
|
export function setProjectDir(dir) {
|
|
19
21
|
_projectDir = dir;
|
|
20
22
|
}
|
|
21
23
|
|
|
22
|
-
/**
|
|
23
|
-
* Store a filesystem snapshot for git-free diff fallback.
|
|
24
|
-
* Call before the coder runs. If git is available, this is unused.
|
|
25
|
-
* @param {Map<string, string>} snapshot
|
|
26
|
-
*/
|
|
24
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ snapshot })` for isolation. */
|
|
27
25
|
export function setSnapshot(snapshot) {
|
|
28
26
|
_snapshot = snapshot;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
|
-
/**
|
|
32
|
-
* Inject an RTK-aware runner so all git operations in this module benefit from token savings.
|
|
33
|
-
* Call once at pipeline start when RTK is available.
|
|
34
|
-
* @param {(command: string, args?: string[], options?: object) => Promise<object>} runner
|
|
35
|
-
*/
|
|
29
|
+
/** Retained for back-compat; pipelines should use `withRunContext({ runner })` for isolation. */
|
|
36
30
|
export function setRunner(runner) {
|
|
37
31
|
_runner = runner;
|
|
38
32
|
}
|
|
39
33
|
|
|
34
|
+
function resolveRunner() {
|
|
35
|
+
return getRunContext()?.runner || _runner || runCommand;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function resolveProjectDir() {
|
|
39
|
+
const ctx = getRunContext();
|
|
40
|
+
if (ctx && "projectDir" in ctx) return ctx.projectDir;
|
|
41
|
+
return _projectDir;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function resolveSnapshot() {
|
|
45
|
+
const ctx = getRunContext();
|
|
46
|
+
if (ctx && "snapshot" in ctx) return ctx.snapshot;
|
|
47
|
+
return _snapshot;
|
|
48
|
+
}
|
|
49
|
+
|
|
40
50
|
function run(command, args, ...rest) {
|
|
41
|
-
return (
|
|
51
|
+
return resolveRunner()(command, args, ...rest);
|
|
42
52
|
}
|
|
43
53
|
|
|
44
54
|
export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
@@ -61,8 +71,9 @@ export async function computeBaseRef({ baseBranch = "main", baseRef = null }) {
|
|
|
61
71
|
}
|
|
62
72
|
|
|
63
73
|
export async function generateDiff({ baseRef, stageNewFiles = false, projectDir = null }) {
|
|
64
|
-
// Auto-resolve projectDir from
|
|
65
|
-
const scopeDir = projectDir ||
|
|
74
|
+
// Auto-resolve projectDir from run context (TSK-0338) or module fallback
|
|
75
|
+
const scopeDir = projectDir || resolveProjectDir() || null;
|
|
76
|
+
const snapshot = resolveSnapshot();
|
|
66
77
|
|
|
67
78
|
// Try git diff first
|
|
68
79
|
try {
|
|
@@ -80,14 +91,16 @@ export async function generateDiff({ baseRef, stageNewFiles = false, projectDir
|
|
|
80
91
|
} catch { /* git not available or failed */ }
|
|
81
92
|
|
|
82
93
|
// Fallback: snapshot-based diff (no git needed)
|
|
83
|
-
if (
|
|
94
|
+
if (snapshot) {
|
|
84
95
|
const dir = projectDir || process.cwd();
|
|
85
|
-
return generateSnapshotDiff(
|
|
96
|
+
return generateSnapshotDiff(snapshot, null, dir);
|
|
86
97
|
}
|
|
87
98
|
|
|
88
|
-
// No git and no snapshot — generate diff of all files as new
|
|
99
|
+
// No git and no snapshot — generate diff of all files as new.
|
|
100
|
+
// TSK-0340: dropped a stray `const { takeSnapshot } = await import(...)`
|
|
101
|
+
// that was never used in this branch (we synthesize an empty Map and
|
|
102
|
+
// let generateSnapshotDiff do the file walk itself).
|
|
89
103
|
const dir = projectDir || process.cwd();
|
|
90
|
-
const { takeSnapshot } = await import("./snapshot-diff.js");
|
|
91
104
|
const emptySnapshot = new Map();
|
|
92
105
|
return generateSnapshotDiff(emptySnapshot, null, dir);
|
|
93
106
|
}
|
|
@@ -67,7 +67,7 @@ export async function generateSnapshotDiff(before, after, rootDir) {
|
|
|
67
67
|
const lines = [];
|
|
68
68
|
|
|
69
69
|
// New files (in after but not before)
|
|
70
|
-
for (const [path,
|
|
70
|
+
for (const [path, _hash] of after) {
|
|
71
71
|
if (!before.has(path)) {
|
|
72
72
|
const content = await safeRead(join(rootDir, path));
|
|
73
73
|
if (content !== null) {
|
package/src/roles/audit-role.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { AgentRole } from "./agent-role.js";
|
|
2
2
|
import { buildAuditPrompt, parseAuditOutput, AUDIT_DIMENSIONS } from "../prompts/audit.js";
|
|
3
3
|
import { measureBasalCost, loadPreviousAudit, saveAuditSnapshot, computeGrowthDelta } from "../audit/basal-cost.js";
|
|
4
|
+
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
5
|
+
import { collectSonarFindings } from "../audit/sonar-findings.js";
|
|
6
|
+
import { collectWebPerfInput } from "../audit/webperf-input.js";
|
|
7
|
+
import { collectOsvFindings } from "../audit/osv-findings.js";
|
|
8
|
+
import { collectSemgrepFindings } from "../audit/semgrep-findings.js";
|
|
4
9
|
|
|
5
10
|
function parseDimensions(dimensionsStr) {
|
|
6
11
|
if (!dimensionsStr || dimensionsStr === "all") return null;
|
|
@@ -26,37 +31,98 @@ export class AuditRole extends AgentRole {
|
|
|
26
31
|
super({ ...opts, name: "audit" });
|
|
27
32
|
}
|
|
28
33
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Run the deterministic side of an audit — basalCost, stack detection,
|
|
36
|
+
* Sonar issues, WebPerf input — without invoking the LLM. Cheap (no
|
|
37
|
+
* tokens, no API call). KJC-TSK-0364 splits the original execute() into
|
|
38
|
+
* this collector + executeWithDeterministic() so the CLI can show
|
|
39
|
+
* deterministic findings first and ask the user before paying for the
|
|
40
|
+
* LLM phase.
|
|
41
|
+
*
|
|
42
|
+
* @param {object|string} input - same shape execute() accepts
|
|
43
|
+
* @returns {Promise<object>} deterministic context (the same data
|
|
44
|
+
* execute() would gather internally)
|
|
45
|
+
*/
|
|
46
|
+
async collectDeterministic(input) {
|
|
47
|
+
const noSonar = typeof input === "object" ? Boolean(input?.noSonar) : false;
|
|
48
|
+
const noOsv = typeof input === "object" ? Boolean(input?.noOsv) : false;
|
|
49
|
+
const noSemgrep = typeof input === "object" ? Boolean(input?.noSemgrep) : false;
|
|
36
50
|
const projectDir = this.config?.projectDir || process.cwd();
|
|
37
51
|
let basalCost = null;
|
|
38
52
|
let growthDelta = null;
|
|
53
|
+
let stack = null;
|
|
54
|
+
let sonarFindings = null;
|
|
55
|
+
let webperf = null;
|
|
56
|
+
let osvFindings = null;
|
|
57
|
+
let semgrepFindings = null;
|
|
39
58
|
try {
|
|
40
59
|
basalCost = await measureBasalCost(projectDir);
|
|
41
60
|
const previous = await loadPreviousAudit(projectDir);
|
|
42
61
|
growthDelta = computeGrowthDelta(basalCost, previous);
|
|
43
62
|
} catch { /* basal cost is best-effort */ }
|
|
63
|
+
try {
|
|
64
|
+
stack = await detectProjectStack(projectDir);
|
|
65
|
+
} catch { /* stack detect is best-effort */ }
|
|
66
|
+
if (!noSonar) {
|
|
67
|
+
try {
|
|
68
|
+
sonarFindings = await collectSonarFindings(this.config, this.logger);
|
|
69
|
+
} catch { /* sonar fetch is best-effort */ }
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
webperf = collectWebPerfInput(stack, this.config);
|
|
73
|
+
} catch { /* webperf input is best-effort */ }
|
|
74
|
+
// OSV vulnerabilities — KJC-TSK-0365. Best-effort: missing
|
|
75
|
+
// osv-scanner binary returns available:false and the audit
|
|
76
|
+
// continues without the section.
|
|
77
|
+
if (!noOsv) {
|
|
78
|
+
try {
|
|
79
|
+
osvFindings = await collectOsvFindings(projectDir, this.logger);
|
|
80
|
+
} catch { /* osv-scanner is best-effort */ }
|
|
81
|
+
}
|
|
82
|
+
// Semgrep SAST — KJC-TSK-0366. Best-effort: missing semgrep binary
|
|
83
|
+
// returns available:false and the audit continues without the section.
|
|
84
|
+
if (!noSemgrep) {
|
|
85
|
+
try {
|
|
86
|
+
semgrepFindings = await collectSemgrepFindings(projectDir, this.logger);
|
|
87
|
+
} catch { /* semgrep is best-effort */ }
|
|
88
|
+
}
|
|
89
|
+
return { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Run the LLM phase of an audit using a pre-collected deterministic
|
|
94
|
+
* context. KJC-TSK-0364: separated from execute() so the CLI can run
|
|
95
|
+
* collectDeterministic() first, show findings, prompt, and only then
|
|
96
|
+
* call this if the user wants to spend tokens.
|
|
97
|
+
*/
|
|
98
|
+
async executeWithDeterministic(input, deterministicCtx) {
|
|
99
|
+
const task = typeof input === "string" ? input : input?.task || this.context?.task || "";
|
|
100
|
+
const onOutput = typeof input === "string" ? null : input?.onOutput || null;
|
|
101
|
+
const rawDimensions = typeof input === "object" ? input?.dimensions || null : null;
|
|
102
|
+
const context = typeof input === "object" ? input?.context || null : null;
|
|
103
|
+
const dimensions = typeof rawDimensions === "string" ? parseDimensions(rawDimensions) : rawDimensions;
|
|
104
|
+
|
|
105
|
+
const { projectDir, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings } = deterministicCtx;
|
|
44
106
|
|
|
45
107
|
const provider = this.resolveProvider();
|
|
46
108
|
const agent = this.createAgentInstance(provider);
|
|
47
|
-
const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context, basalCost, growthDelta });
|
|
109
|
+
const prompt = buildAuditPrompt({ task, instructions: this.instructions, dimensions, context, basalCost, growthDelta, stack, sonarFindings, webperf, osvFindings, semgrepFindings });
|
|
48
110
|
const runArgs = { prompt, role: "audit" };
|
|
49
111
|
if (onOutput) runArgs.onOutput = onOutput;
|
|
112
|
+
const startedAt = Date.now();
|
|
50
113
|
const result = await agent.runTask(runArgs);
|
|
114
|
+
const durationMs = Date.now() - startedAt;
|
|
115
|
+
|
|
116
|
+
const usage = extractUsage(result, { provider, durationMs });
|
|
51
117
|
|
|
52
118
|
if (!result.ok) {
|
|
53
|
-
return { ok: false, result: { error: result.error || result.output || "Audit failed", provider }, summary: `Audit failed: ${result.error || "unknown error"}`, usage
|
|
119
|
+
return { ok: false, result: { error: result.error || result.output || "Audit failed", provider }, summary: `Audit failed: ${result.error || "unknown error"}`, usage };
|
|
54
120
|
}
|
|
55
121
|
|
|
56
122
|
try {
|
|
57
123
|
const parsed = parseAuditOutput(result.output);
|
|
58
124
|
if (!parsed) {
|
|
59
|
-
return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage
|
|
125
|
+
return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage };
|
|
60
126
|
}
|
|
61
127
|
if (basalCost) { try { await saveAuditSnapshot(projectDir, basalCost); } catch { /* best-effort */ } }
|
|
62
128
|
|
|
@@ -65,13 +131,59 @@ export class AuditRole extends AgentRole {
|
|
|
65
131
|
result: {
|
|
66
132
|
summary: parsed.summary, dimensions: parsed.dimensions,
|
|
67
133
|
topRecommendations: parsed.topRecommendations,
|
|
68
|
-
|
|
134
|
+
textSummary: parsed.textSummary || undefined,
|
|
135
|
+
basalCost: basalCost || undefined, growthDelta: growthDelta || undefined,
|
|
136
|
+
stack: stack || undefined,
|
|
137
|
+
sonarFindings: sonarFindings?.available ? sonarFindings : undefined,
|
|
138
|
+
webperf: webperf?.available ? webperf : undefined,
|
|
139
|
+
osvFindings: osvFindings?.available ? osvFindings : undefined,
|
|
140
|
+
semgrepFindings: semgrepFindings?.available ? semgrepFindings : undefined,
|
|
141
|
+
provider
|
|
69
142
|
},
|
|
70
143
|
summary: buildSummary(parsed),
|
|
71
|
-
usage
|
|
144
|
+
usage
|
|
72
145
|
};
|
|
73
146
|
} catch {
|
|
74
|
-
return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage
|
|
147
|
+
return { ok: true, result: { raw: result.output, provider }, summary: "Audit complete (unstructured output)", usage };
|
|
75
148
|
}
|
|
76
149
|
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Convenience: run both phases in one call. Identical to the previous
|
|
153
|
+
* (single-phase) execute() so MCP and pipeline callers don't change.
|
|
154
|
+
*/
|
|
155
|
+
async execute(input) {
|
|
156
|
+
const deterministicCtx = await this.collectDeterministic(input);
|
|
157
|
+
return this.executeWithDeterministic(input, deterministicCtx);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Consolidate the usage telemetry the agent spreads across its top-level
|
|
163
|
+
* result fields. Returns a single object so consumers (CLI formatter,
|
|
164
|
+
* report-file writer, MCP) don't need to know about the spread shape.
|
|
165
|
+
*
|
|
166
|
+
* @param {object} result - raw agent.runTask() return
|
|
167
|
+
* @param {{provider: string, durationMs: number}} ctx
|
|
168
|
+
* @returns {{available: boolean, provider: string, model?: string, tokens_in?: number, tokens_out?: number, total_tokens?: number, cost_usd?: number, durationMs: number, reason?: string}}
|
|
169
|
+
*/
|
|
170
|
+
function extractUsage(result, { provider, durationMs }) {
|
|
171
|
+
if (!result || typeof result !== "object") {
|
|
172
|
+
return { available: false, provider, durationMs, reason: "no agent result" };
|
|
173
|
+
}
|
|
174
|
+
const tokens_in = Number.isFinite(result.tokens_in) ? result.tokens_in : null;
|
|
175
|
+
const tokens_out = Number.isFinite(result.tokens_out) ? result.tokens_out : null;
|
|
176
|
+
if (tokens_in === null && tokens_out === null) {
|
|
177
|
+
return { available: false, provider, model: result.model, durationMs, reason: `provider "${provider}" did not report token usage` };
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
available: true,
|
|
181
|
+
provider,
|
|
182
|
+
model: result.model,
|
|
183
|
+
tokens_in: tokens_in ?? 0,
|
|
184
|
+
tokens_out: tokens_out ?? 0,
|
|
185
|
+
total_tokens: (tokens_in ?? 0) + (tokens_out ?? 0),
|
|
186
|
+
cost_usd: typeof result.cost_usd === "number" ? result.cost_usd : null,
|
|
187
|
+
durationMs,
|
|
188
|
+
};
|
|
77
189
|
}
|
package/src/roles/coder-role.js
CHANGED
|
@@ -22,19 +22,41 @@ export class CoderRole extends AgentRole {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
extractInput(input) {
|
|
25
|
-
if (typeof input === "string")
|
|
25
|
+
if (typeof input === "string") {
|
|
26
|
+
return {
|
|
27
|
+
task: input,
|
|
28
|
+
reviewerFeedback: null, sonarSummary: null, deferredContext: null,
|
|
29
|
+
acceptanceTests: null,
|
|
30
|
+
adrs: null, specSection: null, reviewerFindings: null, huId: null,
|
|
31
|
+
onOutput: null
|
|
32
|
+
};
|
|
33
|
+
}
|
|
26
34
|
return {
|
|
27
35
|
task: input?.task || this.context?.task || "",
|
|
28
36
|
reviewerFeedback: input?.reviewerFeedback || null,
|
|
29
37
|
sonarSummary: input?.sonarSummary || null,
|
|
30
38
|
deferredContext: input?.deferredContext || null,
|
|
39
|
+
// Tests-first (v2.7.5): the coder reads these as the gate it
|
|
40
|
+
// must satisfy. Only the plan-backed HU path populates this;
|
|
41
|
+
// the standard single-task `kj run` flow leaves it null.
|
|
42
|
+
acceptanceTests: input?.acceptanceTests || null,
|
|
43
|
+
// PR F (v2.7.5): per-HU planning context. ADRs constrain HOW,
|
|
44
|
+
// specSection ties this HU to a SPEC heading, reviewerFindings
|
|
45
|
+
// are filtered to entries that mention this HU. All four are
|
|
46
|
+
// null for non-plan-backed runs, which keeps the legacy flow
|
|
47
|
+
// byte-for-byte identical.
|
|
48
|
+
adrs: Array.isArray(input?.adrs) ? input.adrs : null,
|
|
49
|
+
specSection: input?.specSection || null,
|
|
50
|
+
reviewerFindings: input?.reviewerFindings || null,
|
|
51
|
+
huId: input?.huId || null,
|
|
31
52
|
onOutput: input?.onOutput || null
|
|
32
53
|
};
|
|
33
54
|
}
|
|
34
55
|
|
|
35
|
-
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext }) {
|
|
56
|
+
async buildPrompt({ task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests, adrs, specSection, reviewerFindings, huId }) {
|
|
36
57
|
const prompt = await buildCoderPrompt({
|
|
37
|
-
task, reviewerFeedback, sonarSummary, deferredContext,
|
|
58
|
+
task, reviewerFeedback, sonarSummary, deferredContext, acceptanceTests,
|
|
59
|
+
adrs, specSection, reviewerFindings, huId,
|
|
38
60
|
coderRules: this.instructions,
|
|
39
61
|
methodology: this.config?.development?.methodology || "tdd",
|
|
40
62
|
serenaEnabled: Boolean(this.config?.serena?.enabled),
|
|
@@ -65,7 +65,48 @@ export class PlannerRole extends AgentRole {
|
|
|
65
65
|
|
|
66
66
|
const sections = [];
|
|
67
67
|
if (this.instructions) sections.push(this.instructions, "");
|
|
68
|
-
|
|
68
|
+
// Tests-first planning (v2.7.5):
|
|
69
|
+
// The planner MUST emit structured acceptance tests for every step.
|
|
70
|
+
// These are the contract the coder is handed (phase 2 of the
|
|
71
|
+
// tests-first flow); the tester role later runs them as the
|
|
72
|
+
// pass/fail gate. Without them the coder has no executable spec.
|
|
73
|
+
sections.push(
|
|
74
|
+
"Create an implementation plan for this task as structured JSON.",
|
|
75
|
+
"",
|
|
76
|
+
"Output MUST be a JSON object with this exact shape:",
|
|
77
|
+
"",
|
|
78
|
+
"```json",
|
|
79
|
+
"{",
|
|
80
|
+
' "approach": "<2-3 sentence high-level plan>",',
|
|
81
|
+
' "risks": ["<risk 1>", "<risk 2>"],',
|
|
82
|
+
' "outOfScope": ["<excluded item>"],',
|
|
83
|
+
' "steps": [',
|
|
84
|
+
" {",
|
|
85
|
+
' "description": "<what this step achieves, one sentence>",',
|
|
86
|
+
' "spec_section": "<short reference to the SPEC section this implements, e.g. \\"5.3\\" or \\"§5 Initial Scope\\". Optional but strongly preferred.>",',
|
|
87
|
+
' "acceptance_tests": [',
|
|
88
|
+
' { "type": "gherkin", "content": "Given <ctx>\\nWhen <action>\\nThen <outcome>" },',
|
|
89
|
+
' { "type": "shell", "content": "<bash command that must exit 0>" }',
|
|
90
|
+
" ]",
|
|
91
|
+
" }",
|
|
92
|
+
" ]",
|
|
93
|
+
"}",
|
|
94
|
+
"```",
|
|
95
|
+
"",
|
|
96
|
+
"Rules for acceptance_tests (CRITICAL — this is the contract the coder must satisfy):",
|
|
97
|
+
'- EVERY step MUST have at least one acceptance_test. No empty arrays.',
|
|
98
|
+
'- "gherkin" tests describe observable behaviour in natural language. Use them for end-to-end or user-facing assertions.',
|
|
99
|
+
'- "shell" tests are bash commands that exit 0 on success. Use them for concrete verifications (file exists, lint passes, specific exit code, HTTP status).',
|
|
100
|
+
'- Write tests BEFORE the step is implemented — they should fail until the step is done, pass afterwards.',
|
|
101
|
+
'- Prefer 2-4 tests per step: one happy-path, at least one edge case / failure path.',
|
|
102
|
+
'- Do NOT use the generic fallback "npx vitest run" (pre-v2.7.5 placeholder). Each test must assert something specific to the step.',
|
|
103
|
+
"",
|
|
104
|
+
"spec_section: when the task includes a structured SPEC (numbered headings like '## 5. Initial Scope'), every step MUST cite the section it implements. Becomes traceability metadata for the coder and reviewer.",
|
|
105
|
+
"",
|
|
106
|
+
"Steps must be ordered so that each depends only on previous steps (the first step has no prerequisites).",
|
|
107
|
+
"Return ONLY the JSON — no prose, no markdown fences around it.",
|
|
108
|
+
""
|
|
109
|
+
);
|
|
69
110
|
if (this.config?.productContext) sections.push("## Product Context", this.config.productContext, "");
|
|
70
111
|
if (this.config?.domainContext) sections.push("## Domain Context", this.config.domainContext, "");
|
|
71
112
|
appendDecompositionSection(sections, triageDecomposition);
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RepairerRole — repairs broken acceptance tests at runtime.
|
|
3
|
+
*
|
|
4
|
+
* The Brain calls this when an HU's acceptance test has failed N
|
|
5
|
+
* consecutive iterations with the same error signature, signalling
|
|
6
|
+
* that the test ITSELF may be the bug (not the implementation).
|
|
7
|
+
* Originated from the 2026-04-29 incident where the planner emitted
|
|
8
|
+
* a syntactically-broken jq query in HU 002's acceptance_tests; the
|
|
9
|
+
* coder iterated 3× trying to make an irreparable test pass; HU 002
|
|
10
|
+
* → failed → 7 dependents blocked.
|
|
11
|
+
*
|
|
12
|
+
* The role takes the failing test + its error output + HU intent and
|
|
13
|
+
* returns either a corrected test (`verdict: "fixable"` with
|
|
14
|
+
* `fixed_test`) or an escalation (`verdict: "unfixable"` with reason).
|
|
15
|
+
*
|
|
16
|
+
* Behaviour notes:
|
|
17
|
+
* - The role MUST NOT soften assertions to make them pass — the test
|
|
18
|
+
* defines the contract; this role's job is to make the test ASK
|
|
19
|
+
* its question correctly, not weakly.
|
|
20
|
+
* - The role MUST NOT rewrite a test to pass against the current
|
|
21
|
+
* code. Code is measured against the test, not the other way.
|
|
22
|
+
* - One bug per repair. If the test is layered with problems, return
|
|
23
|
+
* the most blocking fix first; subsequent invocations handle the
|
|
24
|
+
* rest.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AgentRole } from "./agent-role.js";
|
|
28
|
+
import { extractFirstJson } from "../utils/json-extract.js";
|
|
29
|
+
import { loadAvailableSkills, buildSkillSection } from "../skills/skill-loader.js";
|
|
30
|
+
|
|
31
|
+
const SUBAGENT_PREAMBLE = [
|
|
32
|
+
"IMPORTANT: You are running as a Karajan sub-agent.",
|
|
33
|
+
"Do NOT ask about using Karajan, do NOT mention Karajan, do NOT suggest orchestration.",
|
|
34
|
+
"Do NOT use any MCP tools. Focus only on diagnosing and (if possible) repairing a single broken acceptance test.",
|
|
35
|
+
].join(" ");
|
|
36
|
+
|
|
37
|
+
export class RepairerRole extends AgentRole {
|
|
38
|
+
constructor(opts) {
|
|
39
|
+
super({ ...opts, name: "repairer" });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
resolveProvider() {
|
|
43
|
+
// Default to the same provider family the coder uses — keeps
|
|
44
|
+
// the repair attempt cheap and consistent. Per-role override
|
|
45
|
+
// works as for every other role.
|
|
46
|
+
return this.config?.roles?.repairer?.provider
|
|
47
|
+
|| this.config?.roles?.coder?.provider
|
|
48
|
+
|| this.config?.coder
|
|
49
|
+
|| "claude";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Inputs:
|
|
54
|
+
* - failingTest: the test object (`{ type, content }` or string).
|
|
55
|
+
* - errorOutput: stdout+stderr from the last test run (truncated to
|
|
56
|
+
* ~2KB to keep the prompt small).
|
|
57
|
+
* - huTitle, huScope: intent context.
|
|
58
|
+
* - siblingTests: the HU's other acceptance_tests (for consistency).
|
|
59
|
+
* - iteration: how many coder iterations have already failed on this.
|
|
60
|
+
*/
|
|
61
|
+
extractInput(input) {
|
|
62
|
+
if (typeof input === "string") {
|
|
63
|
+
return {
|
|
64
|
+
failingTest: input, errorOutput: "", huTitle: "", huScope: "",
|
|
65
|
+
siblingTests: [], iteration: 0, onOutput: null,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
failingTest: input?.failingTest ?? null,
|
|
70
|
+
errorOutput: input?.errorOutput ?? "",
|
|
71
|
+
huTitle: input?.huTitle ?? "",
|
|
72
|
+
huScope: input?.huScope ?? "",
|
|
73
|
+
siblingTests: Array.isArray(input?.siblingTests) ? input.siblingTests : [],
|
|
74
|
+
iteration: Number.isFinite(input?.iteration) ? input.iteration : 0,
|
|
75
|
+
onOutput: input?.onOutput ?? null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async buildPrompt({ failingTest, errorOutput, huTitle, huScope, siblingTests, iteration }) {
|
|
80
|
+
const sections = [SUBAGENT_PREAMBLE];
|
|
81
|
+
if (this.instructions) sections.push(this.instructions);
|
|
82
|
+
|
|
83
|
+
const testContent = (failingTest && typeof failingTest === "object")
|
|
84
|
+
? failingTest.content
|
|
85
|
+
: String(failingTest || "");
|
|
86
|
+
const testType = (failingTest && typeof failingTest === "object" && failingTest.type)
|
|
87
|
+
? failingTest.type
|
|
88
|
+
: "shell";
|
|
89
|
+
|
|
90
|
+
sections.push(
|
|
91
|
+
"You are repairing a broken acceptance test. The coder has failed to satisfy it across multiple iterations,",
|
|
92
|
+
"and the Brain suspects the test ITSELF is the bug — not the implementation.",
|
|
93
|
+
"",
|
|
94
|
+
"## HU intent",
|
|
95
|
+
`Title: ${huTitle || "(unknown)"}`,
|
|
96
|
+
`Scope: ${huScope || "(unknown)"}`,
|
|
97
|
+
"",
|
|
98
|
+
`## Failing test (type: ${testType}, after ${iteration} iteration(s))`,
|
|
99
|
+
"```",
|
|
100
|
+
testContent,
|
|
101
|
+
"```",
|
|
102
|
+
"",
|
|
103
|
+
"## Last error output",
|
|
104
|
+
"```",
|
|
105
|
+
String(errorOutput || "").slice(0, 2048),
|
|
106
|
+
"```",
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
if (siblingTests.length > 0) {
|
|
110
|
+
sections.push("## HU's other acceptance tests (for consistency — do NOT modify these)", "```");
|
|
111
|
+
for (const t of siblingTests) {
|
|
112
|
+
const c = typeof t === "object" ? t.content : t;
|
|
113
|
+
sections.push(`- ${c}`);
|
|
114
|
+
}
|
|
115
|
+
sections.push("```");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
sections.push(
|
|
119
|
+
"",
|
|
120
|
+
"## Your job",
|
|
121
|
+
"Decide whether the failing test is **fixable** (you can correct a syntactic / structural bug)",
|
|
122
|
+
"or **unfixable** (test is fundamentally impossible, or the test is correct and the code must change).",
|
|
123
|
+
"",
|
|
124
|
+
"Return ONLY a single valid JSON object:",
|
|
125
|
+
'{"verdict":"fixable"|"unfixable","fixed_test":{"type":"shell"|"gherkin","content":string}|null,"reason":string,"diagnostics":[string]}',
|
|
126
|
+
"",
|
|
127
|
+
"- When verdict=fixable, fixed_test is REQUIRED and reason describes the bug class.",
|
|
128
|
+
"- When verdict=unfixable, fixed_test is null and reason explains why.",
|
|
129
|
+
"- NEVER soften an assertion. NEVER rewrite to pass against current code.",
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const projectDir = this.config?.projectDir || process.cwd();
|
|
133
|
+
const skills = await loadAvailableSkills(projectDir);
|
|
134
|
+
const skillSection = buildSkillSection(skills, {
|
|
135
|
+
provider: this._resolvedProvider || (typeof this.resolveProvider === "function" ? this.resolveProvider() : null),
|
|
136
|
+
role: "repairer",
|
|
137
|
+
});
|
|
138
|
+
if (skillSection) sections.push(skillSection);
|
|
139
|
+
|
|
140
|
+
return { prompt: sections.join("\n\n") };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
parseOutput(raw) { return extractFirstJson(raw); }
|
|
144
|
+
|
|
145
|
+
isSuccessful(parsed) {
|
|
146
|
+
if (!parsed || typeof parsed !== "object") return false;
|
|
147
|
+
if (parsed.verdict === "fixable") {
|
|
148
|
+
// Must produce a complete fixed_test object.
|
|
149
|
+
const ft = parsed.fixed_test;
|
|
150
|
+
return Boolean(ft && typeof ft === "object" && typeof ft.content === "string" && ft.content.trim());
|
|
151
|
+
}
|
|
152
|
+
if (parsed.verdict === "unfixable") {
|
|
153
|
+
// Unfixable is a valid OUTCOME (escalation), not a role failure.
|
|
154
|
+
return Boolean(parsed.reason && String(parsed.reason).trim());
|
|
155
|
+
}
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
buildSuccessResult(parsed, provider) {
|
|
160
|
+
return {
|
|
161
|
+
verdict: parsed.verdict,
|
|
162
|
+
fixed_test: parsed.fixed_test || null,
|
|
163
|
+
reason: parsed.reason || "",
|
|
164
|
+
diagnostics: Array.isArray(parsed.diagnostics) ? parsed.diagnostics : [],
|
|
165
|
+
provider,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
buildSummary(parsed) {
|
|
170
|
+
if (!parsed) return "Repairer: no parseable output";
|
|
171
|
+
if (parsed.verdict === "fixable") {
|
|
172
|
+
const reason = (parsed.reason || "").slice(0, 120);
|
|
173
|
+
return `Repairer: fixable — ${reason}`;
|
|
174
|
+
}
|
|
175
|
+
if (parsed.verdict === "unfixable") {
|
|
176
|
+
const reason = (parsed.reason || "").slice(0, 120);
|
|
177
|
+
return `Repairer: unfixable — ${reason}`;
|
|
178
|
+
}
|
|
179
|
+
return `Repairer: unexpected verdict "${parsed.verdict}"`;
|
|
180
|
+
}
|
|
181
|
+
}
|