@engineeros/connector 0.15.2 → 0.15.3

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