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.
Files changed (72) hide show
  1. package/package.json +1 -1
  2. package/src/activity-log.js +13 -13
  3. package/src/agents/availability.js +2 -3
  4. package/src/agents/claude-agent.js +42 -21
  5. package/src/agents/model-registry.js +1 -1
  6. package/src/becaria/dispatch.js +1 -1
  7. package/src/becaria/repo.js +3 -3
  8. package/src/cli.js +5 -2
  9. package/src/commands/doctor.js +154 -108
  10. package/src/commands/init.js +101 -90
  11. package/src/commands/plan.js +1 -1
  12. package/src/commands/report.js +77 -71
  13. package/src/commands/roles.js +0 -1
  14. package/src/commands/run.js +2 -3
  15. package/src/config.js +174 -93
  16. package/src/git/automation.js +3 -4
  17. package/src/guards/intent-guard.js +123 -0
  18. package/src/guards/output-guard.js +158 -0
  19. package/src/guards/perf-guard.js +126 -0
  20. package/src/guards/policy-resolver.js +3 -3
  21. package/src/mcp/orphan-guard.js +1 -2
  22. package/src/mcp/progress.js +4 -3
  23. package/src/mcp/run-kj.js +1 -0
  24. package/src/mcp/server-handlers.js +242 -253
  25. package/src/mcp/server.js +4 -3
  26. package/src/mcp/tools.js +2 -0
  27. package/src/orchestrator/agent-fallback.js +1 -3
  28. package/src/orchestrator/iteration-stages.js +206 -170
  29. package/src/orchestrator/pre-loop-stages.js +200 -34
  30. package/src/orchestrator/solomon-rules.js +2 -2
  31. package/src/orchestrator.js +902 -746
  32. package/src/planning-game/adapter.js +23 -20
  33. package/src/planning-game/architect-adrs.js +45 -0
  34. package/src/planning-game/client.js +15 -1
  35. package/src/planning-game/decomposition.js +7 -5
  36. package/src/prompts/architect.js +88 -0
  37. package/src/prompts/discover.js +54 -53
  38. package/src/prompts/planner.js +53 -33
  39. package/src/prompts/triage.js +8 -16
  40. package/src/review/parser.js +18 -19
  41. package/src/review/profiles.js +2 -2
  42. package/src/review/schema.js +3 -3
  43. package/src/review/scope-filter.js +3 -4
  44. package/src/roles/architect-role.js +122 -0
  45. package/src/roles/commiter-role.js +2 -2
  46. package/src/roles/discover-role.js +59 -67
  47. package/src/roles/index.js +1 -0
  48. package/src/roles/planner-role.js +54 -38
  49. package/src/roles/refactorer-role.js +8 -7
  50. package/src/roles/researcher-role.js +6 -7
  51. package/src/roles/reviewer-role.js +4 -5
  52. package/src/roles/security-role.js +3 -4
  53. package/src/roles/solomon-role.js +6 -18
  54. package/src/roles/sonar-role.js +5 -1
  55. package/src/roles/tester-role.js +8 -5
  56. package/src/roles/triage-role.js +2 -2
  57. package/src/session-cleanup.js +29 -24
  58. package/src/session-store.js +1 -1
  59. package/src/sonar/api.js +1 -1
  60. package/src/sonar/manager.js +1 -1
  61. package/src/sonar/project-key.js +5 -5
  62. package/src/sonar/scanner.js +34 -65
  63. package/src/utils/display.js +312 -272
  64. package/src/utils/git.js +3 -3
  65. package/src/utils/logger.js +6 -1
  66. package/src/utils/model-selector.js +5 -5
  67. package/src/utils/process.js +80 -102
  68. package/src/utils/rate-limit-detector.js +13 -13
  69. package/src/utils/run-log.js +55 -52
  70. package/templates/kj.config.yml +33 -0
  71. package/templates/roles/architect.md +62 -0
  72. package/templates/roles/planner.md +1 -0
@@ -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
- export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
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
- // --- Dry-run: return summary without executing anything ---
52
- if (flags.dryRun) {
53
- const dryRunPolicies = applyPolicies({
54
- taskType: flags.taskType || config.taskType || null,
55
- policies: config.policies,
56
- });
57
- const projectDir = config.projectDir || process.cwd();
58
- const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
59
- const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
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
- emitProgress(
102
- emitter,
103
- makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
104
- message: "Dry-run complete — no changes made",
105
- detail: summary
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
- return summary;
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
- const repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
113
- const coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent });
114
- const startedAt = Date.now();
115
- const eventBase = { sessionId: null, iteration: 0, stage: null, startedAt };
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 status = totalCost > budgetLimit ? "fail" : pctUsed >= warnThresholdPct ? "paused" : "ok";
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
- const session = await createSession(sessionInit);
172
-
173
- eventBase.sessionId = session.id;
174
+ return createSession(sessionInit);
175
+ }
174
176
 
175
- // --- Planning Game: mark card as In Progress ---
176
- let pgCard = null;
177
- if (pgTaskId && pgProject && config.planning_game?.enabled !== false) {
178
- try {
179
- const { fetchCard, updateCard } = await import("./planning-game/client.js");
180
- 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`);
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
- session.pg_card = pgCard || null;
203
+ }
200
204
 
201
- emitProgress(
202
- emitter,
203
- makeEvent("session:start", eventBase, {
204
- message: "Session started",
205
- detail: {
206
- task,
207
- coder: coderRole.provider,
208
- reviewer: reviewerRole.provider,
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
- // Accumulate stage results for final summary
215
- const stageResults = {};
216
- const sonarState = { issuesInitial: null, issuesFinal: null };
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
- // --- Discover (pre-triage, opt-in) ---
219
- if (flags.enableDiscover !== undefined) discoverEnabled = Boolean(flags.enableDiscover);
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
- if (triageEnabled) {
226
- const triageResult = await runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
227
- if (triageResult.roleOverrides.plannerEnabled !== undefined) plannerEnabled = triageResult.roleOverrides.plannerEnabled;
228
- if (triageResult.roleOverrides.researcherEnabled !== undefined) researcherEnabled = triageResult.roleOverrides.researcherEnabled;
229
- if (triageResult.roleOverrides.refactorerEnabled !== undefined) refactorerEnabled = triageResult.roleOverrides.refactorerEnabled;
230
- if (triageResult.roleOverrides.reviewerEnabled !== undefined) reviewerEnabled = triageResult.roleOverrides.reviewerEnabled;
231
- if (triageResult.roleOverrides.testerEnabled !== undefined) testerEnabled = triageResult.roleOverrides.testerEnabled;
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
- stageResults.triage.pgSubtasks = createdSubtasks;
265
- logger.info(`Planning Game: created ${createdSubtasks.length} subtasks from decomposition`);
266
-
267
- emitProgress(
268
- emitter,
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
- if (flags.enablePlanner !== undefined) plannerEnabled = Boolean(flags.enablePlanner);
288
- if (flags.enableResearcher !== undefined) researcherEnabled = Boolean(flags.enableResearcher);
289
- if (flags.enableRefactorer !== undefined) refactorerEnabled = Boolean(flags.enableRefactorer);
290
- if (flags.enableReviewer !== undefined) reviewerEnabled = Boolean(flags.enableReviewer);
291
- if (flags.enableTester !== undefined) testerEnabled = Boolean(flags.enableTester);
292
- if (flags.enableSecurity !== undefined) securityEnabled = Boolean(flags.enableSecurity);
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
- // --- Policy resolver: gate stages by taskType ---
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
- // Apply policy gates on shallow copies (never mutate the caller's config)
283
+ let updatedConfig = config;
303
284
  if (!resolvedPolicies.tdd) {
304
- config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
285
+ updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
305
286
  }
306
287
  if (!resolvedPolicies.sonar) {
307
- config = { ...config, sonarqube: { ...config.sonarqube, enabled: false } };
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
- // --- Researcher (pre-planning) ---
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
- if (researcherEnabled) {
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
- // --- Planner ---
330
- let plannedTask = task;
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 plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, triageDecomposition, trackBudget });
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
- // BecarIA: dispatch planner comment (only on resume where PR already exists)
338
- if (Boolean(config.becaria?.enabled) && session.becaria_pr_number) {
339
- try {
340
- const { dispatchComment } = await import("./becaria/dispatch.js");
341
- const { detectRepo } = await import("./becaria/repo.js");
342
- const repo = await detectRepo();
343
- if (repo) {
344
- const p = plannerResult.stageResult;
345
- await dispatchComment({
346
- repo, prNumber: session.becaria_pr_number, agent: "Planner",
347
- body: `Plan: ${p?.summary || plannedTask}`,
348
- becariaConfig: config.becaria
349
- });
350
- }
351
- } catch { /* non-blocking */ }
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 gitCtx = await prepareGitAutomation({ config, task, logger, session });
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
- const projectDir = config.projectDir || process.cwd();
358
- const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
359
- await coderRoleInstance.init();
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 checkpointIntervalMs = (config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
362
- let lastCheckpointAt = Date.now();
363
- let checkpointDisabled = false;
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
- for (let i = 1; i <= config.max_iterations; i += 1) {
366
- const elapsedMinutes = (Date.now() - startedAt) / 60000;
384
+ await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
367
385
 
368
- // --- Interactive checkpoint: pause and ask every N minutes ---
369
- if (!checkpointDisabled && askQuestion && (Date.now() - lastCheckpointAt) >= checkpointIntervalMs) {
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
- emitProgress(
377
- emitter,
378
- makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
379
- message: `Interactive checkpoint at ${elapsedStr} min`,
380
- detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted }
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
- const answer = await askQuestion(
385
- `${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`
386
- );
402
+ return parseCheckpointAnswer({ trimmedAnswer, checkpointDisabled, config });
403
+ }
387
404
 
388
- await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
389
-
390
- // Explicit stop: only when the user clearly chose option 4 or typed "stop".
391
- // A null/empty answer (e.g. elicitInput failure, AI timeout) defaults to
392
- // "continue 5 more minutes" so the session is not killed accidentally.
393
- const trimmedAnswer = (answer || "").trim();
394
- const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
395
-
396
- if (isExplicitStop) {
397
- await markSessionStatus(session, "stopped");
398
- emitProgress(
399
- emitter,
400
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
401
- status: "stopped",
402
- message: "Session stopped by user at checkpoint",
403
- detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
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
- // No answer or unrecognized default to continue 5 more minutes
410
- if (!trimmedAnswer) {
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
- // --- Hard timeout: only when no askQuestion available ---
428
- if (!askQuestion && elapsedMinutes > config.session.max_total_minutes) {
429
- await markSessionStatus(session, "failed");
430
- emitProgress(
431
- emitter,
432
- makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
433
- status: "fail",
434
- message: "Session timed out",
435
- detail: { approved: false, reason: "timeout", budget: budgetSummary() }
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
- if (budgetTracker.isOverBudget(config?.max_budget_usd)) {
442
- await markSessionStatus(session, "failed");
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
- eventBase.iteration = i;
457
- const iterStart = Date.now();
458
- const becariaEnabled = Boolean(config.becaria?.enabled) && gitCtx?.enabled;
459
- logger.setContext({ iteration: i, stage: "iteration" });
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
- emitProgress(
462
- emitter,
463
- makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
464
- message: `Iteration ${i}/${config.max_iterations}`,
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
- logger.info(`Iteration ${i}/${config.max_iterations}`);
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
- // --- Coder ---
472
- const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
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
- // --- Refactorer ---
498
- if (refactorerEnabled) {
499
- const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
500
- if (refResult?.action === "pause") {
501
- return refResult.result;
502
- }
503
- if (refResult?.action === "standby") {
504
- const standbyRetries = session.standby_retry_count || 0;
505
- if (standbyRetries >= MAX_STANDBY_RETRIES) {
506
- await pauseSession(session, {
507
- question: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${refResult.standbyInfo.agent}`,
508
- context: { iteration: i, stage: "refactorer", reason: "standby_exhausted" }
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
- // --- TDD Policy ---
526
- const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion });
527
- if (tddResult.action === "pause") {
528
- return tddResult.result;
529
- }
530
- if (tddResult.action === "continue") {
531
- continue;
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
- // --- SonarQube ---
535
- if (config.sonarqube.enabled) {
536
- const sonarResult = await runSonarStage({
537
- config, logger, emitter, eventBase, session, trackBudget, iteration: i,
538
- repeatDetector, budgetSummary, sonarState,
539
- askQuestion, task
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
- // --- BecarIA Gateway: early PR or incremental push ---
569
- if (becariaEnabled) {
570
- try {
571
- const { dispatchComment } = await import("./becaria/dispatch.js");
572
- const { detectRepo } = await import("./becaria/repo.js");
573
- const repo = await detectRepo();
574
-
575
- if (!session.becaria_pr_number) {
576
- // First iteration: commit + push + create PR
577
- const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
578
- if (earlyPr) {
579
- session.becaria_pr_number = earlyPr.prNumber;
580
- session.becaria_pr_url = earlyPr.prUrl;
581
- session.becaria_commits = earlyPr.commits;
582
- await saveSession(session);
583
- emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
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
- // --- Reviewer ---
622
- let review = {
623
- approved: true,
624
- blocking_issues: [],
625
- non_blocking_suggestions: [],
626
- summary: "Reviewer disabled by pipeline",
627
- confidence: 1
628
- };
629
- if (reviewerEnabled) {
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
- // --- Iteration end ---
664
- const iterDuration = Date.now() - iterStart;
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
- // Reset standby counter after successful iteration
674
- session.standby_retry_count = 0;
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
- // BecarIA: dispatch solomon comment
713
- if (becariaEnabled && session.becaria_pr_number) {
714
- try {
715
- const { dispatchComment } = await import("./becaria/dispatch.js");
716
- const { detectRepo } = await import("./becaria/repo.js");
717
- const repo = await detectRepo();
718
- if (repo) {
719
- const alerts = rulesResult.alerts || [];
720
- const alertMsg = alerts.length > 0
721
- ? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
722
- : "No anomalies detected";
723
- await dispatchComment({
724
- repo, prNumber: session.becaria_pr_number, agent: "Solomon",
725
- body: `Supervisor check iteración ${i}: ${alertMsg}`,
726
- becariaConfig: config.becaria
727
- });
728
- }
729
- } catch { /* non-blocking */ }
730
- }
731
- } catch (err) {
732
- logger.warn(`Solomon rules evaluation failed: ${err.message}`);
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
- // --- BecarIA Gateway: dispatch review result ---
737
- if (becariaEnabled && session.becaria_pr_number) {
738
- try {
739
- const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
740
- const { detectRepo } = await import("./becaria/repo.js");
741
- const repo = await detectRepo();
742
- if (repo) {
743
- const bc = config.becaria;
744
- // Formal review (APPROVE / REQUEST_CHANGES)
745
- if (review.approved) {
746
- await dispatchReview({
747
- repo, prNumber: session.becaria_pr_number,
748
- event: "APPROVE", body: review.summary || "Approved", agent: "Reviewer", becariaConfig: bc
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
- logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
773
- }
774
- } catch (err) {
775
- logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
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
- if (review.approved) {
780
- session.reviewer_retry_count = 0;
781
-
782
- // --- Post-loop stages: Tester → Security ---
783
- const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
784
-
785
- if (testerEnabled) {
786
- const testerResult = await runTesterStage({
787
- config, logger, emitter, eventBase, session, coderRole, trackBudget,
788
- iteration: i, task, diff: postLoopDiff, askQuestion
789
- });
790
- if (testerResult.action === "pause") {
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
- if (securityEnabled) {
818
- const securityResult = await runSecurityStage({
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
- // --- All post-loop checks passed finalize ---
850
- const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
851
- if (stageResults.planner?.ok) {
852
- stageResults.planner.completedSteps = [...(stageResults.planner.steps || [])];
853
- }
854
- session.budget = budgetSummary();
855
- await markSessionStatus(session, "approved");
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
- const deferredIssues = session.deferred_issues || [];
881
- emitProgress(
882
- emitter,
883
- makeEvent("session:end", { ...eventBase, stage: "done" }, {
884
- message: deferredIssues.length > 0
885
- ? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
886
- : "Session approved",
887
- detail: { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues }
888
- })
889
- );
890
- return { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
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
- session.last_reviewer_feedback = review.blocking_issues
894
- .map((x) => `${x.id || "ISSUE"}: ${x.description || "Missing description"}`)
895
- .join("\n");
896
- session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
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
- const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
900
- if (session.reviewer_retry_count >= maxReviewerRetries) {
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
- if (solomonResult.action === "pause") {
921
- return { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_fail_fast" };
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
- if (solomonResult.action === "continue") {
924
- if (solomonResult.humanGuidance) {
925
- session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
926
- }
927
- session.reviewer_retry_count = 0;
928
- await saveSession(session);
929
- continue;
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
- if (solomonResult.action === "subtask") {
932
- return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" };
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.includes(session.status)) {
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
  }