karajan-code 2.0.1 → 2.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/package.json +1 -1
- package/src/agents/claude-agent.js +32 -5
- package/src/config.js +1 -0
- package/src/git/hu-automation.js +138 -0
- package/src/hu/auto-generator.js +163 -0
- package/src/orchestrator/hu-sub-pipeline.js +1 -1
- package/src/orchestrator/stages/architect-stage.js +2 -7
- package/src/orchestrator/stages/coder-stage.js +3 -13
- package/src/orchestrator/stages/planner-stage.js +2 -7
- package/src/orchestrator/stages/research-stage.js +2 -7
- package/src/orchestrator/stages/reviewer-stage.js +2 -7
- package/src/orchestrator/stages/triage-stage.js +2 -7
- package/src/orchestrator.js +206 -16
- package/src/utils/display.js +18 -4
- package/src/utils/events.js +12 -0
package/package.json
CHANGED
|
@@ -116,18 +116,45 @@ function extractTextFromStreamJson(raw) {
|
|
|
116
116
|
* Create a wrapping onOutput that parses stream-json lines and forwards
|
|
117
117
|
* meaningful content (assistant text, tool usage) to the original callback.
|
|
118
118
|
*/
|
|
119
|
+
function summarizeToolCall(name, input = {}) {
|
|
120
|
+
// Produce concise human-readable action line from tool_use block
|
|
121
|
+
const rel = (p) => String(p || "").replace(process.cwd() + "/", "");
|
|
122
|
+
switch (name) {
|
|
123
|
+
case "Read": return `Read ${rel(input.file_path)}`;
|
|
124
|
+
case "Write": return `Write ${rel(input.file_path)}`;
|
|
125
|
+
case "Edit": return `Edit ${rel(input.file_path)}`;
|
|
126
|
+
case "MultiEdit": return `MultiEdit ${rel(input.file_path)} (${(input.edits || []).length} edits)`;
|
|
127
|
+
case "NotebookEdit": return `NotebookEdit ${rel(input.notebook_path)}`;
|
|
128
|
+
case "Glob": return `Glob ${input.pattern || ""}`.trim();
|
|
129
|
+
case "Grep": return `Grep "${(input.pattern || "").slice(0, 60)}"${input.path ? ` in ${rel(input.path)}` : ""}`;
|
|
130
|
+
case "Bash": {
|
|
131
|
+
const cmd = String(input.command || "").replace(/\s+/g, " ").slice(0, 100);
|
|
132
|
+
return `Bash $ ${cmd}`;
|
|
133
|
+
}
|
|
134
|
+
case "TodoWrite": return `Todo ${(input.todos || []).length} items`;
|
|
135
|
+
case "WebFetch": return `WebFetch ${input.url || ""}`;
|
|
136
|
+
case "WebSearch": return `WebSearch "${(input.query || "").slice(0, 60)}"`;
|
|
137
|
+
case "Task": return `Agent: ${input.subagent_type || "?"} — ${(input.description || "").slice(0, 60)}`;
|
|
138
|
+
default: return `${name}${Object.keys(input).length > 0 ? " (…)" : ""}`;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
119
142
|
function createStreamJsonFilter(onOutput) {
|
|
120
143
|
if (!onOutput) return null;
|
|
121
144
|
return ({ stream, line }) => {
|
|
122
145
|
try {
|
|
123
146
|
const obj = JSON.parse(line);
|
|
124
|
-
// Forward assistant
|
|
147
|
+
// Forward assistant content
|
|
125
148
|
if (obj.type === "assistant" && obj.message?.content) {
|
|
126
149
|
for (const block of obj.message.content) {
|
|
127
150
|
if (block.type === "text" && block.text) {
|
|
128
|
-
|
|
151
|
+
// Only emit first line of text for brevity
|
|
152
|
+
const firstLine = block.text.split("\n")[0].trim().slice(0, 120);
|
|
153
|
+
if (firstLine) onOutput({ stream, line: firstLine, kind: "text" });
|
|
129
154
|
} else if (block.type === "tool_use") {
|
|
130
|
-
onOutput({ stream, line:
|
|
155
|
+
onOutput({ stream, line: summarizeToolCall(block.name, block.input), kind: "tool" });
|
|
156
|
+
} else if (block.type === "thinking" && block.thinking) {
|
|
157
|
+
onOutput({ stream, line: `thinking: ${block.thinking.slice(0, 80).replace(/\s+/g, " ")}…`, kind: "thinking" });
|
|
131
158
|
}
|
|
132
159
|
}
|
|
133
160
|
return;
|
|
@@ -135,9 +162,9 @@ function createStreamJsonFilter(onOutput) {
|
|
|
135
162
|
// Forward result
|
|
136
163
|
if (obj.type === "result") {
|
|
137
164
|
const summary = typeof obj.result === "string"
|
|
138
|
-
? obj.result.slice(0,
|
|
165
|
+
? obj.result.split("\n")[0].slice(0, 120)
|
|
139
166
|
: "result received";
|
|
140
|
-
onOutput({ stream, line: `
|
|
167
|
+
onOutput({ stream, line: `done: ${summary}` });
|
|
141
168
|
return;
|
|
142
169
|
}
|
|
143
170
|
} catch { /* not JSON, forward raw */ }
|
package/src/config.js
CHANGED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-HU git automation: branch, commit, push, PR per HU story.
|
|
3
|
+
* Called from the HU sub-pipeline wrapper in orchestrator.js.
|
|
4
|
+
*/
|
|
5
|
+
import { commitAll, pushBranch, createPullRequest, hasChanges } from "../utils/git.js";
|
|
6
|
+
import { runCommand } from "../utils/process.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Build a git-safe branch name for an HU story.
|
|
10
|
+
* @param {string} prefix - e.g. "feat/"
|
|
11
|
+
* @param {object} story - HU story {id, title}
|
|
12
|
+
* @returns {string}
|
|
13
|
+
*/
|
|
14
|
+
export function buildHuBranchName(prefix, story) {
|
|
15
|
+
const baseSlug = String(story.title || story.id || "hu")
|
|
16
|
+
.toLowerCase()
|
|
17
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
18
|
+
.replace(/^-+|-+$/g, "")
|
|
19
|
+
.slice(0, 40);
|
|
20
|
+
return `${prefix}${story.id}-${baseSlug}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the base branch for a given HU based on its dependencies.
|
|
25
|
+
* If it has no blocked_by, uses config.base_branch.
|
|
26
|
+
* If it has parents, uses the last-created parent branch (assumes parents ran first).
|
|
27
|
+
*
|
|
28
|
+
* @param {object} story - the HU story
|
|
29
|
+
* @param {Map<string,string>} huBranches - map of huId → branchName (already created)
|
|
30
|
+
* @param {string} baseBranch - config.base_branch fallback
|
|
31
|
+
* @returns {string} base branch name
|
|
32
|
+
*/
|
|
33
|
+
export function resolveHuBase(story, huBranches, baseBranch) {
|
|
34
|
+
const parents = story.blocked_by || [];
|
|
35
|
+
if (parents.length === 0) return baseBranch;
|
|
36
|
+
// Walk parents in reverse order of declaration to find the most recent one
|
|
37
|
+
for (let i = parents.length - 1; i >= 0; i--) {
|
|
38
|
+
const parentBranch = huBranches.get(parents[i]);
|
|
39
|
+
if (parentBranch) return parentBranch;
|
|
40
|
+
}
|
|
41
|
+
return baseBranch;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Create a branch for an HU starting from its resolved base.
|
|
46
|
+
* Returns the branch name created (or null if git automation is disabled).
|
|
47
|
+
*
|
|
48
|
+
* @param {object} params
|
|
49
|
+
* @param {object} params.story
|
|
50
|
+
* @param {Map} params.huBranches
|
|
51
|
+
* @param {object} params.config
|
|
52
|
+
* @param {object} params.logger
|
|
53
|
+
* @returns {Promise<string|null>}
|
|
54
|
+
*/
|
|
55
|
+
export async function prepareHuBranch({ story, huBranches, config, logger }) {
|
|
56
|
+
if (!config.git?.auto_commit && !config.git?.auto_push && !config.git?.auto_pr) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const baseBranch = resolveHuBase(story, huBranches, config.base_branch || "main");
|
|
60
|
+
const prefix = config.git?.branch_prefix || "feat/";
|
|
61
|
+
const branchName = buildHuBranchName(prefix, story);
|
|
62
|
+
|
|
63
|
+
// Checkout from the resolved base. Use `git checkout -B` to overwrite if rerun.
|
|
64
|
+
const res = await runCommand("git", ["checkout", "-B", branchName, baseBranch], {});
|
|
65
|
+
if (res.exitCode !== 0) {
|
|
66
|
+
logger.warn(`HU git: failed to create branch ${branchName} from ${baseBranch}: ${res.stderr}`);
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
huBranches.set(story.id, branchName);
|
|
70
|
+
logger.info(`HU ${story.id}: branch '${branchName}' from '${baseBranch}'`);
|
|
71
|
+
return branchName;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* After an HU is approved, commit its changes and optionally push + create PR.
|
|
76
|
+
*
|
|
77
|
+
* @param {object} params
|
|
78
|
+
* @param {object} params.story
|
|
79
|
+
* @param {string} params.branchName
|
|
80
|
+
* @param {object} params.config
|
|
81
|
+
* @param {object} params.logger
|
|
82
|
+
* @returns {Promise<{committed: boolean, pushed: boolean, prUrl: string|null}>}
|
|
83
|
+
*/
|
|
84
|
+
export async function finalizeHuCommit({ story, branchName, config, logger }) {
|
|
85
|
+
const result = { committed: false, pushed: false, prUrl: null };
|
|
86
|
+
if (!branchName) return result;
|
|
87
|
+
|
|
88
|
+
const changed = await hasChanges();
|
|
89
|
+
if (!changed) {
|
|
90
|
+
logger.info(`HU ${story.id}: no changes to commit`);
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const title = story.title || story.id;
|
|
95
|
+
const commitMsg = `feat(${story.id}): ${title}`;
|
|
96
|
+
if (config.git?.auto_commit) {
|
|
97
|
+
const commitRes = await commitAll(commitMsg);
|
|
98
|
+
if (commitRes) {
|
|
99
|
+
result.committed = true;
|
|
100
|
+
logger.info(`HU ${story.id}: committed on '${branchName}'`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (config.git?.auto_push && result.committed) {
|
|
105
|
+
try {
|
|
106
|
+
await pushBranch(branchName);
|
|
107
|
+
result.pushed = true;
|
|
108
|
+
logger.info(`HU ${story.id}: pushed '${branchName}'`);
|
|
109
|
+
} catch (err) {
|
|
110
|
+
logger.warn(`HU ${story.id}: push failed: ${err.message}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (config.git?.auto_pr && result.pushed) {
|
|
115
|
+
try {
|
|
116
|
+
const prBody = [
|
|
117
|
+
`## HU ${story.id}: ${title}`,
|
|
118
|
+
"",
|
|
119
|
+
story.certified?.text || "",
|
|
120
|
+
"",
|
|
121
|
+
"### Acceptance criteria",
|
|
122
|
+
...(story.acceptance_criteria || []).map(c => `- ${c}`)
|
|
123
|
+
].join("\n");
|
|
124
|
+
const url = await createPullRequest({
|
|
125
|
+
baseBranch: config.base_branch || "main",
|
|
126
|
+
branch: branchName,
|
|
127
|
+
title: commitMsg,
|
|
128
|
+
body: prBody
|
|
129
|
+
});
|
|
130
|
+
result.prUrl = url;
|
|
131
|
+
logger.info(`HU ${story.id}: PR created ${url}`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
logger.warn(`HU ${story.id}: PR creation failed: ${err.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HU Auto-Generator — converts triage subtasks (+ researcher/architect context)
|
|
3
|
+
* into a certified HU batch ready for hu-sub-pipeline execution.
|
|
4
|
+
*
|
|
5
|
+
* Input: original task, triage subtasks, detected stack, researcher/architect context.
|
|
6
|
+
* Output: HU batch with setup HU (when needed), task HUs with per-HU task_type,
|
|
7
|
+
* and a dependency graph (setup blocks everything; remaining linear by default).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Classify a subtask into a Karajan task_type.
|
|
12
|
+
* Maps free-text subtask descriptions to {infra|sw|add-tests|doc|refactor|nocode}.
|
|
13
|
+
*/
|
|
14
|
+
export function classifyTaskType(text) {
|
|
15
|
+
if (!text || typeof text !== "string") return "sw";
|
|
16
|
+
const t = text.toLowerCase();
|
|
17
|
+
// Order matters: no-code beats infra (Zapier/Notion setups are no-code, not infra)
|
|
18
|
+
if (/\b(no-code|nocode|zapier|make\.com|airtable|notion)\b/.test(t)) return "nocode";
|
|
19
|
+
if (/\b(setup|install|init(?:ialize|iate)?|configure|scaffold|bootstrap)\b/.test(t)) return "infra";
|
|
20
|
+
if (/\b(docker|ci\/cd|pipeline|deploy|workflow\.yml|github actions?)\b/.test(t)) return "infra";
|
|
21
|
+
if (/\b(tests?|coverage|spec|vitest|jest|mocha|playwright)\b/.test(t) && !/\b(component|feature|endpoint)\b/.test(t)) return "add-tests";
|
|
22
|
+
if (/\b(readme|docs?|documentation|guide|tutorial)\b/.test(t)) return "doc";
|
|
23
|
+
if (/\b(refactor|cleanup|reorganiz|restructure|extract)\b/.test(t)) return "refactor";
|
|
24
|
+
return "sw";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Decide whether a setup HU is needed.
|
|
29
|
+
* Needed when: project is new OR stack hints suggest new dependencies.
|
|
30
|
+
*/
|
|
31
|
+
export function needsSetupHu({ isNewProject = false, stackHints = [], subtasks = [] }) {
|
|
32
|
+
if (isNewProject) return true;
|
|
33
|
+
if (stackHints.length > 0) return true;
|
|
34
|
+
// Subtasks mentioning a framework/tool suggest fresh setup
|
|
35
|
+
const setupKeywords = /\b(npm init|package\.json|workspace|monorepo|vite|vitest|express|astro|next\.js|nestjs)\b/i;
|
|
36
|
+
return subtasks.some(s => setupKeywords.test(s));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Build the setup HU story from stack hints + subtasks.
|
|
41
|
+
*/
|
|
42
|
+
function buildSetupHu({ stackHints, subtasks, originalTask }) {
|
|
43
|
+
const hintList = stackHints.length > 0
|
|
44
|
+
? stackHints.map(h => `- ${h}`).join("\n")
|
|
45
|
+
: "- Detect required dependencies from task and install them";
|
|
46
|
+
const certifiedText = [
|
|
47
|
+
`**Setup project infrastructure and dependencies.**`,
|
|
48
|
+
``,
|
|
49
|
+
`Original goal: ${originalTask}`,
|
|
50
|
+
``,
|
|
51
|
+
`**Scope:**`,
|
|
52
|
+
`- Initialize project structure (package.json, workspaces if monorepo)`,
|
|
53
|
+
`- Install all dependencies required by the task`,
|
|
54
|
+
`- Configure tooling (test framework, linter, build tool)`,
|
|
55
|
+
`- Create .env.example with all required env vars`,
|
|
56
|
+
`- Verify install works (npm install, npm run test --run)`,
|
|
57
|
+
``,
|
|
58
|
+
`**Stack hints:**`,
|
|
59
|
+
hintList
|
|
60
|
+
].join("\n");
|
|
61
|
+
return {
|
|
62
|
+
id: "HU-01",
|
|
63
|
+
title: "Setup project infrastructure",
|
|
64
|
+
task_type: "infra",
|
|
65
|
+
status: "certified",
|
|
66
|
+
blocked_by: [],
|
|
67
|
+
certified: { text: certifiedText },
|
|
68
|
+
acceptance_criteria: [
|
|
69
|
+
"Project builds without errors (npm install succeeds)",
|
|
70
|
+
"Test framework is installed and 'npm test' runs (even with 0 tests)",
|
|
71
|
+
"All declared dependencies match what the task requires",
|
|
72
|
+
".env.example exists with documented variables"
|
|
73
|
+
]
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Build a task HU story from a subtask description.
|
|
79
|
+
*/
|
|
80
|
+
function buildTaskHu({ id, subtask, originalTask, blockedBy }) {
|
|
81
|
+
const taskType = classifyTaskType(subtask);
|
|
82
|
+
const certifiedText = [
|
|
83
|
+
`**${subtask}**`,
|
|
84
|
+
``,
|
|
85
|
+
`Part of: ${originalTask}`,
|
|
86
|
+
``,
|
|
87
|
+
`**Scope:** implement this subtask only. Do not touch unrelated subtasks.`
|
|
88
|
+
].join("\n");
|
|
89
|
+
return {
|
|
90
|
+
id,
|
|
91
|
+
title: subtask.length > 80 ? subtask.slice(0, 77) + "..." : subtask,
|
|
92
|
+
task_type: taskType,
|
|
93
|
+
status: "certified",
|
|
94
|
+
blocked_by: blockedBy,
|
|
95
|
+
certified: { text: certifiedText },
|
|
96
|
+
acceptance_criteria: [
|
|
97
|
+
`Subtask '${subtask}' is implemented`,
|
|
98
|
+
`Unit tests cover the new code (where applicable)`,
|
|
99
|
+
`No regressions in existing functionality`
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Main entry point: generate a certified HU batch from triage output.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} input
|
|
108
|
+
* @param {string} input.originalTask - the user's raw task
|
|
109
|
+
* @param {string[]} input.subtasks - triage.subtasks array
|
|
110
|
+
* @param {string[]} [input.stackHints] - detected stack keywords (e.g. ["nodejs", "vitest"])
|
|
111
|
+
* @param {boolean} [input.isNewProject] - true when projectDir is empty/fresh
|
|
112
|
+
* @param {string} [input.researcherContext] - researcher output (optional, used for better HU text)
|
|
113
|
+
* @param {string} [input.architectContext] - architect output (optional, used for dep graph)
|
|
114
|
+
* @returns {{ stories: object[], total: number, certified: number, generated: true }}
|
|
115
|
+
*/
|
|
116
|
+
export function generateHuBatch({
|
|
117
|
+
originalTask,
|
|
118
|
+
subtasks = [],
|
|
119
|
+
stackHints = [],
|
|
120
|
+
isNewProject = false,
|
|
121
|
+
researcherContext = null,
|
|
122
|
+
architectContext = null
|
|
123
|
+
}) {
|
|
124
|
+
if (!originalTask || typeof originalTask !== "string") {
|
|
125
|
+
throw new Error("generateHuBatch: originalTask is required");
|
|
126
|
+
}
|
|
127
|
+
if (!Array.isArray(subtasks) || subtasks.length === 0) {
|
|
128
|
+
throw new Error("generateHuBatch: subtasks array is required");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const stories = [];
|
|
132
|
+
const needsSetup = needsSetupHu({ isNewProject, stackHints, subtasks });
|
|
133
|
+
let nextId = 1;
|
|
134
|
+
|
|
135
|
+
if (needsSetup) {
|
|
136
|
+
stories.push(buildSetupHu({ stackHints, subtasks, originalTask }));
|
|
137
|
+
nextId = 2;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Task HUs: linear dependency chain after setup (conservative default).
|
|
141
|
+
// Architect context could later inform parallel-safe groupings.
|
|
142
|
+
const setupId = needsSetup ? "HU-01" : null;
|
|
143
|
+
let previousId = setupId;
|
|
144
|
+
for (const subtask of subtasks) {
|
|
145
|
+
const id = `HU-${String(nextId).padStart(2, "0")}`;
|
|
146
|
+
const blockedBy = [];
|
|
147
|
+
if (setupId) blockedBy.push(setupId);
|
|
148
|
+
// Conservative: also depend on previous task HU to enforce linear execution.
|
|
149
|
+
// Later phases can relax this with architect-informed graph.
|
|
150
|
+
if (previousId && previousId !== setupId) blockedBy.push(previousId);
|
|
151
|
+
stories.push(buildTaskHu({ id, subtask, originalTask, blockedBy }));
|
|
152
|
+
previousId = id;
|
|
153
|
+
nextId += 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
stories,
|
|
158
|
+
total: stories.length,
|
|
159
|
+
certified: stories.length,
|
|
160
|
+
generated: true,
|
|
161
|
+
source: { triage_subtasks: subtasks.length, researcher: Boolean(researcherContext), architect: Boolean(architectContext) }
|
|
162
|
+
};
|
|
163
|
+
}
|
|
@@ -120,7 +120,7 @@ async function runSingleHu({ storyId, batch, batchSessionId, runIterationFn, emi
|
|
|
120
120
|
}));
|
|
121
121
|
|
|
122
122
|
try {
|
|
123
|
-
const iterResult = await runIterationFn(huTask);
|
|
123
|
+
const iterResult = await runIterationFn(huTask, story);
|
|
124
124
|
const approved = Boolean(iterResult?.approved);
|
|
125
125
|
|
|
126
126
|
// --- Transition to reviewing (post-coder, pre-reviewer evaluation) ---
|
|
@@ -7,7 +7,7 @@ import { ArchitectRole } from "../../roles/architect-role.js";
|
|
|
7
7
|
import { createAgent } from "../../agents/index.js";
|
|
8
8
|
import { createArchitectADRs } from "../../planning-game/architect-adrs.js";
|
|
9
9
|
import { addCheckpoint } from "../../session-store.js";
|
|
10
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
10
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
11
11
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
12
12
|
|
|
13
13
|
async function handleArchitectClarification({ architectOutput, askQuestion, config, logger, emitter, eventBase, session, architectOnOutput, architectProvider, coderRole, researchContext, discoverResult, triageLevel, trackBudget }) {
|
|
@@ -77,12 +77,7 @@ export async function runArchitectStage({ config, logger, emitter, eventBase, se
|
|
|
77
77
|
detail: { architect: architectProvider, provider: architectProvider, executorType: "agent" }
|
|
78
78
|
})
|
|
79
79
|
);
|
|
80
|
-
const architectOnOutput = (
|
|
81
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "architect" }, {
|
|
82
|
-
message: line,
|
|
83
|
-
detail: { stream, agent: architectProvider }
|
|
84
|
-
}));
|
|
85
|
-
};
|
|
80
|
+
const architectOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "architect", architectProvider, payload);
|
|
86
81
|
const architectStall = createStallDetector({
|
|
87
82
|
onOutput: architectOnOutput, emitter, eventBase, stage: "architect", provider: architectProvider
|
|
88
83
|
});
|
|
@@ -10,7 +10,7 @@ import { addCheckpoint, markSessionStatus, saveSession } from "../../session-sto
|
|
|
10
10
|
import { generateDiff, getUntrackedFiles } from "../../review/diff-generator.js";
|
|
11
11
|
import { evaluateTddPolicy } from "../../review/tdd-policy.js";
|
|
12
12
|
import { buildDeferredContext } from "../../review/scope-filter.js";
|
|
13
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
13
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
14
14
|
import { runCoderWithFallback } from "../agent-fallback.js";
|
|
15
15
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
16
16
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
@@ -37,12 +37,7 @@ export async function runCoderStage({ coderRoleInstance, coderRole, config, logg
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
const coderOnOutput = (
|
|
41
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "coder" }, {
|
|
42
|
-
message: line,
|
|
43
|
-
detail: { stream, agent: coderRole.provider }
|
|
44
|
-
}));
|
|
45
|
-
};
|
|
40
|
+
const coderOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "coder", coderRole.provider, payload);
|
|
46
41
|
const coderStall = createStallDetector({
|
|
47
42
|
onOutput: coderOnOutput, emitter, eventBase, stage: "coder", provider: coderRole.provider
|
|
48
43
|
});
|
|
@@ -187,12 +182,7 @@ export async function runRefactorerStage({ refactorerRole, config, logger, emitt
|
|
|
187
182
|
detail: { refactorer: refactorerRole.provider, provider: refactorerRole.provider, executorType: "agent" }
|
|
188
183
|
})
|
|
189
184
|
);
|
|
190
|
-
const refactorerOnOutput = (
|
|
191
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "refactorer" }, {
|
|
192
|
-
message: line,
|
|
193
|
-
detail: { stream, agent: refactorerRole.provider }
|
|
194
|
-
}));
|
|
195
|
-
};
|
|
185
|
+
const refactorerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "refactorer", refactorerRole.provider, payload);
|
|
196
186
|
const refactorerStall = createStallDetector({
|
|
197
187
|
onOutput: refactorerOnOutput, emitter, eventBase, stage: "refactorer", provider: refactorerRole.provider
|
|
198
188
|
});
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import { PlannerRole } from "../../roles/planner-role.js";
|
|
7
7
|
import { createAgent } from "../../agents/index.js";
|
|
8
8
|
import { addCheckpoint, markSessionStatus } from "../../session-store.js";
|
|
9
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
9
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
10
10
|
import { parsePlannerOutput } from "../../prompts/planner.js";
|
|
11
11
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
12
12
|
|
|
@@ -21,12 +21,7 @@ export async function runPlannerStage({ config, logger, emitter, eventBase, sess
|
|
|
21
21
|
})
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
-
const plannerOnOutput = (
|
|
25
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "planner" }, {
|
|
26
|
-
message: line,
|
|
27
|
-
detail: { stream, agent: plannerRole.provider }
|
|
28
|
-
}));
|
|
29
|
-
};
|
|
24
|
+
const plannerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "planner", plannerRole.provider, payload);
|
|
30
25
|
const plannerStall = createStallDetector({
|
|
31
26
|
onOutput: plannerOnOutput, emitter, eventBase, stage: "planner", provider: plannerRole.provider
|
|
32
27
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { ResearcherRole } from "../../roles/researcher-role.js";
|
|
7
7
|
import { addCheckpoint } from "../../session-store.js";
|
|
8
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
9
9
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
10
10
|
|
|
11
11
|
export async function runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget }) {
|
|
@@ -20,12 +20,7 @@ export async function runResearcherStage({ config, logger, emitter, eventBase, s
|
|
|
20
20
|
})
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const researcherOnOutput = (
|
|
24
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "researcher" }, {
|
|
25
|
-
message: line,
|
|
26
|
-
detail: { stream, agent: researcherProvider }
|
|
27
|
-
}));
|
|
28
|
-
};
|
|
23
|
+
const researcherOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "researcher", researcherProvider, payload);
|
|
29
24
|
const researcherStall = createStallDetector({
|
|
30
25
|
onOutput: researcherOnOutput, emitter, eventBase, stage: "researcher", provider: researcherProvider
|
|
31
26
|
});
|
|
@@ -7,7 +7,7 @@ import { addCheckpoint, markSessionStatus, saveSession } from "../../session-sto
|
|
|
7
7
|
import { generateDiff } from "../../review/diff-generator.js";
|
|
8
8
|
import { validateReviewResult } from "../../review/schema.js";
|
|
9
9
|
import { filterReviewScope, buildDeferredContext } from "../../review/scope-filter.js";
|
|
10
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
10
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
11
11
|
import { runReviewerWithFallback } from "../reviewer-fallback.js";
|
|
12
12
|
import { invokeSolomon } from "../solomon-escalation.js";
|
|
13
13
|
import { detectRateLimit } from "../../utils/rate-limit-detector.js";
|
|
@@ -226,12 +226,7 @@ export async function runReviewerStage({ reviewerRole, config, logger, emitter,
|
|
|
226
226
|
};
|
|
227
227
|
}
|
|
228
228
|
|
|
229
|
-
const reviewerOnOutput = (
|
|
230
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "reviewer" }, {
|
|
231
|
-
message: line,
|
|
232
|
-
detail: { stream, agent: reviewerRole.provider }
|
|
233
|
-
}));
|
|
234
|
-
};
|
|
229
|
+
const reviewerOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "reviewer", reviewerRole.provider, payload);
|
|
235
230
|
const reviewerStall = createStallDetector({
|
|
236
231
|
onOutput: reviewerOnOutput, emitter, eventBase, stage: "reviewer", provider: reviewerRole.provider
|
|
237
232
|
});
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { TriageRole } from "../../roles/triage-role.js";
|
|
7
7
|
import { addCheckpoint } from "../../session-store.js";
|
|
8
|
-
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
8
|
+
import { emitProgress, makeEvent, emitAgentOutput } from "../../utils/events.js";
|
|
9
9
|
import { selectModelsForRoles } from "../../utils/model-selector.js";
|
|
10
10
|
import { createStallDetector } from "../../utils/stall-detector.js";
|
|
11
11
|
|
|
@@ -53,12 +53,7 @@ export async function runTriageStage({ config, logger, emitter, eventBase, sessi
|
|
|
53
53
|
})
|
|
54
54
|
);
|
|
55
55
|
|
|
56
|
-
const triageOnOutput = (
|
|
57
|
-
emitProgress(emitter, makeEvent("agent:output", { ...eventBase, stage: "triage" }, {
|
|
58
|
-
message: line,
|
|
59
|
-
detail: { stream, agent: triageProvider }
|
|
60
|
-
}));
|
|
61
|
-
};
|
|
56
|
+
const triageOnOutput = (payload) => emitAgentOutput(emitter, eventBase, "triage", triageProvider, payload);
|
|
62
57
|
const triageStall = createStallDetector({
|
|
63
58
|
onOutput: triageOnOutput, emitter, eventBase, stage: "triage", provider: triageProvider
|
|
64
59
|
});
|
package/src/orchestrator.js
CHANGED
|
@@ -71,14 +71,20 @@ export const parseCheckpointAnswer = _parseCheckpointAnswer;
|
|
|
71
71
|
|
|
72
72
|
// PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
|
|
73
73
|
|
|
74
|
-
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
|
|
74
|
+
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
|
|
75
75
|
let researchContext = null;
|
|
76
76
|
let plannedTask = task;
|
|
77
77
|
|
|
78
|
+
// Brain: track compression across pre-loop roles
|
|
79
|
+
const brainCompress = brainCtx?.enabled
|
|
80
|
+
? (await import("./orchestrator/brain-coordinator.js")).processRoleOutput
|
|
81
|
+
: null;
|
|
82
|
+
|
|
78
83
|
if (pipelineFlags.researcherEnabled) {
|
|
79
84
|
const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
80
85
|
researchContext = researcherResult.researchContext;
|
|
81
86
|
stageResults.researcher = researcherResult.stageResult;
|
|
87
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "researcher", output: researcherResult.stageResult, iteration: 0 });
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
// --- Architect (between researcher and planner) ---
|
|
@@ -93,6 +99,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
93
99
|
});
|
|
94
100
|
architectContext = architectResult.architectContext;
|
|
95
101
|
stageResults.architect = architectResult.stageResult;
|
|
102
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "architect", output: architectResult.stageResult, iteration: 0 });
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
|
|
@@ -101,6 +108,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
101
108
|
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
|
|
102
109
|
plannedTask = plannerResult.plannedTask;
|
|
103
110
|
stageResults.planner = plannerResult.stageResult;
|
|
111
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "planner", output: plannerResult.stageResult, iteration: 0 });
|
|
104
112
|
|
|
105
113
|
await tryCiComment({
|
|
106
114
|
config, session, logger,
|
|
@@ -201,12 +209,12 @@ async function handleStandbyResult({ stageResult, session, emitter, eventBase, i
|
|
|
201
209
|
|
|
202
210
|
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
203
211
|
for (const alert of alerts) {
|
|
204
|
-
emitProgress(emitter, makeEvent("
|
|
212
|
+
emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
|
|
205
213
|
status: alert.severity === "critical" ? "fail" : "warn",
|
|
206
214
|
message: alert.message,
|
|
207
215
|
detail: alert.detail
|
|
208
216
|
}));
|
|
209
|
-
logger.warn(`
|
|
217
|
+
logger.warn(`Rules alert [${alert.rule}]: ${alert.message}`);
|
|
210
218
|
}
|
|
211
219
|
}
|
|
212
220
|
|
|
@@ -313,7 +321,7 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
|
|
316
|
-
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
|
|
324
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger, brainCtx }) {
|
|
317
325
|
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
318
326
|
|
|
319
327
|
if (testerEnabled) {
|
|
@@ -327,6 +335,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
327
335
|
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
328
336
|
await saveSession(session);
|
|
329
337
|
if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
|
|
338
|
+
// Brain: push tester failure into feedback queue + compress for next coder iteration
|
|
339
|
+
if (brainCtx?.enabled) {
|
|
340
|
+
const { processRoleOutput } = await import("./orchestrator/brain-coordinator.js");
|
|
341
|
+
processRoleOutput(brainCtx, { roleName: "tester", output: testerResult.stageResult, iteration: i });
|
|
342
|
+
}
|
|
330
343
|
return { action: "continue" };
|
|
331
344
|
}
|
|
332
345
|
if (testerResult.stageResult) {
|
|
@@ -346,6 +359,11 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
346
359
|
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
347
360
|
await saveSession(session);
|
|
348
361
|
if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
|
|
362
|
+
// Brain: push security findings into feedback queue + compress for next coder iteration
|
|
363
|
+
if (brainCtx?.enabled) {
|
|
364
|
+
const { processRoleOutput } = await import("./orchestrator/brain-coordinator.js");
|
|
365
|
+
processRoleOutput(brainCtx, { roleName: "security", output: securityResult.stageResult, iteration: i });
|
|
366
|
+
}
|
|
349
367
|
return { action: "continue" };
|
|
350
368
|
}
|
|
351
369
|
if (securityResult.stageResult) {
|
|
@@ -494,7 +512,7 @@ async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBa
|
|
|
494
512
|
}
|
|
495
513
|
|
|
496
514
|
|
|
497
|
-
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults }) {
|
|
515
|
+
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults, brainCtx }) {
|
|
498
516
|
// --- HU Reviewer (first stage, before everything else, opt-in) ---
|
|
499
517
|
const huFile = flags.huFile || null;
|
|
500
518
|
if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
|
|
@@ -663,12 +681,17 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
663
681
|
}
|
|
664
682
|
|
|
665
683
|
// --- Researcher → Planner ---
|
|
666
|
-
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
684
|
+
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx });
|
|
667
685
|
|
|
668
686
|
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
669
687
|
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
670
688
|
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
671
689
|
|
|
690
|
+
// --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
|
|
691
|
+
await maybeGenerateAutoHuBatch({
|
|
692
|
+
flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session
|
|
693
|
+
});
|
|
694
|
+
|
|
672
695
|
// --- Auto-install skills based on task + planner output + project detection ---
|
|
673
696
|
// Runs AFTER triage and planner so that the planned task text (which includes
|
|
674
697
|
// planner output like implementation steps) is available for keyword detection.
|
|
@@ -698,6 +721,87 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
698
721
|
return { plannedTask, updatedConfig };
|
|
699
722
|
}
|
|
700
723
|
|
|
724
|
+
/**
|
|
725
|
+
* Auto-generate HU batch from triage decomposition when no manual huFile is present.
|
|
726
|
+
* Runs after researcher/architect/planner so that context is available for better HUs.
|
|
727
|
+
* Sets stageResults.huReviewer so needsSubPipeline picks it up later.
|
|
728
|
+
*/
|
|
729
|
+
async function maybeGenerateAutoHuBatch({ flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session }) {
|
|
730
|
+
// Skip if user passed a manual hu-file
|
|
731
|
+
if (flags?.huFile) return;
|
|
732
|
+
// Skip if hu-reviewer already produced a batch (manual enable + PG stories)
|
|
733
|
+
if (stageResults.huReviewer) return;
|
|
734
|
+
// Need triage decomposition recommendation
|
|
735
|
+
const shouldDecompose = stageResults.triage?.shouldDecompose;
|
|
736
|
+
const subtasks = stageResults.triage?.subtasks;
|
|
737
|
+
if (!shouldDecompose || !Array.isArray(subtasks) || subtasks.length < 2) return;
|
|
738
|
+
|
|
739
|
+
const { generateHuBatch } = await import("./hu/auto-generator.js");
|
|
740
|
+
|
|
741
|
+
// Detect if project is new: empty dir or only .git/.karajan/.gitignore
|
|
742
|
+
let isNewProject = false;
|
|
743
|
+
try {
|
|
744
|
+
const fs = await import("node:fs/promises");
|
|
745
|
+
const entries = await fs.readdir(projectDir);
|
|
746
|
+
const relevant = entries.filter(e => !e.startsWith(".git") && e !== ".karajan" && e !== ".gitignore");
|
|
747
|
+
isNewProject = relevant.length === 0;
|
|
748
|
+
} catch { /* ignore */ }
|
|
749
|
+
|
|
750
|
+
// Extract stack hints from planner + architect output
|
|
751
|
+
const stackHints = [];
|
|
752
|
+
const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
|
|
753
|
+
const stackKeywords = ["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "fastapi", "django", "spring", "gin", "nestjs", "monorepo", "workspaces"];
|
|
754
|
+
for (const kw of stackKeywords) {
|
|
755
|
+
if (combined.includes(kw)) stackHints.push(kw);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const batch = generateHuBatch({
|
|
759
|
+
originalTask: task,
|
|
760
|
+
subtasks,
|
|
761
|
+
stackHints,
|
|
762
|
+
isNewProject,
|
|
763
|
+
researcherContext: stageResults.researcher?.summary || null,
|
|
764
|
+
architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
|
|
765
|
+
});
|
|
766
|
+
|
|
767
|
+
// Persist batch to HU store so hu-sub-pipeline can update story status via saveHuBatch.
|
|
768
|
+
// Use session.id as batchSessionId.
|
|
769
|
+
const batchSessionId = `auto-${session.id}`;
|
|
770
|
+
try {
|
|
771
|
+
const fs = await import("node:fs/promises");
|
|
772
|
+
const path = await import("node:path");
|
|
773
|
+
const { getKarajanHome } = await import("./utils/paths.js");
|
|
774
|
+
const huDir = path.join(getKarajanHome(), "hu", batchSessionId);
|
|
775
|
+
await fs.mkdir(huDir, { recursive: true });
|
|
776
|
+
const persistBatch = {
|
|
777
|
+
session_id: batchSessionId,
|
|
778
|
+
created_at: new Date().toISOString(),
|
|
779
|
+
updated_at: new Date().toISOString(),
|
|
780
|
+
stories: batch.stories
|
|
781
|
+
};
|
|
782
|
+
await fs.writeFile(path.join(huDir, "batch.json"), JSON.stringify(persistBatch, null, 2));
|
|
783
|
+
} catch (err) {
|
|
784
|
+
logger.warn(`Auto-HU: failed to persist batch (${err.message}) — sub-pipeline will use in-memory fallback`);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// Wrap in format compatible with needsSubPipeline + runHuSubPipeline
|
|
788
|
+
stageResults.huReviewer = {
|
|
789
|
+
ok: true,
|
|
790
|
+
stories: batch.stories,
|
|
791
|
+
total: batch.total,
|
|
792
|
+
certified: batch.certified,
|
|
793
|
+
batchSessionId,
|
|
794
|
+
auto_generated: true,
|
|
795
|
+
source: batch.source
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
|
|
799
|
+
emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
|
|
800
|
+
message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
|
|
801
|
+
detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints }
|
|
802
|
+
}));
|
|
803
|
+
}
|
|
804
|
+
|
|
701
805
|
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|
|
702
806
|
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
|
|
703
807
|
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
@@ -870,11 +974,11 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
870
974
|
return { action: "ok", review: reviewerResult.review };
|
|
871
975
|
}
|
|
872
976
|
|
|
873
|
-
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker }) {
|
|
977
|
+
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker, brainCtx }) {
|
|
874
978
|
session.reviewer_retry_count = 0;
|
|
875
979
|
const postLoopResult = await handlePostLoopStages({
|
|
876
980
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
877
|
-
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
|
|
981
|
+
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger, brainCtx
|
|
878
982
|
});
|
|
879
983
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
880
984
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
@@ -883,8 +987,72 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
883
987
|
return { action: "return", result };
|
|
884
988
|
}
|
|
885
989
|
|
|
886
|
-
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
|
|
990
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker, brainCtx }) {
|
|
887
991
|
const budget = budgetSummary();
|
|
992
|
+
|
|
993
|
+
// Brain-owned decision: max_iterations is guidance, not a hard rule.
|
|
994
|
+
// Brain evaluates the feedback queue state to decide extend / finalize / escalate.
|
|
995
|
+
// Solomon is only consulted if Brain cannot decide on its own.
|
|
996
|
+
if (brainCtx?.enabled) {
|
|
997
|
+
const entries = brainCtx.feedbackQueue?.entries || [];
|
|
998
|
+
const pending = entries.map(e => ({ source: e.source, category: e.category, severity: e.severity, description: e.description }));
|
|
999
|
+
const hasSecurity = entries.some(e => e.category === "security" || e.source === "security");
|
|
1000
|
+
const hasCorrectness = entries.some(e => ["correctness", "tests"].includes(e.category));
|
|
1001
|
+
const hasStyleOnly = entries.length > 0 && !hasSecurity && !hasCorrectness;
|
|
1002
|
+
|
|
1003
|
+
if (hasSecurity) {
|
|
1004
|
+
// Brain: security issues unresolved → cannot finalize, escalate
|
|
1005
|
+
logger.warn(`Brain: max_iterations reached with ${entries.filter(e => e.category === "security" || e.source === "security").length} unresolved security issue(s) — cannot finalize`);
|
|
1006
|
+
return { paused: true, sessionId: session.id, question: "Brain: unresolved security issues at max_iterations. Review manually or extend pipeline.", context: "brain_security_block", pending };
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
if (hasCorrectness) {
|
|
1010
|
+
// Brain: correctness/test issues pending → extend iterations (Brain's decision, not a rule)
|
|
1011
|
+
logger.info(`Brain: max_iterations reached with ${entries.filter(e => ["correctness", "tests"].includes(e.category)).length} correctness issue(s) pending — extending iterations`);
|
|
1012
|
+
session.reviewer_retry_count = 0;
|
|
1013
|
+
await saveSession(session);
|
|
1014
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: Math.ceil(config.max_iterations / 2) };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
if (entries.length === 0) {
|
|
1018
|
+
// Brain: no pending feedback → last reviewer approved, finalize
|
|
1019
|
+
logger.info("Brain: max_iterations reached with clean feedback queue — finalizing as approved");
|
|
1020
|
+
return { approved: true, sessionId: session.id, reason: "brain_approved" };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// hasStyleOnly: genuine dilemma → Brain consults Solomon
|
|
1024
|
+
logger.info(`Brain: max_iterations with ${entries.length} style-only issue(s) — consulting Solomon on dilemma`);
|
|
1025
|
+
const { invokeSolomon: invokeSolomonAI } = await import("./orchestrator/solomon-escalation.js");
|
|
1026
|
+
const solomonResult = await invokeSolomonAI({
|
|
1027
|
+
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
1028
|
+
iteration: config.max_iterations,
|
|
1029
|
+
conflict: {
|
|
1030
|
+
stage: "brain-max-iterations",
|
|
1031
|
+
task,
|
|
1032
|
+
iterationCount: config.max_iterations,
|
|
1033
|
+
maxIterations: config.max_iterations,
|
|
1034
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
1035
|
+
dilemma: `Max iterations reached with ${entries.length} style-only issue(s) pending. Accept as-is or request more work?`,
|
|
1036
|
+
pendingIssues: pending,
|
|
1037
|
+
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached" }]
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
// Brain applies Solomon's decision
|
|
1041
|
+
if (solomonResult.action === "approve") {
|
|
1042
|
+
logger.info("Brain: Solomon advised approve for style-only pending — finalizing");
|
|
1043
|
+
return { approved: true, sessionId: session.id, reason: "brain_solomon_approved" };
|
|
1044
|
+
}
|
|
1045
|
+
if (solomonResult.action === "continue") {
|
|
1046
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: solomonResult.extraIterations || Math.ceil(config.max_iterations / 2) };
|
|
1047
|
+
}
|
|
1048
|
+
if (solomonResult.action === "pause") {
|
|
1049
|
+
return { paused: true, sessionId: session.id, question: solomonResult.question, context: "brain_solomon_dilemma" };
|
|
1050
|
+
}
|
|
1051
|
+
// Fallback: escalate to human
|
|
1052
|
+
return { paused: true, sessionId: session.id, question: `Brain+Solomon cannot resolve: ${entries.length} pending issue(s) at max_iterations`, context: "max_iterations" };
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// Legacy path (Brain disabled): original Solomon-driven flow
|
|
888
1056
|
const solomonResult = await invokeSolomon({
|
|
889
1057
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
890
1058
|
iteration: config.max_iterations,
|
|
@@ -898,13 +1066,11 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
898
1066
|
}
|
|
899
1067
|
});
|
|
900
1068
|
|
|
901
|
-
// Solomon approved the work — treat as successful completion
|
|
902
1069
|
if (solomonResult.action === "approve") {
|
|
903
1070
|
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
904
1071
|
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
905
1072
|
}
|
|
906
1073
|
|
|
907
|
-
// Solomon says continue — extend iterations
|
|
908
1074
|
if (solomonResult.action === "continue") {
|
|
909
1075
|
if (solomonResult.humanGuidance) {
|
|
910
1076
|
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
@@ -1074,7 +1240,7 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1074
1240
|
ctx.stageResults = {};
|
|
1075
1241
|
ctx.sonarState = { issuesInitial: null, issuesFinal: null };
|
|
1076
1242
|
|
|
1077
|
-
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults });
|
|
1243
|
+
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults, brainCtx: ctx.brainCtx });
|
|
1078
1244
|
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1079
1245
|
ctx.config = preLoopResult.updatedConfig;
|
|
1080
1246
|
|
|
@@ -1199,7 +1365,7 @@ async function runSingleIteration(ctx) {
|
|
|
1199
1365
|
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1200
1366
|
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1201
1367
|
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
|
|
1202
|
-
rtkTracker: ctx.rtkTracker
|
|
1368
|
+
rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
|
|
1203
1369
|
});
|
|
1204
1370
|
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1205
1371
|
}
|
|
@@ -1320,7 +1486,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1320
1486
|
}
|
|
1321
1487
|
|
|
1322
1488
|
// Solomon decides whether to extend iterations or stop
|
|
1323
|
-
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1489
|
+
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
|
|
1324
1490
|
|
|
1325
1491
|
// Solomon said "continue" — extend iterations and keep going
|
|
1326
1492
|
if (maxIterResult.reason === "max_iterations_extended") {
|
|
@@ -1358,7 +1524,7 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1358
1524
|
}
|
|
1359
1525
|
|
|
1360
1526
|
// Extended iterations also exhausted — final Solomon call
|
|
1361
|
-
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1527
|
+
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx });
|
|
1362
1528
|
return finalResult;
|
|
1363
1529
|
}
|
|
1364
1530
|
|
|
@@ -1452,9 +1618,33 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1452
1618
|
detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
|
|
1453
1619
|
}));
|
|
1454
1620
|
|
|
1621
|
+
// Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
|
|
1622
|
+
const originalMaxIterations = ctx.config.max_iterations;
|
|
1623
|
+
const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
|
|
1624
|
+
const huBranches = new Map();
|
|
1625
|
+
const { prepareHuBranch, finalizeHuCommit } = await import("./git/hu-automation.js");
|
|
1455
1626
|
const subPipelineResult = await runHuSubPipeline({
|
|
1456
1627
|
huReviewerResult: ctx.stageResults.huReviewer,
|
|
1457
|
-
runIterationFn: async (huTask) =>
|
|
1628
|
+
runIterationFn: async (huTask, story) => {
|
|
1629
|
+
ctx.config.max_iterations = huMaxIterations;
|
|
1630
|
+
if (ctx.brainCtx?.enabled) {
|
|
1631
|
+
ctx.brainCtx.extensionCount = 0;
|
|
1632
|
+
const { createBrainContext } = await import("./orchestrator/brain-coordinator.js");
|
|
1633
|
+
const fresh = createBrainContext({ enabled: true });
|
|
1634
|
+
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
1635
|
+
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
1636
|
+
}
|
|
1637
|
+
const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
|
|
1638
|
+
try {
|
|
1639
|
+
const result = await runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger });
|
|
1640
|
+
if (result?.approved) {
|
|
1641
|
+
await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
|
|
1642
|
+
}
|
|
1643
|
+
return result;
|
|
1644
|
+
} finally {
|
|
1645
|
+
ctx.config.max_iterations = originalMaxIterations;
|
|
1646
|
+
}
|
|
1647
|
+
},
|
|
1458
1648
|
emitter,
|
|
1459
1649
|
eventBase: ctx.eventBase,
|
|
1460
1650
|
logger,
|
package/src/utils/display.js
CHANGED
|
@@ -45,6 +45,7 @@ const ICONS = {
|
|
|
45
45
|
"solomon:escalate": "\u26a0\ufe0f",
|
|
46
46
|
"coder:standby": "\u23f3",
|
|
47
47
|
"coder:standby_heartbeat": "\u23f3",
|
|
48
|
+
"agent:heartbeat": "\u23f3",
|
|
48
49
|
"coder:standby_resume": "\u25b6\ufe0f",
|
|
49
50
|
"budget:update": "\ud83d\udcb8",
|
|
50
51
|
"iteration:end": "\u23f1\ufe0f",
|
|
@@ -84,7 +85,8 @@ const BAR = `${ANSI.dim}\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u
|
|
|
84
85
|
|
|
85
86
|
export function printHeader({ task, config }) {
|
|
86
87
|
const version = DISPLAY_VERSION;
|
|
87
|
-
|
|
88
|
+
// Force banner: caller already gates on !jsonMode, so if we're here it's a human-readable run
|
|
89
|
+
printBanner(version, { force: true });
|
|
88
90
|
console.log(`${ANSI.bold}Task:${ANSI.reset} ${task}`);
|
|
89
91
|
console.log(
|
|
90
92
|
`${ANSI.bold}Coder:${ANSI.reset} ${config.roles?.coder?.provider || config.coder} ${ANSI.dim}|${ANSI.reset} ${ANSI.bold}Reviewer:${ANSI.reset} ${config.roles?.reviewer?.provider || config.reviewer}`
|
|
@@ -437,6 +439,15 @@ const EVENT_HANDLERS = {
|
|
|
437
439
|
console.log(` \u251c\u2500 ${ANSI.yellow}${icon} Standby: ${remaining}s remaining${ANSI.reset}`);
|
|
438
440
|
},
|
|
439
441
|
|
|
442
|
+
"agent:heartbeat": (event, icon) => {
|
|
443
|
+
const d = event.detail || {};
|
|
444
|
+
const provider = d.provider || "?";
|
|
445
|
+
const elapsed = d.elapsedMs ? Math.round(d.elapsedMs / 1000) : 0;
|
|
446
|
+
const silent = d.silenceMs ? Math.round(d.silenceMs / 1000) : 0;
|
|
447
|
+
const status = d.status === "waiting" ? `${ANSI.yellow}waiting (silent ${silent}s)` : `${ANSI.green}working`;
|
|
448
|
+
console.log(` \u251c\u2500 ${ANSI.dim}${icon} ${provider} ${status}${ANSI.dim} — ${elapsed}s elapsed${ANSI.reset}`);
|
|
449
|
+
},
|
|
450
|
+
|
|
440
451
|
"coder:standby_resume": (event, icon) => {
|
|
441
452
|
console.log(` \u251c\u2500 ${ANSI.green}${icon} Cooldown expired \u2014 resuming with ${event.detail?.coder || event.detail?.provider || "?"}${ANSI.reset}`);
|
|
442
453
|
},
|
|
@@ -593,12 +604,16 @@ const EVENT_HANDLERS = {
|
|
|
593
604
|
console.log(` \u251c\u2500 ${ANSI.green}\ud83d\ude80 CI PR created: ${url}${ANSI.reset}`);
|
|
594
605
|
},
|
|
595
606
|
|
|
596
|
-
"
|
|
597
|
-
console.log(` \u251c\u2500 ${ANSI.yellow}\
|
|
607
|
+
"brain:rules-alert": (event) => {
|
|
608
|
+
console.log(` \u251c\u2500 ${ANSI.yellow}\u26a0\ufe0f Rules alert: ${event.message || "anomaly detected"}${ANSI.reset}`);
|
|
598
609
|
},
|
|
599
610
|
|
|
600
611
|
"agent:output": (event) => {
|
|
601
612
|
console.log(` \u2502 ${ANSI.dim}${event.message}${ANSI.reset}`);
|
|
613
|
+
},
|
|
614
|
+
|
|
615
|
+
"agent:action": (event) => {
|
|
616
|
+
console.log(` \u2502 ${ANSI.dim}\u2937 ${event.message}${ANSI.reset}`);
|
|
602
617
|
}
|
|
603
618
|
};
|
|
604
619
|
|
|
@@ -608,7 +623,6 @@ const EVENT_HANDLERS = {
|
|
|
608
623
|
const QUIET_SUPPRESSED = new Set([
|
|
609
624
|
"agent:output",
|
|
610
625
|
"agent:stall",
|
|
611
|
-
"agent:heartbeat",
|
|
612
626
|
"pipeline:simplify",
|
|
613
627
|
"pipeline:analysis-only",
|
|
614
628
|
"policies:resolved",
|
package/src/utils/events.js
CHANGED
|
@@ -21,3 +21,15 @@ export function makeEvent(type, base, extra = {}) {
|
|
|
21
21
|
timestamp: new Date().toISOString()
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Standard agent output emitter. Routes tool invocations to agent:action
|
|
27
|
+
* (visible in quiet mode) and everything else to agent:output (verbose only).
|
|
28
|
+
*/
|
|
29
|
+
export function emitAgentOutput(emitter, eventBase, stage, provider, { stream, line, kind }) {
|
|
30
|
+
const eventType = kind === "tool" ? "agent:action" : "agent:output";
|
|
31
|
+
emitProgress(emitter, makeEvent(eventType, { ...eventBase, stage }, {
|
|
32
|
+
message: line,
|
|
33
|
+
detail: { stream, agent: provider, kind }
|
|
34
|
+
}));
|
|
35
|
+
}
|