karajan-code 1.59.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/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 +10 -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/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/reviewer-stage.js +16 -5
- package/src/orchestrator/stages/sonar-stage.js +24 -1
- package/src/orchestrator/verification-gate.js +146 -0
- package/src/orchestrator.js +428 -664
- 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
|
-
|
|
145
|
-
emitProgress(
|
|
146
|
-
emitter,
|
|
147
|
-
makeEvent("dry-run:summary", { sessionId: null, iteration: 0, stage: "dry-run", startedAt: Date.now() }, {
|
|
148
|
-
message: "Dry-run complete — no changes made",
|
|
149
|
-
detail: summary
|
|
150
|
-
})
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
return summary;
|
|
154
|
-
}
|
|
155
41
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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
115
|
|
|
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
116
|
|
|
403
|
-
async function
|
|
404
|
-
if (
|
|
405
|
-
return {
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
const elapsedStr = elapsedMinutes.toFixed(1);
|
|
409
|
-
const stagesCompleted = Object.keys(stageResults).join(", ") || "none";
|
|
410
|
-
|
|
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
|
-
|
|
416
|
-
if (decision.autoContinue) {
|
|
417
|
-
emitProgress(
|
|
418
|
-
emitter,
|
|
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
155
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
async function checkBudgetExceeded({ budgetTracker, config, session, emitter, eventBase, i, budgetLimit, budgetSummary }) {
|
|
502
|
-
if (!budgetTracker.isOverBudget(config?.max_budget_usd)) return;
|
|
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
|
-
|
|
554
|
-
function formatCommitList(commits) {
|
|
555
|
-
return commits.map((c) => `- \`${c.hash.slice(0, 7)}\` ${c.message}`).join("\n");
|
|
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
188
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
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 }) {
|
|
633
214
|
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
634
215
|
|
|
635
216
|
try {
|
|
@@ -643,15 +224,15 @@ async function handleSolomonCheck({ config, session, emitter, eventBase, logger,
|
|
|
643
224
|
if (pauseResult) return pauseResult;
|
|
644
225
|
}
|
|
645
226
|
|
|
646
|
-
if (
|
|
227
|
+
if (ciEnabled && session.ci_pr_number) {
|
|
647
228
|
const alerts = rulesResult.alerts || [];
|
|
648
229
|
const alertMsg = alerts.length > 0
|
|
649
230
|
? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
|
|
650
231
|
: "No anomalies detected";
|
|
651
|
-
await
|
|
232
|
+
await tryCiComment({
|
|
652
233
|
config, session, logger,
|
|
653
234
|
agent: "Solomon",
|
|
654
|
-
body: `Supervisor check
|
|
235
|
+
body: `Supervisor check iteration ${i}: ${alertMsg}`
|
|
655
236
|
});
|
|
656
237
|
}
|
|
657
238
|
} catch (err) {
|
|
@@ -694,59 +275,8 @@ async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i
|
|
|
694
275
|
return null;
|
|
695
276
|
}
|
|
696
277
|
|
|
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
278
|
|
|
749
|
-
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
279
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger }) {
|
|
750
280
|
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
751
281
|
|
|
752
282
|
if (testerEnabled) {
|
|
@@ -755,10 +285,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
755
285
|
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
756
286
|
});
|
|
757
287
|
if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
|
|
758
|
-
if (testerResult.action === "continue")
|
|
288
|
+
if (testerResult.action === "continue") {
|
|
289
|
+
const summary = testerResult.stageResult?.summary || "Tester found issues";
|
|
290
|
+
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
291
|
+
await saveSession(session);
|
|
292
|
+
if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
|
|
293
|
+
return { action: "continue" };
|
|
294
|
+
}
|
|
759
295
|
if (testerResult.stageResult) {
|
|
760
296
|
stageResults.tester = testerResult.stageResult;
|
|
761
|
-
await
|
|
297
|
+
await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
|
|
762
298
|
}
|
|
763
299
|
}
|
|
764
300
|
|
|
@@ -768,10 +304,16 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
768
304
|
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
769
305
|
});
|
|
770
306
|
if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
|
|
771
|
-
if (securityResult.action === "continue")
|
|
307
|
+
if (securityResult.action === "continue") {
|
|
308
|
+
const summary = securityResult.stageResult?.summary || "Security found issues";
|
|
309
|
+
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
310
|
+
await saveSession(session);
|
|
311
|
+
if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
|
|
312
|
+
return { action: "continue" };
|
|
313
|
+
}
|
|
772
314
|
if (securityResult.stageResult) {
|
|
773
315
|
stageResults.security = securityResult.stageResult;
|
|
774
|
-
await
|
|
316
|
+
await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
|
|
775
317
|
}
|
|
776
318
|
}
|
|
777
319
|
|
|
@@ -782,7 +324,7 @@ async function handlePostLoopStages({ config, session, emitter, eventBase, coder
|
|
|
782
324
|
});
|
|
783
325
|
if (auditResult.stageResult) {
|
|
784
326
|
stageResults.audit = auditResult.stageResult;
|
|
785
|
-
await
|
|
327
|
+
await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
786
328
|
}
|
|
787
329
|
if (auditResult.action === "retry") {
|
|
788
330
|
// Audit found actionable issues — loop back to coder
|
|
@@ -817,6 +359,31 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
817
359
|
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
818
360
|
await saveSession(session);
|
|
819
361
|
|
|
362
|
+
// --- Journal: write final files ---
|
|
363
|
+
const journalDir = session._journalDir;
|
|
364
|
+
if (journalDir) {
|
|
365
|
+
try {
|
|
366
|
+
await writeIterationsJournal(journalDir, session._journalIterations || []);
|
|
367
|
+
await writeDecisionsJournal(journalDir, session._journalDecisions || []);
|
|
368
|
+
const hasTree = await writeTreeJournal(journalDir, session.session_start_sha);
|
|
369
|
+
|
|
370
|
+
const journalFiles = [...(session._journalFiles || [])];
|
|
371
|
+
if (session._journalIterations?.length) journalFiles.push("iterations.md");
|
|
372
|
+
if (session._journalDecisions?.length) journalFiles.push("decisions.md");
|
|
373
|
+
if (hasTree) journalFiles.push("tree.txt");
|
|
374
|
+
|
|
375
|
+
await writeSummaryJournal(journalDir, {
|
|
376
|
+
task: session.task, result: "APPROVED", sessionId: session.id,
|
|
377
|
+
iterations: i, durationMs: Date.now() - (session._startedAt || Date.now()),
|
|
378
|
+
budget: budgetSummary(), stages: stageResults,
|
|
379
|
+
commits: gitResult?.commits || [], files: journalFiles
|
|
380
|
+
});
|
|
381
|
+
logger.info(`Session journal written to ${journalDir}`);
|
|
382
|
+
} catch (err) {
|
|
383
|
+
logger.warn(`Journal write failed (non-blocking): ${err.message}`);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
820
387
|
const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
|
|
821
388
|
if (rtkSavings) endDetail.rtk_savings = rtkSavings;
|
|
822
389
|
emitProgress(
|
|
@@ -837,8 +404,13 @@ async function finalizeApprovedSession({ config, gitCtx, task, logger, session,
|
|
|
837
404
|
|
|
838
405
|
async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
|
|
839
406
|
session.last_reviewer_feedback = review.blocking_issues
|
|
840
|
-
.map((x) =>
|
|
841
|
-
|
|
407
|
+
.map((x) => {
|
|
408
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
409
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
410
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
411
|
+
return parts.join("\n");
|
|
412
|
+
})
|
|
413
|
+
.join("\n\n");
|
|
842
414
|
session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
|
|
843
415
|
await saveSession(session);
|
|
844
416
|
|
|
@@ -918,29 +490,6 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
918
490
|
applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
|
|
919
491
|
stageResults.triage = triageResult.stageResult;
|
|
920
492
|
|
|
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
493
|
// --- Persist inline domain if provided via --domain flag ---
|
|
945
494
|
if (flags?.domain) {
|
|
946
495
|
try {
|
|
@@ -1079,13 +628,43 @@ async function runPreLoopStages({ config, logger, emitter, eventBase, session, f
|
|
|
1079
628
|
// --- Researcher → Planner ---
|
|
1080
629
|
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion });
|
|
1081
630
|
|
|
631
|
+
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
632
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
633
|
+
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
634
|
+
|
|
635
|
+
// --- Auto-install skills based on task + planner output + project detection ---
|
|
636
|
+
// Runs AFTER triage and planner so that the planned task text (which includes
|
|
637
|
+
// planner output like implementation steps) is available for keyword detection.
|
|
638
|
+
// This ensures greenfield projects with no package.json still get correct skills.
|
|
639
|
+
const skillProjectDir = updatedConfig.projectDir || process.cwd();
|
|
640
|
+
try {
|
|
641
|
+
const osAvailable = await isOpenSkillsAvailable();
|
|
642
|
+
if (osAvailable) {
|
|
643
|
+
const neededSkills = await detectNeededSkills(plannedTask, skillProjectDir);
|
|
644
|
+
if (neededSkills.length > 0) {
|
|
645
|
+
const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
|
|
646
|
+
if (skillResult.installed.length > 0) {
|
|
647
|
+
session.autoInstalledSkills = skillResult.installed;
|
|
648
|
+
}
|
|
649
|
+
emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
|
|
650
|
+
message: skillResult.installed.length > 0
|
|
651
|
+
? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
|
|
652
|
+
: `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
|
|
653
|
+
detail: skillResult
|
|
654
|
+
}));
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
} catch (err) {
|
|
658
|
+
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
659
|
+
}
|
|
660
|
+
|
|
1082
661
|
return { plannedTask, updatedConfig };
|
|
1083
662
|
}
|
|
1084
663
|
|
|
1085
664
|
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i }) {
|
|
1086
665
|
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
1087
666
|
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
1088
|
-
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger });
|
|
667
|
+
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
|
|
1089
668
|
if (coderStandby.handled) {
|
|
1090
669
|
return coderStandby.action === "return"
|
|
1091
670
|
? { action: "return", result: coderStandby.result }
|
|
@@ -1095,7 +674,7 @@ async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refac
|
|
|
1095
674
|
if (pipelineFlags.refactorerEnabled) {
|
|
1096
675
|
const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
1097
676
|
if (refResult?.action === "pause") return { action: "return", result: refResult.result };
|
|
1098
|
-
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger });
|
|
677
|
+
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
|
|
1099
678
|
if (refStandby.handled) {
|
|
1100
679
|
return refStandby.action === "return"
|
|
1101
680
|
? { action: "return", result: refStandby.result }
|
|
@@ -1188,7 +767,7 @@ async function runQualityGateStages({ config, logger, emitter, eventBase, sessio
|
|
|
1188
767
|
if (sonarResult.action === "continue") return { action: "continue" };
|
|
1189
768
|
if (sonarResult.stageResult) {
|
|
1190
769
|
stageResults.sonar = sonarResult.stageResult;
|
|
1191
|
-
await
|
|
770
|
+
await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
|
|
1192
771
|
}
|
|
1193
772
|
}
|
|
1194
773
|
|
|
@@ -1229,11 +808,26 @@ async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logge
|
|
|
1229
808
|
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion
|
|
1230
809
|
});
|
|
1231
810
|
if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
|
|
1232
|
-
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger });
|
|
811
|
+
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
|
|
1233
812
|
if (revStandby.handled) {
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
813
|
+
if (revStandby.action === "return") return { action: "return", result: revStandby.result };
|
|
814
|
+
if (revStandby.action === "skip") {
|
|
815
|
+
// Solomon said skip review — treat as approved
|
|
816
|
+
return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
|
|
817
|
+
}
|
|
818
|
+
if (revStandby.action === "retry_reviewer_only") {
|
|
819
|
+
// Retry just the reviewer — use alternative agent if Solomon recommended one
|
|
820
|
+
let retryReviewerRole = reviewerRole;
|
|
821
|
+
const alt = session._alternative_agent;
|
|
822
|
+
if (alt?.stage === "reviewer" && alt?.provider) {
|
|
823
|
+
const { createAgent } = await import("./agents/index.js");
|
|
824
|
+
retryReviewerRole = { provider: alt.provider, model: null };
|
|
825
|
+
logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
|
|
826
|
+
delete session._alternative_agent;
|
|
827
|
+
}
|
|
828
|
+
return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
|
|
829
|
+
}
|
|
830
|
+
return { action: "retry" };
|
|
1237
831
|
}
|
|
1238
832
|
if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
|
|
1239
833
|
return { action: "ok", review: reviewerResult.review };
|
|
@@ -1243,7 +837,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1243
837
|
session.reviewer_retry_count = 0;
|
|
1244
838
|
const postLoopResult = await handlePostLoopStages({
|
|
1245
839
|
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
1246
|
-
|
|
840
|
+
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger
|
|
1247
841
|
});
|
|
1248
842
|
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
1249
843
|
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
@@ -1253,7 +847,7 @@ async function handleApprovedReview({ config, session, emitter, eventBase, coder
|
|
|
1253
847
|
}
|
|
1254
848
|
|
|
1255
849
|
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker }) {
|
|
1256
|
-
|
|
850
|
+
const budget = budgetSummary();
|
|
1257
851
|
const solomonResult = await invokeSolomon({
|
|
1258
852
|
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
1259
853
|
iteration: config.max_iterations,
|
|
@@ -1262,17 +856,26 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1262
856
|
task,
|
|
1263
857
|
iterationCount: config.max_iterations,
|
|
1264
858
|
maxIterations: config.max_iterations,
|
|
859
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
1265
860
|
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
|
|
1266
861
|
}
|
|
1267
862
|
});
|
|
1268
863
|
|
|
864
|
+
// Solomon approved the work — treat as successful completion
|
|
865
|
+
if (solomonResult.action === "approve") {
|
|
866
|
+
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
867
|
+
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Solomon says continue — extend iterations
|
|
1269
871
|
if (solomonResult.action === "continue") {
|
|
1270
872
|
if (solomonResult.humanGuidance) {
|
|
1271
|
-
session.last_reviewer_feedback = `
|
|
873
|
+
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1272
874
|
}
|
|
1273
875
|
session.reviewer_retry_count = 0;
|
|
1274
876
|
await saveSession(session);
|
|
1275
|
-
|
|
877
|
+
const extraIterations = solomonResult.extraIterations || config.max_iterations;
|
|
878
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
|
|
1276
879
|
}
|
|
1277
880
|
|
|
1278
881
|
if (solomonResult.action === "pause") {
|
|
@@ -1283,7 +886,7 @@ async function handleMaxIterationsReached({ session, budgetSummary, emitter, eve
|
|
|
1283
886
|
return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
|
|
1284
887
|
}
|
|
1285
888
|
|
|
1286
|
-
// Solomon
|
|
889
|
+
// Solomon couldn't resolve — fail
|
|
1287
890
|
session.budget = budgetSummary();
|
|
1288
891
|
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
1289
892
|
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
@@ -1320,6 +923,44 @@ async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
|
1320
923
|
}
|
|
1321
924
|
|
|
1322
925
|
async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
926
|
+
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
927
|
+
const initProjectDir = config.projectDir || process.cwd();
|
|
928
|
+
await autoInit(initProjectDir, logger);
|
|
929
|
+
|
|
930
|
+
// Smart role assignment: detect installed AIs and assign to roles
|
|
931
|
+
// Only runs if: (a) no roles configured AND (b) not in test environment
|
|
932
|
+
const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
|
|
933
|
+
if (needsAssignment) {
|
|
934
|
+
try {
|
|
935
|
+
const { autoAssignRoles, applyRoleAssignments } = await import("./utils/role-assigner.js");
|
|
936
|
+
const { assignments } = await autoAssignRoles(logger);
|
|
937
|
+
if (assignments) config = applyRoleAssignments({ ...config }, assignments);
|
|
938
|
+
} catch { /* non-blocking — defaults will be used */ }
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
|
|
942
|
+
// When running from a subdirectory of a git repo, use relative path as scope
|
|
943
|
+
let diffScope = config.projectDir || null;
|
|
944
|
+
if (!diffScope) {
|
|
945
|
+
try {
|
|
946
|
+
const { execSync } = await import("node:child_process");
|
|
947
|
+
const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
948
|
+
const cwd = process.cwd();
|
|
949
|
+
if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
|
|
950
|
+
diffScope = cwd.slice(repoRoot.length + 1);
|
|
951
|
+
logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
|
|
952
|
+
}
|
|
953
|
+
} catch { /* git not available */ }
|
|
954
|
+
}
|
|
955
|
+
setDiffProjectDir(diffScope);
|
|
956
|
+
|
|
957
|
+
// Auto-detect Chrome DevTools MCP
|
|
958
|
+
const { detectDevToolsMcp } = await import("./webperf/devtools-detect.js");
|
|
959
|
+
const devToolsAvailable = await detectDevToolsMcp(logger);
|
|
960
|
+
if (devToolsAvailable) {
|
|
961
|
+
config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
|
|
962
|
+
}
|
|
963
|
+
|
|
1323
964
|
const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
|
|
1324
965
|
ctx.askQuestion = askQuestion;
|
|
1325
966
|
ctx.pgTaskId = pgTaskId;
|
|
@@ -1340,23 +981,19 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1340
981
|
ctx.trackBudget = trackBudget;
|
|
1341
982
|
|
|
1342
983
|
// --- 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
|
-
}
|
|
984
|
+
const rtkResult = await detectRtk();
|
|
985
|
+
if (rtkResult.available) {
|
|
986
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
987
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
988
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
989
|
+
setDiffRunner(rtkRunner);
|
|
990
|
+
setGitRunner(rtkRunner);
|
|
991
|
+
ctx.rtkTracker = rtkTracker;
|
|
992
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
993
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
994
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
995
|
+
detail: { version: rtkResult.version, executorType: "local" }
|
|
996
|
+
}));
|
|
1360
997
|
}
|
|
1361
998
|
|
|
1362
999
|
// --- HU Board auto-start ---
|
|
@@ -1382,21 +1019,6 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1382
1019
|
ctx.pgCard = pgAdapterResult.pgCard;
|
|
1383
1020
|
ctx.session.pg_card = ctx.pgCard || null;
|
|
1384
1021
|
|
|
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
1022
|
emitProgress(
|
|
1401
1023
|
emitter,
|
|
1402
1024
|
makeEvent("session:start", ctx.eventBase, {
|
|
@@ -1412,6 +1034,38 @@ async function initFlowContext({ task, config, logger, emitter, askQuestion, pgT
|
|
|
1412
1034
|
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1413
1035
|
ctx.config = preLoopResult.updatedConfig;
|
|
1414
1036
|
|
|
1037
|
+
// --- Session Journal: persist pre-loop outputs + display plan summary ---
|
|
1038
|
+
const reportDir = ctx.config.output?.report_dir || ".reviews";
|
|
1039
|
+
try {
|
|
1040
|
+
ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
|
|
1041
|
+
const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
|
|
1042
|
+
ctx.journalFiles = journalFiles;
|
|
1043
|
+
ctx.journalIterations = [];
|
|
1044
|
+
ctx.journalDecisions = [];
|
|
1045
|
+
|
|
1046
|
+
// Attach journal state to session so finalizeApprovedSession can access it
|
|
1047
|
+
ctx.session._journalDir = ctx.journalDir;
|
|
1048
|
+
ctx.session._journalFiles = journalFiles;
|
|
1049
|
+
ctx.session._journalIterations = ctx.journalIterations;
|
|
1050
|
+
ctx.session._journalDecisions = ctx.journalDecisions;
|
|
1051
|
+
ctx.session._startedAt = ctx.startedAt;
|
|
1052
|
+
|
|
1053
|
+
// Display plan summary in console before iteration loop
|
|
1054
|
+
const planSummary = buildPlanSummary({
|
|
1055
|
+
pipelineFlags: ctx.pipelineFlags,
|
|
1056
|
+
config: ctx.config,
|
|
1057
|
+
stageResults: ctx.stageResults,
|
|
1058
|
+
task
|
|
1059
|
+
});
|
|
1060
|
+
console.log(planSummary);
|
|
1061
|
+
} catch (err) {
|
|
1062
|
+
logger.warn(`Journal init failed (non-blocking): ${err.message}`);
|
|
1063
|
+
ctx.journalDir = null;
|
|
1064
|
+
ctx.journalFiles = [];
|
|
1065
|
+
ctx.journalIterations = [];
|
|
1066
|
+
ctx.journalDecisions = [];
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1415
1069
|
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
1416
1070
|
const projectDir = ctx.config.projectDir || process.cwd();
|
|
1417
1071
|
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
@@ -1424,7 +1078,7 @@ async function runSingleIteration(ctx) {
|
|
|
1424
1078
|
const { config, logger, emitter, eventBase, session, task, iteration: i } = ctx;
|
|
1425
1079
|
|
|
1426
1080
|
const iterStart = Date.now();
|
|
1427
|
-
const
|
|
1081
|
+
const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
|
|
1428
1082
|
logger.setContext({ iteration: i, stage: "iteration" });
|
|
1429
1083
|
|
|
1430
1084
|
const reviewerRetryCount = session.reviewer_retry_count || 0;
|
|
@@ -1455,8 +1109,8 @@ async function runSingleIteration(ctx) {
|
|
|
1455
1109
|
});
|
|
1456
1110
|
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
1457
1111
|
|
|
1458
|
-
await
|
|
1459
|
-
|
|
1112
|
+
await handleCiEarlyPrOrPush({
|
|
1113
|
+
ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
|
|
1460
1114
|
stageResults: ctx.stageResults, i
|
|
1461
1115
|
});
|
|
1462
1116
|
|
|
@@ -1474,13 +1128,26 @@ async function runSingleIteration(ctx) {
|
|
|
1474
1128
|
}));
|
|
1475
1129
|
session.standby_retry_count = 0;
|
|
1476
1130
|
|
|
1131
|
+
// --- Journal: record iteration ---
|
|
1132
|
+
if (ctx.journalIterations) {
|
|
1133
|
+
ctx.journalIterations.push(formatIteration({
|
|
1134
|
+
iteration: i,
|
|
1135
|
+
coderSummary: ctx.stageResults.coder?.summary || null,
|
|
1136
|
+
reviewerSummary: review?.approved ? `Approved: ${review.raw_summary || ""}` : `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`,
|
|
1137
|
+
sonarSummary: ctx.stageResults.sonar?.summary || null,
|
|
1138
|
+
testerSummary: ctx.stageResults.tester?.summary || null,
|
|
1139
|
+
securitySummary: ctx.stageResults.security?.summary || null,
|
|
1140
|
+
durationMs: iterDuration
|
|
1141
|
+
}));
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1477
1144
|
const solomonResult = await handleSolomonCheck({
|
|
1478
1145
|
config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
|
|
1479
|
-
|
|
1146
|
+
ciEnabled, blockingIssues: review?.blocking_issues
|
|
1480
1147
|
});
|
|
1481
1148
|
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
1482
1149
|
|
|
1483
|
-
await
|
|
1150
|
+
await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
|
|
1484
1151
|
|
|
1485
1152
|
if (review.approved) {
|
|
1486
1153
|
const approvedResult = await handleApprovedReview({
|
|
@@ -1500,8 +1167,13 @@ async function runSingleIteration(ctx) {
|
|
|
1500
1167
|
} else {
|
|
1501
1168
|
// Solomon is enabled — feed back the blocking issues for the next coder iteration
|
|
1502
1169
|
session.last_reviewer_feedback = review.blocking_issues
|
|
1503
|
-
.map((x) =>
|
|
1504
|
-
|
|
1170
|
+
.map((x) => {
|
|
1171
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
1172
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
1173
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
1174
|
+
return parts.join("\n");
|
|
1175
|
+
})
|
|
1176
|
+
.join("\n\n");
|
|
1505
1177
|
await saveSession(session);
|
|
1506
1178
|
}
|
|
1507
1179
|
|
|
@@ -1559,14 +1231,92 @@ async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, log
|
|
|
1559
1231
|
ctx.eventBase.iteration = i;
|
|
1560
1232
|
ctx.iteration = i;
|
|
1561
1233
|
|
|
1562
|
-
|
|
1234
|
+
let iterResult;
|
|
1235
|
+
try {
|
|
1236
|
+
iterResult = await runSingleIteration(ctx);
|
|
1237
|
+
} catch (stageError) {
|
|
1238
|
+
// ANY unhandled error in a stage = out of normal flow → Solomon decides
|
|
1239
|
+
logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
|
|
1240
|
+
const solomonResult = await invokeSolomon({
|
|
1241
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
|
|
1242
|
+
stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
|
|
1243
|
+
conflict: {
|
|
1244
|
+
stage: "stage_error",
|
|
1245
|
+
task: loopTask,
|
|
1246
|
+
iterationCount: i,
|
|
1247
|
+
maxIterations: ctx.config.max_iterations,
|
|
1248
|
+
budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
|
|
1249
|
+
history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
if (solomonResult.action === "approve") {
|
|
1254
|
+
logger.info("Solomon approved despite stage error");
|
|
1255
|
+
return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
|
|
1256
|
+
}
|
|
1257
|
+
if (solomonResult.action === "continue") {
|
|
1258
|
+
if (solomonResult.humanGuidance) {
|
|
1259
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1260
|
+
}
|
|
1261
|
+
continue; // next iteration
|
|
1262
|
+
}
|
|
1263
|
+
if (solomonResult.action === "pause") {
|
|
1264
|
+
return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
|
|
1265
|
+
}
|
|
1266
|
+
// Solomon couldn't resolve — fail
|
|
1267
|
+
await markSessionStatus(ctx.session, "failed");
|
|
1268
|
+
return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1563
1271
|
if (iterResult.action === "return") {
|
|
1564
1272
|
return iterResult.result;
|
|
1565
1273
|
}
|
|
1566
1274
|
if (iterResult.action === "retry") { i -= 1; }
|
|
1567
1275
|
}
|
|
1568
1276
|
|
|
1277
|
+
// Solomon decides whether to extend iterations or stop
|
|
1569
1278
|
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 });
|
|
1279
|
+
|
|
1280
|
+
// Solomon said "continue" — extend iterations and keep going
|
|
1281
|
+
if (maxIterResult.reason === "max_iterations_extended") {
|
|
1282
|
+
const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
|
|
1283
|
+
ctx.config.max_iterations += extra;
|
|
1284
|
+
logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
|
|
1285
|
+
|
|
1286
|
+
if (maxIterResult.humanGuidance) {
|
|
1287
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// Continue the loop
|
|
1291
|
+
while (i < ctx.config.max_iterations) {
|
|
1292
|
+
i += 1;
|
|
1293
|
+
const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
|
|
1294
|
+
|
|
1295
|
+
const cpResult = await handleCheckpoint({
|
|
1296
|
+
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1297
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1298
|
+
});
|
|
1299
|
+
if (cpResult.action === "stop") return cpResult.result;
|
|
1300
|
+
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1301
|
+
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1302
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1303
|
+
|
|
1304
|
+
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1305
|
+
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
1306
|
+
|
|
1307
|
+
ctx.eventBase.iteration = i;
|
|
1308
|
+
ctx.iteration = i;
|
|
1309
|
+
|
|
1310
|
+
const iterResult = await runSingleIteration(ctx);
|
|
1311
|
+
if (iterResult.action === "return") return iterResult.result;
|
|
1312
|
+
if (iterResult.action === "retry") { i -= 1; }
|
|
1313
|
+
}
|
|
1314
|
+
|
|
1315
|
+
// Extended iterations also exhausted — final Solomon call
|
|
1316
|
+
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 });
|
|
1317
|
+
return finalResult;
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1570
1320
|
return maxIterResult;
|
|
1571
1321
|
}
|
|
1572
1322
|
|
|
@@ -1577,7 +1327,29 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1577
1327
|
return handleDryRun({ task, config, flags, emitter, pipelineFlags });
|
|
1578
1328
|
}
|
|
1579
1329
|
|
|
1580
|
-
|
|
1330
|
+
let ctx;
|
|
1331
|
+
try {
|
|
1332
|
+
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
1333
|
+
} catch (initError) {
|
|
1334
|
+
// Pre-loop stage failure → Solomon decides
|
|
1335
|
+
logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
|
|
1336
|
+
const tempSession = { id: "init-error", task, status: "failed" };
|
|
1337
|
+
const solomonResult = await invokeSolomon({
|
|
1338
|
+
config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
|
|
1339
|
+
stage: "init_error", askQuestion, session: tempSession, iteration: 0,
|
|
1340
|
+
conflict: {
|
|
1341
|
+
stage: "init_error",
|
|
1342
|
+
task,
|
|
1343
|
+
iterationCount: 0,
|
|
1344
|
+
maxIterations: config.max_iterations || 5,
|
|
1345
|
+
history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
|
|
1346
|
+
}
|
|
1347
|
+
});
|
|
1348
|
+
if (solomonResult.action === "pause") {
|
|
1349
|
+
return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
|
|
1350
|
+
}
|
|
1351
|
+
throw initError; // Solomon couldn't resolve — propagate
|
|
1352
|
+
}
|
|
1581
1353
|
|
|
1582
1354
|
try {
|
|
1583
1355
|
// --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
|
|
@@ -1667,14 +1439,6 @@ export async function runFlow({ task, config, logger, flags = {}, emitter = null
|
|
|
1667
1439
|
await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
|
|
1668
1440
|
return result;
|
|
1669
1441
|
} 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
1442
|
// --- Cleanup auto-installed skills ---
|
|
1679
1443
|
const autoSkills = ctx.session?.autoInstalledSkills;
|
|
1680
1444
|
if (autoSkills && autoSkills.length > 0) {
|