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,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Resolves adapter name to concrete AgentAdapter instance.
|
|
3
|
+
// Central registry; add new adapters here.
|
|
4
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
5
|
+
exports.buildRestrictedEnv = void 0;
|
|
6
|
+
exports.defaultModelForAdapter = defaultModelForAdapter;
|
|
7
|
+
exports.resolveAdapter = resolveAdapter;
|
|
8
|
+
const copilot_adapter_1 = require("./copilot-adapter");
|
|
9
|
+
const claude_code_adapter_1 = require("./claude-code-adapter");
|
|
10
|
+
const codex_adapter_1 = require("./codex-adapter");
|
|
11
|
+
const claude_code_teams_1 = require("./claude-code-teams");
|
|
12
|
+
var agent_adapter_1 = require("./agent-adapter");
|
|
13
|
+
Object.defineProperty(exports, "buildRestrictedEnv", { enumerable: true, get: function () { return agent_adapter_1.buildRestrictedEnv; } });
|
|
14
|
+
const ADAPTER_REGISTRY = {
|
|
15
|
+
'copilot': () => new copilot_adapter_1.CopilotAdapter(),
|
|
16
|
+
'claude-code': () => new claude_code_adapter_1.ClaudeCodeAdapter(),
|
|
17
|
+
'codex': () => new codex_adapter_1.CodexAdapter(),
|
|
18
|
+
'claude-code-teams': () => new claude_code_teams_1.ClaudeCodeTeamsAdapter(),
|
|
19
|
+
};
|
|
20
|
+
// Each adapter's underlying tool has a different default model.
|
|
21
|
+
// Used for cost estimation and reporting when the user hasn't specified --model.
|
|
22
|
+
const DEFAULT_MODELS = {
|
|
23
|
+
'copilot': 'claude-sonnet-4',
|
|
24
|
+
'claude-code': 'claude-sonnet-4',
|
|
25
|
+
'codex': 'gpt-5.4',
|
|
26
|
+
'claude-code-teams': 'claude-opus-4',
|
|
27
|
+
};
|
|
28
|
+
const AVAILABLE_NAMES = Object.keys(ADAPTER_REGISTRY);
|
|
29
|
+
/**
|
|
30
|
+
* Returns the default model name for a given adapter.
|
|
31
|
+
* Falls back to the adapter's known default rather than assuming claude-sonnet-4.
|
|
32
|
+
*/
|
|
33
|
+
function defaultModelForAdapter(adapterName) {
|
|
34
|
+
if (!adapterName)
|
|
35
|
+
return DEFAULT_MODELS['copilot'];
|
|
36
|
+
return DEFAULT_MODELS[adapterName] ?? DEFAULT_MODELS['copilot'];
|
|
37
|
+
}
|
|
38
|
+
function resolveAdapter(name) {
|
|
39
|
+
const factory = ADAPTER_REGISTRY[name];
|
|
40
|
+
if (!factory) {
|
|
41
|
+
throw new Error(`Unknown agent adapter "${name}". Available adapters: ${AVAILABLE_NAMES.join(', ')}`);
|
|
42
|
+
}
|
|
43
|
+
return factory();
|
|
44
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PersistentInteractiveSession = void 0;
|
|
4
|
+
const child_process_1 = require("child_process");
|
|
5
|
+
const DEFAULT_END_TOKEN_PREFIX = 'SWARM_TURN_DONE';
|
|
6
|
+
const DEFAULT_TURN_TIMEOUT_MS = 600_000;
|
|
7
|
+
/**
|
|
8
|
+
* Keeps one CLI process alive and feeds prompts over stdin.
|
|
9
|
+
*
|
|
10
|
+
* The child must echo the requested end token on stdout when a turn is done.
|
|
11
|
+
* If it exits, stalls, or never emits the marker, callers can fall back to the
|
|
12
|
+
* adapter's regular cold-start path.
|
|
13
|
+
*/
|
|
14
|
+
class PersistentInteractiveSession {
|
|
15
|
+
config;
|
|
16
|
+
proc;
|
|
17
|
+
pending;
|
|
18
|
+
stdoutBuffer = '';
|
|
19
|
+
stderrBuffer = '';
|
|
20
|
+
unavailableReason;
|
|
21
|
+
turnCounter = 0;
|
|
22
|
+
endTokenPrefix;
|
|
23
|
+
constructor(config) {
|
|
24
|
+
this.config = config;
|
|
25
|
+
this.endTokenPrefix = config.endTokenPrefix ?? DEFAULT_END_TOKEN_PREFIX;
|
|
26
|
+
}
|
|
27
|
+
get unavailable() {
|
|
28
|
+
return this.unavailableReason !== undefined;
|
|
29
|
+
}
|
|
30
|
+
get reason() {
|
|
31
|
+
return this.unavailableReason;
|
|
32
|
+
}
|
|
33
|
+
async send(prompt, timeoutMs = DEFAULT_TURN_TIMEOUT_MS) {
|
|
34
|
+
if (this.unavailableReason) {
|
|
35
|
+
return this.failedResult(this.unavailableReason, 0);
|
|
36
|
+
}
|
|
37
|
+
if (this.pending) {
|
|
38
|
+
return this.failedResult('persistent session already has an active turn', 0);
|
|
39
|
+
}
|
|
40
|
+
const startedAt = Date.now();
|
|
41
|
+
const proc = this.ensureStarted();
|
|
42
|
+
if (!proc || this.unavailableReason) {
|
|
43
|
+
return this.failedResult(this.unavailableReason ?? 'persistent session failed to start', Date.now() - startedAt);
|
|
44
|
+
}
|
|
45
|
+
const token = `${this.endTokenPrefix}:${Date.now()}:${++this.turnCounter}`;
|
|
46
|
+
const framedPrompt = [
|
|
47
|
+
prompt,
|
|
48
|
+
'',
|
|
49
|
+
'When this turn is fully complete, print this exact marker on its own line:',
|
|
50
|
+
token,
|
|
51
|
+
'',
|
|
52
|
+
].join('\n');
|
|
53
|
+
return new Promise((resolve) => {
|
|
54
|
+
const timeout = setTimeout(() => {
|
|
55
|
+
const pending = this.pending;
|
|
56
|
+
if (!pending || pending.token !== token)
|
|
57
|
+
return;
|
|
58
|
+
this.pending = undefined;
|
|
59
|
+
this.markUnavailable(`persistent turn timed out waiting for ${token}`);
|
|
60
|
+
resolve({
|
|
61
|
+
stdout: pending.stdout,
|
|
62
|
+
stderr: appendLine(pending.stderr, `Timed out waiting for end-of-turn marker ${token}`),
|
|
63
|
+
exitCode: 1,
|
|
64
|
+
durationMs: Date.now() - pending.startedAt,
|
|
65
|
+
});
|
|
66
|
+
}, timeoutMs);
|
|
67
|
+
this.pending = {
|
|
68
|
+
token,
|
|
69
|
+
startedAt,
|
|
70
|
+
stdout: '',
|
|
71
|
+
stderr: '',
|
|
72
|
+
resolve,
|
|
73
|
+
timeout,
|
|
74
|
+
};
|
|
75
|
+
try {
|
|
76
|
+
if (!proc.stdin)
|
|
77
|
+
throw new Error('persistent process stdin is unavailable');
|
|
78
|
+
proc.stdin.write(framedPrompt);
|
|
79
|
+
proc.stdin.write('\n');
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
clearTimeout(timeout);
|
|
83
|
+
this.pending = undefined;
|
|
84
|
+
this.markUnavailable(err instanceof Error ? err.message : String(err));
|
|
85
|
+
resolve(this.failedResult(this.unavailableReason ?? 'stdin write failed', Date.now() - startedAt));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
async shutdown() {
|
|
90
|
+
if (!this.proc)
|
|
91
|
+
return;
|
|
92
|
+
const proc = this.proc;
|
|
93
|
+
this.proc = undefined;
|
|
94
|
+
try {
|
|
95
|
+
proc.stdin?.end();
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Process may already be closed.
|
|
99
|
+
}
|
|
100
|
+
try {
|
|
101
|
+
proc.kill('SIGTERM');
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Process may already be closed.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
ensureStarted() {
|
|
108
|
+
if (this.proc && !this.proc.killed)
|
|
109
|
+
return this.proc;
|
|
110
|
+
const spawnOpts = {
|
|
111
|
+
cwd: this.config.cwd,
|
|
112
|
+
env: {
|
|
113
|
+
...process.env,
|
|
114
|
+
...this.config.env,
|
|
115
|
+
},
|
|
116
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
117
|
+
};
|
|
118
|
+
let proc;
|
|
119
|
+
try {
|
|
120
|
+
proc = (0, child_process_1.spawn)(this.config.command, this.config.args, spawnOpts);
|
|
121
|
+
this.proc = proc;
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
this.markUnavailable(err instanceof Error ? err.message : String(err));
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
proc.stdout?.on('data', (data) => {
|
|
128
|
+
this.handleOutput(data.toString(), 'stdout');
|
|
129
|
+
});
|
|
130
|
+
proc.stderr?.on('data', (data) => {
|
|
131
|
+
this.handleOutput(data.toString(), 'stderr');
|
|
132
|
+
});
|
|
133
|
+
proc.on('close', (code) => {
|
|
134
|
+
this.markUnavailable(`persistent process exited with code ${code ?? 1}`);
|
|
135
|
+
});
|
|
136
|
+
proc.on('error', (err) => {
|
|
137
|
+
this.markUnavailable(err.message);
|
|
138
|
+
});
|
|
139
|
+
return proc;
|
|
140
|
+
}
|
|
141
|
+
handleOutput(text, stream) {
|
|
142
|
+
const pending = this.pending;
|
|
143
|
+
if (pending) {
|
|
144
|
+
if (stream === 'stdout')
|
|
145
|
+
pending.stdout += text;
|
|
146
|
+
else
|
|
147
|
+
pending.stderr += text;
|
|
148
|
+
}
|
|
149
|
+
const buffer = stream === 'stdout'
|
|
150
|
+
? this.stdoutBuffer + text
|
|
151
|
+
: this.stderrBuffer + text;
|
|
152
|
+
const lines = buffer.split('\n');
|
|
153
|
+
const remaining = lines.pop() ?? '';
|
|
154
|
+
if (stream === 'stdout')
|
|
155
|
+
this.stdoutBuffer = remaining;
|
|
156
|
+
else
|
|
157
|
+
this.stderrBuffer = remaining;
|
|
158
|
+
for (const line of lines) {
|
|
159
|
+
this.config.onLine?.(line, stream);
|
|
160
|
+
this.maybeFinishTurn(line);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
maybeFinishTurn(line) {
|
|
164
|
+
const pending = this.pending;
|
|
165
|
+
if (!pending || !line.includes(pending.token))
|
|
166
|
+
return;
|
|
167
|
+
clearTimeout(pending.timeout);
|
|
168
|
+
this.pending = undefined;
|
|
169
|
+
const stdout = pending.stdout.replace(pending.token, '').trimEnd();
|
|
170
|
+
pending.resolve({
|
|
171
|
+
stdout,
|
|
172
|
+
stderr: pending.stderr,
|
|
173
|
+
exitCode: 0,
|
|
174
|
+
durationMs: Date.now() - pending.startedAt,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
markUnavailable(reason) {
|
|
178
|
+
this.unavailableReason = reason;
|
|
179
|
+
if (this.pending) {
|
|
180
|
+
const pending = this.pending;
|
|
181
|
+
clearTimeout(pending.timeout);
|
|
182
|
+
this.pending = undefined;
|
|
183
|
+
pending.resolve({
|
|
184
|
+
stdout: pending.stdout,
|
|
185
|
+
stderr: appendLine(pending.stderr, reason),
|
|
186
|
+
exitCode: 1,
|
|
187
|
+
durationMs: Date.now() - pending.startedAt,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
this.proc?.kill('SIGTERM');
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// Already gone.
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
failedResult(reason, durationMs) {
|
|
198
|
+
return { stdout: '', stderr: reason, exitCode: 1, durationMs };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
exports.PersistentInteractiveSession = PersistentInteractiveSession;
|
|
202
|
+
function appendLine(base, line) {
|
|
203
|
+
return base ? `${base}\n${line}` : line;
|
|
204
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Shared subprocess lifecycle management: stall detection, heartbeat,
|
|
3
|
+
// graceful shutdown. Extracted from SessionExecutor.runCommand() so that
|
|
4
|
+
// all adapters (copilot, claude-code, claude-code-teams) get identical
|
|
5
|
+
// reliability guarantees.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.supervisedSpawn = supervisedSpawn;
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const defaults_1 = require("../defaults");
|
|
10
|
+
const logger_1 = require("../logger");
|
|
11
|
+
const logger = (0, logger_1.getLogger)('process-supervisor');
|
|
12
|
+
// Maximum silence before killing a stalled subprocess.
|
|
13
|
+
// Agent CLIs can go quiet during extended tool-use or thinking phases,
|
|
14
|
+
// so this needs headroom beyond typical inference latency.
|
|
15
|
+
const DEFAULT_STALL_TIMEOUT_MS = defaults_1.DEFAULT_COMMAND_TIMEOUT_MS * 2 + 60_000; // 5 minutes
|
|
16
|
+
// How often to check for stalls (ms)
|
|
17
|
+
const STALL_CHECK_INTERVAL_MS = defaults_1.DEFAULT_HEARTBEAT_INTERVAL_MS * 2;
|
|
18
|
+
// Grace period after SIGTERM before escalating to SIGKILL (ms)
|
|
19
|
+
const KILL_GRACE_MS = defaults_1.DEFAULT_SIGKILL_DELAY_MS;
|
|
20
|
+
// Only show heartbeat during quiet periods longer than this (ms)
|
|
21
|
+
const HEARTBEAT_QUIET_THRESHOLD_MS = defaults_1.DEFAULT_HEARTBEAT_INTERVAL_MS * 3;
|
|
22
|
+
// Heartbeat display interval (ms)
|
|
23
|
+
const HEARTBEAT_INTERVAL_MS = defaults_1.DEFAULT_HEARTBEAT_INTERVAL_MS * 3;
|
|
24
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
25
|
+
/**
|
|
26
|
+
* Spawn a child process with stall detection, heartbeat logging, and
|
|
27
|
+
* line-buffered output. Returns a promise that resolves when the process
|
|
28
|
+
* exits or is killed due to inactivity.
|
|
29
|
+
*/
|
|
30
|
+
function supervisedSpawn(opts) {
|
|
31
|
+
const stallTimeoutMs = opts.stallTimeoutMs ?? DEFAULT_STALL_TIMEOUT_MS;
|
|
32
|
+
return new Promise((resolve) => {
|
|
33
|
+
const stdinMode = opts.stdinMode ?? 'pipe';
|
|
34
|
+
// process.env is merged in first so callers that pass partial env overrides
|
|
35
|
+
// (e.g. buildRestrictedEnv) still inherit PATH, locale, keyring vars, and
|
|
36
|
+
// OAuth state the agent CLI needs at runtime. opts.env is spread last so
|
|
37
|
+
// explicit values, including `undefined` to scrub a key, win over the
|
|
38
|
+
// inherited base.
|
|
39
|
+
const spawnOpts = {
|
|
40
|
+
cwd: opts.cwd,
|
|
41
|
+
env: {
|
|
42
|
+
...process.env,
|
|
43
|
+
...opts.env,
|
|
44
|
+
},
|
|
45
|
+
stdio: [stdinMode, 'pipe', 'pipe'],
|
|
46
|
+
};
|
|
47
|
+
const proc = (0, child_process_1.spawn)(opts.command, opts.args, spawnOpts);
|
|
48
|
+
// Close piped stdin so the subprocess never blocks waiting for interactive input.
|
|
49
|
+
// When stdinMode is 'ignore' there is no proc.stdin to end.
|
|
50
|
+
if (proc.stdin) {
|
|
51
|
+
if (opts.stdinData) {
|
|
52
|
+
proc.stdin.write(opts.stdinData);
|
|
53
|
+
}
|
|
54
|
+
proc.stdin.end();
|
|
55
|
+
}
|
|
56
|
+
let stdout = '';
|
|
57
|
+
let stderr = '';
|
|
58
|
+
let resolved = false;
|
|
59
|
+
let killed = false;
|
|
60
|
+
// Line buffers prevent mid-word breaks in streamed output
|
|
61
|
+
let stdoutBuffer = '';
|
|
62
|
+
let stderrBuffer = '';
|
|
63
|
+
let lineCount = 0;
|
|
64
|
+
let lastOutputTime = Date.now();
|
|
65
|
+
const startTime = Date.now();
|
|
66
|
+
let heartbeatInterval = null;
|
|
67
|
+
let stallCheckInterval = null;
|
|
68
|
+
let heartbeatCount = 0;
|
|
69
|
+
const cleanup = () => {
|
|
70
|
+
if (heartbeatInterval)
|
|
71
|
+
clearInterval(heartbeatInterval);
|
|
72
|
+
if (stallCheckInterval)
|
|
73
|
+
clearInterval(stallCheckInterval);
|
|
74
|
+
};
|
|
75
|
+
const finish = (result) => {
|
|
76
|
+
if (resolved)
|
|
77
|
+
return;
|
|
78
|
+
resolved = true;
|
|
79
|
+
cleanup();
|
|
80
|
+
resolve(result);
|
|
81
|
+
};
|
|
82
|
+
// Stall detection: kill subprocess if no output for stallTimeoutMs
|
|
83
|
+
stallCheckInterval = setInterval(() => {
|
|
84
|
+
const silentMs = Date.now() - lastOutputTime;
|
|
85
|
+
if (silentMs >= stallTimeoutMs) {
|
|
86
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
87
|
+
const stallSec = Math.round(silentMs / 1000);
|
|
88
|
+
if (opts.logPrefix) {
|
|
89
|
+
logger.info(`${opts.logPrefix} ⚠ STALL DETECTED: no output for ${stallSec}s ` +
|
|
90
|
+
`(total ${elapsed}s, ${lineCount} lines). Killing process.`);
|
|
91
|
+
}
|
|
92
|
+
killed = true;
|
|
93
|
+
killGracefully(proc);
|
|
94
|
+
finish({
|
|
95
|
+
stdout,
|
|
96
|
+
stderr: stderr + `\nProcess killed after ${stallSec}s of no output (stall timeout)`,
|
|
97
|
+
exitCode: 1
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}, STALL_CHECK_INTERVAL_MS);
|
|
101
|
+
// Heartbeat: show progress during quiet periods
|
|
102
|
+
if (opts.logPrefix) {
|
|
103
|
+
heartbeatInterval = setInterval(() => {
|
|
104
|
+
const silentMs = Date.now() - lastOutputTime;
|
|
105
|
+
if (silentMs < HEARTBEAT_QUIET_THRESHOLD_MS)
|
|
106
|
+
return;
|
|
107
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
108
|
+
const frame = SPINNER_FRAMES[heartbeatCount % SPINNER_FRAMES.length];
|
|
109
|
+
heartbeatCount++;
|
|
110
|
+
const mins = Math.floor(elapsed / 60);
|
|
111
|
+
const secs = elapsed % 60;
|
|
112
|
+
const timeStr = mins > 0 ? `${mins}m${secs}s` : `${secs}s`;
|
|
113
|
+
logger.info(`${opts.logPrefix} ${frame} ${timeStr} elapsed | ${lineCount} lines`);
|
|
114
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
115
|
+
}
|
|
116
|
+
const processLines = (buffer, newData, stream) => {
|
|
117
|
+
buffer += newData;
|
|
118
|
+
const lines = buffer.split('\n');
|
|
119
|
+
// Keep last (possibly incomplete) line in buffer
|
|
120
|
+
const remaining = lines.pop() || '';
|
|
121
|
+
for (const line of lines) {
|
|
122
|
+
if (line.trim()) {
|
|
123
|
+
lineCount++;
|
|
124
|
+
if (opts.onLine)
|
|
125
|
+
opts.onLine(line, stream);
|
|
126
|
+
if (opts.logPrefix && !killed) {
|
|
127
|
+
if (stream === 'stderr') {
|
|
128
|
+
logger.error(`${opts.logPrefix} ${line}`);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
logger.info(`${opts.logPrefix} ${line}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return remaining;
|
|
137
|
+
};
|
|
138
|
+
if (proc.stdout) {
|
|
139
|
+
proc.stdout.on('data', (data) => {
|
|
140
|
+
const text = data.toString();
|
|
141
|
+
stdout += text;
|
|
142
|
+
lastOutputTime = Date.now();
|
|
143
|
+
stdoutBuffer = processLines(stdoutBuffer, text, 'stdout');
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
if (proc.stderr) {
|
|
147
|
+
proc.stderr.on('data', (data) => {
|
|
148
|
+
const text = data.toString();
|
|
149
|
+
stderr += text;
|
|
150
|
+
lastOutputTime = Date.now();
|
|
151
|
+
stderrBuffer = processLines(stderrBuffer, text, 'stderr');
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
proc.on('close', (code) => {
|
|
155
|
+
// Flush remaining buffered content
|
|
156
|
+
if (opts.logPrefix && !killed) {
|
|
157
|
+
if (stdoutBuffer.trim()) {
|
|
158
|
+
lineCount++;
|
|
159
|
+
logger.info(`${opts.logPrefix} ${stdoutBuffer}`);
|
|
160
|
+
}
|
|
161
|
+
if (stderrBuffer.trim()) {
|
|
162
|
+
lineCount++;
|
|
163
|
+
logger.error(`${opts.logPrefix} ${stderrBuffer}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
finish({ stdout, stderr, exitCode: code ?? 0 });
|
|
167
|
+
});
|
|
168
|
+
proc.on('error', (err) => {
|
|
169
|
+
// Flush buffers on error too
|
|
170
|
+
if (opts.logPrefix && !killed) {
|
|
171
|
+
if (stdoutBuffer.trim()) {
|
|
172
|
+
logger.info(`${opts.logPrefix} ${stdoutBuffer}`);
|
|
173
|
+
}
|
|
174
|
+
if (stderrBuffer.trim()) {
|
|
175
|
+
logger.error(`${opts.logPrefix} ${stderrBuffer}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
finish({
|
|
179
|
+
stdout,
|
|
180
|
+
stderr: stderr + '\n' + err.message,
|
|
181
|
+
exitCode: 1
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// SIGTERM first, then SIGKILL after a grace period if the process
|
|
187
|
+
// hasn't exited. Matches the shutdown behavior from SessionExecutor.
|
|
188
|
+
function killGracefully(proc) {
|
|
189
|
+
try {
|
|
190
|
+
proc.kill('SIGTERM');
|
|
191
|
+
}
|
|
192
|
+
catch { /* already dead */ }
|
|
193
|
+
setTimeout(() => {
|
|
194
|
+
try {
|
|
195
|
+
proc.kill('SIGKILL');
|
|
196
|
+
}
|
|
197
|
+
catch { /* already dead */ }
|
|
198
|
+
}, KILL_GRACE_MS);
|
|
199
|
+
}
|