swarm-orchestrator 7.0.0-alpha.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/LICENSE +15 -0
- package/README.md +199 -0
- package/agents/reviewer.agent.md +50 -0
- package/agents/worker.agent.md +41 -0
- package/config/default-agents.yaml +56 -0
- package/config/pm-agent.yaml +36 -0
- package/config/quality-gates.yaml +88 -0
- package/config/repair-agent.yaml +34 -0
- package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
- package/config/semgrep-rules/exception-swallowing.yaml +13 -0
- package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
- package/config/semgrep-rules/mock-mutation.yaml +6 -0
- package/config/semgrep-rules/test-modification.yaml +6 -0
- package/config/user-agents.yaml +23 -0
- package/dist/src/adapters/adapter-factory.js +29 -0
- package/dist/src/adapters/agent-adapter.js +32 -0
- package/dist/src/adapters/claude-code-adapter.js +126 -0
- package/dist/src/adapters/claude-code-teams.js +119 -0
- package/dist/src/adapters/codex-adapter.js +118 -0
- package/dist/src/adapters/copilot-adapter.js +224 -0
- package/dist/src/adapters/fatal-error-classifier.js +88 -0
- package/dist/src/adapters/index.js +44 -0
- package/dist/src/adapters/persistent-session.js +204 -0
- package/dist/src/adapters/process-supervisor.js +199 -0
- package/dist/src/agents-exporter.js +398 -0
- package/dist/src/analytics-log.js +161 -0
- package/dist/src/baseline-scanner.js +65 -0
- package/dist/src/bootstrap-evidence.js +88 -0
- package/dist/src/bootstrap-orchestrator.js +205 -0
- package/dist/src/bootstrap-types.js +5 -0
- package/dist/src/branch-merger.js +427 -0
- package/dist/src/cli/attest-handlers.js +44 -0
- package/dist/src/cli/cost-prompt.js +51 -0
- package/dist/src/cli/demo-handlers.js +274 -0
- package/dist/src/cli/flags.js +152 -0
- package/dist/src/cli/index.js +57 -0
- package/dist/src/cli/live-status.js +186 -0
- package/dist/src/cli/misc-handlers.js +242 -0
- package/dist/src/cli/plan-handlers.js +350 -0
- package/dist/src/cli/share-handlers.js +163 -0
- package/dist/src/cli/status-handlers.js +457 -0
- package/dist/src/cli/swarm-handlers.js +638 -0
- package/dist/src/cli/usage.js +72 -0
- package/dist/src/cli.js +217 -0
- package/dist/src/commit-pattern-detector.js +143 -0
- package/dist/src/commit-quality-analyzer.js +38 -0
- package/dist/src/config-loader.js +339 -0
- package/dist/src/context-broker.js +358 -0
- package/dist/src/copilot-cli-wrapper.js +258 -0
- package/dist/src/cost-estimator.js +241 -0
- package/dist/src/defaults.js +18 -0
- package/dist/src/demo-mode.js +180 -0
- package/dist/src/deployment-handler.js +78 -0
- package/dist/src/deployment-manager.js +228 -0
- package/dist/src/execution-queue.js +196 -0
- package/dist/src/external-tool-manager.js +184 -0
- package/dist/src/gate-prompt-builder.js +117 -0
- package/dist/src/gate-remediation.js +173 -0
- package/dist/src/github-issues-ingester.js +152 -0
- package/dist/src/github-mcp-integrator.js +164 -0
- package/dist/src/hook-generator.js +229 -0
- package/dist/src/index.js +48 -0
- package/dist/src/knowledge-base.js +300 -0
- package/dist/src/logger.js +111 -0
- package/dist/src/meta-analyzer.js +208 -0
- package/dist/src/metrics-collector.js +209 -0
- package/dist/src/metrics-types.js +5 -0
- package/dist/src/multi-repo-coordinator.js +179 -0
- package/dist/src/orchestrator/async-meta-analysis.js +84 -0
- package/dist/src/orchestrator/fatal-run-error.js +43 -0
- package/dist/src/orchestrator/final-gates-remediation.js +258 -0
- package/dist/src/orchestrator/git-state-utils.js +119 -0
- package/dist/src/orchestrator/pause-controller.js +55 -0
- package/dist/src/orchestrator/replan-runner.js +258 -0
- package/dist/src/orchestrator/step-executor.js +391 -0
- package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
- package/dist/src/owasp-mapper.js +153 -0
- package/dist/src/owasp-report-renderer.js +39 -0
- package/dist/src/plan-files.js +142 -0
- package/dist/src/plan-generator.js +1207 -0
- package/dist/src/pm-agent.js +296 -0
- package/dist/src/post-run-reporter.js +203 -0
- package/dist/src/pr-automation.js +103 -0
- package/dist/src/pr-manager.js +318 -0
- package/dist/src/presenter/index.js +209 -0
- package/dist/src/prompt-builder.js +173 -0
- package/dist/src/quality-gates/config-loader.js +196 -0
- package/dist/src/quality-gates/default-config.js +243 -0
- package/dist/src/quality-gates/file-utils.js +152 -0
- package/dist/src/quality-gates/gate-runner.js +171 -0
- package/dist/src/quality-gates/gates/accessibility.js +355 -0
- package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
- package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
- package/dist/src/quality-gates/gates/readme-claims.js +62 -0
- package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
- package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
- package/dist/src/quality-gates/gates/test-coverage.js +240 -0
- package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
- package/dist/src/quality-gates/gates/test-isolation.js +74 -0
- package/dist/src/quality-gates/index.js +17 -0
- package/dist/src/quality-gates/registry.js +205 -0
- package/dist/src/quality-gates/types.js +2 -0
- package/dist/src/quick-fix-mode.js +302 -0
- package/dist/src/recipe-loader.js +122 -0
- package/dist/src/repair-agent.js +396 -0
- package/dist/src/repo-analyzer.js +342 -0
- package/dist/src/report-generator.js +146 -0
- package/dist/src/report-renderer.js +90 -0
- package/dist/src/requirement-filter.js +52 -0
- package/dist/src/sarif-formatter.js +140 -0
- package/dist/src/scheduling/dependency-analyzer.js +178 -0
- package/dist/src/scheduling/work-stealing-queue.js +123 -0
- package/dist/src/secret-redactor.js +90 -0
- package/dist/src/session-executor.js +587 -0
- package/dist/src/session-manager.js +221 -0
- package/dist/src/share/transcript-verification.js +128 -0
- package/dist/src/share-parser.js +477 -0
- package/dist/src/spinner.js +146 -0
- package/dist/src/step-runner.js +365 -0
- package/dist/src/swarm-orchestrator.js +686 -0
- package/dist/src/task-classifier.js +134 -0
- package/dist/src/test-command-discovery.js +123 -0
- package/dist/src/text-similarity.js +24 -0
- package/dist/src/tier-maps.js +158 -0
- package/dist/src/types.js +5 -0
- package/dist/src/url-shortener.js +135 -0
- package/dist/src/verification/attestation.js +166 -0
- package/dist/src/verification/cheat-detector.js +240 -0
- package/dist/src/verification/command-runner.js +70 -0
- package/dist/src/verification/composite-score.js +157 -0
- package/dist/src/verification/cosign-attestation.js +137 -0
- package/dist/src/verification/diff-analysis.js +89 -0
- package/dist/src/verification/differential-gate.js +173 -0
- package/dist/src/verification/index.js +41 -0
- package/dist/src/verification/mutation-gate.js +242 -0
- package/dist/src/verification/property-gate.js +201 -0
- package/dist/src/verification/test-synthesizer.js +243 -0
- package/dist/src/verifier/outcome-checks.js +396 -0
- package/dist/src/verifier/transcript-checks.js +207 -0
- package/dist/src/verifier/verification-reporters.js +148 -0
- package/dist/src/verifier-engine.js +491 -0
- package/dist/src/wave-resizer.js +131 -0
- package/dist/src/wave-scheduler.js +44 -0
- package/dist/src/worktree-manager.js +368 -0
- package/dist/src/worktree-reserved-paths.js +97 -0
- package/package.json +80 -0
- package/plugin/agents/.gitkeep +0 -0
- package/plugin/agents/backend_master.agent.md +72 -0
- package/plugin/agents/devops_pro.agent.md +45 -0
- package/plugin/agents/frontend_expert.agent.md +59 -0
- package/plugin/agents/integrator_finalizer.agent.md +44 -0
- package/plugin/agents/meta_reviewer.agent.md +44 -0
- package/plugin/agents/security_auditor.agent.md +43 -0
- package/plugin/agents/tester_elite.agent.md +43 -0
- package/plugin/hooks/hooks.json +19 -0
- package/plugin/hooks/post-tool-use.json +9 -0
- package/plugin/hooks/pre-tool-use.json +9 -0
- package/plugin/plugin.json +47 -0
- package/plugin/skills/swarm-gates/SKILL.md +53 -0
- package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
- package/plugin/skills/swarm-verify/SKILL.md +44 -0
- package/templates/README.md +35 -0
- package/templates/cli-tool.json +31 -0
- package/templates/fullstack.json +52 -0
- package/templates/library.json +38 -0
- package/templates/react-app.json +45 -0
- package/templates/recipes/add-api-docs.json +35 -0
- package/templates/recipes/add-auth.json +47 -0
- package/templates/recipes/add-ci.json +39 -0
- package/templates/recipes/add-tests.json +39 -0
- package/templates/recipes/migrate-to-ts.json +42 -0
- package/templates/recipes/refactor-modularize.json +44 -0
- package/templates/recipes/security-audit.json +35 -0
- package/templates/rest-api.json +38 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.executeReplan = executeReplan;
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const plan_generator_1 = require("../plan-generator");
|
|
43
|
+
const repair_agent_1 = __importDefault(require("../repair-agent"));
|
|
44
|
+
const logger_1 = require("../logger");
|
|
45
|
+
const logger = (0, logger_1.getLogger)('orchestrator');
|
|
46
|
+
/**
|
|
47
|
+
* WARNING: This function mutates context.plan by assignment.
|
|
48
|
+
*
|
|
49
|
+
* After executeReplan returns, context.plan points to a different
|
|
50
|
+
* ExecutionPlan object than it did before the call. Any code that
|
|
51
|
+
* cached context.plan or context.plan.steps before the call is
|
|
52
|
+
* now holding a stale reference.
|
|
53
|
+
*
|
|
54
|
+
* The wave scheduler (wave-scheduler-loop.ts) handles this by
|
|
55
|
+
* re-reading context.plan.steps on every loop iteration. Do not
|
|
56
|
+
* change this function's mutation behavior without updating the
|
|
57
|
+
* scheduler's invariant comment and test.
|
|
58
|
+
*
|
|
59
|
+
* Behavior: retries failed steps on new branches with a "-retryN"
|
|
60
|
+
* suffix (preserving completed work), then appends and runs any
|
|
61
|
+
* `replanPayload.addSteps`, finally merging their successful
|
|
62
|
+
* branches to main so subsequent quality-gate re-checks see the
|
|
63
|
+
* remediation changes.
|
|
64
|
+
*
|
|
65
|
+
* @param host - orchestrator surface providing workingDir, resolveAgent,
|
|
66
|
+
* switchBranch, createAgentBranch, executeStepInSwarm, mergeWaveBranches
|
|
67
|
+
* @param context - mutable replan context; `context.plan` may be reassigned
|
|
68
|
+
* @param replanPayload - retries and add-steps to execute
|
|
69
|
+
* @param agents - available agent map
|
|
70
|
+
* @param options - orchestrator options (model, onProgress)
|
|
71
|
+
*/
|
|
72
|
+
async function executeReplan(host, context, replanPayload, agents, options) {
|
|
73
|
+
logger.info('\n🔄 Executing replan...');
|
|
74
|
+
const maxRetries = options?.maxRetries ?? 3;
|
|
75
|
+
// initialize replan state
|
|
76
|
+
context.replanState = {
|
|
77
|
+
triggeredAt: new Date().toISOString(),
|
|
78
|
+
payload: replanPayload,
|
|
79
|
+
retryBranches: new Map()
|
|
80
|
+
};
|
|
81
|
+
// update knowledge base with replan event
|
|
82
|
+
context.knowledgeBase?.addOrUpdatePattern({
|
|
83
|
+
category: 'failure_mode',
|
|
84
|
+
insight: `replan triggered for steps: ${replanPayload.retrySteps.join(', ')}`,
|
|
85
|
+
confidence: 'high',
|
|
86
|
+
evidence: [`replan at ${context.replanState.triggeredAt}`],
|
|
87
|
+
impact: 'medium'
|
|
88
|
+
});
|
|
89
|
+
// execute retries for each failed step using the repair agent
|
|
90
|
+
for (const stepNumber of replanPayload.retrySteps) {
|
|
91
|
+
const step = context.plan.steps.find(s => s.stepNumber === stepNumber);
|
|
92
|
+
if (!step) {
|
|
93
|
+
logger.warn(` replan: step ${stepNumber} not found, skipping`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const agent = host.resolveAgent(agents, step.agentName);
|
|
97
|
+
if (!agent) {
|
|
98
|
+
logger.warn(` replan: agent ${step.agentName} not found, skipping`);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// get current retry count
|
|
102
|
+
const resultIndex = context.results.findIndex(r => r.stepNumber === stepNumber);
|
|
103
|
+
const result = context.results[resultIndex];
|
|
104
|
+
const retryCount = (result?.retryCount || 0) + 1;
|
|
105
|
+
if (retryCount > maxRetries) {
|
|
106
|
+
logger.error(` step ${stepNumber} exceeded max retries (${maxRetries}), skipping`);
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
logger.info(` 🔧 Spawning repair agent for step ${stepNumber} (${agent.name}) - attempt ${retryCount}`);
|
|
110
|
+
// create retry branch with suffix
|
|
111
|
+
const retryBranchName = `swarm/${context.executionId}/step-${stepNumber}-${agent.name.toLowerCase()}-retry${retryCount}`;
|
|
112
|
+
// track retry branch
|
|
113
|
+
if (!context.replanState.retryBranches.has(stepNumber)) {
|
|
114
|
+
context.replanState.retryBranches.set(stepNumber, []);
|
|
115
|
+
}
|
|
116
|
+
context.replanState.retryBranches.get(stepNumber).push(retryBranchName);
|
|
117
|
+
// reset result status
|
|
118
|
+
if (result) {
|
|
119
|
+
result.status = 'pending';
|
|
120
|
+
result.retryCount = retryCount;
|
|
121
|
+
result.branchName = retryBranchName;
|
|
122
|
+
delete result.error;
|
|
123
|
+
delete result.sessionResult;
|
|
124
|
+
delete result.verificationResult;
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
// switch to main branch before creating retry branch
|
|
128
|
+
await host.switchBranch(context.mainBranch);
|
|
129
|
+
await host.createAgentBranch(retryBranchName, context.mainBranch);
|
|
130
|
+
// Build repair context from the failed step's results
|
|
131
|
+
const stepDir = path.join(context.runDir, 'steps', `step-${stepNumber}`);
|
|
132
|
+
const transcriptPath = path.join(stepDir, 'share.md');
|
|
133
|
+
const verificationReportPath = path.join(context.runDir, 'verification', `step-${stepNumber}-verification.md`);
|
|
134
|
+
const failedChecks = [];
|
|
135
|
+
let rootCause = 'Verification checks failed';
|
|
136
|
+
if (result?.verificationResult) {
|
|
137
|
+
const repairAgentHelper = new repair_agent_1.default(host.workingDir, maxRetries);
|
|
138
|
+
const extracted = repairAgentHelper.extractFailedChecks(result.verificationResult);
|
|
139
|
+
failedChecks.push(...extracted);
|
|
140
|
+
// Derive root cause from check types (includes outcome-based checks)
|
|
141
|
+
const hasTestFailure = result.verificationResult.checks.some(c => !c.passed && (c.type === 'test' || c.type === 'test_exec'));
|
|
142
|
+
const hasBuildFailure = result.verificationResult.checks.some(c => !c.passed && (c.type === 'build' || c.type === 'build_exec'));
|
|
143
|
+
const hasCommitFailure = result.verificationResult.checks.some(c => !c.passed && c.type === 'commit');
|
|
144
|
+
const hasNoDiff = result.verificationResult.checks.some(c => !c.passed && c.type === 'git_diff');
|
|
145
|
+
const hasMissingFiles = result.verificationResult.checks.some(c => !c.passed && c.type === 'file_existence');
|
|
146
|
+
if (hasMissingFiles)
|
|
147
|
+
rootCause = 'Expected files were not created';
|
|
148
|
+
else if (hasTestFailure)
|
|
149
|
+
rootCause = 'Tests not executed or failed';
|
|
150
|
+
else if (hasBuildFailure)
|
|
151
|
+
rootCause = 'Build not executed or failed';
|
|
152
|
+
else if (hasNoDiff)
|
|
153
|
+
rootCause = 'Agent made no code changes';
|
|
154
|
+
else if (hasCommitFailure)
|
|
155
|
+
rootCause = 'No commits made';
|
|
156
|
+
}
|
|
157
|
+
const repairContext = {
|
|
158
|
+
stepNumber,
|
|
159
|
+
agentName: agent.name,
|
|
160
|
+
originalTask: step.task,
|
|
161
|
+
transcriptPath,
|
|
162
|
+
verificationReportPath,
|
|
163
|
+
branchName: retryBranchName,
|
|
164
|
+
failedChecks,
|
|
165
|
+
rootCause,
|
|
166
|
+
retryCount,
|
|
167
|
+
failureContext: result.verificationResult?.failureContext,
|
|
168
|
+
};
|
|
169
|
+
const repairAgent = new repair_agent_1.default(host.workingDir, maxRetries);
|
|
170
|
+
const sessionOpts = {
|
|
171
|
+
allowAllTools: true,
|
|
172
|
+
...(options?.model && { model: options.model })
|
|
173
|
+
};
|
|
174
|
+
const repairResult = await repairAgent.attemptRepair(repairContext, sessionOpts, {
|
|
175
|
+
requireTests: /\b(test suite|unit test|integration test|e2e test|write tests)\b/i.test(step.task),
|
|
176
|
+
requireBuild: /\b(npm build|run build|compile|bundle|webpack)\b/i.test(step.task),
|
|
177
|
+
requireCommits: false
|
|
178
|
+
});
|
|
179
|
+
// Log repair cost
|
|
180
|
+
logger.info(` 📊 Repair cost: ~${repairResult.estimatedTokenCost} tokens, ${repairResult.attempts} attempt(s), ${Math.round(repairResult.totalDurationMs / 1000)}s`);
|
|
181
|
+
// Save repair result to run directory
|
|
182
|
+
const repairResultPath = path.join(context.runDir, `repair-step-${stepNumber}.json`);
|
|
183
|
+
fs.writeFileSync(repairResultPath, JSON.stringify(repairResult, null, 2), 'utf8');
|
|
184
|
+
if (repairResult.success) {
|
|
185
|
+
if (result) {
|
|
186
|
+
result.status = 'completed';
|
|
187
|
+
result.endTime = new Date().toISOString();
|
|
188
|
+
}
|
|
189
|
+
logger.info(` ✅ Repair succeeded for step ${stepNumber} after ${repairResult.attempts} attempt(s)`);
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
// Repair failed - fall back to standard re-execution as last resort
|
|
193
|
+
logger.warn(` ⚠️ Repair agent failed; falling back to full re-execution for step ${stepNumber}`);
|
|
194
|
+
const retryStep = { ...step, task: `[RETRY ${retryCount}] ${step.task}` };
|
|
195
|
+
await host.executeStepInSwarm(retryStep, agent, context, options);
|
|
196
|
+
logger.info(` ✅ Fallback retry succeeded for step ${stepNumber}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
const err = error;
|
|
201
|
+
logger.error(` ❌ Retry ${retryCount} failed for step ${stepNumber}: ${err.message}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// append and execute any new steps
|
|
205
|
+
if (replanPayload.addSteps && replanPayload.addSteps.length > 0) {
|
|
206
|
+
const generator = new plan_generator_1.PlanGenerator(Array.from(agents.values()));
|
|
207
|
+
const completed = context.results.filter(r => r.status === 'completed').map(r => r.stepNumber);
|
|
208
|
+
const revised = generator.revisePlan(context.plan, replanPayload, completed);
|
|
209
|
+
const oldMax = Math.max(...context.plan.steps.map(s => s.stepNumber));
|
|
210
|
+
const newSteps = revised.steps.filter((s) => s.stepNumber > oldMax);
|
|
211
|
+
context.plan = revised;
|
|
212
|
+
for (const s of newSteps) {
|
|
213
|
+
context.results.push({
|
|
214
|
+
stepNumber: s.stepNumber,
|
|
215
|
+
agentName: s.agentName,
|
|
216
|
+
status: 'pending'
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
logger.info(` ➕ Replan added ${newSteps.length} new step(s)`);
|
|
220
|
+
// Notify listeners immediately so totalSteps and progress state update
|
|
221
|
+
// before the replan steps start executing
|
|
222
|
+
options?.onProgress?.(context, `replan-added:${newSteps.length}`);
|
|
223
|
+
// Execute added steps in parallel when they have no mutual dependencies
|
|
224
|
+
const replanPromises = newSteps.map(async (added) => {
|
|
225
|
+
const agent = host.resolveAgent(agents, added.agentName);
|
|
226
|
+
if (!agent) {
|
|
227
|
+
logger.warn(` replan: agent ${added.agentName} not found for step ${added.stepNumber}, skipping`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
logger.info(` 🧩 Executing added step ${added.stepNumber} (${agent.name})`);
|
|
231
|
+
try {
|
|
232
|
+
await host.executeStepInSwarm(added, agent, context, options);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
236
|
+
logger.error(` ❌ Replan step ${added.stepNumber} failed: ${msg}`);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
await Promise.allSettled(replanPromises);
|
|
240
|
+
// Merge completed replan branches to main so quality gate re-checks
|
|
241
|
+
// see the remediation changes. Without this, replan work stays on
|
|
242
|
+
// unmerged branches and gate re-runs read stale code.
|
|
243
|
+
const completedReplan = newSteps
|
|
244
|
+
.map(s => context.results.find(r => r.stepNumber === s.stepNumber))
|
|
245
|
+
.filter((r) => !!r && r.status === 'completed' && !!r.branchName);
|
|
246
|
+
if (completedReplan.length > 0) {
|
|
247
|
+
context.contextBroker.forceReleaseStaleLocks();
|
|
248
|
+
await host.mergeWaveBranches(completedReplan, context, options);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
// save replan state to run directory
|
|
252
|
+
const replanPath = path.join(context.runDir, 'replan-state.json');
|
|
253
|
+
fs.writeFileSync(replanPath, JSON.stringify({
|
|
254
|
+
...context.replanState,
|
|
255
|
+
retryBranches: Object.fromEntries(context.replanState.retryBranches)
|
|
256
|
+
}, null, 2), 'utf8');
|
|
257
|
+
logger.info(' 📝 Replan state saved');
|
|
258
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.executeStepInSwarm = executeStepInSwarm;
|
|
40
|
+
const child_process_1 = require("child_process");
|
|
41
|
+
const fs = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const adapters_1 = require("../adapters");
|
|
44
|
+
const commit_quality_analyzer_1 = require("../commit-quality-analyzer");
|
|
45
|
+
const deployment_handler_1 = require("../deployment-handler");
|
|
46
|
+
const hook_generator_1 = require("../hook-generator");
|
|
47
|
+
const prompt_builder_1 = require("../prompt-builder");
|
|
48
|
+
const session_executor_1 = __importDefault(require("../session-executor"));
|
|
49
|
+
const spinner_1 = require("../spinner");
|
|
50
|
+
const defaults_1 = require("../defaults");
|
|
51
|
+
const worktree_reserved_paths_1 = require("../worktree-reserved-paths");
|
|
52
|
+
const live_status_1 = require("../cli/live-status");
|
|
53
|
+
const fatal_run_error_1 = require("./fatal-run-error");
|
|
54
|
+
/**
|
|
55
|
+
* Human-readable label for a CLI agent tool, used in fallback transcript headers.
|
|
56
|
+
*/
|
|
57
|
+
function transcriptToolLabel(tool) {
|
|
58
|
+
switch (tool) {
|
|
59
|
+
case 'codex': return 'Codex';
|
|
60
|
+
case 'claude-code': return 'Claude Code';
|
|
61
|
+
case 'claude-code-teams': return 'Claude Code Teams';
|
|
62
|
+
case 'copilot': return 'Copilot';
|
|
63
|
+
default: return tool.charAt(0).toUpperCase() + tool.slice(1);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Execute a single step end-to-end: wait for deps, create worktree,
|
|
68
|
+
* build prompt, run the agent session, commit
|
|
69
|
+
* uncommitted work, verify, record context / cost / metrics, optional
|
|
70
|
+
* deployment on success, rollback on failure.
|
|
71
|
+
*
|
|
72
|
+
* Mutates `context.results[<stepNumber>]` in place for status,
|
|
73
|
+
* branchName, timestamps, sessionResult, verificationResult, and error.
|
|
74
|
+
* Pushes to `context.stepCostRecords` when cost tracking is active.
|
|
75
|
+
*
|
|
76
|
+
* Throws on verification failure so the scheduler can mark the step
|
|
77
|
+
* failed; a ContextEntry is still added to the broker on failure so
|
|
78
|
+
* downstream waiters don't hang.
|
|
79
|
+
*
|
|
80
|
+
* @param host - orchestrator surface (workingDir, shareParser,
|
|
81
|
+
* verifier, createAgentWorktree)
|
|
82
|
+
* @param step - the plan step to execute
|
|
83
|
+
* @param agent - the agent assigned to this step
|
|
84
|
+
* @param context - mutable execution context
|
|
85
|
+
* @param options - execution options (model, cliAgent, hooks, etc.)
|
|
86
|
+
*/
|
|
87
|
+
async function executeStepInSwarm(host, step, agent, context, options) {
|
|
88
|
+
const resultIndex = context.results.findIndex(r => r.stepNumber === step.stepNumber);
|
|
89
|
+
const result = context.results[resultIndex];
|
|
90
|
+
if (!result) {
|
|
91
|
+
throw new Error(`Result for step ${step.stepNumber} not found`);
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
// wait for dependencies with spinner feedback
|
|
95
|
+
if (step.dependencies.length > 0) {
|
|
96
|
+
const depSpinner = new spinner_1.Spinner(`Step ${step.stepNumber} — Waiting for dependencies (${step.dependencies.join(', ')})...`, { style: 'pulse', prefix: ' ' });
|
|
97
|
+
depSpinner.start();
|
|
98
|
+
try {
|
|
99
|
+
const satisfied = await context.contextBroker.waitForDependencies(step.dependencies, defaults_1.DEFAULT_DEPENDENCY_WAIT_MS);
|
|
100
|
+
if (!satisfied) {
|
|
101
|
+
depSpinner.fail(`Step ${step.stepNumber} — Dependencies timeout`);
|
|
102
|
+
throw new Error('Dependencies timeout after 10 minutes');
|
|
103
|
+
}
|
|
104
|
+
depSpinner.succeed(`Step ${step.stepNumber} — Dependencies ready`);
|
|
105
|
+
}
|
|
106
|
+
catch (depError) {
|
|
107
|
+
depSpinner.fail(`Step ${step.stepNumber} — Dependency failed`);
|
|
108
|
+
throw depError;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Track step execution
|
|
112
|
+
context.metricsCollector?.trackStep(step.stepNumber, agent.name);
|
|
113
|
+
// create per-agent branch and worktree for true parallel isolation
|
|
114
|
+
const branchName = `swarm/${context.executionId}/step-${step.stepNumber}-${agent.name.toLowerCase()}`;
|
|
115
|
+
result.branchName = branchName;
|
|
116
|
+
result.status = 'running';
|
|
117
|
+
result.startTime = new Date().toISOString();
|
|
118
|
+
// Notify progress: step is now running
|
|
119
|
+
options?.onProgress?.(context, `step-running:${step.stepNumber}`);
|
|
120
|
+
// Use git worktree so each agent has its own isolated working directory
|
|
121
|
+
const stepRepoDir = step.repo || host.workingDir;
|
|
122
|
+
const worktreePath = await host.createAgentWorktree(branchName, context.mainBranch, context.runDir, step.stepNumber, stepRepoDir);
|
|
123
|
+
const liveStatus = (0, live_status_1.getLiveStatus)();
|
|
124
|
+
const liveStepId = `step-${step.stepNumber}`;
|
|
125
|
+
const liveLabel = `step-${step.stepNumber} ${agent.name}`;
|
|
126
|
+
// Capture baseline SHA before agent execution for outcome-based verification
|
|
127
|
+
const baseSha = (0, child_process_1.execSync)('git rev-parse HEAD', { cwd: worktreePath, encoding: 'utf8' }).trim();
|
|
128
|
+
// build enhanced prompt with dependency context
|
|
129
|
+
const strictIsolation = options?.strictIsolation ?? false;
|
|
130
|
+
const dependencyContext = context.contextBroker.getDependencyContext(step.dependencies, strictIsolation);
|
|
131
|
+
const enhancedPrompt = (0, prompt_builder_1.buildSwarmPrompt)(step, agent, { ...context, targetProjectRoot: host.workingDir }, dependencyContext);
|
|
132
|
+
// Inner fleet toggle: prefix prompt with /fleet for parallel sub-agent dispatch
|
|
133
|
+
const finalPrompt = options?.useInnerFleet
|
|
134
|
+
? `/fleet ${enhancedPrompt}`
|
|
135
|
+
: enhancedPrompt;
|
|
136
|
+
// execute session on agent branch - IN THE WORKTREE DIRECTORY
|
|
137
|
+
const stepDir = path.join(context.runDir, 'steps', `step-${step.stepNumber}`);
|
|
138
|
+
const transcriptPath = path.join(stepDir, 'share.md');
|
|
139
|
+
// Ensure step directory exists before session runs
|
|
140
|
+
if (!fs.existsSync(stepDir)) {
|
|
141
|
+
fs.mkdirSync(stepDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
// Create a session executor for this worktree
|
|
144
|
+
// Per-step cliAgent takes priority; falls back to run-level option; defaults to copilot
|
|
145
|
+
const adapterName = step.cliAgent || options?.cliAgent || 'copilot';
|
|
146
|
+
const stepAdapter = (0, adapters_1.resolveAdapter)(adapterName);
|
|
147
|
+
const worktreeExecutor = new session_executor_1.default(worktreePath, stepAdapter);
|
|
148
|
+
// Route agent stdout/stderr lines through LiveStatus.print so they appear
|
|
149
|
+
// above the live block, prefixed with `step-N ›` (dim). When LiveStatus is
|
|
150
|
+
// inactive (CI / non-TTY where it was never started, or piped output), the
|
|
151
|
+
// print falls through to a plain stdout write, so behavior degrades.
|
|
152
|
+
const dimOpen = process.stdout.isTTY && !process.env.NO_COLOR ? '\x1b[2m' : '';
|
|
153
|
+
const dimClose = process.stdout.isTTY && !process.env.NO_COLOR ? '\x1b[22m' : '';
|
|
154
|
+
const linePrefix = `${dimOpen}${liveStepId} ›${dimClose}`;
|
|
155
|
+
const actionRe = /^\s*●\s+(.+?)\s*$/;
|
|
156
|
+
const sessionOptions = {
|
|
157
|
+
allowAllTools: true,
|
|
158
|
+
shareToFile: transcriptPath,
|
|
159
|
+
executionMode: 'auto',
|
|
160
|
+
persistentSessionId: `${context.executionId}:${adapterName}:${worktreePath}`,
|
|
161
|
+
onAgentLine: (line) => {
|
|
162
|
+
// Always update the live action from `●` markers so the user sees what
|
|
163
|
+
// the agent is currently doing. Only print the full narration line when
|
|
164
|
+
// --stream-agent is set; otherwise the firehose stays in the run
|
|
165
|
+
// transcript at runs/<id>/steps/step-N/share.md.
|
|
166
|
+
const match = actionRe.exec(line);
|
|
167
|
+
if (match) {
|
|
168
|
+
liveStatus.setAction(liveStepId, match[1]);
|
|
169
|
+
}
|
|
170
|
+
if (options?.streamAgent) {
|
|
171
|
+
liveStatus.print(`${linePrefix} ${line}`);
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
...(options?.model && { model: options.model }),
|
|
175
|
+
};
|
|
176
|
+
// Generate per-step hooks for scope enforcement and evidence capture
|
|
177
|
+
// Hooks default to on unless explicitly disabled via --no-hooks
|
|
178
|
+
let generatedHooks;
|
|
179
|
+
if (options?.hooksEnabled !== false) {
|
|
180
|
+
const hookGen = new hook_generator_1.HookGenerator();
|
|
181
|
+
generatedHooks = hookGen.generateStepHooks({
|
|
182
|
+
step,
|
|
183
|
+
agent,
|
|
184
|
+
executionId: context.executionId,
|
|
185
|
+
runDir: context.runDir,
|
|
186
|
+
stepBranch: branchName,
|
|
187
|
+
workingDir: worktreePath,
|
|
188
|
+
existingTestFiles: context.baselineSnapshot?.testFiles || [],
|
|
189
|
+
});
|
|
190
|
+
// Hooks are auto-loaded by Copilot CLI from <gitRoot>/.github/hooks/
|
|
191
|
+
}
|
|
192
|
+
// only call the agent if we don't already have a session result
|
|
193
|
+
if (!result.sessionResult) {
|
|
194
|
+
liveStatus.addStep(liveStepId, liveLabel);
|
|
195
|
+
const sessionResult = await worktreeExecutor.executeSession(finalPrompt, sessionOptions);
|
|
196
|
+
result.sessionResult = sessionResult;
|
|
197
|
+
// Account-level fatal error: usage-limit, auth, or extended rate-limit.
|
|
198
|
+
// No replan or repair can recover, so abort the run before the
|
|
199
|
+
// verification + replan loop wastes the per-instance budget. The
|
|
200
|
+
// catch block at the bottom of this function records the failure
|
|
201
|
+
// and signals the broker so any waiting dependents unblock.
|
|
202
|
+
if (sessionResult.fatalError) {
|
|
203
|
+
throw new fatal_run_error_1.FatalRunError(sessionResult.fatalError, {
|
|
204
|
+
stepNumber: step.stepNumber,
|
|
205
|
+
agentName: agent.name,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// Non-zero exit code does not mean the agent failed its task.
|
|
209
|
+
// Claude Code often exits non-zero after completing file changes
|
|
210
|
+
// (e.g., cleanup command fails, permission prompt at exit). The
|
|
211
|
+
// verification pipeline below judges whether the work is acceptable;
|
|
212
|
+
// surface the exit code as a non-fatal note via the live block action.
|
|
213
|
+
if (!sessionResult.success) {
|
|
214
|
+
liveStatus.setAction(liveStepId, `non-zero exit ${sessionResult.exitCode}; checking work`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// Clean up hook files after session completes (evidence log in runDir persists)
|
|
218
|
+
if (generatedHooks) {
|
|
219
|
+
const hookGen = new hook_generator_1.HookGenerator();
|
|
220
|
+
hookGen.cleanupHooks(generatedHooks.hooksFilePath);
|
|
221
|
+
}
|
|
222
|
+
// Check if transcript was created, create fallback if not
|
|
223
|
+
if (!fs.existsSync(transcriptPath)) {
|
|
224
|
+
const toolLabel = transcriptToolLabel(options?.cliAgent || 'copilot');
|
|
225
|
+
const fallbackContent = `# ${toolLabel} Session Transcript\n\nSession output:\n\`\`\`\n${result.sessionResult?.output || 'No output captured'}\n\`\`\`\n`;
|
|
226
|
+
fs.writeFileSync(transcriptPath, fallbackContent, 'utf8');
|
|
227
|
+
}
|
|
228
|
+
// parse transcript for context
|
|
229
|
+
const transcriptContent = fs.readFileSync(transcriptPath, 'utf8');
|
|
230
|
+
const shareIndex = host.shareParser.parse(transcriptContent);
|
|
231
|
+
// Track commits from this step
|
|
232
|
+
if (shareIndex.gitCommits) {
|
|
233
|
+
shareIndex.gitCommits.forEach(() => {
|
|
234
|
+
context.metricsCollector?.trackCommit(agent.name);
|
|
235
|
+
});
|
|
236
|
+
// Analyze commit quality for anti-patterns
|
|
237
|
+
await (0, commit_quality_analyzer_1.analyzeCommitQuality)(shareIndex.gitCommits, step.stepNumber, agent.name);
|
|
238
|
+
}
|
|
239
|
+
// Commit any uncommitted work the agent left behind.
|
|
240
|
+
// Some adapters (e.g. Codex) modify files without committing;
|
|
241
|
+
// the git_diff verification check only sees committed changes.
|
|
242
|
+
try {
|
|
243
|
+
const status = (0, child_process_1.execSync)('git status --porcelain', { cwd: worktreePath, encoding: 'utf8' }).trim();
|
|
244
|
+
if (status) {
|
|
245
|
+
// Exclude orchestrator-reserved and build-artifact paths from per-step commits.
|
|
246
|
+
// Bare `git add -A` captures orchestrator state written to runs/, plans/,
|
|
247
|
+
// .quickfix/, node_modules/, __pycache__, etc., bloating commit diffs with
|
|
248
|
+
// noise unrelated to the agent's actual work (smoke5c failure mode, issue #27).
|
|
249
|
+
// gitPathspecExcludes() mirrors the Python-side capture exclusion from
|
|
250
|
+
// worktree_reserved_paths.py so both sides stay in sync. See PR 2.
|
|
251
|
+
const excludes = (0, worktree_reserved_paths_1.gitPathspecExcludes)();
|
|
252
|
+
(0, child_process_1.execSync)(`git add -A -- . ${excludes.map(e => `'${e}'`).join(' ')}`, { cwd: worktreePath, stdio: 'pipe' });
|
|
253
|
+
(0, child_process_1.execSync)(`git commit -m "auto-commit uncommitted work from step ${step.stepNumber} (${agent.name})"`, { cwd: worktreePath, stdio: 'pipe', env: { ...process.env, GIT_TERMINAL_PROMPT: '0' } });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Commit may fail if working tree is truly clean or in detached HEAD; non-fatal
|
|
258
|
+
}
|
|
259
|
+
// Verification phase: surface as a sub-action on the step's live line.
|
|
260
|
+
// When LiveStatus is inactive (non-TTY where it never started, tests),
|
|
261
|
+
// fall back to the legacy spinner so progress is still visible.
|
|
262
|
+
liveStatus.setAction(liveStepId, 'verifying work');
|
|
263
|
+
const verifySpinner = liveStatus.isTTY()
|
|
264
|
+
? null
|
|
265
|
+
: new spinner_1.Spinner(`Step ${step.stepNumber} — Verifying work...`, { style: 'dots', prefix: ' ' });
|
|
266
|
+
verifySpinner?.start();
|
|
267
|
+
const verificationResult = await host.verifier.verifyStep(step.stepNumber, agent.name, transcriptPath, {
|
|
268
|
+
// only require tests if task explicitly mentions testing/test suite
|
|
269
|
+
requireTests: /\b(test suite|unit test|integration test|e2e test|write tests)\b/i.test(step.task),
|
|
270
|
+
// only require build if task explicitly mentions build process (not "build an app")
|
|
271
|
+
requireBuild: /\b(npm build|run build|compile|bundle|webpack)\b/i.test(step.task),
|
|
272
|
+
// commits are desired but not blocking for demo
|
|
273
|
+
requireCommits: false
|
|
274
|
+
}, shareIndex, generatedHooks?.evidenceLogPath, { workdir: worktreePath, baseSha });
|
|
275
|
+
result.verificationResult = verificationResult;
|
|
276
|
+
// Track verification result
|
|
277
|
+
context.metricsCollector?.trackVerification(verificationResult.passed);
|
|
278
|
+
// generate and commit verification report
|
|
279
|
+
const reportPath = path.join(context.runDir, 'verification', `step-${step.stepNumber}-verification.md`);
|
|
280
|
+
await host.verifier.generateVerificationReport(verificationResult, reportPath);
|
|
281
|
+
if (verificationResult.passed) {
|
|
282
|
+
verifySpinner?.stop();
|
|
283
|
+
liveStatus.finishStep(liveStepId, 'done', 'verified');
|
|
284
|
+
// add to shared context BEFORE advisory gates so replan steps can depend on this step
|
|
285
|
+
context.contextBroker.addStepContext({
|
|
286
|
+
stepNumber: step.stepNumber,
|
|
287
|
+
agentName: agent.name,
|
|
288
|
+
timestamp: new Date().toISOString(),
|
|
289
|
+
data: {
|
|
290
|
+
filesChanged: shareIndex.changedFiles,
|
|
291
|
+
outputsSummary: step.expectedOutputs.join(', '),
|
|
292
|
+
branchName,
|
|
293
|
+
commitShas: shareIndex.gitCommits.map(c => c.sha || 'unknown'),
|
|
294
|
+
verificationPassed: verificationResult.passed,
|
|
295
|
+
transcript: transcriptPath
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
await host.verifier.commitVerificationReport(reportPath, step.stepNumber, agent.name, true);
|
|
299
|
+
// Quality gates run only in the final pass (after all branches merged) to avoid
|
|
300
|
+
// spawning extra Copilot sessions mid-execution. The final gates block handles
|
|
301
|
+
// auto-remediation for every gate type, making per-step checks redundant.
|
|
302
|
+
// optional deployment for deployment-focused steps when --confirm-deploy is set
|
|
303
|
+
const isDeploymentStep = /\b(deploy|deployment|vercel|netlify)\b/i.test(step.task);
|
|
304
|
+
if (isDeploymentStep && options?.confirmDeploy) {
|
|
305
|
+
await (0, deployment_handler_1.executeOptionalDeployment)(host.workingDir, step, agent, {
|
|
306
|
+
runDir: context.runDir,
|
|
307
|
+
executionId: context.executionId,
|
|
308
|
+
results: context.results,
|
|
309
|
+
deployments: context.deployments,
|
|
310
|
+
}, {
|
|
311
|
+
...(options.confirmDeploy !== undefined && { confirmDeploy: options.confirmDeploy }),
|
|
312
|
+
...(options.enableExternal !== undefined && { enableExternal: options.enableExternal }),
|
|
313
|
+
...(options.dryRun !== undefined && { dryRun: options.dryRun }),
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
// verification failed - attempt rollback
|
|
319
|
+
verifySpinner?.stop();
|
|
320
|
+
liveStatus.setAction(liveStepId, 'verification failed; rolling back');
|
|
321
|
+
const rollbackResult = await host.verifier.rollback(step.stepNumber, branchName, shareIndex.changedFiles, context.mainBranch);
|
|
322
|
+
const rollbackNote = rollbackResult.success
|
|
323
|
+
? `verification failed · rolled back ${rollbackResult.filesRestored.length} file${rollbackResult.filesRestored.length === 1 ? '' : 's'}`
|
|
324
|
+
: 'verification failed';
|
|
325
|
+
liveStatus.finishStep(liveStepId, 'failed', rollbackNote);
|
|
326
|
+
throw new Error('Step failed verification - see verification report');
|
|
327
|
+
}
|
|
328
|
+
// context was already added before advisory gates block
|
|
329
|
+
result.status = 'completed';
|
|
330
|
+
result.endTime = new Date().toISOString();
|
|
331
|
+
// Record actual cost for this step
|
|
332
|
+
if (context.costEstimator && context.stepCostRecords) {
|
|
333
|
+
const stepEstimate = context.costEstimate?.perStep.find(s => s.stepNumber === step.stepNumber);
|
|
334
|
+
const durationMs = result.startTime && result.endTime
|
|
335
|
+
? new Date(result.endTime).getTime() - new Date(result.startTime).getTime()
|
|
336
|
+
: 0;
|
|
337
|
+
// D5: Use instrumented request count from adapter when available.
|
|
338
|
+
// The adapter parses the actual CLI output for request markers
|
|
339
|
+
// (e.g. Copilot's "Requests N Premium" stderr summary).
|
|
340
|
+
// Fall back to 1 only when the adapter could not determine the count.
|
|
341
|
+
const instrumentedRequests = result.sessionResult?.premiumRequestsConsumed;
|
|
342
|
+
const actualRequests = typeof instrumentedRequests === 'number' ? instrumentedRequests : 1;
|
|
343
|
+
context.costEstimator.recordActual(step.stepNumber, stepEstimate?.estimatedPremiumRequests ?? 1, actualRequests, 0);
|
|
344
|
+
context.stepCostRecords.push({
|
|
345
|
+
stepNumber: step.stepNumber,
|
|
346
|
+
agentName: agent.name,
|
|
347
|
+
estimatedPremiumRequests: stepEstimate?.estimatedPremiumRequests ?? 1,
|
|
348
|
+
actualPremiumRequests: actualRequests,
|
|
349
|
+
retryCount: 0,
|
|
350
|
+
promptTokens: stepEstimate?.estimatedPromptTokens ?? 0,
|
|
351
|
+
fleetMode: !!options?.useInnerFleet,
|
|
352
|
+
durationMs,
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Notify progress: step completed
|
|
356
|
+
options?.onProgress?.(context, `step-done:${step.stepNumber}`);
|
|
357
|
+
// The per-branch merge confirmation ("✅ Merged swarm/...") is
|
|
358
|
+
// already emitted by BranchMerger. No need to announce the step's
|
|
359
|
+
// completion a third time after the verification spinner.
|
|
360
|
+
}
|
|
361
|
+
catch (error) {
|
|
362
|
+
const err = error;
|
|
363
|
+
result.status = 'failed';
|
|
364
|
+
result.error = err.message;
|
|
365
|
+
result.endTime = new Date().toISOString();
|
|
366
|
+
// Signal completion even for failed steps so dependent replan steps
|
|
367
|
+
// don't hang forever in waitForDependencies. The replan step can
|
|
368
|
+
// check context.results to see the step failed and adapt.
|
|
369
|
+
context.contextBroker.addStepContext({
|
|
370
|
+
stepNumber: step.stepNumber,
|
|
371
|
+
agentName: agent.name,
|
|
372
|
+
timestamp: new Date().toISOString(),
|
|
373
|
+
data: {
|
|
374
|
+
filesChanged: [],
|
|
375
|
+
outputsSummary: 'step failed verification',
|
|
376
|
+
branchName: result.branchName || '',
|
|
377
|
+
commitShas: [],
|
|
378
|
+
verificationPassed: false,
|
|
379
|
+
transcript: path.join(context.runDir, 'steps', `step-${step.stepNumber}`, 'share.md')
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
// Notify progress: step failed
|
|
383
|
+
options?.onProgress?.(context, `step-failed:${step.stepNumber}`);
|
|
384
|
+
// Move the step out of the live block with a failure marker. finishStep
|
|
385
|
+
// is idempotent on an already-finished step (it returns silently when the
|
|
386
|
+
// id is gone), so calling it here covers both fail-before-verify and
|
|
387
|
+
// verify-already-finished paths.
|
|
388
|
+
(0, live_status_1.getLiveStatus)().finishStep(`step-${step.stepNumber}`, 'failed', err.message);
|
|
389
|
+
throw error;
|
|
390
|
+
}
|
|
391
|
+
}
|