karajan-code 3.0.0 → 3.2.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 +14 -6
- package/docs/README.es.md +1 -1
- package/package.json +8 -2
- package/packages/hu-board/package.json +3 -1
- package/packages/hu-board/public/app.js +81 -4025
- package/packages/hu-board/public/index.html +21 -0
- package/packages/hu-board/public/styles.css +32 -0
- package/packages/hu-board/public/utils/api.js +139 -0
- package/packages/hu-board/public/utils/board-view.js +462 -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 +273 -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/public/wiki.html +32 -0
- package/packages/hu-board/public/wiki.js +53 -0
- package/packages/hu-board/src/config-yaml.js +14 -0
- package/packages/hu-board/src/db.js +93 -2
- package/packages/hu-board/src/format.js +79 -0
- package/packages/hu-board/src/plan-mutations.js +4 -4
- package/packages/hu-board/src/routes/api.js +43 -10
- package/packages/hu-board/src/routes/rag.js +1 -1
- package/packages/hu-board/src/routes/wiki.js +43 -0
- package/packages/hu-board/src/run-tracker.js +1 -1
- package/packages/hu-board/src/server.js +4 -2
- package/packages/hu-board/src/sync.js +1 -1
- package/scripts/audit-test-diet.mjs +151 -0
- package/scripts/git-hooks/post-merge +13 -2
- package/src/audit/ai-slop-findings.js +102 -0
- package/src/audit/deterministic-summary.js +18 -0
- package/src/audit/harness-scorecard.js +52 -3
- package/src/audit/harness-section.js +6 -2
- package/src/brain/standby-scheduler.js +10 -92
- package/src/brain/standby-store.js +12 -168
- package/src/checks/ai-trash.js +34 -0
- package/src/checks/qmd.js +41 -0
- package/src/checks/runner.js +10 -0
- package/src/checks/squeezr.js +36 -0
- package/src/cli/register-meta.js +36 -2
- package/src/cli/register-pipeline.js +3 -0
- package/src/cli/register-plan.js +10 -1
- package/src/commands/audit.js +6 -4
- package/src/commands/board.js +34 -0
- package/src/commands/clean.js +62 -0
- package/src/commands/doctor.js +6 -0
- package/src/commands/init.js +73 -8
- package/src/commands/plan/generate.js +9 -3
- package/src/commands/qmd.js +86 -0
- package/src/commands/resume.js +6 -45
- package/src/commands/run.js +6 -50
- package/src/commands/sync.js +30 -2
- package/src/config/defaults.js +5 -0
- package/src/config/schema.js +2 -0
- package/src/git/hu-snapshot.js +10 -105
- package/src/orchestrator/config-init.js +1 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
- package/src/orchestrator/drivers/run-hu-batch.js +5 -1
- package/src/orchestrator/hu-sub-pipeline.js +52 -8
- package/src/orchestrator/stages/coder-stage.js +8 -2
- package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
- package/src/orchestrator/stages/tool-judge-stage.js +53 -0
- package/src/plan/plan-hu-ops.js +16 -338
- package/src/plan/plan-id.js +4 -45
- package/src/plan/plan-validation.js +4 -82
- package/src/prompts/tool-correctness-judge.js +31 -0
- package/src/rag/vec-store.js +17 -253
- package/src/review/git-stash-helper.js +104 -0
- package/src/review/tdd-discipline.js +90 -0
- package/src/roles/audit-role.js +11 -1
- package/src/roles/tool-correctness-judge-role.js +78 -0
- package/src/session/mutators.js +4 -0
- package/src/sync/apply-canvas-patch.js +55 -0
- package/src/utils/ai-trash.js +48 -0
- package/src/utils/atomic-write.js +6 -43
- package/src/utils/cli-ask-question.js +76 -0
- package/src/utils/doc-links.js +2 -0
- package/src/utils/os-detect.js +10 -0
- package/src/utils/paths.js +14 -111
- package/src/utils/port-check.js +6 -45
- package/src/utils/process.js +4 -188
- package/src/utils/qmd-collection.js +72 -0
- package/src/utils/qmd-detect.js +18 -0
- package/src/utils/qmd-install.js +37 -0
- package/src/utils/repo-cleaner.js +79 -0
- package/src/utils/run-registry.js +9 -116
- package/src/utils/shared-paths.js +12 -41
- package/src/utils/squeezr-detect.js +18 -0
- package/src/utils/squeezr-install.js +37 -0
- package/src/utils/tool-call-extractor.js +56 -0
- package/src/utils/vector-store-orphans.js +89 -0
|
@@ -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
|
}
|
|
@@ -302,10 +302,26 @@ function wirePlanCallbacks({ session, loadedPlan, projectDir, syncResultsToPlan,
|
|
|
302
302
|
// PR3: per-HU outcome. Stamped once per HU at runSingleHu's exit
|
|
303
303
|
// so the board can show the 📄 indicator as soon as each HU
|
|
304
304
|
// finishes (iterations / duration / commits / blockers / summary).
|
|
305
|
+
//
|
|
306
|
+
// Cost D (KJC-TSK-0515): the outcome may carry `cost_usd` (USD
|
|
307
|
+
// spent on this HU). When present and finite, also persist it to
|
|
308
|
+
// board.db so the per-card badge (Cost F) and the project total
|
|
309
|
+
// chip (Cost G) have data to show — without this write, the
|
|
310
|
+
// `cost_usd` column would always stay NULL and the UI would render
|
|
311
|
+
// nothing. Lazy-import to keep the pre-loop phase free of board
|
|
312
|
+
// deps until we actually need them.
|
|
305
313
|
setLiveOutcomeUpdater(session, async (huId, outcome) => {
|
|
306
314
|
try {
|
|
307
315
|
if (!setHuOutcomeFn(loadedPlan, huId, outcome)) return;
|
|
308
316
|
await savePlanToDisk(projectDir, loadedPlan);
|
|
317
|
+
if (outcome && Number.isFinite(outcome.cost_usd)) {
|
|
318
|
+
try {
|
|
319
|
+
const { setStoryCost } = await import("../../../../packages/hu-board/src/db.js");
|
|
320
|
+
setStoryCost(huId, outcome.cost_usd);
|
|
321
|
+
} catch (err) {
|
|
322
|
+
logger?.warn?.(`Cost write failed for ${huId}: ${err.message}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
309
325
|
} catch (err) {
|
|
310
326
|
logger?.warn?.(`Live outcome update failed for ${huId}: ${err.message}`);
|
|
311
327
|
}
|
|
@@ -360,7 +360,11 @@ export async function runHuBatch({ ctx, task, askQuestion, emitter, logger }) {
|
|
|
360
360
|
onStatusChange: ctx.session?._liveStatusUpdater || null,
|
|
361
361
|
// PR3 (per-HU outcome): same pattern, but for the rich
|
|
362
362
|
// outcome blob written ONCE per HU at the end of runSingleHu.
|
|
363
|
-
onOutcome: ctx.session?._liveOutcomeUpdater || null
|
|
363
|
+
onOutcome: ctx.session?._liveOutcomeUpdater || null,
|
|
364
|
+
// Cost D (KJC-TSK-0515): the sub-pipeline slices BudgetTracker
|
|
365
|
+
// entries per HU and stamps `outcome.cost_usd` so the board can
|
|
366
|
+
// show per-HU spend. Without this the tracker is invisible to it.
|
|
367
|
+
budgetTracker: ctx.budgetTracker || null
|
|
364
368
|
});
|
|
365
369
|
|
|
366
370
|
emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
|
|
@@ -74,6 +74,37 @@ function buildHuTask(story) {
|
|
|
74
74
|
return `Implement HU ${story.id}`;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Sum the cost_usd of a slice of BudgetTracker entries. Returns null
|
|
79
|
+
* (not 0) when the slice is empty or contains no usable cost data —
|
|
80
|
+
* the board uses null as "unmeasured" and renders no badge, while 0
|
|
81
|
+
* means "really free run". Mixing the two would silently hide free
|
|
82
|
+
* runs and falsely show "$0.00" for runs with no telemetry.
|
|
83
|
+
*
|
|
84
|
+
* Cost D (KJC-TSK-0515): the orchestrator calls this once per HU at
|
|
85
|
+
* runSingleHu exit, passing the slice of entries recorded during that
|
|
86
|
+
* HU (snapshotted via the entries.length cursor before the iteration
|
|
87
|
+
* starts). The resulting number flows through buildHuOutcome →
|
|
88
|
+
* notifyOutcome → setStoryCost in inject-loaded-plan.js.
|
|
89
|
+
*
|
|
90
|
+
* @param {Array<{cost_usd?: number}>} entries
|
|
91
|
+
* @returns {number|null}
|
|
92
|
+
*/
|
|
93
|
+
function sumCostUsd(entries) {
|
|
94
|
+
if (!Array.isArray(entries) || entries.length === 0) return null;
|
|
95
|
+
let total = 0;
|
|
96
|
+
let hasAny = false;
|
|
97
|
+
for (const e of entries) {
|
|
98
|
+
const n = Number(e?.cost_usd);
|
|
99
|
+
if (Number.isFinite(n)) {
|
|
100
|
+
total += n;
|
|
101
|
+
hasAny = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (!hasAny) return null;
|
|
105
|
+
return Math.round(total * 1e6) / 1e6;
|
|
106
|
+
}
|
|
107
|
+
|
|
77
108
|
/**
|
|
78
109
|
* Build the per-HU outcome blob from whatever metadata the iteration
|
|
79
110
|
* loop returned. Tolerant by design — most fields are optional and we
|
|
@@ -90,9 +121,11 @@ function buildHuTask(story) {
|
|
|
90
121
|
* @param {object} args.iterResult — whatever runIterationFn returned
|
|
91
122
|
* @param {string} args.status — HU_STATUS.* terminal value
|
|
92
123
|
* @param {number} args.startedAt — Date.now() when runSingleHu started
|
|
124
|
+
* @param {Array} [args.huBudgetEntries] — slice of BudgetTracker
|
|
125
|
+
* entries recorded during this HU. Cost D (KJC-TSK-0515).
|
|
93
126
|
* @returns {object}
|
|
94
127
|
*/
|
|
95
|
-
function buildHuOutcome({ story: _story, iterResult, status, startedAt }) {
|
|
128
|
+
function buildHuOutcome({ story: _story, iterResult, status, startedAt, huBudgetEntries }) {
|
|
96
129
|
const r = iterResult || {};
|
|
97
130
|
const git = r.git || {};
|
|
98
131
|
// Iterations are tracked in the standard pipeline as
|
|
@@ -123,6 +156,7 @@ function buildHuOutcome({ story: _story, iterResult, status, startedAt }) {
|
|
|
123
156
|
pr_url: r.pr_url || r.prUrl || git.pr_url || null,
|
|
124
157
|
blockers,
|
|
125
158
|
summary,
|
|
159
|
+
cost_usd: sumCostUsd(huBudgetEntries),
|
|
126
160
|
finishedAt: new Date().toISOString(),
|
|
127
161
|
};
|
|
128
162
|
}
|
|
@@ -134,7 +168,7 @@ function buildHuOutcome({ story: _story, iterResult, status, startedAt }) {
|
|
|
134
168
|
* @param {object} params
|
|
135
169
|
* @returns {Promise<{huId: string, approved: boolean, result?: object, error?: string, blockedDependents?: string[]}>}
|
|
136
170
|
*/
|
|
137
|
-
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, onStatusChange = null, onOutcome = null }) {
|
|
171
|
+
async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emitter, eventBase, logger, config, results, worktreePath, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
138
172
|
// PR1 (live HU status): every saveHuBatch should also notify the
|
|
139
173
|
// plan JSON so the board reflects state in real time. Defined as a
|
|
140
174
|
// local helper so we don't repeat the try/null-check boilerplate.
|
|
@@ -157,6 +191,16 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
157
191
|
// outcome (wall-clock per HU is what the user needs in the board,
|
|
158
192
|
// not internal stage timings).
|
|
159
193
|
const huStartedAt = Date.now();
|
|
194
|
+
// Cost D (KJC-TSK-0515): snapshot the budget cursor so we can slice
|
|
195
|
+
// out only the entries this HU produced. BudgetTracker is session-
|
|
196
|
+
// scoped (it accumulates across all HUs of the run), so without this
|
|
197
|
+
// cursor every HU after the first would inherit the previous ones'
|
|
198
|
+
// cost too. Null-safe — older callers without a tracker still work.
|
|
199
|
+
const huBudgetStartIdx = budgetTracker?.entries?.length ?? 0;
|
|
200
|
+
const sliceHuBudget = () => {
|
|
201
|
+
if (!budgetTracker?.entries) return [];
|
|
202
|
+
return budgetTracker.entries.slice(huBudgetStartIdx);
|
|
203
|
+
};
|
|
160
204
|
const story = batch.stories.find(s => s.id === storyId);
|
|
161
205
|
|
|
162
206
|
// --- Lazy refinement ---
|
|
@@ -226,7 +270,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
226
270
|
await saveHuBatch(batchSessionId, batch);
|
|
227
271
|
await notifyStatus(story.id, HU_STATUS.DONE);
|
|
228
272
|
// PR3: build + persist the per-HU outcome.
|
|
229
|
-
const okOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.DONE, startedAt: huStartedAt });
|
|
273
|
+
const okOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.DONE, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
230
274
|
await notifyOutcome(story.id, okOutcome);
|
|
231
275
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
232
276
|
message: `HU ${story.id} status → done`,
|
|
@@ -242,7 +286,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
242
286
|
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
243
287
|
await saveHuBatch(batchSessionId, batch);
|
|
244
288
|
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
245
|
-
const failOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.FAILED, startedAt: huStartedAt });
|
|
289
|
+
const failOutcome = buildHuOutcome({ story, iterResult, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
246
290
|
await notifyOutcome(story.id, failOutcome);
|
|
247
291
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
248
292
|
message: `HU ${story.id} status → failed`,
|
|
@@ -259,7 +303,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
259
303
|
updateStoryStatus(batch, story.id, HU_STATUS.FAILED);
|
|
260
304
|
await saveHuBatch(batchSessionId, batch);
|
|
261
305
|
await notifyStatus(story.id, HU_STATUS.FAILED);
|
|
262
|
-
const errOutcome = buildHuOutcome({ story, iterResult: { error: err.message }, status: HU_STATUS.FAILED, startedAt: huStartedAt });
|
|
306
|
+
const errOutcome = buildHuOutcome({ story, iterResult: { error: err.message }, status: HU_STATUS.FAILED, startedAt: huStartedAt, huBudgetEntries: sliceHuBudget() });
|
|
263
307
|
await notifyOutcome(story.id, errOutcome);
|
|
264
308
|
emitProgress(emitter, makeEvent("hu:status-change", { ...eventBase, stage: "hu-sub-pipeline" }, {
|
|
265
309
|
message: `HU ${story.id} status → failed`,
|
|
@@ -287,7 +331,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
287
331
|
* @param {object} params.logger - Logger instance
|
|
288
332
|
* @returns {Promise<{approved: boolean, results: object[], blockedIds: string[]}>}
|
|
289
333
|
*/
|
|
290
|
-
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null, onStatusChange = null, onOutcome = null }) {
|
|
334
|
+
export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitter, eventBase, logger, config = null, onStatusChange = null, onOutcome = null, budgetTracker = null }) {
|
|
291
335
|
const batchSessionId = huReviewerResult.batchSessionId;
|
|
292
336
|
let batch;
|
|
293
337
|
try {
|
|
@@ -404,7 +448,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
404
448
|
// Single HU: run as before, no worktree needed
|
|
405
449
|
const singleResult = await runSingleHu({
|
|
406
450
|
storyId: runnableIds[0], batch, batchSessionId, runIterationFn,
|
|
407
|
-
emitter, eventBase, logger, config, results, onStatusChange, onOutcome
|
|
451
|
+
emitter, eventBase, logger, config, results, onStatusChange, onOutcome, budgetTracker
|
|
408
452
|
});
|
|
409
453
|
results.push(singleResult);
|
|
410
454
|
if (!singleResult.approved) {
|
|
@@ -443,7 +487,7 @@ export async function runHuSubPipeline({ huReviewerResult, runIterationFn, emitt
|
|
|
443
487
|
storyId, batch, batchSessionId, runIterationFn,
|
|
444
488
|
emitter, eventBase, logger, config, results,
|
|
445
489
|
worktreePath: worktrees.get(storyId),
|
|
446
|
-
onStatusChange, onOutcome
|
|
490
|
+
onStatusChange, onOutcome, budgetTracker
|
|
447
491
|
});
|
|
448
492
|
});
|
|
449
493
|
|
|
@@ -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";
|
|
@@ -223,6 +223,12 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
223
223
|
logger?.warn?.(`[verification-gate] Skipped: ${err?.message || err}`);
|
|
224
224
|
}
|
|
225
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
|
+
|
|
226
232
|
await addCheckpoint(session, { stage: "coder", iteration, note: "Coder applied changes", provider: coderRole.provider, model: coderRole.model || null, filesChanged });
|
|
227
233
|
emitProgress(
|
|
228
234
|
emitter,
|
|
@@ -459,5 +465,5 @@ export async function runTddCheckStage({ config, logger, emitter, eventBase, ses
|
|
|
459
465
|
return handleTddFailure({ tddEval, config, logger, emitter, eventBase, session, iteration, askQuestion, task, brainCtx });
|
|
460
466
|
}
|
|
461
467
|
|
|
462
|
-
return { action: "ok" };
|
|
468
|
+
return { action: "ok", sourceFiles: tddEval.sourceFiles, testFiles: tddEval.testFiles };
|
|
463
469
|
}
|
|
@@ -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
|
+
}
|