karajan-code 1.29.1 → 1.31.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 +106 -405
- package/package.json +1 -1
- package/src/cli.js +4 -0
- package/src/commands/resume.js +26 -2
- package/src/commands/run.js +26 -2
- package/src/config.js +20 -5
- package/src/hu/graph.js +71 -0
- package/src/hu/store.js +153 -0
- package/src/mcp/run-kj.js +4 -0
- package/src/mcp/tools.js +3 -0
- package/src/orchestrator/pre-loop-stages.js +210 -0
- package/src/orchestrator.js +16 -4
- package/src/prompts/hu-reviewer.js +130 -0
- package/src/roles/hu-reviewer-role.js +112 -0
- package/src/roles/index.js +1 -0
- package/src/utils/display.js +23 -2
- package/templates/roles/hu-reviewer.md +192 -0
package/src/commands/resume.js
CHANGED
|
@@ -1,10 +1,32 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import readline from "node:readline";
|
|
2
3
|
import { resumeFlow } from "../orchestrator.js";
|
|
3
4
|
import { createActivityLog } from "../activity-log.js";
|
|
4
5
|
import { printEvent } from "../utils/display.js";
|
|
5
6
|
|
|
7
|
+
function createCliAskQuestion() {
|
|
8
|
+
return async (question, context) => {
|
|
9
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
10
|
+
return new Promise((resolve) => {
|
|
11
|
+
console.log(`\n\u2753 ${question}`);
|
|
12
|
+
if (context?.detail) {
|
|
13
|
+
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
14
|
+
}
|
|
15
|
+
rl.question("\n> Your response (or 'stop' to exit): ", (answer) => {
|
|
16
|
+
rl.close();
|
|
17
|
+
if (answer.trim().toLowerCase() === "stop") {
|
|
18
|
+
resolve(null);
|
|
19
|
+
} else {
|
|
20
|
+
resolve(answer.trim());
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
6
27
|
export async function resumeCommand({ sessionId, answer, config, logger, flags }) {
|
|
7
28
|
const jsonMode = flags?.json;
|
|
29
|
+
const quietMode = config.output?.quiet !== false;
|
|
8
30
|
|
|
9
31
|
const emitter = new EventEmitter();
|
|
10
32
|
let activityLog = null;
|
|
@@ -20,17 +42,19 @@ export async function resumeCommand({ sessionId, answer, config, logger, flags }
|
|
|
20
42
|
}
|
|
21
43
|
|
|
22
44
|
if (!jsonMode) {
|
|
23
|
-
printEvent(event);
|
|
45
|
+
printEvent(event, { quiet: quietMode });
|
|
24
46
|
}
|
|
25
47
|
});
|
|
26
48
|
|
|
49
|
+
const askQuestion = createCliAskQuestion();
|
|
27
50
|
const result = await resumeFlow({
|
|
28
51
|
sessionId,
|
|
29
52
|
answer: answer || null,
|
|
30
53
|
config,
|
|
31
54
|
logger,
|
|
32
55
|
flags: flags || {},
|
|
33
|
-
emitter
|
|
56
|
+
emitter,
|
|
57
|
+
askQuestion
|
|
34
58
|
});
|
|
35
59
|
|
|
36
60
|
if (jsonMode || !answer) {
|
package/src/commands/run.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
|
+
import readline from "node:readline";
|
|
2
3
|
import { runFlow } from "../orchestrator.js";
|
|
3
4
|
import { assertAgentsAvailable } from "../agents/availability.js";
|
|
4
5
|
import { createActivityLog } from "../activity-log.js";
|
|
@@ -6,6 +7,26 @@ import { printHeader, printEvent } from "../utils/display.js";
|
|
|
6
7
|
import { resolveRole } from "../config.js";
|
|
7
8
|
import { parseCardId } from "../planning-game/adapter.js";
|
|
8
9
|
|
|
10
|
+
function createCliAskQuestion() {
|
|
11
|
+
return async (question, context) => {
|
|
12
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
13
|
+
return new Promise((resolve) => {
|
|
14
|
+
console.log(`\n\u2753 ${question}`);
|
|
15
|
+
if (context?.detail) {
|
|
16
|
+
console.log(` Context: ${JSON.stringify(context.detail, null, 2)}`);
|
|
17
|
+
}
|
|
18
|
+
rl.question("\n> Your response (or 'stop' to exit): ", (answer) => {
|
|
19
|
+
rl.close();
|
|
20
|
+
if (answer.trim().toLowerCase() === "stop") {
|
|
21
|
+
resolve(null);
|
|
22
|
+
} else {
|
|
23
|
+
resolve(answer.trim());
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
export async function runCommandHandler({ task, config, logger, flags }) {
|
|
10
31
|
// Best-effort session cleanup before starting
|
|
11
32
|
try {
|
|
@@ -33,6 +54,8 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
33
54
|
const pgProject = flags?.pgProject || config.planning_game?.project_id || null;
|
|
34
55
|
|
|
35
56
|
const jsonMode = flags?.json;
|
|
57
|
+
// Quiet mode is the default; --verbose disables it
|
|
58
|
+
const quietMode = config.output?.quiet !== false;
|
|
36
59
|
|
|
37
60
|
const emitter = new EventEmitter();
|
|
38
61
|
let activityLog = null;
|
|
@@ -48,7 +71,7 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
48
71
|
}
|
|
49
72
|
|
|
50
73
|
if (!jsonMode) {
|
|
51
|
-
printEvent(event);
|
|
74
|
+
printEvent(event, { quiet: quietMode });
|
|
52
75
|
}
|
|
53
76
|
});
|
|
54
77
|
|
|
@@ -56,7 +79,8 @@ export async function runCommandHandler({ task, config, logger, flags }) {
|
|
|
56
79
|
printHeader({ task: task, config });
|
|
57
80
|
}
|
|
58
81
|
|
|
59
|
-
const
|
|
82
|
+
const askQuestion = createCliAskQuestion();
|
|
83
|
+
const result = await runFlow({ task: task, config, logger, flags, emitter, askQuestion, pgTaskId: pgCardId || null, pgProject: pgProject || null });
|
|
60
84
|
|
|
61
85
|
if (jsonMode) {
|
|
62
86
|
console.log(JSON.stringify(result, null, 2));
|
package/src/config.js
CHANGED
|
@@ -19,7 +19,8 @@ const DEFAULTS = {
|
|
|
19
19
|
impeccable: { provider: null, model: null },
|
|
20
20
|
triage: { provider: null, model: null },
|
|
21
21
|
discover: { provider: null, model: null },
|
|
22
|
-
architect: { provider: null, model: null }
|
|
22
|
+
architect: { provider: null, model: null },
|
|
23
|
+
hu_reviewer: { provider: null, model: null }
|
|
23
24
|
},
|
|
24
25
|
pipeline: {
|
|
25
26
|
planner: { enabled: false },
|
|
@@ -32,6 +33,7 @@ const DEFAULTS = {
|
|
|
32
33
|
triage: { enabled: true },
|
|
33
34
|
discover: { enabled: false },
|
|
34
35
|
architect: { enabled: false },
|
|
36
|
+
hu_reviewer: { enabled: false },
|
|
35
37
|
auto_simplify: true
|
|
36
38
|
},
|
|
37
39
|
review_mode: "standard",
|
|
@@ -121,7 +123,7 @@ const DEFAULTS = {
|
|
|
121
123
|
planning_game: { enabled: false, project_id: null, codeveloper: null },
|
|
122
124
|
becaria: { enabled: false, review_event: "becaria-review", comment_event: "becaria-comment", comment_prefix: true },
|
|
123
125
|
git: { auto_commit: false, auto_push: false, auto_pr: false, auto_rebase: true, branch_prefix: "feat/" },
|
|
124
|
-
output: { report_dir: "./.reviews", log_level: "info" },
|
|
126
|
+
output: { report_dir: "./.reviews", log_level: "info", quiet: true },
|
|
125
127
|
budget: {
|
|
126
128
|
warn_threshold_pct: 80,
|
|
127
129
|
currency: "usd",
|
|
@@ -281,7 +283,8 @@ const PIPELINE_ENABLE_FLAGS = [
|
|
|
281
283
|
["enableSolomon", "solomon"], ["enableResearcher", "researcher"],
|
|
282
284
|
["enableTester", "tester"], ["enableSecurity", "security"], ["enableImpeccable", "impeccable"],
|
|
283
285
|
["enableTriage", "triage"], ["enableDiscover", "discover"],
|
|
284
|
-
["enableArchitect", "architect"]
|
|
286
|
+
["enableArchitect", "architect"],
|
|
287
|
+
["enableHuReviewer", "hu_reviewer"]
|
|
285
288
|
];
|
|
286
289
|
|
|
287
290
|
const AUTO_SIMPLIFY_FLAG = "autoSimplify";
|
|
@@ -363,6 +366,17 @@ function applyBecariaOverride(out, flags) {
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
|
|
369
|
+
function applyOutputModeOverrides(out, flags) {
|
|
370
|
+
out.output = out.output || {};
|
|
371
|
+
// --verbose explicitly overrides quiet
|
|
372
|
+
if (flags.verbose === true) {
|
|
373
|
+
out.output.quiet = false;
|
|
374
|
+
} else if (flags.quiet === true) {
|
|
375
|
+
out.output.quiet = true;
|
|
376
|
+
}
|
|
377
|
+
// quiet defaults to true (set in DEFAULTS)
|
|
378
|
+
}
|
|
379
|
+
|
|
366
380
|
function applyMiscOverrides(out, flags) {
|
|
367
381
|
if (flags[AUTO_SIMPLIFY_FLAG] !== undefined) out.pipeline.auto_simplify = Boolean(flags[AUTO_SIMPLIFY_FLAG]);
|
|
368
382
|
if (flags.noSonar || flags.sonar === false) out.sonarqube.enabled = false;
|
|
@@ -401,6 +415,7 @@ export function applyRunOverrides(config, flags) {
|
|
|
401
415
|
applyMethodologyOverride(out, flags);
|
|
402
416
|
applyBecariaOverride(out, flags);
|
|
403
417
|
applyMiscOverrides(out, flags);
|
|
418
|
+
applyOutputModeOverrides(out, flags);
|
|
404
419
|
|
|
405
420
|
return out;
|
|
406
421
|
}
|
|
@@ -414,14 +429,14 @@ export function resolveRole(config, role) {
|
|
|
414
429
|
let provider = roleConfig.provider ?? null;
|
|
415
430
|
if (!provider && role === "coder") provider = legacyCoder;
|
|
416
431
|
if (!provider && role === "reviewer") provider = legacyReviewer;
|
|
417
|
-
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "audit")) {
|
|
432
|
+
if (!provider && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "audit" || role === "hu_reviewer" || role === "hu-reviewer")) {
|
|
418
433
|
provider = roles.coder?.provider || legacyCoder;
|
|
419
434
|
}
|
|
420
435
|
|
|
421
436
|
let model = roleConfig.model ?? null;
|
|
422
437
|
if (!model && role === "coder") model = config?.coder_options?.model ?? null;
|
|
423
438
|
if (!model && role === "reviewer") model = config?.reviewer_options?.model ?? null;
|
|
424
|
-
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect")) {
|
|
439
|
+
if (!model && (role === "planner" || role === "refactorer" || role === "solomon" || role === "researcher" || role === "tester" || role === "security" || role === "impeccable" || role === "triage" || role === "discover" || role === "architect" || role === "hu_reviewer" || role === "hu-reviewer")) {
|
|
425
440
|
model = config?.coder_options?.model ?? null;
|
|
426
441
|
}
|
|
427
442
|
|
package/src/hu/graph.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Topological sort of HU stories respecting blocked_by dependencies.
|
|
3
|
+
* Returns ordered array of story IDs (dependencies first).
|
|
4
|
+
* Throws if circular dependency detected.
|
|
5
|
+
* @param {Array<{id: string, blocked_by?: string[]}>} stories
|
|
6
|
+
* @returns {string[]} Sorted story IDs.
|
|
7
|
+
*/
|
|
8
|
+
export function topologicalSort(stories) {
|
|
9
|
+
const ids = new Set(stories.map(s => s.id));
|
|
10
|
+
const adj = new Map(); // id -> [dependents]
|
|
11
|
+
const inDegree = new Map();
|
|
12
|
+
|
|
13
|
+
for (const s of stories) {
|
|
14
|
+
adj.set(s.id, []);
|
|
15
|
+
inDegree.set(s.id, 0);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
for (const s of stories) {
|
|
19
|
+
for (const dep of (s.blocked_by || [])) {
|
|
20
|
+
if (!ids.has(dep)) throw new Error(`Dependency ${dep} not found in batch`);
|
|
21
|
+
adj.get(dep).push(s.id);
|
|
22
|
+
inDegree.set(s.id, (inDegree.get(s.id) || 0) + 1);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const queue = [];
|
|
27
|
+
for (const [id, degree] of inDegree) {
|
|
28
|
+
if (degree === 0) queue.push(id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sorted = [];
|
|
32
|
+
while (queue.length > 0) {
|
|
33
|
+
const id = queue.shift();
|
|
34
|
+
sorted.push(id);
|
|
35
|
+
for (const dependent of adj.get(id)) {
|
|
36
|
+
inDegree.set(dependent, inDegree.get(dependent) - 1);
|
|
37
|
+
if (inDegree.get(dependent) === 0) queue.push(dependent);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (sorted.length !== stories.length) {
|
|
42
|
+
throw new Error("Circular dependency detected in HU batch");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return sorted;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a story is ready to execute (all its dependencies are done).
|
|
50
|
+
* @param {{blocked_by?: string[]}} story
|
|
51
|
+
* @param {{stories: Array<{id: string, status: string}>}} batch
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function isStoryReady(story, batch) {
|
|
55
|
+
if (!story.blocked_by || story.blocked_by.length === 0) return true;
|
|
56
|
+
return story.blocked_by.every(depId => {
|
|
57
|
+
const dep = batch.stories.find(s => s.id === depId);
|
|
58
|
+
return dep && dep.status === "done";
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get next stories ready for execution (certified + all deps done).
|
|
64
|
+
* @param {{stories: Array<{id: string, status: string, blocked_by?: string[]}>}} batch
|
|
65
|
+
* @returns {Array<object>} Stories that are certified and whose deps are all done.
|
|
66
|
+
*/
|
|
67
|
+
export function getNextReadyStories(batch) {
|
|
68
|
+
return batch.stories.filter(s =>
|
|
69
|
+
s.status === "certified" && isStoryReady(s, batch)
|
|
70
|
+
);
|
|
71
|
+
}
|
package/src/hu/store.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { getKarajanHome } from "../utils/paths.js";
|
|
4
|
+
|
|
5
|
+
// FUTURE: hu-storage adapter for PG/Trello/etc — currently local files only
|
|
6
|
+
|
|
7
|
+
/** @returns {string} Path to the hu-stories directory (evaluated at call time). */
|
|
8
|
+
function getHuDir() {
|
|
9
|
+
return path.join(getKarajanHome(), "hu-stories");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a new HU batch from an array of story definitions.
|
|
14
|
+
* @param {string} sessionId - The session identifier.
|
|
15
|
+
* @param {Array<{id?: string, text: string, blocked_by?: string[]}>} stories - Raw story inputs.
|
|
16
|
+
* @returns {Promise<object>} The created batch object.
|
|
17
|
+
*/
|
|
18
|
+
export async function createHuBatch(sessionId, stories) {
|
|
19
|
+
const dir = path.join(getHuDir(), sessionId);
|
|
20
|
+
await fs.mkdir(dir, { recursive: true });
|
|
21
|
+
|
|
22
|
+
const batch = {
|
|
23
|
+
session_id: sessionId,
|
|
24
|
+
created_at: new Date().toISOString(),
|
|
25
|
+
stories: stories.map((s, i) => ({
|
|
26
|
+
id: s.id || `HU-${Date.now()}-${i}`,
|
|
27
|
+
status: "pending",
|
|
28
|
+
original: { text: s.text },
|
|
29
|
+
blocked_by: s.blocked_by || [],
|
|
30
|
+
certified: null,
|
|
31
|
+
quality: null,
|
|
32
|
+
context_requests: [],
|
|
33
|
+
created_at: new Date().toISOString(),
|
|
34
|
+
updated_at: new Date().toISOString()
|
|
35
|
+
}))
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
await fs.writeFile(path.join(dir, "batch.json"), JSON.stringify(batch, null, 2));
|
|
39
|
+
return batch;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Load an existing HU batch from disk.
|
|
44
|
+
* @param {string} sessionId - The session identifier.
|
|
45
|
+
* @returns {Promise<object>} The loaded batch object.
|
|
46
|
+
*/
|
|
47
|
+
export async function loadHuBatch(sessionId) {
|
|
48
|
+
const file = path.join(getHuDir(), sessionId, "batch.json");
|
|
49
|
+
const raw = await fs.readFile(file, "utf8");
|
|
50
|
+
return JSON.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Save a batch back to disk.
|
|
55
|
+
* @param {string} sessionId - The session identifier.
|
|
56
|
+
* @param {object} batch - The batch object to persist.
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
export async function saveHuBatch(sessionId, batch) {
|
|
60
|
+
const dir = path.join(getHuDir(), sessionId);
|
|
61
|
+
batch.updated_at = new Date().toISOString();
|
|
62
|
+
await fs.writeFile(path.join(dir, "batch.json"), JSON.stringify(batch, null, 2));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Update the status of a single story within a batch.
|
|
67
|
+
* @param {object} batch - The batch object.
|
|
68
|
+
* @param {string} storyId - Story ID to update.
|
|
69
|
+
* @param {string} status - New status value.
|
|
70
|
+
* @param {object} [extra={}] - Additional fields to merge.
|
|
71
|
+
* @returns {object} The updated story.
|
|
72
|
+
*/
|
|
73
|
+
export function updateStoryStatus(batch, storyId, status, extra = {}) {
|
|
74
|
+
const story = batch.stories.find(s => s.id === storyId);
|
|
75
|
+
if (!story) throw new Error(`Story ${storyId} not found`);
|
|
76
|
+
story.status = status;
|
|
77
|
+
story.updated_at = new Date().toISOString();
|
|
78
|
+
Object.assign(story, extra);
|
|
79
|
+
return story;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Store quality scores on a story.
|
|
84
|
+
* @param {object} batch - The batch object.
|
|
85
|
+
* @param {string} storyId - Story ID.
|
|
86
|
+
* @param {object} quality - Quality scores object.
|
|
87
|
+
* @returns {object} The updated story.
|
|
88
|
+
*/
|
|
89
|
+
export function updateStoryQuality(batch, storyId, quality) {
|
|
90
|
+
const story = batch.stories.find(s => s.id === storyId);
|
|
91
|
+
if (!story) throw new Error(`Story ${storyId} not found`);
|
|
92
|
+
story.quality = { ...quality, evaluated_at: new Date().toISOString() };
|
|
93
|
+
story.updated_at = new Date().toISOString();
|
|
94
|
+
return story;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Mark a story as certified with the provided certified data.
|
|
99
|
+
* @param {object} batch - The batch object.
|
|
100
|
+
* @param {string} storyId - Story ID.
|
|
101
|
+
* @param {object} certified - Certified HU data.
|
|
102
|
+
* @returns {object} The updated story.
|
|
103
|
+
*/
|
|
104
|
+
export function updateStoryCertified(batch, storyId, certified) {
|
|
105
|
+
const story = batch.stories.find(s => s.id === storyId);
|
|
106
|
+
if (!story) throw new Error(`Story ${storyId} not found`);
|
|
107
|
+
story.certified = certified;
|
|
108
|
+
story.status = "certified";
|
|
109
|
+
story.updated_at = new Date().toISOString();
|
|
110
|
+
return story;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Add a context request to a story and set its status to needs_context.
|
|
115
|
+
* @param {object} batch - The batch object.
|
|
116
|
+
* @param {string} storyId - Story ID.
|
|
117
|
+
* @param {{fields_needed: string[], question: string}} request - Context request.
|
|
118
|
+
* @returns {object} The updated story.
|
|
119
|
+
*/
|
|
120
|
+
export function addContextRequest(batch, storyId, request) {
|
|
121
|
+
const story = batch.stories.find(s => s.id === storyId);
|
|
122
|
+
if (!story) throw new Error(`Story ${storyId} not found`);
|
|
123
|
+
story.context_requests.push({
|
|
124
|
+
requested_at: new Date().toISOString(),
|
|
125
|
+
fields_needed: request.fields_needed,
|
|
126
|
+
question_to_fde: request.question,
|
|
127
|
+
answered_at: null,
|
|
128
|
+
answer: null
|
|
129
|
+
});
|
|
130
|
+
story.status = "needs_context";
|
|
131
|
+
story.updated_at = new Date().toISOString();
|
|
132
|
+
return story;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Answer the most recent pending context request and reset status to pending.
|
|
137
|
+
* @param {object} batch - The batch object.
|
|
138
|
+
* @param {string} storyId - Story ID.
|
|
139
|
+
* @param {string} answer - The FDE's answer.
|
|
140
|
+
* @returns {object} The updated story.
|
|
141
|
+
*/
|
|
142
|
+
export function answerContextRequest(batch, storyId, answer) {
|
|
143
|
+
const story = batch.stories.find(s => s.id === storyId);
|
|
144
|
+
if (!story) throw new Error(`Story ${storyId} not found`);
|
|
145
|
+
const pending = story.context_requests.find(r => !r.answered_at);
|
|
146
|
+
if (pending) {
|
|
147
|
+
pending.answered_at = new Date().toISOString();
|
|
148
|
+
pending.answer = answer;
|
|
149
|
+
}
|
|
150
|
+
story.status = "pending"; // back to pending for re-evaluation
|
|
151
|
+
story.updated_at = new Date().toISOString();
|
|
152
|
+
return story;
|
|
153
|
+
}
|
package/src/mcp/run-kj.js
CHANGED
|
@@ -46,6 +46,8 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
46
46
|
normalizeBoolFlag(options.enableTriage, "--enable-triage", args);
|
|
47
47
|
normalizeBoolFlag(options.enableDiscover, "--enable-discover", args);
|
|
48
48
|
normalizeBoolFlag(options.enableArchitect, "--enable-architect", args);
|
|
49
|
+
normalizeBoolFlag(options.enableHuReviewer, "--enable-hu-reviewer", args);
|
|
50
|
+
addOptionalValue(args, "--hu-file", options.huFile);
|
|
49
51
|
normalizeBoolFlag(options.enableSerena, "--enable-serena", args);
|
|
50
52
|
normalizeBoolFlag(options.autoCommit, "--auto-commit", args);
|
|
51
53
|
normalizeBoolFlag(options.autoPush, "--auto-push", args);
|
|
@@ -60,6 +62,8 @@ export async function runKjCommand({ command, commandArgs = [], options = {}, en
|
|
|
60
62
|
addOptionalValue(args, "--checkpoint-interval", options.checkpointInterval);
|
|
61
63
|
addOptionalValue(args, "--pg-task", options.pgTask);
|
|
62
64
|
addOptionalValue(args, "--pg-project", options.pgProject);
|
|
65
|
+
if (options.quiet === true) args.push("--quiet");
|
|
66
|
+
if (options.quiet === false) args.push("--verbose");
|
|
63
67
|
|
|
64
68
|
const runEnv = {
|
|
65
69
|
...process.env,
|
package/src/mcp/tools.js
CHANGED
|
@@ -74,7 +74,9 @@ export const tools = [
|
|
|
74
74
|
enableTriage: { type: "boolean" },
|
|
75
75
|
enableDiscover: { type: "boolean" },
|
|
76
76
|
enableArchitect: { type: "boolean" },
|
|
77
|
+
enableHuReviewer: { type: "boolean" },
|
|
77
78
|
architectModel: { type: "string" },
|
|
79
|
+
huFile: { type: "string", description: "Path to YAML file with HU stories to certify before coding" },
|
|
78
80
|
enableSerena: { type: "boolean" },
|
|
79
81
|
enableBecaria: { type: "boolean", description: "Enable BecarIA Gateway (early PR + dispatch comments/reviews)" },
|
|
80
82
|
reviewerFallback: { type: "string" },
|
|
@@ -95,6 +97,7 @@ export const tools = [
|
|
|
95
97
|
smartModels: { type: "boolean", description: "Enable/disable smart model selection based on triage complexity" },
|
|
96
98
|
checkpointInterval: { type: "number", description: "Minutes between interactive checkpoints (default: 5). Set 0 to disable." },
|
|
97
99
|
taskType: { type: "string", enum: ["sw", "infra", "doc", "add-tests", "refactor"], description: "Explicit task type for policy resolution. Overrides triage classification." },
|
|
100
|
+
quiet: { type: "boolean", description: "Suppress raw agent output lines, show only stage status (default: true). Set false for verbose output." },
|
|
98
101
|
noSonar: { type: "boolean" },
|
|
99
102
|
enableSonarcloud: { type: "boolean", description: "Enable SonarCloud scan (complementary to SonarQube)" },
|
|
100
103
|
kjHome: { type: "string" },
|
|
@@ -3,6 +3,7 @@ import { ResearcherRole } from "../roles/researcher-role.js";
|
|
|
3
3
|
import { PlannerRole } from "../roles/planner-role.js";
|
|
4
4
|
import { DiscoverRole } from "../roles/discover-role.js";
|
|
5
5
|
import { ArchitectRole } from "../roles/architect-role.js";
|
|
6
|
+
import { HuReviewerRole } from "../roles/hu-reviewer-role.js";
|
|
6
7
|
import { createAgent } from "../agents/index.js";
|
|
7
8
|
import { createArchitectADRs } from "../planning-game/architect-adrs.js";
|
|
8
9
|
import { addCheckpoint, markSessionStatus } from "../session-store.js";
|
|
@@ -10,6 +11,8 @@ import { emitProgress, makeEvent } from "../utils/events.js";
|
|
|
10
11
|
import { parsePlannerOutput } from "../prompts/planner.js";
|
|
11
12
|
import { selectModelsForRoles } from "../utils/model-selector.js";
|
|
12
13
|
import { createStallDetector } from "../utils/stall-detector.js";
|
|
14
|
+
import { createHuBatch, loadHuBatch, saveHuBatch, updateStoryStatus, updateStoryQuality, updateStoryCertified, addContextRequest, answerContextRequest } from "../hu/store.js";
|
|
15
|
+
import { topologicalSort } from "../hu/graph.js";
|
|
13
16
|
|
|
14
17
|
const ROLE_NAMES = ["planner", "researcher", "architect", "refactorer", "reviewer", "tester", "security", "impeccable"];
|
|
15
18
|
|
|
@@ -507,3 +510,210 @@ export async function runDiscoverStage({ config, logger, emitter, eventBase, ses
|
|
|
507
510
|
|
|
508
511
|
return { stageResult };
|
|
509
512
|
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Run the HU Reviewer stage: load stories from YAML, evaluate, certify, and return in topological order.
|
|
516
|
+
* @param {object} params
|
|
517
|
+
* @returns {Promise<{stageResult: object}>}
|
|
518
|
+
*/
|
|
519
|
+
export async function runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion }) {
|
|
520
|
+
logger.setContext({ iteration: 0, stage: "hu-reviewer" });
|
|
521
|
+
emitProgress(
|
|
522
|
+
emitter,
|
|
523
|
+
makeEvent("hu-reviewer:start", { ...eventBase, stage: "hu-reviewer" }, {
|
|
524
|
+
message: "HU Reviewer certifying user stories"
|
|
525
|
+
})
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
// --- Load YAML file ---
|
|
529
|
+
const yaml = await import("js-yaml");
|
|
530
|
+
const fs = await import("node:fs/promises");
|
|
531
|
+
let rawYaml;
|
|
532
|
+
try {
|
|
533
|
+
rawYaml = await fs.readFile(huFile, "utf8");
|
|
534
|
+
} catch (err) {
|
|
535
|
+
const stageResult = { ok: false, error: `Could not read HU file: ${err.message}` };
|
|
536
|
+
emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
|
|
537
|
+
status: "fail", message: stageResult.error
|
|
538
|
+
}));
|
|
539
|
+
return { stageResult };
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
let stories;
|
|
543
|
+
try {
|
|
544
|
+
const parsed = yaml.load(rawYaml);
|
|
545
|
+
stories = Array.isArray(parsed) ? parsed : (parsed?.stories || []);
|
|
546
|
+
} catch (err) {
|
|
547
|
+
const stageResult = { ok: false, error: `Invalid YAML in HU file: ${err.message}` };
|
|
548
|
+
emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
|
|
549
|
+
status: "fail", message: stageResult.error
|
|
550
|
+
}));
|
|
551
|
+
return { stageResult };
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (stories.length === 0) {
|
|
555
|
+
const stageResult = { ok: true, certified: 0, stories: [] };
|
|
556
|
+
emitProgress(emitter, makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
|
|
557
|
+
status: "ok", message: "No stories to evaluate"
|
|
558
|
+
}));
|
|
559
|
+
return { stageResult };
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// --- Create or load batch ---
|
|
563
|
+
const batchSessionId = `hu-${session.id}`;
|
|
564
|
+
let batch;
|
|
565
|
+
try {
|
|
566
|
+
batch = await loadHuBatch(batchSessionId);
|
|
567
|
+
} catch {
|
|
568
|
+
batch = await createHuBatch(batchSessionId, stories);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// --- Evaluate loop (re-evaluate entire batch until all certified or needs_context with no askQuestion) ---
|
|
572
|
+
const huReviewerProvider = config?.roles?.hu_reviewer?.provider || coderRole.provider;
|
|
573
|
+
const huReviewerOnOutput = ({ stream, line }) => {
|
|
574
|
+
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "hu-reviewer" }, {
|
|
575
|
+
message: line,
|
|
576
|
+
detail: { stream, agent: huReviewerProvider }
|
|
577
|
+
}));
|
|
578
|
+
};
|
|
579
|
+
|
|
580
|
+
let maxRounds = 5;
|
|
581
|
+
let round = 0;
|
|
582
|
+
|
|
583
|
+
while (round < maxRounds) {
|
|
584
|
+
round += 1;
|
|
585
|
+
|
|
586
|
+
const pendingStories = batch.stories.filter(s => s.status === "pending" || s.status === "needs_context");
|
|
587
|
+
if (pendingStories.length === 0) break;
|
|
588
|
+
|
|
589
|
+
const storiesToEvaluate = pendingStories.map(s => ({ id: s.id, text: s.original.text }));
|
|
590
|
+
|
|
591
|
+
const stall = createStallDetector({
|
|
592
|
+
onOutput: huReviewerOnOutput, emitter, eventBase, stage: "hu-reviewer", provider: huReviewerProvider
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
const huReviewer = new HuReviewerRole({ config, logger, emitter, createAgentFn: createAgent });
|
|
596
|
+
await huReviewer.init({ task: session.task, sessionId: session.id, iteration: 0 });
|
|
597
|
+
const reviewStart = Date.now();
|
|
598
|
+
let reviewOutput;
|
|
599
|
+
try {
|
|
600
|
+
reviewOutput = await huReviewer.run({ stories: storiesToEvaluate, onOutput: stall.onOutput });
|
|
601
|
+
} catch (err) {
|
|
602
|
+
logger.warn(`HU Reviewer threw: ${err.message}`);
|
|
603
|
+
reviewOutput = { ok: false, summary: `HU Reviewer error: ${err.message}`, result: { error: err.message } };
|
|
604
|
+
} finally {
|
|
605
|
+
stall.stop();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
trackBudget({
|
|
609
|
+
role: "hu-reviewer",
|
|
610
|
+
provider: huReviewerProvider,
|
|
611
|
+
model: config?.roles?.hu_reviewer?.model || coderRole.model,
|
|
612
|
+
result: reviewOutput,
|
|
613
|
+
duration_ms: Date.now() - reviewStart
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
if (!reviewOutput.ok || !reviewOutput.result?.evaluations) {
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// --- Process evaluations ---
|
|
621
|
+
for (const evaluation of reviewOutput.result.evaluations) {
|
|
622
|
+
const storyId = evaluation.story_id;
|
|
623
|
+
try {
|
|
624
|
+
updateStoryQuality(batch, storyId, evaluation.scores);
|
|
625
|
+
} catch {
|
|
626
|
+
continue; // story not found in batch, skip
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (evaluation.verdict === "certified") {
|
|
630
|
+
updateStoryCertified(batch, storyId, evaluation.certified_hu);
|
|
631
|
+
} else if (evaluation.verdict === "needs_context" && evaluation.context_needed) {
|
|
632
|
+
addContextRequest(batch, storyId, {
|
|
633
|
+
fields_needed: evaluation.context_needed.fields_needed || [],
|
|
634
|
+
question: evaluation.context_needed.question_to_fde || ""
|
|
635
|
+
});
|
|
636
|
+
} else if (evaluation.verdict === "needs_rewrite" && evaluation.rewritten) {
|
|
637
|
+
// Accept the rewrite and re-certify
|
|
638
|
+
updateStoryCertified(batch, storyId, evaluation.rewritten);
|
|
639
|
+
} else {
|
|
640
|
+
updateStoryStatus(batch, storyId, "pending");
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
await saveHuBatch(batchSessionId, batch);
|
|
645
|
+
|
|
646
|
+
// --- Check if any need context ---
|
|
647
|
+
const needsContext = batch.stories.filter(s => s.status === "needs_context");
|
|
648
|
+
if (needsContext.length > 0) {
|
|
649
|
+
const consolidatedQuestions = reviewOutput.result.batch_summary?.consolidated_questions
|
|
650
|
+
|| needsContext.map(s => {
|
|
651
|
+
const pending = s.context_requests.find(r => !r.answered_at);
|
|
652
|
+
return pending ? `[${s.id}] ${pending.question_to_fde}` : null;
|
|
653
|
+
}).filter(Boolean).join("\n");
|
|
654
|
+
|
|
655
|
+
if (!askQuestion) {
|
|
656
|
+
// No interactive input — pause session
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
emitProgress(emitter, makeEvent("hu-reviewer:needs-context", { ...eventBase, stage: "hu-reviewer" }, {
|
|
661
|
+
message: `${needsContext.length} story(ies) need context from FDE`,
|
|
662
|
+
detail: { questions: consolidatedQuestions }
|
|
663
|
+
}));
|
|
664
|
+
|
|
665
|
+
const answer = await askQuestion(
|
|
666
|
+
`The HU Reviewer needs additional context:\n\n${consolidatedQuestions}\n\nPlease provide your answers:`,
|
|
667
|
+
{ iteration: 0, stage: "hu-reviewer" }
|
|
668
|
+
);
|
|
669
|
+
|
|
670
|
+
if (!answer) break;
|
|
671
|
+
|
|
672
|
+
// --- Incorporate FDE answers and re-evaluate ---
|
|
673
|
+
for (const s of needsContext) {
|
|
674
|
+
answerContextRequest(batch, s.id, answer);
|
|
675
|
+
}
|
|
676
|
+
await saveHuBatch(batchSessionId, batch);
|
|
677
|
+
// Loop will re-evaluate entire batch
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
await addCheckpoint(session, {
|
|
682
|
+
stage: "hu-reviewer",
|
|
683
|
+
iteration: 0,
|
|
684
|
+
ok: true,
|
|
685
|
+
certified: batch.stories.filter(s => s.status === "certified").length,
|
|
686
|
+
total: batch.stories.length
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
// --- Return certified stories in topological order ---
|
|
690
|
+
const certifiedStories = batch.stories.filter(s => s.status === "certified");
|
|
691
|
+
let orderedIds;
|
|
692
|
+
try {
|
|
693
|
+
orderedIds = topologicalSort(certifiedStories);
|
|
694
|
+
} catch {
|
|
695
|
+
orderedIds = certifiedStories.map(s => s.id);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const orderedStories = orderedIds.map(id => batch.stories.find(s => s.id === id)).filter(Boolean);
|
|
699
|
+
|
|
700
|
+
const stageResult = {
|
|
701
|
+
ok: true,
|
|
702
|
+
certified: certifiedStories.length,
|
|
703
|
+
total: batch.stories.length,
|
|
704
|
+
needsContext: batch.stories.filter(s => s.status === "needs_context").length,
|
|
705
|
+
stories: orderedStories,
|
|
706
|
+
batchSessionId
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
emitProgress(
|
|
710
|
+
emitter,
|
|
711
|
+
makeEvent("hu-reviewer:end", { ...eventBase, stage: "hu-reviewer" }, {
|
|
712
|
+
status: "ok",
|
|
713
|
+
message: `HU Review complete: ${certifiedStories.length}/${batch.stories.length} certified`,
|
|
714
|
+
detail: stageResult
|
|
715
|
+
})
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
return { stageResult };
|
|
719
|
+
}
|