@engineeros/connector 0.5.0 → 0.6.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.
- package/README.md +12 -0
- package/bin/engineeros-connector.mjs +84 -18
- package/package.json +7 -4
- package/src/acp-client.mjs +105 -0
- package/src/connection.mjs +26 -0
- package/src/runner.mjs +77 -5
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# EngineerOS Connector
|
|
2
2
|
|
|
3
|
+
The connector uses an authenticated EngineerOS WebSocket for pairing, workspace identity, run lifecycle, and evidence. Coding work can run through any ACP v1-compatible agent over stdio. Codex CLI remains available as the default built-in adapter.
|
|
4
|
+
|
|
5
|
+
## Pair an ACP coding agent
|
|
6
|
+
|
|
7
|
+
Run the command from the repository the agent should work in:
|
|
8
|
+
|
|
9
|
+
```powershell
|
|
10
|
+
npx --yes @engineeros/connector@latest pair PAIRING-CODE --url http://localhost:8000 --workspace . --onboard --agent-name "My ACP agent" --agent-command my-agent --agent-args '["--acp"]'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
`--agent-command` must start an ACP v1 agent over NDJSON stdio. `--agent-args` is a JSON array so arguments are passed without invoking a shell. Omit both options to use the installed Codex CLI adapter.
|
|
14
|
+
|
|
3
15
|
Connect a local Codex CLI workspace to EngineerOS through an outbound WebSocket.
|
|
4
16
|
|
|
5
17
|
This package is only a connector. EngineerOS owns every prompt, Goal instruction, and
|
|
@@ -11,13 +11,18 @@ import {
|
|
|
11
11
|
} from "../src/config.mjs";
|
|
12
12
|
import {
|
|
13
13
|
assessmentProgressMessage,
|
|
14
|
+
applyAcceptedChange,
|
|
14
15
|
executeAssignment,
|
|
15
16
|
executeConnectedPrompt,
|
|
16
17
|
executeWorkspaceAssessment,
|
|
17
|
-
|
|
18
|
+
inspectCodingAgent,
|
|
18
19
|
stopProcess,
|
|
19
20
|
workspaceSnapshot,
|
|
20
21
|
} from "../src/runner.mjs";
|
|
22
|
+
import {
|
|
23
|
+
describeWebSocketError,
|
|
24
|
+
startConnectionWatchdog,
|
|
25
|
+
} from "../src/connection.mjs";
|
|
21
26
|
|
|
22
27
|
const { command, positional, flags } = parseArgs(process.argv.slice(2));
|
|
23
28
|
|
|
@@ -46,6 +51,10 @@ if (command === "pair") {
|
|
|
46
51
|
workspace: path.resolve(flags.workspace || process.cwd()),
|
|
47
52
|
onboard: flags.onboard === true,
|
|
48
53
|
onboarding_pending: flags.onboard === true,
|
|
54
|
+
agent_protocol: flags["agent-command"] ? "acp" : "codex",
|
|
55
|
+
agent_command: flags["agent-command"] || null,
|
|
56
|
+
agent_args: parseAgentArgs(flags["agent-args"]),
|
|
57
|
+
agent_name: flags["agent-name"] || null,
|
|
49
58
|
name:
|
|
50
59
|
flags.name ||
|
|
51
60
|
`${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
|
|
@@ -55,7 +64,9 @@ if (command === "pair") {
|
|
|
55
64
|
pairing_code: pairingCode,
|
|
56
65
|
name: config.name,
|
|
57
66
|
capabilities: {
|
|
58
|
-
|
|
67
|
+
agent_protocols: flags["agent-command"] ? ["acp"] : ["codex"],
|
|
68
|
+
coding_agent: true,
|
|
69
|
+
codex_cli: !flags["agent-command"],
|
|
59
70
|
platform: process.platform,
|
|
60
71
|
workspace_name: path.basename(config.workspace),
|
|
61
72
|
},
|
|
@@ -75,15 +86,16 @@ if (command === "pair") {
|
|
|
75
86
|
fail("Use `engineeros-connector pair`, `start`, or `status`.");
|
|
76
87
|
}
|
|
77
88
|
|
|
78
|
-
let
|
|
89
|
+
let codingAgent;
|
|
79
90
|
try {
|
|
80
|
-
|
|
91
|
+
codingAgent = await inspectCodingAgent(config, config.workspace);
|
|
81
92
|
} catch (error) {
|
|
82
93
|
fail(error instanceof Error ? error.message : String(error));
|
|
83
94
|
}
|
|
84
|
-
console.log(`Using ${
|
|
95
|
+
console.log(`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`);
|
|
85
96
|
if (firstMessage.type === "pair") {
|
|
86
|
-
firstMessage.capabilities.
|
|
97
|
+
firstMessage.capabilities.agent_name = codingAgent.name;
|
|
98
|
+
firstMessage.capabilities.agent_version = codingAgent.version;
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
let stopped = false;
|
|
@@ -93,12 +105,18 @@ const assessments = [];
|
|
|
93
105
|
const prompts = [];
|
|
94
106
|
let socket;
|
|
95
107
|
let pingTimer;
|
|
108
|
+
let reconnectTimer;
|
|
109
|
+
let clearConnectionWatchdog;
|
|
96
110
|
let reconnectDelay = 1_000;
|
|
97
111
|
let connectionRejected = false;
|
|
112
|
+
let lastConnectionError;
|
|
98
113
|
let snapshotInFlight = false;
|
|
99
114
|
|
|
100
115
|
process.on("SIGINT", async () => {
|
|
101
116
|
stopped = true;
|
|
117
|
+
clearConnectionWatchdog?.();
|
|
118
|
+
clearTimeout(reconnectTimer);
|
|
119
|
+
clearInterval(pingTimer);
|
|
102
120
|
await stopProcess(active?.child);
|
|
103
121
|
socket?.close();
|
|
104
122
|
process.exit(0);
|
|
@@ -109,13 +127,22 @@ await connect();
|
|
|
109
127
|
async function connect() {
|
|
110
128
|
console.log(`Connecting ${config.name} to ${config.server_url}`);
|
|
111
129
|
connectionRejected = false;
|
|
130
|
+
lastConnectionError = undefined;
|
|
112
131
|
socket = new WebSocket(config.server_url);
|
|
132
|
+
clearConnectionWatchdog = startConnectionWatchdog(socket, config.server_url, {
|
|
133
|
+
onTimeout: (message) => {
|
|
134
|
+
lastConnectionError = message;
|
|
135
|
+
console.error(message);
|
|
136
|
+
scheduleReconnect();
|
|
137
|
+
},
|
|
138
|
+
});
|
|
113
139
|
socket.addEventListener("open", () =>
|
|
114
140
|
socket.send(JSON.stringify(firstMessage)),
|
|
115
141
|
);
|
|
116
142
|
socket.addEventListener("message", async (event) => {
|
|
117
143
|
const message = JSON.parse(String(event.data));
|
|
118
144
|
if (message.type === "paired") {
|
|
145
|
+
clearConnectionWatchdog?.();
|
|
119
146
|
config = {
|
|
120
147
|
...config,
|
|
121
148
|
connector_id: message.connector.id,
|
|
@@ -134,6 +161,7 @@ async function connect() {
|
|
|
134
161
|
return;
|
|
135
162
|
}
|
|
136
163
|
if (message.type === "authenticated") {
|
|
164
|
+
clearConnectionWatchdog?.();
|
|
137
165
|
console.log("Connected and waiting for EngineerOS runs.");
|
|
138
166
|
reconnectDelay = 1_000;
|
|
139
167
|
startPings();
|
|
@@ -212,6 +240,7 @@ async function connect() {
|
|
|
212
240
|
}
|
|
213
241
|
});
|
|
214
242
|
socket.addEventListener("close", () => {
|
|
243
|
+
clearConnectionWatchdog?.();
|
|
215
244
|
clearInterval(pingTimer);
|
|
216
245
|
if (active?.kind === "prompt") {
|
|
217
246
|
active.cancelled = true;
|
|
@@ -225,13 +254,27 @@ async function connect() {
|
|
|
225
254
|
return;
|
|
226
255
|
}
|
|
227
256
|
if (stopped) return;
|
|
257
|
+
scheduleReconnect();
|
|
258
|
+
});
|
|
259
|
+
socket.addEventListener("error", (event) => {
|
|
260
|
+
lastConnectionError = describeWebSocketError(event);
|
|
228
261
|
console.error(
|
|
229
|
-
`
|
|
262
|
+
`WebSocket connection to ${config.server_url} failed: ${lastConnectionError}`,
|
|
230
263
|
);
|
|
231
|
-
setTimeout(connect, reconnectDelay);
|
|
232
|
-
reconnectDelay = Math.min(30_000, reconnectDelay * 2);
|
|
233
264
|
});
|
|
234
|
-
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function scheduleReconnect() {
|
|
268
|
+
if (stopped || connectionRejected || reconnectTimer) return;
|
|
269
|
+
const detail = lastConnectionError ? ` Last error: ${lastConnectionError}` : "";
|
|
270
|
+
console.error(
|
|
271
|
+
`Connection unavailable.${detail} Retrying in ${Math.round(reconnectDelay / 1_000)}s.`,
|
|
272
|
+
);
|
|
273
|
+
reconnectTimer = setTimeout(() => {
|
|
274
|
+
reconnectTimer = undefined;
|
|
275
|
+
void connect();
|
|
276
|
+
}, reconnectDelay);
|
|
277
|
+
reconnectDelay = Math.min(30_000, reconnectDelay * 2);
|
|
235
278
|
}
|
|
236
279
|
|
|
237
280
|
async function submitWorkspaceSnapshot() {
|
|
@@ -318,7 +361,7 @@ function pump() {
|
|
|
318
361
|
JSON.stringify({
|
|
319
362
|
type: "run.claim",
|
|
320
363
|
run_id: runId,
|
|
321
|
-
runner_name:
|
|
364
|
+
runner_name: codingAgent.name,
|
|
322
365
|
metadata: {
|
|
323
366
|
hostname: os.hostname(),
|
|
324
367
|
workspace_name: path.basename(config.workspace),
|
|
@@ -330,7 +373,7 @@ function pump() {
|
|
|
330
373
|
async function executePrompt(assignment) {
|
|
331
374
|
const promptId = assignment.prompt_id;
|
|
332
375
|
console.log(
|
|
333
|
-
`Answering ${assignment.purpose || "project"} prompt with
|
|
376
|
+
`Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
|
|
334
377
|
);
|
|
335
378
|
try {
|
|
336
379
|
const result = await executeConnectedPrompt(assignment, config, {
|
|
@@ -368,7 +411,7 @@ async function executePrompt(assignment) {
|
|
|
368
411
|
|
|
369
412
|
async function executeAssessment(assignment) {
|
|
370
413
|
const assessmentId = assignment.assessment_id;
|
|
371
|
-
console.log(`Assessing workspace with
|
|
414
|
+
console.log(`Assessing workspace with ${codingAgent.name} (${assessmentId}).`);
|
|
372
415
|
let progress = 10;
|
|
373
416
|
const reportedMilestones = new Set();
|
|
374
417
|
const reportProgress = (message, { milestone = true } = {}) => {
|
|
@@ -420,7 +463,7 @@ async function executeAssessment(assignment) {
|
|
|
420
463
|
`EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
|
|
421
464
|
);
|
|
422
465
|
}
|
|
423
|
-
console.log(
|
|
466
|
+
console.log(`${codingAgent.name} workspace assessment is current in EngineerOS.`);
|
|
424
467
|
} catch (error) {
|
|
425
468
|
if (socket.readyState === WebSocket.OPEN) {
|
|
426
469
|
socket.send(
|
|
@@ -441,7 +484,7 @@ async function executeAssessment(assignment) {
|
|
|
441
484
|
|
|
442
485
|
async function execute(assignment) {
|
|
443
486
|
const runId = assignment.run_id;
|
|
444
|
-
console.log(`Running Goal ${runId} with
|
|
487
|
+
console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
|
|
445
488
|
let progress = 10;
|
|
446
489
|
const heartbeat = setInterval(() => {
|
|
447
490
|
if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
|
|
@@ -451,7 +494,7 @@ async function execute(assignment) {
|
|
|
451
494
|
type: "run.progress",
|
|
452
495
|
run_id: runId,
|
|
453
496
|
progress_percent: progress,
|
|
454
|
-
message:
|
|
497
|
+
message: `${codingAgent.name} is working`,
|
|
455
498
|
}),
|
|
456
499
|
);
|
|
457
500
|
}
|
|
@@ -463,7 +506,7 @@ async function execute(assignment) {
|
|
|
463
506
|
},
|
|
464
507
|
onEvent: (event) => {
|
|
465
508
|
const message =
|
|
466
|
-
event.message || event.item?.text || event.type ||
|
|
509
|
+
event.message || event.item?.text || event.type || `${codingAgent.name} is working`;
|
|
467
510
|
if (socket.readyState === WebSocket.OPEN) {
|
|
468
511
|
socket.send(
|
|
469
512
|
JSON.stringify({
|
|
@@ -492,7 +535,17 @@ async function execute(assignment) {
|
|
|
492
535
|
throw new Error(
|
|
493
536
|
`EngineerOS rejected the result (${response.status}): ${await response.text()}`,
|
|
494
537
|
);
|
|
495
|
-
|
|
538
|
+
const integration = await applyAcceptedChange(
|
|
539
|
+
config.workspace,
|
|
540
|
+
assignment.base_revision,
|
|
541
|
+
result.head_revision,
|
|
542
|
+
);
|
|
543
|
+
if (integration.applied) {
|
|
544
|
+
console.log(`Run accepted and applied to the connected repository at ${integration.revision}.`);
|
|
545
|
+
} else {
|
|
546
|
+
console.warn(`Run accepted but not applied locally. ${integration.reason}`);
|
|
547
|
+
console.warn(`The verified run workspace remains at ${result.run_workspace}.`);
|
|
548
|
+
}
|
|
496
549
|
} catch (error) {
|
|
497
550
|
if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
498
551
|
socket.send(
|
|
@@ -525,6 +578,19 @@ function parseArgs(args) {
|
|
|
525
578
|
return { command, positional, flags };
|
|
526
579
|
}
|
|
527
580
|
|
|
581
|
+
function parseAgentArgs(value) {
|
|
582
|
+
if (!value) return [];
|
|
583
|
+
try {
|
|
584
|
+
const parsed = JSON.parse(value);
|
|
585
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
586
|
+
throw new Error();
|
|
587
|
+
}
|
|
588
|
+
return parsed;
|
|
589
|
+
} catch {
|
|
590
|
+
fail('--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"');
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
528
594
|
function fail(message) {
|
|
529
595
|
console.error(message);
|
|
530
596
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@engineeros/connector",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Connect a local
|
|
3
|
+
"version": "0.6.1",
|
|
4
|
+
"description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "UNLICENSED",
|
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node ./bin/engineeros-connector.mjs",
|
|
18
|
-
"test": "node --test",
|
|
19
|
-
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/runner.mjs"
|
|
18
|
+
"test": "node --test --test-concurrency=1",
|
|
19
|
+
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
22
22
|
"node": ">=22"
|
|
@@ -31,5 +31,8 @@
|
|
|
31
31
|
},
|
|
32
32
|
"publishConfig": {
|
|
33
33
|
"access": "public"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@agentclientprotocol/sdk": "1.3.0"
|
|
34
37
|
}
|
|
35
38
|
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { Readable, Writable } from "node:stream";
|
|
3
|
+
import * as acp from "@agentclientprotocol/sdk";
|
|
4
|
+
|
|
5
|
+
function permissionOutcome(options = []) {
|
|
6
|
+
const selected =
|
|
7
|
+
options.find((option) => option.kind === "allow_once") ??
|
|
8
|
+
options.find((option) => option.kind === "allow_always");
|
|
9
|
+
return selected
|
|
10
|
+
? { outcome: { outcome: "selected", optionId: selected.optionId } }
|
|
11
|
+
: { outcome: { outcome: "cancelled" } };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function launchAcpAgent(workspace, prompt, config, callbacks = {}) {
|
|
15
|
+
if (!config.agent_command) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"No ACP coding agent is configured. Pair again with --agent-command and optional --agent-args JSON.",
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
const child = spawn(
|
|
21
|
+
config.agent_command,
|
|
22
|
+
Array.isArray(config.agent_args) ? config.agent_args : [],
|
|
23
|
+
{
|
|
24
|
+
cwd: workspace,
|
|
25
|
+
env: process.env,
|
|
26
|
+
shell: false,
|
|
27
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
let stderr = "";
|
|
31
|
+
child.stderr.setEncoding("utf8");
|
|
32
|
+
child.stderr.on("data", (chunk) => {
|
|
33
|
+
stderr += chunk;
|
|
34
|
+
callbacks.onEvent?.({
|
|
35
|
+
type: "agent.stderr",
|
|
36
|
+
message: String(chunk).trim().slice(0, 500),
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const stream = acp.ndJsonStream(
|
|
41
|
+
Writable.toWeb(child.stdin),
|
|
42
|
+
Readable.toWeb(child.stdout),
|
|
43
|
+
);
|
|
44
|
+
let finalMessage = "";
|
|
45
|
+
const completed = acp
|
|
46
|
+
.client({ name: "EngineerOS" })
|
|
47
|
+
.onRequest(acp.methods.client.session.requestPermission, (ctx) =>
|
|
48
|
+
permissionOutcome(ctx.params.options),
|
|
49
|
+
)
|
|
50
|
+
.connectWith(stream, async (ctx) => {
|
|
51
|
+
const initialized = await ctx.request(acp.methods.agent.initialize, {
|
|
52
|
+
protocolVersion: acp.PROTOCOL_VERSION,
|
|
53
|
+
clientCapabilities: {},
|
|
54
|
+
});
|
|
55
|
+
callbacks.onEvent?.({
|
|
56
|
+
type: "agent.connected",
|
|
57
|
+
message: `ACP agent connected with protocol ${initialized.protocolVersion}`,
|
|
58
|
+
});
|
|
59
|
+
return ctx.buildSession(workspace).withSession(async (session) => {
|
|
60
|
+
child.engineerOsCancel = () =>
|
|
61
|
+
ctx.notify(acp.methods.agent.session.cancel, {
|
|
62
|
+
sessionId: session.sessionId,
|
|
63
|
+
});
|
|
64
|
+
void session.prompt(prompt);
|
|
65
|
+
for (;;) {
|
|
66
|
+
const message = await session.nextUpdate();
|
|
67
|
+
if (message.kind === "stop") {
|
|
68
|
+
if (message.stopReason === "error") {
|
|
69
|
+
throw new Error("The ACP agent ended the task with an error.");
|
|
70
|
+
}
|
|
71
|
+
return { finalMessage, model: "acp-agent" };
|
|
72
|
+
}
|
|
73
|
+
const update = message.update;
|
|
74
|
+
if (
|
|
75
|
+
update.sessionUpdate === "agent_message_chunk" &&
|
|
76
|
+
update.content?.type === "text"
|
|
77
|
+
) {
|
|
78
|
+
finalMessage += update.content.text;
|
|
79
|
+
}
|
|
80
|
+
callbacks.onEvent?.({
|
|
81
|
+
type: `acp.${update.sessionUpdate}`,
|
|
82
|
+
message:
|
|
83
|
+
update.title ??
|
|
84
|
+
update.content?.text ??
|
|
85
|
+
update.status ??
|
|
86
|
+
update.sessionUpdate,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
})
|
|
91
|
+
.catch((error) => {
|
|
92
|
+
const detail = stderr.trim().slice(-1_000);
|
|
93
|
+
throw new Error(
|
|
94
|
+
`ACP agent failed: ${error instanceof Error ? error.message : String(error)}${detail ? ` ${detail}` : ""}`,
|
|
95
|
+
);
|
|
96
|
+
})
|
|
97
|
+
.finally(() => {
|
|
98
|
+
child.kill("SIGKILL");
|
|
99
|
+
child.stdin.destroy();
|
|
100
|
+
child.stdout.destroy();
|
|
101
|
+
child.stderr.destroy();
|
|
102
|
+
child.unref();
|
|
103
|
+
});
|
|
104
|
+
return { child, completed };
|
|
105
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function describeWebSocketError(event) {
|
|
2
|
+
return (
|
|
3
|
+
event?.error?.message ||
|
|
4
|
+
event?.message ||
|
|
5
|
+
"The WebSocket connection failed without providing an error detail."
|
|
6
|
+
);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function startConnectionWatchdog(
|
|
10
|
+
socket,
|
|
11
|
+
url,
|
|
12
|
+
{ timeoutMs = 15_000, onTimeout = console.error } = {},
|
|
13
|
+
) {
|
|
14
|
+
const timer = setTimeout(() => {
|
|
15
|
+
onTimeout(
|
|
16
|
+
`EngineerOS did not complete the WebSocket handshake at ${url} within ${Math.round(timeoutMs / 1_000)} seconds. Check that the backend is running and the URL is reachable from this machine.`,
|
|
17
|
+
);
|
|
18
|
+
try {
|
|
19
|
+
socket.close();
|
|
20
|
+
} catch {
|
|
21
|
+
// Reconnect scheduling is owned by the caller.
|
|
22
|
+
}
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
|
|
25
|
+
return () => clearTimeout(timer);
|
|
26
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import os from "node:os";
|
|
|
11
11
|
import path from "node:path";
|
|
12
12
|
import { promisify } from "node:util";
|
|
13
13
|
import { deflateRaw } from "node:zlib";
|
|
14
|
+
import { launchAcpAgent } from "./acp-client.mjs";
|
|
14
15
|
|
|
15
16
|
const deflate = promisify(deflateRaw);
|
|
16
17
|
const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
@@ -94,10 +95,11 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
94
95
|
assignment.run_id,
|
|
95
96
|
assignment.base_revision,
|
|
96
97
|
);
|
|
97
|
-
const controller =
|
|
98
|
+
const controller = launchAgentProcess(
|
|
98
99
|
runWorkspace,
|
|
99
100
|
execution.prompt,
|
|
100
101
|
execution.sandboxMode,
|
|
102
|
+
config,
|
|
101
103
|
callbacks,
|
|
102
104
|
);
|
|
103
105
|
callbacks.onProcess?.(controller.child);
|
|
@@ -110,10 +112,11 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
110
112
|
assignment.base_revision,
|
|
111
113
|
assignment.run_id,
|
|
112
114
|
);
|
|
113
|
-
const verifier =
|
|
115
|
+
const verifier = launchAgentProcess(
|
|
114
116
|
runWorkspace,
|
|
115
117
|
verificationPrompt(assignment, committed.revision, committed.changedFiles),
|
|
116
118
|
"read-only",
|
|
119
|
+
config,
|
|
117
120
|
callbacks,
|
|
118
121
|
);
|
|
119
122
|
callbacks.onProcess?.(verifier.child);
|
|
@@ -135,6 +138,50 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
135
138
|
};
|
|
136
139
|
}
|
|
137
140
|
|
|
141
|
+
export async function applyAcceptedChange(workspace, baseRevision, headRevision) {
|
|
142
|
+
const current = await run("git", ["rev-parse", "HEAD"], workspace, {
|
|
143
|
+
allowFailure: true,
|
|
144
|
+
});
|
|
145
|
+
if (current.code !== 0) {
|
|
146
|
+
return { applied: false, reason: "The connected workspace is not a Git repository." };
|
|
147
|
+
}
|
|
148
|
+
const currentHead = current.stdout.trim();
|
|
149
|
+
const alreadyApplied = await run(
|
|
150
|
+
"git",
|
|
151
|
+
["merge-base", "--is-ancestor", headRevision, currentHead],
|
|
152
|
+
workspace,
|
|
153
|
+
{ allowFailure: true },
|
|
154
|
+
);
|
|
155
|
+
if (alreadyApplied.code === 0) return { applied: true, revision: currentHead };
|
|
156
|
+
const status = await run("git", ["status", "--porcelain"], workspace);
|
|
157
|
+
if (status.stdout.trim()) {
|
|
158
|
+
return {
|
|
159
|
+
applied: false,
|
|
160
|
+
reason: `The connected workspace has uncommitted changes. Apply accepted commit ${headRevision} after preserving them.`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (currentHead !== baseRevision) {
|
|
164
|
+
return {
|
|
165
|
+
applied: false,
|
|
166
|
+
reason: `The connected branch moved from frozen base ${baseRevision} to ${currentHead}. Apply accepted commit ${headRevision} with git cherry-pick.`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const applied = await run("git", ["cherry-pick", headRevision], workspace, {
|
|
170
|
+
allowFailure: true,
|
|
171
|
+
});
|
|
172
|
+
if (applied.code !== 0) {
|
|
173
|
+
await run("git", ["cherry-pick", "--abort"], workspace, {
|
|
174
|
+
allowFailure: true,
|
|
175
|
+
});
|
|
176
|
+
return {
|
|
177
|
+
applied: false,
|
|
178
|
+
reason: `The accepted commit ${headRevision} could not be applied cleanly. Apply it manually with git cherry-pick.`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const integrated = await run("git", ["rev-parse", "HEAD"], workspace);
|
|
182
|
+
return { applied: true, revision: integrated.stdout.trim() };
|
|
183
|
+
}
|
|
184
|
+
|
|
138
185
|
async function commitRunChange(workspace, baseRevision, runId) {
|
|
139
186
|
await run("git", ["add", "-A"], workspace);
|
|
140
187
|
await run(
|
|
@@ -254,10 +301,11 @@ export async function executeWorkspaceAssessment(
|
|
|
254
301
|
changeImpact.markdown,
|
|
255
302
|
)
|
|
256
303
|
: execution.prompt;
|
|
257
|
-
const controller =
|
|
304
|
+
const controller = launchAgentProcess(
|
|
258
305
|
config.workspace,
|
|
259
306
|
prompt,
|
|
260
307
|
execution.sandboxMode,
|
|
308
|
+
config,
|
|
261
309
|
callbacks,
|
|
262
310
|
);
|
|
263
311
|
callbacks.onProcess?.(controller.child);
|
|
@@ -375,10 +423,11 @@ function boundedText(value, maximum) {
|
|
|
375
423
|
|
|
376
424
|
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
377
425
|
const execution = connectorExecution(assignment);
|
|
378
|
-
const controller =
|
|
426
|
+
const controller = launchAgentProcess(
|
|
379
427
|
config.workspace,
|
|
380
428
|
execution.prompt,
|
|
381
429
|
execution.sandboxMode,
|
|
430
|
+
config,
|
|
382
431
|
callbacks,
|
|
383
432
|
);
|
|
384
433
|
callbacks.onProcess?.(controller.child);
|
|
@@ -387,7 +436,22 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
|
387
436
|
if (!content) {
|
|
388
437
|
throw new Error("Codex completed without returning a response.");
|
|
389
438
|
}
|
|
390
|
-
return { content, model: "
|
|
439
|
+
return { content, model: completed.model ?? config.agent_protocol ?? "coding-agent" };
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export async function inspectCodingAgent(config, workspace = process.cwd()) {
|
|
443
|
+
if (config.agent_protocol === "acp") {
|
|
444
|
+
if (!config.agent_command) {
|
|
445
|
+
throw new Error("ACP requires --agent-command when pairing the connector.");
|
|
446
|
+
}
|
|
447
|
+
return {
|
|
448
|
+
protocol: "acp",
|
|
449
|
+
name: config.agent_name || path.basename(config.agent_command),
|
|
450
|
+
version: "ACP v1",
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
const codex = await inspectCodexCli(workspace);
|
|
454
|
+
return { protocol: "codex", name: "Codex CLI", version: codex.version };
|
|
391
455
|
}
|
|
392
456
|
|
|
393
457
|
export async function inspectCodexCli(workspace = process.cwd()) {
|
|
@@ -481,6 +545,7 @@ export function connectorExecution(assignment) {
|
|
|
481
545
|
|
|
482
546
|
export async function stopProcess(child) {
|
|
483
547
|
if (!child || child.exitCode !== null) return;
|
|
548
|
+
await child.engineerOsCancel?.();
|
|
484
549
|
if (process.platform === "win32") {
|
|
485
550
|
await run(
|
|
486
551
|
"taskkill",
|
|
@@ -658,6 +723,13 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
658
723
|
return { child, completed };
|
|
659
724
|
}
|
|
660
725
|
|
|
726
|
+
function launchAgentProcess(workspace, prompt, sandbox, config, callbacks) {
|
|
727
|
+
if (config.agent_protocol === "acp") {
|
|
728
|
+
return launchAcpAgent(workspace, prompt, config, callbacks);
|
|
729
|
+
}
|
|
730
|
+
return launchCodexProcess(workspace, prompt, sandbox, callbacks);
|
|
731
|
+
}
|
|
732
|
+
|
|
661
733
|
function runCodexCommand(command, args, cwd) {
|
|
662
734
|
return new Promise((resolve, reject) => {
|
|
663
735
|
const child = spawn(command, args, {
|