karajan-code 2.4.1 → 2.6.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 +20 -4
- package/package.json +5 -2
- package/src/agents/aider-agent.js +1 -2
- package/src/agents/base-agent.js +63 -1
- package/src/agents/claude-agent.js +3 -4
- package/src/agents/codex-agent.js +1 -2
- package/src/agents/gemini-agent.js +1 -2
- package/src/agents/index.js +11 -2
- package/src/agents/opencode-agent.js +1 -2
- package/src/brain/decision-tracker.js +123 -0
- package/src/brain/decisor.js +243 -0
- package/src/brain/solomon-consult.js +163 -0
- package/src/budget/comparison.js +96 -0
- package/src/checks/binaries.js +123 -0
- package/src/checks/ci.js +105 -0
- package/src/checks/config-files.js +217 -0
- package/src/checks/dir-setup.js +63 -0
- package/src/checks/mcp-health.js +157 -0
- package/src/checks/node.js +61 -0
- package/src/checks/ports.js +110 -0
- package/src/checks/rtk.js +36 -0
- package/src/checks/runner.js +267 -0
- package/src/checks/skills.js +49 -0
- package/src/checks/sonar.js +67 -0
- package/src/checks/system.js +53 -0
- package/src/checks/tokens.js +96 -0
- package/src/checks/types.js +105 -0
- package/src/cli.js +90 -7
- package/src/commands/board.js +23 -0
- package/src/commands/doctor.js +130 -362
- package/src/commands/plan.js +221 -37
- package/src/commands/resume.js +1 -1
- package/src/commands/run.js +2 -1
- package/src/commands/skills.js +49 -0
- package/src/config/schema.js +222 -0
- package/src/config.js +32 -3
- package/src/guards/policy-resolver.js +8 -0
- package/src/hu/store.js +9 -0
- package/src/infrastructure/command-runner.js +44 -0
- package/src/infrastructure/environment.js +38 -0
- package/src/infrastructure/file-system-service.js +63 -0
- package/src/infrastructure/mocks.js +182 -0
- package/src/mcp/handlers/management-handlers.js +7 -2
- package/src/mcp/tools.js +10 -3
- package/src/orchestrator/config-init.js +16 -1
- package/src/orchestrator/flow-runner.js +2085 -0
- package/src/orchestrator/pipeline-context.js +17 -0
- package/src/orchestrator/preflight-checks.js +78 -4
- package/src/orchestrator/solomon-escalation.js +21 -0
- package/src/orchestrator/stages/stage-executor.js +155 -0
- package/src/orchestrator.js +19 -1884
- package/src/plan/plan-executor.js +69 -0
- package/src/plan/plan-hu-ops.js +134 -0
- package/src/plan/plan-id.js +25 -0
- package/src/plan/plan-schema.js +139 -0
- package/src/plan/plan-store.js +58 -25
- package/src/prompts/architect.js +2 -2
- package/src/prompts/coder.js +2 -2
- package/src/prompts/prompt-resolver.js +127 -0
- package/src/prompts/reviewer.js +2 -2
- package/src/roles/architect-role.js +3 -1
- package/src/roles/base-role.js +23 -2
- package/src/roles/coder-role.js +3 -1
- package/src/roles/security-role.js +12 -0
- package/src/roles/tester-role.js +11 -0
- package/src/session/journal/decisions-writer.js +200 -0
- package/src/session/journal/iteration-logger.js +194 -0
- package/src/session/journal/summary-writer.js +176 -0
- package/src/session/journal/tree-writer.js +164 -0
- package/src/skills/semantic-detector.js +147 -0
- package/src/skills/skill-detector.js +186 -8
- package/src/skills/skill-loader.js +131 -1
- package/src/skills/skills-cache.js +128 -0
- package/src/types/agent.js +55 -0
- package/src/types/config.js +114 -0
- package/src/types/hu.js +47 -0
- package/src/types/index.js +34 -0
- package/src/types/policy.js +43 -0
- package/src/types/session.js +74 -0
- package/src/types/stage.js +68 -0
- package/src/utils/display/event-handlers.js +389 -0
- package/src/utils/display/formatters.js +132 -0
- package/src/utils/display/header.js +48 -0
- package/src/utils/display/session-summary.js +108 -0
- package/src/utils/display/solomon.js +42 -0
- package/src/utils/port-check.js +45 -0
- package/src/utils/port-occupant.js +95 -0
- package/src/utils/provider-env.js +116 -0
- package/src/utils/with-timeout.js +42 -0
- package/templates/roles/architect/anthropic.md +65 -0
- package/templates/roles/architect/default.md +63 -0
- package/templates/roles/architect/google.md +52 -0
- package/templates/roles/architect/openai.md +51 -0
- package/templates/roles/architect/opencode.md +50 -0
- package/templates/roles/coder/anthropic.md +72 -0
- package/templates/roles/coder/default.md +69 -0
- package/templates/roles/coder/google.md +78 -0
- package/templates/roles/coder/openai.md +59 -0
- package/templates/roles/coder/opencode.md +49 -0
- package/templates/roles/planner/anthropic.md +50 -0
- package/templates/roles/planner/default.md +48 -0
- package/templates/roles/planner/google.md +66 -0
- package/templates/roles/planner/openai.md +46 -0
- package/templates/roles/planner/opencode.md +53 -0
- package/templates/roles/reviewer/anthropic.md +61 -0
- package/templates/roles/reviewer/default.md +62 -0
- package/templates/roles/reviewer/google.md +63 -0
- package/templates/roles/reviewer/openai.md +57 -0
- package/templates/roles/reviewer/opencode.md +42 -0
- package/templates/roles/solomon/anthropic.md +71 -0
- package/templates/roles/solomon/default.md +83 -0
- package/templates/roles/solomon/google.md +61 -0
- package/templates/roles/solomon/openai.md +53 -0
- package/templates/roles/solomon/opencode.md +55 -0
- package/src/utils/display.js +0 -669
|
@@ -0,0 +1,2085 @@
|
|
|
1
|
+
import { createAgent } from "../agents/index.js";
|
|
2
|
+
import {
|
|
3
|
+
loadSession,
|
|
4
|
+
markSessionStatus,
|
|
5
|
+
pauseSession,
|
|
6
|
+
resumeSessionWithAnswer,
|
|
7
|
+
saveSession,
|
|
8
|
+
addCheckpoint
|
|
9
|
+
} from "../session-store.js";
|
|
10
|
+
import { generateDiff } from "../review/diff-generator.js";
|
|
11
|
+
import { resolveRole } from "../config.js";
|
|
12
|
+
import { resolveReviewProfile } from "../review/profiles.js";
|
|
13
|
+
import { msg, getLang } from "../utils/messages.js";
|
|
14
|
+
import { RepeatDetector, getRepeatThreshold } from "../repeat-detector.js";
|
|
15
|
+
import { emitProgress, makeEvent } from "../utils/events.js";
|
|
16
|
+
import {
|
|
17
|
+
prepareGitAutomation,
|
|
18
|
+
finalizeGitAutomation
|
|
19
|
+
} from "../git/automation.js";
|
|
20
|
+
import { scanDiff } from "../guards/output-guard.js";
|
|
21
|
+
import { scanPerfDiff } from "../guards/perf-guard.js";
|
|
22
|
+
import { classifyIntent } from "../guards/intent-guard.js";
|
|
23
|
+
import { CoderRole } from "../roles/coder-role.js";
|
|
24
|
+
import { invokeSolomon } from "./solomon-escalation.js";
|
|
25
|
+
import { PipelineContext } from "./pipeline-context.js";
|
|
26
|
+
import { runTriageStage, runResearcherStage, runArchitectStage, runPlannerStage, runDiscoverStage, runHuReviewerStage } from "./pre-loop-stages.js";
|
|
27
|
+
import { runDomainCuratorStage } from "./stages/domain-curator-stage.js";
|
|
28
|
+
import { persistInlineDomain } from "../domains/domain-loader.js";
|
|
29
|
+
import { runCoderStage, runRefactorerStage, runTddCheckStage, runSonarStage, runSonarCloudStage, runReviewerStage } from "./iteration-stages.js";
|
|
30
|
+
import { runTesterStage, runSecurityStage, runImpeccableStage, runFinalAuditStage } from "./post-loop-stages.js";
|
|
31
|
+
import { needsSubPipeline, runHuSubPipeline } from "./hu-sub-pipeline.js";
|
|
32
|
+
import { waitForCooldown, MAX_STANDBY_RETRIES } from "./standby.js";
|
|
33
|
+
import { detectTestFramework } from "../utils/project-detect.js";
|
|
34
|
+
import { runPreflightChecks } from "./preflight-checks.js";
|
|
35
|
+
import { detectRtk } from "../utils/rtk-detect.js";
|
|
36
|
+
import { createRtkRunner, RtkSavingsTracker } from "../utils/rtk-wrapper.js";
|
|
37
|
+
import { setRunner as setDiffRunner, setProjectDir as setDiffProjectDir } from "../review/diff-generator.js";
|
|
38
|
+
import { setRunner as setGitRunner } from "../utils/git.js";
|
|
39
|
+
import { detectNeededSkills, autoInstallSkills, cleanupAutoInstalledSkills } from "../skills/skill-detector.js";
|
|
40
|
+
import { isOpenSkillsAvailable } from "../skills/openskills-client.js";
|
|
41
|
+
import { refineSkillsSemantically, resolveSkillsMode } from "../skills/semantic-detector.js";
|
|
42
|
+
|
|
43
|
+
// Extracted modules
|
|
44
|
+
import {
|
|
45
|
+
loadProductContext,
|
|
46
|
+
resolvePipelineFlags, handleDryRun, createBudgetManager,
|
|
47
|
+
initializeSession, applyTriageOverrides, applyAutoSimplify,
|
|
48
|
+
applyFlagOverrides, resolvePipelinePolicies, autoInit,
|
|
49
|
+
updateGitignoreForStack
|
|
50
|
+
} from "./config-init.js";
|
|
51
|
+
import {
|
|
52
|
+
tryCiComment, handleCiEarlyPrOrPush, handleCiReviewDispatch,
|
|
53
|
+
formatBlockingIssues
|
|
54
|
+
} from "./ci-integration.js";
|
|
55
|
+
import {
|
|
56
|
+
handleCheckpoint, checkSessionTimeout, checkBudgetExceeded,
|
|
57
|
+
takeCheckpointSnapshot
|
|
58
|
+
} from "./flow-control.js";
|
|
59
|
+
import {
|
|
60
|
+
createJournalDir, writePreLoopJournal, writeIterationsJournal,
|
|
61
|
+
writeDecisionsJournal, writeTreeJournal, writeSummaryJournal,
|
|
62
|
+
formatIteration, formatDecision, buildPlanSummary
|
|
63
|
+
} from "./session-journal.js";
|
|
64
|
+
|
|
65
|
+
// Public re-exports (loadProductContext, shouldAutoContinueCheckpoint,
|
|
66
|
+
// parseCheckpointAnswer) live in src/orchestrator.js (the barrel).
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
// PG card "In Progress" logic moved to src/planning-game/pipeline-adapter.js → initPgAdapter()
|
|
70
|
+
|
|
71
|
+
async function runPlanningPhases({ config, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx }) {
|
|
72
|
+
let researchContext = null;
|
|
73
|
+
let plannedTask = task;
|
|
74
|
+
|
|
75
|
+
// Brain: track compression across pre-loop roles
|
|
76
|
+
const brainCompress = brainCtx?.enabled
|
|
77
|
+
? (await import("./brain-coordinator.js")).processRoleOutput
|
|
78
|
+
: null;
|
|
79
|
+
|
|
80
|
+
if (pipelineFlags.researcherEnabled) {
|
|
81
|
+
const researcherResult = await runResearcherStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
82
|
+
researchContext = researcherResult.researchContext;
|
|
83
|
+
stageResults.researcher = researcherResult.stageResult;
|
|
84
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "researcher", output: researcherResult.stageResult, iteration: 0 });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// --- Architect (between researcher and planner) ---
|
|
88
|
+
let architectContext = null;
|
|
89
|
+
if (pipelineFlags.architectEnabled) {
|
|
90
|
+
const architectResult = await runArchitectStage({
|
|
91
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
92
|
+
researchContext,
|
|
93
|
+
discoverResult: stageResults.discover || null,
|
|
94
|
+
triageLevel: stageResults.triage?.level || null,
|
|
95
|
+
askQuestion
|
|
96
|
+
});
|
|
97
|
+
architectContext = architectResult.architectContext;
|
|
98
|
+
stageResults.architect = architectResult.stageResult;
|
|
99
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "architect", output: architectResult.stageResult, iteration: 0 });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const triageDecomposition = stageResults.triage?.shouldDecompose ? stageResults.triage.subtasks : null;
|
|
103
|
+
if (pipelineFlags.plannerEnabled) {
|
|
104
|
+
const plannerRole = resolveRole(config, "planner");
|
|
105
|
+
const plannerResult = await runPlannerStage({ config, logger, emitter, eventBase, session, plannerRole, researchContext, architectContext, triageDecomposition, trackBudget });
|
|
106
|
+
plannedTask = plannerResult.plannedTask;
|
|
107
|
+
stageResults.planner = plannerResult.stageResult;
|
|
108
|
+
if (brainCompress) brainCompress(brainCtx, { roleName: "planner", output: plannerResult.stageResult, iteration: 0 });
|
|
109
|
+
|
|
110
|
+
await tryCiComment({
|
|
111
|
+
config, session, logger,
|
|
112
|
+
agent: "Planner",
|
|
113
|
+
body: `Plan: ${plannerResult.stageResult?.summary || plannedTask}`
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return { plannedTask };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
async function handleStandbyResult({ stageResult, session, emitter, eventBase, i, stage, logger, config, askQuestion }) {
|
|
123
|
+
if (stageResult?.action !== "standby") {
|
|
124
|
+
return { handled: false };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const isOutage = stageResult.standbyInfo.isProviderOutage;
|
|
128
|
+
const agent = stageResult.standbyInfo.agent;
|
|
129
|
+
const cooldownUntil = stageResult.standbyInfo.cooldownUntil;
|
|
130
|
+
const cooldownMs = stageResult.standbyInfo.cooldownMs;
|
|
131
|
+
|
|
132
|
+
// Rate limit = out of normal flow → Solomon decides immediately
|
|
133
|
+
const solomonResult = await invokeSolomon({
|
|
134
|
+
config, logger, emitter, eventBase, stage: `${stage}_rate_limit`, askQuestion, session,
|
|
135
|
+
iteration: i,
|
|
136
|
+
conflict: {
|
|
137
|
+
stage: `${stage}_rate_limit`,
|
|
138
|
+
task: session.task,
|
|
139
|
+
iterationCount: i,
|
|
140
|
+
maxIterations: config?.max_iterations || 5,
|
|
141
|
+
cooldownUntil,
|
|
142
|
+
cooldownMs,
|
|
143
|
+
history: [{
|
|
144
|
+
agent: stage,
|
|
145
|
+
feedback: `Agent "${agent}" rate-limited. ${isOutage ? "Provider outage (5xx)." : "API rate limit (429)."} ${cooldownUntil ? `Cooldown until ${cooldownUntil}.` : ""}`
|
|
146
|
+
}]
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
if (solomonResult.action === "approve") {
|
|
151
|
+
// Solomon says skip is safe — only allowed after exhausting alternatives
|
|
152
|
+
logger.info(`Solomon: skip ${stage} after evaluating risk (agent "${agent}" rate-limited)`);
|
|
153
|
+
emitProgress(emitter, makeEvent(`${stage}:rate_limit`, { ...eventBase, stage }, {
|
|
154
|
+
status: "ok",
|
|
155
|
+
message: `Solomon: skip ${stage} (low risk, agent "${agent}" unavailable)`,
|
|
156
|
+
detail: { agent, solomonAction: "approve" }
|
|
157
|
+
}));
|
|
158
|
+
return { handled: true, action: "skip" };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (solomonResult.action === "continue") {
|
|
162
|
+
// Solomon says: wait for cooldown, or retry with alternative agent
|
|
163
|
+
const altAgent = solomonResult.alternativeAgent;
|
|
164
|
+
const waitTarget = solomonResult.waitUntil;
|
|
165
|
+
|
|
166
|
+
if (waitTarget) {
|
|
167
|
+
const waitMs = Math.max(0, new Date(waitTarget).getTime() - Date.now());
|
|
168
|
+
if (waitMs > 0 && waitMs < 10 * 60 * 1000) {
|
|
169
|
+
logger.info(`Solomon: wait ${Math.round(waitMs / 1000)}s for cooldown, then retry ${stage}`);
|
|
170
|
+
await new Promise(r => setTimeout(r, waitMs));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (altAgent) {
|
|
175
|
+
logger.info(`Solomon: retry ${stage} with alternative agent "${altAgent}"`);
|
|
176
|
+
session._alternative_agent = { stage, provider: altAgent };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (solomonResult.humanGuidance) {
|
|
180
|
+
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
181
|
+
}
|
|
182
|
+
await saveSession(session);
|
|
183
|
+
return { handled: true, action: "retry_reviewer_only" };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (solomonResult.action === "pause") {
|
|
187
|
+
return {
|
|
188
|
+
handled: true,
|
|
189
|
+
action: "return",
|
|
190
|
+
result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "rate_limit" }
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Solomon couldn't resolve — pause
|
|
195
|
+
await pauseSession(session, {
|
|
196
|
+
question: `Agent "${agent}" rate-limited. Solomon could not resolve.`,
|
|
197
|
+
context: { iteration: i, stage, reason: "rate_limit" }
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
handled: true,
|
|
201
|
+
action: "return",
|
|
202
|
+
result: { paused: true, sessionId: session.id, question: `Agent "${agent}" rate-limited`, context: "rate_limit" }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
function emitSolomonAlerts(alerts, emitter, eventBase, logger) {
|
|
208
|
+
for (const alert of alerts) {
|
|
209
|
+
emitProgress(emitter, makeEvent("brain:rules-alert", { ...eventBase, stage: "brain" }, {
|
|
210
|
+
status: alert.severity === "critical" ? "fail" : "warn",
|
|
211
|
+
message: alert.message,
|
|
212
|
+
detail: alert.detail
|
|
213
|
+
}));
|
|
214
|
+
logger.warn(`Rules alert [${alert.rule}]: ${alert.message}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handleSolomonCheck({ config, session, emitter, eventBase, logger, task, i, askQuestion, ciEnabled, blockingIssues, brainCtx }) {
|
|
219
|
+
if (config.pipeline?.solomon?.enabled === false) return { action: "continue" };
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
const { evaluateRules, buildRulesContext } = await import("./solomon-rules.js");
|
|
223
|
+
const rulesContext = await buildRulesContext({ session, task, iteration: i, blockingIssues });
|
|
224
|
+
const rulesResult = evaluateRules(rulesContext, config.solomon?.rules);
|
|
225
|
+
|
|
226
|
+
if (rulesResult.alerts.length > 0) {
|
|
227
|
+
emitSolomonAlerts(rulesResult.alerts, emitter, eventBase, logger);
|
|
228
|
+
// Brain gateway: when Brain is the orchestrator, rule alerts are telemetry.
|
|
229
|
+
// On critical alerts Brain consults Solomon (AI judge). Only if Solomon can't
|
|
230
|
+
// resolve does Brain escalate to human. Solomon-rules never prompts directly.
|
|
231
|
+
if (!brainCtx?.enabled) {
|
|
232
|
+
const pauseResult = await checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i });
|
|
233
|
+
if (pauseResult) return pauseResult;
|
|
234
|
+
} else if (rulesResult.hasCritical) {
|
|
235
|
+
const criticalAlerts = rulesResult.alerts.filter(a => a.severity === "critical");
|
|
236
|
+
brainCtx.ruleAlerts = brainCtx.ruleAlerts || [];
|
|
237
|
+
brainCtx.ruleAlerts.push(...criticalAlerts);
|
|
238
|
+
logger.info(`Brain: ${criticalAlerts.length} critical rule alert(s) — consulting Solomon`);
|
|
239
|
+
|
|
240
|
+
const { invokeSolomon } = await import("./solomon-escalation.js");
|
|
241
|
+
const alertSummary = criticalAlerts.map(a => a.message).join("; ");
|
|
242
|
+
const solomonOpinion = await invokeSolomon({
|
|
243
|
+
config, logger, emitter, eventBase, stage: "brain-dilemma", askQuestion, session, iteration: i,
|
|
244
|
+
conflict: {
|
|
245
|
+
stage: "brain-dilemma",
|
|
246
|
+
task,
|
|
247
|
+
iterationCount: i,
|
|
248
|
+
maxIterations: config.max_iterations,
|
|
249
|
+
dilemma: `Brain detected critical rule alerts: ${alertSummary}. Should we continue iterating, pause for human, or stop?`,
|
|
250
|
+
ruleAlerts: criticalAlerts,
|
|
251
|
+
blockingIssues: blockingIssues || [],
|
|
252
|
+
history: (session.checkpoints || []).filter(cp => cp.stage === "reviewer").slice(-5).map(cp => ({ iteration: cp.iteration, feedback: cp.note || "" }))
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (solomonOpinion.action === "pause") {
|
|
257
|
+
logger.info("Brain: Solomon advised pause — escalating to human");
|
|
258
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonOpinion.question || `Brain+Solomon paused: ${alertSummary}`, context: "brain_solomon_dilemma" } };
|
|
259
|
+
}
|
|
260
|
+
if (solomonOpinion.action === "approve") {
|
|
261
|
+
logger.info("Brain: Solomon advised proceeding — treating as approved");
|
|
262
|
+
return { action: "continue", approved: true };
|
|
263
|
+
}
|
|
264
|
+
// action === "continue" | "subtask" | fallback → Brain continues the loop
|
|
265
|
+
logger.info(`Brain: Solomon said '${solomonOpinion.action}' — continuing loop`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (ciEnabled && session.ci_pr_number) {
|
|
270
|
+
const alerts = rulesResult.alerts || [];
|
|
271
|
+
const alertMsg = alerts.length > 0
|
|
272
|
+
? alerts.map(a => `- [${a.severity}] ${a.message}`).join("\n")
|
|
273
|
+
: "No anomalies detected";
|
|
274
|
+
await tryCiComment({
|
|
275
|
+
config, session, logger,
|
|
276
|
+
agent: "Solomon",
|
|
277
|
+
body: `Supervisor check iteration ${i}: ${alertMsg}`
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
logger.warn(`Solomon rules evaluation failed: ${err.message}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { action: "continue" };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
async function checkSolomonCriticalAlerts({ rulesResult, askQuestion, session, i }) {
|
|
288
|
+
if (!rulesResult.hasCritical || !askQuestion) return null;
|
|
289
|
+
|
|
290
|
+
const alertSummary = rulesResult.alerts
|
|
291
|
+
.filter(a => a.severity === "critical")
|
|
292
|
+
.map(a => a.message)
|
|
293
|
+
.join("\n");
|
|
294
|
+
const question = [
|
|
295
|
+
"Solomon detected critical issues:",
|
|
296
|
+
alertSummary,
|
|
297
|
+
"",
|
|
298
|
+
"What should we do?",
|
|
299
|
+
"1 = Continue anyway",
|
|
300
|
+
"2 = Pause the session",
|
|
301
|
+
"3 = Stop the session",
|
|
302
|
+
"",
|
|
303
|
+
"Type 1, 2, or 3:"
|
|
304
|
+
].join("\n");
|
|
305
|
+
const answer = await askQuestion(question, { iteration: i, stage: "solomon" });
|
|
306
|
+
const trimmed = (answer || "").trim().toLowerCase();
|
|
307
|
+
const shouldPause = !answer
|
|
308
|
+
|| trimmed === "2" || trimmed === "3"
|
|
309
|
+
|| trimmed.startsWith("pause") || trimmed.startsWith("stop");
|
|
310
|
+
if (shouldPause) {
|
|
311
|
+
await pauseSession(session, {
|
|
312
|
+
question: `Solomon supervisor paused: ${alertSummary}`,
|
|
313
|
+
context: { iteration: i, stage: "solomon", alerts: rulesResult.alerts }
|
|
314
|
+
});
|
|
315
|
+
return { action: "pause", result: { paused: true, sessionId: session.id, reason: "solomon_alert" } };
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async function handlePostLoopStages({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, ciEnabled, testerEnabled, securityEnabled, askQuestion, logger, brainCtx }) {
|
|
322
|
+
const postLoopDiff = await generateDiff({ baseRef: session.session_start_sha });
|
|
323
|
+
|
|
324
|
+
if (testerEnabled) {
|
|
325
|
+
const testerResult = await runTesterStage({
|
|
326
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
327
|
+
iteration: i, task, diff: postLoopDiff, askQuestion
|
|
328
|
+
});
|
|
329
|
+
if (testerResult.action === "pause") return { action: "return", result: testerResult.result };
|
|
330
|
+
if (testerResult.action === "continue") {
|
|
331
|
+
const summary = testerResult.stageResult?.summary || "Tester found issues";
|
|
332
|
+
session.last_reviewer_feedback = `Tester FAILED — fix these issues:\n${summary}`;
|
|
333
|
+
await saveSession(session);
|
|
334
|
+
if (testerResult.stageResult) stageResults.tester = testerResult.stageResult;
|
|
335
|
+
// Brain: push tester failure into feedback queue + compress for next coder iteration
|
|
336
|
+
if (brainCtx?.enabled) {
|
|
337
|
+
const { processRoleOutput } = await import("./brain-coordinator.js");
|
|
338
|
+
processRoleOutput(brainCtx, { roleName: "tester", output: testerResult.stageResult, iteration: i });
|
|
339
|
+
}
|
|
340
|
+
return { action: "continue" };
|
|
341
|
+
}
|
|
342
|
+
if (testerResult.stageResult) {
|
|
343
|
+
stageResults.tester = testerResult.stageResult;
|
|
344
|
+
await tryCiComment({ config, session, logger, agent: "Tester", body: `Tests: ${testerResult.stageResult.summary || "completed"}` });
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (securityEnabled) {
|
|
349
|
+
const securityResult = await runSecurityStage({
|
|
350
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
351
|
+
iteration: i, task, diff: postLoopDiff, askQuestion, brainCtx
|
|
352
|
+
});
|
|
353
|
+
if (securityResult.action === "pause") return { action: "return", result: securityResult.result };
|
|
354
|
+
if (securityResult.action === "continue") {
|
|
355
|
+
const summary = securityResult.stageResult?.summary || "Security found issues";
|
|
356
|
+
session.last_reviewer_feedback = `Security FAILED — fix these issues:\n${summary}`;
|
|
357
|
+
await saveSession(session);
|
|
358
|
+
if (securityResult.stageResult) stageResults.security = securityResult.stageResult;
|
|
359
|
+
// Brain: push security findings into feedback queue + compress for next coder iteration
|
|
360
|
+
if (brainCtx?.enabled) {
|
|
361
|
+
const { processRoleOutput } = await import("./brain-coordinator.js");
|
|
362
|
+
processRoleOutput(brainCtx, { roleName: "security", output: securityResult.stageResult, iteration: i });
|
|
363
|
+
}
|
|
364
|
+
return { action: "continue" };
|
|
365
|
+
}
|
|
366
|
+
if (securityResult.stageResult) {
|
|
367
|
+
stageResults.security = securityResult.stageResult;
|
|
368
|
+
await tryCiComment({ config, session, logger, agent: "Security", body: `Security scan: ${securityResult.stageResult.summary || "completed"}` });
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// Final audit — last quality gate before declaring success
|
|
373
|
+
const auditResult = await runFinalAuditStage({
|
|
374
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
375
|
+
iteration: i, task, diff: postLoopDiff
|
|
376
|
+
});
|
|
377
|
+
if (auditResult.stageResult) {
|
|
378
|
+
stageResults.audit = auditResult.stageResult;
|
|
379
|
+
await tryCiComment({ config, session, logger, agent: "Audit", body: `Final audit: ${auditResult.stageResult.summary || "completed"}` });
|
|
380
|
+
}
|
|
381
|
+
if (auditResult.action === "retry") {
|
|
382
|
+
// Audit found actionable issues — loop back to coder
|
|
383
|
+
session.last_reviewer_feedback = auditResult.feedback;
|
|
384
|
+
await saveSession(session);
|
|
385
|
+
return { action: "continue" };
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { action: "proceed" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
async function finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker }) {
|
|
392
|
+
const gitResult = await finalizeGitAutomation({ config, gitCtx, task, logger, session, stageResults });
|
|
393
|
+
|
|
394
|
+
// Accumulate final commits for PG card lifecycle tracking
|
|
395
|
+
if (gitResult?.commits?.length) {
|
|
396
|
+
const { accumulateCommit } = await import("../planning-game/pipeline-adapter.js");
|
|
397
|
+
for (const c of gitResult.commits) accumulateCommit(session, c);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (stageResults.planner?.ok) {
|
|
401
|
+
stageResults.planner.completedSteps = [...(stageResults.planner.steps ?? [])];
|
|
402
|
+
}
|
|
403
|
+
session.budget = budgetSummary();
|
|
404
|
+
await markSessionStatus(session, "approved");
|
|
405
|
+
|
|
406
|
+
const { markPgCardToValidate } = await import("../planning-game/pipeline-adapter.js");
|
|
407
|
+
await markPgCardToValidate({ pgCard, pgProject, config, session, gitResult, logger });
|
|
408
|
+
|
|
409
|
+
const deferredIssues = session.deferred_issues || [];
|
|
410
|
+
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
411
|
+
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
412
|
+
await saveSession(session);
|
|
413
|
+
|
|
414
|
+
// --- Journal: write final files ---
|
|
415
|
+
const journalDir = session._journalDir;
|
|
416
|
+
if (journalDir) {
|
|
417
|
+
try {
|
|
418
|
+
await writeIterationsJournal(journalDir, session._journalIterations || []);
|
|
419
|
+
// decisions.md: writes only when Solomon was invoked (AC of KJC-TSK-0287).
|
|
420
|
+
// Pulls structured data from session.solomonRulings + session.brainDecisions.
|
|
421
|
+
const { writeDecisionsJournal: writeDecisionsJournalV2 } =
|
|
422
|
+
await import("../session/journal/decisions-writer.js");
|
|
423
|
+
const decisionsResult = await writeDecisionsJournalV2(session, { journalDir, logger });
|
|
424
|
+
// KJC-TSK-0288: use the new tree-writer with directory grouping
|
|
425
|
+
const { writeTreeJournal: writeTreeJournalV2 } =
|
|
426
|
+
await import("../session/journal/tree-writer.js");
|
|
427
|
+
const treeResult = await writeTreeJournalV2(journalDir, session.session_start_sha, { logger });
|
|
428
|
+
|
|
429
|
+
const journalFiles = [...(session._journalFiles || [])];
|
|
430
|
+
if (session._journalIterations?.length) journalFiles.push("iterations.md");
|
|
431
|
+
if (decisionsResult.written) journalFiles.push("decisions.md");
|
|
432
|
+
if (treeResult.written) journalFiles.push("tree.txt");
|
|
433
|
+
|
|
434
|
+
// KJC-TSK-0289: use the richer summary writer (stages table, budget
|
|
435
|
+
// breakdown, brain/solomon counts, typed links to other journal files).
|
|
436
|
+
const { writeSummaryJournal: writeSummaryJournalV2 } =
|
|
437
|
+
await import("../session/journal/summary-writer.js");
|
|
438
|
+
const startedAt = session._startedAt ? new Date(session._startedAt).toISOString() : undefined;
|
|
439
|
+
await writeSummaryJournalV2(journalDir, {
|
|
440
|
+
task: session.task,
|
|
441
|
+
result: "APPROVED",
|
|
442
|
+
sessionId: session.id,
|
|
443
|
+
iterations: i,
|
|
444
|
+
durationMs: Date.now() - (session._startedAt || Date.now()),
|
|
445
|
+
budget: budgetSummary(),
|
|
446
|
+
stages: stageResults,
|
|
447
|
+
commits: gitResult?.commits || [],
|
|
448
|
+
files: journalFiles,
|
|
449
|
+
startedAt,
|
|
450
|
+
finishedAt: new Date().toISOString(),
|
|
451
|
+
brainDecisions: Array.isArray(session.brainDecisions) ? session.brainDecisions.length : 0,
|
|
452
|
+
solomonInvocations: Array.isArray(session.solomonRulings) ? session.solomonRulings.length : 0,
|
|
453
|
+
}, { logger });
|
|
454
|
+
logger.info(`Session journal written to ${journalDir}`);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
logger.warn(`Journal write failed (non-blocking): ${err.message}`);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const endDetail = { approved: true, iterations: i, stages: stageResults, git: gitResult, budget: budgetSummary(), deferredIssues };
|
|
461
|
+
if (rtkSavings) endDetail.rtk_savings = rtkSavings;
|
|
462
|
+
emitProgress(
|
|
463
|
+
emitter,
|
|
464
|
+
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
465
|
+
message: deferredIssues.length > 0
|
|
466
|
+
? `Session approved (${deferredIssues.length} deferred issue(s) tracked as tech debt)`
|
|
467
|
+
: "Session approved",
|
|
468
|
+
detail: endDetail
|
|
469
|
+
})
|
|
470
|
+
);
|
|
471
|
+
const result = { approved: true, sessionId: session.id, review, git: gitResult, deferredIssues };
|
|
472
|
+
if (rtkSavings) result.rtk_savings = rtkSavings;
|
|
473
|
+
return result;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// PG card "To Validate" logic moved to src/planning-game/pipeline-adapter.js → markPgCardToValidate()
|
|
477
|
+
|
|
478
|
+
async function handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion }) {
|
|
479
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
480
|
+
.map((x) => {
|
|
481
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
482
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
483
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
484
|
+
return parts.join("\n");
|
|
485
|
+
})
|
|
486
|
+
.join("\n\n");
|
|
487
|
+
session.reviewer_retry_count = (session.reviewer_retry_count || 0) + 1;
|
|
488
|
+
await saveSession(session);
|
|
489
|
+
|
|
490
|
+
const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
|
|
491
|
+
if (session.reviewer_retry_count < maxReviewerRetries) {
|
|
492
|
+
return { action: "continue" };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
emitProgress(
|
|
496
|
+
emitter,
|
|
497
|
+
makeEvent("solomon:escalate", { ...eventBase, stage: "reviewer" }, {
|
|
498
|
+
message: `Reviewer sub-loop limit reached (${session.reviewer_retry_count}/${maxReviewerRetries})`,
|
|
499
|
+
detail: { subloop: "reviewer", retryCount: session.reviewer_retry_count, limit: maxReviewerRetries }
|
|
500
|
+
})
|
|
501
|
+
);
|
|
502
|
+
|
|
503
|
+
const solomonResult = await invokeSolomon({
|
|
504
|
+
config, logger, emitter, eventBase, stage: "reviewer", askQuestion, session, iteration: i,
|
|
505
|
+
conflict: {
|
|
506
|
+
stage: "reviewer",
|
|
507
|
+
task,
|
|
508
|
+
iterationCount: session.reviewer_retry_count,
|
|
509
|
+
maxIterations: maxReviewerRetries,
|
|
510
|
+
history: [{ agent: "reviewer", feedback: session.last_reviewer_feedback }]
|
|
511
|
+
}
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
if (solomonResult.action === "pause") {
|
|
515
|
+
return { action: "return", result: { paused: true, sessionId: session.id, question: solomonResult.question, context: "reviewer_fail_fast" } };
|
|
516
|
+
}
|
|
517
|
+
if (solomonResult.action === "continue") {
|
|
518
|
+
if (solomonResult.humanGuidance) {
|
|
519
|
+
session.last_reviewer_feedback += `\nUser guidance: ${solomonResult.humanGuidance}`;
|
|
520
|
+
}
|
|
521
|
+
session.reviewer_retry_count = 0;
|
|
522
|
+
await saveSession(session);
|
|
523
|
+
return { action: "continue" };
|
|
524
|
+
}
|
|
525
|
+
if (solomonResult.action === "subtask") {
|
|
526
|
+
return { action: "return", result: { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "reviewer_subtask" } };
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return { action: "continue" };
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
async function runPreLoopStages({ config, logger, emitter, eventBase, session, flags, pipelineFlags, coderRole, trackBudget, task, askQuestion, pgTaskId, pgProject, stageResults, brainCtx }) {
|
|
534
|
+
// --- HU Reviewer (first stage, before everything else, opt-in) ---
|
|
535
|
+
const huFile = flags.huFile || null;
|
|
536
|
+
if (flags.enableHuReviewer !== undefined) pipelineFlags.huReviewerEnabled = Boolean(flags.enableHuReviewer);
|
|
537
|
+
if (pipelineFlags.huReviewerEnabled && huFile) {
|
|
538
|
+
const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile, askQuestion, pgStories: null });
|
|
539
|
+
stageResults.huReviewer = huResult.stageResult;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// --- Intent classifier (deterministic pre-triage, opt-in) ---
|
|
543
|
+
if (config.guards?.intent?.enabled) {
|
|
544
|
+
const intentResult = classifyIntent(task, config);
|
|
545
|
+
stageResults.intent = intentResult;
|
|
546
|
+
if (intentResult.classified) {
|
|
547
|
+
emitProgress(emitter, makeEvent("intent:classified", { ...eventBase, stage: "intent" }, {
|
|
548
|
+
message: `Intent classified: ${intentResult.taskType} (${intentResult.level}) — ${intentResult.message}`,
|
|
549
|
+
detail: intentResult
|
|
550
|
+
}));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// --- Discover (pre-triage, opt-in) ---
|
|
555
|
+
if (flags.enableDiscover !== undefined) pipelineFlags.discoverEnabled = Boolean(flags.enableDiscover);
|
|
556
|
+
if (pipelineFlags.discoverEnabled) {
|
|
557
|
+
const discoverResult = await runDiscoverStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
558
|
+
stageResults.discover = discoverResult.stageResult;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// --- Triage (always on) ---
|
|
562
|
+
const triageResult = await runTriageStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget });
|
|
563
|
+
applyTriageOverrides(pipelineFlags, triageResult.roleOverrides);
|
|
564
|
+
stageResults.triage = triageResult.stageResult;
|
|
565
|
+
|
|
566
|
+
// --- Brain decisor (opt-in, intent-driven routing) ---
|
|
567
|
+
//
|
|
568
|
+
// When enabled, translates triage output + task text + CLI overrides into a
|
|
569
|
+
// structured Decision and applies it as pipelineFlags overrides. The rest
|
|
570
|
+
// of the orchestrator continues to branch on pipelineFlags as before — the
|
|
571
|
+
// Brain is a router that adjusts WHICH roles run, not HOW they run.
|
|
572
|
+
//
|
|
573
|
+
// Flag precedence:
|
|
574
|
+
// 1. flags.brain === "off" → forced off
|
|
575
|
+
// 2. flags.brain === "on" → forced on
|
|
576
|
+
// 3. config.brain.decisor.enabled (if set)
|
|
577
|
+
// 4. globalThis.__KJ_DEFAULT_BRAIN_DECISOR (test harness default = false)
|
|
578
|
+
// 5. true in production
|
|
579
|
+
const brainDefault = globalThis.__KJ_DEFAULT_BRAIN_DECISOR ?? true;
|
|
580
|
+
let brainDecisorEnabled = config?.brain?.decisor?.enabled ?? brainDefault;
|
|
581
|
+
if (flags?.brain === "off") brainDecisorEnabled = false;
|
|
582
|
+
if (flags?.brain === "on") brainDecisorEnabled = true;
|
|
583
|
+
if (brainDecisorEnabled) {
|
|
584
|
+
try {
|
|
585
|
+
const { buildDecision, applyDecisionToFlags } = await import("../brain/decisor.js");
|
|
586
|
+
const { createTracker, recordDecision, checkLimits } = await import("../brain/decision-tracker.js");
|
|
587
|
+
const tracker = createTracker(session, { max: config?.brain?.decisor?.maxDecisions });
|
|
588
|
+
const limitStatus = checkLimits(tracker);
|
|
589
|
+
if (limitStatus.status === "ok") {
|
|
590
|
+
let decision = buildDecision({
|
|
591
|
+
triage: triageResult.stageResult,
|
|
592
|
+
task,
|
|
593
|
+
config,
|
|
594
|
+
overrides: {
|
|
595
|
+
// Commander variadic --force-role a b → flags.forceRole = ["a","b"].
|
|
596
|
+
// Back-compat: accept both singular (commander) and plural (legacy / API).
|
|
597
|
+
forceRoles: flags?.forceRole || flags?.forceRoles || [],
|
|
598
|
+
skipRoles: flags?.skipRole || flags?.skipRoles || [],
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
// If the decision is low-confidence or ambiguous, consult Solomon
|
|
602
|
+
// for a refined routing suggestion. Solomon's ruling is advisory —
|
|
603
|
+
// if it fails or returns nothing useful, the baseline decision stands.
|
|
604
|
+
if (decision.consultSolomon) {
|
|
605
|
+
try {
|
|
606
|
+
const { consultSolomonForRouting } = await import("../brain/solomon-consult.js");
|
|
607
|
+
decision = await consultSolomonForRouting({
|
|
608
|
+
decision, triage: triageResult.stageResult, task, config, logger,
|
|
609
|
+
emitter, eventBase, session,
|
|
610
|
+
});
|
|
611
|
+
} catch (err) {
|
|
612
|
+
logger.warn(`Brain → Solomon consult failed (non-blocking): ${err.message}`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const newFlags = applyDecisionToFlags(decision, pipelineFlags);
|
|
616
|
+
Object.assign(pipelineFlags, newFlags);
|
|
617
|
+
recordDecision(tracker, decision, {
|
|
618
|
+
taskType: triageResult.stageResult?.taskType,
|
|
619
|
+
level: triageResult.stageResult?.level,
|
|
620
|
+
});
|
|
621
|
+
emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
|
|
622
|
+
status: "ok",
|
|
623
|
+
message: `Brain routing: ${decision.rolesOn.length} role(s) active — ${decision.rationale}`,
|
|
624
|
+
detail: {
|
|
625
|
+
rolesOn: decision.rolesOn,
|
|
626
|
+
rolesOff: decision.rolesOff,
|
|
627
|
+
confidence: decision.confidence,
|
|
628
|
+
consultSolomon: decision.consultSolomon,
|
|
629
|
+
appliedOverrides: decision.appliedOverrides,
|
|
630
|
+
},
|
|
631
|
+
}));
|
|
632
|
+
} else {
|
|
633
|
+
logger.warn(`Brain decisor skipped: ${limitStatus.detail}`);
|
|
634
|
+
emitProgress(emitter, makeEvent("brain:decision", { ...eventBase, stage: "brain" }, {
|
|
635
|
+
status: "warn",
|
|
636
|
+
message: `Brain decisor skipped: ${limitStatus.status}`,
|
|
637
|
+
detail: limitStatus,
|
|
638
|
+
}));
|
|
639
|
+
}
|
|
640
|
+
} catch (err) {
|
|
641
|
+
logger.warn(`Brain decisor failed (non-blocking): ${err.message}`);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// --- Persist inline domain if provided via --domain flag ---
|
|
646
|
+
if (flags?.domain) {
|
|
647
|
+
try {
|
|
648
|
+
await persistInlineDomain(flags.domain, config.projectDir || process.cwd());
|
|
649
|
+
} catch (err) {
|
|
650
|
+
logger.warn(`Failed to persist inline domain: ${err.message}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// --- Domain Curator (after triage + skill auto-install, before planning phases) ---
|
|
655
|
+
const domainHints = triageResult.stageResult?.domainHints || [];
|
|
656
|
+
if (domainHints.length > 0 || config.projectDir) {
|
|
657
|
+
try {
|
|
658
|
+
const { domainContext, stageResult: dcStageResult } = await runDomainCuratorStage({
|
|
659
|
+
config, logger, emitter, eventBase, session, trackBudget,
|
|
660
|
+
domainHints, askQuestion
|
|
661
|
+
});
|
|
662
|
+
stageResults.domainCurator = dcStageResult;
|
|
663
|
+
if (domainContext) {
|
|
664
|
+
config = { ...config, domainContext };
|
|
665
|
+
}
|
|
666
|
+
} catch (err) {
|
|
667
|
+
logger.warn(`Domain Curator failed (non-blocking): ${err.message}`);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// --- HU Reviewer auto-activation from triage (post-triage, no huFile needed) ---
|
|
672
|
+
const triageRoles = new Set(triageResult.stageResult?.roles || []);
|
|
673
|
+
if (triageRoles.has("hu-reviewer") && !stageResults.huReviewer) {
|
|
674
|
+
pipelineFlags.huReviewerEnabled = true;
|
|
675
|
+
// Feed PG card structured data to hu-reviewer when available
|
|
676
|
+
let pgStories = null;
|
|
677
|
+
if (pgTaskId && pgProject && session.pg_card) {
|
|
678
|
+
const { buildHuStoriesFromPgCard } = await import("../planning-game/pipeline-adapter.js");
|
|
679
|
+
pgStories = buildHuStoriesFromPgCard(session.pg_card);
|
|
680
|
+
}
|
|
681
|
+
const huResult = await runHuReviewerStage({ config, logger, emitter, eventBase, session, coderRole, trackBudget, huFile: null, askQuestion, pgStories });
|
|
682
|
+
stageResults.huReviewer = huResult.stageResult;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// --- Auto-simplify pipeline for simple tasks (before explicit flag overrides) ---
|
|
686
|
+
const simplified = applyAutoSimplify({
|
|
687
|
+
pipelineFlags,
|
|
688
|
+
triageLevel: triageResult.stageResult?.level || null,
|
|
689
|
+
config, flags, logger, emitter, eventBase
|
|
690
|
+
});
|
|
691
|
+
if (simplified) stageResults.triage.autoSimplified = true;
|
|
692
|
+
|
|
693
|
+
const { handlePgDecomposition } = await import("../planning-game/pipeline-adapter.js");
|
|
694
|
+
await handlePgDecomposition({ triageResult, pgTaskId, pgProject, config, askQuestion, emitter, eventBase, session, stageResults, logger });
|
|
695
|
+
|
|
696
|
+
applyFlagOverrides(pipelineFlags, flags);
|
|
697
|
+
|
|
698
|
+
// --- Auto-detect TDD applicability when methodology not explicitly set ---
|
|
699
|
+
if (!flags.methodology) {
|
|
700
|
+
const projectDir = config.projectDir || process.cwd();
|
|
701
|
+
const detection = await detectTestFramework(projectDir);
|
|
702
|
+
if (!detection.hasTests) {
|
|
703
|
+
config = { ...config, development: { ...config.development, methodology: "standard", require_test_changes: false } };
|
|
704
|
+
logger.info("No test framework detected — using standard methodology");
|
|
705
|
+
} else {
|
|
706
|
+
config = { ...config, development: { ...config.development, methodology: "tdd", require_test_changes: true } };
|
|
707
|
+
logger.info(`Test framework detected (${detection.framework}) — using TDD methodology`);
|
|
708
|
+
}
|
|
709
|
+
emitProgress(emitter, makeEvent("tdd:auto-detect", { ...eventBase, stage: "pre-loop" }, {
|
|
710
|
+
message: detection.hasTests
|
|
711
|
+
? `TDD auto-detected: ${detection.framework}`
|
|
712
|
+
: "TDD skipped: no test framework found",
|
|
713
|
+
detail: detection
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let updatedConfig = resolvePipelinePolicies({ flags, config, stageResults, emitter, eventBase, session, pipelineFlags });
|
|
718
|
+
|
|
719
|
+
// --- Preflight environment checks ---
|
|
720
|
+
const preflightResult = await runPreflightChecks({
|
|
721
|
+
config: updatedConfig, logger, emitter, eventBase,
|
|
722
|
+
resolvedPolicies: session.resolved_policies,
|
|
723
|
+
securityEnabled: pipelineFlags.securityEnabled
|
|
724
|
+
});
|
|
725
|
+
session.preflight = preflightResult;
|
|
726
|
+
await saveSession(session);
|
|
727
|
+
|
|
728
|
+
// Hard fail if blocking checks failed (SonarQube enabled but not available)
|
|
729
|
+
if (!preflightResult.ok) {
|
|
730
|
+
const errorLines = (preflightResult.errors || [])
|
|
731
|
+
.map(e => ` - ${e.message}\n Fix: ${e.fix}`)
|
|
732
|
+
.join("\n");
|
|
733
|
+
throw new Error(
|
|
734
|
+
`Preflight FAILED — environment changed during session. Fix the issues and retry:\n${errorLines}`
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (preflightResult.configOverrides.securityDisabled) {
|
|
739
|
+
pipelineFlags.securityEnabled = false;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// --- Plan injection: skip researcher/architect/planner if a persisted plan is loaded ---
|
|
743
|
+
if (flags.plan) {
|
|
744
|
+
try {
|
|
745
|
+
const { loadPlan, savePlan: savePlanToDisk } = await import("../plan/plan-store.js");
|
|
746
|
+
const { isPlanV2 } = await import("../plan/plan-schema.js");
|
|
747
|
+
const { planToHuBatch, syncResultsToPlan } = await import("../plan/plan-executor.js");
|
|
748
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
749
|
+
const loadedPlan = await loadPlan(projectDir, flags.plan);
|
|
750
|
+
if (loadedPlan) {
|
|
751
|
+
logger.info(`Loaded persisted plan: ${flags.plan} (v${loadedPlan.version || 1})`);
|
|
752
|
+
emitProgress(emitter, makeEvent("plan:loaded", { ...eventBase, stage: "plan" }, {
|
|
753
|
+
message: `Plan loaded: ${flags.plan}`,
|
|
754
|
+
detail: { planId: flags.plan, task: loadedPlan.task, version: loadedPlan.version, huCount: loadedPlan.hus?.length || 0 }
|
|
755
|
+
}));
|
|
756
|
+
stageResults.researcher = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
757
|
+
stageResults.architect = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
758
|
+
stageResults.planner = { ok: true, summary: "Loaded from persisted plan", fromPlan: flags.plan };
|
|
759
|
+
|
|
760
|
+
// v2 plan with HUs → inject as huReviewer so sub-pipeline picks them up
|
|
761
|
+
if (isPlanV2(loadedPlan) && loadedPlan.hus?.length > 0) {
|
|
762
|
+
const huBatch = planToHuBatch(loadedPlan);
|
|
763
|
+
if (huBatch.ok) {
|
|
764
|
+
stageResults.huReviewer = huBatch;
|
|
765
|
+
// Store plan reference so we can sync results back after execution
|
|
766
|
+
session._planRef = { planId: loadedPlan.planId, projectDir };
|
|
767
|
+
session._syncResultsToPlan = async (subResult) => {
|
|
768
|
+
syncResultsToPlan(loadedPlan, subResult);
|
|
769
|
+
await savePlanToDisk(projectDir, loadedPlan);
|
|
770
|
+
};
|
|
771
|
+
logger.info(`Plan ${flags.plan}: ${huBatch.certified} HUs ready for execution`);
|
|
772
|
+
emitProgress(emitter, makeEvent("plan:hus-loaded", { ...eventBase, stage: "plan" }, {
|
|
773
|
+
message: `${huBatch.certified} HUs loaded from plan`,
|
|
774
|
+
detail: { planId: flags.plan, total: huBatch.total, certified: huBatch.certified }
|
|
775
|
+
}));
|
|
776
|
+
// Mark plan as running
|
|
777
|
+
loadedPlan.status = "running";
|
|
778
|
+
await savePlanToDisk(projectDir, loadedPlan);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Inject contexts
|
|
783
|
+
const ctx = loadedPlan.context || {};
|
|
784
|
+
session.research_context = ctx.researchContext || loadedPlan.researchContext || null;
|
|
785
|
+
session.architect_context = ctx.architectContext || loadedPlan.architectContext || null;
|
|
786
|
+
session.loaded_plan = loadedPlan.approach || loadedPlan.plan || null;
|
|
787
|
+
await saveSession(session);
|
|
788
|
+
|
|
789
|
+
// Build planned task from plan (for non-HU or fallback path)
|
|
790
|
+
let plannedTask = task;
|
|
791
|
+
const plan = loadedPlan.plan || (loadedPlan.approach ? { approach: loadedPlan.approach } : null);
|
|
792
|
+
if (plan && typeof plan === "object" && plan.steps) {
|
|
793
|
+
const stepList = plan.steps.map((s, idx) => `${idx + 1}. ${s.description || s}`).join("\n");
|
|
794
|
+
plannedTask = `${task}\n\n## Implementation Plan\n${plan.approach || ""}\n\n## Steps\n${stepList}`;
|
|
795
|
+
} else if (typeof plan === "string") {
|
|
796
|
+
plannedTask = `${task}\n\n## Implementation Plan\n${plan}`;
|
|
797
|
+
} else if (loadedPlan.approach) {
|
|
798
|
+
plannedTask = `${task}\n\n## Approach\n${loadedPlan.approach}`;
|
|
799
|
+
}
|
|
800
|
+
return { plannedTask, updatedConfig };
|
|
801
|
+
}
|
|
802
|
+
logger.warn(`Plan ${flags.plan} not found — falling back to normal pipeline`);
|
|
803
|
+
} catch (err) {
|
|
804
|
+
logger.warn(`Plan loading failed: ${err.message} — falling back to normal pipeline`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// --- Researcher → Planner ---
|
|
809
|
+
const { plannedTask } = await runPlanningPhases({ config: updatedConfig, logger, emitter, eventBase, session, stageResults, pipelineFlags, coderRole, trackBudget, task, askQuestion, brainCtx });
|
|
810
|
+
|
|
811
|
+
// --- Update .gitignore with stack-specific entries based on planner/architect output ---
|
|
812
|
+
const projectDir = updatedConfig.projectDir || process.cwd();
|
|
813
|
+
await updateGitignoreForStack(projectDir, { stageResults, task, logger });
|
|
814
|
+
|
|
815
|
+
// --- Auto-HU: when triage recommended decomposition and no manual huFile, generate HU batch ---
|
|
816
|
+
await maybeGenerateAutoHuBatch({
|
|
817
|
+
flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
// --- Auto-install skills based on task + planner output + project detection ---
|
|
821
|
+
// Runs AFTER triage and planner so that the planned task text (which includes
|
|
822
|
+
// planner output like implementation steps) is available for keyword detection.
|
|
823
|
+
// This ensures greenfield projects with no package.json still get correct skills.
|
|
824
|
+
//
|
|
825
|
+
// Detection runs UNCONDITIONALLY — even when the openskills CLI is missing —
|
|
826
|
+
// so the session report can recommend which skills would have been used. The
|
|
827
|
+
// actual install is skipped inside autoInstallSkills when unavailable.
|
|
828
|
+
const skillProjectDir = updatedConfig.projectDir || process.cwd();
|
|
829
|
+
try {
|
|
830
|
+
const skillsMode = resolveSkillsMode(updatedConfig, flags);
|
|
831
|
+
if (skillsMode === "none") {
|
|
832
|
+
// User explicitly opted out. Skip detection entirely.
|
|
833
|
+
return { plannedTask, updatedConfig };
|
|
834
|
+
}
|
|
835
|
+
let neededSkills = skillsMode === "semantic"
|
|
836
|
+
? []
|
|
837
|
+
: await detectNeededSkills(plannedTask, skillProjectDir);
|
|
838
|
+
// Semantic mode augments regex detection with a classifier call.
|
|
839
|
+
// "auto" = regex always + semantic when budget allows (v1: always when mode=auto and classifier reachable).
|
|
840
|
+
if (skillsMode === "auto" || skillsMode === "semantic") {
|
|
841
|
+
const extra = await refineSkillsSemantically({
|
|
842
|
+
task: plannedTask,
|
|
843
|
+
alreadyDetected: neededSkills,
|
|
844
|
+
config: updatedConfig,
|
|
845
|
+
logger,
|
|
846
|
+
});
|
|
847
|
+
if (extra.length > 0) {
|
|
848
|
+
logger.info(`Semantic skill detection added: ${extra.join(", ")}`);
|
|
849
|
+
neededSkills = Array.from(new Set([...neededSkills, ...extra]));
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
if (neededSkills.length > 0) {
|
|
853
|
+
const skillResult = await autoInstallSkills(neededSkills, skillProjectDir);
|
|
854
|
+
if (skillResult.installed.length > 0) {
|
|
855
|
+
session.autoInstalledSkills = skillResult.installed;
|
|
856
|
+
}
|
|
857
|
+
if (skillResult.osAvailable === false && skillResult.wouldHaveUsed.length > 0) {
|
|
858
|
+
session.skillsRecommended = skillResult.wouldHaveUsed;
|
|
859
|
+
logger.warn(`OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}. Install openskills globally to auto-inject them next time.`);
|
|
860
|
+
emitProgress(emitter, makeEvent("skills:unavailable", { ...eventBase, stage: "skills" }, {
|
|
861
|
+
message: `OpenSkills CLI not available — would have used: ${skillResult.wouldHaveUsed.join(", ")}`,
|
|
862
|
+
detail: {
|
|
863
|
+
wouldHaveUsed: skillResult.wouldHaveUsed,
|
|
864
|
+
hint: "Install openskills globally: npm install -g openskills",
|
|
865
|
+
},
|
|
866
|
+
}));
|
|
867
|
+
}
|
|
868
|
+
emitProgress(emitter, makeEvent("skills:auto-install", { ...eventBase, stage: "skills" }, {
|
|
869
|
+
message: skillResult.installed.length > 0
|
|
870
|
+
? `Auto-installed ${skillResult.installed.length} skill(s): ${skillResult.installed.join(", ")}`
|
|
871
|
+
: `Skills detected (${neededSkills.join(", ")}) — all already installed or unavailable`,
|
|
872
|
+
detail: skillResult,
|
|
873
|
+
}));
|
|
874
|
+
}
|
|
875
|
+
} catch (err) {
|
|
876
|
+
logger.warn(`Skill auto-install failed (non-blocking): ${err.message}`);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return { plannedTask, updatedConfig };
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
/**
|
|
883
|
+
* Auto-generate HU batch from triage decomposition when no manual huFile is present.
|
|
884
|
+
* Runs after researcher/architect/planner so that context is available for better HUs.
|
|
885
|
+
* Sets stageResults.huReviewer so needsSubPipeline picks it up later.
|
|
886
|
+
*/
|
|
887
|
+
async function maybeGenerateAutoHuBatch({ flags, stageResults, task, plannedTask, logger, emitter, eventBase, projectDir, session }) {
|
|
888
|
+
// Skip if user passed a manual hu-file
|
|
889
|
+
if (flags?.huFile) return;
|
|
890
|
+
// Skip if hu-reviewer already produced a batch (manual enable + PG stories)
|
|
891
|
+
if (stageResults.huReviewer) return;
|
|
892
|
+
// Need triage decomposition recommendation
|
|
893
|
+
const shouldDecompose = stageResults.triage?.shouldDecompose;
|
|
894
|
+
const subtasks = stageResults.triage?.subtasks;
|
|
895
|
+
if (!shouldDecompose || !Array.isArray(subtasks) || subtasks.length < 2) return;
|
|
896
|
+
|
|
897
|
+
const { generateHuBatch } = await import("../hu/auto-generator.js");
|
|
898
|
+
|
|
899
|
+
// Detect if project is new: empty dir or only .git/.karajan/.gitignore
|
|
900
|
+
let isNewProject = false;
|
|
901
|
+
try {
|
|
902
|
+
const fs = await import("node:fs/promises");
|
|
903
|
+
const entries = await fs.readdir(projectDir);
|
|
904
|
+
const relevant = entries.filter(e => !e.startsWith(".git") && e !== ".karajan" && e !== ".gitignore");
|
|
905
|
+
isNewProject = relevant.length === 0;
|
|
906
|
+
} catch { /* ignore */ }
|
|
907
|
+
|
|
908
|
+
// Extract stack hints from planner + architect output
|
|
909
|
+
const stackHints = [];
|
|
910
|
+
const combined = `${stageResults.planner?.plan || ""} ${stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : ""} ${task}`.toLowerCase();
|
|
911
|
+
const stackKeywords = ["express", "vite", "vitest", "jest", "next", "astro", "react", "vue", "svelte", "fastapi", "django", "spring", "gin", "nestjs", "monorepo", "workspaces"];
|
|
912
|
+
for (const kw of stackKeywords) {
|
|
913
|
+
if (combined.includes(kw)) stackHints.push(kw);
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const batch = generateHuBatch({
|
|
917
|
+
originalTask: task,
|
|
918
|
+
subtasks,
|
|
919
|
+
stackHints,
|
|
920
|
+
isNewProject,
|
|
921
|
+
researcherContext: stageResults.researcher?.summary || null,
|
|
922
|
+
architectContext: stageResults.architect?.architecture ? JSON.stringify(stageResults.architect.architecture) : null
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
// Persist batch to HU store so hu-sub-pipeline can update story status via saveHuBatch.
|
|
926
|
+
// Use session.id as batchSessionId.
|
|
927
|
+
const batchSessionId = `auto-${session.id}`;
|
|
928
|
+
try {
|
|
929
|
+
const fs = await import("node:fs/promises");
|
|
930
|
+
const path = await import("node:path");
|
|
931
|
+
const { getKarajanHome } = await import("../utils/paths.js");
|
|
932
|
+
const huDir = path.join(getKarajanHome(), "hu-stories", batchSessionId);
|
|
933
|
+
await fs.mkdir(huDir, { recursive: true });
|
|
934
|
+
const persistBatch = {
|
|
935
|
+
session_id: batchSessionId,
|
|
936
|
+
project_id: batchSessionId,
|
|
937
|
+
project_name: batch.projectName,
|
|
938
|
+
created_at: new Date().toISOString(),
|
|
939
|
+
updated_at: new Date().toISOString(),
|
|
940
|
+
stories: batch.stories
|
|
941
|
+
};
|
|
942
|
+
await fs.writeFile(path.join(huDir, "batch.json"), JSON.stringify(persistBatch, null, 2));
|
|
943
|
+
} catch (err) {
|
|
944
|
+
logger.warn(`Auto-HU: failed to persist batch (${err.message}) — sub-pipeline will use in-memory fallback`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// Wrap in format compatible with needsSubPipeline + runHuSubPipeline
|
|
948
|
+
stageResults.huReviewer = {
|
|
949
|
+
ok: true,
|
|
950
|
+
stories: batch.stories,
|
|
951
|
+
total: batch.total,
|
|
952
|
+
certified: batch.certified,
|
|
953
|
+
batchSessionId,
|
|
954
|
+
auto_generated: true,
|
|
955
|
+
source: batch.source
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
logger.info(`Auto-HU: generated ${batch.total} stories (${batch.source.triage_subtasks} subtasks${isNewProject ? ", new project" : ""}${stackHints.length ? `, stack: ${stackHints.join(",")}` : ""})`);
|
|
959
|
+
emitProgress(emitter, makeEvent("hu:auto-generated", { ...eventBase, stage: "hu-auto-gen" }, {
|
|
960
|
+
message: `Auto-generated ${batch.total} HU(s) from triage decomposition`,
|
|
961
|
+
detail: { total: batch.total, subtasks: batch.source.triage_subtasks, isNewProject, stackHints, projectName: batch.projectName }
|
|
962
|
+
}));
|
|
963
|
+
|
|
964
|
+
// Auto-start the board so the user can see the generated HUs.
|
|
965
|
+
// Always fires when auto-HU runs, independent of hu_board.auto_start flag.
|
|
966
|
+
// Never during vitest — would race the PID file and leave a detached
|
|
967
|
+
// node process around after the suite (TSK-0273).
|
|
968
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
969
|
+
try {
|
|
970
|
+
const { startBoard, renderBoardBanner } = await import("../commands/board.js");
|
|
971
|
+
const desiredPort = session.config_snapshot?.hu_board?.port ?? 4000;
|
|
972
|
+
const boardResult = await startBoard(desiredPort);
|
|
973
|
+
const url = boardResult.url;
|
|
974
|
+
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
975
|
+
const projectName = batch.projectName || "Auto-generated HUs";
|
|
976
|
+
console.log(renderBoardBanner({ url, status, projectName }));
|
|
977
|
+
logger.info(`HU Board ${status} at ${url} (project: ${projectName})`);
|
|
978
|
+
} catch (err) {
|
|
979
|
+
logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function runCoderAndRefactorerStages({ coderRoleInstance, coderRole, refactorerRole, pipelineFlags, config, logger, emitter, eventBase, session, plannedTask, trackBudget, i, brainCtx }) {
|
|
984
|
+
const coderResult = await runCoderStage({ coderRoleInstance, coderRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i, brainCtx });
|
|
985
|
+
if (coderResult?.action === "pause") return { action: "return", result: coderResult.result };
|
|
986
|
+
const coderStandby = await handleStandbyResult({ stageResult: coderResult, session, emitter, eventBase, i, stage: "coder", logger, config });
|
|
987
|
+
if (coderStandby.handled) {
|
|
988
|
+
return coderStandby.action === "return"
|
|
989
|
+
? { action: "return", result: coderStandby.result }
|
|
990
|
+
: { action: "retry" };
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
if (pipelineFlags.refactorerEnabled) {
|
|
994
|
+
const refResult = await runRefactorerStage({ refactorerRole, config, logger, emitter, eventBase, session, plannedTask, trackBudget, iteration: i });
|
|
995
|
+
if (refResult?.action === "pause") return { action: "return", result: refResult.result };
|
|
996
|
+
const refStandby = await handleStandbyResult({ stageResult: refResult, session, emitter, eventBase, i, stage: "refactorer", logger, config });
|
|
997
|
+
if (refStandby.handled) {
|
|
998
|
+
return refStandby.action === "return"
|
|
999
|
+
? { action: "return", result: refStandby.result }
|
|
1000
|
+
: { action: "retry" };
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return { action: "ok" };
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
async function runGuardStages({ config, logger, emitter, eventBase, session, iteration }) {
|
|
1008
|
+
const outputEnabled = config.guards?.output?.enabled !== false;
|
|
1009
|
+
const perfEnabled = config.guards?.perf?.enabled !== false;
|
|
1010
|
+
|
|
1011
|
+
if (!outputEnabled && !perfEnabled) return { action: "ok" };
|
|
1012
|
+
|
|
1013
|
+
const baseBranch = config.base_branch || "main";
|
|
1014
|
+
let diff;
|
|
1015
|
+
try {
|
|
1016
|
+
const { generateDiff: genDiff, computeBaseRef: compBase } = await import("../review/diff-generator.js");
|
|
1017
|
+
const baseRef = await compBase({ baseBranch });
|
|
1018
|
+
diff = await genDiff({ baseRef });
|
|
1019
|
+
} catch {
|
|
1020
|
+
logger.warn("Guards: could not generate diff, skipping");
|
|
1021
|
+
return { action: "ok" };
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (!diff) return { action: "ok" };
|
|
1025
|
+
|
|
1026
|
+
if (outputEnabled) {
|
|
1027
|
+
const outputResult = scanDiff(diff, config);
|
|
1028
|
+
if (outputResult.violations.length > 0) {
|
|
1029
|
+
const critical = outputResult.violations.filter(v => v.severity === "critical");
|
|
1030
|
+
const warnings = outputResult.violations.filter(v => v.severity === "warning");
|
|
1031
|
+
emitProgress(emitter, makeEvent("guard:output", { ...eventBase, stage: "guard" }, {
|
|
1032
|
+
message: `Output guard: ${critical.length} critical, ${warnings.length} warnings`,
|
|
1033
|
+
detail: { violations: outputResult.violations, executorType: "local" }
|
|
1034
|
+
}));
|
|
1035
|
+
logger.info(`Output guard: ${outputResult.violations.length} violation(s) found`);
|
|
1036
|
+
for (const v of outputResult.violations) {
|
|
1037
|
+
logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
|
|
1038
|
+
}
|
|
1039
|
+
await addCheckpoint(session, { stage: "guard-output", iteration, pass: outputResult.pass, violations: outputResult.violations.length });
|
|
1040
|
+
|
|
1041
|
+
if (!outputResult.pass && config.guards.output.on_violation === "block") {
|
|
1042
|
+
await markSessionStatus(session, "failed");
|
|
1043
|
+
emitProgress(emitter, makeEvent("guard:blocked", { ...eventBase, stage: "guard" }, {
|
|
1044
|
+
message: "Output guard blocked: critical violations detected",
|
|
1045
|
+
detail: { violations: critical }
|
|
1046
|
+
}));
|
|
1047
|
+
return {
|
|
1048
|
+
action: "return",
|
|
1049
|
+
result: { approved: false, sessionId: session.id, reason: "guard_blocked", violations: critical }
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
if (perfEnabled) {
|
|
1056
|
+
const perfResult = scanPerfDiff(diff, config);
|
|
1057
|
+
if (!perfResult.skipped && perfResult.violations.length > 0) {
|
|
1058
|
+
emitProgress(emitter, makeEvent("guard:perf", { ...eventBase, stage: "guard" }, {
|
|
1059
|
+
message: `Perf guard: ${perfResult.violations.length} issue(s)`,
|
|
1060
|
+
detail: { violations: perfResult.violations, executorType: "local" }
|
|
1061
|
+
}));
|
|
1062
|
+
logger.info(`Perf guard: ${perfResult.violations.length} issue(s) found`);
|
|
1063
|
+
for (const v of perfResult.violations) {
|
|
1064
|
+
logger.info(` [${v.severity}] ${v.file}:${v.line} — ${v.message}`);
|
|
1065
|
+
}
|
|
1066
|
+
await addCheckpoint(session, { stage: "guard-perf", iteration, pass: perfResult.pass, violations: perfResult.violations.length });
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return { action: "ok" };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
async function runQualityGateStages({ config, logger, emitter, eventBase, session, trackBudget, i, askQuestion, repeatDetector, budgetSummary, sonarState, task, stageResults, coderRole, pipelineFlags, brainCtx }) {
|
|
1074
|
+
const tddResult = await runTddCheckStage({ config, logger, emitter, eventBase, session, trackBudget, iteration: i, askQuestion, task, brainCtx });
|
|
1075
|
+
if (tddResult.action === "pause") return { action: "return", result: tddResult.result };
|
|
1076
|
+
if (tddResult.action === "continue") return { action: "continue" };
|
|
1077
|
+
|
|
1078
|
+
const skipSonarForTaskType = new Set(["infra", "doc", "no-code"]);
|
|
1079
|
+
const effectiveTaskType = session.resolved_policies?.taskType || null;
|
|
1080
|
+
if (config.sonarqube.enabled && !skipSonarForTaskType.has(effectiveTaskType)) {
|
|
1081
|
+
const sonarResult = await runSonarStage({
|
|
1082
|
+
config, logger, emitter, eventBase, session, trackBudget, iteration: i,
|
|
1083
|
+
repeatDetector, budgetSummary, sonarState, askQuestion, task, brainCtx
|
|
1084
|
+
});
|
|
1085
|
+
if (sonarResult.action === "stalled" || sonarResult.action === "pause") return { action: "return", result: sonarResult.result };
|
|
1086
|
+
if (sonarResult.action === "continue") return { action: "continue" };
|
|
1087
|
+
if (sonarResult.stageResult) {
|
|
1088
|
+
stageResults.sonar = sonarResult.stageResult;
|
|
1089
|
+
await tryCiComment({ config, session, logger, agent: "Sonar", body: `SonarQube scan: ${sonarResult.stageResult.summary || "completed"}` });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (config.sonarcloud?.enabled) {
|
|
1094
|
+
const cloudResult = await runSonarCloudStage({
|
|
1095
|
+
config, logger, emitter, eventBase, session, trackBudget, iteration: i
|
|
1096
|
+
});
|
|
1097
|
+
if (cloudResult.stageResult) {
|
|
1098
|
+
stageResults.sonarcloud = cloudResult.stageResult;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (pipelineFlags?.impeccableEnabled) {
|
|
1103
|
+
const diff = await generateDiff({ baseRef: session.session_start_sha });
|
|
1104
|
+
const impeccableMode = pipelineFlags?.impeccableMode || "audit";
|
|
1105
|
+
const impeccableResult = await runImpeccableStage({
|
|
1106
|
+
config, logger, emitter, eventBase, session, coderRole, trackBudget,
|
|
1107
|
+
iteration: i, task, diff, mode: impeccableMode
|
|
1108
|
+
});
|
|
1109
|
+
if (impeccableResult.stageResult) {
|
|
1110
|
+
stageResults.impeccable = impeccableResult.stageResult;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
return { action: "ok" };
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
async function runReviewerGateStage({ pipelineFlags, reviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx }) {
|
|
1118
|
+
if (!pipelineFlags.reviewerEnabled) {
|
|
1119
|
+
return {
|
|
1120
|
+
action: "ok",
|
|
1121
|
+
review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Reviewer disabled by pipeline", confidence: 1 }
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
const reviewerResult = await runReviewerStage({
|
|
1126
|
+
reviewerRole, config, logger, emitter, eventBase, session, trackBudget,
|
|
1127
|
+
iteration: i, reviewRules, task, repeatDetector, budgetSummary, askQuestion, brainCtx
|
|
1128
|
+
});
|
|
1129
|
+
if (reviewerResult.action === "pause") return { action: "return", result: reviewerResult.result };
|
|
1130
|
+
const revStandby = await handleStandbyResult({ stageResult: reviewerResult, session, emitter, eventBase, i, stage: "reviewer", logger, config, askQuestion });
|
|
1131
|
+
if (revStandby.handled) {
|
|
1132
|
+
if (revStandby.action === "return") return { action: "return", result: revStandby.result };
|
|
1133
|
+
if (revStandby.action === "skip") {
|
|
1134
|
+
// Solomon said skip review — treat as approved
|
|
1135
|
+
return { action: "ok", review: { approved: true, blocking_issues: [], non_blocking_suggestions: [], summary: "Review skipped (agent rate-limited, Solomon approved)", confidence: 0.7 } };
|
|
1136
|
+
}
|
|
1137
|
+
if (revStandby.action === "retry_reviewer_only") {
|
|
1138
|
+
// Retry just the reviewer — use alternative agent if Solomon recommended one
|
|
1139
|
+
let retryReviewerRole = reviewerRole;
|
|
1140
|
+
const alt = session._alternative_agent;
|
|
1141
|
+
if (alt?.stage === "reviewer" && alt?.provider) {
|
|
1142
|
+
const { createAgent } = await import("../agents/index.js");
|
|
1143
|
+
retryReviewerRole = { provider: alt.provider, model: null };
|
|
1144
|
+
logger.info(`Retrying reviewer with alternative agent: ${alt.provider}`);
|
|
1145
|
+
delete session._alternative_agent;
|
|
1146
|
+
}
|
|
1147
|
+
return runReviewerGateStage({ pipelineFlags: { reviewerEnabled: true }, reviewerRole: retryReviewerRole, config, logger, emitter, eventBase, session, trackBudget, i, reviewRules, task, repeatDetector, budgetSummary, askQuestion });
|
|
1148
|
+
}
|
|
1149
|
+
return { action: "retry" };
|
|
1150
|
+
}
|
|
1151
|
+
if (reviewerResult.stalled) return { action: "return", result: reviewerResult.stalledResult };
|
|
1152
|
+
return { action: "ok", review: reviewerResult.review };
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
async function handleApprovedReview({ config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults, pipelineFlags, askQuestion, logger, gitCtx, budgetSummary, pgCard, pgProject, review, rtkTracker, brainCtx }) {
|
|
1156
|
+
session.reviewer_retry_count = 0;
|
|
1157
|
+
const postLoopResult = await handlePostLoopStages({
|
|
1158
|
+
config, session, emitter, eventBase, coderRole, trackBudget, i, task, stageResults,
|
|
1159
|
+
ciEnabled: Boolean(config.ci?.enabled), testerEnabled: pipelineFlags.testerEnabled, securityEnabled: pipelineFlags.securityEnabled, askQuestion, logger, brainCtx
|
|
1160
|
+
});
|
|
1161
|
+
if (postLoopResult.action === "return") return { action: "return", result: postLoopResult.result };
|
|
1162
|
+
if (postLoopResult.action === "continue") return { action: "continue" };
|
|
1163
|
+
|
|
1164
|
+
const result = await finalizeApprovedSession({ config, gitCtx, task, logger, session, stageResults, emitter, eventBase, budgetSummary, pgCard, pgProject, review, i, rtkTracker });
|
|
1165
|
+
return { action: "return", result };
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
async function handleMaxIterationsReached({ session, budgetSummary, emitter, eventBase, config, stageResults, logger, askQuestion, task, rtkTracker, brainCtx }) {
|
|
1169
|
+
const budget = budgetSummary();
|
|
1170
|
+
|
|
1171
|
+
// Brain-owned decision: max_iterations is guidance, not a hard rule.
|
|
1172
|
+
// Brain evaluates the feedback queue state to decide extend / finalize / escalate.
|
|
1173
|
+
// Solomon is only consulted if Brain cannot decide on its own.
|
|
1174
|
+
if (brainCtx?.enabled) {
|
|
1175
|
+
const entries = brainCtx.feedbackQueue?.entries || [];
|
|
1176
|
+
const pending = entries.map(e => ({ source: e.source, category: e.category, severity: e.severity, description: e.description }));
|
|
1177
|
+
const hasSecurity = entries.some(e => e.category === "security" || e.source === "security");
|
|
1178
|
+
const hasCorrectness = entries.some(e => ["correctness", "tests"].includes(e.category));
|
|
1179
|
+
const hasStyleOnly = entries.length > 0 && !hasSecurity && !hasCorrectness;
|
|
1180
|
+
|
|
1181
|
+
if (hasSecurity) {
|
|
1182
|
+
// Brain: security issues unresolved → cannot finalize, escalate
|
|
1183
|
+
logger.warn(`Brain: max_iterations reached with ${entries.filter(e => e.category === "security" || e.source === "security").length} unresolved security issue(s) — cannot finalize`);
|
|
1184
|
+
return { paused: true, sessionId: session.id, question: "Brain: unresolved security issues at max_iterations. Review manually or extend pipeline.", context: "brain_security_block", pending };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
if (hasCorrectness) {
|
|
1188
|
+
// Brain: correctness/test issues pending. Cap at MAX_EXTENSIONS to avoid infinite extensions.
|
|
1189
|
+
const MAX_EXTENSIONS = 2;
|
|
1190
|
+
if (brainCtx.extensionCount >= MAX_EXTENSIONS) {
|
|
1191
|
+
logger.warn(`Brain: ${brainCtx.extensionCount} extensions exhausted with correctness issues still pending — escalating to human`);
|
|
1192
|
+
return { paused: true, sessionId: session.id, question: `Brain exhausted ${MAX_EXTENSIONS} extensions with correctness/tests still pending. Manual intervention needed.`, context: "brain_extension_cap", pending };
|
|
1193
|
+
}
|
|
1194
|
+
brainCtx.extensionCount += 1;
|
|
1195
|
+
logger.info(`Brain: max_iterations reached with ${entries.filter(e => ["correctness", "tests"].includes(e.category)).length} correctness issue(s) pending — extending iterations (extension ${brainCtx.extensionCount}/${MAX_EXTENSIONS})`);
|
|
1196
|
+
session.reviewer_retry_count = 0;
|
|
1197
|
+
await saveSession(session);
|
|
1198
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: Math.ceil(config.max_iterations / 2) };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (entries.length === 0) {
|
|
1202
|
+
// Brain: no pending feedback → last reviewer approved, finalize
|
|
1203
|
+
logger.info("Brain: max_iterations reached with clean feedback queue — finalizing as approved");
|
|
1204
|
+
return { approved: true, sessionId: session.id, reason: "brain_approved" };
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// hasStyleOnly: genuine dilemma → Brain consults Solomon
|
|
1208
|
+
logger.info(`Brain: max_iterations with ${entries.length} style-only issue(s) — consulting Solomon on dilemma`);
|
|
1209
|
+
const { invokeSolomon: invokeSolomonAI } = await import("./solomon-escalation.js");
|
|
1210
|
+
const solomonResult = await invokeSolomonAI({
|
|
1211
|
+
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
1212
|
+
iteration: config.max_iterations,
|
|
1213
|
+
conflict: {
|
|
1214
|
+
stage: "brain-max-iterations",
|
|
1215
|
+
task,
|
|
1216
|
+
iterationCount: config.max_iterations,
|
|
1217
|
+
maxIterations: config.max_iterations,
|
|
1218
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
1219
|
+
dilemma: `Max iterations reached with ${entries.length} style-only issue(s) pending. Accept as-is or request more work?`,
|
|
1220
|
+
pendingIssues: pending,
|
|
1221
|
+
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached" }]
|
|
1222
|
+
}
|
|
1223
|
+
});
|
|
1224
|
+
// Brain applies Solomon's decision
|
|
1225
|
+
if (solomonResult.action === "approve") {
|
|
1226
|
+
logger.info("Brain: Solomon advised approve for style-only pending — finalizing");
|
|
1227
|
+
return { approved: true, sessionId: session.id, reason: "brain_solomon_approved" };
|
|
1228
|
+
}
|
|
1229
|
+
if (solomonResult.action === "continue") {
|
|
1230
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", extraIterations: solomonResult.extraIterations || Math.ceil(config.max_iterations / 2) };
|
|
1231
|
+
}
|
|
1232
|
+
if (solomonResult.action === "pause") {
|
|
1233
|
+
return { paused: true, sessionId: session.id, question: solomonResult.question, context: "brain_solomon_dilemma" };
|
|
1234
|
+
}
|
|
1235
|
+
// Fallback: escalate to human
|
|
1236
|
+
return { paused: true, sessionId: session.id, question: `Brain+Solomon cannot resolve: ${entries.length} pending issue(s) at max_iterations`, context: "max_iterations" };
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
// Legacy path (Brain disabled): original Solomon-driven flow
|
|
1240
|
+
const solomonResult = await invokeSolomon({
|
|
1241
|
+
config, logger, emitter, eventBase, stage: "max_iterations", askQuestion, session,
|
|
1242
|
+
iteration: config.max_iterations,
|
|
1243
|
+
conflict: {
|
|
1244
|
+
stage: "max_iterations",
|
|
1245
|
+
task,
|
|
1246
|
+
iterationCount: config.max_iterations,
|
|
1247
|
+
maxIterations: config.max_iterations,
|
|
1248
|
+
budget_usd: budget?.total_cost_usd || 0,
|
|
1249
|
+
history: [{ agent: "pipeline", feedback: session.last_reviewer_feedback || "Max iterations reached without reviewer approval" }]
|
|
1250
|
+
}
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1253
|
+
if (solomonResult.action === "approve") {
|
|
1254
|
+
logger.info("Solomon approved coder's work at max_iterations checkpoint");
|
|
1255
|
+
return { approved: true, sessionId: session.id, reason: "solomon_approved" };
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
if (solomonResult.action === "continue") {
|
|
1259
|
+
if (solomonResult.humanGuidance) {
|
|
1260
|
+
session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1261
|
+
}
|
|
1262
|
+
session.reviewer_retry_count = 0;
|
|
1263
|
+
await saveSession(session);
|
|
1264
|
+
const extraIterations = solomonResult.extraIterations || config.max_iterations;
|
|
1265
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations_extended", humanGuidance: solomonResult.humanGuidance, extraIterations };
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
if (solomonResult.action === "pause") {
|
|
1269
|
+
return { paused: true, sessionId: session.id, question: solomonResult.question, context: "max_iterations" };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
if (solomonResult.action === "subtask") {
|
|
1273
|
+
return { paused: true, sessionId: session.id, subtask: solomonResult.subtask, context: "max_iterations_subtask" };
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// Solomon couldn't resolve — fail
|
|
1277
|
+
session.budget = budgetSummary();
|
|
1278
|
+
const rtkSavings = rtkTracker?.hasData() ? rtkTracker.summary() : undefined;
|
|
1279
|
+
if (rtkSavings) session.rtk_savings = rtkSavings;
|
|
1280
|
+
await markSessionStatus(session, "failed");
|
|
1281
|
+
const failDetail = { approved: false, reason: "max_iterations", iterations: config.max_iterations, stages: stageResults, budget: budgetSummary() };
|
|
1282
|
+
if (rtkSavings) failDetail.rtk_savings = rtkSavings;
|
|
1283
|
+
emitProgress(
|
|
1284
|
+
emitter,
|
|
1285
|
+
makeEvent("session:end", { ...eventBase, stage: "done" }, {
|
|
1286
|
+
status: "fail",
|
|
1287
|
+
message: "Max iterations reached (Solomon could not resolve)",
|
|
1288
|
+
detail: failDetail
|
|
1289
|
+
})
|
|
1290
|
+
);
|
|
1291
|
+
return { approved: false, sessionId: session.id, reason: "max_iterations" };
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
async function tryAutoStartBoard(config, logger, emitter, eventBase) {
|
|
1295
|
+
// TSK-0273: gate on hu_board.auto_start alone. The previous double-gate
|
|
1296
|
+
// (enabled && auto_start) was confusing because `enabled` was never the
|
|
1297
|
+
// user-facing switch — only auto_start was. This matches the AC: "kj run
|
|
1298
|
+
// auto-inicia el board server si hu_board.auto_start es true".
|
|
1299
|
+
if (!config.hu_board?.auto_start) return;
|
|
1300
|
+
// Never auto-start during vitest: races the PID file and starts a detached
|
|
1301
|
+
// process that outlives the test run.
|
|
1302
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test") return;
|
|
1303
|
+
|
|
1304
|
+
try {
|
|
1305
|
+
const { startBoard, renderBoardBanner } = await import("../commands/board.js");
|
|
1306
|
+
const boardPort = config.hu_board.port || 4000;
|
|
1307
|
+
const boardResult = await startBoard(boardPort);
|
|
1308
|
+
const status = boardResult.alreadyRunning ? "already running" : "started";
|
|
1309
|
+
// Highlighted URL box — visible regardless of log level (matches the
|
|
1310
|
+
// post-planner auto-HU banner so the user gets the same signal either
|
|
1311
|
+
// way the board got started).
|
|
1312
|
+
console.log(renderBoardBanner({ url: boardResult.url, status }));
|
|
1313
|
+
logger.info(`HU Board ${status} at ${boardResult.url}`);
|
|
1314
|
+
emitProgress(emitter, makeEvent("board:started", eventBase, {
|
|
1315
|
+
message: `HU Board running at ${boardResult.url}`,
|
|
1316
|
+
detail: { pid: boardResult.pid, port: boardPort, url: boardResult.url }
|
|
1317
|
+
}));
|
|
1318
|
+
} catch (err) {
|
|
1319
|
+
logger.warn(`HU Board auto-start failed (non-blocking): ${err.message}`);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
async function initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags }) {
|
|
1324
|
+
// Auto-init .karajan/ if missing (copies coder-rules, review-rules, role templates)
|
|
1325
|
+
const initProjectDir = config.projectDir || process.cwd();
|
|
1326
|
+
await autoInit(initProjectDir, logger);
|
|
1327
|
+
|
|
1328
|
+
// Smart role assignment: detect installed AIs and assign to roles
|
|
1329
|
+
// Only runs if: (a) no roles configured AND (b) not in test environment
|
|
1330
|
+
const needsAssignment = !config.roles?.coder?.provider && !config.coder && process.env.NODE_ENV !== "test" && !process.env.VITEST;
|
|
1331
|
+
if (needsAssignment) {
|
|
1332
|
+
try {
|
|
1333
|
+
const { autoAssignRoles, applyRoleAssignments } = await import("../utils/role-assigner.js");
|
|
1334
|
+
const { assignments } = await autoAssignRoles(logger);
|
|
1335
|
+
if (assignments) config = applyRoleAssignments({ ...config }, assignments);
|
|
1336
|
+
} catch { /* non-blocking — defaults will be used */ }
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
// Scope all git diffs to projectDir (prevents leaking unrelated branch changes)
|
|
1340
|
+
// When running from a subdirectory of a git repo, use relative path as scope
|
|
1341
|
+
let diffScope = config.projectDir || null;
|
|
1342
|
+
if (!diffScope) {
|
|
1343
|
+
try {
|
|
1344
|
+
const { execSync } = await import("node:child_process");
|
|
1345
|
+
const repoRoot = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
|
|
1346
|
+
const cwd = process.cwd();
|
|
1347
|
+
if (cwd !== repoRoot && cwd.startsWith(repoRoot)) {
|
|
1348
|
+
diffScope = cwd.slice(repoRoot.length + 1);
|
|
1349
|
+
logger.info(`Running from subdirectory — diff scoped to ${diffScope}/`);
|
|
1350
|
+
}
|
|
1351
|
+
} catch { /* git not available */ }
|
|
1352
|
+
}
|
|
1353
|
+
setDiffProjectDir(diffScope);
|
|
1354
|
+
|
|
1355
|
+
// Auto-detect Chrome DevTools MCP
|
|
1356
|
+
const { detectDevToolsMcp } = await import("../webperf/devtools-detect.js");
|
|
1357
|
+
const devToolsAvailable = await detectDevToolsMcp(logger);
|
|
1358
|
+
if (devToolsAvailable) {
|
|
1359
|
+
config = { ...config, webperf: { ...config.webperf, devtools_mcp: true } };
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
const ctx = new PipelineContext({ config, session: null, logger, emitter, task, flags });
|
|
1363
|
+
ctx.askQuestion = askQuestion;
|
|
1364
|
+
ctx.pgTaskId = pgTaskId;
|
|
1365
|
+
ctx.pgProject = pgProject;
|
|
1366
|
+
|
|
1367
|
+
ctx.coderRole = resolveRole(config, "coder");
|
|
1368
|
+
ctx.reviewerRole = resolveRole(config, "reviewer");
|
|
1369
|
+
ctx.refactorerRole = resolveRole(config, "refactorer");
|
|
1370
|
+
ctx.pipelineFlags = resolvePipelineFlags(config);
|
|
1371
|
+
ctx.repeatDetector = new RepeatDetector({ threshold: getRepeatThreshold(config) });
|
|
1372
|
+
ctx.coderRoleInstance = new CoderRole({ config, logger, emitter, createAgentFn: createAgent, askHost: askQuestion });
|
|
1373
|
+
ctx.startedAt = Date.now();
|
|
1374
|
+
ctx.eventBase = { sessionId: null, iteration: 0, stage: null, startedAt: ctx.startedAt };
|
|
1375
|
+
const { budgetTracker, budgetLimit, budgetSummary, trackBudget } = createBudgetManager({
|
|
1376
|
+
config,
|
|
1377
|
+
emitter,
|
|
1378
|
+
eventBase: ctx.eventBase,
|
|
1379
|
+
// Provides fresh compression data for "With KJ vs Without KJ" comparison
|
|
1380
|
+
// (KJC-TSK-0274). Invoked on every budgetSummary() call.
|
|
1381
|
+
getCompressionStats: () => ({
|
|
1382
|
+
rtkSavings: ctx.rtkTracker?.hasData() ? ctx.rtkTracker.summary() : (ctx.session?.rtk_savings || null),
|
|
1383
|
+
brainCtx: ctx.brainCtx || null,
|
|
1384
|
+
}),
|
|
1385
|
+
});
|
|
1386
|
+
ctx.budgetTracker = budgetTracker;
|
|
1387
|
+
ctx.budgetLimit = budgetLimit;
|
|
1388
|
+
ctx.budgetSummary = budgetSummary;
|
|
1389
|
+
ctx.trackBudget = trackBudget;
|
|
1390
|
+
|
|
1391
|
+
// --- RTK detection ---
|
|
1392
|
+
const rtkResult = await detectRtk();
|
|
1393
|
+
if (rtkResult.available) {
|
|
1394
|
+
config = { ...config, rtk: { available: true, version: rtkResult.version } };
|
|
1395
|
+
const rtkTracker = new RtkSavingsTracker();
|
|
1396
|
+
const rtkRunner = createRtkRunner(true, rtkTracker);
|
|
1397
|
+
setDiffRunner(rtkRunner);
|
|
1398
|
+
setGitRunner(rtkRunner);
|
|
1399
|
+
ctx.rtkTracker = rtkTracker;
|
|
1400
|
+
logger.info(`RTK detected (${rtkResult.version}) — wrapping internal git/diff commands with rtk`);
|
|
1401
|
+
emitProgress(emitter, makeEvent("rtk:detected", ctx.eventBase, {
|
|
1402
|
+
message: "RTK detected — internal commands wrapped for token optimization",
|
|
1403
|
+
detail: { version: rtkResult.version, executorType: "local" }
|
|
1404
|
+
}));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// --- HU Board auto-start ---
|
|
1408
|
+
await tryAutoStartBoard(config, logger, emitter, ctx.eventBase);
|
|
1409
|
+
|
|
1410
|
+
// --- Product Context ---
|
|
1411
|
+
const ctxProjectDir = config.projectDir || process.cwd();
|
|
1412
|
+
const { content: productContext, source: productContextSource } = await loadProductContext(ctxProjectDir);
|
|
1413
|
+
if (productContext) {
|
|
1414
|
+
config = { ...config, productContext };
|
|
1415
|
+
logger.info(`Product context loaded from ${productContextSource}`);
|
|
1416
|
+
emitProgress(emitter, makeEvent("context:loaded", ctx.eventBase, {
|
|
1417
|
+
message: "Product context loaded",
|
|
1418
|
+
detail: { source: productContextSource }
|
|
1419
|
+
}));
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
ctx.session = await initializeSession({ task, config, flags, pgTaskId, pgProject });
|
|
1423
|
+
ctx.eventBase.sessionId = ctx.session.id;
|
|
1424
|
+
|
|
1425
|
+
// Karajan Brain: initialize runtime context (opt-in via config.brain.enabled)
|
|
1426
|
+
const { createBrainContext, isBrainEnabled } = await import("./brain-coordinator.js");
|
|
1427
|
+
ctx.brainCtx = createBrainContext({ enabled: isBrainEnabled(config) });
|
|
1428
|
+
if (ctx.brainCtx.enabled) {
|
|
1429
|
+
logger.info("Karajan Brain enabled — feedback queue, verification, compression active");
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
const { initPgAdapter } = await import("../planning-game/pipeline-adapter.js");
|
|
1433
|
+
const pgAdapterResult = await initPgAdapter({ session: ctx.session, config, logger, pgTaskId, pgProject });
|
|
1434
|
+
ctx.pgCard = pgAdapterResult.pgCard;
|
|
1435
|
+
ctx.session.pg_card = ctx.pgCard || null;
|
|
1436
|
+
|
|
1437
|
+
emitProgress(
|
|
1438
|
+
emitter,
|
|
1439
|
+
makeEvent("session:start", ctx.eventBase, {
|
|
1440
|
+
message: "Session started",
|
|
1441
|
+
detail: { task, coder: ctx.coderRole.provider, reviewer: ctx.reviewerRole.provider, maxIterations: config.max_iterations }
|
|
1442
|
+
})
|
|
1443
|
+
);
|
|
1444
|
+
|
|
1445
|
+
ctx.stageResults = {};
|
|
1446
|
+
ctx.sonarState = { issuesInitial: null, issuesFinal: null };
|
|
1447
|
+
|
|
1448
|
+
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 });
|
|
1449
|
+
ctx.plannedTask = preLoopResult.plannedTask;
|
|
1450
|
+
ctx.config = preLoopResult.updatedConfig;
|
|
1451
|
+
|
|
1452
|
+
// --- Session Journal: persist pre-loop outputs + display plan summary ---
|
|
1453
|
+
const reportDir = ctx.config.output?.report_dir || ".reviews";
|
|
1454
|
+
try {
|
|
1455
|
+
ctx.journalDir = await createJournalDir(reportDir, ctx.session.id);
|
|
1456
|
+
const journalFiles = await writePreLoopJournal(ctx.journalDir, ctx.stageResults);
|
|
1457
|
+
ctx.journalFiles = journalFiles;
|
|
1458
|
+
ctx.journalIterations = [];
|
|
1459
|
+
ctx.journalDecisions = [];
|
|
1460
|
+
|
|
1461
|
+
// Attach journal state to session so finalizeApprovedSession can access it
|
|
1462
|
+
ctx.session._journalDir = ctx.journalDir;
|
|
1463
|
+
ctx.session._journalFiles = journalFiles;
|
|
1464
|
+
ctx.session._journalIterations = ctx.journalIterations;
|
|
1465
|
+
ctx.session._journalDecisions = ctx.journalDecisions;
|
|
1466
|
+
ctx.session._startedAt = ctx.startedAt;
|
|
1467
|
+
|
|
1468
|
+
// Display plan summary in console before iteration loop
|
|
1469
|
+
const planSummary = buildPlanSummary({
|
|
1470
|
+
pipelineFlags: ctx.pipelineFlags,
|
|
1471
|
+
config: ctx.config,
|
|
1472
|
+
stageResults: ctx.stageResults,
|
|
1473
|
+
task
|
|
1474
|
+
});
|
|
1475
|
+
console.log(planSummary);
|
|
1476
|
+
} catch (err) {
|
|
1477
|
+
logger.warn(`Journal init failed (non-blocking): ${err.message}`);
|
|
1478
|
+
ctx.journalDir = null;
|
|
1479
|
+
ctx.journalFiles = [];
|
|
1480
|
+
ctx.journalIterations = [];
|
|
1481
|
+
ctx.journalDecisions = [];
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
ctx.gitCtx = await prepareGitAutomation({ config: ctx.config, task, logger, session: ctx.session });
|
|
1485
|
+
const projectDir = ctx.config.projectDir || process.cwd();
|
|
1486
|
+
ctx.reviewRules = (await resolveReviewProfile({ mode: ctx.config.review_mode, projectDir })).rules;
|
|
1487
|
+
await ctx.coderRoleInstance.init();
|
|
1488
|
+
|
|
1489
|
+
return ctx;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function runSingleIteration(ctx) {
|
|
1493
|
+
// Use plannedTask (HU-scoped or planner-enriched) over the raw original task.
|
|
1494
|
+
// When running per-HU sub-pipelines, plannedTask is the HU's text, not the full spec.
|
|
1495
|
+
const { config, logger, emitter, eventBase, session, iteration: i } = ctx;
|
|
1496
|
+
const task = ctx.plannedTask || ctx.task;
|
|
1497
|
+
|
|
1498
|
+
const iterStart = Date.now();
|
|
1499
|
+
const ciEnabled = Boolean(config.ci?.enabled) && ctx.gitCtx?.enabled;
|
|
1500
|
+
logger.setContext({ iteration: i, stage: "iteration" });
|
|
1501
|
+
|
|
1502
|
+
const reviewerRetryCount = session.reviewer_retry_count || 0;
|
|
1503
|
+
const maxReviewerRetries = config.session.max_reviewer_retries ?? config.session.fail_fast_repeats;
|
|
1504
|
+
const iterLang = getLang(config);
|
|
1505
|
+
const iterMsg = msg("pipeline_iteration", iterLang, { current: i, max: config.max_iterations });
|
|
1506
|
+
emitProgress(emitter, makeEvent("iteration:start", { ...eventBase, stage: "iteration" }, {
|
|
1507
|
+
message: iterMsg,
|
|
1508
|
+
detail: { iteration: i, maxIterations: config.max_iterations, reviewerRetryCount, maxReviewerRetries }
|
|
1509
|
+
}));
|
|
1510
|
+
logger.info(iterMsg);
|
|
1511
|
+
|
|
1512
|
+
const crResult = await runCoderAndRefactorerStages({
|
|
1513
|
+
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole, refactorerRole: ctx.refactorerRole,
|
|
1514
|
+
pipelineFlags: ctx.pipelineFlags, config, logger, emitter, eventBase, session,
|
|
1515
|
+
plannedTask: ctx.plannedTask, trackBudget: ctx.trackBudget, i, brainCtx: ctx.brainCtx
|
|
1516
|
+
});
|
|
1517
|
+
if (crResult.action === "return" || crResult.action === "retry") return crResult;
|
|
1518
|
+
|
|
1519
|
+
const guardResult = await runGuardStages({ config, logger, emitter, eventBase, session, iteration: i });
|
|
1520
|
+
if (guardResult.action === "return") return guardResult;
|
|
1521
|
+
|
|
1522
|
+
const qgResult = await runQualityGateStages({
|
|
1523
|
+
config, logger, emitter, eventBase, session, trackBudget: ctx.trackBudget, i,
|
|
1524
|
+
askQuestion: ctx.askQuestion, repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary,
|
|
1525
|
+
sonarState: ctx.sonarState, task, stageResults: ctx.stageResults, coderRole: ctx.coderRole,
|
|
1526
|
+
pipelineFlags: ctx.pipelineFlags, brainCtx: ctx.brainCtx
|
|
1527
|
+
});
|
|
1528
|
+
if (qgResult.action === "return" || qgResult.action === "continue") return qgResult;
|
|
1529
|
+
|
|
1530
|
+
await handleCiEarlyPrOrPush({
|
|
1531
|
+
ciEnabled, config, session, emitter, eventBase, gitCtx: ctx.gitCtx, task, logger,
|
|
1532
|
+
stageResults: ctx.stageResults, i
|
|
1533
|
+
});
|
|
1534
|
+
|
|
1535
|
+
const revResult = await runReviewerGateStage({
|
|
1536
|
+
pipelineFlags: ctx.pipelineFlags, reviewerRole: ctx.reviewerRole, config, logger, emitter, eventBase,
|
|
1537
|
+
session, trackBudget: ctx.trackBudget, i, reviewRules: ctx.reviewRules, task,
|
|
1538
|
+
repeatDetector: ctx.repeatDetector, budgetSummary: ctx.budgetSummary, askQuestion: ctx.askQuestion,
|
|
1539
|
+
brainCtx: ctx.brainCtx
|
|
1540
|
+
});
|
|
1541
|
+
if (revResult.action === "return" || revResult.action === "retry") return revResult;
|
|
1542
|
+
const review = revResult.review;
|
|
1543
|
+
|
|
1544
|
+
const iterDuration = Date.now() - iterStart;
|
|
1545
|
+
emitProgress(emitter, makeEvent("iteration:end", { ...eventBase, stage: "iteration" }, {
|
|
1546
|
+
message: `Iteration ${i} completed`, detail: { duration: iterDuration }
|
|
1547
|
+
}));
|
|
1548
|
+
session.standby_retry_count = 0;
|
|
1549
|
+
|
|
1550
|
+
// --- Journal: record iteration ---
|
|
1551
|
+
// Uses the richer iteration-logger from TSK-0286: captures blocking-issue
|
|
1552
|
+
// details, sonar counts and Solomon rulings in addition to summaries.
|
|
1553
|
+
if (ctx.journalIterations) {
|
|
1554
|
+
try {
|
|
1555
|
+
const { recordIteration, extractIterationData } = await import("../session/journal/iteration-logger.js");
|
|
1556
|
+
const iterationData = extractIterationData({
|
|
1557
|
+
iteration: i,
|
|
1558
|
+
durationMs: iterDuration,
|
|
1559
|
+
stageResults: ctx.stageResults,
|
|
1560
|
+
session: ctx.session,
|
|
1561
|
+
});
|
|
1562
|
+
// Derive a richer reviewer summary if none was produced.
|
|
1563
|
+
if (!iterationData.reviewerSummary && review) {
|
|
1564
|
+
iterationData.reviewerSummary = review.approved
|
|
1565
|
+
? `Approved: ${review.raw_summary || ""}`
|
|
1566
|
+
: `Rejected: ${(review?.blocking_issues || []).length} blocking issue(s)`;
|
|
1567
|
+
}
|
|
1568
|
+
if (!iterationData.blockingIssues || iterationData.blockingIssues.length === 0) {
|
|
1569
|
+
iterationData.blockingIssues = review?.blocking_issues || [];
|
|
1570
|
+
}
|
|
1571
|
+
recordIteration(ctx.session, iterationData);
|
|
1572
|
+
} catch (err) {
|
|
1573
|
+
logger.warn(`Iteration journal record failed (non-blocking): ${err.message}`);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
const solomonResult = await handleSolomonCheck({
|
|
1578
|
+
config, session, emitter, eventBase, logger, task, i, askQuestion: ctx.askQuestion,
|
|
1579
|
+
ciEnabled, blockingIssues: review?.blocking_issues, brainCtx: ctx.brainCtx
|
|
1580
|
+
});
|
|
1581
|
+
if (solomonResult.action === "pause") return { action: "return", result: solomonResult.result };
|
|
1582
|
+
|
|
1583
|
+
await handleCiReviewDispatch({ ciEnabled, config, session, review, i, logger });
|
|
1584
|
+
|
|
1585
|
+
if (review.approved) {
|
|
1586
|
+
const approvedResult = await handleApprovedReview({
|
|
1587
|
+
config, session, emitter, eventBase, coderRole: ctx.coderRole, trackBudget: ctx.trackBudget, i, task,
|
|
1588
|
+
stageResults: ctx.stageResults, pipelineFlags: ctx.pipelineFlags, askQuestion: ctx.askQuestion, logger,
|
|
1589
|
+
gitCtx: ctx.gitCtx, budgetSummary: ctx.budgetSummary, pgCard: ctx.pgCard, pgProject: ctx.pgProject, review,
|
|
1590
|
+
rtkTracker: ctx.rtkTracker, brainCtx: ctx.brainCtx
|
|
1591
|
+
});
|
|
1592
|
+
if (approvedResult.action === "return" || approvedResult.action === "continue") return approvedResult;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
// Solomon already evaluated the rejection in runReviewerStage -> handleReviewerRejection
|
|
1596
|
+
// Only use retry counter as fallback if Solomon is disabled
|
|
1597
|
+
if (!config.pipeline?.solomon?.enabled) {
|
|
1598
|
+
const retryResult = await handleReviewerRetryAndSolomon({ config, session, emitter, eventBase, logger, review, task, i, askQuestion: ctx.askQuestion });
|
|
1599
|
+
if (retryResult.action === "return") return retryResult;
|
|
1600
|
+
} else {
|
|
1601
|
+
// Solomon is enabled — feed back the blocking issues for the next coder iteration
|
|
1602
|
+
session.last_reviewer_feedback = review.blocking_issues
|
|
1603
|
+
.map((x) => {
|
|
1604
|
+
const parts = [`[${x.severity || "high"}] ${x.id || "ISSUE"}: ${x.description || "Missing description"}`];
|
|
1605
|
+
if (x.file) parts.push(` File: ${x.file}${x.line ? `:${x.line}` : ""}`);
|
|
1606
|
+
if (x.suggested_fix) parts.push(` Fix: ${x.suggested_fix}`);
|
|
1607
|
+
return parts.join("\n");
|
|
1608
|
+
})
|
|
1609
|
+
.join("\n\n");
|
|
1610
|
+
await saveSession(session);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
return { action: "next" };
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
async function writeHistoryRecord({ sessionId, task, result, logger }) {
|
|
1617
|
+
try {
|
|
1618
|
+
const { createHistoryRecord } = await import("../hu/store.js");
|
|
1619
|
+
const approved = Boolean(result?.approved);
|
|
1620
|
+
const summary = result?.review?.summary || result?.reason || null;
|
|
1621
|
+
await createHistoryRecord(sessionId, {
|
|
1622
|
+
task,
|
|
1623
|
+
result: JSON.stringify(result),
|
|
1624
|
+
approved,
|
|
1625
|
+
summary
|
|
1626
|
+
});
|
|
1627
|
+
} catch (err) {
|
|
1628
|
+
logger.warn(`HU history record failed (non-blocking): ${err.message}`);
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Run the standard iteration loop for a given task using the pipeline context.
|
|
1634
|
+
* Returns a result object with at least { approved: boolean }.
|
|
1635
|
+
* Used both as the main loop and as the per-HU callback in sub-pipeline mode.
|
|
1636
|
+
*/
|
|
1637
|
+
async function runIterationLoop(ctx, { task: loopTask, askQuestion, emitter, logger }) {
|
|
1638
|
+
ctx.plannedTask = loopTask;
|
|
1639
|
+
|
|
1640
|
+
const checkpointIntervalMs = (ctx.config.session.checkpoint_interval_minutes ?? 5) * 60 * 1000;
|
|
1641
|
+
let lastCheckpointAt = Date.now();
|
|
1642
|
+
let checkpointDisabled = false;
|
|
1643
|
+
let lastCheckpointSnapshot = takeCheckpointSnapshot(ctx.session);
|
|
1644
|
+
|
|
1645
|
+
let i = 0;
|
|
1646
|
+
while (i < ctx.config.max_iterations) {
|
|
1647
|
+
i += 1;
|
|
1648
|
+
const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
|
|
1649
|
+
|
|
1650
|
+
const cpResult = await handleCheckpoint({
|
|
1651
|
+
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1652
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1653
|
+
});
|
|
1654
|
+
if (cpResult.action === "stop") {
|
|
1655
|
+
return cpResult.result;
|
|
1656
|
+
}
|
|
1657
|
+
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1658
|
+
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1659
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1660
|
+
|
|
1661
|
+
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1662
|
+
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
1663
|
+
|
|
1664
|
+
ctx.eventBase.iteration = i;
|
|
1665
|
+
ctx.iteration = i;
|
|
1666
|
+
|
|
1667
|
+
let iterResult;
|
|
1668
|
+
try {
|
|
1669
|
+
iterResult = await runSingleIteration(ctx);
|
|
1670
|
+
} catch (stageError) {
|
|
1671
|
+
// ANY unhandled error in a stage = out of normal flow → Solomon decides
|
|
1672
|
+
logger.warn(`Stage error caught — escalating to Solomon: ${stageError.message}`);
|
|
1673
|
+
const solomonResult = await invokeSolomon({
|
|
1674
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
|
|
1675
|
+
stage: "stage_error", askQuestion, session: ctx.session, iteration: i,
|
|
1676
|
+
conflict: {
|
|
1677
|
+
stage: "stage_error",
|
|
1678
|
+
task: loopTask,
|
|
1679
|
+
iterationCount: i,
|
|
1680
|
+
maxIterations: ctx.config.max_iterations,
|
|
1681
|
+
budget_usd: ctx.budgetSummary()?.total_cost_usd || 0,
|
|
1682
|
+
history: [{ agent: "pipeline", feedback: `Stage threw: ${stageError.message}` }]
|
|
1683
|
+
}
|
|
1684
|
+
});
|
|
1685
|
+
|
|
1686
|
+
if (solomonResult.action === "approve") {
|
|
1687
|
+
logger.info("Solomon approved despite stage error");
|
|
1688
|
+
return { approved: true, sessionId: ctx.session.id, reason: "solomon_approved_after_error" };
|
|
1689
|
+
}
|
|
1690
|
+
if (solomonResult.action === "continue") {
|
|
1691
|
+
if (solomonResult.humanGuidance) {
|
|
1692
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${solomonResult.humanGuidance}`;
|
|
1693
|
+
}
|
|
1694
|
+
continue; // next iteration
|
|
1695
|
+
}
|
|
1696
|
+
if (solomonResult.action === "pause") {
|
|
1697
|
+
return { paused: true, sessionId: ctx.session.id, question: solomonResult.question, context: "stage_error" };
|
|
1698
|
+
}
|
|
1699
|
+
// Solomon couldn't resolve — fail
|
|
1700
|
+
await markSessionStatus(ctx.session, "failed");
|
|
1701
|
+
return { approved: false, sessionId: ctx.session.id, reason: "stage_error", error: stageError.message };
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
if (iterResult.action === "return") {
|
|
1705
|
+
return iterResult.result;
|
|
1706
|
+
}
|
|
1707
|
+
if (iterResult.action === "retry") { i -= 1; }
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
// Solomon decides whether to extend iterations or stop
|
|
1711
|
+
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, brainCtx: ctx.brainCtx });
|
|
1712
|
+
|
|
1713
|
+
// Solomon said "continue" — extend iterations and keep going
|
|
1714
|
+
if (maxIterResult.reason === "max_iterations_extended") {
|
|
1715
|
+
const extra = maxIterResult.extraIterations || ctx.config.max_iterations;
|
|
1716
|
+
ctx.config.max_iterations += extra;
|
|
1717
|
+
logger.info(`Solomon extended pipeline by ${extra} iterations (new max: ${ctx.config.max_iterations})`);
|
|
1718
|
+
|
|
1719
|
+
if (maxIterResult.humanGuidance) {
|
|
1720
|
+
ctx.session.last_reviewer_feedback = `Solomon guidance: ${maxIterResult.humanGuidance}`;
|
|
1721
|
+
}
|
|
1722
|
+
|
|
1723
|
+
// Continue the loop
|
|
1724
|
+
while (i < ctx.config.max_iterations) {
|
|
1725
|
+
i += 1;
|
|
1726
|
+
const elapsedMinutes = (Date.now() - ctx.startedAt) / 60000;
|
|
1727
|
+
|
|
1728
|
+
const cpResult = await handleCheckpoint({
|
|
1729
|
+
checkpointDisabled, askQuestion, lastCheckpointAt, checkpointIntervalMs, elapsedMinutes,
|
|
1730
|
+
i, config: ctx.config, budgetTracker: ctx.budgetTracker, stageResults: ctx.stageResults, emitter, eventBase: ctx.eventBase, session: ctx.session, budgetSummary: ctx.budgetSummary, lastCheckpointSnapshot
|
|
1731
|
+
});
|
|
1732
|
+
if (cpResult.action === "stop") return cpResult.result;
|
|
1733
|
+
checkpointDisabled = cpResult.checkpointDisabled;
|
|
1734
|
+
lastCheckpointAt = cpResult.lastCheckpointAt;
|
|
1735
|
+
if (cpResult.lastCheckpointSnapshot !== undefined) lastCheckpointSnapshot = cpResult.lastCheckpointSnapshot;
|
|
1736
|
+
|
|
1737
|
+
await checkSessionTimeout({ askQuestion, elapsedMinutes, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetSummary: ctx.budgetSummary });
|
|
1738
|
+
await checkBudgetExceeded({ budgetTracker: ctx.budgetTracker, config: ctx.config, session: ctx.session, emitter, eventBase: ctx.eventBase, i, budgetLimit: ctx.budgetLimit, budgetSummary: ctx.budgetSummary });
|
|
1739
|
+
|
|
1740
|
+
ctx.eventBase.iteration = i;
|
|
1741
|
+
ctx.iteration = i;
|
|
1742
|
+
|
|
1743
|
+
const iterResult = await runSingleIteration(ctx);
|
|
1744
|
+
if (iterResult.action === "return") return iterResult.result;
|
|
1745
|
+
if (iterResult.action === "retry") { i -= 1; }
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// Extended iterations also exhausted — final Solomon call
|
|
1749
|
+
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, brainCtx: ctx.brainCtx });
|
|
1750
|
+
return finalResult;
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
return maxIterResult;
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
export async function runFlow({ task, config, logger, flags = {}, emitter = null, askQuestion = null, pgTaskId = null, pgProject = null }) {
|
|
1757
|
+
const pipelineFlags = resolvePipelineFlags(config);
|
|
1758
|
+
|
|
1759
|
+
if (flags.dryRun) {
|
|
1760
|
+
return handleDryRun({ task, config, flags, emitter, pipelineFlags });
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
let ctx;
|
|
1764
|
+
try {
|
|
1765
|
+
ctx = await initFlowContext({ task, config, logger, emitter, askQuestion, pgTaskId, pgProject, flags });
|
|
1766
|
+
} catch (initError) {
|
|
1767
|
+
// Pre-loop stage failure → Solomon decides
|
|
1768
|
+
logger.warn(`Init/pre-loop error — escalating to Solomon: ${initError.message}`);
|
|
1769
|
+
const tempSession = { id: "init-error", task, status: "failed" };
|
|
1770
|
+
const solomonResult = await invokeSolomon({
|
|
1771
|
+
config, logger, emitter, eventBase: { sessionId: "init-error", iteration: 0, stage: "init", startedAt: Date.now() },
|
|
1772
|
+
stage: "init_error", askQuestion, session: tempSession, iteration: 0,
|
|
1773
|
+
conflict: {
|
|
1774
|
+
stage: "init_error",
|
|
1775
|
+
task,
|
|
1776
|
+
iterationCount: 0,
|
|
1777
|
+
maxIterations: config.max_iterations || 5,
|
|
1778
|
+
history: [{ agent: "pipeline", feedback: `Initialization failed: ${initError.message}` }]
|
|
1779
|
+
}
|
|
1780
|
+
});
|
|
1781
|
+
if (solomonResult.action === "pause") {
|
|
1782
|
+
return { paused: true, sessionId: "init-error", question: solomonResult.question, context: "init_error" };
|
|
1783
|
+
}
|
|
1784
|
+
throw initError; // Solomon couldn't resolve — propagate
|
|
1785
|
+
}
|
|
1786
|
+
|
|
1787
|
+
try {
|
|
1788
|
+
// --- Analysis-only flow: skip coder/reviewer when coderRequired === false ---
|
|
1789
|
+
if (ctx.pipelineFlags.coderRequired === false) {
|
|
1790
|
+
logger.info("Analysis-only task — skipping coder/reviewer iteration loop");
|
|
1791
|
+
emitProgress(emitter, makeEvent("pipeline:analysis-only", { ...ctx.eventBase, stage: "analysis" }, {
|
|
1792
|
+
message: "Analysis-only task — running security and audit stages only",
|
|
1793
|
+
detail: { taskType: ctx.session.resolved_policies?.taskType, coderRequired: false }
|
|
1794
|
+
}));
|
|
1795
|
+
|
|
1796
|
+
const analysisStageResults = ctx.stageResults;
|
|
1797
|
+
const postLoopDiff = await generateDiff({ baseRef: ctx.session.session_start_sha });
|
|
1798
|
+
|
|
1799
|
+
if (ctx.pipelineFlags.securityEnabled) {
|
|
1800
|
+
const securityResult = await runSecurityStage({
|
|
1801
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
|
|
1802
|
+
coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
1803
|
+
iteration: 1, task: ctx.plannedTask, diff: postLoopDiff, askQuestion, brainCtx: ctx.brainCtx
|
|
1804
|
+
});
|
|
1805
|
+
if (securityResult.stageResult) analysisStageResults.security = securityResult.stageResult;
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const auditResult = await runFinalAuditStage({
|
|
1809
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase, session: ctx.session,
|
|
1810
|
+
coderRole: ctx.coderRole, trackBudget: ctx.trackBudget,
|
|
1811
|
+
iteration: 1, task: ctx.plannedTask, diff: postLoopDiff
|
|
1812
|
+
});
|
|
1813
|
+
if (auditResult.stageResult) analysisStageResults.audit = auditResult.stageResult;
|
|
1814
|
+
|
|
1815
|
+
ctx.session.budget = ctx.budgetSummary();
|
|
1816
|
+
await markSessionStatus(ctx.session, "approved");
|
|
1817
|
+
|
|
1818
|
+
const analysisResult = {
|
|
1819
|
+
approved: true,
|
|
1820
|
+
sessionId: ctx.session.id,
|
|
1821
|
+
analysisOnly: true,
|
|
1822
|
+
stages: analysisStageResults,
|
|
1823
|
+
budget: ctx.budgetSummary()
|
|
1824
|
+
};
|
|
1825
|
+
await writeHistoryRecord({ sessionId: ctx.session.id, task, result: analysisResult, logger });
|
|
1826
|
+
|
|
1827
|
+
emitProgress(emitter, makeEvent("session:end", { ...ctx.eventBase, stage: "done" }, {
|
|
1828
|
+
message: "Analysis-only session completed",
|
|
1829
|
+
detail: analysisResult
|
|
1830
|
+
}));
|
|
1831
|
+
|
|
1832
|
+
return analysisResult;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
// --- HU Sub-Pipeline: run each certified HU as an independent iteration loop ---
|
|
1836
|
+
if (needsSubPipeline(ctx.stageResults.huReviewer)) {
|
|
1837
|
+
logger.info(`HU sub-pipeline: ${ctx.stageResults.huReviewer.certified} certified stories — running each as a sub-pipeline`);
|
|
1838
|
+
emitProgress(emitter, makeEvent("hu:sub-pipeline:start", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
|
|
1839
|
+
message: `Running ${ctx.stageResults.huReviewer.certified} HUs as sub-pipelines`,
|
|
1840
|
+
detail: { total: ctx.stageResults.huReviewer.total, certified: ctx.stageResults.huReviewer.certified }
|
|
1841
|
+
}));
|
|
1842
|
+
|
|
1843
|
+
// Per-HU pipeline: focused max_iterations, fresh Brain state, own git branch.
|
|
1844
|
+
const originalMaxIterations = ctx.config.max_iterations;
|
|
1845
|
+
const huMaxIterations = ctx.config.hu_max_iterations ?? 3;
|
|
1846
|
+
const huBranches = new Map();
|
|
1847
|
+
const { prepareHuBranch, finalizeHuCommit } = await import("../git/hu-automation.js");
|
|
1848
|
+
const subPipelineResult = await runHuSubPipeline({
|
|
1849
|
+
huReviewerResult: ctx.stageResults.huReviewer,
|
|
1850
|
+
runIterationFn: async (huTask, story) => {
|
|
1851
|
+
ctx.config.max_iterations = huMaxIterations;
|
|
1852
|
+
if (ctx.brainCtx?.enabled) {
|
|
1853
|
+
ctx.brainCtx.extensionCount = 0;
|
|
1854
|
+
const { createBrainContext } = await import("./brain-coordinator.js");
|
|
1855
|
+
const fresh = createBrainContext({ enabled: true });
|
|
1856
|
+
ctx.brainCtx.feedbackQueue = fresh.feedbackQueue;
|
|
1857
|
+
ctx.brainCtx.verificationTracker = fresh.verificationTracker;
|
|
1858
|
+
}
|
|
1859
|
+
// Apply per-HU policies based on task_type (infra skips reviewer/sonar/tdd)
|
|
1860
|
+
const { applyPolicies } = await import("../guards/policy-resolver.js");
|
|
1861
|
+
const huPolicies = applyPolicies({ taskType: story.task_type, policies: ctx.config.policies });
|
|
1862
|
+
const savedFlags = { ...ctx.pipelineFlags };
|
|
1863
|
+
if (!huPolicies.reviewer) ctx.pipelineFlags.reviewerEnabled = false;
|
|
1864
|
+
if (!huPolicies.tdd) ctx.config.development = { ...ctx.config.development, methodology: "standard", require_test_changes: false };
|
|
1865
|
+
if (!huPolicies.sonar) ctx.config.sonarqube = { ...ctx.config.sonarqube, enabled: false };
|
|
1866
|
+
if (!huPolicies.testsRequired) ctx.pipelineFlags.testerEnabled = false;
|
|
1867
|
+
logger.info(`HU ${story.id} (${story.task_type}): policies → reviewer=${huPolicies.reviewer}, tdd=${huPolicies.tdd}, sonar=${huPolicies.sonar}, tests=${huPolicies.testsRequired}`);
|
|
1868
|
+
|
|
1869
|
+
const branchName = await prepareHuBranch({ story, huBranches, config: ctx.config, logger });
|
|
1870
|
+
const projectDir = ctx.config.projectDir || process.cwd();
|
|
1871
|
+
|
|
1872
|
+
// If HU has acceptance_tests, Brain runs them as the gate instead of
|
|
1873
|
+
// the standard reviewer/tester pipeline. This is the radical fix:
|
|
1874
|
+
// concrete executable tests replace subjective reviewer opinions.
|
|
1875
|
+
if (story.acceptance_tests?.length > 0) {
|
|
1876
|
+
const { runAcceptanceTests, buildDiagnosticPrompt } = await import("../hu/acceptance-runner.js");
|
|
1877
|
+
|
|
1878
|
+
for (let attempt = 1; attempt <= ctx.config.max_iterations; attempt++) {
|
|
1879
|
+
logger.info(`HU ${story.id}: coder iteration ${attempt}/${ctx.config.max_iterations}`);
|
|
1880
|
+
emitProgress(emitter, makeEvent("iteration:start", { ...ctx.eventBase, stage: "iteration" }, {
|
|
1881
|
+
message: `Iteration ${attempt}/${ctx.config.max_iterations}`,
|
|
1882
|
+
detail: { iteration: attempt, maxIterations: ctx.config.max_iterations }
|
|
1883
|
+
}));
|
|
1884
|
+
|
|
1885
|
+
// Coder runs with the HU task + any diagnostic feedback from previous attempt
|
|
1886
|
+
const coderResult = await runCoderStage({
|
|
1887
|
+
coderRoleInstance: ctx.coderRoleInstance, coderRole: ctx.coderRole,
|
|
1888
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
|
|
1889
|
+
session: ctx.session, plannedTask: ctx.plannedTask,
|
|
1890
|
+
trackBudget: ctx.trackBudget, iteration: attempt, brainCtx: ctx.brainCtx
|
|
1891
|
+
});
|
|
1892
|
+
if (coderResult?.action === "standby" || coderResult?.action === "pause") {
|
|
1893
|
+
return coderResult?.result || { approved: false, reason: "coder_failed" };
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
// Sonar quality gate (sw task_type only, when policies say sonar=true)
|
|
1897
|
+
if (huPolicies.sonar && ctx.config.sonarqube?.enabled) {
|
|
1898
|
+
try {
|
|
1899
|
+
const sonarResult = await runSonarStage({
|
|
1900
|
+
config: ctx.config, logger, emitter, eventBase: ctx.eventBase,
|
|
1901
|
+
session: ctx.session, trackBudget: ctx.trackBudget, iteration: attempt,
|
|
1902
|
+
askQuestion, brainCtx: ctx.brainCtx
|
|
1903
|
+
});
|
|
1904
|
+
if (sonarResult?.action === "continue") {
|
|
1905
|
+
// Sonar failed — add to feedback for next coder attempt
|
|
1906
|
+
ctx.plannedTask = `${huTask}\n\n--- SONAR FAILURE ---\n${ctx.session.last_reviewer_feedback}`;
|
|
1907
|
+
continue;
|
|
1908
|
+
}
|
|
1909
|
+
} catch (err) {
|
|
1910
|
+
logger.warn(`HU ${story.id}: sonar stage failed (non-blocking): ${err.message}`);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
// Brain runs acceptance tests
|
|
1915
|
+
logger.info(`HU ${story.id}: running ${story.acceptance_tests.length} acceptance tests`);
|
|
1916
|
+
emitProgress(emitter, makeEvent("hu:acceptance-start", { ...ctx.eventBase, stage: "acceptance" }, {
|
|
1917
|
+
message: `Running ${story.acceptance_tests.length} acceptance tests`,
|
|
1918
|
+
detail: { huId: story.id, testCount: story.acceptance_tests.length }
|
|
1919
|
+
}));
|
|
1920
|
+
|
|
1921
|
+
const testResult = await runAcceptanceTests(story.acceptance_tests, projectDir);
|
|
1922
|
+
emitProgress(emitter, makeEvent("hu:acceptance-end", { ...ctx.eventBase, stage: "acceptance" }, {
|
|
1923
|
+
status: testResult.allPassed ? "ok" : "fail",
|
|
1924
|
+
message: testResult.summary,
|
|
1925
|
+
detail: { allPassed: testResult.allPassed, results: testResult.results.map(r => ({ cmd: r.cmd, passed: r.passed })) }
|
|
1926
|
+
}));
|
|
1927
|
+
|
|
1928
|
+
if (testResult.allPassed) {
|
|
1929
|
+
logger.info(`HU ${story.id}: all acceptance tests PASSED — approved`);
|
|
1930
|
+
await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
|
|
1931
|
+
return { approved: true, sessionId: ctx.session.id, reason: "acceptance_tests_passed" };
|
|
1932
|
+
}
|
|
1933
|
+
|
|
1934
|
+
// Brain diagnoses failures and sends concrete fix to coder
|
|
1935
|
+
const failed = testResult.results.filter(r => !r.passed);
|
|
1936
|
+
const diagnostic = buildDiagnosticPrompt(failed);
|
|
1937
|
+
logger.warn(`HU ${story.id}: ${failed.length} acceptance test(s) FAILED — sending diagnostic to coder`);
|
|
1938
|
+
ctx.session.last_reviewer_feedback = diagnostic;
|
|
1939
|
+
ctx.plannedTask = `${huTask}\n\n--- ACCEPTANCE TEST FAILURES ---\n${diagnostic}`;
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
// All iterations exhausted
|
|
1943
|
+
logger.warn(`HU ${story.id}: max iterations reached with acceptance tests still failing`);
|
|
1944
|
+
return { approved: false, sessionId: ctx.session.id, reason: "acceptance_tests_failed" };
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
// Fallback: no acceptance_tests → standard pipeline (reviewer/tester)
|
|
1948
|
+
try {
|
|
1949
|
+
const result = await runIterationLoop(ctx, { task: huTask, askQuestion, emitter, logger });
|
|
1950
|
+
if (result?.approved) {
|
|
1951
|
+
await finalizeHuCommit({ story, branchName, config: ctx.config, logger });
|
|
1952
|
+
}
|
|
1953
|
+
return result;
|
|
1954
|
+
} finally {
|
|
1955
|
+
ctx.config.max_iterations = originalMaxIterations;
|
|
1956
|
+
Object.assign(ctx.pipelineFlags, savedFlags);
|
|
1957
|
+
}
|
|
1958
|
+
},
|
|
1959
|
+
emitter,
|
|
1960
|
+
eventBase: ctx.eventBase,
|
|
1961
|
+
logger,
|
|
1962
|
+
config: ctx.config
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1965
|
+
emitProgress(emitter, makeEvent("hu:sub-pipeline:end", { ...ctx.eventBase, stage: "hu-sub-pipeline" }, {
|
|
1966
|
+
status: subPipelineResult.approved ? "ok" : "fail",
|
|
1967
|
+
message: subPipelineResult.approved
|
|
1968
|
+
? "All HUs completed successfully"
|
|
1969
|
+
: `Sub-pipeline finished with failures (${subPipelineResult.blockedIds.length} blocked)`,
|
|
1970
|
+
detail: { results: subPipelineResult.results, blockedIds: subPipelineResult.blockedIds }
|
|
1971
|
+
}));
|
|
1972
|
+
|
|
1973
|
+
// Sync results back to plan file if this was a plan-based run
|
|
1974
|
+
if (ctx.session._syncResultsToPlan) {
|
|
1975
|
+
try {
|
|
1976
|
+
await ctx.session._syncResultsToPlan(subPipelineResult);
|
|
1977
|
+
logger.info("Plan updated with execution results");
|
|
1978
|
+
} catch (err) {
|
|
1979
|
+
logger.warn(`Failed to sync results to plan: ${err.message}`);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
const finalResult = {
|
|
1984
|
+
approved: subPipelineResult.approved,
|
|
1985
|
+
sessionId: ctx.session.id,
|
|
1986
|
+
huResults: subPipelineResult.results,
|
|
1987
|
+
blockedIds: subPipelineResult.blockedIds,
|
|
1988
|
+
planId: ctx.session._planRef?.planId || null
|
|
1989
|
+
};
|
|
1990
|
+
await writeHistoryRecord({ sessionId: ctx.session.id, task, result: finalResult, logger });
|
|
1991
|
+
return finalResult;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
// --- Standard single-task pipeline (1 HU or no HU reviewer) ---
|
|
1995
|
+
const result = await runIterationLoop(ctx, { task: ctx.plannedTask, askQuestion, emitter, logger });
|
|
1996
|
+
await writeHistoryRecord({ sessionId: ctx.session.id, task, result, logger });
|
|
1997
|
+
return result;
|
|
1998
|
+
} finally {
|
|
1999
|
+
// --- Cleanup auto-installed skills ---
|
|
2000
|
+
const autoSkills = ctx.session?.autoInstalledSkills;
|
|
2001
|
+
if (autoSkills && autoSkills.length > 0) {
|
|
2002
|
+
const cleanProjectDir = ctx.config?.projectDir || process.cwd();
|
|
2003
|
+
try {
|
|
2004
|
+
const cleanupResult = await cleanupAutoInstalledSkills(autoSkills, cleanProjectDir);
|
|
2005
|
+
if (cleanupResult.removed.length > 0) {
|
|
2006
|
+
logger.info(`Cleaned up ${cleanupResult.removed.length} auto-installed skill(s): ${cleanupResult.removed.join(", ")}`);
|
|
2007
|
+
}
|
|
2008
|
+
} catch (err) {
|
|
2009
|
+
logger.warn(`Skill cleanup failed (non-blocking): ${err.message}`);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
// --- Telemetry: anonymous pipeline_complete event (non-blocking) ---
|
|
2014
|
+
try {
|
|
2015
|
+
const { sendTelemetryEvent } = await import("../utils/telemetry.js");
|
|
2016
|
+
const durationS = Math.round((Date.now() - ctx.startedAt) / 1000);
|
|
2017
|
+
const sessionStatus = ctx.session?.status || "unknown";
|
|
2018
|
+
sendTelemetryEvent("pipeline_complete", {
|
|
2019
|
+
mode: config.review_mode,
|
|
2020
|
+
agent: ctx.coderRole?.provider || config.coder,
|
|
2021
|
+
duration_s: durationS,
|
|
2022
|
+
success: sessionStatus === "approved",
|
|
2023
|
+
taskType: ctx.session?.resolved_policies?.taskType || null
|
|
2024
|
+
}, config).catch(() => {});
|
|
2025
|
+
} catch { /* non-blocking */ }
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
|
|
2029
|
+
export async function resumeFlow({ sessionId, answer, config, logger, flags = {}, emitter = null, askQuestion = null }) {
|
|
2030
|
+
const session = answer
|
|
2031
|
+
? await resumeSessionWithAnswer(sessionId, answer)
|
|
2032
|
+
: await loadSession(sessionId);
|
|
2033
|
+
|
|
2034
|
+
if (session.status === "paused" && !answer) {
|
|
2035
|
+
logger.info(`Session ${sessionId} is paused. Provide --answer to resume.`);
|
|
2036
|
+
return session;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// Allow resuming "stopped" sessions (checkpoint stop) and "failed" sessions
|
|
2040
|
+
const resumableStatuses = new Set(["running", "stopped", "failed"]);
|
|
2041
|
+
if (!resumableStatuses.has(session.status)) {
|
|
2042
|
+
logger.info(`Session ${sessionId} has status ${session.status} — not resumable`);
|
|
2043
|
+
return session;
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// Mark as running again for stopped/failed sessions
|
|
2047
|
+
if (session.status !== "running") {
|
|
2048
|
+
logger.info(`Resuming ${session.status} session ${sessionId}`);
|
|
2049
|
+
session.status = "running";
|
|
2050
|
+
await saveSession(session);
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Session was paused and now resumed with answer - re-run the flow
|
|
2054
|
+
const task = session.task;
|
|
2055
|
+
const sessionConfig = config || session.config_snapshot;
|
|
2056
|
+
if (!sessionConfig) {
|
|
2057
|
+
throw new Error("No config available to resume session");
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
logger.info(`Resuming session ${sessionId} with answer: ${answer}`);
|
|
2061
|
+
|
|
2062
|
+
// Inject the answer as additional feedback for the coder
|
|
2063
|
+
if (session.paused_state?.context?.lastFeedback) {
|
|
2064
|
+
session.last_reviewer_feedback = `Previous feedback: ${session.paused_state.context.lastFeedback}\nUser guidance: ${answer}`;
|
|
2065
|
+
}
|
|
2066
|
+
session.repeated_issue_count = 0;
|
|
2067
|
+
session.sonar_retry_count = 0;
|
|
2068
|
+
session.reviewer_retry_count = 0;
|
|
2069
|
+
session.standby_retry_count = 0;
|
|
2070
|
+
session.tester_retry_count = 0;
|
|
2071
|
+
session.security_retry_count = 0;
|
|
2072
|
+
session.last_sonar_issue_signature = null;
|
|
2073
|
+
session.sonar_repeat_count = 0;
|
|
2074
|
+
session.last_reviewer_issue_signature = null;
|
|
2075
|
+
session.reviewer_repeat_count = 0;
|
|
2076
|
+
await saveSession(session);
|
|
2077
|
+
|
|
2078
|
+
// Re-run the flow with the existing session context
|
|
2079
|
+
try {
|
|
2080
|
+
return await runFlow({ task, config: sessionConfig, logger, flags, emitter, askQuestion });
|
|
2081
|
+
} catch (err) {
|
|
2082
|
+
await markSessionStatus(session, "failed");
|
|
2083
|
+
throw err;
|
|
2084
|
+
}
|
|
2085
|
+
}
|