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