@engineeros/connector 0.10.4 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,779 +1,790 @@
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
- import {
15
- assessmentProgressMessage,
16
- assessmentStreamDelta,
17
- applyAcceptedChange,
18
- executeAssignment,
19
- executeConnectedPrompt,
20
- executeWorkspaceAssessment,
21
- inspectCodingAgent,
22
- promptStreamEvent,
23
- stopProcess,
24
- workspaceSnapshot,
25
- } from "../src/runner.mjs";
26
- import { disposeAcpRuntimes } from "../src/acp-client.mjs";
27
- import {
28
- installRegisteredAgent,
29
- inspectRegisteredAgent,
30
- registeredAgentConfig,
31
- registeredAgents,
32
- } from "../src/agent-registry.mjs";
33
- import {
34
- describeWebSocketError,
35
- startConnectionWatchdog,
36
- } from "../src/connection.mjs";
37
- import { advertisedCapabilities } from "../src/capabilities.mjs";
38
- import { parseConnectorArgs } from "../src/cli-args.mjs";
39
- import { runMcpServer } from "../src/mcp-server.mjs";
40
- import packageJson from "../package.json" with { type: "json" };
41
-
42
- const { command, positional, flags } = parseConnectorArgs(
43
- process.argv.slice(2),
44
- );
45
-
46
- if (command === "agents") {
47
- let agents;
48
- try {
49
- agents = await registeredAgents();
50
- } catch (error) {
51
- fail(error instanceof Error ? error.message : String(error));
52
- }
53
- const statuses = await Promise.all(
54
- agents.map(async (agent) => {
55
- try {
56
- await inspectRegisteredAgent(agent.id);
57
- return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
58
- } catch (error) {
59
- const detail = error instanceof Error ? error.message : String(error);
60
- return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
61
- }
62
- }),
63
- );
64
- for (const status of statuses) {
65
- console.log(status);
66
- }
67
- process.exit(0);
68
- }
69
-
70
- if (command === "agent") {
71
- const agentId = positional[0];
72
- const action = positional[1] || "check";
73
- if (!agentId || !["check", "install"].includes(action)) {
74
- fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
75
- }
76
- try {
77
- const installed =
78
- action === "install"
79
- ? await installRegisteredAgent(agentId)
80
- : await inspectRegisteredAgent(agentId);
81
- console.log(
82
- `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
83
- );
84
- } catch (error) {
85
- fail(error instanceof Error ? error.message : String(error));
86
- }
87
- process.exit(0);
88
- }
89
-
90
- if (command === "mcp") {
91
- const config = await loadConfig(flags.workspace || process.cwd());
92
- if (!config) fail("This workspace is not paired with EngineerOS.");
93
- const runId = flags["run-id"] || positional[0];
94
- if (!runId)
95
- fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
96
- await runMcpServer({
97
- config,
98
- runId,
99
- version: packageJson.version,
100
- input: process.stdin,
101
- output: process.stdout,
102
- });
103
- process.exit(0);
104
- }
105
-
106
- if (command === "status") {
107
- const config = await loadConfig(flags.workspace || process.cwd());
108
- console.log(
109
- config
110
- ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
111
- : "Not paired",
112
- );
113
- process.exit(config ? 0 : 1);
114
- }
115
-
116
- let config;
117
- let firstMessage;
118
- let existingConfig;
119
- if (command === "pair") {
120
- const pairingCode = positional[0];
121
- if (!pairingCode)
122
- fail(
123
- "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
124
- );
125
- const url = flags.url;
126
- if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
127
- if (flags.agent && flags["agent-command"]) {
128
- fail("Use either --agent or --agent-command, not both.");
129
- }
130
- let agentConfig = {};
131
- try {
132
- if (flags.agent) {
133
- await inspectRegisteredAgent(flags.agent);
134
- agentConfig = await registeredAgentConfig(flags.agent);
135
- }
136
- } catch (error) {
137
- fail(error instanceof Error ? error.message : String(error));
138
- }
139
- config = {
140
- server_url: socketUrl(url),
141
- workspace: path.resolve(flags.workspace || process.cwd()),
142
- onboard: flags.onboard === true,
143
- onboarding_pending: flags.onboard === true,
144
- agent_protocol: flags["agent-command"] ? "acp" : "codex",
145
- agent_command: flags["agent-command"] || null,
146
- agent_args: parseAgentArgs(flags["agent-args"]),
147
- agent_name: flags["agent-name"] || null,
148
- ...agentConfig,
149
- skip_git_repo_check: flags["skip-git-repo-check"] === true,
150
- name:
151
- flags.name ||
152
- `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
153
- };
154
- existingConfig = await loadConfig(config.workspace);
155
- firstMessage = {
156
- type: "pair",
157
- pairing_code: pairingCode,
158
- name: config.name,
159
- capabilities: {},
160
- ...connectorResumeCredentials(existingConfig, config.server_url),
161
- };
162
- } else if (command === "start") {
163
- config = await loadConfig(flags.workspace || process.cwd());
164
- if (!config)
165
- fail(
166
- "This connector is not paired. Create a pairing command in EngineerOS first.",
167
- );
168
- firstMessage = {
169
- type: "authenticate",
170
- connector_id: config.connector_id,
171
- token: config.token,
172
- };
173
- } else {
174
- fail(
175
- "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
176
- );
177
- }
178
-
179
- let codingAgent;
180
- try {
181
- codingAgent = await inspectCodingAgent(config, config.workspace);
182
- } catch (error) {
183
- fail(error instanceof Error ? error.message : String(error));
184
- }
185
- console.log(
186
- `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
187
- );
188
- const capabilities = advertisedCapabilities(config, codingAgent);
189
- firstMessage.capabilities = capabilities;
190
-
191
- let stopped = false;
192
- let active = null;
193
- const available = [];
194
- const assessments = [];
195
- const activePrompts = new Map();
196
- let socket;
197
- let pingTimer;
198
- let reconnectTimer;
199
- let clearConnectionWatchdog;
200
- let reconnectDelay = 1_000;
201
- let connectionRejected = false;
202
- let lastConnectionError;
203
- let snapshotInFlight = false;
204
-
205
- process.on("SIGINT", async () => {
206
- stopped = true;
207
- clearConnectionWatchdog?.();
208
- clearTimeout(reconnectTimer);
209
- clearInterval(pingTimer);
210
- await stopProcess(active?.child);
211
- await Promise.all([...activePrompts.values()].map(cancelPrompt));
212
- await disposeAcpRuntimes();
213
- socket?.close();
214
- process.exit(0);
215
- });
216
-
217
- await connect();
218
-
219
- async function connect() {
220
- console.log(`Connecting ${config.name} to ${config.server_url}`);
221
- connectionRejected = false;
222
- lastConnectionError = undefined;
223
- socket = new WebSocket(config.server_url);
224
- clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
225
- onTimeout: (message) => {
226
- lastConnectionError = message;
227
- console.error(message);
228
- scheduleReconnect();
229
- },
230
- });
231
- socket.addEventListener("open", () =>
232
- socket.send(JSON.stringify(firstMessage)),
233
- );
234
- socket.addEventListener("message", async (event) => {
235
- const message = JSON.parse(String(event.data));
236
- if (message.type === "paired") {
237
- clearConnectionWatchdog?.();
238
- const reusedConnector =
239
- existingConfig?.connector_id === message.connector.id;
240
- config = mergePairedConfig(
241
- config,
242
- existingConfig,
243
- message.connector.id,
244
- message.token,
245
- );
246
- await saveConfig(config);
247
- firstMessage = {
248
- type: "authenticate",
249
- connector_id: config.connector_id,
250
- token: config.token,
251
- capabilities,
252
- };
253
- console.log(
254
- reusedConnector
255
- ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
256
- : `Paired. Connector ${config.connector_id} is online.`,
257
- );
258
- reconnectDelay = 1_000;
259
- startPings();
260
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
261
- return;
262
- }
263
- if (message.type === "authenticated") {
264
- clearConnectionWatchdog?.();
265
- console.log("Connected and waiting for EngineerOS runs.");
266
- reconnectDelay = 1_000;
267
- startPings();
268
- if (config.onboarding_pending) void submitWorkspaceSnapshot();
269
- return;
270
- }
271
- if (message.type === "workspace.refresh") {
272
- void submitWorkspaceSnapshot();
273
- return;
274
- }
275
- if (message.type === "workspace.assessment") {
276
- if (
277
- active?.runId !== message.assessment_id &&
278
- !assessments.some(
279
- (candidate) => candidate.assessment_id === message.assessment_id,
280
- )
281
- ) {
282
- assessments.push(message);
283
- }
284
- pump();
285
- return;
286
- }
287
- if (message.type === "prompt.execute") {
288
- if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
289
- return;
290
- }
291
- if (message.type === "prompt.cancel") {
292
- const promptState = activePrompts.get(message.prompt_id);
293
- if (promptState) await cancelPrompt(promptState);
294
- return;
295
- }
296
- if (message.type === "run.available") {
297
- if (!available.includes(message.run_id)) available.push(message.run_id);
298
- pump();
299
- return;
300
- }
301
- if (message.type === "run.assignment") {
302
- await execute(message);
303
- return;
304
- }
305
- if (message.type === "run.cancelled" && active?.runId === message.run_id) {
306
- console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
307
- active.cancelled = true;
308
- await stopProcess(active.child);
309
- return;
310
- }
311
- if (message.type === "connector.revoked") {
312
- stopped = true;
313
- console.error("This connector was revoked in EngineerOS.");
314
- socket.close();
315
- return;
316
- }
317
- if (message.type === "run.error") {
318
- console.error(`EngineerOS: ${message.message}`);
319
- if (!message.run_id) snapshotInFlight = false;
320
- if (active?.runId === message.run_id && !active.child) {
321
- active = null;
322
- pump();
323
- }
324
- }
325
- if (message.type === "connection.error") {
326
- connectionRejected = true;
327
- console.error(`EngineerOS: ${message.message}`);
328
- }
329
- });
330
- socket.addEventListener("close", (event) => {
331
- clearConnectionWatchdog?.();
332
- clearInterval(pingTimer);
333
- for (const promptState of activePrompts.values())
334
- void cancelPrompt(promptState);
335
- if (event.code === 4001 && active?.kind === "assessment") {
336
- void stopProcess(active.child);
337
- }
338
- if (connectionRejected) {
339
- stopped = true;
340
- console.error(
341
- "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
342
- );
343
- return;
344
- }
345
- if (stopped) return;
346
- scheduleReconnect();
347
- });
348
- socket.addEventListener("error", (event) => {
349
- lastConnectionError = describeWebSocketError(event);
350
- console.error(
351
- `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
352
- );
353
- });
354
- }
355
-
356
- function scheduleReconnect() {
357
- if (stopped || connectionRejected || reconnectTimer) return;
358
- const detail = lastConnectionError
359
- ? ` Last error: ${lastConnectionError}`
360
- : "";
361
- console.error(
362
- `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
363
- );
364
- reconnectTimer = setTimeout(() => {
365
- reconnectTimer = undefined;
366
- void connect();
367
- }, reconnectDelay);
368
- reconnectDelay = Math.min(30_000, reconnectDelay * 2);
369
- }
370
-
371
- async function submitWorkspaceSnapshot() {
372
- if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
373
- snapshotInFlight = true;
374
- console.log("Inspecting the workspace without executing its code.");
375
- try {
376
- const snapshot = await workspaceSnapshot(config.workspace);
377
- const response = await fetch(
378
- workspaceUrl(config.server_url, config.connector_id),
379
- {
380
- method: "POST",
381
- headers: {
382
- "Content-Type": "application/json",
383
- Authorization: `Bearer ${config.token}`,
384
- },
385
- body: JSON.stringify(snapshot),
386
- },
387
- );
388
- if (!response.ok) {
389
- throw new Error(
390
- `EngineerOS rejected the workspace (${response.status}): ${await response.text()}`,
391
- );
392
- }
393
- const result = await response.json();
394
- config = { ...config, onboarding_pending: false };
395
- await saveConfig(config);
396
- console.log(
397
- `Sent ${snapshot.file_count} safe file(s); ${snapshot.excluded_file_count} sensitive or generated path(s) excluded.`,
398
- );
399
- console.log(
400
- `Workspace inventory registered as ${result.workspace_kind}. Assessment queued.`,
401
- );
402
- snapshotInFlight = false;
403
- } catch (error) {
404
- snapshotInFlight = false;
405
- console.error(
406
- `Workspace assessment failed: ${error instanceof Error ? error.message : String(error)}`,
407
- );
408
- }
409
- }
410
-
411
- function startPings() {
412
- clearInterval(pingTimer);
413
- pingTimer = setInterval(() => {
414
- if (socket.readyState === WebSocket.OPEN) {
415
- socket.send(
416
- JSON.stringify({
417
- type: "ping",
418
- active_run_id: active?.kind === "goal" ? active.runId : null,
419
- }),
420
- );
421
- }
422
- }, 10_000);
423
- }
424
-
425
- function sendPromptEvent(promptId, event) {
426
- if (!event || socket.readyState !== WebSocket.OPEN) return;
427
- const payload = {
428
- type: "prompt.event",
429
- prompt_id: promptId,
430
- kind: event.kind,
431
- };
432
- if (event.message) payload.message = String(event.message).slice(0, 500);
433
- if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
434
- if (event.status) payload.status = String(event.status).slice(0, 100);
435
- socket.send(JSON.stringify(payload));
436
- }
437
-
438
- function pump() {
439
- if (active || socket.readyState !== WebSocket.OPEN) return;
440
- const assessment = assessments.shift();
441
- if (assessment) {
442
- active = {
443
- kind: "assessment",
444
- runId: assessment.assessment_id,
445
- child: null,
446
- };
447
- void executeAssessment(assessment);
448
- return;
449
- }
450
- const runId = available.shift();
451
- if (!runId) return;
452
- active = { kind: "goal", runId, child: null, cancelled: false };
453
- socket.send(
454
- JSON.stringify({
455
- type: "run.claim",
456
- run_id: runId,
457
- runner_name: codingAgent.name,
458
- metadata: {
459
- hostname: os.hostname(),
460
- workspace_name: path.basename(config.workspace),
461
- },
462
- }),
463
- );
464
- }
465
-
466
- async function executePrompt(assignment) {
467
- const promptId = assignment.prompt_id;
468
- const promptState = {
469
- runId: promptId,
470
- child: null,
471
- controller: null,
472
- cancelled: false,
473
- };
474
- activePrompts.set(promptId, promptState);
475
- let lastEvent;
476
- const reportEvent = (event) => {
477
- const serialized = event ? JSON.stringify(event) : null;
478
- if (
479
- !serialized ||
480
- serialized === lastEvent ||
481
- socket.readyState !== WebSocket.OPEN ||
482
- activePrompts.get(promptId) !== promptState
483
- ) {
484
- return;
485
- }
486
- lastEvent = serialized;
487
- sendPromptEvent(promptId, event);
488
- };
489
- sendPromptEvent(promptId, {
490
- kind: "status",
491
- message: "Agent started this request",
492
- });
493
- console.log(
494
- `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
495
- );
496
- try {
497
- const result = await executeConnectedPrompt(assignment, config, {
498
- onProcess: (child) => {
499
- if (activePrompts.get(promptId) === promptState)
500
- promptState.child = child;
501
- },
502
- onController: (controller) => {
503
- if (activePrompts.get(promptId) === promptState) {
504
- promptState.controller = controller;
505
- }
506
- },
507
- onEvent: (event) => reportEvent(promptStreamEvent(event)),
508
- });
509
- config = {
510
- ...config,
511
- sessions: {
512
- ...(config.sessions || {}),
513
- [result.sessionKey]: result.sessionId,
514
- },
515
- };
516
- await saveConfig(config);
517
- if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
518
- socket.send(
519
- JSON.stringify({
520
- type: "prompt.completed",
521
- prompt_id: promptId,
522
- content: result.content,
523
- model: result.model,
524
- session_id: result.sessionId,
525
- }),
526
- );
527
- } catch (error) {
528
- if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
529
- socket.send(
530
- JSON.stringify({
531
- type: "prompt.failed",
532
- prompt_id: promptId,
533
- message: error instanceof Error ? error.message : String(error),
534
- }),
535
- );
536
- }
537
- if (!promptState.cancelled) {
538
- console.error(error instanceof Error ? error.message : String(error));
539
- }
540
- } finally {
541
- if (activePrompts.get(promptId) === promptState)
542
- activePrompts.delete(promptId);
543
- }
544
- }
545
-
546
- async function cancelPrompt(promptState) {
547
- promptState.cancelled = true;
548
- if (typeof promptState.controller?.cancel === "function") {
549
- await promptState.controller.cancel();
550
- return;
551
- }
552
- await stopProcess(promptState.child);
553
- }
554
-
555
- async function executeAssessment(assignment) {
556
- const assessmentId = assignment.assessment_id;
557
- console.log(`Agent is assessing the workspace (${assessmentId}).`);
558
- let progress = 10;
559
- let outputBuffer = "";
560
- let outputLength = 0;
561
- let outputTimer;
562
- const reportedMilestones = new Set();
563
- const reportProgress = (message, { milestone = true } = {}) => {
564
- if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId)
565
- return;
566
- if (milestone && reportedMilestones.has(message)) return;
567
- if (milestone) reportedMilestones.add(message);
568
- socket.send(
569
- JSON.stringify({
570
- type: "workspace.assessment.progress",
571
- assessment_id: assessmentId,
572
- progress_percent: progress,
573
- message: String(message).slice(0, 500),
574
- }),
575
- );
576
- };
577
- const flushOutput = () => {
578
- if (outputTimer) clearTimeout(outputTimer);
579
- outputTimer = undefined;
580
- const output = outputBuffer;
581
- outputBuffer = "";
582
- if (
583
- !output ||
584
- socket.readyState !== WebSocket.OPEN ||
585
- active?.runId !== assessmentId
586
- ) {
587
- return;
588
- }
589
- for (let offset = 0; offset < output.length; offset += 50_000) {
590
- socket.send(
591
- JSON.stringify({
592
- type: "workspace.assessment.output",
593
- assessment_id: assessmentId,
594
- delta: output.slice(offset, offset + 50_000),
595
- }),
596
- );
597
- }
598
- };
599
- const reportOutput = (delta) => {
600
- const remaining = 120_000 - outputLength;
601
- if (!delta || remaining <= 0) return;
602
- const bounded = String(delta).slice(0, remaining);
603
- outputBuffer += bounded;
604
- outputLength += bounded.length;
605
- if (outputBuffer.length >= 50_000) {
606
- flushOutput();
607
- } else if (!outputTimer) {
608
- outputTimer = setTimeout(flushOutput, 500);
609
- }
610
- };
611
- reportProgress("Starting read-only repository assessment");
612
- const heartbeat = setInterval(() => {
613
- progress = Math.min(90, progress + 5);
614
- reportProgress("Assessment in progress", { milestone: false });
615
- }, 15_000);
616
- try {
617
- if (assignment.assessment_mode === "incremental") {
618
- progress = 15;
619
- reportProgress("Calculating changed files and affected evidence");
620
- }
621
- const result = await executeWorkspaceAssessment(assignment, config, {
622
- onProcess: (child) => {
623
- if (active?.runId === assessmentId) active.child = child;
624
- },
625
- onEvent: (event) => {
626
- reportOutput(assessmentStreamDelta(event));
627
- const message = assessmentProgressMessage(event);
628
- if (message) reportProgress(message);
629
- },
630
- });
631
- flushOutput();
632
- const response = await fetch(
633
- assessmentResultUrl(config.server_url, config.connector_id, assessmentId),
634
- {
635
- method: "POST",
636
- headers: {
637
- "Content-Type": "application/json",
638
- Authorization: `Bearer ${config.token}`,
639
- },
640
- body: JSON.stringify(result),
641
- },
642
- );
643
- if (!response.ok) {
644
- throw new Error(
645
- `EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
646
- );
647
- }
648
- console.log("Workspace assessment is current in EngineerOS.");
649
- } catch (error) {
650
- flushOutput();
651
- if (socket.readyState === WebSocket.OPEN) {
652
- socket.send(
653
- JSON.stringify({
654
- type: "workspace.assessment.failed",
655
- assessment_id: assessmentId,
656
- message: error instanceof Error ? error.message : String(error),
657
- }),
658
- );
659
- }
660
- console.error(error instanceof Error ? error.message : String(error));
661
- } finally {
662
- if (outputTimer) clearTimeout(outputTimer);
663
- clearInterval(heartbeat);
664
- active = null;
665
- pump();
666
- }
667
- }
668
-
669
- async function execute(assignment) {
670
- const runId = assignment.run_id;
671
- console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
672
- let progress = 10;
673
- const heartbeat = setInterval(() => {
674
- if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
675
- progress = Math.min(90, progress + 5);
676
- socket.send(
677
- JSON.stringify({
678
- type: "run.progress",
679
- run_id: runId,
680
- progress_percent: progress,
681
- message: "Agent is working",
682
- }),
683
- );
684
- }
685
- }, 15_000);
686
- try {
687
- const result = await executeAssignment(assignment, config, {
688
- onProcess: (child) => {
689
- if (active?.runId === runId) active.child = child;
690
- },
691
- onEvent: (event) => {
692
- const message =
693
- event.message || event.item?.text || event.type || "Agent is working";
694
- if (socket.readyState === WebSocket.OPEN) {
695
- socket.send(
696
- JSON.stringify({
697
- type: "run.progress",
698
- run_id: runId,
699
- progress_percent: progress,
700
- message: String(message).slice(0, 500),
701
- }),
702
- );
703
- }
704
- },
705
- });
706
- if (active?.cancelled) return;
707
- const response = await fetch(
708
- resultUrl(config.server_url, config.connector_id, runId),
709
- {
710
- method: "POST",
711
- headers: {
712
- "Content-Type": "application/json",
713
- Authorization: `Bearer ${config.token}`,
714
- },
715
- body: JSON.stringify(result),
716
- },
717
- );
718
- if (!response.ok)
719
- throw new Error(
720
- `EngineerOS rejected the result (${response.status}): ${await response.text()}`,
721
- );
722
- const integration = await applyAcceptedChange(
723
- config.workspace,
724
- assignment.base_revision,
725
- result.head_revision,
726
- );
727
- if (integration.applied) {
728
- console.log(
729
- `Run accepted and applied to the connected repository at ${integration.revision}.`,
730
- );
731
- } else {
732
- console.warn(
733
- `Run accepted but not applied locally. ${integration.reason}`,
734
- );
735
- console.warn(
736
- `The verified run workspace remains at ${result.run_workspace}.`,
737
- );
738
- }
739
- } catch (error) {
740
- if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
741
- socket.send(
742
- JSON.stringify({
743
- type: "run.failed",
744
- run_id: runId,
745
- message: error instanceof Error ? error.message : String(error),
746
- }),
747
- );
748
- }
749
- if (!active?.cancelled)
750
- console.error(error instanceof Error ? error.message : String(error));
751
- } finally {
752
- clearInterval(heartbeat);
753
- active = null;
754
- pump();
755
- }
756
- }
757
-
758
- function parseAgentArgs(value) {
759
- if (!value) return [];
760
- try {
761
- const parsed = JSON.parse(value);
762
- if (
763
- !Array.isArray(parsed) ||
764
- parsed.some((item) => typeof item !== "string")
765
- ) {
766
- throw new Error();
767
- }
768
- return parsed;
769
- } catch {
770
- fail(
771
- '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
772
- );
773
- }
774
- }
775
-
776
- function fail(message) {
777
- console.error(message);
778
- process.exit(1);
779
- }
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
+ import {
15
+ assessmentProgressMessage,
16
+ assessmentStreamDelta,
17
+ applyAcceptedChange,
18
+ executeAssignment,
19
+ executeConnectedPrompt,
20
+ executeWorkspaceAssessment,
21
+ inspectCodingAgent,
22
+ promptStreamEvent,
23
+ stopProcess,
24
+ workspaceSnapshot,
25
+ } from "../src/runner.mjs";
26
+ import { disposeAcpRuntimes } from "../src/acp-client.mjs";
27
+ import {
28
+ installRegisteredAgent,
29
+ inspectRegisteredAgent,
30
+ registeredAgentConfig,
31
+ registeredAgents,
32
+ } from "../src/agent-registry.mjs";
33
+ import {
34
+ describeWebSocketError,
35
+ startConnectionWatchdog,
36
+ } from "../src/connection.mjs";
37
+ import { advertisedCapabilities } from "../src/capabilities.mjs";
38
+ import { parseConnectorArgs } from "../src/cli-args.mjs";
39
+ import { runMcpServer } from "../src/mcp-server.mjs";
40
+ import packageJson from "../package.json" with { type: "json" };
41
+
42
+ const { command, positional, flags } = parseConnectorArgs(
43
+ process.argv.slice(2),
44
+ );
45
+
46
+ if (command === "agents") {
47
+ let agents;
48
+ try {
49
+ agents = await registeredAgents();
50
+ } catch (error) {
51
+ fail(error instanceof Error ? error.message : String(error));
52
+ }
53
+ const statuses = await Promise.all(
54
+ agents.map(async (agent) => {
55
+ try {
56
+ await inspectRegisteredAgent(agent.id);
57
+ return `${agent.id}: ready (${agent.name} ${agent.version}, ${agent.distribution_type})`;
58
+ } catch (error) {
59
+ const detail = error instanceof Error ? error.message : String(error);
60
+ return `${agent.id}: setup needed (${agent.name} ${agent.version}, ${agent.distribution_type}) - ${detail}`;
61
+ }
62
+ }),
63
+ );
64
+ for (const status of statuses) {
65
+ console.log(status);
66
+ }
67
+ process.exit(0);
68
+ }
69
+
70
+ if (command === "agent") {
71
+ const agentId = positional[0];
72
+ const action = positional[1] || "check";
73
+ if (!agentId || !["check", "install"].includes(action)) {
74
+ fail("Usage: engineeros-connector agent AGENT_ID [check|install]");
75
+ }
76
+ try {
77
+ const installed =
78
+ action === "install"
79
+ ? await installRegisteredAgent(agentId)
80
+ : await inspectRegisteredAgent(agentId);
81
+ console.log(
82
+ `${installed.name} is ready (${installed.version}, ${installed.distribution}).`,
83
+ );
84
+ } catch (error) {
85
+ fail(error instanceof Error ? error.message : String(error));
86
+ }
87
+ process.exit(0);
88
+ }
89
+
90
+ if (command === "mcp") {
91
+ const config = await loadConfig(flags.workspace || process.cwd());
92
+ if (!config) fail("This workspace is not paired with EngineerOS.");
93
+ const runId = flags["run-id"] || positional[0];
94
+ if (!runId)
95
+ fail("Usage: engineeros-connector mcp --run-id RUN_ID [--workspace PATH]");
96
+ await runMcpServer({
97
+ config,
98
+ runId,
99
+ version: packageJson.version,
100
+ input: process.stdin,
101
+ output: process.stdout,
102
+ });
103
+ process.exit(0);
104
+ }
105
+
106
+ if (command === "status") {
107
+ const config = await loadConfig(flags.workspace || process.cwd());
108
+ console.log(
109
+ config
110
+ ? `Paired as ${config.name} (${config.connector_id}) for ${config.workspace}`
111
+ : "Not paired",
112
+ );
113
+ process.exit(config ? 0 : 1);
114
+ }
115
+
116
+ let config;
117
+ let firstMessage;
118
+ let existingConfig;
119
+ if (command === "pair") {
120
+ const pairingCode = positional[0];
121
+ if (!pairingCode)
122
+ fail(
123
+ "Usage: engineeros-connector pair CODE --url URL [--agent AGENT_ID] [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
124
+ );
125
+ const url = flags.url;
126
+ if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
127
+ if (flags.agent && flags["agent-command"]) {
128
+ fail("Use either --agent or --agent-command, not both.");
129
+ }
130
+ let agentConfig = {};
131
+ try {
132
+ if (flags.agent) {
133
+ await inspectRegisteredAgent(flags.agent);
134
+ agentConfig = await registeredAgentConfig(flags.agent);
135
+ }
136
+ } catch (error) {
137
+ fail(error instanceof Error ? error.message : String(error));
138
+ }
139
+ config = {
140
+ server_url: socketUrl(url),
141
+ workspace: path.resolve(flags.workspace || process.cwd()),
142
+ onboard: flags.onboard === true,
143
+ onboarding_pending: flags.onboard === true,
144
+ agent_protocol: flags["agent-command"] ? "acp" : "codex",
145
+ agent_command: flags["agent-command"] || null,
146
+ agent_args: parseAgentArgs(flags["agent-args"]),
147
+ agent_name: flags["agent-name"] || null,
148
+ ...agentConfig,
149
+ skip_git_repo_check: flags["skip-git-repo-check"] === true,
150
+ name:
151
+ flags.name ||
152
+ `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
153
+ };
154
+ existingConfig = await loadConfig(config.workspace);
155
+ firstMessage = {
156
+ type: "pair",
157
+ pairing_code: pairingCode,
158
+ name: config.name,
159
+ capabilities: {},
160
+ ...connectorResumeCredentials(existingConfig, config.server_url),
161
+ };
162
+ } else if (command === "start") {
163
+ config = await loadConfig(flags.workspace || process.cwd());
164
+ if (!config)
165
+ fail(
166
+ "This connector is not paired. Create a pairing command in EngineerOS first.",
167
+ );
168
+ firstMessage = {
169
+ type: "authenticate",
170
+ connector_id: config.connector_id,
171
+ token: config.token,
172
+ };
173
+ } else {
174
+ fail(
175
+ "Use `engineeros-connector pair`, `start`, `status`, `agents`, `agent`, or `mcp`.",
176
+ );
177
+ }
178
+
179
+ let codingAgent;
180
+ try {
181
+ codingAgent = await inspectCodingAgent(config, config.workspace);
182
+ } catch (error) {
183
+ fail(error instanceof Error ? error.message : String(error));
184
+ }
185
+ console.log(
186
+ `Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`,
187
+ );
188
+ const capabilities = advertisedCapabilities(config, codingAgent);
189
+ firstMessage.capabilities = capabilities;
190
+
191
+ let stopped = false;
192
+ let active = null;
193
+ const available = [];
194
+ const assessments = [];
195
+ const activePrompts = new Map();
196
+ let socket;
197
+ let pingTimer;
198
+ let reconnectTimer;
199
+ let clearConnectionWatchdog;
200
+ let reconnectDelay = 1_000;
201
+ let connectionRejected = false;
202
+ let lastConnectionError;
203
+ let snapshotInFlight = false;
204
+
205
+ process.on("SIGINT", async () => {
206
+ stopped = true;
207
+ clearConnectionWatchdog?.();
208
+ clearTimeout(reconnectTimer);
209
+ clearInterval(pingTimer);
210
+ await stopProcess(active?.child);
211
+ await Promise.all([...activePrompts.values()].map(cancelPrompt));
212
+ await disposeAcpRuntimes();
213
+ socket?.close();
214
+ process.exit(0);
215
+ });
216
+
217
+ await connect();
218
+
219
+ async function connect() {
220
+ console.log(`Connecting ${config.name} to ${config.server_url}`);
221
+ connectionRejected = false;
222
+ lastConnectionError = undefined;
223
+ socket = new WebSocket(config.server_url);
224
+ clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
225
+ onTimeout: (message) => {
226
+ lastConnectionError = message;
227
+ console.error(message);
228
+ scheduleReconnect();
229
+ },
230
+ });
231
+ socket.addEventListener("open", () =>
232
+ socket.send(JSON.stringify(firstMessage)),
233
+ );
234
+ socket.addEventListener("message", async (event) => {
235
+ const message = JSON.parse(String(event.data));
236
+ if (message.type === "paired") {
237
+ clearConnectionWatchdog?.();
238
+ const reusedConnector =
239
+ existingConfig?.connector_id === message.connector.id;
240
+ config = mergePairedConfig(
241
+ config,
242
+ existingConfig,
243
+ message.connector.id,
244
+ message.token,
245
+ );
246
+ await saveConfig(config);
247
+ firstMessage = {
248
+ type: "authenticate",
249
+ connector_id: config.connector_id,
250
+ token: config.token,
251
+ capabilities,
252
+ };
253
+ console.log(
254
+ reusedConnector
255
+ ? `Reconnected existing workspace connector ${config.connector_id}; assessment history is preserved.`
256
+ : `Paired. Connector ${config.connector_id} is online.`,
257
+ );
258
+ reconnectDelay = 1_000;
259
+ startPings();
260
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
261
+ return;
262
+ }
263
+ if (message.type === "authenticated") {
264
+ clearConnectionWatchdog?.();
265
+ console.log("Connected and waiting for EngineerOS runs.");
266
+ reconnectDelay = 1_000;
267
+ startPings();
268
+ if (config.onboarding_pending) void submitWorkspaceSnapshot();
269
+ return;
270
+ }
271
+ if (message.type === "workspace.refresh") {
272
+ void submitWorkspaceSnapshot();
273
+ return;
274
+ }
275
+ if (message.type === "workspace.assessment") {
276
+ if (
277
+ active?.runId !== message.assessment_id &&
278
+ !assessments.some(
279
+ (candidate) => candidate.assessment_id === message.assessment_id,
280
+ )
281
+ ) {
282
+ assessments.push(message);
283
+ }
284
+ pump();
285
+ return;
286
+ }
287
+ if (message.type === "prompt.execute") {
288
+ if (!activePrompts.has(message.prompt_id)) void executePrompt(message);
289
+ return;
290
+ }
291
+ if (message.type === "prompt.cancel") {
292
+ const promptState = activePrompts.get(message.prompt_id);
293
+ if (promptState) await cancelPrompt(promptState);
294
+ return;
295
+ }
296
+ if (message.type === "run.available") {
297
+ if (!available.includes(message.run_id)) available.push(message.run_id);
298
+ pump();
299
+ return;
300
+ }
301
+ if (message.type === "run.assignment") {
302
+ await execute(message);
303
+ return;
304
+ }
305
+ if (message.type === "run.cancelled" && active?.runId === message.run_id) {
306
+ console.log(`Run ${message.run_id} cancelled by EngineerOS.`);
307
+ active.cancelled = true;
308
+ await stopProcess(active.child);
309
+ return;
310
+ }
311
+ if (message.type === "connector.revoked") {
312
+ stopped = true;
313
+ console.error("This connector was revoked in EngineerOS.");
314
+ socket.close();
315
+ return;
316
+ }
317
+ if (message.type === "run.error") {
318
+ console.error(`EngineerOS: ${message.message}`);
319
+ if (!message.run_id) snapshotInFlight = false;
320
+ if (active?.runId === message.run_id && !active.child) {
321
+ active = null;
322
+ pump();
323
+ }
324
+ }
325
+ if (message.type === "connection.error") {
326
+ connectionRejected = true;
327
+ console.error(`EngineerOS: ${message.message}`);
328
+ }
329
+ });
330
+ socket.addEventListener("close", (event) => {
331
+ clearConnectionWatchdog?.();
332
+ clearInterval(pingTimer);
333
+ for (const promptState of activePrompts.values())
334
+ void cancelPrompt(promptState);
335
+ if (event.code === 4001 && active?.kind === "assessment") {
336
+ void stopProcess(active.child);
337
+ }
338
+ if (connectionRejected) {
339
+ stopped = true;
340
+ console.error(
341
+ "Connection rejected. Create a new pairing command in EngineerOS if this connector was revoked.",
342
+ );
343
+ return;
344
+ }
345
+ if (stopped) return;
346
+ scheduleReconnect();
347
+ });
348
+ socket.addEventListener("error", (event) => {
349
+ lastConnectionError = describeWebSocketError(event);
350
+ console.error(
351
+ `WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
352
+ );
353
+ });
354
+ }
355
+
356
+ function scheduleReconnect() {
357
+ if (stopped || connectionRejected || reconnectTimer) return;
358
+ const detail = lastConnectionError
359
+ ? ` Last error: ${lastConnectionError}`
360
+ : "";
361
+ console.error(
362
+ `Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
363
+ );
364
+ reconnectTimer = setTimeout(() => {
365
+ reconnectTimer = undefined;
366
+ void connect();
367
+ }, reconnectDelay);
368
+ reconnectDelay = Math.min(30_000, reconnectDelay * 2);
369
+ }
370
+
371
+ async function submitWorkspaceSnapshot() {
372
+ if (snapshotInFlight || socket.readyState !== WebSocket.OPEN) return;
373
+ snapshotInFlight = true;
374
+ console.log("Inspecting the workspace without executing its code.");
375
+ try {
376
+ const snapshot = await workspaceSnapshot(config.workspace);
377
+ const response = await fetch(
378
+ workspaceUrl(config.server_url, config.connector_id),
379
+ {
380
+ method: "POST",
381
+ headers: {
382
+ "Content-Type": "application/json",
383
+ Authorization: `Bearer ${config.token}`,
384
+ },
385
+ body: JSON.stringify(snapshot),
386
+ },
387
+ );
388
+ if (!response.ok) {
389
+ throw new Error(
390
+ `EngineerOS rejected the workspace (${response.status}): ${await response.text()}`,
391
+ );
392
+ }
393
+ const result = await response.json();
394
+ config = { ...config, onboarding_pending: false };
395
+ await saveConfig(config);
396
+ console.log(
397
+ `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
398
+ );
399
+ console.log(
400
+ `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized evidence file(s).`,
401
+ );
402
+ console.log(
403
+ `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
404
+ );
405
+ if (snapshot.omitted_evidence_file_count > 0) {
406
+ console.log(
407
+ `${snapshot.omitted_evidence_file_count.toLocaleString()} additional file(s) remain available to the connected Agent locally.`,
408
+ );
409
+ }
410
+ console.log(
411
+ `Workspace inventory registered as ${result.workspace_kind}. Assessment queued.`,
412
+ );
413
+ snapshotInFlight = false;
414
+ } catch (error) {
415
+ snapshotInFlight = false;
416
+ console.error(
417
+ `Workspace assessment failed: ${error instanceof Error ? error.message : String(error)}`,
418
+ );
419
+ }
420
+ }
421
+
422
+ function startPings() {
423
+ clearInterval(pingTimer);
424
+ pingTimer = setInterval(() => {
425
+ if (socket.readyState === WebSocket.OPEN) {
426
+ socket.send(
427
+ JSON.stringify({
428
+ type: "ping",
429
+ active_run_id: active?.kind === "goal" ? active.runId : null,
430
+ }),
431
+ );
432
+ }
433
+ }, 10_000);
434
+ }
435
+
436
+ function sendPromptEvent(promptId, event) {
437
+ if (!event || socket.readyState !== WebSocket.OPEN) return;
438
+ const payload = {
439
+ type: "prompt.event",
440
+ prompt_id: promptId,
441
+ kind: event.kind,
442
+ };
443
+ if (event.message) payload.message = String(event.message).slice(0, 500);
444
+ if (event.delta) payload.delta = String(event.delta).slice(0, 50_000);
445
+ if (event.status) payload.status = String(event.status).slice(0, 100);
446
+ socket.send(JSON.stringify(payload));
447
+ }
448
+
449
+ function pump() {
450
+ if (active || socket.readyState !== WebSocket.OPEN) return;
451
+ const assessment = assessments.shift();
452
+ if (assessment) {
453
+ active = {
454
+ kind: "assessment",
455
+ runId: assessment.assessment_id,
456
+ child: null,
457
+ };
458
+ void executeAssessment(assessment);
459
+ return;
460
+ }
461
+ const runId = available.shift();
462
+ if (!runId) return;
463
+ active = { kind: "goal", runId, child: null, cancelled: false };
464
+ socket.send(
465
+ JSON.stringify({
466
+ type: "run.claim",
467
+ run_id: runId,
468
+ runner_name: codingAgent.name,
469
+ metadata: {
470
+ hostname: os.hostname(),
471
+ workspace_name: path.basename(config.workspace),
472
+ },
473
+ }),
474
+ );
475
+ }
476
+
477
+ async function executePrompt(assignment) {
478
+ const promptId = assignment.prompt_id;
479
+ const promptState = {
480
+ runId: promptId,
481
+ child: null,
482
+ controller: null,
483
+ cancelled: false,
484
+ };
485
+ activePrompts.set(promptId, promptState);
486
+ let lastEvent;
487
+ const reportEvent = (event) => {
488
+ const serialized = event ? JSON.stringify(event) : null;
489
+ if (
490
+ !serialized ||
491
+ serialized === lastEvent ||
492
+ socket.readyState !== WebSocket.OPEN ||
493
+ activePrompts.get(promptId) !== promptState
494
+ ) {
495
+ return;
496
+ }
497
+ lastEvent = serialized;
498
+ sendPromptEvent(promptId, event);
499
+ };
500
+ sendPromptEvent(promptId, {
501
+ kind: "status",
502
+ message: "Agent started this request",
503
+ });
504
+ console.log(
505
+ `Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
506
+ );
507
+ try {
508
+ const result = await executeConnectedPrompt(assignment, config, {
509
+ onProcess: (child) => {
510
+ if (activePrompts.get(promptId) === promptState)
511
+ promptState.child = child;
512
+ },
513
+ onController: (controller) => {
514
+ if (activePrompts.get(promptId) === promptState) {
515
+ promptState.controller = controller;
516
+ }
517
+ },
518
+ onEvent: (event) => reportEvent(promptStreamEvent(event)),
519
+ });
520
+ config = {
521
+ ...config,
522
+ sessions: {
523
+ ...(config.sessions || {}),
524
+ [result.sessionKey]: result.sessionId,
525
+ },
526
+ };
527
+ await saveConfig(config);
528
+ if (promptState.cancelled || socket.readyState !== WebSocket.OPEN) return;
529
+ socket.send(
530
+ JSON.stringify({
531
+ type: "prompt.completed",
532
+ prompt_id: promptId,
533
+ content: result.content,
534
+ model: result.model,
535
+ session_id: result.sessionId,
536
+ }),
537
+ );
538
+ } catch (error) {
539
+ if (!promptState.cancelled && socket.readyState === WebSocket.OPEN) {
540
+ socket.send(
541
+ JSON.stringify({
542
+ type: "prompt.failed",
543
+ prompt_id: promptId,
544
+ message: error instanceof Error ? error.message : String(error),
545
+ }),
546
+ );
547
+ }
548
+ if (!promptState.cancelled) {
549
+ console.error(error instanceof Error ? error.message : String(error));
550
+ }
551
+ } finally {
552
+ if (activePrompts.get(promptId) === promptState)
553
+ activePrompts.delete(promptId);
554
+ }
555
+ }
556
+
557
+ async function cancelPrompt(promptState) {
558
+ promptState.cancelled = true;
559
+ if (typeof promptState.controller?.cancel === "function") {
560
+ await promptState.controller.cancel();
561
+ return;
562
+ }
563
+ await stopProcess(promptState.child);
564
+ }
565
+
566
+ async function executeAssessment(assignment) {
567
+ const assessmentId = assignment.assessment_id;
568
+ console.log(`Agent is assessing the workspace (${assessmentId}).`);
569
+ let progress = 10;
570
+ let outputBuffer = "";
571
+ let outputLength = 0;
572
+ let outputTimer;
573
+ const reportedMilestones = new Set();
574
+ const reportProgress = (message, { milestone = true } = {}) => {
575
+ if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId)
576
+ return;
577
+ if (milestone && reportedMilestones.has(message)) return;
578
+ if (milestone) reportedMilestones.add(message);
579
+ socket.send(
580
+ JSON.stringify({
581
+ type: "workspace.assessment.progress",
582
+ assessment_id: assessmentId,
583
+ progress_percent: progress,
584
+ message: String(message).slice(0, 500),
585
+ }),
586
+ );
587
+ };
588
+ const flushOutput = () => {
589
+ if (outputTimer) clearTimeout(outputTimer);
590
+ outputTimer = undefined;
591
+ const output = outputBuffer;
592
+ outputBuffer = "";
593
+ if (
594
+ !output ||
595
+ socket.readyState !== WebSocket.OPEN ||
596
+ active?.runId !== assessmentId
597
+ ) {
598
+ return;
599
+ }
600
+ for (let offset = 0; offset < output.length; offset += 50_000) {
601
+ socket.send(
602
+ JSON.stringify({
603
+ type: "workspace.assessment.output",
604
+ assessment_id: assessmentId,
605
+ delta: output.slice(offset, offset + 50_000),
606
+ }),
607
+ );
608
+ }
609
+ };
610
+ const reportOutput = (delta) => {
611
+ const remaining = 120_000 - outputLength;
612
+ if (!delta || remaining <= 0) return;
613
+ const bounded = String(delta).slice(0, remaining);
614
+ outputBuffer += bounded;
615
+ outputLength += bounded.length;
616
+ if (outputBuffer.length >= 50_000) {
617
+ flushOutput();
618
+ } else if (!outputTimer) {
619
+ outputTimer = setTimeout(flushOutput, 500);
620
+ }
621
+ };
622
+ reportProgress("Starting read-only repository assessment");
623
+ const heartbeat = setInterval(() => {
624
+ progress = Math.min(90, progress + 5);
625
+ reportProgress("Assessment in progress", { milestone: false });
626
+ }, 15_000);
627
+ try {
628
+ if (assignment.assessment_mode === "incremental") {
629
+ progress = 15;
630
+ reportProgress("Calculating changed files and affected evidence");
631
+ }
632
+ const result = await executeWorkspaceAssessment(assignment, config, {
633
+ onProcess: (child) => {
634
+ if (active?.runId === assessmentId) active.child = child;
635
+ },
636
+ onEvent: (event) => {
637
+ reportOutput(assessmentStreamDelta(event));
638
+ const message = assessmentProgressMessage(event);
639
+ if (message) reportProgress(message);
640
+ },
641
+ });
642
+ flushOutput();
643
+ const response = await fetch(
644
+ assessmentResultUrl(config.server_url, config.connector_id, assessmentId),
645
+ {
646
+ method: "POST",
647
+ headers: {
648
+ "Content-Type": "application/json",
649
+ Authorization: `Bearer ${config.token}`,
650
+ },
651
+ body: JSON.stringify(result),
652
+ },
653
+ );
654
+ if (!response.ok) {
655
+ throw new Error(
656
+ `EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
657
+ );
658
+ }
659
+ console.log("Workspace assessment is current in EngineerOS.");
660
+ } catch (error) {
661
+ flushOutput();
662
+ if (socket.readyState === WebSocket.OPEN) {
663
+ socket.send(
664
+ JSON.stringify({
665
+ type: "workspace.assessment.failed",
666
+ assessment_id: assessmentId,
667
+ message: error instanceof Error ? error.message : String(error),
668
+ }),
669
+ );
670
+ }
671
+ console.error(error instanceof Error ? error.message : String(error));
672
+ } finally {
673
+ if (outputTimer) clearTimeout(outputTimer);
674
+ clearInterval(heartbeat);
675
+ active = null;
676
+ pump();
677
+ }
678
+ }
679
+
680
+ async function execute(assignment) {
681
+ const runId = assignment.run_id;
682
+ console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
683
+ let progress = 10;
684
+ const heartbeat = setInterval(() => {
685
+ if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
686
+ progress = Math.min(90, progress + 5);
687
+ socket.send(
688
+ JSON.stringify({
689
+ type: "run.progress",
690
+ run_id: runId,
691
+ progress_percent: progress,
692
+ message: "Agent is working",
693
+ }),
694
+ );
695
+ }
696
+ }, 15_000);
697
+ try {
698
+ const result = await executeAssignment(assignment, config, {
699
+ onProcess: (child) => {
700
+ if (active?.runId === runId) active.child = child;
701
+ },
702
+ onEvent: (event) => {
703
+ const message =
704
+ event.message || event.item?.text || event.type || "Agent is working";
705
+ if (socket.readyState === WebSocket.OPEN) {
706
+ socket.send(
707
+ JSON.stringify({
708
+ type: "run.progress",
709
+ run_id: runId,
710
+ progress_percent: progress,
711
+ message: String(message).slice(0, 500),
712
+ }),
713
+ );
714
+ }
715
+ },
716
+ });
717
+ if (active?.cancelled) return;
718
+ const response = await fetch(
719
+ resultUrl(config.server_url, config.connector_id, runId),
720
+ {
721
+ method: "POST",
722
+ headers: {
723
+ "Content-Type": "application/json",
724
+ Authorization: `Bearer ${config.token}`,
725
+ },
726
+ body: JSON.stringify(result),
727
+ },
728
+ );
729
+ if (!response.ok)
730
+ throw new Error(
731
+ `EngineerOS rejected the result (${response.status}): ${await response.text()}`,
732
+ );
733
+ const integration = await applyAcceptedChange(
734
+ config.workspace,
735
+ assignment.base_revision,
736
+ result.head_revision,
737
+ );
738
+ if (integration.applied) {
739
+ console.log(
740
+ `Run accepted and applied to the connected repository at ${integration.revision}.`,
741
+ );
742
+ } else {
743
+ console.warn(
744
+ `Run accepted but not applied locally. ${integration.reason}`,
745
+ );
746
+ console.warn(
747
+ `The verified run workspace remains at ${result.run_workspace}.`,
748
+ );
749
+ }
750
+ } catch (error) {
751
+ if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
752
+ socket.send(
753
+ JSON.stringify({
754
+ type: "run.failed",
755
+ run_id: runId,
756
+ message: error instanceof Error ? error.message : String(error),
757
+ }),
758
+ );
759
+ }
760
+ if (!active?.cancelled)
761
+ console.error(error instanceof Error ? error.message : String(error));
762
+ } finally {
763
+ clearInterval(heartbeat);
764
+ active = null;
765
+ pump();
766
+ }
767
+ }
768
+
769
+ function parseAgentArgs(value) {
770
+ if (!value) return [];
771
+ try {
772
+ const parsed = JSON.parse(value);
773
+ if (
774
+ !Array.isArray(parsed) ||
775
+ parsed.some((item) => typeof item !== "string")
776
+ ) {
777
+ throw new Error();
778
+ }
779
+ return parsed;
780
+ } catch {
781
+ fail(
782
+ '--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"',
783
+ );
784
+ }
785
+ }
786
+
787
+ function fail(message) {
788
+ console.error(message);
789
+ process.exit(1);
790
+ }