karajan-code 1.59.1 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/package.json +1 -1
- package/scripts/install.js +18 -15
- package/src/agents/aider-agent.js +0 -7
- package/src/agents/claude-agent.js +1 -4
- package/src/agents/codex-agent.js +2 -6
- package/src/agents/gemini-agent.js +0 -4
- package/src/agents/opencode-agent.js +0 -4
- package/src/{becaria → ci}/dispatch.js +13 -13
- package/src/{becaria → ci}/pr-diff.js +1 -1
- package/src/cli.js +2 -2
- package/src/commands/agents.js +0 -1
- package/src/commands/board.js +8 -1
- package/src/commands/doctor.js +22 -40
- package/src/commands/init.js +8 -8
- package/src/commands/report.js +0 -13
- package/src/commands/review.js +17 -17
- package/src/commands/run.js +1 -2
- package/src/commands/status.js +1 -7
- package/src/config.js +17 -52
- package/src/git/automation.js +9 -9
- package/src/guards/output-guard.js +0 -17
- package/src/mcp/handlers/direct-handlers.js +1 -1
- package/src/mcp/handlers/run-handler.js +1 -2
- package/src/mcp/sovereignty-guard.js +1 -1
- package/src/mcp/tools.js +1 -1
- package/src/orchestrator/brain-coordinator.js +165 -0
- package/src/orchestrator/ci-integration.js +139 -0
- package/src/orchestrator/config-init.js +435 -0
- package/src/orchestrator/direct-actions.js +186 -0
- package/src/orchestrator/feedback-enrichment.js +140 -0
- package/src/orchestrator/feedback-queue.js +152 -0
- package/src/orchestrator/flow-control.js +159 -0
- package/src/orchestrator/pipeline-context.js +3 -0
- package/src/orchestrator/post-loop-stages.js +20 -15
- package/src/orchestrator/preflight-checks.js +7 -3
- package/src/orchestrator/role-output-compressor.js +190 -0
- package/src/orchestrator/session-journal.js +344 -0
- package/src/orchestrator/solomon-escalation.js +38 -5
- package/src/orchestrator/solomon-rules.js +32 -33
- package/src/orchestrator/stages/coder-stage.js +49 -4
- package/src/orchestrator/stages/reviewer-stage.js +52 -9
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +481 -672
- package/src/planning-game/client.js +2 -2
- package/src/planning-game/pipeline-adapter.js +1 -1
- package/src/prompts/coder.js +17 -3
- package/src/prompts/reviewer.js +2 -2
- package/src/prompts/rtk-snippet.js +3 -5
- package/src/review/diff-generator.js +66 -17
- package/src/review/snapshot-diff.js +122 -0
- package/src/review/tdd-policy.js +4 -2
- package/src/roles/agent-role.js +135 -0
- package/src/roles/architect-role.js +61 -108
- package/src/roles/audit-role.js +25 -71
- package/src/roles/coder-role.js +32 -57
- package/src/roles/discover-role.js +30 -71
- package/src/roles/hu-reviewer-role.js +51 -99
- package/src/roles/impeccable-role.js +33 -88
- package/src/roles/index.js +2 -0
- package/src/roles/karajan-brain-role.js +134 -0
- package/src/roles/planner-role.js +40 -75
- package/src/roles/refactorer-role.js +22 -64
- package/src/roles/researcher-role.js +51 -115
- package/src/roles/reviewer-role.js +68 -125
- package/src/roles/security-role.js +38 -102
- package/src/roles/solomon-role.js +69 -11
- package/src/roles/tester-role.js +121 -95
- package/src/roles/triage-role.js +50 -111
- package/src/session-store.js +1 -1
- package/src/skills/skill-detector.js +11 -0
- package/src/sonar/config-resolver.js +20 -1
- package/src/sonar/credentials.js +17 -0
- package/src/sonar/project-key.js +1 -1
- package/src/sonar/scanner.js +1 -1
- package/src/utils/display.js +33 -14
- package/src/utils/injection-guard.js +23 -8
- package/src/utils/logger.js +6 -1
- package/src/utils/process.js +3 -4
- package/src/utils/role-assigner.js +131 -0
- package/src/utils/update-check.js +5 -1
- package/src/webperf/devtools-detect.js +70 -23
- package/templates/roles/coder.md +7 -0
- package/templates/roles/karajan-brain.md +95 -0
- package/templates/roles/solomon.md +67 -106
- package/templates/workflows/automerge.yml +4 -4
- package/templates/workflows/houston-override.yml +4 -4
- package/templates/workflows/{becaria-gateway.yml → kj-ci-gateway.yml} +12 -12
- package/src/proxy/adapters/anthropic.js +0 -131
- package/src/proxy/adapters/gemini.js +0 -106
- package/src/proxy/adapters/index.js +0 -51
- package/src/proxy/adapters/openai.js +0 -71
- package/src/proxy/compression/ai-compressor.js +0 -209
- package/src/proxy/compression/dedup-cache.js +0 -100
- package/src/proxy/compression/deterministic/bash-build.js +0 -60
- package/src/proxy/compression/deterministic/bash-git.js +0 -101
- package/src/proxy/compression/deterministic/bash-infra.js +0 -64
- package/src/proxy/compression/deterministic/bash-misc.js +0 -53
- package/src/proxy/compression/deterministic/bash-pkg.js +0 -49
- package/src/proxy/compression/deterministic/bash-test.js +0 -61
- package/src/proxy/compression/deterministic/glob.js +0 -26
- package/src/proxy/compression/deterministic/grep.js +0 -53
- package/src/proxy/compression/deterministic/index.js +0 -58
- package/src/proxy/compression/deterministic/read.js +0 -22
- package/src/proxy/compression/deterministic/utils.js +0 -33
- package/src/proxy/compression/pipeline.js +0 -270
- package/src/proxy/middleware/prompt-injector.js +0 -165
- package/src/proxy/middleware/response-interceptor.js +0 -223
- package/src/proxy/progress-monitor.js +0 -98
- package/src/proxy/proxy-lifecycle.js +0 -201
- package/src/proxy/proxy-server.js +0 -239
- package/src/proxy/security.js +0 -45
- /package/src/{becaria → ci}/index.js +0 -0
- /package/src/{becaria → ci}/repo.js +0 -0
package/src/orchestrator.js
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
|
-
import fs from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
1
|
import { createAgent } from "./agents/index.js";
|
|
4
2
|
import {
|
|
5
|
-
createSession,
|
|
6
3
|
loadSession,
|
|
7
4
|
markSessionStatus,
|
|
8
5
|
pauseSession,
|
|
@@ -10,28 +7,21 @@ import {
|
|
|
10
7
|
saveSession,
|
|
11
8
|
addCheckpoint
|
|
12
9
|
} from "./session-store.js";
|
|
13
|
-
import {
|
|
14
|
-
import { buildCoderPrompt } from "./prompts/coder.js";
|
|
15
|
-
import { buildReviewerPrompt } from "./prompts/reviewer.js";
|
|
10
|
+
import { generateDiff } from "./review/diff-generator.js";
|
|
16
11
|
import { resolveRole } from "./config.js";
|
|
12
|
+
import { resolveReviewProfile } from "./review/profiles.js";
|
|
13
|
+
import { msg, getLang } from "./utils/messages.js";
|
|
17
14
|
import { RepeatDetector, getRepeatThreshold } from "./repeat-detector.js";
|
|
18
15
|
import { emitProgress, makeEvent } from "./utils/events.js";
|
|
19
|
-
import { BudgetTracker, extractUsageMetrics } from "./utils/budget.js";
|
|
20
16
|
import {
|
|
21
17
|
prepareGitAutomation,
|
|
22
|
-
finalizeGitAutomation
|
|
23
|
-
earlyPrCreation,
|
|
24
|
-
incrementalPush
|
|
18
|
+
finalizeGitAutomation
|
|
25
19
|
} from "./git/automation.js";
|
|
26
|
-
import { resolveRoleMdPath, loadFirstExisting } from "./roles/base-role.js";
|
|
27
|
-
import { applyPolicies } from "./guards/policy-resolver.js";
|
|
28
20
|
import { scanDiff } from "./guards/output-guard.js";
|
|
29
21
|
import { scanPerfDiff } from "./guards/perf-guard.js";
|
|
30
22
|
import { classifyIntent } from "./guards/intent-guard.js";
|
|
31
|
-
import { resolveReviewProfile } from "./review/profiles.js";
|
|
32
23
|
import { CoderRole } from "./roles/coder-role.js";
|
|
33
24
|
import { invokeSolomon } from "./orchestrator/solomon-escalation.js";
|
|
34
|
-
import { msg, getLang } from "./utils/messages.js";
|
|
35
25
|
import { PipelineContext } from "./orchestrator/pipeline-context.js";
|
|
36
26
|
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./orchestrator/pre-loop-stages.js";
|
|
37
27
|
import { runDomainCuratorStage } from "./orchestrator/stages/domain-curator-stage.js";
|
|
@@ -44,267 +34,43 @@ import { detectTestFramework } from "./utils/project-detect.js";
|
|
|
44
34
|
import { runPreflightChecks } from "./orchestrator/preflight-checks.js";
|
|
45
35
|
import { detectRtk } from "./utils/rtk-detect.js";
|
|
46
36
|
import { createRtkRunner, RtkSavingsTracker } from "./utils/rtk-wrapper.js";
|
|
47
|
-
import { setRunner as setDiffRunner } from "./review/diff-generator.js";
|
|
37
|
+
import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "./review/diff-generator.js";
|
|
48
38
|
import { setRunner as setGitRunner } from "./utils/git.js";
|
|
49
39
|
import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "./skills/skill-detector.js";
|
|
50
40
|
import { isOpenSkillsAvailable } from "./skills/openskills-client.js";
|
|
51
|
-
import { startProxy, stopProxy } from "./proxy/proxy-lifecycle.js";
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
// --- Product Context loader ---
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Load product context from well-known file locations.
|
|
58
|
-
* Returns the file content or null if no file is found.
|
|
59
|
-
* @param {string|null} projectDir
|
|
60
|
-
* @returns {Promise<{content: string|null, source: string|null}>}
|
|
61
|
-
*/
|
|
62
|
-
export async function loadProductContext(projectDir) {
|
|
63
|
-
const base = projectDir || process.cwd();
|
|
64
|
-
const candidates = [
|
|
65
|
-
path.join(base, ".karajan", "context.md"),
|
|
66
|
-
path.join(base, "product-vision.md")
|
|
67
|
-
];
|
|
68
|
-
for (const file of candidates) {
|
|
69
|
-
try {
|
|
70
|
-
const content = await fs.readFile(file, "utf8");
|
|
71
|
-
return { content, source: file };
|
|
72
|
-
} catch { /* not found, try next */ }
|
|
73
|
-
}
|
|
74
|
-
return { content: null, source: null };
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// --- Extracted helper functions (pure refactoring, zero behavior change) ---
|
|
78
|
-
|
|
79
|
-
function resolvePipelineFlags(config) {
|
|
80
|
-
return {
|
|
81
|
-
plannerEnabled: Boolean(config.pipeline?.planner?.enabled),
|
|
82
|
-
refactorerEnabled: Boolean(config.pipeline?.refactorer?.enabled),
|
|
83
|
-
researcherEnabled: Boolean(config.pipeline?.researcher?.enabled),
|
|
84
|
-
testerEnabled: Boolean(config.pipeline?.tester?.enabled),
|
|
85
|
-
securityEnabled: Boolean(config.pipeline?.security?.enabled),
|
|
86
|
-
impeccableEnabled: Boolean(config.pipeline?.impeccable?.enabled),
|
|
87
|
-
reviewerEnabled: config.pipeline?.reviewer?.enabled !== false,
|
|
88
|
-
discoverEnabled: Boolean(config.pipeline?.discover?.enabled),
|
|
89
|
-
architectEnabled: Boolean(config.pipeline?.architect?.enabled),
|
|
90
|
-
huReviewerEnabled: Boolean(config.pipeline?.hu_reviewer?.enabled),
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
async function handleDryRun({ task, config, flags, emitter, pipelineFlags }) {
|
|
95
|
-
const { plannerEnabled, refactorerEnabled, researcherEnabled, testerEnabled, securityEnabled, impeccableEnabled, reviewerEnabled, discoverEnabled, architectEnabled, huReviewerEnabled } = pipelineFlags;
|
|
96
|
-
const plannerRole = resolveRole(config, "planner");
|
|
97
|
-
const coderRole = resolveRole(config, "coder");
|
|
98
|
-
const reviewerRole = resolveRole(config, "reviewer");
|
|
99
|
-
const refactorerRole = resolveRole(config, "refactorer");
|
|
100
|
-
const triageEnabled = true;
|
|
101
|
-
|
|
102
|
-
const dryRunPolicies = applyPolicies({
|
|
103
|
-
taskType: flags.taskType || config.taskType || null,
|
|
104
|
-
policies: config.policies,
|
|
105
|
-
});
|
|
106
|
-
const projectDir = config.projectDir || process.cwd();
|
|
107
|
-
const { rules: reviewRules } = await resolveReviewProfile({ mode: config.review_mode, projectDir });
|
|
108
|
-
const coderRules = await loadFirstExisting(resolveRoleMdPath("coder", projectDir));
|
|
109
|
-
const coderPrompt = await buildCoderPrompt({ task, coderRules, methodology: config.development?.methodology, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
|
|
110
|
-
const reviewerPrompt = await buildReviewerPrompt({ task, diff: "(dry-run: no diff)", reviewRules, mode: config.review_mode, serenaEnabled: Boolean(config.serena?.enabled), rtkAvailable: Boolean(config.rtk?.available), proxyEnabled: Boolean(config.proxy?.enabled), productContext: config.productContext || null });
|
|
111
|
-
|
|
112
|
-
const summary = {
|
|
113
|
-
dry_run: true,
|
|
114
|
-
task,
|
|
115
|
-
policies: dryRunPolicies,
|
|
116
|
-
roles: { planner: plannerRole, coder: coderRole, reviewer: reviewerRole, refactorer: refactorerRole },
|
|
117
|
-
pipeline: {
|
|
118
|
-
discover_enabled: discoverEnabled,
|
|
119
|
-
architect_enabled: architectEnabled,
|
|
120
|
-
triage_enabled: triageEnabled,
|
|
121
|
-
planner_enabled: plannerEnabled,
|
|
122
|
-
refactorer_enabled: refactorerEnabled,
|
|
123
|
-
sonar_enabled: Boolean(config.sonarqube?.enabled),
|
|
124
|
-
reviewer_enabled: reviewerEnabled,
|
|
125
|
-
researcher_enabled: researcherEnabled,
|
|
126
|
-
tester_enabled: testerEnabled,
|
|
127
|
-
security_enabled: securityEnabled,
|
|
128
|
-
impeccable_enabled: impeccableEnabled,
|
|
129
|
-
solomon_enabled: Boolean(config.pipeline?.solomon?.enabled),
|
|
130
|
-
hu_reviewer_enabled: huReviewerEnabled
|
|
131
|
-
},
|
|
132
|
-
limits: {
|
|
133
|
-
max_iterations: config.max_iterations,
|
|
134
|
-
max_iteration_minutes: config.session?.max_iteration_minutes,
|
|
135
|
-
max_total_minutes: config.session?.max_total_minutes,
|
|
136
|
-
max_sonar_retries: config.session?.max_sonar_retries,
|
|
137
|
-
max_reviewer_retries: config.session?.max_reviewer_retries,
|
|
138
|
-
max_tester_retries: config.session?.max_tester_retries,
|
|
139
|
-
max_security_retries: config.session?.max_security_retries
|
|
140
|
-
},
|
|
141
|
-
prompts: { coder: coderPrompt, reviewer: reviewerPrompt },
|
|
142
|
-
git: config.git
|
|
143
|
-
};
|
|
144
41
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
function trackBudget({ role, provider, model, result, duration_ms, promptSize }) {
|
|
170
|
-
// Attach promptSize to result if provided, so extractUsageMetrics can estimate tokens
|
|
171
|
-
const enrichedResult = promptSize && result ? { ...result, promptSize } : result;
|
|
172
|
-
const metrics = extractUsageMetrics(enrichedResult, model);
|
|
173
|
-
budgetTracker.record({ role, provider, ...metrics, duration_ms, stage_index: stageCounter++ });
|
|
174
|
-
|
|
175
|
-
if (!hasBudgetLimit) return;
|
|
176
|
-
const totalCost = budgetTracker.total().cost_usd;
|
|
177
|
-
const pctUsed = budgetLimit === 0 ? 100 : (totalCost / budgetLimit) * 100;
|
|
178
|
-
const warnOrOk = pctUsed >= warnThresholdPct ? "paused" : "ok";
|
|
179
|
-
const status = totalCost > budgetLimit ? "fail" : warnOrOk;
|
|
180
|
-
emitProgress(
|
|
181
|
-
emitter,
|
|
182
|
-
makeEvent("budget:update", { ...eventBase, stage: role }, {
|
|
183
|
-
status,
|
|
184
|
-
message: `Budget: $${totalCost.toFixed(2)} / $${budgetLimit.toFixed(2)}`,
|
|
185
|
-
detail: {
|
|
186
|
-
...budgetSummary(),
|
|
187
|
-
max_budget_usd: budgetLimit,
|
|
188
|
-
warn_threshold_pct: warnThresholdPct,
|
|
189
|
-
pct_used: Number(pctUsed.toFixed(2)),
|
|
190
|
-
remaining_usd: budgetTracker.remaining(budgetLimit),
|
|
191
|
-
executorType: "system"
|
|
192
|
-
}
|
|
193
|
-
})
|
|
194
|
-
);
|
|
195
|
-
}
|
|
42
|
+
// Extracted modules
|
|
43
|
+
import {
|
|
44
|
+
loadProductContext as _loadProductContext,
|
|
45
|
+
resolvePipelineFlags, handleDryRun, createBudgetManager,
|
|
46
|
+
initializeSession, applyTriageOverrides, applyAutoSimplify,
|
|
47
|
+
applyFlagOverrides, resolvePipelinePolicies, autoInit,
|
|
48
|
+
updateGitignoreForStack
|
|
49
|
+
} from "./orchestrator/config-init.js";
|
|
50
|
+
import {
|
|
51
|
+
tryCiComment, handleCiEarlyPrOrPush, handleCiReviewDispatch,
|
|
52
|
+
formatBlockingIssues
|
|
53
|
+
} from "./orchestrator/ci-integration.js";
|
|
54
|
+
import {
|
|
55
|
+
shouldAutoContinueCheckpoint as _shouldAutoContinueCheckpoint,
|
|
56
|
+
parseCheckpointAnswer as _parseCheckpointAnswer,
|
|
57
|
+
handleCheckpoint, checkSessionTimeout, checkBudgetExceeded,
|
|
58
|
+
takeCheckpointSnapshot
|
|
59
|
+
} from "./orchestrator/flow-control.js";
|
|
60
|
+
import {
|
|
61
|
+
createJournalDir, writePreLoopJournal, writeIterationsJournal,
|
|
62
|
+
writeDecisionsJournal, writeTreeJournal, writeSummaryJournal,
|
|
63
|
+
formatIteration, formatDecision, buildPlanSummary
|
|
64
|
+
} from "./orchestrator/session-journal.js";
|
|
196
65
|
|
|
197
|
-
|
|
198
|
-
|
|
66
|
+
// Re-export for external consumers
|
|
67
|
+
export const loadProductContext = _loadProductContext;
|
|
68
|
+
export const shouldAutoContinueCheckpoint = _shouldAutoContinueCheckpoint;
|
|
69
|
+
export const parseCheckpointAnswer = _parseCheckpointAnswer;
|
|
199
70
|
|
|
200
|
-
async function initializeSession({ task, config, flags, pgTaskId, pgProject }) {
|
|
201
|
-
const baseRef = await computeBaseRef({ baseBranch: config.base_branch, baseRef: flags.baseRef || null });
|
|
202
|
-
const sessionInit = {
|
|
203
|
-
task,
|
|
204
|
-
config_snapshot: config,
|
|
205
|
-
base_ref: baseRef,
|
|
206
|
-
session_start_sha: baseRef,
|
|
207
|
-
last_reviewer_feedback: null,
|
|
208
|
-
repeated_issue_count: 0,
|
|
209
|
-
sonar_retry_count: 0,
|
|
210
|
-
reviewer_retry_count: 0,
|
|
211
|
-
standby_retry_count: 0,
|
|
212
|
-
last_sonar_issue_signature: null,
|
|
213
|
-
sonar_repeat_count: 0,
|
|
214
|
-
last_reviewer_issue_signature: null,
|
|
215
|
-
reviewer_repeat_count: 0,
|
|
216
|
-
deferred_issues: []
|
|
217
|
-
};
|
|
218
|
-
if (pgTaskId) sessionInit.pg_task_id = pgTaskId;
|
|
219
|
-
if (pgProject) sessionInit.pg_project_id = pgProject;
|
|
220
|
-
return createSession(sessionInit);
|
|
221
|
-
}
|
|
222
71
|
|
|
223
72
|
// PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
|
|
224
73
|
|
|
225
|
-
function applyTriageOverrides(pipelineFlags, roleOverrides) {
|
|
226
|
-
const keys = ["plannerEnabled", "researcherEnabled", "architectEnabled", "refactorerEnabled", "reviewerEnabled", "testerEnabled", "securityEnabled", "impeccableEnabled"];
|
|
227
|
-
for (const key of keys) {
|
|
228
|
-
if (roleOverrides[key] !== undefined) {
|
|
229
|
-
pipelineFlags[key] = roleOverrides[key];
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const SIMPLE_LEVELS = new Set(["trivial", "simple"]);
|
|
235
|
-
|
|
236
|
-
function applyAutoSimplify({ pipelineFlags, triageLevel, config, flags, logger, emitter, eventBase }) {
|
|
237
|
-
if (!config.pipeline?.auto_simplify) return false;
|
|
238
|
-
if (!triageLevel || !SIMPLE_LEVELS.has(triageLevel)) return false;
|
|
239
|
-
if (flags.mode) return false;
|
|
240
|
-
if (flags.enableReviewer !== undefined || flags.enableTester !== undefined) return false;
|
|
241
|
-
|
|
242
|
-
pipelineFlags.reviewerEnabled = false;
|
|
243
|
-
pipelineFlags.testerEnabled = false;
|
|
244
|
-
|
|
245
|
-
const disabledRoles = ["reviewer", "tester"];
|
|
246
|
-
logger.info(`Simple task (${triageLevel}) — lightweight pipeline (disabled: ${disabledRoles.join(", ")})`);
|
|
247
|
-
emitProgress(
|
|
248
|
-
emitter,
|
|
249
|
-
makeEvent("pipeline:simplify", { ...eventBase, stage: "triage" }, {
|
|
250
|
-
message: `Simple task (${triageLevel}) — lightweight pipeline`,
|
|
251
|
-
detail: { level: triageLevel, disabledRoles }
|
|
252
|
-
})
|
|
253
|
-
);
|
|
254
|
-
return true;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
// PG decomposition logic moved to src/planning-game/pipeline-adapter.js → handlePgDecomposition()
|
|
258
|
-
|
|
259
|
-
function applyFlagOverrides(pipelineFlags, flags) {
|
|
260
|
-
if (flags.enablePlanner !== undefined) pipelineFlags.plannerEnabled = Boolean(flags.enablePlanner);
|
|
261
|
-
if (flags.enableResearcher !== undefined) pipelineFlags.researcherEnabled = Boolean(flags.enableResearcher);
|
|
262
|
-
if (flags.enableArchitect !== undefined) pipelineFlags.architectEnabled = Boolean(flags.enableArchitect);
|
|
263
|
-
if (flags.enableRefactorer !== undefined) pipelineFlags.refactorerEnabled = Boolean(flags.enableRefactorer);
|
|
264
|
-
if (flags.enableReviewer !== undefined) pipelineFlags.reviewerEnabled = Boolean(flags.enableReviewer);
|
|
265
|
-
if (flags.enableTester !== undefined) pipelineFlags.testerEnabled = Boolean(flags.enableTester);
|
|
266
|
-
if (flags.enableSecurity !== undefined) pipelineFlags.securityEnabled = Boolean(flags.enableSecurity);
|
|
267
|
-
if (flags.enableImpeccable !== undefined) pipelineFlags.impeccableEnabled = Boolean(flags.enableImpeccable);
|
|
268
|
-
|
|
269
|
-
// --design flag: force-enable impeccable in refactoring mode
|
|
270
|
-
if (flags.design) {
|
|
271
|
-
pipelineFlags.impeccableEnabled = true;
|
|
272
|
-
pipelineFlags.impeccableMode = "refactoring";
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags }) {
|
|
277
|
-
const resolvedPolicies = applyPolicies({
|
|
278
|
-
taskType: flags.taskType || config.taskType || stageResults.triage?.taskType || stageResults.intent?.taskType || null,
|
|
279
|
-
policies: config.policies,
|
|
280
|
-
});
|
|
281
|
-
session.resolved_policies = resolvedPolicies;
|
|
282
|
-
|
|
283
|
-
let updatedConfig = config;
|
|
284
|
-
if (!resolvedPolicies.tdd) {
|
|
285
|
-
updatedConfig = { ...updatedConfig, development: { ...updatedConfig.development, methodology: "standard", require_test_changes: false } };
|
|
286
|
-
}
|
|
287
|
-
if (!resolvedPolicies.sonar) {
|
|
288
|
-
updatedConfig = { ...updatedConfig, sonarqube: { ...updatedConfig.sonarqube, enabled: false } };
|
|
289
|
-
}
|
|
290
|
-
if (!resolvedPolicies.reviewer) {
|
|
291
|
-
pipelineFlags.reviewerEnabled = false;
|
|
292
|
-
}
|
|
293
|
-
if (resolvedPolicies.coderRequired === false) {
|
|
294
|
-
pipelineFlags.coderRequired = false;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
emitProgress(
|
|
298
|
-
emitter,
|
|
299
|
-
makeEvent("policies:resolved", eventBase, {
|
|
300
|
-
message: `Policies resolved for taskType="${resolvedPolicies.taskType}"`,
|
|
301
|
-
detail: resolvedPolicies
|
|
302
|
-
})
|
|
303
|
-
);
|
|
304
|
-
|
|
305
|
-
return updatedConfig;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
74
|
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion }) {
|
|
309
75
|
let researchContext = null;
|
|
310
76
|
let plannedTask = task;
|
|
@@ -336,7 +102,7 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
336
102
|
plannedTask = plannerResult.plannedTask;
|
|
337
103
|
stageResults.planner = plannerResult.stageResult;
|
|
338
104
|
|
|
339
|
-
await
|
|
105
|
+
await tryCiComment({
|
|
340
106
|
config, session, logger,
|
|
341
107
|
agent: "Planner",
|
|
342
108
|
body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
|
|
@@ -346,277 +112,92 @@ async function runPlanningPhases({ config, logger, emitter, eventBase, session,
|
|
|
346
112
|
return { plannedTask };
|
|
347
113
|
}
|
|
348
114
|
|
|
349
|
-
async function tryBecariaComment({ config, session, logger, agent, body }) {
|
|
350
|
-
if (!config.becaria?.enabled || !session.becaria_pr_number) return;
|
|
351
|
-
try {
|
|
352
|
-
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
353
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
354
|
-
const repo = await detectRepo();
|
|
355
|
-
if (repo) {
|
|
356
|
-
await dispatchComment({
|
|
357
|
-
repo, prNumber: session.becaria_pr_number, agent,
|
|
358
|
-
body, becariaConfig: config.becaria
|
|
359
|
-
});
|
|
360
|
-
}
|
|
361
|
-
} catch { /* non-blocking */ }
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
function detectCheckpointProgress(session, lastCheckpointSnapshot) {
|
|
365
|
-
if (!lastCheckpointSnapshot) return true; // First checkpoint — assume progress
|
|
366
|
-
const currentIteration = session.reviewer_retry_count ?? 0;
|
|
367
|
-
const currentStages = Object.keys(session.resolved_policies || {}).length;
|
|
368
|
-
const currentCheckpoints = (session.checkpoints || []).length;
|
|
369
|
-
|
|
370
|
-
const iterationAdvanced = currentIteration !== lastCheckpointSnapshot.iteration;
|
|
371
|
-
const stagesChanged = currentStages !== lastCheckpointSnapshot.stagesCount;
|
|
372
|
-
const checkpointsChanged = currentCheckpoints !== lastCheckpointSnapshot.checkpointsCount;
|
|
373
|
-
|
|
374
|
-
return iterationAdvanced || stagesChanged || checkpointsChanged;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
function takeCheckpointSnapshot(session) {
|
|
378
|
-
return {
|
|
379
|
-
iteration: session.reviewer_retry_count ?? 0,
|
|
380
|
-
stagesCount: Object.keys(session.resolved_policies || {}).length,
|
|
381
|
-
checkpointsCount: (session.checkpoints || []).length
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Determine if checkpoint should auto-continue without asking the user.
|
|
387
|
-
* Exported for testing.
|
|
388
|
-
*/
|
|
389
|
-
export function shouldAutoContinueCheckpoint(session, hasProgress) {
|
|
390
|
-
if (hasProgress) {
|
|
391
|
-
session._checkpoint_stall_count = 0;
|
|
392
|
-
return { autoContinue: true, reason: "progress_detected" };
|
|
393
|
-
}
|
|
394
|
-
const wasRateLimited = (session.standby_retry_count || 0) > 0;
|
|
395
|
-
const consecutiveStalls = (session._checkpoint_stall_count || 0) + 1;
|
|
396
|
-
session._checkpoint_stall_count = consecutiveStalls;
|
|
397
|
-
if (wasRateLimited && consecutiveStalls < 3) {
|
|
398
|
-
return { autoContinue: true, reason: "recoverable_stall" };
|
|
399
|
-
}
|
|
400
|
-
return { autoContinue: false, reason: consecutiveStalls >= 3 ? "max_stalls_reached" : "no_progress" };
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
async function handleCheckpoint({ checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes, i, config, budgetTracker, stageResults, emitter, eventBase, session, budgetSummary, lastCheckpointSnapshot }) {
|
|
404
|
-
if (checkpointDisabled || !askQuestion || (Date.now() - lastCheckpointAt) < checkpointIntervalMs) {
|
|
405
|
-
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt, lastCheckpointSnapshot };
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
409
|
-
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
410
115
|
|
|
411
|
-
// Decide whether to auto-continue or ask the user
|
|
412
|
-
const hasProgress = detectCheckpointProgress(session, lastCheckpointSnapshot);
|
|
413
|
-
const newSnapshot = takeCheckpointSnapshot(session);
|
|
414
|
-
const decision = shouldAutoContinueCheckpoint(session, hasProgress);
|
|
415
116
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
420
|
-
message: `Checkpoint: ${decision.reason === "progress_detected" ? "progress detected" : "stall caused by rate limit/cooldown"}, auto-continuing (${elapsedStr} min elapsed)`,
|
|
421
|
-
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: true, reason: decision.reason }
|
|
422
|
-
})
|
|
423
|
-
);
|
|
424
|
-
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now(), lastCheckpointSnapshot: newSnapshot };
|
|
117
|
+
async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger, config, askQuestion }) {
|
|
118
|
+
if (stageResult?.action !== "standby") {
|
|
119
|
+
return { handled: false };
|
|
425
120
|
}
|
|
426
121
|
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
emitProgress(
|
|
433
|
-
emitter,
|
|
434
|
-
makeEvent("session:checkpoint", { ...eventBase, iteration: i, stage: "checkpoint" }, {
|
|
435
|
-
message: `Interactive checkpoint at ${elapsedStr} min (stalled)`,
|
|
436
|
-
detail: { elapsed_minutes: Number(elapsedStr), iterations_done: i - 1, stages: stagesCompleted, auto_continued: false }
|
|
437
|
-
})
|
|
438
|
-
);
|
|
439
|
-
|
|
440
|
-
const lang = getLang(config);
|
|
441
|
-
const answer = await askQuestion(
|
|
442
|
-
`${checkpointMsg}\n\n${msg("checkpoint_options", lang)}`
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
await addCheckpoint(session, { stage: "interactive-checkpoint", elapsed_minutes: Number(elapsedStr), answer });
|
|
446
|
-
|
|
447
|
-
const trimmedAnswer = (answer || "").trim();
|
|
448
|
-
const isExplicitStop = trimmedAnswer === "4" || trimmedAnswer.toLowerCase().startsWith("stop");
|
|
449
|
-
|
|
450
|
-
if (isExplicitStop) {
|
|
451
|
-
await markSessionStatus(session, "stopped");
|
|
452
|
-
emitProgress(
|
|
453
|
-
emitter,
|
|
454
|
-
makeEvent("session:end", { ...eventBase, iteration: i, stage: "user-stop" }, {
|
|
455
|
-
status: "stopped",
|
|
456
|
-
message: "Session stopped by user at checkpoint",
|
|
457
|
-
detail: { approved: false, reason: "user_stopped", elapsed_minutes: Number(elapsedStr), budget: budgetSummary() }
|
|
458
|
-
})
|
|
459
|
-
);
|
|
460
|
-
return { action: "stop", result: { approved: false, sessionId: session.id, reason: "user_stopped", elapsed_minutes: Number(elapsedStr) } };
|
|
461
|
-
}
|
|
122
|
+
const isOutage = stageResult.standbyInfo.isProviderOutage;
|
|
123
|
+
const agent = stageResult.standbyInfo.agent;
|
|
124
|
+
const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
|
|
125
|
+
const cooldownMs = stageResult.standbyInfo.cooldownMs;
|
|
462
126
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
127
|
+
// Rate limit = out of normal flow → Solomon decides immediately
|
|
128
|
+
const solomonResult = await invokeSolomon({
|
|
129
|
+
config, logger, emitter, eventBase, stage: `${stage}_rate_limit`, askQuestion, session,
|
|
130
|
+
iteration: i,
|
|
131
|
+
conflict: {
|
|
132
|
+
stage: `${stage}_rate_limit`,
|
|
133
|
+
task: session.task,
|
|
134
|
+
iterationCount: i,
|
|
135
|
+
maxIterations: config?.max_iterations || 5,
|
|
136
|
+
cooldownUntil,
|
|
137
|
+
cooldownMs,
|
|
138
|
+
history: [{
|
|
139
|
+
agent: stage,
|
|
140
|
+
feedback: `Agent "${agent}" rate-limited. ${isOutage ? "Provider outage (5xx)." : "API rate limit (429)."} ${cooldownUntil ? `Cooldown until ${cooldownUntil}.` : ""}`
|
|
141
|
+
}]
|
|
142
|
+
}
|
|
143
|
+
});
|
|
467
144
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
return {
|
|
477
|
-
}
|
|
478
|
-
const customMinutes = Number.parseInt(trimmedAnswer.replaceAll(/\D/g, ""), 10);
|
|
479
|
-
if (customMinutes > 0) {
|
|
480
|
-
config.session.checkpoint_interval_minutes = customMinutes;
|
|
481
|
-
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
145
|
+
if (solomonResult.action === "approve") {
|
|
146
|
+
// Solomon says skip is safe — only allowed after exhausting alternatives
|
|
147
|
+
logger.info(`Solomon: skip ${stage} after evaluating risk (agent "${agent}" rate-limited)`);
|
|
148
|
+
emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
|
|
149
|
+
status: "ok",
|
|
150
|
+
message: `Solomon: skip ${stage} (low risk, agent "${agent}" unavailable)`,
|
|
151
|
+
detail: { agent, solomonAction: "approve" }
|
|
152
|
+
}));
|
|
153
|
+
return { handled: true, action: "skip" };
|
|
482
154
|
}
|
|
483
|
-
return { action: "continue_loop", checkpointDisabled, lastCheckpointAt: Date.now() };
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
async function checkSessionTimeout({ askQuestion, elapsedMinutes, config, session, emitter, eventBase, i, budgetSummary }) {
|
|
487
|
-
if (askQuestion || elapsedMinutes <= config.session.max_total_minutes) return;
|
|
488
|
-
|
|
489
|
-
await markSessionStatus(session, "failed");
|
|
490
|
-
emitProgress(
|
|
491
|
-
emitter,
|
|
492
|
-
makeEvent("session:end", { ...eventBase, iteration: i, stage: "timeout" }, {
|
|
493
|
-
status: "fail",
|
|
494
|
-
message: "Session timed out",
|
|
495
|
-
detail: { approved: false, reason: "timeout", budget: budgetSummary() }
|
|
496
|
-
})
|
|
497
|
-
);
|
|
498
|
-
throw new Error("Session timed out");
|
|
499
|
-
}
|
|
500
155
|
|
|
501
|
-
|
|
502
|
-
|
|
156
|
+
if (solomonResult.action === "continue") {
|
|
157
|
+
// Solomon says: wait for cooldown, or retry with alternative agent
|
|
158
|
+
const altAgent = solomonResult.alternativeAgent;
|
|
159
|
+
const waitTarget = solomonResult.waitUntil;
|
|
160
|
+
|
|
161
|
+
if (waitTarget) {
|
|
162
|
+
const waitMs = Math.max(0, new Date(waitTarget).getTime() - Date.now());
|
|
163
|
+
if (waitMs > 0 && waitMs < 10 * 60 * 1000) {
|
|
164
|
+
logger.info(`Solomon: wait ${Math.round(waitMs / 1000)}s for cooldown, then retry ${stage}`);
|
|
165
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
503
168
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
emitter,
|
|
509
|
-
makeEvent("session:end", { ...eventBase, iteration: i, stage: "budget" }, {
|
|
510
|
-
status: "fail",
|
|
511
|
-
message,
|
|
512
|
-
detail: { approved: false, reason: "budget_exceeded", budget: budgetSummary(), max_budget_usd: budgetLimit }
|
|
513
|
-
})
|
|
514
|
-
);
|
|
515
|
-
throw new Error(message);
|
|
516
|
-
}
|
|
169
|
+
if (altAgent) {
|
|
170
|
+
logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
|
|
171
|
+
session._alternative_agent = { stage, provider: altAgent };
|
|
172
|
+
}
|
|
517
173
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
174
|
+
if (solomonResult.humanGuidance) {
|
|
175
|
+
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
176
|
+
}
|
|
177
|
+
await saveSession(session);
|
|
178
|
+
return { handled: true, action: "retry_reviewer_only" };
|
|
521
179
|
}
|
|
522
180
|
|
|
523
|
-
|
|
524
|
-
const isOutage = stageResult.standbyInfo.isProviderOutage;
|
|
525
|
-
const pauseReason = isOutage
|
|
526
|
-
? `Provider outage (${stageResult.standbyInfo.message || "5xx/connection error"}) — retried ${standbyRetries} times. This is NOT a KJ or code problem.`
|
|
527
|
-
: `Rate limit standby exhausted after ${standbyRetries} retries. Agent: ${stageResult.standbyInfo.agent}`;
|
|
528
|
-
|
|
529
|
-
if (standbyRetries >= MAX_STANDBY_RETRIES) {
|
|
530
|
-
session.last_reviewer_feedback = isOutage
|
|
531
|
-
? "IMPORTANT: The previous interruption was caused by a provider outage (API 500 error), NOT by a problem in your code or in Karajan. Continue from where you left off."
|
|
532
|
-
: session.last_reviewer_feedback;
|
|
533
|
-
await pauseSession(session, {
|
|
534
|
-
question: pauseReason,
|
|
535
|
-
context: { iteration: i, stage, reason: isOutage ? "provider_outage" : "standby_exhausted" }
|
|
536
|
-
});
|
|
537
|
-
emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
|
|
538
|
-
status: "paused",
|
|
539
|
-
message: `Standby exhausted after ${standbyRetries} retries`,
|
|
540
|
-
detail: { agent: stageResult.standbyInfo.agent, sessionId: session.id }
|
|
541
|
-
}));
|
|
181
|
+
if (solomonResult.action === "pause") {
|
|
542
182
|
return {
|
|
543
183
|
handled: true,
|
|
544
184
|
action: "return",
|
|
545
|
-
result: { paused: true, sessionId: session.id, question:
|
|
185
|
+
result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "rate_limit" }
|
|
546
186
|
};
|
|
547
187
|
}
|
|
548
|
-
session.standby_retry_count = standbyRetries + 1;
|
|
549
|
-
await saveSession(session);
|
|
550
|
-
await waitForCooldown({ ...stageResult.standbyInfo, retryCount: standbyRetries, emitter, eventBase, logger, session });
|
|
551
|
-
return { handled: true, action: "retry" };
|
|
552
|
-
}
|
|
553
188
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
async function becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment }) {
|
|
559
|
-
const pushResult = await incrementalPush({ gitCtx, task, logger, session });
|
|
560
|
-
if (!pushResult) return;
|
|
561
|
-
|
|
562
|
-
// Accumulate commits for PG card lifecycle tracking
|
|
563
|
-
const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
|
|
564
|
-
for (const c of pushResult.commits) accumulateCommit(session, c);
|
|
565
|
-
|
|
566
|
-
session.becaria_commits = [...(session.becaria_commits ?? []), ...pushResult.commits];
|
|
567
|
-
await saveSession(session);
|
|
568
|
-
|
|
569
|
-
if (!repo) return;
|
|
570
|
-
const feedback = session.last_reviewer_feedback || "N/A";
|
|
571
|
-
await dispatchComment({
|
|
572
|
-
repo, prNumber: session.becaria_pr_number, agent: "Coder",
|
|
573
|
-
body: `Issues corregidos:\n${feedback}\n\nCommits:\n${formatCommitList(pushResult.commits)}`,
|
|
574
|
-
becariaConfig: config.becaria
|
|
575
|
-
});
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
async function becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment }) {
|
|
579
|
-
const earlyPr = await earlyPrCreation({ gitCtx, task, logger, session, stageResults });
|
|
580
|
-
if (!earlyPr) return;
|
|
581
|
-
|
|
582
|
-
// Accumulate commits for PG card lifecycle tracking
|
|
583
|
-
const { accumulateCommit } = await import("./planning-game/pipeline-adapter.js");
|
|
584
|
-
for (const c of earlyPr.commits) accumulateCommit(session, c);
|
|
585
|
-
|
|
586
|
-
session.becaria_pr_number = earlyPr.prNumber;
|
|
587
|
-
session.becaria_pr_url = earlyPr.prUrl;
|
|
588
|
-
session.becaria_commits = earlyPr.commits;
|
|
589
|
-
await saveSession(session);
|
|
590
|
-
emitProgress(emitter, makeEvent("becaria:pr-created", { ...eventBase, stage: "becaria" }, {
|
|
591
|
-
message: `Early PR created: #${earlyPr.prNumber}`,
|
|
592
|
-
detail: { prNumber: earlyPr.prNumber, prUrl: earlyPr.prUrl }
|
|
593
|
-
}));
|
|
594
|
-
|
|
595
|
-
if (!repo) return;
|
|
596
|
-
await dispatchComment({
|
|
597
|
-
repo, prNumber: earlyPr.prNumber, agent: "Coder",
|
|
598
|
-
body: `Iteración ${i} completada.\n\nCommits:\n${formatCommitList(earlyPr.commits)}`,
|
|
599
|
-
becariaConfig: config.becaria
|
|
189
|
+
// Solomon couldn't resolve — pause
|
|
190
|
+
await pauseSession(session, {
|
|
191
|
+
question: `Agent "${agent}" rate-limited. Solomon could not resolve.`,
|
|
192
|
+
context: { iteration: i, stage, reason: "rate_limit" }
|
|
600
193
|
});
|
|
194
|
+
return {
|
|
195
|
+
handled: true,
|
|
196
|
+
action: "return",
|
|
197
|
+
result: { paused: true, sessionId: session.id, question: `Agent "${agent}" rate-limited`, context: "rate_limit" }
|
|
198
|
+
};
|
|
601
199
|
}
|
|
602
200
|
|
|
603
|
-
async function handleBecariaEarlyPrOrPush({ becariaEnabled, config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i }) {
|
|
604
|
-
if (!becariaEnabled) return;
|
|
605
|
-
|
|
606
|
-
try {
|
|
607
|
-
const { dispatchComment } = await import("./becaria/dispatch.js");
|
|
608
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
609
|
-
const repo = await detectRepo();
|
|
610
|
-
|
|
611
|
-
if (session.becaria_pr_number) {
|
|
612
|
-
await becariaIncrementalPush({ config, session, gitCtx, task, logger, repo, dispatchComment });
|
|
613
|
-
} else {
|
|
614
|
-
await becariaCreateEarlyPr({ config, session, emitter, eventBase, gitCtx, task, logger, stageResults, i, repo, dispatchComment });
|
|
615
|
-
}
|
|
616
|
-
} catch (err) {
|
|
617
|
-
logger.warn(`BecarIA early PR/push failed (non-blocking): ${err.message}`);
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
201
|
|
|
621
202
|
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
622
203
|
for (const alert of alerts) {
|
|
@@ -629,7 +210,7 @@ function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
|
629
210
|
}
|
|
630
211
|
}
|
|
631
212
|
|
|
632
|
-
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion,
|
|
213
|
+
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
|
|
633
214
|
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
634
215
|
|
|
635
216
|
try {
|
|
@@ -639,19 +220,56 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
|
|
|
639
220
|
|
|
640
221
|
if (rulesResult.alerts.length > 0) {
|
|
641
222
|
emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
|
|
642
|
-
|
|
643
|
-
|
|
223
|
+
// Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
|
|
224
|
+
// On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
|
|
225
|
+
// resolve does Brain escalate to human. Solomon-rules never prompts directly.
|
|
226
|
+
if (!brainCtx?.enabled) {
|
|
227
|
+
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
228
|
+
if (pauseResult) return pauseResult;
|
|
229
|
+
} else if (rulesResult.hasCritical) {
|
|
230
|
+
const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
|
|
231
|
+
brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
|
|
232
|
+
brainCtx.ruleAlerts.push(...criticalAlerts);
|
|
233
|
+
logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
|
|
234
|
+
|
|
235
|
+
const { invokeSolomon } = await import("./orchestrator/solomon-escalation.js");
|
|
236
|
+
const alertSummary = criticalAlerts.map(a => a.message).join("; ");
|
|
237
|
+
const solomonOpinion = await invokeSolomon({
|
|
238
|
+
config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
|
|
239
|
+
conflict: {
|
|
240
|
+
stage: "brain-dilemma",
|
|
241
|
+
task,
|
|
242
|
+
iterationCount: i,
|
|
243
|
+
maxIterations: config.max_iterations,
|
|
244
|
+
dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
|
|
245
|
+
ruleAlerts: criticalAlerts,
|
|
246
|
+
blockingIssues: blockingIssues || [],
|
|
247
|
+
history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (solomonOpinion.action === "pause") {
|
|
252
|
+
logger.info("Brain: Solomon advised pause — escalating to human");
|
|
253
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
|
|
254
|
+
}
|
|
255
|
+
if (solomonOpinion.action === "approve") {
|
|
256
|
+
logger.info("Brain: Solomon advised proceeding — treating as approved");
|
|
257
|
+
return { action: "continue", approved: true };
|
|
258
|
+
}
|
|
259
|
+
// action === "continue" | "subtask" | fallback → Brain continues the loop
|
|
260
|
+
logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
|
|
261
|
+
}
|
|
644
262
|
}
|
|
645
263
|
|
|
646
|
-
if (
|
|
264
|
+
if (ciEnabled && session.ci_pr_number) {
|
|
647
265
|
const alerts = rulesResult.alerts || [];
|
|
648
266
|
const alertMsg = alerts.length > 0
|
|
649
267
|
? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
|
|
650
268
|
: "No anomalies detected";
|
|
651
|
-
await
|
|
269
|
+
await tryCiComment({
|
|
652
270
|
config, session, logger,
|
|
653
271
|
agent: "Solomon",
|
|
654
|
-
body: `Supervisor check
|
|
272
|
+
body: `Supervisor check iteration ${i}: ${alertMsg}`
|
|
655
273
|
});
|
|
656
274
|
}
|
|
657
275
|
} catch (err) {
|
|
@@ -694,59 +312,8 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
694
312
|
return null;
|
|
695
313
|
}
|
|
696
314
|
|
|
697
|
-
function formatBlockingIssues(issues) {
|
|
698
|
-
return issues?.map((x) => `- ${x.id || "ISSUE"} [${x.severity || ""}] ${x.description}`).join("\n") || "";
|
|
699
|
-
}
|
|
700
|
-
|
|
701
|
-
function formatSuggestions(suggestions) {
|
|
702
|
-
return suggestions?.map((s) => {
|
|
703
|
-
const detail = typeof s === "string" ? s : `${s.id || ""} ${s.description || s}`;
|
|
704
|
-
return `- ${detail}`;
|
|
705
|
-
}).join("\n") || "";
|
|
706
|
-
}
|
|
707
|
-
|
|
708
|
-
function buildReviewCommentBody(review, i) {
|
|
709
|
-
const status = review.approved ? "APPROVED" : "REQUEST_CHANGES";
|
|
710
|
-
const blocking = formatBlockingIssues(review.blocking_issues);
|
|
711
|
-
const suggestions = formatSuggestions(review.non_blocking_suggestions);
|
|
712
|
-
let body = `Review iteración ${i}: ${status}`;
|
|
713
|
-
if (blocking) body += `\n\n**Blocking:**\n${blocking}`;
|
|
714
|
-
if (suggestions) body += `\n\n**Suggestions:**\n${suggestions}`;
|
|
715
|
-
return body;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async function handleBecariaReviewDispatch({ becariaEnabled, config, session, review, i, logger }) {
|
|
719
|
-
if (!becariaEnabled || !session.becaria_pr_number) return;
|
|
720
|
-
|
|
721
|
-
try {
|
|
722
|
-
const { dispatchReview, dispatchComment } = await import("./becaria/dispatch.js");
|
|
723
|
-
const { detectRepo } = await import("./becaria/repo.js");
|
|
724
|
-
const repo = await detectRepo();
|
|
725
|
-
if (!repo) return;
|
|
726
|
-
|
|
727
|
-
const bc = config.becaria;
|
|
728
|
-
const reviewEvent = review.approved ? "APPROVE" : "REQUEST_CHANGES";
|
|
729
|
-
const reviewBody = review.approved
|
|
730
|
-
? (review.summary || "Approved")
|
|
731
|
-
: (formatBlockingIssues(review.blocking_issues) || review.summary || "Changes requested");
|
|
732
|
-
|
|
733
|
-
await dispatchReview({
|
|
734
|
-
repo, prNumber: session.becaria_pr_number,
|
|
735
|
-
event: reviewEvent, body: reviewBody, agent: "Reviewer", becariaConfig: bc
|
|
736
|
-
});
|
|
737
|
-
|
|
738
|
-
await dispatchComment({
|
|
739
|
-
repo, prNumber: session.becaria_pr_number, agent: "Reviewer",
|
|
740
|
-
body: buildReviewCommentBody(review, i), becariaConfig: bc
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
logger.info(`BecarIA: dispatched review for PR #${session.becaria_pr_number}`);
|
|
744
|
-
} catch (err) {
|
|
745
|
-
logger.warn(`BecarIA dispatch failed (non-blocking): ${err.message}`);
|
|
746
|
-
}
|
|
747
|
-
}
|
|
748
315
|
|
|
749
|
-
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
316
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
|
|
750
317
|
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
751
318
|
|
|
752
319
|
if (testerEnabled) {
|
|
@@ -755,10 +322,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
755
322
|
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
756
323
|
});
|
|
757
324
|
if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
|
|
758
|
-
if (testerResult.action === "continue")
|
|
325
|
+
if (testerResult.action === "continue") {
|
|
326
|
+
const summary = testerResult.stageResult?.summary || "Tester found issues";
|
|
327
|
+
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
328
|
+
await saveSession(session);
|
|
329
|
+
if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
|
|
330
|
+
return { action: "continue" };
|
|
331
|
+
}
|
|
759
332
|
if (testerResult.stageResult) {
|
|
760
333
|
stageResults.tester = testerResult.stageResult;
|
|
761
|
-
await
|
|
334
|
+
await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
|
|
762
335
|
}
|
|
763
336
|
}
|
|
764
337
|
|
|
@@ -768,10 +341,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
768
341
|
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
769
342
|
});
|
|
770
343
|
if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
|
|
771
|
-
if (securityResult.action === "continue")
|
|
344
|
+
if (securityResult.action === "continue") {
|
|
345
|
+
const summary = securityResult.stageResult?.summary || "Security found issues";
|
|
346
|
+
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
347
|
+
await saveSession(session);
|
|
348
|
+
if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
|
|
349
|
+
return { action: "continue" };
|
|
350
|
+
}
|
|
772
351
|
if (securityResult.stageResult) {
|
|
773
352
|
stageResults.security = securityResult.stageResult;
|
|
774
|
-
await
|
|
353
|
+
await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
|
|
775
354
|
}
|
|
776
355
|
}
|
|
777
356
|
|
|
@@ -782,7 +361,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
782
361
|
});
|
|
783
362
|
if (auditResult.stageResult) {
|
|
784
363
|
stageResults.audit = auditResult.stageResult;
|
|
785
|
-
await
|
|
364
|
+
await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
786
365
|
}
|
|
787
366
|
if (auditResult.action === "retry") {
|
|
788
367
|
// Audit found actionable issues — loop back to coder
|
|
@@ -817,6 +396,31 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
817
396
|
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
818
397
|
await saveSession(session);
|
|
819
398
|
|
|
399
|
+
// --- Journal: write final files ---
|
|
400
|
+
const journalDir = session._journalDir;
|
|
401
|
+
if (journalDir) {
|
|
402
|
+
try {
|
|
403
|
+
await writeIterationsJournal(journalDir, session._journalIterations || []);
|
|
404
|
+
await writeDecisionsJournal(journalDir, session._journalDecisions || []);
|
|
405
|
+
const hasTree = await writeTreeJournal(journalDir, session.session_start_sha);
|
|
406
|
+
|
|
407
|
+
const journalFiles = [...(session._journalFiles || [])];
|
|
408
|
+
if (session._journalIterations?.length) journalFiles.push("iterations.md");
|
|
409
|
+
if (session._journalDecisions?.length) journalFiles.push("decisions.md");
|
|
410
|
+
if (hasTree) journalFiles.push("tree.txt");
|
|
411
|
+
|
|
412
|
+
await writeSummaryJournal(journalDir, {
|
|
413
|
+
task: session.task, result: "APPROVED", sessionId: session.id,
|
|
414
|
+
iterations: i, durationMs: Date.now() - (session._startedAt || Date.now()),
|
|
415
|
+
budget: budgetSummary(), stages: stageResults,
|
|
416
|
+
commits: gitResult?.commits || [], files: journalFiles
|
|
417
|
+
});
|
|
418
|
+
logger.info(`Session journal written to ${journalDir}`);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
logger.warn(`Journal write failed (non-blocking): ${err.message}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
820
424
|
const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
|
|
821
425
|
if (rtkSavings) endDetail.rtk_savings = rtkSavings;
|
|
822
426
|
emitProgress(
|
|
@@ -837,8 +441,13 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
837
441
|
|
|
838
442
|
async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
|
|
839
443
|
session.last_reviewer_feedback = review.blocking_issues
|
|
840
|
-
.map((x) =>
|
|
841
|
-
|
|
444
|
+
.map((x) => {
|
|
445
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
446
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
447
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
448
|
+
return parts.join("\n");
|
|
449
|
+
})
|
|
450
|
+
.join("\n\n");
|
|
842
451
|
session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
|
|
843
452
|
await saveSession(session);
|
|
844
453
|
|
|
@@ -918,29 +527,6 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
918
527
|
applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
|
|
919
528
|
stageResults.triage = triageResult.stageResult;
|
|
920
529
|
|
|
921
|
-
// --- Auto-install skills based on task + project detection ---
|
|
922
|
-
const projectDir = config.projectDir || process.cwd();
|
|
923
|
-
try {
|
|
924
|
-
const osAvailable = await isOpenSkillsAvailable();
|
|
925
|
-
if (osAvailable) {
|
|
926
|
-
const neededSkills = await detectNeededSkills(task, projectDir);
|
|
927
|
-
if (neededSkills.length > 0) {
|
|
928
|
-
const skillResult = await autoInstallSkills(neededSkills, projectDir);
|
|
929
|
-
if (skillResult.installed.length > 0) {
|
|
930
|
-
session.autoInstalledSkills = skillResult.installed;
|
|
931
|
-
}
|
|
932
|
-
emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "triage" }, {
|
|
933
|
-
message: skillResult.installed.length > 0
|
|
934
|
-
? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
|
|
935
|
-
: `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
|
|
936
|
-
detail: skillResult
|
|
937
|
-
}));
|
|
938
|
-
}
|
|
939
|
-
}
|
|
940
|
-
} catch (err) {
|
|
941
|
-
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
530
|
// --- Persist inline domain if provided via --domain flag ---
|
|
945
531
|
if (flags?.domain) {
|
|
946
532
|
try {
|
|
@@ -1079,13 +665,43 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
1079
665
|
// --- Researcher → Planner ---
|
|
1080
666
|
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
1081
667
|
|
|
668
|
+
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
669
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
670
|
+
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
671
|
+
|
|
672
|
+
// --- Auto-install skills based on task + planner output + project detection ---
|
|
673
|
+
// Runs AFTER triage and planner so that the planned task text (which includes
|
|
674
|
+
// planner output like implementation steps) is available for keyword detection.
|
|
675
|
+
// This ensures greenfield projects with no package.json still get correct skills.
|
|
676
|
+
const skillProjectDir = updatedConfig.projectDir || process.cwd();
|
|
677
|
+
try {
|
|
678
|
+
const osAvailable = await isOpenSkillsAvailable();
|
|
679
|
+
if (osAvailable) {
|
|
680
|
+
const neededSkills = await detectNeededSkills(plannedTask, skillProjectDir);
|
|
681
|
+
if (neededSkills.length > 0) {
|
|
682
|
+
const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
|
|
683
|
+
if (skillResult.installed.length > 0) {
|
|
684
|
+
session.autoInstalledSkills = skillResult.installed;
|
|
685
|
+
}
|
|
686
|
+
emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
|
|
687
|
+
message: skillResult.installed.length > 0
|
|
688
|
+
? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
|
|
689
|
+
: `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
|
|
690
|
+
detail: skillResult
|
|
691
|
+
}));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
} catch (err) {
|
|
695
|
+
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
696
|
+
}
|
|
697
|
+
|
|
1082
698
|
return { plannedTask, updatedConfig };
|
|
1083
699
|
}
|
|
1084
700
|
|
|
1085
|
-
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
|
|
1086
|
-
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
701
|
+
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|
|
702
|
+
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
|
|
1087
703
|
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
1088
|
-
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger });
|
|
704
|
+
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
|
|
1089
705
|
if (coderStandby.handled) {
|
|
1090
706
|
return coderStandby.action === "return"
|
|
1091
707
|
? { action: "return", result: coderStandby.result }
|
|
@@ -1095,7 +711,7 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
|
|
|
1095
711
|
if (pipelineFlags.refactorerEnabled) {
|
|
1096
712
|
const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
1097
713
|
if (refResult?.action === "pause") return { action: "return", result: refResult.result };
|
|
1098
|
-
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger });
|
|
714
|
+
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
|
|
1099
715
|
if (refStandby.handled) {
|
|
1100
716
|
return refStandby.action === "return"
|
|
1101
717
|
? { action: "return", result: refStandby.result }
|
|
@@ -1188,7 +804,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
1188
804
|
if (sonarResult.action === "continue") return { action: "continue" };
|
|
1189
805
|
if (sonarResult.stageResult) {
|
|
1190
806
|
stageResults.sonar = sonarResult.stageResult;
|
|
1191
|
-
await
|
|
807
|
+
await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
|
|
1192
808
|
}
|
|
1193
809
|
}
|
|
1194
810
|
|
|
@@ -1216,7 +832,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
1216
832
|
return { action: "ok" };
|
|
1217
833
|
}
|
|
1218
834
|
|
|
1219
|
-
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion }) {
|
|
835
|
+
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
|
|
1220
836
|
if (!pipelineFlags.reviewerEnabled) {
|
|
1221
837
|
return {
|
|
1222
838
|
action: "ok",
|
|
@@ -1226,14 +842,29 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
1226
842
|
|
|
1227
843
|
const reviewerResult = await runReviewerStage({
|
|
1228
844
|
reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
|
|
1229
|
-
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
|
|
845
|
+
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
|
|
1230
846
|
});
|
|
1231
847
|
if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
|
|
1232
|
-
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger });
|
|
848
|
+
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
|
|
1233
849
|
if (revStandby.handled) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
850
|
+
if (revStandby.action === "return") return { action: "return", result: revStandby.result };
|
|
851
|
+
if (revStandby.action === "skip") {
|
|
852
|
+
// Solomon said skip review — treat as approved
|
|
853
|
+
return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
|
|
854
|
+
}
|
|
855
|
+
if (revStandby.action === "retry_reviewer_only") {
|
|
856
|
+
// Retry just the reviewer — use alternative agent if Solomon recommended one
|
|
857
|
+
let retryReviewerRole = reviewerRole;
|
|
858
|
+
const alt = session._alternative_agent;
|
|
859
|
+
if (alt?.stage === "reviewer" && alt?.provider) {
|
|
860
|
+
const { createAgent } = await import("./agents/index.js");
|
|
861
|
+
retryReviewerRole = { provider: alt.provider, model: null };
|
|
862
|
+
logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
|
|
863
|
+
delete session._alternative_agent;
|
|
864
|
+
}
|
|
865
|
+
return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
|
|
866
|
+
}
|
|
867
|
+
return { action: "retry" };
|
|
1237
868
|
}
|
|
1238
869
|
if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
|
|
1239
870
|
return { action: "ok", review: reviewerResult.review };
|
|
@@ -1243,7 +874,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1243
874
|
session.reviewer_retry_count = 0;
|
|
1244
875
|
const postLoopResult = await handlePostLoopStages({
|
|
1245
876
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
1246
|
-
|
|
877
|
+
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
|
|
1247
878
|
});
|
|
1248
879
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
1249
880
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
@@ -1253,7 +884,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1253
884
|
}
|
|
1254
885
|
|
|
1255
886
|
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
|
|
1256
|
-
|
|
887
|
+
const budget = budgetSummary();
|
|
1257
888
|
const solomonResult = await invokeSolomon({
|
|
1258
889
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
1259
890
|
iteration: config.max_iterations,
|
|
@@ -1262,17 +893,26 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1262
893
|
task,
|
|
1263
894
|
iterationCount: config.max_iterations,
|
|
1264
895
|
maxIterations: config.max_iterations,
|
|
896
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
1265
897
|
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
|
|
1266
898
|
}
|
|
1267
899
|
});
|
|
1268
900
|
|
|
901
|
+
// Solomon approved the work — treat as successful completion
|
|
902
|
+
if (solomonResult.action === "approve") {
|
|
903
|
+
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
904
|
+
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Solomon says continue — extend iterations
|
|
1269
908
|
if (solomonResult.action === "continue") {
|
|
1270
909
|
if (solomonResult.humanGuidance) {
|
|
1271
|
-
session.last_reviewer_feedback = `
|
|
910
|
+
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1272
911
|
}
|
|
1273
912
|
session.reviewer_retry_count = 0;
|
|
1274
913
|
await saveSession(session);
|
|
1275
|
-
|
|
914
|
+
const extraIterations = solomonResult.extraIterations || config.max_iterations;
|
|
915
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
|
|
1276
916
|
}
|
|
1277
917
|
|
|
1278
918
|
if (solomonResult.action === "pause") {
|
|
@@ -1283,7 +923,7 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1283
923
|
return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
|
|
1284
924
|
}
|
|
1285
925
|
|
|
1286
|
-
// Solomon
|
|
926
|
+
// Solomon couldn't resolve — fail
|
|
1287
927
|
session.budget = budgetSummary();
|
|
1288
928
|
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
1289
929
|
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
@@ -1320,6 +960,44 @@ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
|
1320
960
|
}
|
|
1321
961
|
|
|
1322
962
|
async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
963
|
+
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
964
|
+
const initProjectDir = config.projectDir || process.cwd();
|
|
965
|
+
await autoInit(initProjectDir, logger);
|
|
966
|
+
|
|
967
|
+
// Smart role assignment: detect installed AIs and assign to roles
|
|
968
|
+
// Only runs if: (a) no roles configured AND (b) not in test environment
|
|
969
|
+
const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
|
|
970
|
+
if (needsAssignment) {
|
|
971
|
+
try {
|
|
972
|
+
const { autoAssignRoles, applyRoleAssignments } = await import("./utils/role-assigner.js");
|
|
973
|
+
const { assignments } = await autoAssignRoles(logger);
|
|
974
|
+
if (assignments) config = applyRoleAssignments({ ...config }, assignments);
|
|
975
|
+
} catch { /* non-blocking — defaults will be used */ }
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
|
|
979
|
+
// When running from a subdirectory of a git repo, use relative path as scope
|
|
980
|
+
let diffScope = config.projectDir || null;
|
|
981
|
+
if (!diffScope) {
|
|
982
|
+
try {
|
|
983
|
+
const { execSync } = await import("node:child_process");
|
|
984
|
+
const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
985
|
+
const cwd = process.cwd();
|
|
986
|
+
if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
|
|
987
|
+
diffScope = cwd.slice(repoRoot.length + 1);
|
|
988
|
+
logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
|
|
989
|
+
}
|
|
990
|
+
} catch { /* git not available */ }
|
|
991
|
+
}
|
|
992
|
+
setDiffProjectDir(diffScope);
|
|
993
|
+
|
|
994
|
+
// Auto-detect Chrome DevTools MCP
|
|
995
|
+
const { detectDevToolsMcp } = await import("./webperf/devtools-detect.js");
|
|
996
|
+
const devToolsAvailable = await detectDevToolsMcp(logger);
|
|
997
|
+
if (devToolsAvailable) {
|
|
998
|
+
config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
|
|
999
|
+
}
|
|
1000
|
+
|
|
1323
1001
|
const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
|
|
1324
1002
|
ctx.askQuestion = askQuestion;
|
|
1325
1003
|
ctx.pgTaskId = pgTaskId;
|
|
@@ -1340,23 +1018,19 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1340
1018
|
ctx.trackBudget = trackBudget;
|
|
1341
1019
|
|
|
1342
1020
|
// --- RTK detection ---
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
const
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1357
|
-
detail: { version: rtkResult.version, executorType: "local" }
|
|
1358
|
-
}));
|
|
1359
|
-
}
|
|
1021
|
+
const rtkResult = await detectRtk();
|
|
1022
|
+
if (rtkResult.available) {
|
|
1023
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1024
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
1025
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
1026
|
+
setDiffRunner(rtkRunner);
|
|
1027
|
+
setGitRunner(rtkRunner);
|
|
1028
|
+
ctx.rtkTracker = rtkTracker;
|
|
1029
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
1030
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1031
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1032
|
+
detail: { version: rtkResult.version, executorType: "local" }
|
|
1033
|
+
}));
|
|
1360
1034
|
}
|
|
1361
1035
|
|
|
1362
1036
|
// --- HU Board auto-start ---
|
|
@@ -1377,26 +1051,18 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1377
1051
|
ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
1378
1052
|
ctx.eventBase.sessionId = ctx.session.id;
|
|
1379
1053
|
|
|
1054
|
+
// Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
|
|
1055
|
+
const { createBrainContext, isBrainEnabled } = await import("./orchestrator/brain-coordinator.js");
|
|
1056
|
+
ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
|
|
1057
|
+
if (ctx.brainCtx.enabled) {
|
|
1058
|
+
logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1380
1061
|
const { initPgAdapter } = await import("./planning-game/pipeline-adapter.js");
|
|
1381
1062
|
const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
|
|
1382
1063
|
ctx.pgCard = pgAdapterResult.pgCard;
|
|
1383
1064
|
ctx.session.pg_card = ctx.pgCard || null;
|
|
1384
1065
|
|
|
1385
|
-
// --- Proxy startup ---
|
|
1386
|
-
if (config.proxy?.enabled !== false) {
|
|
1387
|
-
try {
|
|
1388
|
-
const proxyResult = await startProxy({ config: config.proxy || {}, sessionId: ctx.session.id });
|
|
1389
|
-
ctx.proxyPort = proxyResult.port;
|
|
1390
|
-
logger.info(`Proxy started on 127.0.0.1:${proxyResult.port}`);
|
|
1391
|
-
emitProgress(emitter, makeEvent("proxy:started", ctx.eventBase, {
|
|
1392
|
-
message: `Proxy started on port ${proxyResult.port}`,
|
|
1393
|
-
detail: { port: proxyResult.port, baseUrls: proxyResult.baseUrls }
|
|
1394
|
-
}));
|
|
1395
|
-
} catch (err) {
|
|
1396
|
-
logger.warn(`Proxy startup failed (non-blocking): ${err.message}`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
1066
|
emitProgress(
|
|
1401
1067
|
emitter,
|
|
1402
1068
|
makeEvent("session:start", ctx.eventBase, {
|
|
@@ -1412,6 +1078,38 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1412
1078
|
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1413
1079
|
ctx.config = preLoopResult.updatedConfig;
|
|
1414
1080
|
|
|
1081
|
+
// --- Session Journal: persist pre-loop outputs + display plan summary ---
|
|
1082
|
+
const reportDir = ctx.config.output?.report_dir || ".reviews";
|
|
1083
|
+
try {
|
|
1084
|
+
ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
|
|
1085
|
+
const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
|
|
1086
|
+
ctx.journalFiles = journalFiles;
|
|
1087
|
+
ctx.journalIterations = [];
|
|
1088
|
+
ctx.journalDecisions = [];
|
|
1089
|
+
|
|
1090
|
+
// Attach journal state to session so finalizeApprovedSession can access it
|
|
1091
|
+
ctx.session._journalDir = ctx.journalDir;
|
|
1092
|
+
ctx.session._journalFiles = journalFiles;
|
|
1093
|
+
ctx.session._journalIterations = ctx.journalIterations;
|
|
1094
|
+
ctx.session._journalDecisions = ctx.journalDecisions;
|
|
1095
|
+
ctx.session._startedAt = ctx.startedAt;
|
|
1096
|
+
|
|
1097
|
+
// Display plan summary in console before iteration loop
|
|
1098
|
+
const planSummary = buildPlanSummary({
|
|
1099
|
+
pipelineFlags: ctx.pipelineFlags,
|
|
1100
|
+
config: ctx.config,
|
|
1101
|
+
stageResults: ctx.stageResults,
|
|
1102
|
+
task
|
|
1103
|
+
});
|
|
1104
|
+
console.log(planSummary);
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
logger.warn(`Journal init failed (non-blocking): ${err.message}`);
|
|
1107
|
+
ctx.journalDir = null;
|
|
1108
|
+
ctx.journalFiles = [];
|
|
1109
|
+
ctx.journalIterations = [];
|
|
1110
|
+
ctx.journalDecisions = [];
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1415
1113
|
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
1416
1114
|
const projectDir = ctx.config.projectDir || process.cwd();
|
|
1417
1115
|
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
@@ -1424,7 +1122,7 @@ async function runSingleIteration(ctx) {
|
|
|
1424
1122
|
const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
|
|
1425
1123
|
|
|
1426
1124
|
const iterStart = Date.now();
|
|
1427
|
-
const
|
|
1125
|
+
const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
|
|
1428
1126
|
logger.setContext({ iteration: i, stage: "iteration" });
|
|
1429
1127
|
|
|
1430
1128
|
const reviewerRetryCount = session.reviewer_retry_count || 0;
|
|
@@ -1440,7 +1138,7 @@ async function runSingleIteration(ctx) {
|
|
|
1440
1138
|
const crResult = await runCoderAndRefactorerStages({
|
|
1441
1139
|
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
|
|
1442
1140
|
pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
|
|
1443
|
-
plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i
|
|
1141
|
+
plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
|
|
1444
1142
|
});
|
|
1445
1143
|
if (crResult.action === "return" || crResult.action === "retry") return crResult;
|
|
1446
1144
|
|
|
@@ -1455,15 +1153,16 @@ async function runSingleIteration(ctx) {
|
|
|
1455
1153
|
});
|
|
1456
1154
|
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
1457
1155
|
|
|
1458
|
-
await
|
|
1459
|
-
|
|
1156
|
+
await handleCiEarlyPrOrPush({
|
|
1157
|
+
ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
|
|
1460
1158
|
stageResults: ctx.stageResults, i
|
|
1461
1159
|
});
|
|
1462
1160
|
|
|
1463
1161
|
const revResult = await runReviewerGateStage({
|
|
1464
1162
|
pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
|
|
1465
1163
|
session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
|
|
1466
|
-
repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion
|
|
1164
|
+
repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
|
|
1165
|
+
brainCtx: ctx.brainCtx
|
|
1467
1166
|
});
|
|
1468
1167
|
if (revResult.action === "return" || revResult.action === "retry") return revResult;
|
|
1469
1168
|
const review = revResult.review;
|
|
@@ -1474,13 +1173,26 @@ async function runSingleIteration(ctx) {
|
|
|
1474
1173
|
}));
|
|
1475
1174
|
session.standby_retry_count = 0;
|
|
1476
1175
|
|
|
1176
|
+
// --- Journal: record iteration ---
|
|
1177
|
+
if (ctx.journalIterations) {
|
|
1178
|
+
ctx.journalIterations.push(formatIteration({
|
|
1179
|
+
iteration: i,
|
|
1180
|
+
coderSummary: ctx.stageResults.coder?.summary || null,
|
|
1181
|
+
reviewerSummary: review?.approved ? `Approved: ${review.raw_summary || ""}` : `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`,
|
|
1182
|
+
sonarSummary: ctx.stageResults.sonar?.summary || null,
|
|
1183
|
+
testerSummary: ctx.stageResults.tester?.summary || null,
|
|
1184
|
+
securitySummary: ctx.stageResults.security?.summary || null,
|
|
1185
|
+
durationMs: iterDuration
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1477
1189
|
const solomonResult = await handleSolomonCheck({
|
|
1478
1190
|
config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
|
|
1479
|
-
|
|
1191
|
+
ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
|
|
1480
1192
|
});
|
|
1481
1193
|
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
1482
1194
|
|
|
1483
|
-
await
|
|
1195
|
+
await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
|
|
1484
1196
|
|
|
1485
1197
|
if (review.approved) {
|
|
1486
1198
|
const approvedResult = await handleApprovedReview({
|
|
@@ -1500,8 +1212,13 @@ async function runSingleIteration(ctx) {
|
|
|
1500
1212
|
} else {
|
|
1501
1213
|
// Solomon is enabled — feed back the blocking issues for the next coder iteration
|
|
1502
1214
|
session.last_reviewer_feedback = review.blocking_issues
|
|
1503
|
-
.map((x) =>
|
|
1504
|
-
|
|
1215
|
+
.map((x) => {
|
|
1216
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
1217
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
1218
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
1219
|
+
return parts.join("\n");
|
|
1220
|
+
})
|
|
1221
|
+
.join("\n\n");
|
|
1505
1222
|
await saveSession(session);
|
|
1506
1223
|
}
|
|
1507
1224
|
|
|
@@ -1559,14 +1276,92 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1559
1276
|
ctx.eventBase.iteration = i;
|
|
1560
1277
|
ctx.iteration = i;
|
|
1561
1278
|
|
|
1562
|
-
|
|
1279
|
+
let iterResult;
|
|
1280
|
+
try {
|
|
1281
|
+
iterResult = await runSingleIteration(ctx);
|
|
1282
|
+
} catch (stageError) {
|
|
1283
|
+
// ANY unhandled error in a stage = out of normal flow → Solomon decides
|
|
1284
|
+
logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
|
|
1285
|
+
const solomonResult = await invokeSolomon({
|
|
1286
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
|
|
1287
|
+
stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
|
|
1288
|
+
conflict: {
|
|
1289
|
+
stage: "stage_error",
|
|
1290
|
+
task: loopTask,
|
|
1291
|
+
iterationCount: i,
|
|
1292
|
+
maxIterations: ctx.config.max_iterations,
|
|
1293
|
+
budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
|
|
1294
|
+
history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
|
|
1295
|
+
}
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
if (solomonResult.action === "approve") {
|
|
1299
|
+
logger.info("Solomon approved despite stage error");
|
|
1300
|
+
return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
|
|
1301
|
+
}
|
|
1302
|
+
if (solomonResult.action === "continue") {
|
|
1303
|
+
if (solomonResult.humanGuidance) {
|
|
1304
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1305
|
+
}
|
|
1306
|
+
continue; // next iteration
|
|
1307
|
+
}
|
|
1308
|
+
if (solomonResult.action === "pause") {
|
|
1309
|
+
return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
|
|
1310
|
+
}
|
|
1311
|
+
// Solomon couldn't resolve — fail
|
|
1312
|
+
await markSessionStatus(ctx.session, "failed");
|
|
1313
|
+
return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1563
1316
|
if (iterResult.action === "return") {
|
|
1564
1317
|
return iterResult.result;
|
|
1565
1318
|
}
|
|
1566
1319
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1567
1320
|
}
|
|
1568
1321
|
|
|
1322
|
+
// Solomon decides whether to extend iterations or stop
|
|
1569
1323
|
const maxIterResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1324
|
+
|
|
1325
|
+
// Solomon said "continue" — extend iterations and keep going
|
|
1326
|
+
if (maxIterResult.reason === "max_iterations_extended") {
|
|
1327
|
+
const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
|
|
1328
|
+
ctx.config.max_iterations += extra;
|
|
1329
|
+
logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
|
|
1330
|
+
|
|
1331
|
+
if (maxIterResult.humanGuidance) {
|
|
1332
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
// Continue the loop
|
|
1336
|
+
while (i < ctx.config.max_iterations) {
|
|
1337
|
+
i += 1;
|
|
1338
|
+
const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
|
|
1339
|
+
|
|
1340
|
+
const cpResult = await handleCheckpoint({
|
|
1341
|
+
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1342
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1343
|
+
});
|
|
1344
|
+
if (cpResult.action === "stop") return cpResult.result;
|
|
1345
|
+
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1346
|
+
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1347
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1348
|
+
|
|
1349
|
+
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1350
|
+
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
1351
|
+
|
|
1352
|
+
ctx.eventBase.iteration = i;
|
|
1353
|
+
ctx.iteration = i;
|
|
1354
|
+
|
|
1355
|
+
const iterResult = await runSingleIteration(ctx);
|
|
1356
|
+
if (iterResult.action === "return") return iterResult.result;
|
|
1357
|
+
if (iterResult.action === "retry") { i -= 1; }
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Extended iterations also exhausted — final Solomon call
|
|
1361
|
+
const finalResult = await handleMaxIterationsReached({ session: ctx.session, budgetSummary: ctx.budgetSummary, emitter, eventBase: ctx.eventBase, config: ctx.config, stageResults: ctx.stageResults, logger, askQuestion, task: loopTask, rtkTracker: ctx.rtkTracker });
|
|
1362
|
+
return finalResult;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1570
1365
|
return maxIterResult;
|
|
1571
1366
|
}
|
|
1572
1367
|
|
|
@@ -1577,7 +1372,29 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1577
1372
|
return handleDryRun({ task, config, flags, emitter, pipelineFlags });
|
|
1578
1373
|
}
|
|
1579
1374
|
|
|
1580
|
-
|
|
1375
|
+
let ctx;
|
|
1376
|
+
try {
|
|
1377
|
+
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
1378
|
+
} catch (initError) {
|
|
1379
|
+
// Pre-loop stage failure → Solomon decides
|
|
1380
|
+
logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
|
|
1381
|
+
const tempSession = { id: "init-error", task, status: "failed" };
|
|
1382
|
+
const solomonResult = await invokeSolomon({
|
|
1383
|
+
config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
|
|
1384
|
+
stage: "init_error", askQuestion, session: tempSession, iteration: 0,
|
|
1385
|
+
conflict: {
|
|
1386
|
+
stage: "init_error",
|
|
1387
|
+
task,
|
|
1388
|
+
iterationCount: 0,
|
|
1389
|
+
maxIterations: config.max_iterations || 5,
|
|
1390
|
+
history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
if (solomonResult.action === "pause") {
|
|
1394
|
+
return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
|
|
1395
|
+
}
|
|
1396
|
+
throw initError; // Solomon couldn't resolve — propagate
|
|
1397
|
+
}
|
|
1581
1398
|
|
|
1582
1399
|
try {
|
|
1583
1400
|
// --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
|
|
@@ -1667,14 +1484,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1667
1484
|
await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
|
|
1668
1485
|
return result;
|
|
1669
1486
|
} finally {
|
|
1670
|
-
// --- Proxy shutdown ---
|
|
1671
|
-
try {
|
|
1672
|
-
await stopProxy();
|
|
1673
|
-
if (ctx.proxyPort) {
|
|
1674
|
-
logger.info("Proxy stopped");
|
|
1675
|
-
}
|
|
1676
|
-
} catch { /* non-blocking */ }
|
|
1677
|
-
|
|
1678
1487
|
// --- Cleanup auto-installed skills ---
|
|
1679
1488
|
const autoSkills = ctx.session?.autoInstalledSkills;
|
|
1680
1489
|
if (autoSkills && autoSkills.length > 0) {
|