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.
Files changed (104) hide show
  1. package/README.md +14 -6
  2. package/docs/README.es.md +1 -1
  3. package/package.json +8 -2
  4. package/packages/hu-board/package.json +3 -1
  5. package/packages/hu-board/public/app.js +81 -4025
  6. package/packages/hu-board/public/index.html +21 -0
  7. package/packages/hu-board/public/styles.css +32 -0
  8. package/packages/hu-board/public/utils/api.js +139 -0
  9. package/packages/hu-board/public/utils/board-view.js +462 -0
  10. package/packages/hu-board/public/utils/command-launcher.js +213 -0
  11. package/packages/hu-board/public/utils/config-editor.js +181 -0
  12. package/packages/hu-board/public/utils/dashboard-view.js +103 -0
  13. package/packages/hu-board/public/utils/formatters.js +273 -0
  14. package/packages/hu-board/public/utils/graph-view.js +216 -0
  15. package/packages/hu-board/public/utils/hu-actions.js +224 -0
  16. package/packages/hu-board/public/utils/init-listeners.js +180 -0
  17. package/packages/hu-board/public/utils/log-panel.js +222 -0
  18. package/packages/hu-board/public/utils/modals.js +290 -0
  19. package/packages/hu-board/public/utils/plan-rollup-view.js +53 -0
  20. package/packages/hu-board/public/utils/preflight-view.js +89 -0
  21. package/packages/hu-board/public/utils/project-actions.js +143 -0
  22. package/packages/hu-board/public/utils/project-picker-view.js +139 -0
  23. package/packages/hu-board/public/utils/run-launcher.js +169 -0
  24. package/packages/hu-board/public/utils/server-push.js +252 -0
  25. package/packages/hu-board/public/utils/sessions-view.js +199 -0
  26. package/packages/hu-board/public/utils/story-detail-view.js +324 -0
  27. package/packages/hu-board/public/utils/story-edit-form.js +271 -0
  28. package/packages/hu-board/public/wiki.html +32 -0
  29. package/packages/hu-board/public/wiki.js +53 -0
  30. package/packages/hu-board/src/config-yaml.js +14 -0
  31. package/packages/hu-board/src/db.js +93 -2
  32. package/packages/hu-board/src/format.js +79 -0
  33. package/packages/hu-board/src/plan-mutations.js +4 -4
  34. package/packages/hu-board/src/routes/api.js +43 -10
  35. package/packages/hu-board/src/routes/rag.js +1 -1
  36. package/packages/hu-board/src/routes/wiki.js +43 -0
  37. package/packages/hu-board/src/run-tracker.js +1 -1
  38. package/packages/hu-board/src/server.js +4 -2
  39. package/packages/hu-board/src/sync.js +1 -1
  40. package/scripts/audit-test-diet.mjs +151 -0
  41. package/scripts/git-hooks/post-merge +13 -2
  42. package/src/audit/ai-slop-findings.js +102 -0
  43. package/src/audit/deterministic-summary.js +18 -0
  44. package/src/audit/harness-scorecard.js +52 -3
  45. package/src/audit/harness-section.js +6 -2
  46. package/src/brain/standby-scheduler.js +10 -92
  47. package/src/brain/standby-store.js +12 -168
  48. package/src/checks/ai-trash.js +34 -0
  49. package/src/checks/qmd.js +41 -0
  50. package/src/checks/runner.js +10 -0
  51. package/src/checks/squeezr.js +36 -0
  52. package/src/cli/register-meta.js +36 -2
  53. package/src/cli/register-pipeline.js +3 -0
  54. package/src/cli/register-plan.js +10 -1
  55. package/src/commands/audit.js +6 -4
  56. package/src/commands/board.js +34 -0
  57. package/src/commands/clean.js +62 -0
  58. package/src/commands/doctor.js +6 -0
  59. package/src/commands/init.js +73 -8
  60. package/src/commands/plan/generate.js +9 -3
  61. package/src/commands/qmd.js +86 -0
  62. package/src/commands/resume.js +6 -45
  63. package/src/commands/run.js +6 -50
  64. package/src/commands/sync.js +30 -2
  65. package/src/config/defaults.js +5 -0
  66. package/src/config/schema.js +2 -0
  67. package/src/git/hu-snapshot.js +10 -105
  68. package/src/orchestrator/config-init.js +1 -0
  69. package/src/orchestrator/drivers/iteration-phases/quality-gates.js +23 -0
  70. package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +16 -0
  71. package/src/orchestrator/drivers/run-hu-batch.js +5 -1
  72. package/src/orchestrator/hu-sub-pipeline.js +52 -8
  73. package/src/orchestrator/stages/coder-stage.js +8 -2
  74. package/src/orchestrator/stages/tdd-discipline-stage.js +56 -0
  75. package/src/orchestrator/stages/tool-judge-stage.js +53 -0
  76. package/src/plan/plan-hu-ops.js +16 -338
  77. package/src/plan/plan-id.js +4 -45
  78. package/src/plan/plan-validation.js +4 -82
  79. package/src/prompts/tool-correctness-judge.js +31 -0
  80. package/src/rag/vec-store.js +17 -253
  81. package/src/review/git-stash-helper.js +104 -0
  82. package/src/review/tdd-discipline.js +90 -0
  83. package/src/roles/audit-role.js +11 -1
  84. package/src/roles/tool-correctness-judge-role.js +78 -0
  85. package/src/session/mutators.js +4 -0
  86. package/src/sync/apply-canvas-patch.js +55 -0
  87. package/src/utils/ai-trash.js +48 -0
  88. package/src/utils/atomic-write.js +6 -43
  89. package/src/utils/cli-ask-question.js +76 -0
  90. package/src/utils/doc-links.js +2 -0
  91. package/src/utils/os-detect.js +10 -0
  92. package/src/utils/paths.js +14 -111
  93. package/src/utils/port-check.js +6 -45
  94. package/src/utils/process.js +4 -188
  95. package/src/utils/qmd-collection.js +72 -0
  96. package/src/utils/qmd-detect.js +18 -0
  97. package/src/utils/qmd-install.js +37 -0
  98. package/src/utils/repo-cleaner.js +79 -0
  99. package/src/utils/run-registry.js +9 -116
  100. package/src/utils/shared-paths.js +12 -41
  101. package/src/utils/squeezr-detect.js +18 -0
  102. package/src/utils/squeezr-install.js +37 -0
  103. package/src/utils/tool-call-extractor.js +56 -0
  104. 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
+ }