karajan-code 2.34.0 → 3.1.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 +120 -38
- package/bin/kj-rag-mcp.js +75 -0
- package/docs/README.es.md +5 -5
- package/package.json +22 -17
- package/packages/hu-board/package.json +5 -2
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +20 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +453 -0
- package/packages/hu-board/public/utils/command-launcher.js +213 -0
- package/packages/hu-board/public/utils/config-editor.js +181 -0
- package/packages/hu-board/public/utils/dashboard-view.js +103 -0
- package/packages/hu-board/public/utils/formatters.js +232 -0
- package/packages/hu-board/public/utils/graph-view.js +216 -0
- package/packages/hu-board/public/utils/hu-actions.js +224 -0
- package/packages/hu-board/public/utils/init-listeners.js +180 -0
- package/packages/hu-board/public/utils/log-panel.js +222 -0
- package/packages/hu-board/public/utils/modals.js +290 -0
- package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
- package/packages/hu-board/public/utils/preflight-view.js +89 -0
- package/packages/hu-board/public/utils/project-actions.js +143 -0
- package/packages/hu-board/public/utils/project-picker-view.js +139 -0
- package/packages/hu-board/public/utils/run-launcher.js +169 -0
- package/packages/hu-board/public/utils/server-push.js +252 -0
- package/packages/hu-board/public/utils/sessions-view.js +199 -0
- package/packages/hu-board/public/utils/story-detail-view.js +324 -0
- package/packages/hu-board/public/utils/story-edit-form.js +271 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +15 -2
- package/packages/hu-board/src/routes/api.js +27 -9
- package/scripts/audit-test-diet.mjs +151 -0
- package/src/audit/deterministic-summary.js +20 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/audit/injection-findings.js +105 -0
- package/src/checks/hardware.js +150 -0
- package/src/checks/node.js +9 -10
- package/src/checks/runner.js +10 -0
- package/src/cli/register-meta.js +48 -2
- package/src/cli/register-plan.js +10 -1
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +2 -0
- package/src/commands/init.js +41 -1
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/rag-mcp.js +18 -0
- package/src/commands/sync.js +30 -2
- package/src/commands/telemetry.js +70 -0
- package/src/config/defaults.js +9 -1
- package/src/config/schema.js +2 -0
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/stages/coder-stage.js +11 -2
- package/src/orchestrator/stages/reviewer-stage.js +11 -0
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +9 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sonar/scanner.js +51 -16
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/paths.js +12 -0
- package/src/utils/quota-simulator.js +56 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/telemetry.js +34 -12
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
package/src/commands/init.js
CHANGED
|
@@ -12,6 +12,7 @@ import { createWizard, isTTY } from "../utils/wizard.js";
|
|
|
12
12
|
import { runCommand } from "../utils/process.js";
|
|
13
13
|
import { getInstallCommand } from "../utils/os-detect.js";
|
|
14
14
|
import { detectOsLocale, SUPPORTED_LANGUAGES } from "../utils/locale.js";
|
|
15
|
+
import { buildTelemetryPayload, sendTelemetryEvent } from "../utils/telemetry.js";
|
|
15
16
|
import { detectRtk } from "../utils/rtk-detect.js";
|
|
16
17
|
import { installRtk } from "../utils/rtk-install.js";
|
|
17
18
|
import { detectProjectStack } from "../utils/stack-detect.js";
|
|
@@ -256,6 +257,42 @@ async function askBoardSecurity(wizard, config, logger) {
|
|
|
256
257
|
logger.info(` -> HU Board port: ${config.hu_board.port}`);
|
|
257
258
|
}
|
|
258
259
|
|
|
260
|
+
/**
|
|
261
|
+
* Explicit, opt-in telemetry consent. We do NOT inherit a "Karajan-installed
|
|
262
|
+
* users want to help" assumption — silence and missing config keys both mean
|
|
263
|
+
* "no". The prompt is rendered in the OS locale (EN/ES today; falls back to
|
|
264
|
+
* EN for everything else) so users see it in their own language even before
|
|
265
|
+
* they've set `config.language`. See src/utils/telemetry.js and the
|
|
266
|
+
* "Privacy & Telemetry" section of README.md for what gets sent.
|
|
267
|
+
*/
|
|
268
|
+
async function askTelemetry(wizard, config, logger) {
|
|
269
|
+
const locale = detectOsLocale();
|
|
270
|
+
const prompts = {
|
|
271
|
+
es: {
|
|
272
|
+
question: "¿Quieres ayudar a mejorar Karajan enviando telemetría anónima al proyecto?",
|
|
273
|
+
detail: " (versión, OS, comandos, duración del pipeline — sin código, sin tareas, sin datos personales)",
|
|
274
|
+
previewIntro: " Ejemplo del payload exacto que se enviaría al ejecutar `kj run` (puedes inspeccionar más con `kj telemetry preview`):",
|
|
275
|
+
enabled: " -> Telemetría: activada (¡gracias!)",
|
|
276
|
+
disabled: " -> Telemetría: desactivada",
|
|
277
|
+
},
|
|
278
|
+
en: {
|
|
279
|
+
question: "Help improve Karajan by sending anonymous telemetry to the project?",
|
|
280
|
+
detail: " (version, OS, commands, pipeline duration — no code, no tasks, no personal data)",
|
|
281
|
+
previewIntro: " Example of the exact payload that would be sent when you run `kj run` (inspect more with `kj telemetry preview`):",
|
|
282
|
+
enabled: " -> Telemetry: enabled (thank you!)",
|
|
283
|
+
disabled: " -> Telemetry: disabled",
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
const t = prompts[locale] || prompts.en;
|
|
287
|
+
logger.info(t.detail);
|
|
288
|
+
const example = buildTelemetryPayload("cli_command", { version: "x.y.z", command: "run" });
|
|
289
|
+
logger.info(t.previewIntro);
|
|
290
|
+
logger.info(` ${JSON.stringify(example)}`);
|
|
291
|
+
const enabled = await wizard.confirm(t.question, false);
|
|
292
|
+
config.telemetry = enabled;
|
|
293
|
+
logger.info(enabled ? t.enabled : t.disabled);
|
|
294
|
+
}
|
|
295
|
+
|
|
259
296
|
async function runWizard(config, logger) {
|
|
260
297
|
const agents = await detectAvailableAgents();
|
|
261
298
|
const available = agents.filter((a) => a.available);
|
|
@@ -318,6 +355,10 @@ async function runWizard(config, logger) {
|
|
|
318
355
|
await askBoardSecurity(wizard, config, logger);
|
|
319
356
|
}
|
|
320
357
|
|
|
358
|
+
logger.info("");
|
|
359
|
+
logger.info("Privacy:");
|
|
360
|
+
await askTelemetry(wizard, config, logger);
|
|
361
|
+
|
|
321
362
|
logger.info("");
|
|
322
363
|
} finally {
|
|
323
364
|
wizard.close();
|
|
@@ -735,7 +776,6 @@ export async function initCommand({ logger, flags = {} }) {
|
|
|
735
776
|
await writeInitConfig(configPath, config);
|
|
736
777
|
|
|
737
778
|
// Telemetry: anonymous install event (non-blocking)
|
|
738
|
-
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
739
779
|
const { readFileSync } = await import("node:fs");
|
|
740
780
|
const { fileURLToPath } = await import("node:url");
|
|
741
781
|
try {
|
|
@@ -21,14 +21,14 @@ import { formatPlan, formatHuTable } from "./_shared.js";
|
|
|
21
21
|
/**
|
|
22
22
|
* kj plan "task" — generate plan + HUs
|
|
23
23
|
*/
|
|
24
|
-
export async function planGenerateCommand({ task, config, logger, json, context, flags = {} }) {
|
|
24
|
+
export async function planGenerateCommand({ task, config, logger, json, context, flags = {}, sourceCanvasPath = null }) {
|
|
25
25
|
const { withCliRunLog } = await import("../../utils/cli-run-log.js");
|
|
26
26
|
return withCliRunLog("plan", { projectDir: config?.projectDir, logger }, async ({ runLog }) => {
|
|
27
|
-
return planGenerateImpl({ task, config, logger, json, context, runLog, flags });
|
|
27
|
+
return planGenerateImpl({ task, config, logger, json, context, runLog, flags, sourceCanvasPath });
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {} }) {
|
|
31
|
+
async function planGenerateImpl({ task, config, logger, json, context, runLog, flags = {}, sourceCanvasPath = null }) {
|
|
32
32
|
// Auto-GC: prune orphan plans (project dir gone) + old finalised plans/
|
|
33
33
|
// sessions/HU batches before we start. Silent unless something was
|
|
34
34
|
// removed; one-liner summary on stderr in that case so --json stays
|
|
@@ -164,6 +164,12 @@ async function planGenerateImpl({ task, config, logger, json, context, runLog, f
|
|
|
164
164
|
plan.approach = parsed?.approach || (typeof parsed === "string" ? parsed : (typeof result.output === "string" ? result.output : null));
|
|
165
165
|
plan.risks = parsed?.risks || [];
|
|
166
166
|
plan.outOfScope = parsed?.outOfScope || [];
|
|
167
|
+
// KJC-TSK-0348: persist the SPEC.canvas.md path so `kj sync --apply`
|
|
168
|
+
// can later locate it and append drift notes. Only set when the plan
|
|
169
|
+
// actually came from a Canvas SPEC; flat-task plans stay unstamped.
|
|
170
|
+
if (typeof sourceCanvasPath === "string" && sourceCanvasPath.length > 0) {
|
|
171
|
+
plan.sourceCanvasPath = sourceCanvasPath;
|
|
172
|
+
}
|
|
167
173
|
|
|
168
174
|
// Convert planner steps → HUs, carrying the structured acceptance_tests
|
|
169
175
|
// the LLM was asked to emit. If the LLM fell back to legacy behaviour
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { pathToFileURL } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `kj rag mcp` — spawn the standalone RAG MCP server inline.
|
|
7
|
+
*
|
|
8
|
+
* KJC-TSK-0492 PR3. Adds CLI discoverability for the `kj-rag-mcp` binary
|
|
9
|
+
* so external IDE users can find it via the same `kj rag <subcommand>`
|
|
10
|
+
* surface they already know. The implementation dynamic-imports the
|
|
11
|
+
* binary entry so behavior stays identical to running `kj-rag-mcp`
|
|
12
|
+
* directly — same handlers, same transport, same orphan guard.
|
|
13
|
+
*/
|
|
14
|
+
export async function ragMcpCommand() {
|
|
15
|
+
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const binPath = path.resolve(moduleDir, "../../bin/kj-rag-mcp.js");
|
|
17
|
+
await import(pathToFileURL(binPath).href);
|
|
18
|
+
}
|
package/src/commands/sync.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `kj sync` — closed-loop drift detection between code and Canvas/plan.
|
|
3
|
-
* Issue #540 MVP: detection + report
|
|
3
|
+
* Issue #540 MVP: detection + report.
|
|
4
|
+
* KJC-TSK-0348: `--apply` writes drift notes back into the source Canvas
|
|
5
|
+
* markdown (with `.bak` backup) so the user can review + commit.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import fs from "node:fs";
|
|
7
9
|
import path from "node:path";
|
|
8
10
|
import { listPlans, loadPlan } from "../plan/plan-store.js";
|
|
9
11
|
import { classifyDrift, formatDriftReport } from "../sync/detect-drift.js";
|
|
12
|
+
import { applyCanvasPatch } from "../sync/apply-canvas-patch.js";
|
|
10
13
|
|
|
11
14
|
const SKIP_DIRS = new Set(["node_modules", ".git", "dist", "build", "coverage", ".karajan", ".kj", "tmp", ".cache", ".astro", ".next"]);
|
|
12
15
|
|
|
@@ -36,8 +39,11 @@ function listSourceFiles(rootDir, out = []) {
|
|
|
36
39
|
* @param {object} args.logger
|
|
37
40
|
* @param {string} [args.planId] — when set, sync against this plan; else use the latest
|
|
38
41
|
* @param {boolean} [args.json]
|
|
42
|
+
* @param {boolean} [args.apply] — when true, write a drift patch back into
|
|
43
|
+
* the Canvas markdown the plan came from
|
|
44
|
+
* (with `.bak` backup).
|
|
39
45
|
*/
|
|
40
|
-
export async function syncCommand({ config, logger, planId, json }) {
|
|
46
|
+
export async function syncCommand({ config, logger, planId, json, apply }) {
|
|
41
47
|
const projectDir = config?.projectDir || process.cwd();
|
|
42
48
|
|
|
43
49
|
// Resolve which plan to sync against.
|
|
@@ -60,14 +66,36 @@ export async function syncCommand({ config, logger, planId, json }) {
|
|
|
60
66
|
const files = listSourceFiles(projectDir);
|
|
61
67
|
const report = classifyDrift(plan, files);
|
|
62
68
|
|
|
69
|
+
let applyResult = null;
|
|
70
|
+
if (apply) {
|
|
71
|
+
if (!plan.sourceCanvasPath) {
|
|
72
|
+
throw new Error(
|
|
73
|
+
"kj sync --apply: this plan has no sourceCanvasPath — it was not generated from a SPEC.canvas.md. "
|
|
74
|
+
+ "Re-run `kj plan generate --task-file SPEC.canvas.md` so the plan remembers the Canvas it came from."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
applyResult = await applyCanvasPatch({ canvasPath: plan.sourceCanvasPath, report });
|
|
78
|
+
}
|
|
79
|
+
|
|
63
80
|
if (json) {
|
|
64
81
|
console.log(JSON.stringify({
|
|
65
82
|
planId: plan.planId,
|
|
66
83
|
projectDir,
|
|
67
84
|
...report,
|
|
85
|
+
apply: applyResult,
|
|
68
86
|
}, null, 2));
|
|
69
87
|
} else {
|
|
70
88
|
console.log(formatDriftReport(report, plan));
|
|
89
|
+
if (applyResult) {
|
|
90
|
+
if (applyResult.skipped) {
|
|
91
|
+
console.log("\nkj sync --apply: no actionable drift, Canvas left untouched.");
|
|
92
|
+
} else {
|
|
93
|
+
console.log(
|
|
94
|
+
`\nkj sync --apply: wrote ${applyResult.mutations} drift note(s) to ${applyResult.patchedPath} `
|
|
95
|
+
+ `(backup at ${applyResult.backupPath}).`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
71
99
|
}
|
|
72
100
|
|
|
73
101
|
// Exit 0 always — drift is informational, not an error. CI users
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { TELEMETRY_ENDPOINT, buildTelemetryPayload, isTelemetryEnabled } from "../utils/telemetry.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render the exact payload that would be POSTed for every event type.
|
|
5
|
+
*
|
|
6
|
+
* Read-only by design: this never opens a socket. It exists so users
|
|
7
|
+
* can inspect what telemetry sends before (or after) opting in,
|
|
8
|
+
* without grepping the source. The contract here MUST stay in sync
|
|
9
|
+
* with the producers in src/cli/_shared.js and the pipeline emitter
|
|
10
|
+
* in src/orchestrator/flow-runner.js — if a new field appears there,
|
|
11
|
+
* mirror it in the example below.
|
|
12
|
+
*/
|
|
13
|
+
function buildPreview(version) {
|
|
14
|
+
const v = version || "x.y.z";
|
|
15
|
+
return {
|
|
16
|
+
endpoint: TELEMETRY_ENDPOINT,
|
|
17
|
+
events: [
|
|
18
|
+
buildTelemetryPayload("install", { version: v }),
|
|
19
|
+
buildTelemetryPayload("cli_command", { version: v, command: "run" }),
|
|
20
|
+
buildTelemetryPayload("pipeline_complete", {
|
|
21
|
+
version: v,
|
|
22
|
+
mode: "standard",
|
|
23
|
+
agent: "claude",
|
|
24
|
+
duration_s: 42,
|
|
25
|
+
success: true,
|
|
26
|
+
taskType: "feature"
|
|
27
|
+
})
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function telemetryPreviewCommand({ config, logger, version, json }) {
|
|
33
|
+
const preview = buildPreview(version);
|
|
34
|
+
if (json) {
|
|
35
|
+
logger.info(JSON.stringify(preview, null, 2));
|
|
36
|
+
return preview;
|
|
37
|
+
}
|
|
38
|
+
const enabled = isTelemetryEnabled(config);
|
|
39
|
+
logger.info(`Telemetry endpoint: ${preview.endpoint}`);
|
|
40
|
+
logger.info(`Telemetry enabled: ${enabled ? "yes" : "no"}`);
|
|
41
|
+
logger.info("");
|
|
42
|
+
logger.info("Exact payloads that would be POSTed (one per event type):");
|
|
43
|
+
logger.info("");
|
|
44
|
+
for (const payload of preview.events) {
|
|
45
|
+
logger.info(` [${payload.event}]`);
|
|
46
|
+
logger.info(` ${JSON.stringify(payload)}`);
|
|
47
|
+
logger.info("");
|
|
48
|
+
}
|
|
49
|
+
logger.info("No data was sent — `kj telemetry preview` never opens a socket.");
|
|
50
|
+
return preview;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function telemetryStatusCommand({ config, logger, json }) {
|
|
54
|
+
const enabled = isTelemetryEnabled(config);
|
|
55
|
+
const report = {
|
|
56
|
+
enabled,
|
|
57
|
+
endpoint: TELEMETRY_ENDPOINT,
|
|
58
|
+
flipHint: enabled
|
|
59
|
+
? "Set `telemetry: false` in ~/.karajan/kj.config.yml (or re-run `kj init`) to turn it off."
|
|
60
|
+
: "Set `telemetry: true` in ~/.karajan/kj.config.yml (or re-run `kj init`) to turn it on."
|
|
61
|
+
};
|
|
62
|
+
if (json) {
|
|
63
|
+
logger.info(JSON.stringify(report, null, 2));
|
|
64
|
+
return report;
|
|
65
|
+
}
|
|
66
|
+
logger.info(`Telemetry: ${enabled ? "enabled" : "disabled"}`);
|
|
67
|
+
logger.info(`Endpoint: ${report.endpoint}`);
|
|
68
|
+
logger.info(report.flipHint);
|
|
69
|
+
return report;
|
|
70
|
+
}
|
package/src/config/defaults.js
CHANGED
|
@@ -70,6 +70,11 @@ const DEFAULTS = {
|
|
|
70
70
|
development: {
|
|
71
71
|
methodology: "tdd",
|
|
72
72
|
require_test_changes: true,
|
|
73
|
+
// KJC-TSK-0398: opt-in red-then-green gate. When true, the pipeline
|
|
74
|
+
// stashes source files and re-runs the test suite to confirm the new
|
|
75
|
+
// tests actually fail without the implementation. Off by default to
|
|
76
|
+
// preserve current iteration latency.
|
|
77
|
+
require_red_then_green: false,
|
|
73
78
|
test_file_patterns: ["/tests/", "/__tests__/", ".test.", ".spec."],
|
|
74
79
|
source_file_extensions: [".js", ".jsx", ".ts", ".tsx", ".py", ".go", ".java", ".rb", ".php", ".cs"]
|
|
75
80
|
},
|
|
@@ -196,7 +201,10 @@ const DEFAULTS = {
|
|
|
196
201
|
// hook. Both default ON; flip to false to opt out without uninstalling.
|
|
197
202
|
autoUpdate: { onCommit: true, onRun: true }
|
|
198
203
|
},
|
|
199
|
-
|
|
204
|
+
// Opt-in. Set by the `kj init` wizard (explicit consent prompt in the OS
|
|
205
|
+
// locale). When the key is absent or false, no events are sent. See
|
|
206
|
+
// src/utils/telemetry.js and the Privacy & Telemetry section of README.md.
|
|
207
|
+
telemetry: false,
|
|
200
208
|
guards: {
|
|
201
209
|
output: {
|
|
202
210
|
enabled: true,
|
package/src/config/schema.js
CHANGED
|
@@ -115,6 +115,8 @@ const PipelineMap = v.optional(v.looseObject({
|
|
|
115
115
|
const Development = v.optional(v.looseObject({
|
|
116
116
|
methodology: v.optional(Methodology),
|
|
117
117
|
require_test_changes: v.optional(v.boolean()),
|
|
118
|
+
// KJC-TSK-0398: opt-in flag; module lands in PR1, integration in PR3.
|
|
119
|
+
require_red_then_green: v.optional(v.boolean()),
|
|
118
120
|
test_file_patterns: v.optional(v.array(v.string())),
|
|
119
121
|
source_file_extensions: v.optional(v.array(v.string())),
|
|
120
122
|
}));
|
|
@@ -229,6 +229,7 @@ export function resolvePipelineFlags(config) {
|
|
|
229
229
|
securityEnabled: Boolean(config.pipeline?.security?.enabled),
|
|
230
230
|
impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
|
|
231
231
|
perfEnabled: Boolean(config.pipeline?.perf?.enabled),
|
|
232
|
+
toolJudgeEnabled: Boolean(config.pipeline?.tool_judge?.enabled),
|
|
232
233
|
reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
|
|
233
234
|
discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
|
|
234
235
|
architectEnabled: Boolean(config.pipeline?.architect?.enabled),
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from "../../iteration-stages.js";
|
|
20
20
|
import { runImpeccableStage } from "../../post-loop-stages.js";
|
|
21
21
|
import { runPerfStage } from "../../stages/perf-stage.js";
|
|
22
|
+
import { runTddDisciplineStage } from "../../stages/tdd-discipline-stage.js";
|
|
23
|
+
import { runToolJudgeStage } from "../../stages/tool-judge-stage.js";
|
|
22
24
|
import { tryCiComment } from "../../ci-integration.js";
|
|
23
25
|
|
|
24
26
|
export async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
|
|
@@ -26,6 +28,16 @@ export async function runQualityGateStages({ config, logger, emitter, eventBase,
|
|
|
26
28
|
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
27
29
|
if (tddResult.action === "continue") return { action: "continue" };
|
|
28
30
|
|
|
31
|
+
// KJC-TSK-0398 PR3: opt-in red-then-green check.
|
|
32
|
+
if (config.development?.require_red_then_green) {
|
|
33
|
+
const disc = await runTddDisciplineStage({
|
|
34
|
+
config, logger, emitter, eventBase,
|
|
35
|
+
sourceFiles: tddResult.sourceFiles || [],
|
|
36
|
+
testFiles: tddResult.testFiles || [],
|
|
37
|
+
});
|
|
38
|
+
if (disc.action === "continue") return { action: "continue" };
|
|
39
|
+
}
|
|
40
|
+
|
|
29
41
|
// Sonar runs for code tasks per policy. Since v2.7.4 it is NOT
|
|
30
42
|
// toggleable via config — that's intrinsic to Karajan. The taskType
|
|
31
43
|
// policy (resolved_policies.sonar) is the single source of truth.
|
|
@@ -84,5 +96,16 @@ export async function runQualityGateStages({ config, logger, emitter, eventBase,
|
|
|
84
96
|
if (perfResult.action === "continue") return { action: "continue" };
|
|
85
97
|
}
|
|
86
98
|
|
|
99
|
+
// KJC-TSK-0375 PR3: opt-in tool-call quality judge. Non-blocking.
|
|
100
|
+
if (pipelineFlags?.toolJudgeEnabled) {
|
|
101
|
+
const judgeResult = await runToolJudgeStage({
|
|
102
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
103
|
+
iteration: i, task,
|
|
104
|
+
});
|
|
105
|
+
if (judgeResult.stageResult) {
|
|
106
|
+
stageResults.toolJudge = judgeResult.stageResult;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
87
110
|
return { action: "ok" };
|
|
88
111
|
}
|
|
@@ -8,7 +8,7 @@ import { CoderRole } from "../../roles/coder-role.js";
|
|
|
8
8
|
import { RefactorerRole } from "../../roles/refactorer-role.js";
|
|
9
9
|
import { addCheckpoint, markSessionStatus, saveSession } from "../../session/store.js";
|
|
10
10
|
import {
|
|
11
|
-
setReviewerFeedback, incrementRetryCount, resetRetryCount,
|
|
11
|
+
setReviewerFeedback, setCoderTranscript, incrementRetryCount, resetRetryCount,
|
|
12
12
|
} from "../../session/mutators.js";
|
|
13
13
|
import { generateDiff, getUntrackedFiles } from "../../review/diff-generator.js";
|
|
14
14
|
import { evaluateTddPolicy } from "../../review/tdd-policy.js";
|
|
@@ -19,6 +19,7 @@ import { invokeSolomon } from "../solomon-escalation.js";
|
|
|
19
19
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
20
20
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
21
21
|
import { buildStandbyState } from "../../brain/standby-store.js";
|
|
22
|
+
import { applyQuotaSimulation } from "../../utils/quota-simulator.js";
|
|
22
23
|
import { snapshotHomeTopLevel, detectNewHomeEntries, formatLeakMessage, verifyLeaksAgainstTranscript, detectTranscriptCdLeaks } from "../fs-leak-detector.js";
|
|
23
24
|
|
|
24
25
|
export async function runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration, brainCtx, acceptanceTests = null, adrs = null, specSection = null, reviewerFindings = null, huId = null }) {
|
|
@@ -76,6 +77,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
76
77
|
coderStall.stop();
|
|
77
78
|
}
|
|
78
79
|
trackBudget({ role: "coder", provider: coderRole.provider, model: coderRole.model, result: coderExecResult.result, duration_ms: Date.now() - coderStart });
|
|
80
|
+
applyQuotaSimulation(coderExecResult, { iteration, agent: coderRole.provider });
|
|
79
81
|
|
|
80
82
|
if (!coderExecResult.ok) {
|
|
81
83
|
const details = coderExecResult.result?.error || coderExecResult.summary || "unknown error";
|
|
@@ -221,6 +223,12 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
221
223
|
logger?.warn?.(`[verification-gate] Skipped: ${err?.message || err}`);
|
|
222
224
|
}
|
|
223
225
|
|
|
226
|
+
// KJC-TSK-0375 PR3: stash the coder's raw transcript on the session so
|
|
227
|
+
// the optional tool-judge stage can extract structured tool calls later
|
|
228
|
+
// without re-invoking the coder. Cleared between iterations by the coder
|
|
229
|
+
// overwriting it; ignored when the tool-judge flag is off.
|
|
230
|
+
setCoderTranscript(session, coderTranscript);
|
|
231
|
+
|
|
224
232
|
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null, filesChanged });
|
|
225
233
|
emitProgress(
|
|
226
234
|
emitter,
|
|
@@ -281,6 +289,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
281
289
|
refactorerStall.stop();
|
|
282
290
|
}
|
|
283
291
|
trackBudget({ role: "refactorer", provider: refactorerRole.provider, model: refactorerRole.model, result: refResult.result, duration_ms: Date.now() - refactorerStart });
|
|
292
|
+
applyQuotaSimulation(refResult, { iteration, agent: refactorerRole.provider });
|
|
284
293
|
if (!refResult.ok) {
|
|
285
294
|
const details = refResult.result?.error || refResult.summary || "unknown error";
|
|
286
295
|
// Quota cap → hibernate cleanly, same as the coder stage above.
|
|
@@ -456,5 +465,5 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
|
|
|
456
465
|
return handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion, task, brainCtx });
|
|
457
466
|
}
|
|
458
467
|
|
|
459
|
-
return { action: "ok" };
|
|
468
|
+
return { action: "ok", sourceFiles: tddEval.sourceFiles, testFiles: tddEval.testFiles };
|
|
460
469
|
}
|
|
@@ -13,6 +13,7 @@ import { runReviewerWithFallback } from "../reviewer-fallback.js";
|
|
|
13
13
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
14
14
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
15
15
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
16
|
+
import { applyQuotaSimulation } from "../../utils/quota-simulator.js";
|
|
16
17
|
|
|
17
18
|
function categorizeIssues(issues) {
|
|
18
19
|
const categories = { security: 0, correctness: 0, tests: 0, style: 0, other: 0 };
|
|
@@ -269,6 +270,16 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
269
270
|
reviewerStall.stop();
|
|
270
271
|
}
|
|
271
272
|
|
|
273
|
+
if (applyQuotaSimulation(reviewerExec.execResult, { iteration, agent: reviewerRole.provider })) {
|
|
274
|
+
// Reviewer wraps the agent call: detectRateLimit reads `lastAttempt.result.error`,
|
|
275
|
+
// so propagate the synthetic line into the most recent attempt too.
|
|
276
|
+
const last = reviewerExec.attempts?.at(-1);
|
|
277
|
+
if (last) {
|
|
278
|
+
if (!last.result || typeof last.result !== "object") last.result = {};
|
|
279
|
+
last.result.error = reviewerExec.execResult.result.error;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
272
283
|
if (!reviewerExec.execResult?.ok) {
|
|
273
284
|
const lastAttempt = reviewerExec.attempts.at(-1);
|
|
274
285
|
const details =
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// TDD discipline stage (KJC-TSK-0398 PR3).
|
|
2
|
+
// Runs the red-then-green check when `development.require_red_then_green`
|
|
3
|
+
// is true. Wraps `verifyTddDiscipline` with the surgical stash helper and
|
|
4
|
+
// the project's detected test framework. Gate is opt-in; an unsupported
|
|
5
|
+
// framework reports a warning and does not block the iteration.
|
|
6
|
+
|
|
7
|
+
import { detectTestFramework } from "../../utils/project-detect.js";
|
|
8
|
+
import { verifyTddDiscipline } from "../../review/tdd-discipline.js";
|
|
9
|
+
import { stashFilesAside, restoreFiles } from "../../review/git-stash-helper.js";
|
|
10
|
+
import { runCommand } from "../../utils/process.js";
|
|
11
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
12
|
+
|
|
13
|
+
const TDD_DISCIPLINE_COMMANDS = {
|
|
14
|
+
vitest: ["npx", ["vitest", "run", "--reporter=verbose"]],
|
|
15
|
+
jest: ["npx", ["jest", "--verbose"]],
|
|
16
|
+
pytest: ["pytest", ["--tb=short"]],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export async function runTddDisciplineStage({
|
|
20
|
+
config, logger, emitter, eventBase, sourceFiles = [], testFiles = [],
|
|
21
|
+
detectFramework = detectTestFramework,
|
|
22
|
+
runTestsImpl,
|
|
23
|
+
stashImpl = stashFilesAside,
|
|
24
|
+
restoreImpl = restoreFiles,
|
|
25
|
+
} = {}) {
|
|
26
|
+
const projectDir = config?.projectDir || process.cwd();
|
|
27
|
+
const detection = await detectFramework(projectDir);
|
|
28
|
+
const framework = detection?.framework || null;
|
|
29
|
+
|
|
30
|
+
const runTests = runTestsImpl || (async () => {
|
|
31
|
+
const cmd = TDD_DISCIPLINE_COMMANDS[framework];
|
|
32
|
+
if (!cmd) return { exitCode: 0, output: "" };
|
|
33
|
+
const res = await runCommand(cmd[0], cmd[1], { cwd: projectDir, reject: false });
|
|
34
|
+
return { exitCode: res.exitCode ?? 0, output: `${res.stdout || ""}\n${res.stderr || ""}` };
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await verifyTddDiscipline({
|
|
38
|
+
projectDir, sourceFiles, testFiles, framework,
|
|
39
|
+
runTests,
|
|
40
|
+
stashFiles: (files) => stashImpl(files, { projectDir }),
|
|
41
|
+
restoreFiles: (token) => restoreImpl(token, { projectDir }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
emitProgress(emitter, makeEvent("tdd:discipline", { ...eventBase, stage: "tdd" }, {
|
|
45
|
+
status: result.ok ? "ok" : "fail",
|
|
46
|
+
message: result.message || `TDD discipline: ${result.reason}`,
|
|
47
|
+
detail: { reason: result.reason, framework },
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
if (result.warning) logger?.warn?.(result.warning);
|
|
51
|
+
if (!result.ok) {
|
|
52
|
+
logger?.warn?.(`TDD discipline violation: ${result.reason}`);
|
|
53
|
+
return { action: "continue", result };
|
|
54
|
+
}
|
|
55
|
+
return { action: "ok", result };
|
|
56
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// KJC-TSK-0375 PR3 — ToolJudgeStage.
|
|
2
|
+
// Opt-in (`pipeline.tool_judge.enabled`). Scores the coder's tool calls
|
|
3
|
+
// extracted from `session.last_coder_transcript`. Non-blocking: a low
|
|
4
|
+
// score emits a warn event and surfaces in summary but never gates the
|
|
5
|
+
// iteration loop. Wiring lives in `quality-gates.js`.
|
|
6
|
+
|
|
7
|
+
import { ToolCorrectnessJudgeRole } from "../../roles/tool-correctness-judge-role.js";
|
|
8
|
+
import { extractToolCalls } from "../../utils/tool-call-extractor.js";
|
|
9
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
10
|
+
|
|
11
|
+
export async function runToolJudgeStage({
|
|
12
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
13
|
+
iteration, task,
|
|
14
|
+
extractFn = extractToolCalls,
|
|
15
|
+
RoleClass = ToolCorrectnessJudgeRole,
|
|
16
|
+
} = {}) {
|
|
17
|
+
logger?.setContext?.({ iteration, stage: "tool-judge" });
|
|
18
|
+
emitProgress(emitter, makeEvent("toolJudge:start", { ...eventBase, stage: "tool-judge" }, {
|
|
19
|
+
message: "Tool-judge scoring coder tool calls",
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const transcript = session?.last_coder_transcript || "";
|
|
23
|
+
const provider = coderRole?.provider || "claude";
|
|
24
|
+
const toolCalls = extractFn({ transcript, provider });
|
|
25
|
+
|
|
26
|
+
const role = new RoleClass({ config, logger, emitter });
|
|
27
|
+
const roleResult = await role.execute({ task, toolCalls });
|
|
28
|
+
|
|
29
|
+
try { trackBudget?.("tool-judge", roleResult.usage || { tokens_in: 0, tokens_out: 0, cost_usd: 0 }); } catch { /* best-effort */ }
|
|
30
|
+
|
|
31
|
+
const r = roleResult.result;
|
|
32
|
+
const status = r?.skipped ? "skip" : r?.lowQuality ? "warn" : roleResult.ok ? "ok" : "fail";
|
|
33
|
+
|
|
34
|
+
emitProgress(emitter, makeEvent("toolJudge:end", { ...eventBase, stage: "tool-judge" }, {
|
|
35
|
+
status,
|
|
36
|
+
message: roleResult.summary || "tool-judge done",
|
|
37
|
+
detail: { score: r?.score ?? null, threshold: r?.threshold ?? null, skipped: Boolean(r?.skipped), callCount: toolCalls.length },
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
action: "ok",
|
|
42
|
+
stageResult: {
|
|
43
|
+
ok: roleResult.ok !== false,
|
|
44
|
+
score: r?.score ?? null,
|
|
45
|
+
lowQuality: Boolean(r?.lowQuality),
|
|
46
|
+
skipped: Boolean(r?.skipped),
|
|
47
|
+
threshold: r?.threshold ?? null,
|
|
48
|
+
summary: roleResult.summary || "",
|
|
49
|
+
breakdown: r?.breakdown || [],
|
|
50
|
+
callCount: toolCalls.length,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// KJC-TSK-0375 — Prompt builder for the ToolCorrectnessJudge role.
|
|
2
|
+
// Inspired by deepeval's tool-correctness metric.
|
|
3
|
+
|
|
4
|
+
const PREAMBLE = "IMPORTANT: You are running as a Karajan sub-agent. "
|
|
5
|
+
+ "Do NOT use any MCP tools, do NOT mention Karajan. "
|
|
6
|
+
+ "Your job is to judge another agent's tool choices.";
|
|
7
|
+
|
|
8
|
+
export function buildToolCorrectnessJudgePrompt({ task, toolCalls, instructions }) {
|
|
9
|
+
const calls = toolCalls
|
|
10
|
+
.map((c, i) => `${i + 1}. **${c.tool}** — args: ${JSON.stringify(c.args)}`)
|
|
11
|
+
.join("\n");
|
|
12
|
+
const body = [
|
|
13
|
+
"You are the ToolCorrectnessJudge for Karajan Code.",
|
|
14
|
+
"Input: (a) a task description and (b) the ordered tool calls an LLM coder",
|
|
15
|
+
"made (Read/Edit/Write/Bash/Glob/Grep/MultiEdit). Judge each call on:",
|
|
16
|
+
" - tool choice: right tool for the intent?",
|
|
17
|
+
" - argument quality: paths plausible, commands valid, no garbage placeholders?",
|
|
18
|
+
"Per-call verdict: `correct=true|false` + one-sentence `reason`.",
|
|
19
|
+
"Aggregate into a single 0-100 score:",
|
|
20
|
+
" - 100 = every call right with clean args",
|
|
21
|
+
" - 70-99 = mostly right, minor missteps",
|
|
22
|
+
" - 40-69 = several wrong choices, budget wasted",
|
|
23
|
+
" - 0-39 = predominantly bad, retry advised",
|
|
24
|
+
"Return a single valid JSON object and nothing else.",
|
|
25
|
+
'Schema: {"score":0-100,"summary":string,"breakdown":[{"tool":string,"args":object,"correct":boolean,"reason":string}]}',
|
|
26
|
+
].join("\n");
|
|
27
|
+
const sections = [PREAMBLE];
|
|
28
|
+
if (instructions) sections.push(instructions);
|
|
29
|
+
sections.push(body, `## Task\n${task}`, `## Tool calls (${toolCalls.length})\n${calls || "(no tool calls captured)"}`);
|
|
30
|
+
return sections.join("\n\n");
|
|
31
|
+
}
|