karajan-code 1.16.0 → 1.17.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/activity-log.js +13 -13
- package/src/agents/availability.js +2 -3
- package/src/agents/claude-agent.js +42 -21
- package/src/agents/model-registry.js +1 -1
- package/src/becaria/dispatch.js +1 -1
- package/src/becaria/repo.js +3 -3
- package/src/cli.js +5 -2
- package/src/commands/doctor.js +154 -108
- package/src/commands/init.js +101 -90
- package/src/commands/plan.js +1 -1
- package/src/commands/report.js +77 -71
- package/src/commands/roles.js +0 -1
- package/src/commands/run.js +2 -3
- package/src/config.js +155 -93
- package/src/git/automation.js +3 -4
- package/src/guards/policy-resolver.js +3 -3
- package/src/mcp/orphan-guard.js +1 -2
- package/src/mcp/progress.js +4 -3
- package/src/mcp/run-kj.js +1 -0
- package/src/mcp/server-handlers.js +242 -253
- package/src/mcp/server.js +4 -3
- package/src/mcp/tools.js +2 -0
- package/src/orchestrator/agent-fallback.js +1 -3
- package/src/orchestrator/iteration-stages.js +206 -170
- package/src/orchestrator/pre-loop-stages.js +200 -34
- package/src/orchestrator/solomon-rules.js +2 -2
- package/src/orchestrator.js +820 -748
- package/src/planning-game/adapter.js +23 -20
- package/src/planning-game/architect-adrs.js +45 -0
- package/src/planning-game/client.js +15 -1
- package/src/planning-game/decomposition.js +7 -5
- package/src/prompts/architect.js +88 -0
- package/src/prompts/discover.js +54 -53
- package/src/prompts/planner.js +53 -33
- package/src/prompts/triage.js +8 -16
- package/src/review/parser.js +18 -19
- package/src/review/profiles.js +2 -2
- package/src/review/schema.js +3 -3
- package/src/review/scope-filter.js +3 -4
- package/src/roles/architect-role.js +122 -0
- package/src/roles/commiter-role.js +2 -2
- package/src/roles/discover-role.js +59 -67
- package/src/roles/index.js +1 -0
- package/src/roles/planner-role.js +54 -38
- package/src/roles/refactorer-role.js +8 -7
- package/src/roles/researcher-role.js +6 -7
- package/src/roles/reviewer-role.js +4 -5
- package/src/roles/security-role.js +3 -4
- package/src/roles/solomon-role.js +6 -18
- package/src/roles/sonar-role.js +5 -1
- package/src/roles/tester-role.js +8 -5
- package/src/roles/triage-role.js +2 -2
- package/src/session-cleanup.js +29 -24
- package/src/session-store.js +1 -1
- package/src/sonar/api.js +1 -1
- package/src/sonar/manager.js +1 -1
- package/src/sonar/project-key.js +5 -5
- package/src/sonar/scanner.js +34 -65
- package/src/utils/display.js +312 -272
- package/src/utils/git.js +3 -3
- package/src/utils/logger.js +6 -1
- package/src/utils/model-selector.js +5 -5
- package/src/utils/process.js +80 -102
- package/src/utils/rate-limit-detector.js +13 -13
- package/src/utils/run-log.js +55 -52
- package/templates/roles/architect.md +62 -0
- package/templates/roles/planner.md +1 -0
package/src/orchestrator.js
CHANGED
|
@@ -26,97 +26,93 @@ import { applyPolicies } from "./guards/policy-resolver.js";
|
|
|
26
26
|
import { resolveReviewProfile } from "./review/profiles.js";
|
|
27
27
|
import { CoderRole } from "./roles/coder-role.js";
|
|
28
28
|
import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
29
|
-
import { runTriageStage, runResearcherStage, runPlannerStage, runDiscoverStage } from "./orchestrator/pre-loop-stages.js";
|
|
29
|
+
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage } from "./orchestrator/pre-loop-stages.js";
|
|
30
30
|
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runReviewerStage } from "./orchestrator/iteration-stages.js";
|
|
31
31
|
import { runTesterStage, runSecurityStage } from "./orchestrator/post-loop-stages.js";
|
|
32
32
|
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./orchestrator/standby.js";
|
|
33
33
|
|
|
34
34
|
|
|
35
|
+
// --- Extracted helper functions (pure refactoring, zero behavior change) ---
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
function resolvePipelineFlags(config) {
|
|
38
|
+
return {
|
|
39
|
+
plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
|
|
40
|
+
refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
|
|
41
|
+
researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
|
|
42
|
+
testerEnabled: Boolean(config.pipeline?.tester?.enabled),
|
|
43
|
+
securityEnabled: Boolean(config.pipeline?.security?.enabled),
|
|
44
|
+
reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
|
|
45
|
+
discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
|
|
46
|
+
architectEnabled: Boolean(config.pipeline?.architect?.enabled),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
51
|
+
const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, reviewerEnabled, discoverEnabled, architectEnabled } = pipelineFlags;
|
|
37
52
|
const plannerRole = resolveRole(config, "planner");
|
|
38
53
|
const coderRole = resolveRole(config, "coder");
|
|
39
54
|
const reviewerRole = resolveRole(config, "reviewer");
|
|
40
55
|
const refactorerRole = resolveRole(config, "refactorer");
|
|
41
|
-
let plannerEnabled = Boolean(config.pipeline?.planner?.enabled);
|
|
42
|
-
let refactorerEnabled = Boolean(config.pipeline?.refactorer?.enabled);
|
|
43
|
-
let researcherEnabled = Boolean(config.pipeline?.researcher?.enabled);
|
|
44
|
-
let testerEnabled = Boolean(config.pipeline?.tester?.enabled);
|
|
45
|
-
let securityEnabled = Boolean(config.pipeline?.security?.enabled);
|
|
46
|
-
let reviewerEnabled = config.pipeline?.reviewer?.enabled !== false;
|
|
47
|
-
let discoverEnabled = Boolean(config.pipeline?.discover?.enabled);
|
|
48
|
-
// Triage is always mandatory — it classifies taskType for policy resolution
|
|
49
56
|
const triageEnabled = true;
|
|
50
57
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
61
|
-
const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
62
|
-
|
|
63
|
-
const summary = {
|
|
64
|
-
dry_run: true,
|
|
65
|
-
task,
|
|
66
|
-
policies: dryRunPolicies,
|
|
67
|
-
roles: {
|
|
68
|
-
planner: plannerRole,
|
|
69
|
-
coder: coderRole,
|
|
70
|
-
reviewer: reviewerRole,
|
|
71
|
-
refactorer: refactorerRole
|
|
72
|
-
},
|
|
73
|
-
pipeline: {
|
|
74
|
-
discover_enabled: discoverEnabled,
|
|
75
|
-
triage_enabled: triageEnabled,
|
|
76
|
-
planner_enabled: plannerEnabled,
|
|
77
|
-
refactorer_enabled: refactorerEnabled,
|
|
78
|
-
sonar_enabled: Boolean(config.sonarqube?.enabled),
|
|
79
|
-
reviewer_enabled: reviewerEnabled,
|
|
80
|
-
researcher_enabled: researcherEnabled,
|
|
81
|
-
tester_enabled: testerEnabled,
|
|
82
|
-
security_enabled: securityEnabled,
|
|
83
|
-
solomon_enabled: Boolean(config.pipeline?.solomon?.enabled)
|
|
84
|
-
},
|
|
85
|
-
limits: {
|
|
86
|
-
max_iterations: config.max_iterations,
|
|
87
|
-
max_iteration_minutes: config.session?.max_iteration_minutes,
|
|
88
|
-
max_total_minutes: config.session?.max_total_minutes,
|
|
89
|
-
max_sonar_retries: config.session?.max_sonar_retries,
|
|
90
|
-
max_reviewer_retries: config.session?.max_reviewer_retries,
|
|
91
|
-
max_tester_retries: config.session?.max_tester_retries,
|
|
92
|
-
max_security_retries: config.session?.max_security_retries
|
|
93
|
-
},
|
|
94
|
-
prompts: {
|
|
95
|
-
coder: coderPrompt,
|
|
96
|
-
reviewer: reviewerPrompt
|
|
97
|
-
},
|
|
98
|
-
git: config.git
|
|
99
|
-
};
|
|
58
|
+
const dryRunPolicies = applyPolicies({
|
|
59
|
+
taskType: flags.taskType || config.taskType || null,
|
|
60
|
+
policies: config.policies,
|
|
61
|
+
});
|
|
62
|
+
const projectDir = config.projectDir || process.cwd();
|
|
63
|
+
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
64
|
+
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
65
|
+
const coderPrompt = buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
66
|
+
const reviewerPrompt = buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled) });
|
|
100
67
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
68
|
+
const summary = {
|
|
69
|
+
dry_run: true,
|
|
70
|
+
task,
|
|
71
|
+
policies: dryRunPolicies,
|
|
72
|
+
roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
|
|
73
|
+
pipeline: {
|
|
74
|
+
discover_enabled: discoverEnabled,
|
|
75
|
+
architect_enabled: architectEnabled,
|
|
76
|
+
triage_enabled: triageEnabled,
|
|
77
|
+
planner_enabled: plannerEnabled,
|
|
78
|
+
refactorer_enabled: refactorerEnabled,
|
|
79
|
+
sonar_enabled: Boolean(config.sonarqube?.enabled),
|
|
80
|
+
reviewer_enabled: reviewerEnabled,
|
|
81
|
+
researcher_enabled: researcherEnabled,
|
|
82
|
+
tester_enabled: testerEnabled,
|
|
83
|
+
security_enabled: securityEnabled,
|
|
84
|
+
solomon_enabled: Boolean(config.pipeline?.solomon?.enabled)
|
|
85
|
+
},
|
|
86
|
+
limits: {
|
|
87
|
+
max_iterations: config.max_iterations,
|
|
88
|
+
max_iteration_minutes: config.session?.max_iteration_minutes,
|
|
89
|
+
max_total_minutes: config.session?.max_total_minutes,
|
|
90
|
+
max_sonar_retries: config.session?.max_sonar_retries,
|
|
91
|
+
max_reviewer_retries: config.session?.max_reviewer_retries,
|
|
92
|
+
max_tester_retries: config.session?.max_tester_retries,
|
|
93
|
+
max_security_retries: config.session?.max_security_retries
|
|
94
|
+
},
|
|
95
|
+
prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
|
|
96
|
+
git: config.git
|
|
97
|
+
};
|
|
108
98
|
|
|
109
|
-
|
|
110
|
-
|
|
99
|
+
emitProgress(
|
|
100
|
+
emitter,
|
|
101
|
+
makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
|
|
102
|
+
message: "Dry-run complete — no changes made",
|
|
103
|
+
detail: summary
|
|
104
|
+
})
|
|
105
|
+
);
|
|
111
106
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
107
|
+
return summary;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function createBudgetManager({ config, emitter, eventBase }) {
|
|
116
111
|
const budgetTracker = new BudgetTracker({ pricing: config?.budget?.pricing });
|
|
117
112
|
const budgetLimit = Number(config?.max_budget_usd);
|
|
118
113
|
const hasBudgetLimit = Number.isFinite(budgetLimit) && budgetLimit >= 0;
|
|
119
114
|
const warnThresholdPct = Number(config?.budget?.warn_threshold_pct ?? 80);
|
|
115
|
+
let stageCounter = 0;
|
|
120
116
|
|
|
121
117
|
function budgetSummary() {
|
|
122
118
|
const s = budgetTracker.summary();
|
|
@@ -124,7 +120,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
124
120
|
return s;
|
|
125
121
|
}
|
|
126
122
|
|
|
127
|
-
let stageCounter = 0;
|
|
128
123
|
function trackBudget({ role, provider, model, result, duration_ms }) {
|
|
129
124
|
const metrics = extractUsageMetrics(result, model);
|
|
130
125
|
budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
|
|
@@ -132,7 +127,8 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
132
127
|
if (!hasBudgetLimit) return;
|
|
133
128
|
const totalCost = budgetTracker.total().cost_usd;
|
|
134
129
|
const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
|
|
135
|
-
const
|
|
130
|
+
const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
|
|
131
|
+
const status = totalCost > budgetLimit ? "fail" : warnOrOk;
|
|
136
132
|
emitProgress(
|
|
137
133
|
emitter,
|
|
138
134
|
makeEvent("budget:update", { ...eventBase, stage: role }, {
|
|
@@ -149,6 +145,10 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
149
145
|
);
|
|
150
146
|
}
|
|
151
147
|
|
|
148
|
+
return { budgetTracker, budgetLimit, budgetSummary, trackBudget };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
|
|
152
152
|
const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
|
|
153
153
|
const sessionInit = {
|
|
154
154
|
task,
|
|
@@ -168,146 +168,124 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
168
168
|
};
|
|
169
169
|
if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
|
|
170
170
|
if (pgProject) sessionInit.pg_project_id = pgProject;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
eventBase.sessionId = session.id;
|
|
171
|
+
return createSession(sessionInit);
|
|
172
|
+
}
|
|
174
173
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
|
|
174
|
+
async function markPgCardInProgress({ pgTaskId, pgProject, config, logger }) {
|
|
175
|
+
if (!pgTaskId || !pgProject || config.planning_game?.enabled === false) {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const { fetchCard, updateCard } = await import("./planning-game/client.js");
|
|
180
|
+
const pgCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId });
|
|
181
|
+
if (pgCard && pgCard.status !== "In Progress") {
|
|
182
|
+
await updateCard({
|
|
183
|
+
projectId: pgProject,
|
|
184
|
+
cardId: pgTaskId,
|
|
185
|
+
firebaseId: pgCard.firebaseId,
|
|
186
|
+
updates: {
|
|
187
|
+
status: "In Progress",
|
|
188
|
+
startDate: new Date().toISOString(),
|
|
189
|
+
developer: "dev_016",
|
|
190
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
logger.info(`Planning Game: ${pgTaskId} → In Progress`);
|
|
197
194
|
}
|
|
195
|
+
return pgCard;
|
|
196
|
+
} catch (err) {
|
|
197
|
+
logger.warn(`Planning Game: could not update ${pgTaskId}: ${err.message}`);
|
|
198
|
+
return null;
|
|
198
199
|
}
|
|
199
|
-
|
|
200
|
+
}
|
|
200
201
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
maxIterations: config.max_iterations
|
|
210
|
-
}
|
|
211
|
-
})
|
|
212
|
-
);
|
|
202
|
+
function applyTriageOverrides(pipelineFlags, roleOverrides) {
|
|
203
|
+
const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled"];
|
|
204
|
+
for (const key of keys) {
|
|
205
|
+
if (roleOverrides[key] !== undefined) {
|
|
206
|
+
pipelineFlags[key] = roleOverrides[key];
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
213
210
|
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
211
|
+
async function handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger }) {
|
|
212
|
+
const shouldDecompose = triageResult.stageResult?.shouldDecompose
|
|
213
|
+
&& triageResult.stageResult.subtasks?.length > 1
|
|
214
|
+
&& pgTaskId
|
|
215
|
+
&& pgProject
|
|
216
|
+
&& config.planning_game?.enabled !== false
|
|
217
|
+
&& askQuestion;
|
|
218
|
+
|
|
219
|
+
if (!shouldDecompose) return;
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./planning-game/decomposition.js");
|
|
223
|
+
const { createCard, relateCards, fetchCard } = await import("./planning-game/client.js");
|
|
224
|
+
|
|
225
|
+
const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
|
|
226
|
+
const answer = await askQuestion(question);
|
|
227
|
+
|
|
228
|
+
if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
|
|
229
|
+
const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
|
|
230
|
+
const createdSubtasks = await createDecompositionSubtasks({
|
|
231
|
+
client: { createCard, relateCards },
|
|
232
|
+
projectId: pgProject,
|
|
233
|
+
parentCardId: pgTaskId,
|
|
234
|
+
parentFirebaseId: parentCard?.firebaseId || null,
|
|
235
|
+
subtasks: triageResult.stageResult.subtasks,
|
|
236
|
+
epic: parentCard?.epic || null,
|
|
237
|
+
sprint: parentCard?.sprint || null,
|
|
238
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
239
|
+
});
|
|
217
240
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (discoverEnabled) {
|
|
221
|
-
const discoverResult = await runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
222
|
-
stageResults.discover = discoverResult.stageResult;
|
|
223
|
-
}
|
|
241
|
+
stageResults.triage.pgSubtasks = createdSubtasks;
|
|
242
|
+
logger.info(`Planning Game: created ${createdSubtasks.length} subtasks from decomposition`);
|
|
224
243
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (triageResult.roleOverrides.securityEnabled !== undefined) securityEnabled = triageResult.roleOverrides.securityEnabled;
|
|
233
|
-
stageResults.triage = triageResult.stageResult;
|
|
234
|
-
|
|
235
|
-
// --- PG decomposition: offer to create subtasks in Planning Game ---
|
|
236
|
-
const pgDecompose = triageResult.stageResult?.shouldDecompose
|
|
237
|
-
&& triageResult.stageResult.subtasks?.length > 1
|
|
238
|
-
&& pgTaskId
|
|
239
|
-
&& pgProject
|
|
240
|
-
&& config.planning_game?.enabled !== false
|
|
241
|
-
&& askQuestion;
|
|
242
|
-
|
|
243
|
-
if (pgDecompose) {
|
|
244
|
-
try {
|
|
245
|
-
const { buildDecompositionQuestion, createDecompositionSubtasks } = await import("./planning-game/decomposition.js");
|
|
246
|
-
const { createCard, relateCards, fetchCard } = await import("./planning-game/client.js");
|
|
247
|
-
|
|
248
|
-
const question = buildDecompositionQuestion(triageResult.stageResult.subtasks, pgTaskId);
|
|
249
|
-
const answer = await askQuestion(question);
|
|
250
|
-
|
|
251
|
-
if (answer && (answer.trim().toLowerCase() === "yes" || answer.trim().toLowerCase() === "sí" || answer.trim().toLowerCase() === "si")) {
|
|
252
|
-
const parentCard = await fetchCard({ projectId: pgProject, cardId: pgTaskId }).catch(() => null);
|
|
253
|
-
const createdSubtasks = await createDecompositionSubtasks({
|
|
254
|
-
client: { createCard, relateCards },
|
|
255
|
-
projectId: pgProject,
|
|
256
|
-
parentCardId: pgTaskId,
|
|
257
|
-
parentFirebaseId: parentCard?.firebaseId || null,
|
|
258
|
-
subtasks: triageResult.stageResult.subtasks,
|
|
259
|
-
epic: parentCard?.epic || null,
|
|
260
|
-
sprint: parentCard?.sprint || null,
|
|
261
|
-
codeveloper: config.planning_game?.codeveloper || null
|
|
262
|
-
});
|
|
244
|
+
emitProgress(
|
|
245
|
+
emitter,
|
|
246
|
+
makeEvent("pg:decompose", { ...eventBase, stage: "triage" }, {
|
|
247
|
+
message: `Created ${createdSubtasks.length} subtasks in Planning Game`,
|
|
248
|
+
detail: { subtasks: createdSubtasks.map((s) => ({ cardId: s.cardId, title: s.title })) }
|
|
249
|
+
})
|
|
250
|
+
);
|
|
263
251
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
makeEvent("pg:decompose", { ...eventBase, stage: "triage" }, {
|
|
270
|
-
message: `Created ${createdSubtasks.length} subtasks in Planning Game`,
|
|
271
|
-
detail: { subtasks: createdSubtasks.map((s) => ({ cardId: s.cardId, title: s.title })) }
|
|
272
|
-
})
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
await addCheckpoint(session, {
|
|
276
|
-
stage: "pg-decompose",
|
|
277
|
-
subtasksCreated: createdSubtasks.length,
|
|
278
|
-
cardIds: createdSubtasks.map((s) => s.cardId)
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
} catch (err) {
|
|
282
|
-
logger.warn(`Planning Game decomposition failed: ${err.message}`);
|
|
283
|
-
}
|
|
252
|
+
await addCheckpoint(session, {
|
|
253
|
+
stage: "pg-decompose",
|
|
254
|
+
subtasksCreated: createdSubtasks.length,
|
|
255
|
+
cardIds: createdSubtasks.map((s) => s.cardId)
|
|
256
|
+
});
|
|
284
257
|
}
|
|
258
|
+
} catch (err) {
|
|
259
|
+
logger.warn(`Planning Game decomposition failed: ${err.message}`);
|
|
285
260
|
}
|
|
261
|
+
}
|
|
286
262
|
|
|
287
|
-
|
|
288
|
-
if (flags.
|
|
289
|
-
if (flags.
|
|
290
|
-
if (flags.
|
|
291
|
-
if (flags.
|
|
292
|
-
if (flags.
|
|
263
|
+
function applyFlagOverrides(pipelineFlags, flags) {
|
|
264
|
+
if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
|
|
265
|
+
if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
|
|
266
|
+
if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
|
|
267
|
+
if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
|
|
268
|
+
if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
|
|
269
|
+
if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
|
|
270
|
+
if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
|
|
271
|
+
}
|
|
293
272
|
|
|
294
|
-
|
|
295
|
-
// Priority: explicit flag > config > triage classification > default (sw)
|
|
273
|
+
function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
|
|
296
274
|
const resolvedPolicies = applyPolicies({
|
|
297
275
|
taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || null,
|
|
298
276
|
policies: config.policies,
|
|
299
277
|
});
|
|
300
278
|
session.resolved_policies = resolvedPolicies;
|
|
301
279
|
|
|
302
|
-
|
|
280
|
+
let updatedConfig = config;
|
|
303
281
|
if (!resolvedPolicies.tdd) {
|
|
304
|
-
|
|
282
|
+
updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
|
|
305
283
|
}
|
|
306
284
|
if (!resolvedPolicies.sonar) {
|
|
307
|
-
|
|
285
|
+
updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
|
|
308
286
|
}
|
|
309
287
|
if (!resolvedPolicies.reviewer) {
|
|
310
|
-
reviewerEnabled = false;
|
|
288
|
+
pipelineFlags.reviewerEnabled = false;
|
|
311
289
|
}
|
|
312
290
|
|
|
313
291
|
emitProgress(
|
|
@@ -318,622 +296,579 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
318
296
|
})
|
|
319
297
|
);
|
|
320
298
|
|
|
321
|
-
|
|
299
|
+
return updatedConfig;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
|
|
322
303
|
let researchContext = null;
|
|
323
|
-
|
|
304
|
+
let plannedTask = task;
|
|
305
|
+
|
|
306
|
+
if (pipelineFlags.researcherEnabled) {
|
|
324
307
|
const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
325
308
|
researchContext = researcherResult.researchContext;
|
|
326
309
|
stageResults.researcher = researcherResult.stageResult;
|
|
327
310
|
}
|
|
328
311
|
|
|
329
|
-
// ---
|
|
330
|
-
let
|
|
312
|
+
// --- Architect (between researcher and planner) ---
|
|
313
|
+
let architectContext = null;
|
|
314
|
+
if (pipelineFlags.architectEnabled) {
|
|
315
|
+
const architectResult = await runArchitectStage({
|
|
316
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
317
|
+
researchContext,
|
|
318
|
+
discoverResult: stageResults.discover || null,
|
|
319
|
+
triageLevel: stageResults.triage?.level || null,
|
|
320
|
+
askQuestion
|
|
321
|
+
});
|
|
322
|
+
architectContext = architectResult.architectContext;
|
|
323
|
+
stageResults.architect = architectResult.stageResult;
|
|
324
|
+
}
|
|
325
|
+
|
|
331
326
|
const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
|
|
332
|
-
if (plannerEnabled) {
|
|
333
|
-
const
|
|
327
|
+
if (pipelineFlags.plannerEnabled) {
|
|
328
|
+
const plannerRole = resolveRole(config, "planner");
|
|
329
|
+
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
|
|
334
330
|
plannedTask = plannerResult.plannedTask;
|
|
335
331
|
stageResults.planner = plannerResult.stageResult;
|
|
336
332
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
333
|
+
await tryBecariaComment({
|
|
334
|
+
config, session, logger,
|
|
335
|
+
agent: "Planner",
|
|
336
|
+
body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return { plannedTask };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async function tryBecariaComment({ config, session, logger, agent, body }) {
|
|
344
|
+
if (!config.becaria?.enabled || !session.becaria_pr_number) return;
|
|
345
|
+
try {
|
|
346
|
+
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
347
|
+
const { detectRepo } = await import("./becaria/repo.js");
|
|
348
|
+
const repo = await detectRepo();
|
|
349
|
+
if (repo) {
|
|
350
|
+
await dispatchComment({
|
|
351
|
+
repo, prNumber: session.becaria_pr_number, agent,
|
|
352
|
+
body, becariaConfig: config.becaria
|
|
353
|
+
});
|
|
352
354
|
}
|
|
355
|
+
} catch { /* non-blocking */ }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary }) {
|
|
359
|
+
if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
|
|
360
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt };
|
|
353
361
|
}
|
|
354
362
|
|
|
355
|
-
const
|
|
363
|
+
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
364
|
+
const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
|
|
365
|
+
const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
|
|
366
|
+
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
367
|
+
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. What would you like to do?`;
|
|
356
368
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
369
|
+
emitProgress(
|
|
370
|
+
emitter,
|
|
371
|
+
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
372
|
+
message: `Interactive checkpoint at ${elapsedStr} min`,
|
|
373
|
+
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted }
|
|
374
|
+
})
|
|
375
|
+
);
|
|
360
376
|
|
|
361
|
-
const
|
|
362
|
-
|
|
363
|
-
|
|
377
|
+
const answer = await askQuestion(
|
|
378
|
+
`${checkpointMsg}\n\nOptions:\n1. Continue 5 more minutes\n2. Continue until done (no more checkpoints)\n3. Continue for N minutes (reply with the number)\n4. Stop now`
|
|
379
|
+
);
|
|
364
380
|
|
|
365
|
-
|
|
366
|
-
const elapsedMinutes = (Date.now() - startedAt) / 60000;
|
|
381
|
+
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
367
382
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
371
|
-
const iterInfo = `${i - 1}/${config.max_iterations} iterations completed`;
|
|
372
|
-
const budgetInfo = budgetTracker.total().cost_usd > 0 ? ` | Budget: $${budgetTracker.total().cost_usd.toFixed(2)}` : "";
|
|
373
|
-
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
374
|
-
const checkpointMsg = `Checkpoint — ${elapsedStr} min elapsed | ${iterInfo}${budgetInfo} | Stages completed: ${stagesCompleted}. What would you like to do?`;
|
|
383
|
+
const trimmedAnswer = (answer || "").trim();
|
|
384
|
+
const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
|
|
375
385
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
386
|
+
if (isExplicitStop) {
|
|
387
|
+
await markSessionStatus(session, "stopped");
|
|
388
|
+
emitProgress(
|
|
389
|
+
emitter,
|
|
390
|
+
makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
|
|
391
|
+
status: "stopped",
|
|
392
|
+
message: "Session stopped by user at checkpoint",
|
|
393
|
+
detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
|
|
394
|
+
})
|
|
395
|
+
);
|
|
396
|
+
return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
|
|
397
|
+
}
|
|
383
398
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
);
|
|
399
|
+
return parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
|
|
400
|
+
}
|
|
387
401
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
);
|
|
406
|
-
return { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) };
|
|
407
|
-
}
|
|
402
|
+
function parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config }) {
|
|
403
|
+
if (!trimmedAnswer) {
|
|
404
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
405
|
+
}
|
|
406
|
+
if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
|
|
407
|
+
return { action: "continue_loop", checkpointDisabled: true, lastCheckpointAt: Date.now() };
|
|
408
|
+
}
|
|
409
|
+
if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
|
|
410
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
411
|
+
}
|
|
412
|
+
const customMinutes = Number.parseInt(trimmedAnswer.replaceAll(/\D/g, ""), 10);
|
|
413
|
+
if (customMinutes > 0) {
|
|
414
|
+
config.session.checkpoint_interval_minutes = customMinutes;
|
|
415
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
416
|
+
}
|
|
417
|
+
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
418
|
+
}
|
|
408
419
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
lastCheckpointAt = Date.now();
|
|
412
|
-
} else if (trimmedAnswer === "2" || trimmedAnswer.toLowerCase().startsWith("continue until")) {
|
|
413
|
-
checkpointDisabled = true;
|
|
414
|
-
} else if (trimmedAnswer === "1" || trimmedAnswer.toLowerCase().includes("5 m")) {
|
|
415
|
-
lastCheckpointAt = Date.now();
|
|
416
|
-
} else {
|
|
417
|
-
const customMinutes = parseInt(trimmedAnswer.replace(/\D/g, ""), 10);
|
|
418
|
-
if (customMinutes > 0) {
|
|
419
|
-
lastCheckpointAt = Date.now();
|
|
420
|
-
config.session.checkpoint_interval_minutes = customMinutes;
|
|
421
|
-
} else {
|
|
422
|
-
lastCheckpointAt = Date.now();
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
420
|
+
async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
|
|
421
|
+
if (askQuestion || elapsedMinutes <= config.session.max_total_minutes) return;
|
|
426
422
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
throw new Error("Session timed out");
|
|
439
|
-
}
|
|
423
|
+
await markSessionStatus(session, "failed");
|
|
424
|
+
emitProgress(
|
|
425
|
+
emitter,
|
|
426
|
+
makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
|
|
427
|
+
status: "fail",
|
|
428
|
+
message: "Session timed out",
|
|
429
|
+
detail: { approved: false, reason: "timeout", budget: budgetSummary() }
|
|
430
|
+
})
|
|
431
|
+
);
|
|
432
|
+
throw new Error("Session timed out");
|
|
433
|
+
}
|
|
440
434
|
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
const totalCost = budgetTracker.total().cost_usd;
|
|
444
|
-
const message = `Budget exceeded: $${totalCost.toFixed(2)} > $${budgetLimit.toFixed(2)}`;
|
|
445
|
-
emitProgress(
|
|
446
|
-
emitter,
|
|
447
|
-
makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
|
|
448
|
-
status: "fail",
|
|
449
|
-
message,
|
|
450
|
-
detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
|
|
451
|
-
})
|
|
452
|
-
);
|
|
453
|
-
throw new Error(message);
|
|
454
|
-
}
|
|
435
|
+
async function checkBudgetExceeded({ budgetTracker, config, session, emitter, eventBase, i, budgetLimit, budgetSummary }) {
|
|
436
|
+
if (!budgetTracker.isOverBudget(config?.max_budget_usd)) return;
|
|
455
437
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
438
|
+
await markSessionStatus(session, "failed");
|
|
439
|
+
const totalCost = budgetTracker.total().cost_usd;
|
|
440
|
+
const message = `Budget exceeded: $${totalCost.toFixed(2)} > $${budgetLimit.toFixed(2)}`;
|
|
441
|
+
emitProgress(
|
|
442
|
+
emitter,
|
|
443
|
+
makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
|
|
444
|
+
status: "fail",
|
|
445
|
+
message,
|
|
446
|
+
detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
|
|
447
|
+
})
|
|
448
|
+
);
|
|
449
|
+
throw new Error(message);
|
|
450
|
+
}
|
|
460
451
|
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
detail: { iteration: i, maxIterations: config.max_iterations }
|
|
466
|
-
})
|
|
467
|
-
);
|
|
452
|
+
async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger }) {
|
|
453
|
+
if (!stageResult?.action || stageResult.action !== "standby") {
|
|
454
|
+
return { handled: false };
|
|
455
|
+
}
|
|
468
456
|
|
|
469
|
-
|
|
457
|
+
const standbyRetries = session.standby_retry_count || 0;
|
|
458
|
+
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
459
|
+
await pauseSession(session, {
|
|
460
|
+
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${stageResult.standbyInfo.agent}`,
|
|
461
|
+
context: { iteration: i, stage, reason: "standby_exhausted" }
|
|
462
|
+
});
|
|
463
|
+
emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
|
|
464
|
+
status: "paused",
|
|
465
|
+
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
466
|
+
detail: { agent: stageResult.standbyInfo.agent, sessionId: session.id }
|
|
467
|
+
}));
|
|
468
|
+
return {
|
|
469
|
+
handled: true,
|
|
470
|
+
action: "return",
|
|
471
|
+
result: { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" }
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
session.standby_retry_count = standbyRetries + 1;
|
|
475
|
+
await saveSession(session);
|
|
476
|
+
await waitForCooldown({ ...stageResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
477
|
+
return { handled: true, action: "retry" };
|
|
478
|
+
}
|
|
470
479
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
if (coderResult?.action === "pause") {
|
|
474
|
-
return coderResult.result;
|
|
475
|
-
}
|
|
476
|
-
if (coderResult?.action === "standby") {
|
|
477
|
-
const standbyRetries = session.standby_retry_count || 0;
|
|
478
|
-
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
479
|
-
await pauseSession(session, {
|
|
480
|
-
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${coderResult.standbyInfo.agent}`,
|
|
481
|
-
context: { iteration: i, stage: "coder", reason: "standby_exhausted" }
|
|
482
|
-
});
|
|
483
|
-
emitProgress(emitter, makeEvent("coder:rate_limit", { ...eventBase, stage: "coder" }, {
|
|
484
|
-
status: "paused",
|
|
485
|
-
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
486
|
-
detail: { agent: coderResult.standbyInfo.agent, sessionId: session.id }
|
|
487
|
-
}));
|
|
488
|
-
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
489
|
-
}
|
|
490
|
-
session.standby_retry_count = standbyRetries + 1;
|
|
491
|
-
await saveSession(session);
|
|
492
|
-
await waitForCooldown({ ...coderResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
493
|
-
i -= 1; // Retry the same iteration
|
|
494
|
-
continue;
|
|
495
|
-
}
|
|
480
|
+
async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
|
|
481
|
+
if (!becariaEnabled) return;
|
|
496
482
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
483
|
+
try {
|
|
484
|
+
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
485
|
+
const { detectRepo } = await import("./becaria/repo.js");
|
|
486
|
+
const repo = await detectRepo();
|
|
487
|
+
|
|
488
|
+
if (session.becaria_pr_number) {
|
|
489
|
+
const pushResult = await incrementalPush({ gitCtx, task, logger, session });
|
|
490
|
+
if (pushResult) {
|
|
491
|
+
session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
|
|
492
|
+
await saveSession(session);
|
|
493
|
+
|
|
494
|
+
if (repo) {
|
|
495
|
+
const feedback = session.last_reviewer_feedback || "N/A";
|
|
496
|
+
const commitList = pushResult.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
497
|
+
await dispatchComment({
|
|
498
|
+
repo, prNumber: session.becaria_pr_number, agent: "Coder",
|
|
499
|
+
body: `Issues corregidos:\n${feedback}\n\nCommits:\n${commitList}`,
|
|
500
|
+
becariaConfig: config.becaria
|
|
509
501
|
});
|
|
510
|
-
emitProgress(emitter, makeEvent("refactorer:rate_limit", { ...eventBase, stage: "refactorer" }, {
|
|
511
|
-
status: "paused",
|
|
512
|
-
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
513
|
-
detail: { agent: refResult.standbyInfo.agent, sessionId: session.id }
|
|
514
|
-
}));
|
|
515
|
-
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
516
502
|
}
|
|
517
|
-
session.standby_retry_count = standbyRetries + 1;
|
|
518
|
-
await saveSession(session);
|
|
519
|
-
await waitForCooldown({ ...refResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
520
|
-
i -= 1; // Retry the same iteration
|
|
521
|
-
continue;
|
|
522
503
|
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
504
|
+
} else {
|
|
505
|
+
const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
|
|
506
|
+
if (earlyPr) {
|
|
507
|
+
session.becaria_pr_number = earlyPr.prNumber;
|
|
508
|
+
session.becaria_pr_url = earlyPr.prUrl;
|
|
509
|
+
session.becaria_commits = earlyPr.commits;
|
|
510
|
+
await saveSession(session);
|
|
511
|
+
emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
|
|
512
|
+
message: `Early PR created: #${earlyPr.prNumber}`,
|
|
513
|
+
detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
|
|
514
|
+
}));
|
|
533
515
|
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if (sonarResult.action === "stalled" || sonarResult.action === "pause") {
|
|
542
|
-
return sonarResult.result;
|
|
543
|
-
}
|
|
544
|
-
if (sonarResult.action === "continue") {
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
if (sonarResult.stageResult) {
|
|
548
|
-
stageResults.sonar = sonarResult.stageResult;
|
|
549
|
-
// BecarIA: dispatch sonar comment
|
|
550
|
-
if (becariaEnabled && session.becaria_pr_number) {
|
|
551
|
-
try {
|
|
552
|
-
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
553
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
554
|
-
const repo = await detectRepo();
|
|
555
|
-
if (repo) {
|
|
556
|
-
const s = sonarResult.stageResult;
|
|
557
|
-
await dispatchComment({
|
|
558
|
-
repo, prNumber: session.becaria_pr_number, agent: "Sonar",
|
|
559
|
-
body: `SonarQube scan: ${s.summary || "completed"}`,
|
|
560
|
-
becariaConfig: config.becaria
|
|
561
|
-
});
|
|
562
|
-
}
|
|
563
|
-
} catch { /* non-blocking */ }
|
|
516
|
+
if (repo) {
|
|
517
|
+
const commitList = earlyPr.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
518
|
+
await dispatchComment({
|
|
519
|
+
repo, prNumber: earlyPr.prNumber, agent: "Coder",
|
|
520
|
+
body: `Iteración ${i} completada.\n\nCommits:\n${commitList}`,
|
|
521
|
+
becariaConfig: config.becaria
|
|
522
|
+
});
|
|
564
523
|
}
|
|
565
524
|
}
|
|
566
525
|
}
|
|
526
|
+
} catch (err) {
|
|
527
|
+
logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
567
530
|
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
message: `Early PR created: #${earlyPr.prNumber}`,
|
|
585
|
-
detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
|
|
586
|
-
}));
|
|
587
|
-
|
|
588
|
-
// Post coder comment on new PR
|
|
589
|
-
if (repo) {
|
|
590
|
-
const commitList = earlyPr.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
591
|
-
await dispatchComment({
|
|
592
|
-
repo, prNumber: earlyPr.prNumber, agent: "Coder",
|
|
593
|
-
body: `Iteración ${i} completada.\n\nCommits:\n${commitList}`,
|
|
594
|
-
becariaConfig: config.becaria
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
}
|
|
598
|
-
} else {
|
|
599
|
-
// Subsequent iterations: incremental push + comment
|
|
600
|
-
const pushResult = await incrementalPush({ gitCtx, task, logger, session });
|
|
601
|
-
if (pushResult) {
|
|
602
|
-
session.becaria_commits = [...(session.becaria_commits || []), ...pushResult.commits];
|
|
603
|
-
await saveSession(session);
|
|
604
|
-
|
|
605
|
-
if (repo) {
|
|
606
|
-
const feedback = session.last_reviewer_feedback || "N/A";
|
|
607
|
-
const commitList = pushResult.commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
608
|
-
await dispatchComment({
|
|
609
|
-
repo, prNumber: session.becaria_pr_number, agent: "Coder",
|
|
610
|
-
body: `Issues corregidos:\n${feedback}\n\nCommits:\n${commitList}`,
|
|
611
|
-
becariaConfig: config.becaria
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
} catch (err) {
|
|
617
|
-
logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
|
|
531
|
+
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled }) {
|
|
532
|
+
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
533
|
+
|
|
534
|
+
try {
|
|
535
|
+
const { evaluateRules, buildRulesContext } = await import("./orchestrator/solomon-rules.js");
|
|
536
|
+
const rulesContext = await buildRulesContext({ session, task, iteration: i });
|
|
537
|
+
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
538
|
+
|
|
539
|
+
if (rulesResult.alerts.length > 0) {
|
|
540
|
+
for (const alert of rulesResult.alerts) {
|
|
541
|
+
emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
|
|
542
|
+
status: alert.severity === "critical" ? "fail" : "warn",
|
|
543
|
+
message: alert.message,
|
|
544
|
+
detail: alert.detail
|
|
545
|
+
}));
|
|
546
|
+
logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
|
|
618
547
|
}
|
|
548
|
+
|
|
549
|
+
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
550
|
+
if (pauseResult) return pauseResult;
|
|
619
551
|
}
|
|
620
552
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
const reviewerResult = await runReviewerStage({
|
|
631
|
-
reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
|
|
632
|
-
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
|
|
553
|
+
if (becariaEnabled && session.becaria_pr_number) {
|
|
554
|
+
const alerts = rulesResult.alerts || [];
|
|
555
|
+
const alertMsg = alerts.length > 0
|
|
556
|
+
? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
|
|
557
|
+
: "No anomalies detected";
|
|
558
|
+
await tryBecariaComment({
|
|
559
|
+
config, session, logger,
|
|
560
|
+
agent: "Solomon",
|
|
561
|
+
body: `Supervisor check iteración ${i}: ${alertMsg}`
|
|
633
562
|
});
|
|
634
|
-
if (reviewerResult.action === "pause") {
|
|
635
|
-
return reviewerResult.result;
|
|
636
|
-
}
|
|
637
|
-
if (reviewerResult.action === "standby") {
|
|
638
|
-
const standbyRetries = session.standby_retry_count || 0;
|
|
639
|
-
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
640
|
-
await pauseSession(session, {
|
|
641
|
-
question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${reviewerResult.standbyInfo.agent}`,
|
|
642
|
-
context: { iteration: i, stage: "reviewer", reason: "standby_exhausted" }
|
|
643
|
-
});
|
|
644
|
-
emitProgress(emitter, makeEvent("reviewer:rate_limit", { ...eventBase, stage: "reviewer" }, {
|
|
645
|
-
status: "paused",
|
|
646
|
-
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
647
|
-
detail: { agent: reviewerResult.standbyInfo.agent, sessionId: session.id }
|
|
648
|
-
}));
|
|
649
|
-
return { paused: true, sessionId: session.id, question: `Rate limit standby exhausted after ${standbyRetries} retries`, context: "standby_exhausted" };
|
|
650
|
-
}
|
|
651
|
-
session.standby_retry_count = standbyRetries + 1;
|
|
652
|
-
await saveSession(session);
|
|
653
|
-
await waitForCooldown({ ...reviewerResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
654
|
-
i -= 1; // Retry the same iteration
|
|
655
|
-
continue;
|
|
656
|
-
}
|
|
657
|
-
review = reviewerResult.review;
|
|
658
|
-
if (reviewerResult.stalled) {
|
|
659
|
-
return reviewerResult.stalledResult;
|
|
660
|
-
}
|
|
661
563
|
}
|
|
564
|
+
} catch (err) {
|
|
565
|
+
logger.warn(`Solomon rules evaluation failed: ${err.message}`);
|
|
566
|
+
}
|
|
662
567
|
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
emitProgress(
|
|
666
|
-
emitter,
|
|
667
|
-
makeEvent("iteration:end", { ...eventBase, stage: "iteration" }, {
|
|
668
|
-
message: `Iteration ${i} completed`,
|
|
669
|
-
detail: { duration: iterDuration }
|
|
670
|
-
})
|
|
671
|
-
);
|
|
568
|
+
return { action: "continue" };
|
|
569
|
+
}
|
|
672
570
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
// --- Solomon supervisor: anomaly detection after each iteration ---
|
|
677
|
-
if (config.pipeline?.solomon?.enabled !== false) {
|
|
678
|
-
try {
|
|
679
|
-
const { evaluateRules, buildRulesContext } = await import("./orchestrator/solomon-rules.js");
|
|
680
|
-
const rulesContext = await buildRulesContext({ session, task, iteration: i });
|
|
681
|
-
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
682
|
-
|
|
683
|
-
if (rulesResult.alerts.length > 0) {
|
|
684
|
-
for (const alert of rulesResult.alerts) {
|
|
685
|
-
emitProgress(emitter, makeEvent("solomon:alert", { ...eventBase, stage: "solomon" }, {
|
|
686
|
-
status: alert.severity === "critical" ? "fail" : "warn",
|
|
687
|
-
message: alert.message,
|
|
688
|
-
detail: alert.detail
|
|
689
|
-
}));
|
|
690
|
-
logger.warn(`Solomon alert [${alert.rule}]: ${alert.message}`);
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
if (rulesResult.hasCritical && askQuestion) {
|
|
694
|
-
const alertSummary = rulesResult.alerts
|
|
695
|
-
.filter(a => a.severity === "critical")
|
|
696
|
-
.map(a => a.message)
|
|
697
|
-
.join("\n");
|
|
698
|
-
const answer = await askQuestion(
|
|
699
|
-
`Solomon detected critical issues:\n${alertSummary}\n\nShould I continue, pause, or revert?`,
|
|
700
|
-
{ iteration: i, stage: "solomon" }
|
|
701
|
-
);
|
|
702
|
-
if (!answer || answer.toLowerCase().includes("pause") || answer.toLowerCase().includes("stop")) {
|
|
703
|
-
await pauseSession(session, {
|
|
704
|
-
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
705
|
-
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
706
|
-
});
|
|
707
|
-
return { paused: true, sessionId: session.id, reason: "solomon_alert" };
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
571
|
+
async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i }) {
|
|
572
|
+
if (!rulesResult.hasCritical || !askQuestion) return null;
|
|
711
573
|
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
574
|
+
const alertSummary = rulesResult.alerts
|
|
575
|
+
.filter(a => a.severity === "critical")
|
|
576
|
+
.map(a => a.message)
|
|
577
|
+
.join("\n");
|
|
578
|
+
const answer = await askQuestion(
|
|
579
|
+
`Solomon detected critical issues:\n${alertSummary}\n\nShould I continue, pause, or revert?`,
|
|
580
|
+
{ iteration: i, stage: "solomon" }
|
|
581
|
+
);
|
|
582
|
+
if (!answer || answer.toLowerCase().includes("pause") || answer.toLowerCase().includes("stop")) {
|
|
583
|
+
await pauseSession(session, {
|
|
584
|
+
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
585
|
+
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
586
|
+
});
|
|
587
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, reason: "solomon_alert" } };
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
|
|
593
|
+
if (!becariaEnabled || !session.becaria_pr_number) return;
|
|
594
|
+
|
|
595
|
+
try {
|
|
596
|
+
const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
|
|
597
|
+
const { detectRepo } = await import("./becaria/repo.js");
|
|
598
|
+
const repo = await detectRepo();
|
|
599
|
+
if (!repo) return;
|
|
600
|
+
|
|
601
|
+
const bc = config.becaria;
|
|
602
|
+
if (review.approved) {
|
|
603
|
+
await dispatchReview({
|
|
604
|
+
repo, prNumber: session.becaria_pr_number,
|
|
605
|
+
event: "APPROVE", body: review.summary || "Approved", agent: "Reviewer", becariaConfig: bc
|
|
606
|
+
});
|
|
607
|
+
} else {
|
|
608
|
+
const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
609
|
+
await dispatchReview({
|
|
610
|
+
repo, prNumber: session.becaria_pr_number,
|
|
611
|
+
event: "REQUEST_CHANGES",
|
|
612
|
+
body: blocking || review.summary || "Changes requested",
|
|
613
|
+
agent: "Reviewer", becariaConfig: bc
|
|
614
|
+
});
|
|
734
615
|
}
|
|
735
616
|
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
});
|
|
750
|
-
} else {
|
|
751
|
-
const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
752
|
-
await dispatchReview({
|
|
753
|
-
repo, prNumber: session.becaria_pr_number,
|
|
754
|
-
event: "REQUEST_CHANGES",
|
|
755
|
-
body: blocking || review.summary || "Changes requested",
|
|
756
|
-
agent: "Reviewer", becariaConfig: bc
|
|
757
|
-
});
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
// Detailed comment
|
|
761
|
-
const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
|
|
762
|
-
const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
763
|
-
const suggestions = review.non_blocking_suggestions?.map((s) => `- ${typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`}`).join("\n") || "";
|
|
764
|
-
let reviewBody = `Review iteración ${i}: ${status}`;
|
|
765
|
-
if (blocking) reviewBody += `\n\n**Blocking:**\n${blocking}`;
|
|
766
|
-
if (suggestions) reviewBody += `\n\n**Suggestions:**\n${suggestions}`;
|
|
767
|
-
await dispatchComment({
|
|
768
|
-
repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
|
|
769
|
-
body: reviewBody, becariaConfig: bc
|
|
770
|
-
});
|
|
617
|
+
const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
|
|
618
|
+
const blocking = review.blocking_issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
619
|
+
const suggestions = review.non_blocking_suggestions?.map((s) => {
|
|
620
|
+
const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
|
|
621
|
+
return `- ${detail}`;
|
|
622
|
+
}).join("\n") || "";
|
|
623
|
+
let reviewBody = `Review iteración ${i}: ${status}`;
|
|
624
|
+
if (blocking) reviewBody += `\n\n**Blocking:**\n${blocking}`;
|
|
625
|
+
if (suggestions) reviewBody += `\n\n**Suggestions:**\n${suggestions}`;
|
|
626
|
+
await dispatchComment({
|
|
627
|
+
repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
|
|
628
|
+
body: reviewBody, becariaConfig: bc
|
|
629
|
+
});
|
|
771
630
|
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
631
|
+
logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
|
|
632
|
+
} catch (err) {
|
|
633
|
+
logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, becariaEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
|
|
638
|
+
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
639
|
+
|
|
640
|
+
if (testerEnabled) {
|
|
641
|
+
const testerResult = await runTesterStage({
|
|
642
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
643
|
+
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
644
|
+
});
|
|
645
|
+
if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
|
|
646
|
+
if (testerResult.action === "continue") return { action: "continue" };
|
|
647
|
+
if (testerResult.stageResult) {
|
|
648
|
+
stageResults.tester = testerResult.stageResult;
|
|
649
|
+
await tryBecariaComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
|
|
777
650
|
}
|
|
651
|
+
}
|
|
778
652
|
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
return testerResult.result;
|
|
792
|
-
}
|
|
793
|
-
if (testerResult.action === "continue") {
|
|
794
|
-
continue;
|
|
795
|
-
}
|
|
796
|
-
if (testerResult.stageResult) {
|
|
797
|
-
stageResults.tester = testerResult.stageResult;
|
|
798
|
-
// BecarIA: dispatch tester comment
|
|
799
|
-
if (becariaEnabled && session.becaria_pr_number) {
|
|
800
|
-
try {
|
|
801
|
-
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
802
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
803
|
-
const repo = await detectRepo();
|
|
804
|
-
if (repo) {
|
|
805
|
-
const t = testerResult.stageResult;
|
|
806
|
-
await dispatchComment({
|
|
807
|
-
repo, prNumber: session.becaria_pr_number, agent: "Tester",
|
|
808
|
-
body: `Tests: ${t.summary || "completed"}`,
|
|
809
|
-
becariaConfig: config.becaria
|
|
810
|
-
});
|
|
811
|
-
}
|
|
812
|
-
} catch { /* non-blocking */ }
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
}
|
|
653
|
+
if (securityEnabled) {
|
|
654
|
+
const securityResult = await runSecurityStage({
|
|
655
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
656
|
+
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
657
|
+
});
|
|
658
|
+
if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
|
|
659
|
+
if (securityResult.action === "continue") return { action: "continue" };
|
|
660
|
+
if (securityResult.stageResult) {
|
|
661
|
+
stageResults.security = securityResult.stageResult;
|
|
662
|
+
await tryBecariaComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
|
|
663
|
+
}
|
|
664
|
+
}
|
|
816
665
|
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
820
|
-
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
821
|
-
});
|
|
822
|
-
if (securityResult.action === "pause") {
|
|
823
|
-
return securityResult.result;
|
|
824
|
-
}
|
|
825
|
-
if (securityResult.action === "continue") {
|
|
826
|
-
continue;
|
|
827
|
-
}
|
|
828
|
-
if (securityResult.stageResult) {
|
|
829
|
-
stageResults.security = securityResult.stageResult;
|
|
830
|
-
// BecarIA: dispatch security comment
|
|
831
|
-
if (becariaEnabled && session.becaria_pr_number) {
|
|
832
|
-
try {
|
|
833
|
-
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
834
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
835
|
-
const repo = await detectRepo();
|
|
836
|
-
if (repo) {
|
|
837
|
-
const s = securityResult.stageResult;
|
|
838
|
-
await dispatchComment({
|
|
839
|
-
repo, prNumber: session.becaria_pr_number, agent: "Security",
|
|
840
|
-
body: `Security scan: ${s.summary || "completed"}`,
|
|
841
|
-
becariaConfig: config.becaria
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
} catch { /* non-blocking */ }
|
|
845
|
-
}
|
|
846
|
-
}
|
|
847
|
-
}
|
|
666
|
+
return { action: "proceed" };
|
|
667
|
+
}
|
|
848
668
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
// --- Planning Game: mark card as To Validate ---
|
|
858
|
-
if (pgCard && pgProject) {
|
|
859
|
-
try {
|
|
860
|
-
const { updateCard } = await import("./planning-game/client.js");
|
|
861
|
-
const { buildCompletionUpdates } = await import("./planning-game/adapter.js");
|
|
862
|
-
const pgUpdates = buildCompletionUpdates({
|
|
863
|
-
approved: true,
|
|
864
|
-
commits: gitResult?.commits || [],
|
|
865
|
-
startDate: session.pg_card?.startDate || session.created_at,
|
|
866
|
-
codeveloper: config.planning_game?.codeveloper || null
|
|
867
|
-
});
|
|
868
|
-
await updateCard({
|
|
869
|
-
projectId: pgProject,
|
|
870
|
-
cardId: session.pg_task_id,
|
|
871
|
-
firebaseId: pgCard.firebaseId,
|
|
872
|
-
updates: pgUpdates
|
|
873
|
-
});
|
|
874
|
-
logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
|
|
875
|
-
} catch (err) {
|
|
876
|
-
logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
|
|
877
|
-
}
|
|
878
|
-
}
|
|
669
|
+
async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i }) {
|
|
670
|
+
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
|
|
671
|
+
if (stageResults.planner?.ok) {
|
|
672
|
+
stageResults.planner.completedSteps = [...(stageResults.planner.steps ?? [])];
|
|
673
|
+
}
|
|
674
|
+
session.budget = budgetSummary();
|
|
675
|
+
await markSessionStatus(session, "approved");
|
|
879
676
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
)
|
|
890
|
-
|
|
677
|
+
await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
|
|
678
|
+
|
|
679
|
+
const deferredIssues = session.deferred_issues || [];
|
|
680
|
+
emitProgress(
|
|
681
|
+
emitter,
|
|
682
|
+
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
683
|
+
message: deferredIssues.length > 0
|
|
684
|
+
? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
|
|
685
|
+
: "Session approved",
|
|
686
|
+
detail: { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues }
|
|
687
|
+
})
|
|
688
|
+
);
|
|
689
|
+
return { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
async function markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger }) {
|
|
693
|
+
if (!pgCard || !pgProject) return;
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
const { updateCard } = await import("./planning-game/client.js");
|
|
697
|
+
const { buildCompletionUpdates } = await import("./planning-game/adapter.js");
|
|
698
|
+
const pgUpdates = buildCompletionUpdates({
|
|
699
|
+
approved: true,
|
|
700
|
+
commits: gitResult?.commits || [],
|
|
701
|
+
startDate: session.pg_card?.startDate || session.created_at,
|
|
702
|
+
codeveloper: config.planning_game?.codeveloper || null
|
|
703
|
+
});
|
|
704
|
+
await updateCard({
|
|
705
|
+
projectId: pgProject,
|
|
706
|
+
cardId: session.pg_task_id,
|
|
707
|
+
firebaseId: pgCard.firebaseId,
|
|
708
|
+
updates: pgUpdates
|
|
709
|
+
});
|
|
710
|
+
logger.info(`Planning Game: ${session.pg_task_id} → To Validate`);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
logger.warn(`Planning Game: could not update ${session.pg_task_id} on completion: ${err.message}`);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
|
|
717
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
718
|
+
.map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
|
|
719
|
+
.join("\n");
|
|
720
|
+
session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
|
|
721
|
+
await saveSession(session);
|
|
722
|
+
|
|
723
|
+
const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
|
|
724
|
+
if (session.reviewer_retry_count < maxReviewerRetries) {
|
|
725
|
+
return { action: "continue" };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
emitProgress(
|
|
729
|
+
emitter,
|
|
730
|
+
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
731
|
+
message: `Reviewer sub-loop limit reached (${session.reviewer_retry_count}/${maxReviewerRetries})`,
|
|
732
|
+
detail: { subloop: "reviewer", retryCount: session.reviewer_retry_count, limit: maxReviewerRetries }
|
|
733
|
+
})
|
|
734
|
+
);
|
|
735
|
+
|
|
736
|
+
const solomonResult = await invokeSolomon({
|
|
737
|
+
config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration: i,
|
|
738
|
+
conflict: {
|
|
739
|
+
stage: "reviewer",
|
|
740
|
+
task,
|
|
741
|
+
iterationCount: session.reviewer_retry_count,
|
|
742
|
+
maxIterations: maxReviewerRetries,
|
|
743
|
+
history: [{ agent: "reviewer", feedback: session.last_reviewer_feedback }]
|
|
891
744
|
}
|
|
745
|
+
});
|
|
892
746
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
747
|
+
if (solomonResult.action === "pause") {
|
|
748
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_fail_fast" } };
|
|
749
|
+
}
|
|
750
|
+
if (solomonResult.action === "continue") {
|
|
751
|
+
if (solomonResult.humanGuidance) {
|
|
752
|
+
session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
|
|
753
|
+
}
|
|
754
|
+
session.reviewer_retry_count = 0;
|
|
897
755
|
await saveSession(session);
|
|
756
|
+
return { action: "continue" };
|
|
757
|
+
}
|
|
758
|
+
if (solomonResult.action === "subtask") {
|
|
759
|
+
return { action: "return", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
|
|
760
|
+
}
|
|
898
761
|
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
emitProgress(
|
|
902
|
-
emitter,
|
|
903
|
-
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
904
|
-
message: `Reviewer sub-loop limit reached (${session.reviewer_retry_count}/${maxReviewerRetries})`,
|
|
905
|
-
detail: { subloop: "reviewer", retryCount: session.reviewer_retry_count, limit: maxReviewerRetries }
|
|
906
|
-
})
|
|
907
|
-
);
|
|
762
|
+
return { action: "continue" };
|
|
763
|
+
}
|
|
908
764
|
|
|
909
|
-
const solomonResult = await invokeSolomon({
|
|
910
|
-
config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration: i,
|
|
911
|
-
conflict: {
|
|
912
|
-
stage: "reviewer",
|
|
913
|
-
task,
|
|
914
|
-
iterationCount: session.reviewer_retry_count,
|
|
915
|
-
maxIterations: maxReviewerRetries,
|
|
916
|
-
history: [{ agent: "reviewer", feedback: session.last_reviewer_feedback }]
|
|
917
|
-
}
|
|
918
|
-
});
|
|
919
765
|
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
766
|
+
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults }) {
|
|
767
|
+
// --- Discover (pre-triage, opt-in) ---
|
|
768
|
+
if (flags.enableDiscover !== undefined) pipelineFlags.discoverEnabled = Boolean(flags.enableDiscover);
|
|
769
|
+
if (pipelineFlags.discoverEnabled) {
|
|
770
|
+
const discoverResult = await runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
771
|
+
stageResults.discover = discoverResult.stageResult;
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
// --- Triage (always on) ---
|
|
775
|
+
const triageResult = await runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
776
|
+
applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
|
|
777
|
+
stageResults.triage = triageResult.stageResult;
|
|
778
|
+
|
|
779
|
+
await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
|
|
780
|
+
|
|
781
|
+
applyFlagOverrides(pipelineFlags, flags);
|
|
782
|
+
const updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
783
|
+
|
|
784
|
+
// --- Researcher → Planner ---
|
|
785
|
+
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
786
|
+
|
|
787
|
+
return { plannedTask, updatedConfig };
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
|
|
791
|
+
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
792
|
+
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
793
|
+
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger });
|
|
794
|
+
if (coderStandby.handled) {
|
|
795
|
+
return coderStandby.action === "return"
|
|
796
|
+
? { action: "return", result: coderStandby.result }
|
|
797
|
+
: { action: "retry" };
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (pipelineFlags.refactorerEnabled) {
|
|
801
|
+
const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
802
|
+
if (refResult?.action === "pause") return { action: "return", result: refResult.result };
|
|
803
|
+
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger });
|
|
804
|
+
if (refStandby.handled) {
|
|
805
|
+
return refStandby.action === "return"
|
|
806
|
+
? { action: "return", result: refStandby.result }
|
|
807
|
+
: { action: "retry" };
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return { action: "ok" };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults }) {
|
|
815
|
+
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion });
|
|
816
|
+
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
817
|
+
if (tddResult.action === "continue") return { action: "continue" };
|
|
818
|
+
|
|
819
|
+
if (config.sonarqube.enabled) {
|
|
820
|
+
const sonarResult = await runSonarStage({
|
|
821
|
+
config, logger, emitter, eventBase, session, trackBudget, iteration: i,
|
|
822
|
+
repeatDetector, budgetSummary, sonarState, askQuestion, task
|
|
823
|
+
});
|
|
824
|
+
if (sonarResult.action === "stalled" || sonarResult.action === "pause") return { action: "return", result: sonarResult.result };
|
|
825
|
+
if (sonarResult.action === "continue") return { action: "continue" };
|
|
826
|
+
if (sonarResult.stageResult) {
|
|
827
|
+
stageResults.sonar = sonarResult.stageResult;
|
|
828
|
+
await tryBecariaComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
|
|
934
829
|
}
|
|
935
830
|
}
|
|
936
831
|
|
|
832
|
+
return { action: "ok" };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
|
|
836
|
+
if (!pipelineFlags.reviewerEnabled) {
|
|
837
|
+
return {
|
|
838
|
+
action: "ok",
|
|
839
|
+
review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Reviewer disabled by pipeline", confidence: 1 }
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const reviewerResult = await runReviewerStage({
|
|
844
|
+
reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
|
|
845
|
+
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
|
|
846
|
+
});
|
|
847
|
+
if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
|
|
848
|
+
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger });
|
|
849
|
+
if (revStandby.handled) {
|
|
850
|
+
return revStandby.action === "return"
|
|
851
|
+
? { action: "return", result: revStandby.result }
|
|
852
|
+
: { action: "retry" };
|
|
853
|
+
}
|
|
854
|
+
if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
|
|
855
|
+
return { action: "ok", review: reviewerResult.review };
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review }) {
|
|
859
|
+
session.reviewer_retry_count = 0;
|
|
860
|
+
const postLoopResult = await handlePostLoopStages({
|
|
861
|
+
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
862
|
+
becariaEnabled: Boolean(config.becaria?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
|
|
863
|
+
});
|
|
864
|
+
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
865
|
+
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
866
|
+
|
|
867
|
+
const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i });
|
|
868
|
+
return { action: "return", result };
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults }) {
|
|
937
872
|
session.budget = budgetSummary();
|
|
938
873
|
await markSessionStatus(session, "failed");
|
|
939
874
|
emitProgress(
|
|
@@ -947,6 +882,143 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
947
882
|
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
948
883
|
}
|
|
949
884
|
|
|
885
|
+
async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
886
|
+
const coderRole = resolveRole(config, "coder");
|
|
887
|
+
const reviewerRole = resolveRole(config, "reviewer");
|
|
888
|
+
const refactorerRole = resolveRole(config, "refactorer");
|
|
889
|
+
const pipelineFlags = resolvePipelineFlags(config);
|
|
890
|
+
const repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
|
|
891
|
+
const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent });
|
|
892
|
+
const startedAt = Date.now();
|
|
893
|
+
const eventBase = { sessionId: null, iteration: 0, stage: null, startedAt };
|
|
894
|
+
const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({ config, emitter, eventBase });
|
|
895
|
+
|
|
896
|
+
const session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
897
|
+
eventBase.sessionId = session.id;
|
|
898
|
+
|
|
899
|
+
const pgCard = await markPgCardInProgress({ pgTaskId, pgProject, config, logger });
|
|
900
|
+
session.pg_card = pgCard || null;
|
|
901
|
+
|
|
902
|
+
emitProgress(
|
|
903
|
+
emitter,
|
|
904
|
+
makeEvent("session:start", eventBase, {
|
|
905
|
+
message: "Session started",
|
|
906
|
+
detail: { task, coder: coderRole.provider, reviewer: reviewerRole.provider, maxIterations: config.max_iterations }
|
|
907
|
+
})
|
|
908
|
+
);
|
|
909
|
+
|
|
910
|
+
const stageResults = {};
|
|
911
|
+
const sonarState = { issuesInitial: null, issuesFinal: null };
|
|
912
|
+
|
|
913
|
+
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults });
|
|
914
|
+
const { plannedTask } = preLoopResult;
|
|
915
|
+
const updatedConfig = preLoopResult.updatedConfig;
|
|
916
|
+
|
|
917
|
+
const gitCtx = await prepareGitAutomation({ config: updatedConfig, task, logger, session });
|
|
918
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
919
|
+
const { rules: reviewRules } = await resolveReviewProfile({ mode: updatedConfig.review_mode, projectDir });
|
|
920
|
+
await coderRoleInstance.init();
|
|
921
|
+
|
|
922
|
+
return {
|
|
923
|
+
coderRole, reviewerRole, refactorerRole, pipelineFlags, repeatDetector, coderRoleInstance,
|
|
924
|
+
startedAt, eventBase, budgetTracker, budgetLimit, budgetSummary, trackBudget,
|
|
925
|
+
session, pgCard, stageResults, sonarState, plannedTask, config: updatedConfig,
|
|
926
|
+
gitCtx, reviewRules
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
async function runSingleIteration(ctx) {
|
|
931
|
+
const {
|
|
932
|
+
coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase,
|
|
933
|
+
session, plannedTask, trackBudget, i, reviewerRole, reviewRules, task, repeatDetector,
|
|
934
|
+
budgetSummary, askQuestion, sonarState, stageResults, gitCtx, pgCard, pgProject
|
|
935
|
+
} = ctx;
|
|
936
|
+
|
|
937
|
+
const iterStart = Date.now();
|
|
938
|
+
const becariaEnabled = Boolean(config.becaria?.enabled) && gitCtx?.enabled;
|
|
939
|
+
logger.setContext({ iteration: i, stage: "iteration" });
|
|
940
|
+
|
|
941
|
+
emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
|
|
942
|
+
message: `Iteration ${i}/${config.max_iterations}`,
|
|
943
|
+
detail: { iteration: i, maxIterations: config.max_iterations }
|
|
944
|
+
}));
|
|
945
|
+
logger.info(`Iteration ${i}/${config.max_iterations}`);
|
|
946
|
+
|
|
947
|
+
const crResult = await runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i });
|
|
948
|
+
if (crResult.action === "return" || crResult.action === "retry") return crResult;
|
|
949
|
+
|
|
950
|
+
const qgResult = await runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults });
|
|
951
|
+
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
952
|
+
|
|
953
|
+
await handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i });
|
|
954
|
+
|
|
955
|
+
const revResult = await runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
|
|
956
|
+
if (revResult.action === "return" || revResult.action === "retry") return revResult;
|
|
957
|
+
const review = revResult.review;
|
|
958
|
+
|
|
959
|
+
const iterDuration = Date.now() - iterStart;
|
|
960
|
+
emitProgress(emitter, makeEvent("iteration:end", { ...eventBase, stage: "iteration" }, {
|
|
961
|
+
message: `Iteration ${i} completed`, detail: { duration: iterDuration }
|
|
962
|
+
}));
|
|
963
|
+
session.standby_retry_count = 0;
|
|
964
|
+
|
|
965
|
+
const solomonResult = await handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, becariaEnabled });
|
|
966
|
+
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
967
|
+
|
|
968
|
+
await handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger });
|
|
969
|
+
|
|
970
|
+
if (review.approved) {
|
|
971
|
+
const approvedResult = await handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review });
|
|
972
|
+
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion });
|
|
976
|
+
if (retryResult.action === "return") return retryResult;
|
|
977
|
+
|
|
978
|
+
return { action: "next" };
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
|
|
982
|
+
const pipelineFlags = resolvePipelineFlags(config);
|
|
983
|
+
|
|
984
|
+
if (flags.dryRun) {
|
|
985
|
+
return handleDryRun({ task, config, flags, emitter, pipelineFlags });
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
989
|
+
config = ctx.config;
|
|
990
|
+
|
|
991
|
+
const checkpointIntervalMs = (config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
992
|
+
let lastCheckpointAt = Date.now();
|
|
993
|
+
let checkpointDisabled = false;
|
|
994
|
+
|
|
995
|
+
let i = 0;
|
|
996
|
+
while (i < config.max_iterations) {
|
|
997
|
+
i += 1;
|
|
998
|
+
const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
|
|
999
|
+
|
|
1000
|
+
const cpResult = await handleCheckpoint({
|
|
1001
|
+
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1002
|
+
i, config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary
|
|
1003
|
+
});
|
|
1004
|
+
if (cpResult.action === "stop") return cpResult.result;
|
|
1005
|
+
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1006
|
+
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1007
|
+
|
|
1008
|
+
await checkSessionTimeout({ askQuestion, elapsedMinutes, config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1009
|
+
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
1010
|
+
|
|
1011
|
+
ctx.eventBase.iteration = i;
|
|
1012
|
+
ctx.i = i;
|
|
1013
|
+
|
|
1014
|
+
const iterResult = await runSingleIteration({ ...ctx, config, logger, emitter, askQuestion, task, pgProject, i });
|
|
1015
|
+
if (iterResult.action === "return") return iterResult.result;
|
|
1016
|
+
if (iterResult.action === "retry") { i -= 1; }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
return handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config, stageResults: ctx.stageResults });
|
|
1020
|
+
}
|
|
1021
|
+
|
|
950
1022
|
export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
|
|
951
1023
|
const session = answer
|
|
952
1024
|
? await resumeSessionWithAnswer(sessionId, answer)
|
|
@@ -958,8 +1030,8 @@ export async function resumeFlow({ sessionId, answer, config, logger, flags = {}
|
|
|
958
1030
|
}
|
|
959
1031
|
|
|
960
1032
|
// Allow resuming "stopped" sessions (checkpoint stop) and "failed" sessions
|
|
961
|
-
const resumableStatuses = ["running", "stopped", "failed"];
|
|
962
|
-
if (!resumableStatuses.
|
|
1033
|
+
const resumableStatuses = new Set(["running", "stopped", "failed"]);
|
|
1034
|
+
if (!resumableStatuses.has(session.status)) {
|
|
963
1035
|
logger.info(`Session ${sessionId} has status ${session.status} — not resumable`);
|
|
964
1036
|
return session;
|
|
965
1037
|
}
|