karajan-code 2.7.3 → 2.8.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 +4 -4
- package/package.json +24 -4
- package/scripts/diet-baseline.mjs +170 -0
- package/scripts/regen-arch-stats.sh +71 -0
- package/src/agents/availability.js +15 -9
- package/src/agents/claude-agent.js +143 -45
- package/src/agents/model-registry.js +1 -1
- package/src/audit/agent-readiness.js +240 -0
- package/src/bootstrap.js +26 -1
- package/src/brain/decision-tracker.js +3 -1
- package/src/brain/decisor.js +1 -1
- package/src/canvas/parse-md.js +238 -0
- package/src/canvas/schema.js +101 -0
- package/src/canvas/to-flat.js +85 -0
- package/src/canvas/validate.js +163 -0
- package/src/checks/binaries.js +1 -1
- package/src/checks/ports.js +97 -27
- package/src/checks/runner.js +26 -10
- package/src/checks/sonar.js +1 -1
- package/src/checks/system.js +1 -1
- package/src/checks/tokens.js +77 -24
- package/src/cli/_shared.js +30 -0
- package/src/cli/register-meta.js +204 -0
- package/src/cli/register-pipeline.js +212 -0
- package/src/cli/register-plan.js +152 -0
- package/src/cli/register-roles-skills.js +38 -0
- package/src/cli/register-sonar.js +28 -0
- package/src/cli.js +83 -468
- package/src/commands/agents.js +18 -6
- package/src/commands/architect.js +29 -2
- package/src/commands/audit.js +27 -3
- package/src/commands/board.js +160 -30
- package/src/commands/clean.js +83 -0
- package/src/commands/code.js +7 -2
- package/src/commands/discover.js +7 -2
- package/src/commands/init.js +2 -2
- package/src/commands/plan/_shared.js +40 -0
- package/src/commands/plan/add-hu.js +21 -0
- package/src/commands/plan/delete.js +9 -0
- package/src/commands/plan/generate.js +284 -0
- package/src/commands/plan/index.js +14 -0
- package/src/commands/plan/list.js +21 -0
- package/src/commands/plan/ready.js +46 -0
- package/src/commands/plan/remove-hu.js +17 -0
- package/src/commands/plan/show.js +81 -0
- package/src/commands/plan/validate.js +20 -0
- package/src/commands/plan.js +14 -291
- package/src/commands/researcher.js +7 -2
- package/src/commands/resume.js +24 -1
- package/src/commands/review.js +7 -2
- package/src/commands/run.js +52 -1
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +75 -0
- package/src/commands/triage.js +7 -2
- package/src/commands/undo.js +1 -1
- package/src/config/defaults.js +213 -0
- package/src/config/loader.js +137 -0
- package/src/config/overrides.js +199 -0
- package/src/config/role-resolver.js +159 -0
- package/src/config/test-harness.js +79 -0
- package/src/config.js +30 -609
- package/src/git/automation.js +2 -2
- package/src/guards/output-guard.js +0 -2
- package/src/hu/acceptance-runner.js +52 -10
- package/src/hu/store.js +11 -0
- package/src/mcp/handlers/direct-handlers.js +2 -5
- package/src/mcp/handlers/management-handlers.js +48 -4
- package/src/mcp/handlers/run-handler.js +9 -8
- package/src/mcp/preflight.js +32 -13
- package/src/mcp/server-handlers.js +4 -3
- package/src/mcp/suggest-handler.js +3 -2
- package/src/mcp/tools.js +15 -0
- package/src/orchestrator/agent-fallback.js +1 -1
- package/src/orchestrator/ci-integration.js +10 -10
- package/src/orchestrator/config-init.js +25 -7
- package/src/orchestrator/direct-actions.js +6 -2
- package/src/orchestrator/drivers/error-recovery.js +287 -0
- package/src/orchestrator/drivers/init-context.js +222 -0
- package/src/orchestrator/drivers/iteration-loop.js +311 -0
- package/src/orchestrator/drivers/iteration-phases/coder-and-refactorer.js +48 -0
- package/src/orchestrator/drivers/iteration-phases/guards.js +84 -0
- package/src/orchestrator/drivers/iteration-phases/handle-approved.js +29 -0
- package/src/orchestrator/drivers/iteration-phases/quality-gates.js +76 -0
- package/src/orchestrator/drivers/iteration-phases/reviewer-gate.js +60 -0
- package/src/orchestrator/drivers/post-loop.js +370 -0
- package/src/orchestrator/drivers/pre-loop-phases/auto-hu-batch.js +116 -0
- package/src/orchestrator/drivers/pre-loop-phases/config-deprecations.js +47 -0
- package/src/orchestrator/drivers/pre-loop-phases/ensure-addyosmani-skills.js +84 -0
- package/src/orchestrator/drivers/pre-loop-phases/inject-loaded-plan.js +268 -0
- package/src/orchestrator/drivers/pre-loop.js +434 -0
- package/src/orchestrator/drivers/run-hu-batch.js +367 -0
- package/src/orchestrator/flow-control.js +4 -3
- package/src/orchestrator/flow-runner.js +121 -2019
- package/src/orchestrator/fs-leak-detector.js +156 -0
- package/src/orchestrator/hu-sub-pipeline.js +197 -9
- package/src/orchestrator/integrations.js +88 -0
- package/src/orchestrator/post-loop-stages.js +35 -8
- package/src/orchestrator/preflight-checks.js +30 -3
- package/src/orchestrator/repair/repair-detector.js +117 -0
- package/src/orchestrator/repair/repair-runner.js +142 -0
- package/src/orchestrator/reviewer-fallback.js +1 -1
- package/src/orchestrator/solomon-escalation.js +3 -2
- package/src/orchestrator/solomon-rules.js +7 -3
- package/src/orchestrator/stages/architect-stage.js +12 -11
- package/src/orchestrator/stages/coder-stage.js +56 -7
- package/src/orchestrator/stages/hu-reviewer-phases/detect-and-apply-splits.js +127 -0
- package/src/orchestrator/stages/hu-reviewer-stage.js +6 -79
- package/src/orchestrator/stages/planner-stage.js +1 -1
- package/src/orchestrator/stages/research-stage.js +1 -1
- package/src/orchestrator/stages/reviewer-stage.js +7 -7
- package/src/orchestrator/stages/sonar-stage.js +94 -10
- package/src/orchestrator/stages/stage-classes.js +86 -0
- package/src/orchestrator/stages/stage-executor.js +1 -1
- package/src/orchestrator/stages/triage-stage.js +1 -1
- package/src/orchestrator/standby.js +3 -2
- package/src/orchestrator/verification-gate.js +20 -12
- package/src/plan/adr-generator.js +170 -0
- package/src/plan/adr-loader.js +60 -0
- package/src/plan/plan-executor.js +19 -1
- package/src/plan/plan-hu-ops.js +170 -1
- package/src/plan/plan-reviewer.js +199 -0
- package/src/plan/plan-schema.js +69 -1
- package/src/plan/plan-store.js +40 -3
- package/src/plan/spec-detector.js +61 -0
- package/src/plan/tests-synthesizer.js +116 -0
- package/src/planning-game/client.js +3 -14
- package/src/planning-game/decomposition.js +1 -1
- package/src/planning-game/pipeline-adapter.js +36 -5
- package/src/prompts/coder.js +172 -1
- package/src/prompts/hu-reviewer.js +0 -1
- package/src/prompts/planner.js +81 -4
- package/src/review/diff-generator.js +35 -22
- package/src/review/snapshot-diff.js +1 -1
- package/src/roles/coder-role.js +25 -3
- package/src/roles/planner-role.js +42 -1
- package/src/roles/repairer-role.js +181 -0
- package/src/roles/tester-role.js +70 -4
- package/src/session/mutators.js +304 -0
- package/src/session/runtime-overrides.js +59 -0
- package/src/{session-store.js → session/store.js} +8 -2
- package/src/skills/semantic-detector.js +10 -5
- package/src/skills/skill-detector.js +1 -1
- package/src/sonar/cloud-scanner.js +0 -1
- package/src/sonar/credentials.js +14 -6
- package/src/sonar/discovery.js +123 -0
- package/src/sonar/manager.js +18 -0
- package/src/sync/detect-drift.js +113 -0
- package/src/tracker-bootstrap.js +44 -0
- package/src/utils/agent-detect.js +15 -11
- package/src/utils/board-prompt-bridge.js +109 -0
- package/src/utils/cli-progress.js +161 -0
- package/src/utils/cwd-confirm.js +39 -0
- package/src/utils/derive-project-name-from-cwd.js +119 -0
- package/src/utils/garbage-collector.js +396 -0
- package/src/utils/git.js +16 -4
- package/src/utils/locale.js +1 -1
- package/src/utils/paths.js +21 -1
- package/src/utils/process.js +1 -1
- package/src/utils/prompt-project-name.js +35 -0
- package/src/utils/run-context.js +75 -0
- package/src/utils/run-log.js +12 -0
- package/src/utils/task-file.js +1 -1
- package/src/utils/telemetry.js +13 -1
- package/src/webperf/devtools-detect.js +1 -1
- package/templates/coder-rules.md +13 -0
- package/templates/roles/coder.md +34 -0
- package/templates/roles/repairer.md +94 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Error-recovery driver — extracted from src/orchestrator/flow-runner.js
|
|
3
|
+
* in TSK-0335 (Oleada 3 of the v2.7.4 audit refactor).
|
|
4
|
+
*
|
|
5
|
+
* Five handlers that react to pipeline-level failure signals and decide
|
|
6
|
+
* whether to retry, pause, or escalate to Solomon:
|
|
7
|
+
*
|
|
8
|
+
* - handleStandbyResult: rate-limit / provider-outage →
|
|
9
|
+
* Solomon decides skip/retry/wait/pause.
|
|
10
|
+
* - emitSolomonAlerts: forward Solomon-rules findings to
|
|
11
|
+
* the progress stream.
|
|
12
|
+
* - handleSolomonCheck: evaluate Solomon-rules context,
|
|
13
|
+
* escalate critical alerts (direct to
|
|
14
|
+
* human without Brain, via Solomon
|
|
15
|
+
* with Brain).
|
|
16
|
+
* - checkSolomonCriticalAlerts: non-Brain branch — ask the user
|
|
17
|
+
* whether to continue/pause/stop on
|
|
18
|
+
* critical alerts.
|
|
19
|
+
* - handleReviewerRetryAndSolomon: reviewer sub-loop limit → invoke
|
|
20
|
+
* Solomon to resolve the stall.
|
|
21
|
+
*
|
|
22
|
+
* These were extracted verbatim; no behavior change. flow-runner.js now
|
|
23
|
+
* imports them instead of defining them. Part of breaking up the 2254-LOC
|
|
24
|
+
* god-module — see docs/ARCHITECTURE.md and the TSK-0335 acceptance
|
|
25
|
+
* criteria.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { invokeSolomon } from "../solomon-escalation.js";
|
|
29
|
+
import { saveSession, pauseSession } from "../../session/store.js";
|
|
30
|
+
import {
|
|
31
|
+
setReviewerFeedback, setAlternativeAgent, resetRetryCount,
|
|
32
|
+
incrementRetryCount,
|
|
33
|
+
} from "../../session/mutators.js";
|
|
34
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
35
|
+
import { tryCiComment } from "../ci-integration.js";
|
|
36
|
+
|
|
37
|
+
export async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger, config, askQuestion }) {
|
|
38
|
+
if (stageResult?.action !== "standby") {
|
|
39
|
+
return { handled: false };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const isOutage = stageResult.standbyInfo.isProviderOutage;
|
|
43
|
+
const agent = stageResult.standbyInfo.agent;
|
|
44
|
+
const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
|
|
45
|
+
const cooldownMs = stageResult.standbyInfo.cooldownMs;
|
|
46
|
+
|
|
47
|
+
// Rate limit = out of normal flow → Solomon decides immediately
|
|
48
|
+
const solomonResult = await invokeSolomon({
|
|
49
|
+
config, logger, emitter, eventBase, stage: `${stage}_rate_limit`, askQuestion, session,
|
|
50
|
+
iteration: i,
|
|
51
|
+
conflict: {
|
|
52
|
+
stage: `${stage}_rate_limit`,
|
|
53
|
+
task: session.task,
|
|
54
|
+
iterationCount: i,
|
|
55
|
+
maxIterations: config?.max_iterations || 5,
|
|
56
|
+
cooldownUntil,
|
|
57
|
+
cooldownMs,
|
|
58
|
+
history: [{
|
|
59
|
+
agent: stage,
|
|
60
|
+
feedback: `Agent "${agent}" rate-limited. ${isOutage ? "Provider outage (5xx)." : "API rate limit (429)."} ${cooldownUntil ? `Cooldown until ${cooldownUntil}.` : ""}`
|
|
61
|
+
}]
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
if (solomonResult.action === "approve") {
|
|
66
|
+
// Solomon says skip is safe — only allowed after exhausting alternatives
|
|
67
|
+
logger.info(`Solomon: skip ${stage} after evaluating risk (agent "${agent}" rate-limited)`);
|
|
68
|
+
emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
|
|
69
|
+
status: "ok",
|
|
70
|
+
message: `Solomon: skip ${stage} (low risk, agent "${agent}" unavailable)`,
|
|
71
|
+
detail: { agent, solomonAction: "approve" }
|
|
72
|
+
}));
|
|
73
|
+
return { handled: true, action: "skip" };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (solomonResult.action === "continue") {
|
|
77
|
+
// Solomon says: wait for cooldown, or retry with alternative agent
|
|
78
|
+
const altAgent = solomonResult.alternativeAgent;
|
|
79
|
+
const waitTarget = solomonResult.waitUntil;
|
|
80
|
+
|
|
81
|
+
if (waitTarget) {
|
|
82
|
+
const waitMs = Math.max(0, new Date(waitTarget).getTime() - Date.now());
|
|
83
|
+
if (waitMs > 0 && waitMs < 10 * 60 * 1000) {
|
|
84
|
+
logger.info(`Solomon: wait ${Math.round(waitMs / 1000)}s for cooldown, then retry ${stage}`);
|
|
85
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (altAgent) {
|
|
90
|
+
logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
|
|
91
|
+
setAlternativeAgent(session, stage, altAgent);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (solomonResult.humanGuidance) {
|
|
95
|
+
setReviewerFeedback(session, `Solomon guidance: ${solomonResult.humanGuidance}`);
|
|
96
|
+
}
|
|
97
|
+
await saveSession(session);
|
|
98
|
+
return { handled: true, action: "retry_reviewer_only" };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (solomonResult.action === "pause") {
|
|
102
|
+
return {
|
|
103
|
+
handled: true,
|
|
104
|
+
action: "return",
|
|
105
|
+
result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "rate_limit" }
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Solomon couldn't resolve — pause
|
|
110
|
+
await pauseSession(session, {
|
|
111
|
+
question: `Agent "${agent}" rate-limited. Solomon could not resolve.`,
|
|
112
|
+
context: { iteration: i, stage, reason: "rate_limit" }
|
|
113
|
+
});
|
|
114
|
+
return {
|
|
115
|
+
handled: true,
|
|
116
|
+
action: "return",
|
|
117
|
+
result: { paused: true, sessionId: session.id, question: `Agent "${agent}" rate-limited`, context: "rate_limit" }
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
export function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
123
|
+
for (const alert of alerts) {
|
|
124
|
+
emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
|
|
125
|
+
status: alert.severity === "critical" ? "fail" : "warn",
|
|
126
|
+
message: alert.message,
|
|
127
|
+
detail: alert.detail
|
|
128
|
+
}));
|
|
129
|
+
logger.warn(`Rules alert [${alert.rule}]: ${alert.message}`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
|
|
134
|
+
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
135
|
+
|
|
136
|
+
try {
|
|
137
|
+
const { evaluateRules, buildRulesContext } = await import("../solomon-rules.js");
|
|
138
|
+
const rulesContext = await buildRulesContext({ session, task, iteration: i, blockingIssues });
|
|
139
|
+
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
140
|
+
|
|
141
|
+
if (rulesResult.alerts.length > 0) {
|
|
142
|
+
emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
|
|
143
|
+
// Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
|
|
144
|
+
// On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
|
|
145
|
+
// resolve does Brain escalate to human. Solomon-rules never prompts directly.
|
|
146
|
+
if (!brainCtx?.enabled) {
|
|
147
|
+
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
148
|
+
if (pauseResult) return pauseResult;
|
|
149
|
+
} else if (rulesResult.hasCritical) {
|
|
150
|
+
const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
|
|
151
|
+
brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
|
|
152
|
+
brainCtx.ruleAlerts.push(...criticalAlerts);
|
|
153
|
+
logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
|
|
154
|
+
|
|
155
|
+
const alertSummary = criticalAlerts.map(a => a.message).join("; ");
|
|
156
|
+
const solomonOpinion = await invokeSolomon({
|
|
157
|
+
config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
|
|
158
|
+
conflict: {
|
|
159
|
+
stage: "brain-dilemma",
|
|
160
|
+
task,
|
|
161
|
+
iterationCount: i,
|
|
162
|
+
maxIterations: config.max_iterations,
|
|
163
|
+
dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
|
|
164
|
+
ruleAlerts: criticalAlerts,
|
|
165
|
+
blockingIssues: blockingIssues || [],
|
|
166
|
+
history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (solomonOpinion.action === "pause") {
|
|
171
|
+
logger.info("Brain: Solomon advised pause — escalating to human");
|
|
172
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
|
|
173
|
+
}
|
|
174
|
+
if (solomonOpinion.action === "approve") {
|
|
175
|
+
logger.info("Brain: Solomon advised proceeding — treating as approved");
|
|
176
|
+
return { action: "continue", approved: true };
|
|
177
|
+
}
|
|
178
|
+
// action === "continue" | "subtask" | fallback → Brain continues the loop
|
|
179
|
+
logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (ciEnabled && session.ci_pr_number) {
|
|
184
|
+
const alerts = rulesResult.alerts || [];
|
|
185
|
+
const alertMsg = alerts.length > 0
|
|
186
|
+
? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
|
|
187
|
+
: "No anomalies detected";
|
|
188
|
+
await tryCiComment({
|
|
189
|
+
config, session, logger,
|
|
190
|
+
agent: "Solomon",
|
|
191
|
+
body: `Supervisor check iteration ${i}: ${alertMsg}`
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
logger.warn(`Solomon rules evaluation failed: ${err.message}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { action: "continue" };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i }) {
|
|
202
|
+
if (!rulesResult.hasCritical || !askQuestion) return null;
|
|
203
|
+
|
|
204
|
+
const alertSummary = rulesResult.alerts
|
|
205
|
+
.filter(a => a.severity === "critical")
|
|
206
|
+
.map(a => a.message)
|
|
207
|
+
.join("\n");
|
|
208
|
+
const question = [
|
|
209
|
+
"Solomon detected critical issues:",
|
|
210
|
+
alertSummary,
|
|
211
|
+
"",
|
|
212
|
+
"What should we do?",
|
|
213
|
+
"1 = Continue anyway",
|
|
214
|
+
"2 = Pause the session",
|
|
215
|
+
"3 = Stop the session",
|
|
216
|
+
"",
|
|
217
|
+
"Type 1, 2, or 3:"
|
|
218
|
+
].join("\n");
|
|
219
|
+
const answer = await askQuestion(question, { iteration: i, stage: "solomon" });
|
|
220
|
+
const trimmed = (answer || "").trim().toLowerCase();
|
|
221
|
+
const shouldPause = !answer
|
|
222
|
+
|| trimmed === "2" || trimmed === "3"
|
|
223
|
+
|| trimmed.startsWith("pause") || trimmed.startsWith("stop");
|
|
224
|
+
if (shouldPause) {
|
|
225
|
+
await pauseSession(session, {
|
|
226
|
+
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
227
|
+
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
228
|
+
});
|
|
229
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, reason: "solomon_alert" } };
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
|
|
235
|
+
setReviewerFeedback(session, review.blocking_issues
|
|
236
|
+
.map((x) => {
|
|
237
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
238
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
239
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
240
|
+
return parts.join("\n");
|
|
241
|
+
})
|
|
242
|
+
.join("\n\n"));
|
|
243
|
+
incrementRetryCount(session, "reviewer");
|
|
244
|
+
await saveSession(session);
|
|
245
|
+
|
|
246
|
+
const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
|
|
247
|
+
if (session.reviewer_retry_count < maxReviewerRetries) {
|
|
248
|
+
return { action: "continue" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
emitProgress(
|
|
252
|
+
emitter,
|
|
253
|
+
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
254
|
+
message: `Reviewer sub-loop limit reached (${session.reviewer_retry_count}/${maxReviewerRetries})`,
|
|
255
|
+
detail: { subloop: "reviewer", retryCount: session.reviewer_retry_count, limit: maxReviewerRetries }
|
|
256
|
+
})
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const solomonResult = await invokeSolomon({
|
|
260
|
+
config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration: i,
|
|
261
|
+
conflict: {
|
|
262
|
+
stage: "reviewer",
|
|
263
|
+
task,
|
|
264
|
+
iterationCount: session.reviewer_retry_count,
|
|
265
|
+
maxIterations: maxReviewerRetries,
|
|
266
|
+
history: [{ agent: "reviewer", feedback: session.last_reviewer_feedback }]
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
if (solomonResult.action === "pause") {
|
|
271
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_fail_fast" } };
|
|
272
|
+
}
|
|
273
|
+
if (solomonResult.action === "continue") {
|
|
274
|
+
if (solomonResult.humanGuidance) {
|
|
275
|
+
// Append, not overwrite — keep the reviewer's blocking issues context.
|
|
276
|
+
setReviewerFeedback(session, `${session.last_reviewer_feedback}\nUser guidance: ${solomonResult.humanGuidance}`);
|
|
277
|
+
}
|
|
278
|
+
resetRetryCount(session, "reviewer");
|
|
279
|
+
await saveSession(session);
|
|
280
|
+
return { action: "continue" };
|
|
281
|
+
}
|
|
282
|
+
if (solomonResult.action === "subtask") {
|
|
283
|
+
return { action: "return", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return { action: "continue" };
|
|
287
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init-context driver — extracted from src/orchestrator/flow-runner.js in
|
|
3
|
+
* TSK-0335 (Oleada 3 of the v2.7.4 audit refactor).
|
|
4
|
+
*
|
|
5
|
+
* Builds the PipelineContext for runFlow: loads config, resolves roles,
|
|
6
|
+
* wires budget tracker, attaches RTK wrappers, starts the HU Board,
|
|
7
|
+
* loads product context, creates the session, runs the pre-loop stages,
|
|
8
|
+
* and sets up the session journal + git automation.
|
|
9
|
+
*
|
|
10
|
+
* Extracted verbatim; no behavior change. Split from pre-loop.js because
|
|
11
|
+
* together they would exceed the 600-LOC/driver ceiling set by the
|
|
12
|
+
* TSK-0335 acceptance criteria.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createAgent } from "../../agents/index.js";
|
|
16
|
+
import { resolveRole } from "../../config.js";
|
|
17
|
+
import { resolveReviewProfile } from "../../review/profiles.js";
|
|
18
|
+
import { RepeatDetector, getRepeatThreshold } from "../../repeat-detector.js";
|
|
19
|
+
import { emitProgress, makeEvent } from "../../utils/events.js";
|
|
20
|
+
import { prepareGitAutomation } from "../../git/automation.js";
|
|
21
|
+
import { CoderRole } from "../../roles/coder-role.js";
|
|
22
|
+
import { PipelineContext } from "../pipeline-context.js";
|
|
23
|
+
import { detectRtk } from "../../utils/rtk-detect.js";
|
|
24
|
+
import { createRtkRunner, RtkSavingsTracker } from "../../utils/rtk-wrapper.js";
|
|
25
|
+
import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "../../review/diff-generator.js";
|
|
26
|
+
import { setRunner as setGitRunner } from "../../utils/git.js";
|
|
27
|
+
import {
|
|
28
|
+
loadProductContext, resolvePipelineFlags, createBudgetManager,
|
|
29
|
+
initializeSession, autoInit,
|
|
30
|
+
} from "../config-init.js";
|
|
31
|
+
import {
|
|
32
|
+
createJournalDir, writePreLoopJournal, buildPlanSummary,
|
|
33
|
+
} from "../session-journal.js";
|
|
34
|
+
import { setPgCard, setJournalContext } from "../../session/mutators.js";
|
|
35
|
+
import { getRunContext } from "../../utils/run-context.js";
|
|
36
|
+
import { getIntegration } from "../integrations.js";
|
|
37
|
+
import { tryAutoStartBoard } from "./post-loop.js";
|
|
38
|
+
import { runPreLoopStages } from "./pre-loop.js";
|
|
39
|
+
|
|
40
|
+
export async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
41
|
+
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
42
|
+
const initProjectDir = config.projectDir || process.cwd();
|
|
43
|
+
await autoInit(initProjectDir, logger);
|
|
44
|
+
|
|
45
|
+
// Smart role assignment: detect installed AIs and assign to roles
|
|
46
|
+
// Only runs if: (a) no roles configured AND (b) not in test environment
|
|
47
|
+
const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
|
|
48
|
+
if (needsAssignment) {
|
|
49
|
+
try {
|
|
50
|
+
const { autoAssignRoles, applyRoleAssignments } = await import("../../utils/role-assigner.js");
|
|
51
|
+
const { assignments } = await autoAssignRoles(logger);
|
|
52
|
+
if (assignments) config = applyRoleAssignments({ ...config }, assignments);
|
|
53
|
+
} catch { /* non-blocking — defaults will be used */ }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
|
|
57
|
+
// When running from a subdirectory of a git repo, use relative path as scope
|
|
58
|
+
let diffScope = config.projectDir || null;
|
|
59
|
+
if (!diffScope) {
|
|
60
|
+
try {
|
|
61
|
+
// Audit follow-up: same execSync→execFileSync migration as #555.
|
|
62
|
+
const { execFileSync } = await import("node:child_process");
|
|
63
|
+
const repoRoot = execFileSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" }).trim();
|
|
64
|
+
const cwd = process.cwd();
|
|
65
|
+
if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
|
|
66
|
+
diffScope = cwd.slice(repoRoot.length + 1);
|
|
67
|
+
logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
|
|
68
|
+
}
|
|
69
|
+
} catch { /* git not available */ }
|
|
70
|
+
}
|
|
71
|
+
// TSK-0338: write into the per-run AsyncLocalStorage context instead of
|
|
72
|
+
// mutating module-level state. Falls back to setDiffProjectDir() when no
|
|
73
|
+
// run context is active (legacy CLI callers, tests not going through
|
|
74
|
+
// runFlow). The write-to-both keeps back-compat without trading isolation.
|
|
75
|
+
const runCtx = getRunContext();
|
|
76
|
+
if (runCtx) runCtx.projectDir = diffScope;
|
|
77
|
+
else setDiffProjectDir(diffScope);
|
|
78
|
+
|
|
79
|
+
// Auto-detect Chrome DevTools MCP
|
|
80
|
+
const { detectDevToolsMcp } = await import("../../webperf/devtools-detect.js");
|
|
81
|
+
const devToolsAvailable = await detectDevToolsMcp(logger);
|
|
82
|
+
if (devToolsAvailable) {
|
|
83
|
+
config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
|
|
87
|
+
ctx.askQuestion = askQuestion;
|
|
88
|
+
ctx.pgTaskId = pgTaskId;
|
|
89
|
+
ctx.pgProject = pgProject;
|
|
90
|
+
|
|
91
|
+
ctx.coderRole = resolveRole(config, "coder");
|
|
92
|
+
ctx.reviewerRole = resolveRole(config, "reviewer");
|
|
93
|
+
ctx.refactorerRole = resolveRole(config, "refactorer");
|
|
94
|
+
ctx.pipelineFlags = resolvePipelineFlags(config);
|
|
95
|
+
ctx.repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
|
|
96
|
+
ctx.coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
|
|
97
|
+
ctx.startedAt = Date.now();
|
|
98
|
+
ctx.eventBase = { sessionId: null, iteration: 0, stage: null, startedAt: ctx.startedAt };
|
|
99
|
+
const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({
|
|
100
|
+
config,
|
|
101
|
+
emitter,
|
|
102
|
+
eventBase: ctx.eventBase,
|
|
103
|
+
// Provides fresh compression data for "With KJ vs Without KJ" comparison
|
|
104
|
+
// (KJC-TSK-0274). Invoked on every budgetSummary() call.
|
|
105
|
+
getCompressionStats: () => ({
|
|
106
|
+
rtkSavings: ctx.rtkTracker?.hasData() ? ctx.rtkTracker.summary() : (ctx.session?.rtk_savings || null),
|
|
107
|
+
brainCtx: ctx.brainCtx || null,
|
|
108
|
+
}),
|
|
109
|
+
});
|
|
110
|
+
ctx.budgetTracker = budgetTracker;
|
|
111
|
+
ctx.budgetLimit = budgetLimit;
|
|
112
|
+
ctx.budgetSummary = budgetSummary;
|
|
113
|
+
ctx.trackBudget = trackBudget;
|
|
114
|
+
|
|
115
|
+
// --- RTK detection ---
|
|
116
|
+
const rtkResult = await detectRtk();
|
|
117
|
+
if (rtkResult.available) {
|
|
118
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
119
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
120
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
121
|
+
// TSK-0338: install rtkRunner into the per-run context; module-level
|
|
122
|
+
// setters are the back-compat path for runs outside a withRunContext scope.
|
|
123
|
+
if (runCtx) runCtx.runner = rtkRunner;
|
|
124
|
+
else {
|
|
125
|
+
setDiffRunner(rtkRunner);
|
|
126
|
+
setGitRunner(rtkRunner);
|
|
127
|
+
}
|
|
128
|
+
ctx.rtkTracker = rtkTracker;
|
|
129
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
130
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
131
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
132
|
+
detail: { version: rtkResult.version, executorType: "local" }
|
|
133
|
+
}));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// --- HU Board auto-start ---
|
|
137
|
+
await tryAutoStartBoard(config, logger, emitter, ctx.eventBase);
|
|
138
|
+
|
|
139
|
+
// --- Product Context ---
|
|
140
|
+
const ctxProjectDir = config.projectDir || process.cwd();
|
|
141
|
+
const { content: productContext, source: productContextSource } = await loadProductContext(ctxProjectDir);
|
|
142
|
+
if (productContext) {
|
|
143
|
+
config = { ...config, productContext };
|
|
144
|
+
logger.info(`Product context loaded from ${productContextSource}`);
|
|
145
|
+
emitProgress(emitter, makeEvent("context:loaded", ctx.eventBase, {
|
|
146
|
+
message: "Product context loaded",
|
|
147
|
+
detail: { source: productContextSource }
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
152
|
+
ctx.eventBase.sessionId = ctx.session.id;
|
|
153
|
+
|
|
154
|
+
// Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
|
|
155
|
+
const { createBrainContext, isBrainEnabled } = await import("../brain-coordinator.js");
|
|
156
|
+
ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
|
|
157
|
+
if (ctx.brainCtx.enabled) {
|
|
158
|
+
logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const trackerResult = await getIntegration("tracker")?.onSessionStart?.({
|
|
162
|
+
session: ctx.session, config, logger, pgTaskId, pgProject,
|
|
163
|
+
});
|
|
164
|
+
ctx.pgCard = trackerResult?.pgCard ?? null;
|
|
165
|
+
setPgCard(ctx.session, ctx.pgCard || null);
|
|
166
|
+
|
|
167
|
+
emitProgress(
|
|
168
|
+
emitter,
|
|
169
|
+
makeEvent("session:start", ctx.eventBase, {
|
|
170
|
+
message: "Session started",
|
|
171
|
+
detail: { task, coder: ctx.coderRole.provider, reviewer: ctx.reviewerRole.provider, maxIterations: config.max_iterations }
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
ctx.stageResults = {};
|
|
176
|
+
ctx.sonarState = { issuesInitial: null, issuesFinal: null };
|
|
177
|
+
|
|
178
|
+
const preLoopResult = await runPreLoopStages({ config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session, flags, pipelineFlags: ctx.pipelineFlags, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults: ctx.stageResults, brainCtx: ctx.brainCtx });
|
|
179
|
+
ctx.plannedTask = preLoopResult.plannedTask;
|
|
180
|
+
ctx.config = preLoopResult.updatedConfig;
|
|
181
|
+
|
|
182
|
+
// --- Session Journal: persist pre-loop outputs + display plan summary ---
|
|
183
|
+
const reportDir = ctx.config.output?.report_dir || ".reviews";
|
|
184
|
+
try {
|
|
185
|
+
ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
|
|
186
|
+
const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
|
|
187
|
+
ctx.journalFiles = journalFiles;
|
|
188
|
+
ctx.journalIterations = [];
|
|
189
|
+
ctx.journalDecisions = [];
|
|
190
|
+
|
|
191
|
+
// Attach journal state to session so finalizeApprovedSession can access it
|
|
192
|
+
setJournalContext(ctx.session, {
|
|
193
|
+
dir: ctx.journalDir,
|
|
194
|
+
files: journalFiles,
|
|
195
|
+
iterations: ctx.journalIterations,
|
|
196
|
+
decisions: ctx.journalDecisions,
|
|
197
|
+
startedAt: ctx.startedAt,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// Display plan summary in console before iteration loop
|
|
201
|
+
const planSummary = buildPlanSummary({
|
|
202
|
+
pipelineFlags: ctx.pipelineFlags,
|
|
203
|
+
config: ctx.config,
|
|
204
|
+
stageResults: ctx.stageResults,
|
|
205
|
+
task
|
|
206
|
+
});
|
|
207
|
+
console.log(planSummary);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
logger.warn(`Journal init failed (non-blocking): ${err.message}`);
|
|
210
|
+
ctx.journalDir = null;
|
|
211
|
+
ctx.journalFiles = [];
|
|
212
|
+
ctx.journalIterations = [];
|
|
213
|
+
ctx.journalDecisions = [];
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
217
|
+
const projectDir = ctx.config.projectDir || process.cwd();
|
|
218
|
+
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
219
|
+
await ctx.coderRoleInstance.init();
|
|
220
|
+
|
|
221
|
+
return ctx;
|
|
222
|
+
}
|