@engineeros/connector 0.15.2 → 0.16.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.
@@ -1,1047 +1,1086 @@
1
- #!/usr/bin/env node
2
- import os from "node:os";
3
- import path from "node:path";
4
- import {
5
- assessmentResultUrl,
6
- connectorResumeCredentials,
7
- loadConfig,
8
- mergePairedConfig,
9
- resultUrl,
10
- saveConfig,
11
- socketUrl,
12
- workspaceUrl,
13
- } from "../src/config.mjs";
1
+ #!/usr/bin/env node
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import {
5
+ assessmentResultUrl,
6
+ connectorResumeCredentials,
7
+ loadConfig,
8
+ mergePairedConfig,
9
+ resultUrl,
10
+ saveConfig,
11
+ socketUrl,
12
+ workspaceUrl,
13
+ } from "../src/config.mjs";
14
14
  import {
15
15
  ASSESSMENT_RESULT_TIMEOUT_MS,
16
- attachAssessmentRejectionCorrection,
17
- assessmentWorkerSnapshots,
18
- assessmentInactivityFailure,
19
- assessmentProgressMessage,
20
- applyAcceptedChange,
21
- enqueueWorkspaceAssessment,
22
- executeAssignment,
23
- executeConnectedPrompt,
24
- executeWorkspaceAssessment,
25
- inspectCodingAgent,
26
- promptStreamEvent,
27
- requeueInterruptedAssessment,
28
- stopProcess,
16
+ DEFAULT_ASSESSMENT_WORKERS,
17
+ attachAssessmentRejectionCorrection,
18
+ assessmentWorkerSnapshots,
19
+ assessmentInactivityFailure,
20
+ assessmentProgressMessage,
21
+ applyAcceptedChange,
22
+ enqueueWorkspaceAssessment,
23
+ executeAssignment,
24
+ executeConnectedPrompt,
25
+ executeWorkspaceAssessment,
26
+ inspectCodingAgent,
27
+ promptStreamEvent,
28
+ requeueInterruptedAssessment,
29
+ stopProcess,
29
30
  submitAssessmentResultWithRetry,
30
31
  submitAssessmentWithValidationRepair,
31
32
  takeWorkspaceAssessmentWave,
32
33
  workspaceAssessmentWorkerLimit,
33
34
  workspaceSnapshot,
34
35
  } from "../src/runner.mjs";
35
- import {
36
- assessmentCheckpointPayload,
37
- assessmentCompletionBundle,
36
+ import {
37
+ assessmentCheckpointPayload,
38
+ assessmentCompletionBundle,
38
39
  clearAssessmentSpool,
40
+ loadAssessmentStageSession,
39
41
  loadAssessmentStageResult,
40
42
  persistAssessmentStageResult,
41
- prepareLocalAssessmentAssignment,
42
- removeAssessmentStageResult,
43
- } from "../src/assessment-spool.mjs";
44
- import { disposeAcpRuntimes } from "../src/acp-client.mjs";
45
- import {
46
- installRegisteredAgent,
47
- inspectRegisteredAgent,
48
- registeredAgentConfig,
49
- registeredAgents,
50
- } from "../src/agent-registry.mjs";
51
- import {
43
+ persistAssessmentStageSession,
44
+ prepareLocalAssessmentAssignment,
45
+ removeAssessmentStageResult,
46
+ } from "../src/assessment-spool.mjs";
47
+ import { disposeAcpRuntimes } from "../src/acp-client.mjs";
48
+ import {
49
+ installRegisteredAgent,
50
+ inspectRegisteredAgent,
51
+ registeredAgentConfig,
52
+ registeredAgents,
53
+ } from "../src/agent-registry.mjs";
54
+ import {
52
55
  describeRejectedResponse,
53
56
  describeWebSocketError,
54
57
  protocolFailureMessage,
58
+ sendConnectionMessage,
55
59
  startConnectionWatchdog,
56
60
  } from "../src/connection.mjs";
57
- import { advertisedCapabilities } from "../src/capabilities.mjs";
58
- import { parseConnectorArgs } from "../src/cli-args.mjs";
59
- import { runMcpServer } from "../src/mcp-server.mjs";
60
- import packageJson from "../package.json" with { type: "json" };
61
-
61
+ import { advertisedCapabilities } from "../src/capabilities.mjs";
62
+ import { parseConnectorArgs } from "../src/cli-args.mjs";
63
+ import { runMcpServer } from "../src/mcp-server.mjs";
64
+ import packageJson from "../package.json" with { type: "json" };
65
+
62
66
  const { command, positional, flags } = parseConnectorArgs(
63
67
  process.argv.slice(2),
64
68
  );
65
69
 
66
- if (command === "agents") {
67
- let agents;
68
- try {
69
- agents = await registeredAgents();
70
- } catch (error) {
71
- fail(error instanceof Error ? error.message : String(error));
72
- }
73
- const statuses = await Promise.all(
74
- agents.map(async (agent) => {
75
- try {
76
- await inspectRegisteredAgent(agent.id);
77
- return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
78
- } catch (error) {
79
- const detail = error instanceof Error ? error.message : String(error);
80
- return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
81
- }
82
- }),
83
- );
84
- for (const status of statuses) {
85
- console.log(status);
86
- }
87
- process.exit(0);
88
- }
89
-
90
- if (command === "agent") {
91
- const agentId = positional[0];
92
- const action = positional[1] || "check";
93
- if (!agentId || !["check", "install"].includes(action)) {
94
- fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
95
- }
96
- try {
97
- const installed =
98
- action === "install"
99
- ? await installRegisteredAgent(agentId)
100
- : await inspectRegisteredAgent(agentId);
101
- console.log(
102
- `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
103
- );
104
- } catch (error) {
105
- fail(error instanceof Error ? error.message : String(error));
106
- }
107
- process.exit(0);
108
- }
109
-
110
- if (command === "mcp") {
111
- const config = await loadConfig(flags.workspace || process.cwd());
112
- if (!config) fail("This workspace is not paired with EngineerOS.");
113
- const runId = flags["run-id"] || positional[0];
114
- if (!runId)
115
- fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
116
- await runMcpServer({
117
- config,
118
- runId,
119
- version: packageJson.version,
120
- input: process.stdin,
121
- output: process.stdout,
122
- });
123
- process.exit(0);
124
- }
125
-
126
- if (command === "status") {
127
- const config = await loadConfig(flags.workspace || process.cwd());
128
- console.log(
129
- config
130
- ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
131
- : "Not paired",
132
- );
133
- process.exit(config ? 0 : 1);
134
- }
135
-
136
- let config;
137
- let firstMessage;
138
- let existingConfig;
139
- if (command === "pair") {
140
- const pairingCode = positional[0];
141
- if (!pairingCode)
142
- fail(
143
- "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--assessment-workers COUNT] [--skip-git-repo-check]",
144
- );
145
- const url = flags.url;
146
- if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
147
- if (flags.agent && flags["agent-command"]) {
148
- fail("Use either --agent or --agent-command, not both.");
149
- }
150
- let agentConfig = {};
151
- try {
152
- if (flags.agent) {
153
- await inspectRegisteredAgent(flags.agent);
154
- agentConfig = await registeredAgentConfig(flags.agent);
155
- }
156
- } catch (error) {
157
- fail(error instanceof Error ? error.message : String(error));
158
- }
159
- config = {
160
- server_url: socketUrl(url),
161
- workspace: path.resolve(flags.workspace || process.cwd()),
162
- onboard: flags.onboard === true,
163
- onboarding_pending: flags.onboard === true,
164
- agent_protocol: flags["agent-command"] ? "acp" : "codex",
165
- agent_command: flags["agent-command"] || null,
166
- agent_args: parseAgentArgs(flags["agent-args"]),
167
- agent_name: flags["agent-name"] || null,
168
- ...agentConfig,
169
- ...(flags["assessment-workers"] === undefined
170
- ? {}
171
- : {
172
- assessment_workers: configuredAssessmentWorkerLimit(
173
- flags["assessment-workers"],
174
- ),
175
- }),
176
- skip_git_repo_check: flags["skip-git-repo-check"] === true,
177
- name:
178
- flags.name ||
179
- `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
180
- };
181
- existingConfig = await loadConfig(config.workspace);
182
- firstMessage = {
183
- type: "pair",
184
- pairing_code: pairingCode,
185
- name: config.name,
186
- capabilities: {},
187
- ...connectorResumeCredentials(existingConfig, config.server_url),
188
- };
189
- } else if (command === "start") {
190
- config = await loadConfig(flags.workspace || process.cwd());
191
- if (!config)
192
- fail(
193
- "This connector is not paired. Create a pairing command in EngineerOS first.",
194
- );
195
- firstMessage = {
196
- type: "authenticate",
197
- connector_id: config.connector_id,
198
- token: config.token,
199
- };
200
- } else {
70
+ if (flags["assessment-workers"] !== undefined) {
201
71
  fail(
202
- "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
72
+ "--assessment-workers is no longer supported. Select 3 to 8 parallel agents in EngineerOS before starting the assessment.",
203
73
  );
204
74
  }
205
-
206
- let codingAgent;
207
- try {
208
- codingAgent = await inspectCodingAgent(config, config.workspace);
209
- } catch (error) {
210
- fail(error instanceof Error ? error.message : String(error));
211
- }
212
- console.log(
213
- `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
214
- );
215
- const capabilities = advertisedCapabilities(config, codingAgent);
216
- firstMessage.capabilities = capabilities;
217
-
218
- let stopped = false;
219
- let active = null;
75
+
76
+ if (command === "agents") {
77
+ let agents;
78
+ try {
79
+ agents = await registeredAgents();
80
+ } catch (error) {
81
+ fail(error instanceof Error ? error.message : String(error));
82
+ }
83
+ const statuses = await Promise.all(
84
+ agents.map(async (agent) => {
85
+ try {
86
+ await inspectRegisteredAgent(agent.id);
87
+ return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
88
+ } catch (error) {
89
+ const detail = error instanceof Error ? error.message : String(error);
90
+ return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
91
+ }
92
+ }),
93
+ );
94
+ for (const status of statuses) {
95
+ console.log(status);
96
+ }
97
+ process.exit(0);
98
+ }
99
+
100
+ if (command === "agent") {
101
+ const agentId = positional[0];
102
+ const action = positional[1] || "check";
103
+ if (!agentId || !["check", "install"].includes(action)) {
104
+ fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
105
+ }
106
+ try {
107
+ const installed =
108
+ action === "install"
109
+ ? await installRegisteredAgent(agentId)
110
+ : await inspectRegisteredAgent(agentId);
111
+ console.log(
112
+ `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
113
+ );
114
+ } catch (error) {
115
+ fail(error instanceof Error ? error.message : String(error));
116
+ }
117
+ process.exit(0);
118
+ }
119
+
120
+ if (command === "mcp") {
121
+ const config = await loadConfig(flags.workspace || process.cwd());
122
+ if (!config) fail("This workspace is not paired with EngineerOS.");
123
+ const runId = flags["run-id"] || positional[0];
124
+ if (!runId)
125
+ fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
126
+ await runMcpServer({
127
+ config,
128
+ runId,
129
+ version: packageJson.version,
130
+ input: process.stdin,
131
+ output: process.stdout,
132
+ });
133
+ process.exit(0);
134
+ }
135
+
136
+ if (command === "status") {
137
+ const config = await loadConfig(flags.workspace || process.cwd());
138
+ console.log(
139
+ config
140
+ ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
141
+ : "Not paired",
142
+ );
143
+ process.exit(config ? 0 : 1);
144
+ }
145
+
146
+ let config;
147
+ let firstMessage;
148
+ let existingConfig;
149
+ if (command === "pair") {
150
+ const pairingCode = positional[0];
151
+ if (!pairingCode)
152
+ fail(
153
+ "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
154
+ );
155
+ const url = flags.url;
156
+ if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
157
+ if (flags.agent && flags["agent-command"]) {
158
+ fail("Use either --agent or --agent-command, not both.");
159
+ }
160
+ let agentConfig = {};
161
+ try {
162
+ if (flags.agent) {
163
+ await inspectRegisteredAgent(flags.agent);
164
+ agentConfig = await registeredAgentConfig(flags.agent);
165
+ }
166
+ } catch (error) {
167
+ fail(error instanceof Error ? error.message : String(error));
168
+ }
169
+ config = {
170
+ server_url: socketUrl(url),
171
+ workspace: path.resolve(flags.workspace || process.cwd()),
172
+ onboard: flags.onboard === true,
173
+ onboarding_pending: flags.onboard === true,
174
+ agent_protocol: flags["agent-command"] ? "acp" : "codex",
175
+ agent_command: flags["agent-command"] || null,
176
+ agent_args: parseAgentArgs(flags["agent-args"]),
177
+ agent_name: flags["agent-name"] || null,
178
+ ...agentConfig,
179
+ skip_git_repo_check: flags["skip-git-repo-check"] === true,
180
+ name:
181
+ flags.name ||
182
+ `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
183
+ };
184
+ existingConfig = await loadConfig(config.workspace);
185
+ firstMessage = {
186
+ type: "pair",
187
+ pairing_code: pairingCode,
188
+ name: config.name,
189
+ capabilities: {},
190
+ ...connectorResumeCredentials(existingConfig, config.server_url),
191
+ };
192
+ } else if (command === "start") {
193
+ config = await loadConfig(flags.workspace || process.cwd());
194
+ if (!config)
195
+ fail(
196
+ "This connector is not paired. Create a pairing command in EngineerOS first.",
197
+ );
198
+ firstMessage = {
199
+ type: "authenticate",
200
+ connector_id: config.connector_id,
201
+ token: config.token,
202
+ };
203
+ } else {
204
+ fail(
205
+ "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
206
+ );
207
+ }
208
+
209
+ let codingAgent;
210
+ try {
211
+ codingAgent = await inspectCodingAgent(config, config.workspace);
212
+ } catch (error) {
213
+ fail(error instanceof Error ? error.message : String(error));
214
+ }
215
+ console.log(
216
+ `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
217
+ );
218
+ const capabilities = advertisedCapabilities(config, codingAgent);
219
+ firstMessage.capabilities = capabilities;
220
+
221
+ let stopped = false;
222
+ let active = null;
220
223
  const activeAssessments = new Map();
221
- const ASSESSMENT_WORKER_LIMIT = configuredAssessmentWorkerLimit(
222
- flags["assessment-workers"] ?? config.assessment_workers,
223
- );
224
224
  console.log(
225
- `Assessment supervisor can run up to ${ASSESSMENT_WORKER_LIMIT} independent ${codingAgent.name} stages.`,
225
+ `Assessment supervisor defaults to ${DEFAULT_ASSESSMENT_WORKERS} parallel ${codingAgent.name} stages; each assessment run can select its bounded limit.`,
226
226
  );
227
- const available = [];
228
- const assessments = [];
229
- const activePrompts = new Map();
230
- let socket;
231
- let pingTimer;
232
- let reconnectTimer;
233
- let clearConnectionWatchdog;
234
- let reconnectDelay = 1_000;
235
- let connectionRejected = false;
236
- let lastConnectionError;
237
- let snapshotInFlight = false;
238
-
239
- process.on("SIGINT", async () => {
240
- stopped = true;
241
- clearConnectionWatchdog?.();
242
- clearTimeout(reconnectTimer);
243
- clearInterval(pingTimer);
244
- await Promise.all([
245
- stopProcess(active?.child),
246
- ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
247
- ]);
248
- await Promise.all([...activePrompts.values()].map(cancelPrompt));
249
- await disposeAcpRuntimes();
250
- socket?.close();
251
- process.exit(0);
252
- });
253
-
254
- await connect();
255
-
227
+ const available = [];
228
+ const assessments = [];
229
+ const activePrompts = new Map();
230
+ let socket;
231
+ let pingTimer;
232
+ let reconnectTimer;
233
+ let clearConnectionWatchdog;
234
+ let reconnectDelay = 1_000;
235
+ let connectionRejected = false;
236
+ let lastConnectionError;
237
+ let snapshotInFlight = false;
238
+
239
+ process.on("SIGINT", async () => {
240
+ stopped = true;
241
+ clearConnectionWatchdog?.();
242
+ clearTimeout(reconnectTimer);
243
+ clearInterval(pingTimer);
244
+ await Promise.all([
245
+ stopProcess(active?.child),
246
+ ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
247
+ ]);
248
+ await Promise.all([...activePrompts.values()].map(cancelPrompt));
249
+ await disposeAcpRuntimes();
250
+ socket?.close();
251
+ process.exit(0);
252
+ });
253
+
254
+ await connect();
255
+
256
256
  async function connect() {
257
257
  console.log(`Connecting ${config.name} to ${config.server_url}`);
258
258
  connectionRejected = false;
259
259
  lastConnectionError = undefined;
260
- socket = new WebSocket(config.server_url);
261
- clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
260
+ const connection = new WebSocket(config.server_url);
261
+ socket = connection;
262
+ const clearWatchdog = startConnectionWatchdog(connection, config.server_url, {
262
263
  onTimeout: (message) => {
264
+ if (socket !== connection) return;
263
265
  lastConnectionError = message;
264
266
  console.error(message);
265
267
  scheduleReconnect();
266
268
  },
267
269
  });
268
- socket.addEventListener("open", () =>
269
- socket.send(JSON.stringify(firstMessage)),
270
- );
271
- socket.addEventListener("message", async (event) => {
272
- const message = JSON.parse(String(event.data));
273
- if (message.type === "paired") {
274
- clearConnectionWatchdog?.();
275
- const reusedConnector =
276
- existingConfig?.connector_id === message.connector.id;
277
- config = mergePairedConfig(
278
- config,
279
- existingConfig,
280
- message.connector.id,
281
- message.token,
282
- );
283
- await saveConfig(config);
284
- firstMessage = {
285
- type: "authenticate",
286
- connector_id: config.connector_id,
287
- token: config.token,
288
- capabilities,
289
- };
290
- console.log(
291
- reusedConnector
292
- ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
293
- : `Paired. Connector ${config.connector_id} is online.`,
294
- );
295
- reconnectDelay = 1_000;
296
- startPings();
297
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
298
- return;
299
- }
300
- if (message.type === "authenticated") {
301
- clearConnectionWatchdog?.();
302
- console.log("Connected and waiting for EngineerOS runs.");
303
- reconnectDelay = 1_000;
304
- startPings();
305
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
270
+ clearConnectionWatchdog = clearWatchdog;
271
+ connection.addEventListener("open", () => {
272
+ if (socket !== connection) {
273
+ connection.close();
306
274
  return;
307
275
  }
308
- if (message.type === "workspace.refresh") {
309
- void submitWorkspaceSnapshot();
310
- return;
311
- }
312
- if (message.type === "workspace.assessment") {
313
- const assessmentKey = `${message.assessment_id}:${message.stage}`;
314
- const disposition = enqueueWorkspaceAssessment(
315
- assessments,
316
- activeAssessments.get(assessmentKey),
317
- message,
276
+ try {
277
+ if (sendConnectionMessage(socket, connection, firstMessage)) return;
278
+ throw new Error("The WebSocket was not open when authentication started.");
279
+ } catch (error) {
280
+ lastConnectionError = protocolFailureMessage(error);
281
+ console.error(
282
+ `Could not authenticate the WebSocket connection: ${lastConnectionError}`,
318
283
  );
319
- if (disposition === "deferred") {
320
- console.log(
321
- `Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
322
- );
284
+ try {
285
+ connection.close();
286
+ } catch {
287
+ // The reconnect timer below owns recovery.
323
288
  }
324
- pump();
325
- return;
326
- }
327
- if (message.type === "prompt.execute") {
328
- if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
329
- return;
330
- }
331
- if (message.type === "prompt.cancel") {
332
- const promptState = activePrompts.get(message.prompt_id);
333
- if (promptState) await cancelPrompt(promptState);
334
- return;
335
- }
336
- if (message.type === "run.available") {
337
- if (!available.includes(message.run_id)) available.push(message.run_id);
338
- pump();
339
- return;
340
- }
341
- if (message.type === "run.assignment") {
342
- await execute(message);
343
- return;
344
- }
345
- if (message.type === "run.cancelled" && active?.runId === message.run_id) {
346
- console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
347
- active.cancelled = true;
348
- await stopProcess(active.child);
349
- return;
289
+ scheduleReconnect();
350
290
  }
291
+ });
292
+ connection.addEventListener("message", async (event) => {
293
+ if (socket !== connection) return;
294
+ const message = JSON.parse(String(event.data));
295
+ if (message.type === "paired") {
296
+ clearWatchdog();
297
+ const reusedConnector =
298
+ existingConfig?.connector_id === message.connector.id;
299
+ config = mergePairedConfig(
300
+ config,
301
+ existingConfig,
302
+ message.connector.id,
303
+ message.token,
304
+ );
305
+ await saveConfig(config);
306
+ firstMessage = {
307
+ type: "authenticate",
308
+ connector_id: config.connector_id,
309
+ token: config.token,
310
+ capabilities,
311
+ };
312
+ console.log(
313
+ reusedConnector
314
+ ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
315
+ : `Paired. Connector ${config.connector_id} is online.`,
316
+ );
317
+ reconnectDelay = 1_000;
318
+ startPings();
319
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
320
+ return;
321
+ }
322
+ if (message.type === "authenticated") {
323
+ clearWatchdog();
324
+ console.log("Connected and waiting for EngineerOS runs.");
325
+ reconnectDelay = 1_000;
326
+ startPings();
327
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
328
+ return;
329
+ }
330
+ if (message.type === "workspace.refresh") {
331
+ void submitWorkspaceSnapshot();
332
+ return;
333
+ }
334
+ if (message.type === "workspace.assessment") {
335
+ const assessmentKey = `${message.assessment_id}:${message.stage}`;
336
+ const disposition = enqueueWorkspaceAssessment(
337
+ assessments,
338
+ activeAssessments.get(assessmentKey),
339
+ message,
340
+ );
341
+ if (disposition === "deferred") {
342
+ console.log(
343
+ `Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
344
+ );
345
+ }
346
+ pump();
347
+ return;
348
+ }
349
+ if (message.type === "prompt.execute") {
350
+ if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
351
+ return;
352
+ }
353
+ if (message.type === "prompt.cancel") {
354
+ const promptState = activePrompts.get(message.prompt_id);
355
+ if (promptState) await cancelPrompt(promptState);
356
+ return;
357
+ }
358
+ if (message.type === "run.available") {
359
+ if (!available.includes(message.run_id)) available.push(message.run_id);
360
+ pump();
361
+ return;
362
+ }
363
+ if (message.type === "run.assignment") {
364
+ await execute(message);
365
+ return;
366
+ }
367
+ if (message.type === "run.cancelled" && active?.runId === message.run_id) {
368
+ console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
369
+ active.cancelled = true;
370
+ await stopProcess(active.child);
371
+ return;
372
+ }
351
373
  if (message.type === "connector.revoked") {
352
374
  stopped = true;
353
375
  console.error("This connector was revoked in EngineerOS.");
354
- socket.close();
355
- return;
356
- }
357
- if (message.type === "run.error") {
358
- console.error(`EngineerOS: ${message.message}`);
359
- if (!message.run_id) snapshotInFlight = false;
360
- if (active?.runId === message.run_id && !active.child) {
361
- active = null;
362
- pump();
363
- }
364
- }
365
- if (message.type === "connection.error") {
366
- connectionRejected = true;
367
- console.error(`EngineerOS: ${message.message}`);
368
- }
369
- });
370
- socket.addEventListener("close", (event) => {
371
- clearConnectionWatchdog?.();
376
+ connection.close();
377
+ return;
378
+ }
379
+ if (message.type === "run.error") {
380
+ console.error(`EngineerOS: ${message.message}`);
381
+ if (!message.run_id) snapshotInFlight = false;
382
+ if (active?.runId === message.run_id && !active.child) {
383
+ active = null;
384
+ pump();
385
+ }
386
+ }
387
+ if (message.type === "connection.error") {
388
+ connectionRejected = true;
389
+ console.error(`EngineerOS: ${message.message}`);
390
+ }
391
+ });
392
+ connection.addEventListener("close", (event) => {
393
+ clearWatchdog();
394
+ if (socket !== connection) return;
372
395
  clearInterval(pingTimer);
373
- for (const promptState of activePrompts.values())
374
- void cancelPrompt(promptState);
375
- for (const assessmentState of activeAssessments.values()) {
376
- if (event.code === 4001) void stopProcess(assessmentState.child);
377
- }
378
- if (connectionRejected) {
379
- stopped = true;
380
- console.error(
381
- "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
382
- );
383
- return;
384
- }
385
- if (stopped) return;
386
- scheduleReconnect();
387
- });
388
- socket.addEventListener("error", (event) => {
396
+ for (const promptState of activePrompts.values())
397
+ void cancelPrompt(promptState);
398
+ for (const assessmentState of activeAssessments.values()) {
399
+ if (event.code === 4001) void stopProcess(assessmentState.child);
400
+ }
401
+ if (connectionRejected) {
402
+ stopped = true;
403
+ console.error(
404
+ "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
405
+ );
406
+ return;
407
+ }
408
+ if (stopped) return;
409
+ scheduleReconnect();
410
+ });
411
+ connection.addEventListener("error", (event) => {
412
+ if (socket !== connection) return;
389
413
  lastConnectionError = describeWebSocketError(event);
390
- console.error(
391
- `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
392
- );
393
- });
394
- }
395
-
396
- function scheduleReconnect() {
397
- if (stopped || connectionRejected || reconnectTimer) return;
398
- const detail = lastConnectionError
399
- ? ` Last error: ${lastConnectionError}`
400
- : "";
401
- console.error(
402
- `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
403
- );
404
- reconnectTimer = setTimeout(() => {
405
- reconnectTimer = undefined;
406
- void connect();
407
- }, reconnectDelay);
408
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
409
- }
410
-
411
- async function submitWorkspaceSnapshot() {
412
- if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
413
- snapshotInFlight = true;
414
- console.log("Inspecting the workspace without executing its code.");
415
- try {
416
- const snapshot = await workspaceSnapshot(config.workspace);
417
- const response = await fetch(
418
- workspaceUrl(config.server_url, config.connector_id),
419
- {
420
- method: "POST",
421
- headers: {
422
- "Content-Type": "application/json",
423
- Authorization: `Bearer ${config.token}`,
424
- },
425
- body: JSON.stringify(snapshot),
426
- },
427
- );
428
- if (!response.ok) {
429
- throw new Error(
430
- await describeRejectedResponse(response, "the workspace"),
431
- );
432
- }
433
- const result = await response.json();
434
- config = { ...config, onboarding_pending: false };
435
- await saveConfig(config);
436
- console.log(
437
- `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
438
- );
439
- console.log(
440
- `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
441
- );
442
- console.log(
443
- `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
444
- );
445
- if (snapshot.omitted_evidence_file_count > 0) {
446
- console.log(
447
- `${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
448
- );
449
- }
450
- const executionProfiles =
451
- result.connector?.capabilities?.execution_profiles;
452
- const canSelectAssessmentModel =
453
- executionProfiles?.model_selection === true &&
454
- Array.isArray(executionProfiles.models) &&
455
- executionProfiles.models.length > 0;
456
- console.log(
457
- canSelectAssessmentModel
458
- ? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
459
- : `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
460
- );
461
- snapshotInFlight = false;
462
- } catch (error) {
463
- snapshotInFlight = false;
464
- console.error(
465
- `Workspace assessment failed: ${protocolFailureMessage(error)}`,
466
- );
467
- }
468
- }
469
-
470
- function startPings() {
471
- clearInterval(pingTimer);
472
- sendPing();
473
- pingTimer = setInterval(sendPing, 10_000);
474
- }
475
-
476
- function sendPing() {
477
- if (socket.readyState !== WebSocket.OPEN) return;
478
- try {
479
- socket.send(
480
- JSON.stringify({
481
- type: "ping",
482
- active_run_id: active?.kind === "goal" ? active.runId : null,
483
- assessment_workers: assessmentWorkerSnapshots(activeAssessments),
484
- }),
485
- );
486
- } catch (error) {
487
- lastConnectionError = protocolFailureMessage(error);
488
- try {
489
- socket.close();
490
- } catch {
491
- // The reconnect timer below owns recovery.
492
- }
493
- scheduleReconnect();
494
- }
495
- }
496
-
497
- function sendPromptEvent(promptId, event) {
498
- if (!event || socket.readyState !== WebSocket.OPEN) return;
499
- const payload = {
500
- type: "prompt.event",
501
- prompt_id: promptId,
502
- kind: event.kind,
503
- };
504
- if (event.message) payload.message = String(event.message).slice(0, 500);
505
- if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
506
- if (event.status) payload.status = String(event.status).slice(0, 100);
507
- socket.send(JSON.stringify(payload));
508
- }
509
-
414
+ console.error(
415
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
416
+ );
417
+ });
418
+ }
419
+
420
+ function scheduleReconnect() {
421
+ if (stopped || connectionRejected || reconnectTimer) return;
422
+ const detail = lastConnectionError
423
+ ? ` Last error: ${lastConnectionError}`
424
+ : "";
425
+ console.error(
426
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
427
+ );
428
+ reconnectTimer = setTimeout(() => {
429
+ reconnectTimer = undefined;
430
+ void connect();
431
+ }, reconnectDelay);
432
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
433
+ }
434
+
435
+ async function submitWorkspaceSnapshot() {
436
+ if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
437
+ snapshotInFlight = true;
438
+ console.log("Inspecting the workspace without executing its code.");
439
+ try {
440
+ const snapshot = await workspaceSnapshot(config.workspace);
441
+ const response = await fetch(
442
+ workspaceUrl(config.server_url, config.connector_id),
443
+ {
444
+ method: "POST",
445
+ headers: {
446
+ "Content-Type": "application/json",
447
+ Authorization: `Bearer ${config.token}`,
448
+ },
449
+ body: JSON.stringify(snapshot),
450
+ },
451
+ );
452
+ if (!response.ok) {
453
+ throw new Error(
454
+ await describeRejectedResponse(response, "the workspace"),
455
+ );
456
+ }
457
+ const result = await response.json();
458
+ config = { ...config, onboarding_pending: false };
459
+ await saveConfig(config);
460
+ console.log(
461
+ `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
462
+ );
463
+ console.log(
464
+ `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
465
+ );
466
+ console.log(
467
+ `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
468
+ );
469
+ if (snapshot.omitted_evidence_file_count > 0) {
470
+ console.log(
471
+ `${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
472
+ );
473
+ }
474
+ const executionProfiles =
475
+ result.connector?.capabilities?.execution_profiles;
476
+ const canSelectAssessmentModel =
477
+ executionProfiles?.model_selection === true &&
478
+ Array.isArray(executionProfiles.models) &&
479
+ executionProfiles.models.length > 0;
480
+ console.log(
481
+ canSelectAssessmentModel
482
+ ? `Workspace inventory registered as ${result.workspace_kind}. Select the assessment model in EngineerOS to continue.`
483
+ : `Workspace inventory registered as ${result.workspace_kind}. This agent did not advertise selectable models; update it and reconnect before starting the baseline assessment.`,
484
+ );
485
+ snapshotInFlight = false;
486
+ } catch (error) {
487
+ snapshotInFlight = false;
488
+ console.error(
489
+ `Workspace assessment failed: ${protocolFailureMessage(error)}`,
490
+ );
491
+ }
492
+ }
493
+
494
+ function startPings() {
495
+ clearInterval(pingTimer);
496
+ sendPing();
497
+ pingTimer = setInterval(sendPing, 10_000);
498
+ }
499
+
500
+ function sendPing() {
501
+ if (socket.readyState !== WebSocket.OPEN) return;
502
+ try {
503
+ socket.send(
504
+ JSON.stringify({
505
+ type: "ping",
506
+ active_run_id: active?.kind === "goal" ? active.runId : null,
507
+ assessment_workers: assessmentWorkerSnapshots(activeAssessments),
508
+ }),
509
+ );
510
+ } catch (error) {
511
+ lastConnectionError = protocolFailureMessage(error);
512
+ try {
513
+ socket.close();
514
+ } catch {
515
+ // The reconnect timer below owns recovery.
516
+ }
517
+ scheduleReconnect();
518
+ }
519
+ }
520
+
521
+ function sendPromptEvent(promptId, event) {
522
+ if (!event || socket.readyState !== WebSocket.OPEN) return;
523
+ const payload = {
524
+ type: "prompt.event",
525
+ prompt_id: promptId,
526
+ kind: event.kind,
527
+ };
528
+ if (event.message) payload.message = String(event.message).slice(0, 500);
529
+ if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
530
+ if (event.status) payload.status = String(event.status).slice(0, 100);
531
+ socket.send(JSON.stringify(payload));
532
+ }
533
+
510
534
  function pump() {
511
535
  if (socket.readyState !== WebSocket.OPEN || active) return;
536
+ const activeWorker = activeAssessments.values().next().value;
537
+ const workerLimit = workspaceAssessmentWorkerLimit(
538
+ activeWorker?.workerLimit ?? assessments[0]?.worker_limit,
539
+ );
512
540
  const wave = takeWorkspaceAssessmentWave(
513
541
  assessments,
514
542
  activeAssessments.size,
515
- ASSESSMENT_WORKER_LIMIT,
543
+ workerLimit,
516
544
  );
517
- for (const assessment of wave) {
518
- const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
519
- const assessmentState = {
520
- kind: "assessment",
521
- key: assessmentKey,
522
- runId: assessment.assessment_id,
523
- stage: assessment.stage,
524
- child: null,
525
- controller: null,
526
- resumeAssignment: null,
527
- phase: "starting",
528
- progressPercent: 5,
529
- lastMessage: `Starting ${assessment.stage} assessment stage`,
545
+ for (const assessment of wave) {
546
+ const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
547
+ const assessmentState = {
548
+ kind: "assessment",
549
+ key: assessmentKey,
550
+ runId: assessment.assessment_id,
551
+ stage: assessment.stage,
552
+ child: null,
553
+ controller: null,
554
+ resumeAssignment: null,
555
+ phase: "starting",
556
+ progressPercent: 5,
557
+ lastMessage: `Starting ${assessment.stage} assessment stage`,
530
558
  startedAt: Date.now(),
531
- lastActivityAt: Date.now(),
532
- eventCount: 0,
533
- };
534
- activeAssessments.set(assessmentKey, assessmentState);
535
- void executeAssessment(assessment, assessmentState);
536
- }
537
- if (wave.length > 0) sendPing();
538
- if (activeAssessments.size > 0 || assessments.length > 0) return;
539
- const runId = available.shift();
540
- if (!runId) return;
541
- active = { kind: "goal", runId, child: null, cancelled: false };
542
- socket.send(
543
- JSON.stringify({
544
- type: "run.claim",
545
- run_id: runId,
546
- runner_name: codingAgent.name,
547
- metadata: {
548
- hostname: os.hostname(),
549
- workspace_name: path.basename(config.workspace),
550
- },
551
- }),
552
- );
553
- }
554
-
555
- async function executePrompt(assignment) {
556
- const promptId = assignment.prompt_id;
557
- const promptState = {
558
- runId: promptId,
559
- child: null,
560
- controller: null,
561
- cancelled: false,
562
- };
563
- activePrompts.set(promptId, promptState);
564
- let lastEvent;
565
- const reportEvent = (event) => {
566
- const serialized = event ? JSON.stringify(event) : null;
567
- if (
568
- !serialized ||
569
- serialized === lastEvent ||
570
- socket.readyState !== WebSocket.OPEN ||
571
- activePrompts.get(promptId) !== promptState
572
- ) {
573
- return;
574
- }
575
- lastEvent = serialized;
576
- sendPromptEvent(promptId, event);
577
- };
578
- sendPromptEvent(promptId, {
579
- kind: "status",
580
- message: "Agent started this request",
581
- });
582
- console.log(
583
- `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
584
- );
585
- try {
586
- const result = await executeConnectedPrompt(assignment, config, {
587
- onProcess: (child) => {
588
- if (activePrompts.get(promptId) === promptState)
589
- promptState.child = child;
590
- },
591
- onController: (controller) => {
592
- if (activePrompts.get(promptId) === promptState) {
593
- promptState.controller = controller;
594
- }
595
- },
596
- onEvent: (event) => reportEvent(promptStreamEvent(event)),
597
- });
598
- config = {
599
- ...config,
600
- sessions: {
601
- ...(config.sessions || {}),
602
- [result.sessionKey]: result.sessionId,
603
- },
604
- };
605
- await saveConfig(config);
606
- if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
607
- socket.send(
608
- JSON.stringify({
609
- type: "prompt.completed",
610
- prompt_id: promptId,
611
- content: result.content,
612
- model: result.model,
613
- session_id: result.sessionId,
614
- usage: result.usage,
615
- }),
616
- );
617
- } catch (error) {
618
- const message = protocolFailureMessage(error);
619
- if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
620
- socket.send(
621
- JSON.stringify({
622
- type: "prompt.failed",
623
- prompt_id: promptId,
624
- message,
625
- }),
626
- );
627
- }
628
- if (!promptState.cancelled) {
629
- console.error(message);
630
- }
631
- } finally {
632
- if (activePrompts.get(promptId) === promptState)
633
- activePrompts.delete(promptId);
634
- }
635
- }
636
-
637
- async function cancelPrompt(promptState) {
638
- promptState.cancelled = true;
639
- if (typeof promptState.controller?.cancel === "function") {
640
- await promptState.controller.cancel();
641
- return;
642
- }
643
- await stopProcess(promptState.child);
644
- }
645
-
646
- async function cancelAssessment(assessmentState) {
647
- if (typeof assessmentState.controller?.cancel === "function") {
648
- await assessmentState.controller.cancel();
649
- return;
650
- }
651
- await stopProcess(assessmentState.child);
652
- }
653
-
654
- async function executeAssessment(assignment, assessmentState) {
655
- const assessmentId = assignment.assessment_id;
656
- const stage = assignment.stage;
657
- console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
658
- const stageStartedAt = assessmentState.startedAt;
659
- let agentProcessStarted = false;
660
- let agentCompleted = false;
661
- let statusTicks = 0;
662
- let progress = 5;
663
- let accepted = false;
664
- let failureReported = false;
665
- let inactivityFailure = null;
666
- let lastReportedMilestone = null;
667
- const creditedMilestones = new Set();
668
- const isActiveAssessment = () =>
669
- activeAssessments.get(assessmentState.key) === assessmentState;
670
- const sendAssessmentMessage = (payload) => {
671
- if (socket.readyState !== WebSocket.OPEN) return false;
672
- try {
673
- socket.send(JSON.stringify(payload));
674
- return true;
675
- } catch (error) {
676
- lastConnectionError = protocolFailureMessage(error);
677
- try {
678
- socket.close();
679
- } catch {
680
- // The reconnect timer below owns recovery.
681
- }
682
- scheduleReconnect();
683
- return false;
684
- }
685
- };
686
- const reportProgress = (message, { milestone = true } = {}) => {
687
- if (!isActiveAssessment()) return;
688
- if (milestone && lastReportedMilestone === message) return;
689
- if (milestone) {
690
- lastReportedMilestone = message;
691
- if (!creditedMilestones.has(message)) {
692
- creditedMilestones.add(message);
693
- progress = Math.min(90, progress + 10);
694
- }
695
- console.log(`[assessment:${stage}] ${message}.`);
696
- }
697
- const boundedMessage = String(message).slice(0, 500);
698
- assessmentState.progressPercent = progress;
699
- assessmentState.lastMessage = boundedMessage;
700
- };
701
- reportProgress(`Starting ${stage} assessment stage`);
702
- const heartbeat = setInterval(() => {
703
- const now = Date.now();
704
- const inactiveMs = now - assessmentState.lastActivityAt;
705
- const nextInactivityFailure =
706
- agentProcessStarted && !agentCompleted
707
- ? assessmentInactivityFailure(stage, inactiveMs)
708
- : null;
709
- if (!inactivityFailure && nextInactivityFailure) {
710
- inactivityFailure = nextInactivityFailure;
711
- console.error(`[assessment:${stage}] ${inactivityFailure}`);
712
- void cancelAssessment(assessmentState);
713
- return;
714
- }
715
- if (inactivityFailure) return;
716
- statusTicks += 1;
717
- if (statusTicks % 2 === 0) {
718
- console.log(
719
- `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
720
- `${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
721
- `(${assessmentState.eventCount.toLocaleString()} events).`,
722
- );
723
- }
724
- }, 15_000);
559
+ workerLimit: workspaceAssessmentWorkerLimit(assessment.worker_limit),
560
+ lastActivityAt: Date.now(),
561
+ eventCount: 0,
562
+ };
563
+ activeAssessments.set(assessmentKey, assessmentState);
564
+ void executeAssessment(assessment, assessmentState);
565
+ }
566
+ if (wave.length > 0) sendPing();
567
+ if (activeAssessments.size > 0 || assessments.length > 0) return;
568
+ const runId = available.shift();
569
+ if (!runId) return;
570
+ active = { kind: "goal", runId, child: null, cancelled: false };
571
+ socket.send(
572
+ JSON.stringify({
573
+ type: "run.claim",
574
+ run_id: runId,
575
+ runner_name: codingAgent.name,
576
+ metadata: {
577
+ hostname: os.hostname(),
578
+ workspace_name: path.basename(config.workspace),
579
+ },
580
+ }),
581
+ );
582
+ }
583
+
584
+ async function executePrompt(assignment) {
585
+ const promptId = assignment.prompt_id;
586
+ const promptState = {
587
+ runId: promptId,
588
+ child: null,
589
+ controller: null,
590
+ cancelled: false,
591
+ };
592
+ activePrompts.set(promptId, promptState);
593
+ let lastEvent;
594
+ const reportEvent = (event) => {
595
+ const serialized = event ? JSON.stringify(event) : null;
596
+ if (
597
+ !serialized ||
598
+ serialized === lastEvent ||
599
+ socket.readyState !== WebSocket.OPEN ||
600
+ activePrompts.get(promptId) !== promptState
601
+ ) {
602
+ return;
603
+ }
604
+ lastEvent = serialized;
605
+ sendPromptEvent(promptId, event);
606
+ };
607
+ sendPromptEvent(promptId, {
608
+ kind: "status",
609
+ message: "Agent started this request",
610
+ });
611
+ console.log(
612
+ `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
613
+ );
614
+ try {
615
+ const result = await executeConnectedPrompt(assignment, config, {
616
+ onProcess: (child) => {
617
+ if (activePrompts.get(promptId) === promptState)
618
+ promptState.child = child;
619
+ },
620
+ onController: (controller) => {
621
+ if (activePrompts.get(promptId) === promptState) {
622
+ promptState.controller = controller;
623
+ }
624
+ },
625
+ onEvent: (event) => reportEvent(promptStreamEvent(event)),
626
+ });
627
+ config = {
628
+ ...config,
629
+ sessions: {
630
+ ...(config.sessions || {}),
631
+ [result.sessionKey]: result.sessionId,
632
+ },
633
+ };
634
+ await saveConfig(config);
635
+ if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
636
+ socket.send(
637
+ JSON.stringify({
638
+ type: "prompt.completed",
639
+ prompt_id: promptId,
640
+ content: result.content,
641
+ model: result.model,
642
+ session_id: result.sessionId,
643
+ usage: result.usage,
644
+ }),
645
+ );
646
+ } catch (error) {
647
+ const message = protocolFailureMessage(error);
648
+ if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
649
+ socket.send(
650
+ JSON.stringify({
651
+ type: "prompt.failed",
652
+ prompt_id: promptId,
653
+ message,
654
+ }),
655
+ );
656
+ }
657
+ if (!promptState.cancelled) {
658
+ console.error(message);
659
+ }
660
+ } finally {
661
+ if (activePrompts.get(promptId) === promptState)
662
+ activePrompts.delete(promptId);
663
+ }
664
+ }
665
+
666
+ async function cancelPrompt(promptState) {
667
+ promptState.cancelled = true;
668
+ if (typeof promptState.controller?.cancel === "function") {
669
+ await promptState.controller.cancel();
670
+ return;
671
+ }
672
+ await stopProcess(promptState.child);
673
+ }
674
+
675
+ async function cancelAssessment(assessmentState) {
676
+ if (typeof assessmentState.controller?.cancel === "function") {
677
+ await assessmentState.controller.cancel();
678
+ return;
679
+ }
680
+ await stopProcess(assessmentState.child);
681
+ }
682
+
683
+ async function executeAssessment(assignment, assessmentState) {
684
+ const assessmentId = assignment.assessment_id;
685
+ const stage = assignment.stage;
686
+ console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
687
+ const stageStartedAt = assessmentState.startedAt;
688
+ let agentProcessStarted = false;
689
+ let agentCompleted = false;
690
+ let statusTicks = 0;
691
+ let progress = 5;
692
+ let accepted = false;
693
+ let failureReported = false;
694
+ let inactivityFailure = null;
695
+ let lastReportedMilestone = null;
696
+ const creditedMilestones = new Set();
697
+ const isActiveAssessment = () =>
698
+ activeAssessments.get(assessmentState.key) === assessmentState;
699
+ const sendAssessmentMessage = (payload) => {
700
+ if (socket.readyState !== WebSocket.OPEN) return false;
701
+ try {
702
+ socket.send(JSON.stringify(payload));
703
+ return true;
704
+ } catch (error) {
705
+ lastConnectionError = protocolFailureMessage(error);
706
+ try {
707
+ socket.close();
708
+ } catch {
709
+ // The reconnect timer below owns recovery.
710
+ }
711
+ scheduleReconnect();
712
+ return false;
713
+ }
714
+ };
715
+ const reportProgress = (message, { milestone = true } = {}) => {
716
+ if (!isActiveAssessment()) return;
717
+ if (milestone && lastReportedMilestone === message) return;
718
+ if (milestone) {
719
+ lastReportedMilestone = message;
720
+ if (!creditedMilestones.has(message)) {
721
+ creditedMilestones.add(message);
722
+ progress = Math.min(90, progress + 10);
723
+ }
724
+ console.log(`[assessment:${stage}] ${message}.`);
725
+ }
726
+ const boundedMessage = String(message).slice(0, 500);
727
+ assessmentState.progressPercent = progress;
728
+ assessmentState.lastMessage = boundedMessage;
729
+ };
730
+ reportProgress(`Starting ${stage} assessment stage`);
731
+ const heartbeat = setInterval(() => {
732
+ const now = Date.now();
733
+ const inactiveMs = now - assessmentState.lastActivityAt;
734
+ const nextInactivityFailure =
735
+ agentProcessStarted && !agentCompleted
736
+ ? assessmentInactivityFailure(stage, inactiveMs)
737
+ : null;
738
+ if (!inactivityFailure && nextInactivityFailure) {
739
+ inactivityFailure = nextInactivityFailure;
740
+ console.error(`[assessment:${stage}] ${inactivityFailure}`);
741
+ void cancelAssessment(assessmentState);
742
+ return;
743
+ }
744
+ if (inactivityFailure) return;
745
+ statusTicks += 1;
746
+ if (statusTicks % 2 === 0) {
747
+ console.log(
748
+ `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
749
+ `${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
750
+ `(${assessmentState.eventCount.toLocaleString()} events).`,
751
+ );
752
+ }
753
+ }, 15_000);
725
754
  const assessmentAgentCallbacks = {
726
- onController: (controller) => {
727
- if (isActiveAssessment()) assessmentState.controller = controller;
728
- },
729
- onProcess: (child) => {
730
- if (isActiveAssessment()) {
731
- assessmentState.child = child;
732
- agentProcessStarted = true;
733
- if (assessmentState.phase !== "correcting") {
734
- assessmentState.phase = "running";
735
- reportProgress("Connected agent process started");
736
- }
737
- assessmentState.lastActivityAt = Date.now();
738
- }
739
- },
740
- onEvent: (event) => {
741
- assessmentState.lastActivityAt = Date.now();
742
- assessmentState.eventCount += 1;
743
- const message = assessmentProgressMessage(event);
744
- if (message) reportProgress(message);
755
+ onSession: async (sessionId) => {
756
+ if (!isActiveAssessment()) return;
757
+ await persistAssessmentStageSession(
758
+ config.workspace,
759
+ assessmentId,
760
+ stage,
761
+ sessionId,
762
+ );
745
763
  },
746
- };
747
- const attachStoredCorrection = (candidate, preparedAssignment) =>
748
- attachAssessmentRejectionCorrection(
749
- candidate,
750
- preparedAssignment,
751
- config,
752
- assessmentAgentCallbacks,
753
- {
754
- draftPath: path
755
- .join(
756
- ".engineeros",
757
- "assessments",
758
- String(assessmentId),
759
- candidate.report_file,
760
- )
761
- .split(path.sep)
762
- .join("/"),
763
- },
764
- );
765
- try {
766
- if (assignment.assessment_mode === "incremental") {
767
- progress = 15;
768
- reportProgress("Calculating changed files and affected behavior");
769
- }
770
- const preparedAssignment = await prepareLocalAssessmentAssignment(
771
- config.workspace,
772
- assignment,
773
- );
764
+ onController: (controller) => {
765
+ if (isActiveAssessment()) assessmentState.controller = controller;
766
+ },
767
+ onProcess: (child) => {
768
+ if (isActiveAssessment()) {
769
+ assessmentState.child = child;
770
+ agentProcessStarted = true;
771
+ if (assessmentState.phase !== "correcting") {
772
+ assessmentState.phase = "running";
773
+ reportProgress("Connected agent process started");
774
+ }
775
+ assessmentState.lastActivityAt = Date.now();
776
+ }
777
+ },
778
+ onEvent: (event) => {
779
+ assessmentState.lastActivityAt = Date.now();
780
+ assessmentState.eventCount += 1;
781
+ const message = assessmentProgressMessage(event);
782
+ if (message) reportProgress(message);
783
+ },
784
+ };
785
+ const attachStoredCorrection = (candidate, preparedAssignment) =>
786
+ attachAssessmentRejectionCorrection(
787
+ candidate,
788
+ preparedAssignment,
789
+ config,
790
+ assessmentAgentCallbacks,
791
+ {
792
+ draftPath: path
793
+ .join(
794
+ ".engineeros",
795
+ "assessments",
796
+ String(assessmentId),
797
+ candidate.report_file,
798
+ )
799
+ .split(path.sep)
800
+ .join("/"),
801
+ },
802
+ );
803
+ try {
804
+ if (assignment.assessment_mode === "incremental") {
805
+ progress = 15;
806
+ reportProgress("Calculating changed files and affected behavior");
807
+ }
808
+ const preparedAssignment = await prepareLocalAssessmentAssignment(
809
+ config.workspace,
810
+ assignment,
811
+ );
774
812
  let result = await loadAssessmentStageResult(
775
- config.workspace,
776
- assessmentId,
777
- stage,
778
- );
813
+ config.workspace,
814
+ assessmentId,
815
+ stage,
816
+ );
779
817
  if (result) {
780
- attachStoredCorrection(result, preparedAssignment);
781
- reportProgress("Recovered completed stage report from connector storage");
818
+ attachStoredCorrection(result, preparedAssignment);
819
+ reportProgress("Recovered completed stage report from connector storage");
782
820
  } else {
783
- result = await executeWorkspaceAssessment(
784
- preparedAssignment,
785
- config,
786
- assessmentAgentCallbacks,
787
- );
788
- await persistAssessmentStageResult(
821
+ const previousSessionId = await loadAssessmentStageSession(
789
822
  config.workspace,
790
823
  assessmentId,
791
- preparedAssignment,
792
- result,
793
- );
794
- }
795
- agentCompleted = true;
796
- assessmentState.phase = "delivering";
797
- assessmentState.progressPercent = 95;
798
- assessmentState.lastMessage =
799
- stage === "synthesis"
800
- ? "Delivering completed assessment bundle"
801
- : "Checkpointing connector-local stage result";
802
- const submitResult = (payload) =>
803
- fetch(
804
- assessmentResultUrl(
805
- config.server_url,
806
- config.connector_id,
807
- assessmentId,
808
- ),
809
- {
810
- method: "POST",
811
- headers: {
812
- "Content-Type": "application/json",
813
- Authorization: `Bearer ${config.token}`,
814
- },
815
- body: JSON.stringify(payload),
816
- signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
817
- },
818
- );
819
- const deliverResult = (payload) =>
820
- submitAssessmentResultWithRetry(payload, submitResult, {
821
- isActive: () => isActiveAssessment() && !stopped,
822
- onRetry: (error, delayMs) => {
823
- reportProgress("Completed result is waiting for EngineerOS", {
824
- milestone: false,
825
- });
826
- console.warn(
827
- `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
828
- `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
829
- );
830
- },
831
- });
832
- const repairedDelivery = await submitAssessmentWithValidationRepair(
833
- result,
834
- {
835
- submit: deliverResult,
836
- payloadFor: (candidate) =>
837
- stage === "synthesis"
838
- ? assessmentCompletionBundle(
839
- config.workspace,
840
- assessmentId,
841
- candidate,
842
- )
843
- : assessmentCheckpointPayload(candidate),
844
- rejectionFor: (response) =>
845
- describeRejectedResponse(response, "the assessment"),
846
- correct: async (candidate, rejection) => {
847
- agentCompleted = false;
848
- assessmentState.phase = "correcting";
849
- assessmentState.lastMessage = "Correcting a rejected stage result";
850
- if (!candidate.correctAfterRejection) {
851
- throw new Error(
852
- `${rejection} The connector could not start the required agent correction.`,
853
- );
854
- }
855
- return candidate.correctAfterRejection(rejection);
856
- },
857
- onCorrected: async (candidate) => {
858
- await removeAssessmentStageResult(
859
- config.workspace,
860
- assessmentId,
861
- stage,
862
- );
863
- const persisted = await persistAssessmentStageResult(
864
- config.workspace,
865
- assessmentId,
866
- preparedAssignment,
867
- candidate,
868
- );
869
- attachStoredCorrection(persisted, preparedAssignment);
870
- agentCompleted = true;
871
- assessmentState.phase = "delivering";
872
- assessmentState.lastMessage = "Delivering corrected stage result";
873
- return persisted;
874
- },
875
- },
876
- );
877
- result = repairedDelivery.result;
878
- const response = repairedDelivery.response;
879
- if (!response.ok) {
880
- throw new Error(
881
- await describeRejectedResponse(response, "the assessment"),
882
- );
883
- }
884
- accepted = true;
885
- if (stage === "synthesis") {
886
- await clearAssessmentSpool(config.workspace, assessmentId);
887
- console.log(
888
- `Workspace assessment ${assessmentId} was accepted by EngineerOS.`,
889
- );
890
- } else {
891
- console.log(
892
- `Workspace assessment stage ${stage} was checkpointed by EngineerOS.`,
893
- );
894
- }
895
- } catch (error) {
896
- const message = inactivityFailure || protocolFailureMessage(error);
897
- if (isActiveAssessment()) {
898
- failureReported = sendAssessmentMessage({
899
- type: "workspace.assessment.failed",
900
- assessment_id: assessmentId,
901
824
  stage,
902
- message,
903
- });
904
- }
905
- console.error(message);
906
- } finally {
907
- clearInterval(heartbeat);
908
- if (isActiveAssessment()) {
909
- activeAssessments.delete(assessmentState.key);
910
- const replayQueued = requeueInterruptedAssessment(
911
- assessments,
912
- assessmentState,
913
- { accepted, failureReported },
914
825
  );
915
- if (replayQueued) {
916
- console.log(`Restarting interrupted assessment stage ${stage}.`);
826
+ if (previousSessionId) {
827
+ reportProgress("Restoring interrupted agent session");
917
828
  }
918
- pump();
919
- }
920
- }
921
- }
922
-
923
- function formatAssessmentDuration(milliseconds) {
924
- const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
925
- const minutes = Math.floor(totalSeconds / 60);
926
- const seconds = totalSeconds % 60;
927
- return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
928
- }
929
-
930
- async function execute(assignment) {
931
- const runId = assignment.run_id;
932
- console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
933
- let progress = 10;
934
- const heartbeat = setInterval(() => {
935
- if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
936
- progress = Math.min(90, progress + 5);
937
- socket.send(
938
- JSON.stringify({
939
- type: "run.progress",
940
- run_id: runId,
941
- progress_percent: progress,
942
- message: "Agent is working",
943
- }),
944
- );
945
- }
946
- }, 15_000);
947
- try {
948
- const result = await executeAssignment(assignment, config, {
949
- onProcess: (child) => {
950
- if (active?.runId === runId) active.child = child;
951
- },
952
- onEvent: (event) => {
953
- const message =
954
- event.message || event.item?.text || event.type || "Agent is working";
955
- if (socket.readyState === WebSocket.OPEN) {
956
- socket.send(
957
- JSON.stringify({
958
- type: "run.progress",
959
- run_id: runId,
960
- progress_percent: progress,
961
- message: String(message).slice(0, 500),
962
- }),
963
- );
964
- }
965
- },
966
- });
967
- if (active?.cancelled) return;
968
- const response = await fetch(
969
- resultUrl(config.server_url, config.connector_id, runId),
970
- {
971
- method: "POST",
972
- headers: {
973
- "Content-Type": "application/json",
974
- Authorization: `Bearer ${config.token}`,
975
- },
976
- body: JSON.stringify(result),
977
- },
978
- );
979
- if (!response.ok) {
980
- throw new Error(await describeRejectedResponse(response, "the result"));
981
- }
982
- const integration = await applyAcceptedChange(
983
- config.workspace,
984
- assignment.base_revision,
985
- result.head_revision,
986
- );
987
- if (integration.applied) {
988
- console.log(
989
- `Run accepted and applied to the connected repository at ${integration.revision}.`,
990
- );
991
- } else {
992
- console.warn(
993
- `Run accepted but not applied locally. ${integration.reason}`,
994
- );
995
- console.warn(
996
- `The verified run workspace remains at ${result.run_workspace}.`,
997
- );
998
- }
999
- } catch (error) {
1000
- const message = protocolFailureMessage(error);
1001
- if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
1002
- socket.send(
1003
- JSON.stringify({
1004
- type: "run.failed",
1005
- run_id: runId,
1006
- message,
1007
- }),
829
+ result = await executeWorkspaceAssessment(
830
+ preparedAssignment,
831
+ config,
832
+ assessmentAgentCallbacks,
833
+ { previousSessionId },
1008
834
  );
1009
- }
1010
- if (!active?.cancelled) console.error(message);
1011
- } finally {
1012
- clearInterval(heartbeat);
1013
- active = null;
1014
- pump();
1015
- }
1016
- }
1017
-
1018
- function parseAgentArgs(value) {
1019
- if (!value) return [];
1020
- try {
1021
- const parsed = JSON.parse(value);
1022
- if (
1023
- !Array.isArray(parsed) ||
1024
- parsed.some((item) => typeof item !== "string")
1025
- ) {
1026
- throw new Error();
1027
- }
1028
- return parsed;
1029
- } catch {
1030
- fail(
1031
- '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
1032
- );
1033
- }
1034
- }
1035
-
1036
- function configuredAssessmentWorkerLimit(value) {
1037
- try {
1038
- return workspaceAssessmentWorkerLimit(value);
1039
- } catch (error) {
1040
- fail(error instanceof Error ? error.message : String(error));
1041
- }
1042
- }
1043
-
835
+ await persistAssessmentStageResult(
836
+ config.workspace,
837
+ assessmentId,
838
+ preparedAssignment,
839
+ result,
840
+ );
841
+ }
842
+ agentCompleted = true;
843
+ assessmentState.phase = "delivering";
844
+ assessmentState.progressPercent = 95;
845
+ assessmentState.lastMessage =
846
+ stage === "synthesis"
847
+ ? "Delivering completed assessment bundle"
848
+ : "Checkpointing connector-local stage result";
849
+ const submitResult = (payload) =>
850
+ fetch(
851
+ assessmentResultUrl(
852
+ config.server_url,
853
+ config.connector_id,
854
+ assessmentId,
855
+ ),
856
+ {
857
+ method: "POST",
858
+ headers: {
859
+ "Content-Type": "application/json",
860
+ Authorization: `Bearer ${config.token}`,
861
+ },
862
+ body: JSON.stringify(payload),
863
+ signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
864
+ },
865
+ );
866
+ const deliverResult = (payload) =>
867
+ submitAssessmentResultWithRetry(payload, submitResult, {
868
+ isActive: () => isActiveAssessment() && !stopped,
869
+ onRetry: (error, delayMs) => {
870
+ reportProgress("Completed result is waiting for EngineerOS", {
871
+ milestone: false,
872
+ });
873
+ console.warn(
874
+ `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
875
+ `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
876
+ );
877
+ },
878
+ });
879
+ const repairedDelivery = await submitAssessmentWithValidationRepair(
880
+ result,
881
+ {
882
+ submit: deliverResult,
883
+ payloadFor: (candidate) =>
884
+ stage === "synthesis"
885
+ ? assessmentCompletionBundle(
886
+ config.workspace,
887
+ assessmentId,
888
+ candidate,
889
+ )
890
+ : assessmentCheckpointPayload(candidate),
891
+ rejectionFor: (response) =>
892
+ describeRejectedResponse(response, "the assessment"),
893
+ correct: async (candidate, rejection) => {
894
+ agentCompleted = false;
895
+ assessmentState.phase = "correcting";
896
+ assessmentState.lastMessage = "Correcting a rejected stage result";
897
+ if (!candidate.correctAfterRejection) {
898
+ throw new Error(
899
+ `${rejection} The connector could not start the required agent correction.`,
900
+ );
901
+ }
902
+ return candidate.correctAfterRejection(rejection);
903
+ },
904
+ onCorrected: async (candidate) => {
905
+ await removeAssessmentStageResult(
906
+ config.workspace,
907
+ assessmentId,
908
+ stage,
909
+ );
910
+ const persisted = await persistAssessmentStageResult(
911
+ config.workspace,
912
+ assessmentId,
913
+ preparedAssignment,
914
+ candidate,
915
+ );
916
+ attachStoredCorrection(persisted, preparedAssignment);
917
+ agentCompleted = true;
918
+ assessmentState.phase = "delivering";
919
+ assessmentState.lastMessage = "Delivering corrected stage result";
920
+ return persisted;
921
+ },
922
+ },
923
+ );
924
+ result = repairedDelivery.result;
925
+ const response = repairedDelivery.response;
926
+ if (!response.ok) {
927
+ throw new Error(
928
+ await describeRejectedResponse(response, "the assessment"),
929
+ );
930
+ }
931
+ accepted = true;
932
+ if (stage === "synthesis") {
933
+ await clearAssessmentSpool(config.workspace, assessmentId);
934
+ console.log(
935
+ `Workspace assessment ${assessmentId} was accepted by EngineerOS.`,
936
+ );
937
+ } else {
938
+ console.log(
939
+ `Workspace assessment stage ${stage} was checkpointed by EngineerOS.`,
940
+ );
941
+ }
942
+ } catch (error) {
943
+ const message = inactivityFailure || protocolFailureMessage(error);
944
+ if (isActiveAssessment()) {
945
+ failureReported = sendAssessmentMessage({
946
+ type: "workspace.assessment.failed",
947
+ assessment_id: assessmentId,
948
+ stage,
949
+ message,
950
+ });
951
+ }
952
+ console.error(message);
953
+ } finally {
954
+ clearInterval(heartbeat);
955
+ if (isActiveAssessment()) {
956
+ activeAssessments.delete(assessmentState.key);
957
+ const replayQueued = requeueInterruptedAssessment(
958
+ assessments,
959
+ assessmentState,
960
+ { accepted, failureReported },
961
+ );
962
+ if (replayQueued) {
963
+ console.log(`Restarting interrupted assessment stage ${stage}.`);
964
+ }
965
+ pump();
966
+ }
967
+ }
968
+ }
969
+
970
+ function formatAssessmentDuration(milliseconds) {
971
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
972
+ const minutes = Math.floor(totalSeconds / 60);
973
+ const seconds = totalSeconds % 60;
974
+ return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
975
+ }
976
+
977
+ async function execute(assignment) {
978
+ const runId = assignment.run_id;
979
+ console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
980
+ let progress = 10;
981
+ const heartbeat = setInterval(() => {
982
+ if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
983
+ progress = Math.min(90, progress + 5);
984
+ socket.send(
985
+ JSON.stringify({
986
+ type: "run.progress",
987
+ run_id: runId,
988
+ progress_percent: progress,
989
+ message: "Agent is working",
990
+ }),
991
+ );
992
+ }
993
+ }, 15_000);
994
+ try {
995
+ const result = await executeAssignment(assignment, config, {
996
+ onProcess: (child) => {
997
+ if (active?.runId === runId) active.child = child;
998
+ },
999
+ onEvent: (event) => {
1000
+ const message =
1001
+ event.message || event.item?.text || event.type || "Agent is working";
1002
+ if (socket.readyState === WebSocket.OPEN) {
1003
+ socket.send(
1004
+ JSON.stringify({
1005
+ type: "run.progress",
1006
+ run_id: runId,
1007
+ progress_percent: progress,
1008
+ message: String(message).slice(0, 500),
1009
+ }),
1010
+ );
1011
+ }
1012
+ },
1013
+ });
1014
+ if (active?.cancelled) return;
1015
+ const response = await fetch(
1016
+ resultUrl(config.server_url, config.connector_id, runId),
1017
+ {
1018
+ method: "POST",
1019
+ headers: {
1020
+ "Content-Type": "application/json",
1021
+ Authorization: `Bearer ${config.token}`,
1022
+ },
1023
+ body: JSON.stringify(result),
1024
+ },
1025
+ );
1026
+ if (!response.ok) {
1027
+ throw new Error(await describeRejectedResponse(response, "the result"));
1028
+ }
1029
+ const integration = await applyAcceptedChange(
1030
+ config.workspace,
1031
+ assignment.base_revision,
1032
+ result.head_revision,
1033
+ );
1034
+ if (integration.applied) {
1035
+ console.log(
1036
+ `Run accepted and applied to the connected repository at ${integration.revision}.`,
1037
+ );
1038
+ } else {
1039
+ console.warn(
1040
+ `Run accepted but not applied locally. ${integration.reason}`,
1041
+ );
1042
+ console.warn(
1043
+ `The verified run workspace remains at ${result.run_workspace}.`,
1044
+ );
1045
+ }
1046
+ } catch (error) {
1047
+ const message = protocolFailureMessage(error);
1048
+ if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
1049
+ socket.send(
1050
+ JSON.stringify({
1051
+ type: "run.failed",
1052
+ run_id: runId,
1053
+ message,
1054
+ }),
1055
+ );
1056
+ }
1057
+ if (!active?.cancelled) console.error(message);
1058
+ } finally {
1059
+ clearInterval(heartbeat);
1060
+ active = null;
1061
+ pump();
1062
+ }
1063
+ }
1064
+
1065
+ function parseAgentArgs(value) {
1066
+ if (!value) return [];
1067
+ try {
1068
+ const parsed = JSON.parse(value);
1069
+ if (
1070
+ !Array.isArray(parsed) ||
1071
+ parsed.some((item) => typeof item !== "string")
1072
+ ) {
1073
+ throw new Error();
1074
+ }
1075
+ return parsed;
1076
+ } catch {
1077
+ fail(
1078
+ '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
1079
+ );
1080
+ }
1081
+ }
1082
+
1044
1083
  function fail(message) {
1045
- console.error(message);
1046
- process.exit(1);
1047
- }
1084
+ console.error(message);
1085
+ process.exit(1);
1086
+ }