karajan-code 1.16.0 → 1.17.0

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