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