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.
Files changed (174) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +199 -0
  3. package/agents/reviewer.agent.md +50 -0
  4. package/agents/worker.agent.md +41 -0
  5. package/config/default-agents.yaml +56 -0
  6. package/config/pm-agent.yaml +36 -0
  7. package/config/quality-gates.yaml +88 -0
  8. package/config/repair-agent.yaml +34 -0
  9. package/config/semgrep-rules/complexity-mismatch.yaml +6 -0
  10. package/config/semgrep-rules/exception-swallowing.yaml +13 -0
  11. package/config/semgrep-rules/hardcoded-answer.yaml +6 -0
  12. package/config/semgrep-rules/mock-mutation.yaml +6 -0
  13. package/config/semgrep-rules/test-modification.yaml +6 -0
  14. package/config/user-agents.yaml +23 -0
  15. package/dist/src/adapters/adapter-factory.js +29 -0
  16. package/dist/src/adapters/agent-adapter.js +32 -0
  17. package/dist/src/adapters/claude-code-adapter.js +126 -0
  18. package/dist/src/adapters/claude-code-teams.js +119 -0
  19. package/dist/src/adapters/codex-adapter.js +118 -0
  20. package/dist/src/adapters/copilot-adapter.js +224 -0
  21. package/dist/src/adapters/fatal-error-classifier.js +88 -0
  22. package/dist/src/adapters/index.js +44 -0
  23. package/dist/src/adapters/persistent-session.js +204 -0
  24. package/dist/src/adapters/process-supervisor.js +199 -0
  25. package/dist/src/agents-exporter.js +398 -0
  26. package/dist/src/analytics-log.js +161 -0
  27. package/dist/src/baseline-scanner.js +65 -0
  28. package/dist/src/bootstrap-evidence.js +88 -0
  29. package/dist/src/bootstrap-orchestrator.js +205 -0
  30. package/dist/src/bootstrap-types.js +5 -0
  31. package/dist/src/branch-merger.js +427 -0
  32. package/dist/src/cli/attest-handlers.js +44 -0
  33. package/dist/src/cli/cost-prompt.js +51 -0
  34. package/dist/src/cli/demo-handlers.js +274 -0
  35. package/dist/src/cli/flags.js +152 -0
  36. package/dist/src/cli/index.js +57 -0
  37. package/dist/src/cli/live-status.js +186 -0
  38. package/dist/src/cli/misc-handlers.js +242 -0
  39. package/dist/src/cli/plan-handlers.js +350 -0
  40. package/dist/src/cli/share-handlers.js +163 -0
  41. package/dist/src/cli/status-handlers.js +457 -0
  42. package/dist/src/cli/swarm-handlers.js +638 -0
  43. package/dist/src/cli/usage.js +72 -0
  44. package/dist/src/cli.js +217 -0
  45. package/dist/src/commit-pattern-detector.js +143 -0
  46. package/dist/src/commit-quality-analyzer.js +38 -0
  47. package/dist/src/config-loader.js +339 -0
  48. package/dist/src/context-broker.js +358 -0
  49. package/dist/src/copilot-cli-wrapper.js +258 -0
  50. package/dist/src/cost-estimator.js +241 -0
  51. package/dist/src/defaults.js +18 -0
  52. package/dist/src/demo-mode.js +180 -0
  53. package/dist/src/deployment-handler.js +78 -0
  54. package/dist/src/deployment-manager.js +228 -0
  55. package/dist/src/execution-queue.js +196 -0
  56. package/dist/src/external-tool-manager.js +184 -0
  57. package/dist/src/gate-prompt-builder.js +117 -0
  58. package/dist/src/gate-remediation.js +173 -0
  59. package/dist/src/github-issues-ingester.js +152 -0
  60. package/dist/src/github-mcp-integrator.js +164 -0
  61. package/dist/src/hook-generator.js +229 -0
  62. package/dist/src/index.js +48 -0
  63. package/dist/src/knowledge-base.js +300 -0
  64. package/dist/src/logger.js +111 -0
  65. package/dist/src/meta-analyzer.js +208 -0
  66. package/dist/src/metrics-collector.js +209 -0
  67. package/dist/src/metrics-types.js +5 -0
  68. package/dist/src/multi-repo-coordinator.js +179 -0
  69. package/dist/src/orchestrator/async-meta-analysis.js +84 -0
  70. package/dist/src/orchestrator/fatal-run-error.js +43 -0
  71. package/dist/src/orchestrator/final-gates-remediation.js +258 -0
  72. package/dist/src/orchestrator/git-state-utils.js +119 -0
  73. package/dist/src/orchestrator/pause-controller.js +55 -0
  74. package/dist/src/orchestrator/replan-runner.js +258 -0
  75. package/dist/src/orchestrator/step-executor.js +391 -0
  76. package/dist/src/orchestrator/wave-scheduler-loop.js +204 -0
  77. package/dist/src/owasp-mapper.js +153 -0
  78. package/dist/src/owasp-report-renderer.js +39 -0
  79. package/dist/src/plan-files.js +142 -0
  80. package/dist/src/plan-generator.js +1207 -0
  81. package/dist/src/pm-agent.js +296 -0
  82. package/dist/src/post-run-reporter.js +203 -0
  83. package/dist/src/pr-automation.js +103 -0
  84. package/dist/src/pr-manager.js +318 -0
  85. package/dist/src/presenter/index.js +209 -0
  86. package/dist/src/prompt-builder.js +173 -0
  87. package/dist/src/quality-gates/config-loader.js +196 -0
  88. package/dist/src/quality-gates/default-config.js +243 -0
  89. package/dist/src/quality-gates/file-utils.js +152 -0
  90. package/dist/src/quality-gates/gate-runner.js +171 -0
  91. package/dist/src/quality-gates/gates/accessibility.js +355 -0
  92. package/dist/src/quality-gates/gates/duplicate-blocks.js +144 -0
  93. package/dist/src/quality-gates/gates/hardcoded-config.js +52 -0
  94. package/dist/src/quality-gates/gates/readme-claims.js +62 -0
  95. package/dist/src/quality-gates/gates/runtime-checks.js +265 -0
  96. package/dist/src/quality-gates/gates/scaffold-defaults.js +108 -0
  97. package/dist/src/quality-gates/gates/test-coverage.js +240 -0
  98. package/dist/src/quality-gates/gates/test-file-protection.js +85 -0
  99. package/dist/src/quality-gates/gates/test-isolation.js +74 -0
  100. package/dist/src/quality-gates/index.js +17 -0
  101. package/dist/src/quality-gates/registry.js +205 -0
  102. package/dist/src/quality-gates/types.js +2 -0
  103. package/dist/src/quick-fix-mode.js +302 -0
  104. package/dist/src/recipe-loader.js +122 -0
  105. package/dist/src/repair-agent.js +396 -0
  106. package/dist/src/repo-analyzer.js +342 -0
  107. package/dist/src/report-generator.js +146 -0
  108. package/dist/src/report-renderer.js +90 -0
  109. package/dist/src/requirement-filter.js +52 -0
  110. package/dist/src/sarif-formatter.js +140 -0
  111. package/dist/src/scheduling/dependency-analyzer.js +178 -0
  112. package/dist/src/scheduling/work-stealing-queue.js +123 -0
  113. package/dist/src/secret-redactor.js +90 -0
  114. package/dist/src/session-executor.js +587 -0
  115. package/dist/src/session-manager.js +221 -0
  116. package/dist/src/share/transcript-verification.js +128 -0
  117. package/dist/src/share-parser.js +477 -0
  118. package/dist/src/spinner.js +146 -0
  119. package/dist/src/step-runner.js +365 -0
  120. package/dist/src/swarm-orchestrator.js +686 -0
  121. package/dist/src/task-classifier.js +134 -0
  122. package/dist/src/test-command-discovery.js +123 -0
  123. package/dist/src/text-similarity.js +24 -0
  124. package/dist/src/tier-maps.js +158 -0
  125. package/dist/src/types.js +5 -0
  126. package/dist/src/url-shortener.js +135 -0
  127. package/dist/src/verification/attestation.js +166 -0
  128. package/dist/src/verification/cheat-detector.js +240 -0
  129. package/dist/src/verification/command-runner.js +70 -0
  130. package/dist/src/verification/composite-score.js +157 -0
  131. package/dist/src/verification/cosign-attestation.js +137 -0
  132. package/dist/src/verification/diff-analysis.js +89 -0
  133. package/dist/src/verification/differential-gate.js +173 -0
  134. package/dist/src/verification/index.js +41 -0
  135. package/dist/src/verification/mutation-gate.js +242 -0
  136. package/dist/src/verification/property-gate.js +201 -0
  137. package/dist/src/verification/test-synthesizer.js +243 -0
  138. package/dist/src/verifier/outcome-checks.js +396 -0
  139. package/dist/src/verifier/transcript-checks.js +207 -0
  140. package/dist/src/verifier/verification-reporters.js +148 -0
  141. package/dist/src/verifier-engine.js +491 -0
  142. package/dist/src/wave-resizer.js +131 -0
  143. package/dist/src/wave-scheduler.js +44 -0
  144. package/dist/src/worktree-manager.js +368 -0
  145. package/dist/src/worktree-reserved-paths.js +97 -0
  146. package/package.json +80 -0
  147. package/plugin/agents/.gitkeep +0 -0
  148. package/plugin/agents/backend_master.agent.md +72 -0
  149. package/plugin/agents/devops_pro.agent.md +45 -0
  150. package/plugin/agents/frontend_expert.agent.md +59 -0
  151. package/plugin/agents/integrator_finalizer.agent.md +44 -0
  152. package/plugin/agents/meta_reviewer.agent.md +44 -0
  153. package/plugin/agents/security_auditor.agent.md +43 -0
  154. package/plugin/agents/tester_elite.agent.md +43 -0
  155. package/plugin/hooks/hooks.json +19 -0
  156. package/plugin/hooks/post-tool-use.json +9 -0
  157. package/plugin/hooks/pre-tool-use.json +9 -0
  158. package/plugin/plugin.json +47 -0
  159. package/plugin/skills/swarm-gates/SKILL.md +53 -0
  160. package/plugin/skills/swarm-orchestrate/SKILL.md +34 -0
  161. package/plugin/skills/swarm-verify/SKILL.md +44 -0
  162. package/templates/README.md +35 -0
  163. package/templates/cli-tool.json +31 -0
  164. package/templates/fullstack.json +52 -0
  165. package/templates/library.json +38 -0
  166. package/templates/react-app.json +45 -0
  167. package/templates/recipes/add-api-docs.json +35 -0
  168. package/templates/recipes/add-auth.json +47 -0
  169. package/templates/recipes/add-ci.json +39 -0
  170. package/templates/recipes/add-tests.json +39 -0
  171. package/templates/recipes/migrate-to-ts.json +42 -0
  172. package/templates/recipes/refactor-modularize.json +44 -0
  173. package/templates/recipes/security-audit.json +35 -0
  174. 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
+ }