karajan-code 2.0.2 → 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/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.js +111 -1
package/package.json
CHANGED
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) ---
|
package/src/orchestrator.js
CHANGED
|
@@ -687,6 +687,11 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
687
687
|
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
688
688
|
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
689
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
|
+
|
|
690
695
|
// --- Auto-install skills based on task + planner output + project detection ---
|
|
691
696
|
// Runs AFTER triage and planner so that the planned task text (which includes
|
|
692
697
|
// planner output like implementation steps) is available for keyword detection.
|
|
@@ -716,6 +721,87 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
716
721
|
return { plannedTask, updatedConfig };
|
|
717
722
|
}
|
|
718
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
|
+
|
|
719
805
|
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|
|
720
806
|
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
|
|
721
807
|
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
@@ -1532,9 +1618,33 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1532
1618
|
detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
|
|
1533
1619
|
}));
|
|
1534
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");
|
|
1535
1626
|
const subPipelineResult = await runHuSubPipeline({
|
|
1536
1627
|
huReviewerResult: ctx.stageResults.huReviewer,
|
|
1537
|
-
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
|
+
},
|
|
1538
1648
|
emitter,
|
|
1539
1649
|
eventBase: ctx.eventBase,
|
|
1540
1650
|
logger,
|