@magnusekdahl/parallix 1.2.0 → 1.2.1

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 (51) hide show
  1. package/lib/agents/agents.js +737 -838
  2. package/lib/agents/agents.ts +930 -0
  3. package/lib/agents/claude-telemetry.js +149 -154
  4. package/lib/agents/claude-telemetry.ts +233 -0
  5. package/lib/agents/claude.js +121 -131
  6. package/lib/agents/claude.ts +156 -0
  7. package/lib/agents/codex-telemetry.js +176 -164
  8. package/lib/agents/codex-telemetry.ts +205 -0
  9. package/lib/agents/codex.js +168 -201
  10. package/lib/agents/codex.ts +236 -0
  11. package/lib/agents/limit-hit.js +216 -226
  12. package/lib/agents/limit-hit.ts +262 -0
  13. package/lib/agents/mistral-telemetry.js +7 -10
  14. package/lib/agents/mistral-telemetry.ts +44 -0
  15. package/lib/agents/mistral.js +40 -58
  16. package/lib/agents/mistral.ts +84 -0
  17. package/lib/agents/opencode-export.js +126 -127
  18. package/lib/agents/opencode-export.ts +160 -0
  19. package/lib/agents/opencode-telemetry.js +282 -261
  20. package/lib/agents/opencode-telemetry.ts +350 -0
  21. package/lib/agents/opencode.js +263 -305
  22. package/lib/agents/opencode.ts +370 -0
  23. package/lib/agents/stage-telemetry.js +23 -24
  24. package/lib/agents/stage-telemetry.ts +47 -0
  25. package/lib/commands/integrate.js +9 -3
  26. package/lib/core/fmt.js +140 -211
  27. package/lib/core/fmt.ts +189 -0
  28. package/lib/core/git.js +121 -102
  29. package/lib/core/git.ts +146 -0
  30. package/lib/core/gitignore.js +59 -106
  31. package/lib/core/gitignore.ts +103 -0
  32. package/lib/core/mission-utils.js +833 -875
  33. package/lib/core/mission-utils.ts +998 -0
  34. package/lib/core/persistent-data-migration.js +247 -205
  35. package/lib/core/persistent-data-migration.ts +255 -0
  36. package/lib/core/product-config.js +400 -476
  37. package/lib/core/product-config.ts +518 -0
  38. package/lib/core/runtime-matrix.js +41 -87
  39. package/lib/core/runtime-matrix.ts +84 -0
  40. package/lib/core/spawn-tee.js +152 -192
  41. package/lib/core/spawn-tee.ts +202 -0
  42. package/lib/core/state-map.js +92 -92
  43. package/lib/core/state-map.ts +115 -0
  44. package/lib/core/storage.js +101 -129
  45. package/lib/core/storage.ts +182 -0
  46. package/lib/core/verification.js +148 -138
  47. package/lib/core/verification.ts +200 -0
  48. package/package.json +4 -2
  49. package/prompts/draft.md +1 -0
  50. package/prompts/portfolio.md +1 -1
  51. package/px.js +0 -2
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { eligibleAgentsForStep, workflowLauncherStatus } from '../agents/agents.js';
4
+
5
+ const CONFIG_PATH = path.join(__dirname, '..', '..', 'config', 'agents.json');
6
+
7
+ interface LauncherStatusResult {
8
+ supported: boolean;
9
+ detail: string;
10
+ health?: string;
11
+ reason?: string;
12
+ }
13
+
14
+ interface BuildMatrixOptions {
15
+ step?: string;
16
+ eligibleAgentsForStepFn?: typeof eligibleAgentsForStep;
17
+ workflowLauncherStatusFn?: typeof workflowLauncherStatus;
18
+ configPath?: string;
19
+ existsSyncFn?: typeof fs.existsSync;
20
+ }
21
+
22
+ export function launcherStatus(agent: string, options: { workflowLauncherStatusFn?: typeof workflowLauncherStatus } = {}): LauncherStatusResult {
23
+ const { workflowLauncherStatusFn = workflowLauncherStatus } = options;
24
+ return workflowLauncherStatusFn(agent);
25
+ }
26
+
27
+ export function buildAutonomousReviewMatrix(options: BuildMatrixOptions = {}): { step: string; agents: string[]; configPath: string; configPresent: boolean; launchers: Record<string, LauncherStatusResult> } {
28
+ const {
29
+ step = 'review',
30
+ eligibleAgentsForStepFn = eligibleAgentsForStep,
31
+ workflowLauncherStatusFn = workflowLauncherStatus,
32
+ configPath = CONFIG_PATH,
33
+ existsSyncFn = fs.existsSync
34
+ } = options;
35
+
36
+ const agents = eligibleAgentsForStepFn(step);
37
+ const launchers: Record<string, LauncherStatusResult> = Object.fromEntries(
38
+ agents.map((agent: string) => [agent, workflowLauncherStatusFn(agent)])
39
+ );
40
+
41
+ return {
42
+ step,
43
+ agents,
44
+ configPath,
45
+ configPresent: existsSyncFn(configPath),
46
+ launchers
47
+ };
48
+ }
49
+
50
+ export function formatMatrixSummary(matrix: { step: string; agents: string[]; configPath: string; configPresent: boolean; launchers: Record<string, LauncherStatusResult> }): string[] {
51
+ const lines: string[] = [];
52
+
53
+ lines.push(`Agent eligibility config: ${matrix.configPresent ? 'present' : 'missing'} (${matrix.configPath})`);
54
+ lines.push(`Launcher support matrix (step: ${matrix.step}):`);
55
+ for (const agent of matrix.agents) {
56
+ const launcher = matrix.launchers[agent];
57
+ const status = launcher.supported ? 'supported' : 'blocked';
58
+ const health = launcher.health ? `, ${launcher.health}` : '';
59
+ const reason = launcher.reason ? `; ${launcher.reason}` : '';
60
+ lines.push(` - ${agent}: ${status} (${launcher.detail}${health}${reason})`);
61
+ }
62
+ lines.push(
63
+ 'Reviewer is chosen at runtime from the eligible-and-supported pool, ' +
64
+ 'excluding the implementer (config-driven via agents.json; no hardcoded routing).'
65
+ );
66
+
67
+ return lines;
68
+ }
69
+
70
+ interface RunnableDiffFamilyOptions {
71
+ step?: string;
72
+ eligibleAgentsForStepFn?: typeof eligibleAgentsForStep;
73
+ workflowLauncherStatusFn?: typeof workflowLauncherStatus;
74
+ }
75
+
76
+ export function runnableDifferentFamilyExists(implementer: string, options: RunnableDiffFamilyOptions = {}): boolean {
77
+ const {
78
+ step = 'review',
79
+ eligibleAgentsForStepFn = eligibleAgentsForStep,
80
+ workflowLauncherStatusFn = workflowLauncherStatus
81
+ } = options;
82
+ const agents = eligibleAgentsForStepFn(step);
83
+ return agents.some((a: string) => a !== implementer && workflowLauncherStatusFn(a).supported);
84
+ }
@@ -1,200 +1,160 @@
1
- 'use strict';
2
-
3
- const childProcess = require('child_process');
4
- const path = require('path');
5
-
6
- // Bound the in-memory transcript so a noisy or long-running agent cannot
7
- // turn the harness into an O(total-output) memory hog. Detection only needs
8
- // a recent window to find limit-hit phrases (limit-hit.js clips ~200 chars
9
- // around the match), so a tail buffer is sufficient. The cap is generous
10
- // enough that a real limit-hit message and its surrounding reset-time
11
- // context land in the buffer even if the agent printed a lot before exiting.
12
- const DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
13
-
14
- /** @typedef {Buffer|string} ChunkType */
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TailBuffer = exports.DEFAULT_MAX_TAIL_BYTES = void 0;
7
+ exports.spawnAndTee = spawnAndTee;
8
+ const node_child_process_1 = __importDefault(require("node:child_process"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ exports.DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
15
11
  class TailBuffer {
16
- /** @param {number} maxBytes */
17
- constructor(maxBytes) {
18
- /** @type {number} */
19
- this.maxBytes = maxBytes;
20
- /** @type {ChunkType[]} */
21
- this.chunks = [];
22
- /** @type {number} */
23
- this.size = 0;
24
- }
25
-
26
- /** @param {ChunkType} chunk */
27
- push(chunk) {
28
- this.chunks.push(chunk);
29
- this.size += chunk.length;
30
- while (this.size > this.maxBytes && this.chunks.length > 0) {
31
- const head = this.chunks[0];
32
- const overflow = this.size - this.maxBytes;
33
- if (head.length <= overflow) {
34
- this.chunks.shift();
35
- this.size -= head.length;
36
- } else {
37
- if (Buffer.isBuffer(head)) {
38
- this.chunks[0] = head.subarray(overflow);
39
- } else {
40
- this.chunks.shift();
41
- this.size -= head.length;
12
+ maxBytes;
13
+ chunks;
14
+ size;
15
+ constructor(maxBytes) {
16
+ this.maxBytes = maxBytes;
17
+ this.chunks = [];
18
+ this.size = 0;
19
+ }
20
+ push(chunk) {
21
+ this.chunks.push(chunk);
22
+ this.size += chunk.length;
23
+ while (this.size > this.maxBytes && this.chunks.length > 0) {
24
+ const head = this.chunks[0];
25
+ const overflow = this.size - this.maxBytes;
26
+ if (head.length <= overflow) {
27
+ this.chunks.shift();
28
+ this.size -= head.length;
29
+ }
30
+ else {
31
+ if (Buffer.isBuffer(head)) {
32
+ this.chunks[0] = head.subarray(overflow);
33
+ }
34
+ else {
35
+ this.chunks.shift();
36
+ this.size -= head.length;
37
+ }
38
+ this.size -= overflow;
39
+ }
40
+ }
41
+ }
42
+ toString() {
43
+ if (this.chunks.length === 0) {
44
+ return '';
42
45
  }
43
- this.size -= overflow;
44
- }
46
+ return Buffer.concat(this.chunks).toString('utf8');
45
47
  }
46
- }
47
-
48
- toString() {
49
- if (this.chunks.length === 0) {return '';}
50
- return Buffer.concat(/** @type {Buffer[]} */(this.chunks)).toString('utf8');
51
- }
52
48
  }
53
-
54
- /**
55
- * Spawn a child process and tee its stdout/stderr to the parent's streams
56
- * while keeping a bounded tail of the output in memory. Returns a Promise
57
- * resolving with a result shaped like `spawnSync` (status, signal, stdout,
58
- * stderr, error) but with stdout/stderr as utf-8 strings populated even
59
- * when stdio:'inherit' is requested. Forces stdio to ['inherit', 'pipe',
60
- * 'pipe'] internally so the parent's stdin is preserved while child output
61
- * remains capturable.
62
- *
63
- * Pure I/O wrapper: no policy, no retry, no agent knowledge.
64
- */
65
- /**
66
- * @param {string} command
67
- * @param {string[]} args
68
- * @param {{stdoutSink?: NodeJS.WriteStream, stderrSink?: NodeJS.WriteStream, maxTailBytes?: number, noOutputWatchdog?: {onNoOutput?: Function, initialDelayMs?: number, intervalMs?: number}, cwd?: string, env?: Record<string, string>, [key: string]: any}} [options]
69
- */
49
+ exports.TailBuffer = TailBuffer;
70
50
  function spawnAndTee(command, args, options = {}) {
71
- const {
72
- stdoutSink = process.stdout,
73
- stderrSink = process.stderr,
74
- maxTailBytes = DEFAULT_MAX_TAIL_BYTES,
75
- noOutputWatchdog = null,
76
- ...spawnOptions
77
- } = options;
78
-
79
- return new Promise((resolve) => {
80
- const stdoutTail = new TailBuffer(maxTailBytes);
81
- const stderrTail = new TailBuffer(maxTailBytes);
82
- /** @type {boolean} */
83
- let settled = false;
84
- /** @type {boolean} */
85
- let sawOutput = false;
86
- /** @type {ReturnType<typeof setTimeout> | null} */
87
- let watchdogTimer = null;
88
- const startTime = Date.now();
89
- const resolvedCwd = path.resolve(spawnOptions.cwd || process.cwd());
90
- const env = {
91
- ...process.env,
92
- ...(spawnOptions.env || {}),
93
- PWD: resolvedCwd
94
- };
95
-
96
- let child;
97
- try {
98
- child = childProcess.spawn(command, args, {
99
- ...spawnOptions,
100
- env,
101
- stdio: ['inherit', 'pipe', 'pipe']
102
- });
103
- } catch (err) {
104
- resolve({
105
- status: null, signal: null, stdout: '', stderr: '', error: err,
106
- startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
107
- });
108
- return;
109
- }
110
-
111
- const clearWatchdog = () => {
112
- if (watchdogTimer) {
113
- clearTimeout(watchdogTimer);
114
- watchdogTimer = null;
115
- }
116
- };
117
-
118
- /** @param {{status: number|null, signal: string|null, stdout: string, stderr: string, error: any, startedAt: string, endedAt: string}} payload */
119
- const finish = (payload) => {
120
- if (settled) {return;}
121
- settled = true;
122
- clearWatchdog();
123
- payload.startedAt = new Date(startTime).toISOString();
124
- payload.endedAt = new Date().toISOString();
125
- resolve(payload);
126
- };
127
-
128
- /** @param {number} delayMs */
129
- const scheduleWatchdog = (delayMs) => {
130
- if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {return;}
131
- const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
132
- watchdogTimer = setTimeout(() => {
133
- watchdogTimer = null;
134
- if (settled || sawOutput) {return;}
135
- if (typeof noOutputWatchdog.onNoOutput === 'function') {
136
- noOutputWatchdog.onNoOutput({
137
- command,
138
- args,
139
- pid: child.pid,
140
- elapsedMs: Date.now() - startTime
51
+ const { stdoutSink = process.stdout, stderrSink = process.stderr, maxTailBytes = exports.DEFAULT_MAX_TAIL_BYTES, noOutputWatchdog = null, ...spawnOptions } = options;
52
+ return new Promise((resolve) => {
53
+ const stdoutTail = new TailBuffer(maxTailBytes);
54
+ const stderrTail = new TailBuffer(maxTailBytes);
55
+ let settled = false;
56
+ let sawOutput = false;
57
+ let watchdogTimer = null;
58
+ const startTime = Date.now();
59
+ const resolvedCwd = node_path_1.default.resolve(spawnOptions.cwd || process.cwd());
60
+ const env = {
61
+ ...process.env,
62
+ ...(spawnOptions.env || {}),
63
+ PWD: resolvedCwd
64
+ };
65
+ let child;
66
+ try {
67
+ child = node_child_process_1.default.spawn(command, args, {
68
+ ...spawnOptions,
69
+ env,
70
+ stdio: ['inherit', 'pipe', 'pipe']
71
+ });
72
+ }
73
+ catch (err) {
74
+ resolve({
75
+ status: null, signal: null, stdout: '', stderr: '', error: err,
76
+ startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
77
+ });
78
+ return;
79
+ }
80
+ const clearWatchdog = () => {
81
+ if (watchdogTimer) {
82
+ clearTimeout(watchdogTimer);
83
+ watchdogTimer = null;
84
+ }
85
+ };
86
+ const finish = (payload) => {
87
+ if (settled) {
88
+ return;
89
+ }
90
+ settled = true;
91
+ clearWatchdog();
92
+ payload.startedAt = new Date(startTime).toISOString();
93
+ payload.endedAt = new Date().toISOString();
94
+ resolve(payload);
95
+ };
96
+ const scheduleWatchdog = (delayMs) => {
97
+ if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {
98
+ return;
99
+ }
100
+ const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
101
+ watchdogTimer = setTimeout(() => {
102
+ watchdogTimer = null;
103
+ if (settled || sawOutput) {
104
+ return;
105
+ }
106
+ if (typeof noOutputWatchdog.onNoOutput === 'function') {
107
+ noOutputWatchdog.onNoOutput({
108
+ command,
109
+ args,
110
+ pid: child.pid,
111
+ elapsedMs: Date.now() - startTime
112
+ });
113
+ }
114
+ scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
115
+ }, delay);
116
+ if (typeof watchdogTimer.unref === 'function') {
117
+ watchdogTimer.unref();
118
+ }
119
+ };
120
+ const noteOutput = () => {
121
+ sawOutput = true;
122
+ clearWatchdog();
123
+ };
124
+ if (noOutputWatchdog) {
125
+ scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
126
+ }
127
+ child.stdout?.on('data', (chunk) => {
128
+ noteOutput();
129
+ stdoutTail.push(chunk);
130
+ if (stdoutSink && typeof stdoutSink.write === 'function') {
131
+ stdoutSink.write(chunk);
132
+ }
133
+ });
134
+ child.stderr?.on('data', (chunk) => {
135
+ noteOutput();
136
+ stderrTail.push(chunk);
137
+ if (stderrSink && typeof stderrSink.write === 'function') {
138
+ stderrSink.write(chunk);
139
+ }
140
+ });
141
+ child.on('error', (err) => {
142
+ finish({
143
+ status: null,
144
+ signal: null,
145
+ stdout: stdoutTail.toString(),
146
+ stderr: stderrTail.toString(),
147
+ error: err,
148
+ });
149
+ });
150
+ child.on('close', (code, signal) => {
151
+ finish({
152
+ status: code,
153
+ signal,
154
+ stdout: stdoutTail.toString(),
155
+ stderr: stderrTail.toString(),
156
+ error: null,
157
+ });
141
158
  });
142
- }
143
- scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
144
- }, delay);
145
- if (typeof watchdogTimer.unref === 'function') {
146
- watchdogTimer.unref();
147
- }
148
- };
149
-
150
- const noteOutput = () => {
151
- sawOutput = true;
152
- clearWatchdog();
153
- };
154
-
155
- if (noOutputWatchdog) {
156
- scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
157
- }
158
-
159
- child.stdout.on('data', (chunk) => {
160
- noteOutput();
161
- stdoutTail.push(chunk);
162
- if (stdoutSink && typeof stdoutSink.write === 'function') {
163
- stdoutSink.write(chunk);
164
- }
165
- });
166
- child.stderr.on('data', (chunk) => {
167
- noteOutput();
168
- stderrTail.push(chunk);
169
- if (stderrSink && typeof stderrSink.write === 'function') {
170
- stderrSink.write(chunk);
171
- }
172
- });
173
-
174
- child.on('error', (/** @type {Error} */ err) => {
175
- finish({
176
- status: null,
177
- signal: null,
178
- stdout: stdoutTail.toString(),
179
- stderr: stderrTail.toString(),
180
- error: err,
181
- startedAt: new Date(startTime).toISOString(),
182
- endedAt: new Date().toISOString()
183
- });
184
- });
185
-
186
- child.on('close', (/** @type {number|null} */ code, /** @type {string|null} */ signal) => {
187
- finish({
188
- status: code,
189
- signal,
190
- stdout: stdoutTail.toString(),
191
- stderr: stderrTail.toString(),
192
- error: null,
193
- startedAt: new Date(startTime).toISOString(),
194
- endedAt: new Date().toISOString()
195
- });
196
159
  });
197
- });
198
160
  }
199
-
200
- module.exports = { spawnAndTee, DEFAULT_MAX_TAIL_BYTES };
@@ -0,0 +1,202 @@
1
+ import childProcess from 'node:child_process';
2
+ import type { SpawnOptions, ChildProcess } from 'node:child_process';
3
+ import path from 'node:path';
4
+
5
+ export const DEFAULT_MAX_TAIL_BYTES = 64 * 1024;
6
+
7
+ type ChunkType = Buffer | string;
8
+
9
+ interface SpawnTeeResult {
10
+ status: number | null;
11
+ signal: string | null;
12
+ stdout: string;
13
+ stderr: string;
14
+ error: unknown | null;
15
+ startedAt: string;
16
+ endedAt: string;
17
+ }
18
+
19
+ interface NoOutputWatchdog {
20
+ onNoOutput?: (event: { command: string; args: string[]; pid: number | undefined; elapsedMs: number }) => void;
21
+ initialDelayMs?: number;
22
+ intervalMs?: number;
23
+ }
24
+
25
+ interface SpawnTeeOptions {
26
+ stdoutSink?: NodeJS.WriteStream;
27
+ stderrSink?: NodeJS.WriteStream;
28
+ maxTailBytes?: number;
29
+ noOutputWatchdog?: NoOutputWatchdog | null;
30
+ cwd?: string;
31
+ env?: Record<string, string>;
32
+ [key: string]: unknown;
33
+ }
34
+
35
+ export class TailBuffer {
36
+ maxBytes: number;
37
+ chunks: ChunkType[];
38
+ size: number;
39
+
40
+ constructor(maxBytes: number) {
41
+ this.maxBytes = maxBytes;
42
+ this.chunks = [];
43
+ this.size = 0;
44
+ }
45
+
46
+ push(chunk: ChunkType): void {
47
+ this.chunks.push(chunk);
48
+ this.size += chunk.length;
49
+ while (this.size > this.maxBytes && this.chunks.length > 0) {
50
+ const head = this.chunks[0];
51
+ const overflow = this.size - this.maxBytes;
52
+ if (head.length <= overflow) {
53
+ this.chunks.shift();
54
+ this.size -= head.length;
55
+ } else {
56
+ if (Buffer.isBuffer(head)) {
57
+ this.chunks[0] = head.subarray(overflow);
58
+ } else {
59
+ this.chunks.shift();
60
+ this.size -= head.length;
61
+ }
62
+ this.size -= overflow;
63
+ }
64
+ }
65
+ }
66
+
67
+ toString(): string {
68
+ if (this.chunks.length === 0) {return '';}
69
+ return Buffer.concat(this.chunks as Buffer[]).toString('utf8');
70
+ }
71
+ }
72
+
73
+ interface FinishPayload {
74
+ status: number | null;
75
+ signal: string | null;
76
+ stdout: string;
77
+ stderr: string;
78
+ error: unknown | null;
79
+ startedAt?: string;
80
+ endedAt?: string;
81
+ }
82
+
83
+ export function spawnAndTee(command: string, args: string[], options: SpawnTeeOptions = {}): Promise<SpawnTeeResult> {
84
+ const {
85
+ stdoutSink = process.stdout,
86
+ stderrSink = process.stderr,
87
+ maxTailBytes = DEFAULT_MAX_TAIL_BYTES,
88
+ noOutputWatchdog = null,
89
+ ...spawnOptions
90
+ } = options;
91
+
92
+ return new Promise((resolve) => {
93
+ const stdoutTail = new TailBuffer(maxTailBytes);
94
+ const stderrTail = new TailBuffer(maxTailBytes);
95
+ let settled = false;
96
+ let sawOutput = false;
97
+ let watchdogTimer: ReturnType<typeof setTimeout> | null = null;
98
+ const startTime = Date.now();
99
+ const resolvedCwd = path.resolve((spawnOptions.cwd as string) || process.cwd());
100
+ const env: Record<string, string> = {
101
+ ...process.env,
102
+ ...(spawnOptions.env || {}),
103
+ PWD: resolvedCwd
104
+ };
105
+
106
+ let child: ChildProcess;
107
+ try {
108
+ child = childProcess.spawn(command, args, {
109
+ ...spawnOptions,
110
+ env,
111
+ stdio: ['inherit', 'pipe', 'pipe']
112
+ } as SpawnOptions);
113
+ } catch (err) {
114
+ resolve({
115
+ status: null, signal: null, stdout: '', stderr: '', error: err,
116
+ startedAt: new Date(startTime).toISOString(), endedAt: new Date().toISOString()
117
+ });
118
+ return;
119
+ }
120
+
121
+ const clearWatchdog = (): void => {
122
+ if (watchdogTimer) {
123
+ clearTimeout(watchdogTimer);
124
+ watchdogTimer = null;
125
+ }
126
+ };
127
+
128
+ const finish = (payload: FinishPayload): void => {
129
+ if (settled) {return;}
130
+ settled = true;
131
+ clearWatchdog();
132
+ payload.startedAt = new Date(startTime).toISOString();
133
+ payload.endedAt = new Date().toISOString();
134
+ resolve(payload as SpawnTeeResult);
135
+ };
136
+
137
+ const scheduleWatchdog = (delayMs: number): void => {
138
+ if (!noOutputWatchdog || typeof noOutputWatchdog.onNoOutput !== 'function') {return;}
139
+ const delay = Number.isFinite(delayMs) && delayMs >= 0 ? delayMs : 0;
140
+ watchdogTimer = setTimeout(() => {
141
+ watchdogTimer = null;
142
+ if (settled || sawOutput) {return;}
143
+ if (typeof noOutputWatchdog.onNoOutput === 'function') {
144
+ noOutputWatchdog.onNoOutput({
145
+ command,
146
+ args,
147
+ pid: child.pid,
148
+ elapsedMs: Date.now() - startTime
149
+ });
150
+ }
151
+ scheduleWatchdog(noOutputWatchdog.intervalMs ?? 0);
152
+ }, delay);
153
+ if (typeof watchdogTimer.unref === 'function') {
154
+ watchdogTimer.unref();
155
+ }
156
+ };
157
+
158
+ const noteOutput = (): void => {
159
+ sawOutput = true;
160
+ clearWatchdog();
161
+ };
162
+
163
+ if (noOutputWatchdog) {
164
+ scheduleWatchdog(noOutputWatchdog.initialDelayMs ?? 0);
165
+ }
166
+
167
+ child.stdout?.on('data', (chunk: Buffer) => {
168
+ noteOutput();
169
+ stdoutTail.push(chunk);
170
+ if (stdoutSink && typeof stdoutSink.write === 'function') {
171
+ stdoutSink.write(chunk);
172
+ }
173
+ });
174
+ child.stderr?.on('data', (chunk: Buffer) => {
175
+ noteOutput();
176
+ stderrTail.push(chunk);
177
+ if (stderrSink && typeof stderrSink.write === 'function') {
178
+ stderrSink.write(chunk);
179
+ }
180
+ });
181
+
182
+ child.on('error', (err: Error) => {
183
+ finish({
184
+ status: null,
185
+ signal: null,
186
+ stdout: stdoutTail.toString(),
187
+ stderr: stderrTail.toString(),
188
+ error: err,
189
+ });
190
+ });
191
+
192
+ child.on('close', (code: number | null, signal: string | null) => {
193
+ finish({
194
+ status: code,
195
+ signal,
196
+ stdout: stdoutTail.toString(),
197
+ stderr: stderrTail.toString(),
198
+ error: null,
199
+ });
200
+ });
201
+ });
202
+ }