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,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Claude Code CLI adapter: spawns `claude` with --dangerously-skip-permissions
|
|
3
|
+
// for non-interactive use. No transcript sharing; stdout/stderr captured directly.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.ClaudeCodeAdapter = void 0;
|
|
6
|
+
const agent_adapter_1 = require("./agent-adapter");
|
|
7
|
+
const fatal_error_classifier_1 = require("./fatal-error-classifier");
|
|
8
|
+
const persistent_session_1 = require("./persistent-session");
|
|
9
|
+
const process_supervisor_1 = require("./process-supervisor");
|
|
10
|
+
// Claude Code can spend several minutes on internal reasoning and multi-file
|
|
11
|
+
// operations without producing stdout, unlike streaming CLI tools.
|
|
12
|
+
const STALL_TIMEOUT_MS = 600_000;
|
|
13
|
+
class ClaudeCodeAdapter {
|
|
14
|
+
name = 'claude-code';
|
|
15
|
+
supportsPersistentInteractive = true;
|
|
16
|
+
persistentSessions = new Map();
|
|
17
|
+
async spawn(opts) {
|
|
18
|
+
const persistent = await this.tryPersistent(opts);
|
|
19
|
+
if (persistent)
|
|
20
|
+
return persistent;
|
|
21
|
+
const startTime = Date.now();
|
|
22
|
+
// Pipe the prompt via stdin instead of passing it as a CLI argument.
|
|
23
|
+
// Long prompts (SWE-bench issue descriptions with embedded code) can
|
|
24
|
+
// exceed Linux ARG_MAX (~2 MB total argv+env), causing spawn E2BIG.
|
|
25
|
+
// Claude Code supports: echo "prompt" | claude --dangerously-skip-permissions -p -
|
|
26
|
+
const args = ['--dangerously-skip-permissions', '-p', '-'];
|
|
27
|
+
if (opts.model) {
|
|
28
|
+
args.push('--model', opts.model);
|
|
29
|
+
}
|
|
30
|
+
const result = await (0, process_supervisor_1.supervisedSpawn)({
|
|
31
|
+
command: 'claude',
|
|
32
|
+
args,
|
|
33
|
+
cwd: opts.workdir,
|
|
34
|
+
env: (0, agent_adapter_1.buildRestrictedEnv)(['ANTHROPIC_API_KEY']),
|
|
35
|
+
logPrefix: opts.logPrefix,
|
|
36
|
+
stallTimeoutMs: opts.timeout ?? STALL_TIMEOUT_MS,
|
|
37
|
+
stdinData: opts.prompt,
|
|
38
|
+
onLine: opts.onAgentLine ? (line) => opts.onAgentLine(line) : undefined,
|
|
39
|
+
});
|
|
40
|
+
const durationMs = Date.now() - startTime;
|
|
41
|
+
// D5: Parse premium request count from Claude Code CLI output.
|
|
42
|
+
// Claude Code prints a cost/usage summary that we can extract.
|
|
43
|
+
// Fallback: count conversation turns (each assistant response = 1 request).
|
|
44
|
+
const premiumRequestsConsumed = this.parseRequestCount(result.stdout, result.stderr);
|
|
45
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, result.stderr, result.exitCode);
|
|
46
|
+
return {
|
|
47
|
+
stdout: result.stdout,
|
|
48
|
+
stderr: result.stderr,
|
|
49
|
+
exitCode: result.exitCode,
|
|
50
|
+
durationMs,
|
|
51
|
+
executionMode: 'cold-start',
|
|
52
|
+
premiumRequestsConsumed,
|
|
53
|
+
...(fatalError ? { fatalError } : {}),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
async shutdown() {
|
|
57
|
+
await Promise.all(Array.from(this.persistentSessions.values()).map(session => session.shutdown()));
|
|
58
|
+
this.persistentSessions.clear();
|
|
59
|
+
}
|
|
60
|
+
async tryPersistent(opts) {
|
|
61
|
+
if (!shouldAttemptPersistent(opts))
|
|
62
|
+
return undefined;
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const sessionKey = opts.persistentSessionId ?? `${opts.workdir}:${opts.model ?? 'default'}`;
|
|
65
|
+
const args = ['--dangerously-skip-permissions'];
|
|
66
|
+
if (opts.model)
|
|
67
|
+
args.push('--model', opts.model);
|
|
68
|
+
let session = this.persistentSessions.get(sessionKey);
|
|
69
|
+
if (!session || session.unavailable) {
|
|
70
|
+
session = new persistent_session_1.PersistentInteractiveSession({
|
|
71
|
+
command: 'claude',
|
|
72
|
+
args,
|
|
73
|
+
cwd: opts.workdir,
|
|
74
|
+
env: (0, agent_adapter_1.buildRestrictedEnv)(['ANTHROPIC_API_KEY']),
|
|
75
|
+
onLine: (line) => opts.onAgentLine?.(`[claude-code:persistent] ${line}`),
|
|
76
|
+
});
|
|
77
|
+
this.persistentSessions.set(sessionKey, session);
|
|
78
|
+
}
|
|
79
|
+
const result = await session.send(opts.prompt, opts.persistentTurnTimeoutMs ?? opts.timeout ?? STALL_TIMEOUT_MS);
|
|
80
|
+
if (result.exitCode === 0) {
|
|
81
|
+
return {
|
|
82
|
+
...result,
|
|
83
|
+
executionMode: 'persistent-interactive',
|
|
84
|
+
premiumRequestsConsumed: this.parseRequestCount(result.stdout, result.stderr),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, result.stderr, result.exitCode);
|
|
88
|
+
const reason = session.reason ?? (result.stderr || 'persistent interactive mode failed');
|
|
89
|
+
this.persistentSessions.delete(sessionKey);
|
|
90
|
+
await session.shutdown();
|
|
91
|
+
// Fatal account-level errors short-circuit the cold-start fallback for
|
|
92
|
+
// the same reason as in the codex adapter: another spawn cannot recover
|
|
93
|
+
// from a usage-limit or auth wall.
|
|
94
|
+
if (opts.executionMode === 'persistent-interactive' || fatalError) {
|
|
95
|
+
return {
|
|
96
|
+
stdout: result.stdout,
|
|
97
|
+
stderr: result.stderr,
|
|
98
|
+
exitCode: result.exitCode,
|
|
99
|
+
durationMs: Date.now() - startTime,
|
|
100
|
+
executionMode: 'persistent-interactive',
|
|
101
|
+
fallbackReason: reason,
|
|
102
|
+
...(fatalError ? { fatalError } : {}),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* D5: Extract the number of premium API requests from Claude Code output.
|
|
109
|
+
* Claude Code's `claude -p` does not currently emit a stable per-session
|
|
110
|
+
* "premium requests consumed" marker on stdout or stderr — the turn
|
|
111
|
+
* markers the old version counted never appear in non-interactive mode.
|
|
112
|
+
*
|
|
113
|
+
* Return undefined rather than a synthetic "1" so the orchestrator can
|
|
114
|
+
* fall back honestly and `premium_requests_actual` does not silently
|
|
115
|
+
* encode "one per step" when the real count is unknown.
|
|
116
|
+
*/
|
|
117
|
+
parseRequestCount(_stdout, _stderr) {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
|
|
122
|
+
function shouldAttemptPersistent(opts) {
|
|
123
|
+
if (opts.executionMode === 'persistent-interactive')
|
|
124
|
+
return true;
|
|
125
|
+
return opts.executionMode === 'auto' && process.env.SWARM_ENABLE_PERSISTENT_INTERACTIVE === '1';
|
|
126
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ClaudeCodeTeamsAdapter = void 0;
|
|
4
|
+
const claude_code_adapter_1 = require("./claude-code-adapter");
|
|
5
|
+
const process_supervisor_1 = require("./process-supervisor");
|
|
6
|
+
const logger_1 = require("../logger");
|
|
7
|
+
const logger = (0, logger_1.getLogger)('claude-code-teams');
|
|
8
|
+
class ClaudeCodeTeamsAdapter {
|
|
9
|
+
name = 'claude-code-teams';
|
|
10
|
+
fallbackAdapter;
|
|
11
|
+
constructor() {
|
|
12
|
+
this.fallbackAdapter = new claude_code_adapter_1.ClaudeCodeAdapter();
|
|
13
|
+
}
|
|
14
|
+
// Single-step spawn delegates to team execution with one step.
|
|
15
|
+
// Preserves the AgentAdapter contract for callers that don't batch.
|
|
16
|
+
async spawn(opts) {
|
|
17
|
+
const results = await this.executeWave([opts]);
|
|
18
|
+
return results[0];
|
|
19
|
+
}
|
|
20
|
+
async executeWave(steps) {
|
|
21
|
+
if (steps.length === 0)
|
|
22
|
+
return [];
|
|
23
|
+
try {
|
|
24
|
+
return await this.spawnTeamWave(steps);
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
28
|
+
return this.fallbackToPerStep(steps, error);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Check if a model string indicates Opus 4.6+, which is required for
|
|
32
|
+
// Agent Teams. Non-Opus models will trigger a fallback to per-step execution.
|
|
33
|
+
static isOpusModel(model) {
|
|
34
|
+
if (!model)
|
|
35
|
+
return false;
|
|
36
|
+
return /opus/i.test(model);
|
|
37
|
+
}
|
|
38
|
+
async spawnTeamWave(steps) {
|
|
39
|
+
// Build the team lead prompt that spawns teammates for each step
|
|
40
|
+
const teamLeadPrompt = this.buildTeamLeadPrompt(steps);
|
|
41
|
+
const workdir = steps[0].workdir;
|
|
42
|
+
const model = steps[0].model;
|
|
43
|
+
// Agent Teams requires Opus model. Check before attempting to spawn.
|
|
44
|
+
if (model && !ClaudeCodeTeamsAdapter.isOpusModel(model)) {
|
|
45
|
+
throw new Error(`Agent Teams requires Opus 4.6+, got "${model}". Use --tool claude-code for non-Opus models.`);
|
|
46
|
+
}
|
|
47
|
+
const startTime = Date.now();
|
|
48
|
+
// Spawn the team lead as a single claude session with the
|
|
49
|
+
// CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS env var enabled
|
|
50
|
+
const spawnOpts = {
|
|
51
|
+
prompt: teamLeadPrompt,
|
|
52
|
+
workdir,
|
|
53
|
+
};
|
|
54
|
+
if (model)
|
|
55
|
+
spawnOpts.model = model;
|
|
56
|
+
const result = await this.fallbackAdapter.spawn(spawnOpts);
|
|
57
|
+
const durationMs = Date.now() - startTime;
|
|
58
|
+
// If the team lead failed, throw to trigger fallback
|
|
59
|
+
if (result.exitCode !== 0) {
|
|
60
|
+
throw new Error(`Team lead exited with code ${result.exitCode}: ${result.stderr.slice(0, 500)}`);
|
|
61
|
+
}
|
|
62
|
+
// Map the single team lead output to per-step results.
|
|
63
|
+
// Each step gets the same transcript since the team lead orchestrated all work.
|
|
64
|
+
// fatalError is propagated identically across mapped results because
|
|
65
|
+
// a usage-limit / auth error on the team lead applies to every teammate
|
|
66
|
+
// it would have spawned — they share the same underlying account.
|
|
67
|
+
return steps.map((_step, _i) => ({
|
|
68
|
+
stdout: result.stdout,
|
|
69
|
+
stderr: result.stderr,
|
|
70
|
+
exitCode: result.exitCode,
|
|
71
|
+
durationMs,
|
|
72
|
+
shareTranscriptPath: undefined,
|
|
73
|
+
...(result.fatalError ? { fatalError: result.fatalError } : {}),
|
|
74
|
+
}));
|
|
75
|
+
}
|
|
76
|
+
buildTeamLeadPrompt(steps) {
|
|
77
|
+
const sections = [
|
|
78
|
+
`You are the team lead for ${steps.length} task(s).`,
|
|
79
|
+
'Spawn one teammate per task and execute them concurrently.',
|
|
80
|
+
'',
|
|
81
|
+
'For each task below, spawn a teammate with the given prompt.',
|
|
82
|
+
'Wait for all teammates to complete before reporting results.',
|
|
83
|
+
''
|
|
84
|
+
];
|
|
85
|
+
for (let i = 0; i < steps.length; i++) {
|
|
86
|
+
sections.push(`=== Teammate ${i + 1} ===`);
|
|
87
|
+
sections.push(steps[i].prompt);
|
|
88
|
+
sections.push('');
|
|
89
|
+
}
|
|
90
|
+
sections.push('Report the combined results of all teammates when done.');
|
|
91
|
+
return sections.join('\n');
|
|
92
|
+
}
|
|
93
|
+
async fallbackToPerStep(steps, error) {
|
|
94
|
+
logger.warn(`Teams fallback: ${error.message}`);
|
|
95
|
+
// Execute each step individually via the standard claude-code adapter
|
|
96
|
+
const results = [];
|
|
97
|
+
for (const step of steps) {
|
|
98
|
+
results.push(await this.fallbackAdapter.spawn(step));
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
spawnClaudeProcess(prompt, opts) {
|
|
103
|
+
const args = ['-p', prompt, '--yes'];
|
|
104
|
+
if (opts.model) {
|
|
105
|
+
args.push('--model', opts.model);
|
|
106
|
+
}
|
|
107
|
+
const spawnOpts = {
|
|
108
|
+
command: 'claude',
|
|
109
|
+
args,
|
|
110
|
+
cwd: opts.workdir
|
|
111
|
+
};
|
|
112
|
+
if (opts.additionalEnv)
|
|
113
|
+
spawnOpts.env = opts.additionalEnv;
|
|
114
|
+
if (opts.logPrefix)
|
|
115
|
+
spawnOpts.logPrefix = `${opts.logPrefix} [teams]`;
|
|
116
|
+
return (0, process_supervisor_1.supervisedSpawn)(spawnOpts);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.ClaudeCodeTeamsAdapter = ClaudeCodeTeamsAdapter;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// OpenAI Codex CLI adapter: spawns `codex exec` for non-interactive use.
|
|
3
|
+
// Uses --dangerously-bypass-approvals-and-sandbox because git worktrees have
|
|
4
|
+
// .git references to the parent repo, which --full-auto's sandbox blocks.
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.CodexAdapter = void 0;
|
|
7
|
+
const agent_adapter_1 = require("./agent-adapter");
|
|
8
|
+
const fatal_error_classifier_1 = require("./fatal-error-classifier");
|
|
9
|
+
const persistent_session_1 = require("./persistent-session");
|
|
10
|
+
const process_supervisor_1 = require("./process-supervisor");
|
|
11
|
+
// Codex can spend significant time on reasoning and file operations
|
|
12
|
+
// without producing stdout, similar to Claude Code. supervisedSpawn's
|
|
13
|
+
// heartbeat surfaces progress during these quiet periods.
|
|
14
|
+
const STALL_TIMEOUT_MS = 600_000;
|
|
15
|
+
class CodexAdapter {
|
|
16
|
+
name = 'codex';
|
|
17
|
+
supportsPersistentInteractive = true;
|
|
18
|
+
persistentSessions = new Map();
|
|
19
|
+
async spawn(opts) {
|
|
20
|
+
const persistent = await this.tryPersistent(opts);
|
|
21
|
+
if (persistent)
|
|
22
|
+
return persistent;
|
|
23
|
+
const startTime = Date.now();
|
|
24
|
+
const args = [
|
|
25
|
+
'exec',
|
|
26
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
27
|
+
'-C', opts.workdir,
|
|
28
|
+
];
|
|
29
|
+
if (opts.model) {
|
|
30
|
+
args.push('-m', opts.model);
|
|
31
|
+
}
|
|
32
|
+
args.push(opts.prompt);
|
|
33
|
+
// stdinMode: 'ignore' prevents Codex from printing
|
|
34
|
+
// "Reading additional input from stdin..." into the transcript when it
|
|
35
|
+
// detects a piped (but empty) stdin. With stdin wired to /dev/null Codex
|
|
36
|
+
// treats the prompt arg as the full instruction.
|
|
37
|
+
const result = await (0, process_supervisor_1.supervisedSpawn)({
|
|
38
|
+
command: 'codex',
|
|
39
|
+
args,
|
|
40
|
+
cwd: opts.workdir,
|
|
41
|
+
env: (0, agent_adapter_1.buildRestrictedEnv)(['OPENAI_API_KEY']),
|
|
42
|
+
logPrefix: opts.logPrefix ?? '[codex]',
|
|
43
|
+
stallTimeoutMs: opts.timeout ?? STALL_TIMEOUT_MS,
|
|
44
|
+
stdinMode: 'ignore',
|
|
45
|
+
onLine: opts.onAgentLine ? (line) => opts.onAgentLine(line) : undefined,
|
|
46
|
+
});
|
|
47
|
+
const durationMs = Date.now() - startTime;
|
|
48
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, result.stderr, result.exitCode);
|
|
49
|
+
return {
|
|
50
|
+
stdout: result.stdout,
|
|
51
|
+
stderr: result.stderr,
|
|
52
|
+
exitCode: result.exitCode,
|
|
53
|
+
durationMs,
|
|
54
|
+
executionMode: 'cold-start',
|
|
55
|
+
...(fatalError ? { fatalError } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
async shutdown() {
|
|
59
|
+
await Promise.all(Array.from(this.persistentSessions.values()).map(session => session.shutdown()));
|
|
60
|
+
this.persistentSessions.clear();
|
|
61
|
+
}
|
|
62
|
+
async tryPersistent(opts) {
|
|
63
|
+
if (!shouldAttemptPersistent(opts))
|
|
64
|
+
return undefined;
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
const sessionKey = opts.persistentSessionId ?? `${opts.workdir}:${opts.model ?? 'default'}`;
|
|
67
|
+
const args = [
|
|
68
|
+
'--dangerously-bypass-approvals-and-sandbox',
|
|
69
|
+
'-C', opts.workdir,
|
|
70
|
+
'--no-alt-screen',
|
|
71
|
+
];
|
|
72
|
+
if (opts.model)
|
|
73
|
+
args.push('-m', opts.model);
|
|
74
|
+
let session = this.persistentSessions.get(sessionKey);
|
|
75
|
+
if (!session || session.unavailable) {
|
|
76
|
+
session = new persistent_session_1.PersistentInteractiveSession({
|
|
77
|
+
command: 'codex',
|
|
78
|
+
args,
|
|
79
|
+
cwd: opts.workdir,
|
|
80
|
+
env: (0, agent_adapter_1.buildRestrictedEnv)(['OPENAI_API_KEY']),
|
|
81
|
+
onLine: (line) => opts.onAgentLine?.(`[codex:persistent] ${line}`),
|
|
82
|
+
});
|
|
83
|
+
this.persistentSessions.set(sessionKey, session);
|
|
84
|
+
}
|
|
85
|
+
const result = await session.send(opts.prompt, opts.persistentTurnTimeoutMs ?? opts.timeout ?? STALL_TIMEOUT_MS);
|
|
86
|
+
if (result.exitCode === 0) {
|
|
87
|
+
return {
|
|
88
|
+
...result,
|
|
89
|
+
executionMode: 'persistent-interactive',
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, result.stderr, result.exitCode);
|
|
93
|
+
const reason = session.reason ?? (result.stderr || 'persistent interactive mode failed');
|
|
94
|
+
this.persistentSessions.delete(sessionKey);
|
|
95
|
+
await session.shutdown();
|
|
96
|
+
// A fatal account-level error short-circuits the cold-start fallback —
|
|
97
|
+
// a fresh subprocess will hit the same usage-limit / auth wall, just
|
|
98
|
+
// ten seconds later and after another wasted spawn. Surface it now.
|
|
99
|
+
if (opts.executionMode === 'persistent-interactive' || fatalError) {
|
|
100
|
+
return {
|
|
101
|
+
stdout: result.stdout,
|
|
102
|
+
stderr: result.stderr,
|
|
103
|
+
exitCode: result.exitCode,
|
|
104
|
+
durationMs: Date.now() - startTime,
|
|
105
|
+
executionMode: 'persistent-interactive',
|
|
106
|
+
fallbackReason: reason,
|
|
107
|
+
...(fatalError ? { fatalError } : {}),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.CodexAdapter = CodexAdapter;
|
|
114
|
+
function shouldAttemptPersistent(opts) {
|
|
115
|
+
if (opts.executionMode === 'persistent-interactive')
|
|
116
|
+
return true;
|
|
117
|
+
return opts.executionMode === 'auto' && process.env.SWARM_ENABLE_PERSISTENT_INTERACTIVE === '1';
|
|
118
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Copilot CLI adapter: spawns `copilot -p` with the same flags and behavior
|
|
3
|
+
// as the original SessionExecutor.runCommand path. This is a refactor, not
|
|
4
|
+
// new behavior; it preserves the stall detection, line buffering, and heartbeat
|
|
5
|
+
// logic from the original implementation.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.CopilotAdapter = void 0;
|
|
8
|
+
exports.scrubCopilotHostileTokens = scrubCopilotHostileTokens;
|
|
9
|
+
exports.parseCopilotRequestCount = parseCopilotRequestCount;
|
|
10
|
+
exports.hasFatalStderrError = hasFatalStderrError;
|
|
11
|
+
const fatal_error_classifier_1 = require("./fatal-error-classifier");
|
|
12
|
+
const persistent_session_1 = require("./persistent-session");
|
|
13
|
+
const process_supervisor_1 = require("./process-supervisor");
|
|
14
|
+
// Maximum silence before killing a stalled copilot subprocess.
|
|
15
|
+
// Copilot CLI can go quiet for several minutes during extended tool-use
|
|
16
|
+
// or thinking phases, so this needs headroom beyond typical inference time.
|
|
17
|
+
const STALL_TIMEOUT_MS = 300_000;
|
|
18
|
+
// Copilot CLI outputs these when an agent tries to access paths outside its sandbox
|
|
19
|
+
const SCOPE_NOISE_PATTERNS = [
|
|
20
|
+
/Permission denied and could not request permission/i,
|
|
21
|
+
/could not request permission from user/i,
|
|
22
|
+
];
|
|
23
|
+
// Env vars Copilot CLI prefers over its keyring OAuth for authentication.
|
|
24
|
+
// If the user has a repo-scoped PAT (e.g. GITHUB_TOKEN in .env) that lacks
|
|
25
|
+
// "Copilot Requests" permission, Copilot picks it up and every session 401s
|
|
26
|
+
// before a single tool call runs. Scrub these by default so Copilot falls
|
|
27
|
+
// back to the keyring token the user already authenticated with via
|
|
28
|
+
// `copilot /login`. Opt back in by setting SWARM_USE_ENV_GITHUB_TOKEN=1
|
|
29
|
+
// when you do have a Copilot-capable PAT and want to force env-based auth.
|
|
30
|
+
const COPILOT_AUTH_ENV_VARS = ['COPILOT_GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_TOKEN'];
|
|
31
|
+
function scrubCopilotHostileTokens(env) {
|
|
32
|
+
if (env.SWARM_USE_ENV_GITHUB_TOKEN === '1') {
|
|
33
|
+
return { ...env };
|
|
34
|
+
}
|
|
35
|
+
const copy = { ...env };
|
|
36
|
+
for (const key of COPILOT_AUTH_ENV_VARS) {
|
|
37
|
+
delete copy[key];
|
|
38
|
+
}
|
|
39
|
+
return copy;
|
|
40
|
+
}
|
|
41
|
+
// Build the env spread the spawn helpers feed Copilot. The supervisor merges
|
|
42
|
+
// process.env first, so the scrubbed tokens have to be set to `undefined`
|
|
43
|
+
// here (not just deleted from a copy) to actually unset them in the child.
|
|
44
|
+
// Returns the same shape for cold-start and persistent paths so they stay
|
|
45
|
+
// in sync. Respects SWARM_USE_ENV_GITHUB_TOKEN=1 by leaving the tokens
|
|
46
|
+
// alone when the user has explicitly opted into env-based Copilot auth.
|
|
47
|
+
function copilotChildEnv() {
|
|
48
|
+
const env = {
|
|
49
|
+
GIT_AUTHOR_NAME: 'swarm-orchestrator',
|
|
50
|
+
GIT_AUTHOR_EMAIL: 'swarm@localhost',
|
|
51
|
+
GIT_COMMITTER_NAME: 'swarm-orchestrator',
|
|
52
|
+
GIT_COMMITTER_EMAIL: 'swarm@localhost',
|
|
53
|
+
COPILOT_ALLOW_ALL: 'true',
|
|
54
|
+
};
|
|
55
|
+
if (process.env.SWARM_USE_ENV_GITHUB_TOKEN !== '1') {
|
|
56
|
+
for (const key of COPILOT_AUTH_ENV_VARS) {
|
|
57
|
+
env[key] = undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return env;
|
|
61
|
+
}
|
|
62
|
+
// Billing-accurate premium-request count, as reported by the Copilot CLI
|
|
63
|
+
// on its stderr summary block. Format example:
|
|
64
|
+
//
|
|
65
|
+
// Changes +2 -0
|
|
66
|
+
// Requests 4 Premium (112s)
|
|
67
|
+
// Tokens ↑ ...
|
|
68
|
+
//
|
|
69
|
+
// Copilot bills by Premium requests, not raw model invocations, and a
|
|
70
|
+
// multi-tool-use session is usually billed as 1. Parsing this line is
|
|
71
|
+
// the only way to get a number that matches the user's actual bill.
|
|
72
|
+
// Returns undefined when the marker is absent (e.g. auth failure).
|
|
73
|
+
const COPILOT_REQUEST_LINE_RE = /^\s*Requests\s+(\d+)\s+Premium\b/m;
|
|
74
|
+
function parseCopilotRequestCount(stderr) {
|
|
75
|
+
if (!stderr)
|
|
76
|
+
return undefined;
|
|
77
|
+
const m = stderr.match(COPILOT_REQUEST_LINE_RE);
|
|
78
|
+
if (!m)
|
|
79
|
+
return undefined;
|
|
80
|
+
const n = parseInt(m[1], 10);
|
|
81
|
+
return Number.isFinite(n) && n >= 0 ? n : undefined;
|
|
82
|
+
}
|
|
83
|
+
// Copilot CLI sometimes exits 0 even on fatal errors like invalid model
|
|
84
|
+
// names or auth failures. These patterns on stderr indicate the session
|
|
85
|
+
// never ran at all.
|
|
86
|
+
const FATAL_STDERR_PATTERNS = [
|
|
87
|
+
/Model ".*" from --model flag is not available/i,
|
|
88
|
+
/Error:.*not available/i,
|
|
89
|
+
/Authentication failed/i,
|
|
90
|
+
/token.*(?:invalid|expired|lacking)/i,
|
|
91
|
+
];
|
|
92
|
+
function isScopeNoise(line) {
|
|
93
|
+
return SCOPE_NOISE_PATTERNS.some(p => p.test(line));
|
|
94
|
+
}
|
|
95
|
+
// Exported for direct testing without spawning real subprocesses
|
|
96
|
+
function hasFatalStderrError(stderr) {
|
|
97
|
+
return FATAL_STDERR_PATTERNS.some(p => p.test(stderr));
|
|
98
|
+
}
|
|
99
|
+
class CopilotAdapter {
|
|
100
|
+
name = 'copilot';
|
|
101
|
+
supportsPersistentInteractive = true;
|
|
102
|
+
persistentSessions = new Map();
|
|
103
|
+
async spawn(opts) {
|
|
104
|
+
const persistent = await this.tryPersistent(opts);
|
|
105
|
+
if (persistent)
|
|
106
|
+
return persistent;
|
|
107
|
+
const startTime = Date.now();
|
|
108
|
+
const args = ['-p', opts.prompt];
|
|
109
|
+
if (opts.model) {
|
|
110
|
+
args.push('--model', opts.model);
|
|
111
|
+
}
|
|
112
|
+
// --allow-all covers tools, paths, and URLs so the subprocess
|
|
113
|
+
// never blocks waiting for interactive approval on stdin.
|
|
114
|
+
args.push('--allow-all');
|
|
115
|
+
if (opts.copilotAgent) {
|
|
116
|
+
args.push('--agent', opts.copilotAgent);
|
|
117
|
+
}
|
|
118
|
+
const result = await (0, process_supervisor_1.supervisedSpawn)({
|
|
119
|
+
command: 'copilot',
|
|
120
|
+
args,
|
|
121
|
+
cwd: opts.workdir,
|
|
122
|
+
// Copilot CLI authenticates via gh's local keyring, not env-var API keys.
|
|
123
|
+
// It needs the full user environment (XDG_CONFIG_HOME, DBUS_SESSION_BUS_ADDRESS,
|
|
124
|
+
// keyring paths, etc.) to locate stored credentials. Restricting the env
|
|
125
|
+
// like we do for API-key-based adapters breaks auth silently.
|
|
126
|
+
env: copilotChildEnv(),
|
|
127
|
+
logPrefix: opts.logPrefix,
|
|
128
|
+
stallTimeoutMs: opts.timeout ?? STALL_TIMEOUT_MS,
|
|
129
|
+
onLine: opts.onAgentLine ? (line) => opts.onAgentLine(line) : undefined,
|
|
130
|
+
});
|
|
131
|
+
const durationMs = Date.now() - startTime;
|
|
132
|
+
// Copilot prints scope-enforcement messages on stderr when an agent tries
|
|
133
|
+
// to access paths outside its sandbox. These add no diagnostic value to
|
|
134
|
+
// verification or fatal-error parsing, so strip them from the captured
|
|
135
|
+
// stderr before downstream consumers see it. Live streaming via logPrefix
|
|
136
|
+
// may still surface them in real time, which is the correct UX.
|
|
137
|
+
const filteredStderr = result.stderr
|
|
138
|
+
.split('\n')
|
|
139
|
+
.filter((l) => !isScopeNoise(l))
|
|
140
|
+
.join('\n');
|
|
141
|
+
// Copilot CLI exits 0 for certain fatal errors (e.g. invalid model name)
|
|
142
|
+
// that produce no stdout. Detect these and correct the exit code so the
|
|
143
|
+
// orchestrator treats the session as failed rather than empty-but-successful.
|
|
144
|
+
let exitCode = result.exitCode;
|
|
145
|
+
if (exitCode === 0 && !result.stdout.trim() && hasFatalStderrError(filteredStderr)) {
|
|
146
|
+
exitCode = 1;
|
|
147
|
+
}
|
|
148
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, filteredStderr, exitCode);
|
|
149
|
+
return {
|
|
150
|
+
stdout: result.stdout,
|
|
151
|
+
stderr: filteredStderr,
|
|
152
|
+
exitCode,
|
|
153
|
+
durationMs,
|
|
154
|
+
executionMode: 'cold-start',
|
|
155
|
+
premiumRequestsConsumed: parseCopilotRequestCount(filteredStderr),
|
|
156
|
+
...(fatalError ? { fatalError } : {}),
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
async shutdown() {
|
|
160
|
+
await Promise.all(Array.from(this.persistentSessions.values()).map(session => session.shutdown()));
|
|
161
|
+
this.persistentSessions.clear();
|
|
162
|
+
}
|
|
163
|
+
async tryPersistent(opts) {
|
|
164
|
+
if (!shouldAttemptPersistent(opts))
|
|
165
|
+
return undefined;
|
|
166
|
+
const startTime = Date.now();
|
|
167
|
+
const sessionKey = opts.persistentSessionId ?? `${opts.workdir}:${opts.model ?? 'default'}:${opts.copilotAgent ?? ''}`;
|
|
168
|
+
const args = ['--allow-all', '--no-ask-user', '--stream', 'on'];
|
|
169
|
+
if (opts.model)
|
|
170
|
+
args.push('--model', opts.model);
|
|
171
|
+
if (opts.copilotAgent)
|
|
172
|
+
args.push('--agent', opts.copilotAgent);
|
|
173
|
+
let session = this.persistentSessions.get(sessionKey);
|
|
174
|
+
if (!session || session.unavailable) {
|
|
175
|
+
session = new persistent_session_1.PersistentInteractiveSession({
|
|
176
|
+
command: 'copilot',
|
|
177
|
+
args,
|
|
178
|
+
cwd: opts.workdir,
|
|
179
|
+
env: {
|
|
180
|
+
...scrubCopilotHostileTokens(process.env),
|
|
181
|
+
GIT_AUTHOR_NAME: 'swarm-orchestrator',
|
|
182
|
+
GIT_AUTHOR_EMAIL: 'swarm@localhost',
|
|
183
|
+
GIT_COMMITTER_NAME: 'swarm-orchestrator',
|
|
184
|
+
GIT_COMMITTER_EMAIL: 'swarm@localhost',
|
|
185
|
+
COPILOT_ALLOW_ALL: 'true',
|
|
186
|
+
},
|
|
187
|
+
onLine: (line) => opts.onAgentLine?.(`[copilot:persistent] ${line}`),
|
|
188
|
+
});
|
|
189
|
+
this.persistentSessions.set(sessionKey, session);
|
|
190
|
+
}
|
|
191
|
+
const result = await session.send(opts.prompt, opts.persistentTurnTimeoutMs ?? opts.timeout ?? STALL_TIMEOUT_MS);
|
|
192
|
+
if (result.exitCode === 0) {
|
|
193
|
+
return {
|
|
194
|
+
...result,
|
|
195
|
+
executionMode: 'persistent-interactive',
|
|
196
|
+
premiumRequestsConsumed: parseCopilotRequestCount(result.stderr),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
const fatalError = (0, fatal_error_classifier_1.classifyFatalAgentError)(result.stdout, result.stderr, result.exitCode);
|
|
200
|
+
const reason = session.reason ?? (result.stderr || 'persistent interactive mode failed');
|
|
201
|
+
this.persistentSessions.delete(sessionKey);
|
|
202
|
+
await session.shutdown();
|
|
203
|
+
// Same short-circuit rationale as in codex / claude adapters: a fatal
|
|
204
|
+
// account-level error makes the cold-start fallback equally doomed.
|
|
205
|
+
if (opts.executionMode === 'persistent-interactive' || fatalError) {
|
|
206
|
+
return {
|
|
207
|
+
stdout: result.stdout,
|
|
208
|
+
stderr: result.stderr,
|
|
209
|
+
exitCode: result.exitCode,
|
|
210
|
+
durationMs: Date.now() - startTime,
|
|
211
|
+
executionMode: 'persistent-interactive',
|
|
212
|
+
fallbackReason: reason,
|
|
213
|
+
...(fatalError ? { fatalError } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
exports.CopilotAdapter = CopilotAdapter;
|
|
220
|
+
function shouldAttemptPersistent(opts) {
|
|
221
|
+
if (opts.executionMode === 'persistent-interactive')
|
|
222
|
+
return true;
|
|
223
|
+
return opts.executionMode === 'auto' && process.env.SWARM_ENABLE_PERSISTENT_INTERACTIVE === '1';
|
|
224
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Fatal-error classification for agent CLI failures.
|
|
4
|
+
*
|
|
5
|
+
* An agent run can fail in two distinct ways:
|
|
6
|
+
*
|
|
7
|
+
* 1. Recoverable: the agent ran but produced a bad patch. The orchestrator's
|
|
8
|
+
* replan and repair loops can usefully retry.
|
|
9
|
+
*
|
|
10
|
+
* 2. Unrecoverable: the agent never ran (or refused to run) because of an
|
|
11
|
+
* account-level condition such as a usage-limit hit, an expired
|
|
12
|
+
* subscription, or a missing API key. No amount of replanning will
|
|
13
|
+
* change that, and each replan still consumes the per-instance budget.
|
|
14
|
+
*
|
|
15
|
+
* This module turns the second case into a structured signal so the
|
|
16
|
+
* orchestrator can short-circuit instead of burning the 10-minute
|
|
17
|
+
* dependency-wait window per replan step.
|
|
18
|
+
*
|
|
19
|
+
* Patterns are matched against the union of stdout + stderr because the
|
|
20
|
+
* three CLIs we wrap split error reporting differently:
|
|
21
|
+
* - codex prints `ERROR: You've hit your usage limit. Upgrade to Pro ...`
|
|
22
|
+
* on stderr.
|
|
23
|
+
* - claude prints rate-limit / auth errors with a `429` or `401` token in
|
|
24
|
+
* either stream depending on transport.
|
|
25
|
+
* - copilot prints `gh: authentication required` on stderr and `403`
|
|
26
|
+
* bodies on stdout.
|
|
27
|
+
*
|
|
28
|
+
* Patterns are intentionally narrow. False positives here would suppress
|
|
29
|
+
* legitimate retries on transient errors, which is worse than the current
|
|
30
|
+
* 10-minute wait it's trying to avoid.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.classifyFatalAgentError = classifyFatalAgentError;
|
|
34
|
+
// Order matters: more specific patterns first. The first match wins.
|
|
35
|
+
const PATTERNS = [
|
|
36
|
+
// Codex CLI usage-limit message. Verbatim from `codex exec` stderr when an
|
|
37
|
+
// OpenAI account hits its plan ceiling — the only actionable response is
|
|
38
|
+
// "wait until <reset time> or upgrade", neither of which the orchestrator
|
|
39
|
+
// can do mid-sweep. See smoke run 2026-04-28-codex stderr.
|
|
40
|
+
{ kind: 'usage-limit', pattern: /you'?ve hit your usage limit/i },
|
|
41
|
+
{ kind: 'usage-limit', pattern: /upgrade to (pro|plus|team)\b/i },
|
|
42
|
+
{ kind: 'usage-limit', pattern: /quota.*(exceed|exhaust)/i },
|
|
43
|
+
{ kind: 'usage-limit', pattern: /monthly limit reached/i },
|
|
44
|
+
// Auth: missing/invalid credentials. The agent CLI cannot recover by
|
|
45
|
+
// retrying with the same credentials.
|
|
46
|
+
{ kind: 'auth', pattern: /\b401\s+unauthorized\b/i },
|
|
47
|
+
{ kind: 'auth', pattern: /\b403\s+forbidden\b/i },
|
|
48
|
+
{ kind: 'auth', pattern: /authentication\s+(required|failed)/i },
|
|
49
|
+
{ kind: 'auth', pattern: /invalid api[_-]?key/i },
|
|
50
|
+
{ kind: 'auth', pattern: /api[_-]?key.*(missing|not found|not set)/i },
|
|
51
|
+
{ kind: 'auth', pattern: /not (logged in|authenticated)/i },
|
|
52
|
+
// Rate limit with a retry-after window we cannot wait through within a
|
|
53
|
+
// smoke task budget. Matches "try again at HH:MM (PM|AM)" from codex and
|
|
54
|
+
// "rate limit reached.*retry-after: \d{4,}" from anthropic-style payloads.
|
|
55
|
+
{ kind: 'rate-limit-extended', pattern: /try again at \d{1,2}:\d{2}\s*(am|pm)/i },
|
|
56
|
+
{ kind: 'rate-limit-extended', pattern: /retry-after:\s*\d{4,}/i },
|
|
57
|
+
];
|
|
58
|
+
/**
|
|
59
|
+
* Inspect agent CLI output for unrecoverable error signatures.
|
|
60
|
+
*
|
|
61
|
+
* @param stdout combined stdout from the agent process. Pass an empty string when unavailable.
|
|
62
|
+
* @param stderr combined stderr from the agent process. Pass an empty string when unavailable.
|
|
63
|
+
* @param exitCode process exit code; only consulted to gate detection (zero exit means the
|
|
64
|
+
* agent reported success, and we do not classify a successful run as fatal).
|
|
65
|
+
* @returns a FatalAgentError when a known pattern matches, otherwise undefined.
|
|
66
|
+
*/
|
|
67
|
+
function classifyFatalAgentError(stdout, stderr, exitCode) {
|
|
68
|
+
if (exitCode === 0)
|
|
69
|
+
return undefined;
|
|
70
|
+
const haystack = `${stderr ?? ''}\n${stdout ?? ''}`;
|
|
71
|
+
if (!haystack.trim())
|
|
72
|
+
return undefined;
|
|
73
|
+
for (const { kind, pattern } of PATTERNS) {
|
|
74
|
+
const match = haystack.match(pattern);
|
|
75
|
+
if (!match)
|
|
76
|
+
continue;
|
|
77
|
+
const lineStart = haystack.lastIndexOf('\n', match.index ?? 0) + 1;
|
|
78
|
+
const lineEndRaw = haystack.indexOf('\n', (match.index ?? 0) + match[0].length);
|
|
79
|
+
const lineEnd = lineEndRaw === -1 ? haystack.length : lineEndRaw;
|
|
80
|
+
const evidence = haystack.slice(lineStart, lineEnd).trim().slice(0, 240);
|
|
81
|
+
return {
|
|
82
|
+
kind,
|
|
83
|
+
message: evidence || match[0],
|
|
84
|
+
evidence,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|