@engineeros/connector 0.4.7 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -0
- package/bin/engineeros-connector.mjs +45 -14
- package/package.json +6 -3
- package/src/acp-client.mjs +105 -0
- package/src/runner.mjs +181 -14
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,10 +11,11 @@ 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";
|
|
@@ -46,6 +47,10 @@ if (command === "pair") {
|
|
|
46
47
|
workspace: path.resolve(flags.workspace || process.cwd()),
|
|
47
48
|
onboard: flags.onboard === true,
|
|
48
49
|
onboarding_pending: flags.onboard === true,
|
|
50
|
+
agent_protocol: flags["agent-command"] ? "acp" : "codex",
|
|
51
|
+
agent_command: flags["agent-command"] || null,
|
|
52
|
+
agent_args: parseAgentArgs(flags["agent-args"]),
|
|
53
|
+
agent_name: flags["agent-name"] || null,
|
|
49
54
|
name:
|
|
50
55
|
flags.name ||
|
|
51
56
|
`${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
|
|
@@ -55,7 +60,9 @@ if (command === "pair") {
|
|
|
55
60
|
pairing_code: pairingCode,
|
|
56
61
|
name: config.name,
|
|
57
62
|
capabilities: {
|
|
58
|
-
|
|
63
|
+
agent_protocols: flags["agent-command"] ? ["acp"] : ["codex"],
|
|
64
|
+
coding_agent: true,
|
|
65
|
+
codex_cli: !flags["agent-command"],
|
|
59
66
|
platform: process.platform,
|
|
60
67
|
workspace_name: path.basename(config.workspace),
|
|
61
68
|
},
|
|
@@ -75,15 +82,16 @@ if (command === "pair") {
|
|
|
75
82
|
fail("Use `engineeros-connector pair`, `start`, or `status`.");
|
|
76
83
|
}
|
|
77
84
|
|
|
78
|
-
let
|
|
85
|
+
let codingAgent;
|
|
79
86
|
try {
|
|
80
|
-
|
|
87
|
+
codingAgent = await inspectCodingAgent(config, config.workspace);
|
|
81
88
|
} catch (error) {
|
|
82
89
|
fail(error instanceof Error ? error.message : String(error));
|
|
83
90
|
}
|
|
84
|
-
console.log(`Using ${
|
|
91
|
+
console.log(`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`);
|
|
85
92
|
if (firstMessage.type === "pair") {
|
|
86
|
-
firstMessage.capabilities.
|
|
93
|
+
firstMessage.capabilities.agent_name = codingAgent.name;
|
|
94
|
+
firstMessage.capabilities.agent_version = codingAgent.version;
|
|
87
95
|
}
|
|
88
96
|
|
|
89
97
|
let stopped = false;
|
|
@@ -318,7 +326,7 @@ function pump() {
|
|
|
318
326
|
JSON.stringify({
|
|
319
327
|
type: "run.claim",
|
|
320
328
|
run_id: runId,
|
|
321
|
-
runner_name:
|
|
329
|
+
runner_name: codingAgent.name,
|
|
322
330
|
metadata: {
|
|
323
331
|
hostname: os.hostname(),
|
|
324
332
|
workspace_name: path.basename(config.workspace),
|
|
@@ -330,7 +338,7 @@ function pump() {
|
|
|
330
338
|
async function executePrompt(assignment) {
|
|
331
339
|
const promptId = assignment.prompt_id;
|
|
332
340
|
console.log(
|
|
333
|
-
`Answering ${assignment.purpose || "project"} prompt with
|
|
341
|
+
`Answering ${assignment.purpose || "project"} prompt with ${codingAgent.name}.`,
|
|
334
342
|
);
|
|
335
343
|
try {
|
|
336
344
|
const result = await executeConnectedPrompt(assignment, config, {
|
|
@@ -368,7 +376,7 @@ async function executePrompt(assignment) {
|
|
|
368
376
|
|
|
369
377
|
async function executeAssessment(assignment) {
|
|
370
378
|
const assessmentId = assignment.assessment_id;
|
|
371
|
-
console.log(`Assessing workspace with
|
|
379
|
+
console.log(`Assessing workspace with ${codingAgent.name} (${assessmentId}).`);
|
|
372
380
|
let progress = 10;
|
|
373
381
|
const reportedMilestones = new Set();
|
|
374
382
|
const reportProgress = (message, { milestone = true } = {}) => {
|
|
@@ -420,7 +428,7 @@ async function executeAssessment(assignment) {
|
|
|
420
428
|
`EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
|
|
421
429
|
);
|
|
422
430
|
}
|
|
423
|
-
console.log(
|
|
431
|
+
console.log(`${codingAgent.name} workspace assessment is current in EngineerOS.`);
|
|
424
432
|
} catch (error) {
|
|
425
433
|
if (socket.readyState === WebSocket.OPEN) {
|
|
426
434
|
socket.send(
|
|
@@ -441,7 +449,7 @@ async function executeAssessment(assignment) {
|
|
|
441
449
|
|
|
442
450
|
async function execute(assignment) {
|
|
443
451
|
const runId = assignment.run_id;
|
|
444
|
-
console.log(`Running Goal ${runId} with
|
|
452
|
+
console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
|
|
445
453
|
let progress = 10;
|
|
446
454
|
const heartbeat = setInterval(() => {
|
|
447
455
|
if (socket.readyState === WebSocket.OPEN && active?.runId === runId) {
|
|
@@ -451,7 +459,7 @@ async function execute(assignment) {
|
|
|
451
459
|
type: "run.progress",
|
|
452
460
|
run_id: runId,
|
|
453
461
|
progress_percent: progress,
|
|
454
|
-
message:
|
|
462
|
+
message: `${codingAgent.name} is working`,
|
|
455
463
|
}),
|
|
456
464
|
);
|
|
457
465
|
}
|
|
@@ -463,7 +471,7 @@ async function execute(assignment) {
|
|
|
463
471
|
},
|
|
464
472
|
onEvent: (event) => {
|
|
465
473
|
const message =
|
|
466
|
-
event.message || event.item?.text || event.type ||
|
|
474
|
+
event.message || event.item?.text || event.type || `${codingAgent.name} is working`;
|
|
467
475
|
if (socket.readyState === WebSocket.OPEN) {
|
|
468
476
|
socket.send(
|
|
469
477
|
JSON.stringify({
|
|
@@ -492,7 +500,17 @@ async function execute(assignment) {
|
|
|
492
500
|
throw new Error(
|
|
493
501
|
`EngineerOS rejected the result (${response.status}): ${await response.text()}`,
|
|
494
502
|
);
|
|
495
|
-
|
|
503
|
+
const integration = await applyAcceptedChange(
|
|
504
|
+
config.workspace,
|
|
505
|
+
assignment.base_revision,
|
|
506
|
+
result.head_revision,
|
|
507
|
+
);
|
|
508
|
+
if (integration.applied) {
|
|
509
|
+
console.log(`Run accepted and applied to the connected repository at ${integration.revision}.`);
|
|
510
|
+
} else {
|
|
511
|
+
console.warn(`Run accepted but not applied locally. ${integration.reason}`);
|
|
512
|
+
console.warn(`The verified run workspace remains at ${result.run_workspace}.`);
|
|
513
|
+
}
|
|
496
514
|
} catch (error) {
|
|
497
515
|
if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
|
|
498
516
|
socket.send(
|
|
@@ -525,6 +543,19 @@ function parseArgs(args) {
|
|
|
525
543
|
return { command, positional, flags };
|
|
526
544
|
}
|
|
527
545
|
|
|
546
|
+
function parseAgentArgs(value) {
|
|
547
|
+
if (!value) return [];
|
|
548
|
+
try {
|
|
549
|
+
const parsed = JSON.parse(value);
|
|
550
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) {
|
|
551
|
+
throw new Error();
|
|
552
|
+
}
|
|
553
|
+
return parsed;
|
|
554
|
+
} catch {
|
|
555
|
+
fail('--agent-args must be a JSON array, for example: --agent-args "[\\"acp\\"]"');
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
528
559
|
function fail(message) {
|
|
529
560
|
console.error(message);
|
|
530
561
|
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.0",
|
|
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,7 +15,7 @@
|
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"start": "node ./bin/engineeros-connector.mjs",
|
|
18
|
-
"test": "node --test",
|
|
18
|
+
"test": "node --test --test-concurrency=1",
|
|
19
19
|
"type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/runner.mjs"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|
|
@@ -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
|
+
}
|
package/src/runner.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { spawn } from "node:child_process";
|
|
3
2
|
import {
|
|
4
3
|
lstat,
|
|
@@ -12,6 +11,7 @@ import os from "node:os";
|
|
|
12
11
|
import path from "node:path";
|
|
13
12
|
import { promisify } from "node:util";
|
|
14
13
|
import { deflateRaw } from "node:zlib";
|
|
14
|
+
import { launchAcpAgent } from "./acp-client.mjs";
|
|
15
15
|
|
|
16
16
|
const deflate = promisify(deflateRaw);
|
|
17
17
|
const MAX_ARCHIVE_BYTES = 25 * 1024 * 1024;
|
|
@@ -95,10 +95,11 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
95
95
|
assignment.run_id,
|
|
96
96
|
assignment.base_revision,
|
|
97
97
|
);
|
|
98
|
-
const controller =
|
|
98
|
+
const controller = launchAgentProcess(
|
|
99
99
|
runWorkspace,
|
|
100
100
|
execution.prompt,
|
|
101
101
|
execution.sandboxMode,
|
|
102
|
+
config,
|
|
102
103
|
callbacks,
|
|
103
104
|
);
|
|
104
105
|
callbacks.onProcess?.(controller.child);
|
|
@@ -106,21 +107,162 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
106
107
|
const changedFiles = await changedFilePaths(runWorkspace);
|
|
107
108
|
if (!changedFiles.length)
|
|
108
109
|
throw new Error("Codex completed without changing any files.");
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
110
|
+
const committed = await commitRunChange(
|
|
111
|
+
runWorkspace,
|
|
112
|
+
assignment.base_revision,
|
|
113
|
+
assignment.run_id,
|
|
114
|
+
);
|
|
115
|
+
const verifier = launchAgentProcess(
|
|
116
|
+
runWorkspace,
|
|
117
|
+
verificationPrompt(assignment, committed.revision, committed.changedFiles),
|
|
118
|
+
"read-only",
|
|
119
|
+
config,
|
|
120
|
+
callbacks,
|
|
121
|
+
);
|
|
122
|
+
callbacks.onProcess?.(verifier.child);
|
|
123
|
+
const verification = await verifier.completed;
|
|
124
|
+
const proofEvidence = parseVerificationReport(
|
|
125
|
+
verification.finalMessage,
|
|
126
|
+
assignment.proof_checks,
|
|
127
|
+
config.connector_id,
|
|
128
|
+
assignment.run_id,
|
|
129
|
+
);
|
|
112
130
|
return {
|
|
113
|
-
head_revision: revision,
|
|
131
|
+
head_revision: committed.revision,
|
|
114
132
|
repository_locator: assignment.repository_locator,
|
|
115
133
|
external_reference: `connector:${config.connector_id}/run:${assignment.run_id}`,
|
|
116
|
-
diff_patch: diffPatch,
|
|
117
|
-
changed_files: changedFiles,
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
134
|
+
diff_patch: committed.diffPatch,
|
|
135
|
+
changed_files: committed.changedFiles,
|
|
136
|
+
proof_evidence: proofEvidence,
|
|
137
|
+
verification_report: verification.finalMessage,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
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
|
+
|
|
185
|
+
async function commitRunChange(workspace, baseRevision, runId) {
|
|
186
|
+
await run("git", ["add", "-A"], workspace);
|
|
187
|
+
await run(
|
|
188
|
+
"git",
|
|
189
|
+
[
|
|
190
|
+
"-c",
|
|
191
|
+
"user.name=EngineerOS Codex",
|
|
192
|
+
"-c",
|
|
193
|
+
"user.email=codex@engineeros.local",
|
|
194
|
+
"commit",
|
|
195
|
+
"-m",
|
|
196
|
+
`EngineerOS Goal Run ${runId}`,
|
|
197
|
+
],
|
|
198
|
+
workspace,
|
|
199
|
+
);
|
|
200
|
+
const head = await run("git", ["rev-parse", "HEAD"], workspace);
|
|
201
|
+
const revision = head.stdout.trim();
|
|
202
|
+
const diff = await run(
|
|
203
|
+
"git",
|
|
204
|
+
["diff", "--binary", baseRevision, revision, "--"],
|
|
205
|
+
workspace,
|
|
206
|
+
);
|
|
207
|
+
const paths = await run(
|
|
208
|
+
"git",
|
|
209
|
+
["diff", "--name-only", "-z", baseRevision, revision, "--"],
|
|
210
|
+
workspace,
|
|
211
|
+
);
|
|
212
|
+
return {
|
|
213
|
+
revision,
|
|
214
|
+
diffPatch: diff.stdout,
|
|
215
|
+
changedFiles: gitPathList(paths.stdout).sort(),
|
|
121
216
|
};
|
|
122
217
|
}
|
|
123
218
|
|
|
219
|
+
export function verificationPrompt(assignment, revision, changedFiles) {
|
|
220
|
+
const checks = (assignment.proof_checks ?? [])
|
|
221
|
+
.map(
|
|
222
|
+
(proof, index) =>
|
|
223
|
+
`## Proof ${index + 1}\n- Check: ${proof.check ?? ""}\n- Expected: ${proof.expected ?? ""}`,
|
|
224
|
+
)
|
|
225
|
+
.join("\n\n");
|
|
226
|
+
return `# EngineerOS Connected Verification
|
|
227
|
+
|
|
228
|
+
Independently verify the frozen Goal at Git commit \`${revision}\`. Do not modify files. Run the commands or inspections needed for every Proof item, and check the Goal boundaries and constraints in the supplied packet.
|
|
229
|
+
|
|
230
|
+
Changed files:
|
|
231
|
+
${changedFiles.map((item) => `- \`${item}\``).join("\n")}
|
|
232
|
+
|
|
233
|
+
${checks}
|
|
234
|
+
|
|
235
|
+
Return structured Markdown only, with exactly one section per Proof:
|
|
236
|
+
|
|
237
|
+
## Proof 1
|
|
238
|
+
- Status: passed or failed
|
|
239
|
+
- Exit code: integer or none
|
|
240
|
+
- Evidence: concise observed output and command
|
|
241
|
+
|
|
242
|
+
Do not claim a Proof passed unless you observed it directly.`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function parseVerificationReport(report, proofChecks, connectorId, runId) {
|
|
246
|
+
if (!report?.trim()) throw new Error("Codex returned no connected verification report.");
|
|
247
|
+
return (proofChecks ?? []).map((_, index) => {
|
|
248
|
+
const start = new RegExp(`^## Proof ${index + 1}\\s*$`, "im").exec(report);
|
|
249
|
+
const tail = start ? report.slice(start.index + start[0].length) : "";
|
|
250
|
+
const section = tail.split(/^## Proof \d+\s*$/im)[0] ?? "";
|
|
251
|
+
const status = /^- Status:\s*(passed|failed)\s*$/im.exec(section)?.[1] ?? "failed";
|
|
252
|
+
const exitValue = /^- Exit code:\s*(\d+|none)\s*$/im.exec(section)?.[1] ?? "none";
|
|
253
|
+
const evidence = /^- Evidence:\s*(.+)$/im.exec(section)?.[1]?.trim();
|
|
254
|
+
return {
|
|
255
|
+
proof_index: index,
|
|
256
|
+
status,
|
|
257
|
+
verifier_type: "agent_reported",
|
|
258
|
+
verifier_identity: "Connected Codex CLI verifier",
|
|
259
|
+
locator: `connector:${connectorId}/run:${runId}#proof-${index + 1}`,
|
|
260
|
+
output_excerpt: evidence || "The connected verifier did not provide evidence for this Proof.",
|
|
261
|
+
exit_code: exitValue === "none" ? null : Number(exitValue),
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
124
266
|
export async function executeWorkspaceAssessment(
|
|
125
267
|
assignment,
|
|
126
268
|
config,
|
|
@@ -159,10 +301,11 @@ export async function executeWorkspaceAssessment(
|
|
|
159
301
|
changeImpact.markdown,
|
|
160
302
|
)
|
|
161
303
|
: execution.prompt;
|
|
162
|
-
const controller =
|
|
304
|
+
const controller = launchAgentProcess(
|
|
163
305
|
config.workspace,
|
|
164
306
|
prompt,
|
|
165
307
|
execution.sandboxMode,
|
|
308
|
+
config,
|
|
166
309
|
callbacks,
|
|
167
310
|
);
|
|
168
311
|
callbacks.onProcess?.(controller.child);
|
|
@@ -280,10 +423,11 @@ function boundedText(value, maximum) {
|
|
|
280
423
|
|
|
281
424
|
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
282
425
|
const execution = connectorExecution(assignment);
|
|
283
|
-
const controller =
|
|
426
|
+
const controller = launchAgentProcess(
|
|
284
427
|
config.workspace,
|
|
285
428
|
execution.prompt,
|
|
286
429
|
execution.sandboxMode,
|
|
430
|
+
config,
|
|
287
431
|
callbacks,
|
|
288
432
|
);
|
|
289
433
|
callbacks.onProcess?.(controller.child);
|
|
@@ -292,7 +436,22 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
|
292
436
|
if (!content) {
|
|
293
437
|
throw new Error("Codex completed without returning a response.");
|
|
294
438
|
}
|
|
295
|
-
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 };
|
|
296
455
|
}
|
|
297
456
|
|
|
298
457
|
export async function inspectCodexCli(workspace = process.cwd()) {
|
|
@@ -386,6 +545,7 @@ export function connectorExecution(assignment) {
|
|
|
386
545
|
|
|
387
546
|
export async function stopProcess(child) {
|
|
388
547
|
if (!child || child.exitCode !== null) return;
|
|
548
|
+
await child.engineerOsCancel?.();
|
|
389
549
|
if (process.platform === "win32") {
|
|
390
550
|
await run(
|
|
391
551
|
"taskkill",
|
|
@@ -563,6 +723,13 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
563
723
|
return { child, completed };
|
|
564
724
|
}
|
|
565
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
|
+
|
|
566
733
|
function runCodexCommand(command, args, cwd) {
|
|
567
734
|
return new Promise((resolve, reject) => {
|
|
568
735
|
const child = spawn(command, args, {
|