@engineeros/connector 0.4.0 → 0.4.4
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 +16 -0
- package/bin/engineeros-connector.mjs +15 -3
- package/package.json +1 -1
- package/src/runner.mjs +156 -35
package/README.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
Connect a local Codex CLI workspace to EngineerOS through an outbound WebSocket.
|
|
4
4
|
|
|
5
|
+
This package is only a connector. EngineerOS owns every prompt, Goal instruction, and
|
|
6
|
+
execution boundary. The connector validates the assignment envelope, passes the supplied
|
|
7
|
+
Markdown to Codex CLI unchanged, streams lifecycle events, and returns bounded results.
|
|
8
|
+
It contains no product, assessment, planning, architecture, or delivery prompt templates.
|
|
9
|
+
|
|
5
10
|
## Onboard a workspace
|
|
6
11
|
|
|
7
12
|
Create a connection command from **Project steering -> Connect workspace**, then run it inside the local folder:
|
|
@@ -31,3 +36,14 @@ Credentials are stored per workspace under `~/.engineeros/connectors` with owner
|
|
|
31
36
|
Keep the connector online to receive Goals assigned from EngineerOS. Each Goal runs in an isolated worktree below `~/.engineeros/runs`. Cancellation stops Codex. The connector returns changed paths, a bounded diff, and the exact repository ZIP; a human still performs independent attestation.
|
|
32
37
|
|
|
33
38
|
Requirements: Node.js 22 or newer, Git, and an authenticated Codex CLI (`codex login`).
|
|
39
|
+
|
|
40
|
+
## Codex CLI compatibility
|
|
41
|
+
|
|
42
|
+
The connector prints the exact Codex CLI version it will use before connecting. If the
|
|
43
|
+
configured model requires a newer CLI, update Codex and restart the connector:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm install -g @openai/codex@latest
|
|
47
|
+
codex --version
|
|
48
|
+
npx @engineeros/connector start --workspace .
|
|
49
|
+
```
|
|
@@ -10,9 +10,11 @@ import {
|
|
|
10
10
|
workspaceUrl,
|
|
11
11
|
} from "../src/config.mjs";
|
|
12
12
|
import {
|
|
13
|
+
assessmentProgressMessage,
|
|
13
14
|
executeAssignment,
|
|
14
15
|
executeConnectedPrompt,
|
|
15
16
|
executeWorkspaceAssessment,
|
|
17
|
+
inspectCodexCli,
|
|
16
18
|
stopProcess,
|
|
17
19
|
workspaceSnapshot,
|
|
18
20
|
} from "../src/runner.mjs";
|
|
@@ -73,6 +75,17 @@ if (command === "pair") {
|
|
|
73
75
|
fail("Use `engineeros-connector pair`, `start`, or `status`.");
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
let codexCli;
|
|
79
|
+
try {
|
|
80
|
+
codexCli = await inspectCodexCli(config.workspace);
|
|
81
|
+
} catch (error) {
|
|
82
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
83
|
+
}
|
|
84
|
+
console.log(`Using ${codexCli.version}.`);
|
|
85
|
+
if (firstMessage.type === "pair") {
|
|
86
|
+
firstMessage.capabilities.codex_cli_version = codexCli.version;
|
|
87
|
+
}
|
|
88
|
+
|
|
76
89
|
let stopped = false;
|
|
77
90
|
let active = null;
|
|
78
91
|
const available = [];
|
|
@@ -375,9 +388,8 @@ async function executeAssessment(assignment) {
|
|
|
375
388
|
if (active?.runId === assessmentId) active.child = child;
|
|
376
389
|
},
|
|
377
390
|
onEvent: (event) => {
|
|
378
|
-
const message =
|
|
379
|
-
|
|
380
|
-
reportProgress(message);
|
|
391
|
+
const message = assessmentProgressMessage(event);
|
|
392
|
+
if (message) reportProgress(message);
|
|
381
393
|
},
|
|
382
394
|
});
|
|
383
395
|
const response = await fetch(
|
package/package.json
CHANGED
package/src/runner.mjs
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
stat,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from "node:fs/promises";
|
|
4
11
|
import os from "node:os";
|
|
5
12
|
import path from "node:path";
|
|
6
13
|
import { promisify } from "node:util";
|
|
@@ -82,14 +89,16 @@ const CODE_MARKERS = new Set([
|
|
|
82
89
|
]);
|
|
83
90
|
|
|
84
91
|
export async function executeAssignment(assignment, config, callbacks) {
|
|
92
|
+
const execution = connectorExecution(assignment);
|
|
85
93
|
const runWorkspace = await prepareRunWorkspace(
|
|
86
94
|
config.workspace,
|
|
87
95
|
assignment.run_id,
|
|
88
96
|
assignment.base_revision,
|
|
89
97
|
);
|
|
90
|
-
const controller =
|
|
98
|
+
const controller = launchCodexProcess(
|
|
91
99
|
runWorkspace,
|
|
92
|
-
|
|
100
|
+
execution.prompt,
|
|
101
|
+
execution.sandboxMode,
|
|
93
102
|
callbacks,
|
|
94
103
|
);
|
|
95
104
|
callbacks.onProcess?.(controller.child);
|
|
@@ -112,41 +121,67 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
112
121
|
};
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
export async function executeWorkspaceAssessment(
|
|
116
|
-
|
|
124
|
+
export async function executeWorkspaceAssessment(
|
|
125
|
+
assignment,
|
|
126
|
+
config,
|
|
127
|
+
callbacks,
|
|
128
|
+
) {
|
|
129
|
+
const execution = connectorExecution(assignment);
|
|
130
|
+
const startingRevision = await run(
|
|
117
131
|
"git",
|
|
118
132
|
["rev-parse", "HEAD"],
|
|
119
133
|
config.workspace,
|
|
120
134
|
{ allowFailure: true },
|
|
121
135
|
);
|
|
122
|
-
const
|
|
136
|
+
const startingHead =
|
|
137
|
+
startingRevision.code === 0
|
|
138
|
+
? startingRevision.stdout.trim().slice(0, 128)
|
|
139
|
+
: null;
|
|
140
|
+
if (
|
|
141
|
+
assignment.target_head_revision &&
|
|
142
|
+
startingHead !== assignment.target_head_revision
|
|
143
|
+
) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
"This workspace is no longer at the commit inventoried by EngineerOS. Refresh the workspace inventory before assessing it.",
|
|
146
|
+
);
|
|
147
|
+
}
|
|
123
148
|
const controller = launchCodexProcess(
|
|
124
149
|
config.workspace,
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
execution.prompt,
|
|
151
|
+
execution.sandboxMode,
|
|
127
152
|
callbacks,
|
|
128
153
|
);
|
|
129
154
|
callbacks.onProcess?.(controller.child);
|
|
130
155
|
const completed = await controller.completed;
|
|
131
156
|
const report = completed.finalMessage.trim();
|
|
132
|
-
if (!report
|
|
157
|
+
if (!report) throw new Error("Codex completed without returning a response.");
|
|
158
|
+
const endingRevision = await run(
|
|
159
|
+
"git",
|
|
160
|
+
["rev-parse", "HEAD"],
|
|
161
|
+
config.workspace,
|
|
162
|
+
{ allowFailure: true },
|
|
163
|
+
);
|
|
164
|
+
const endingHead =
|
|
165
|
+
endingRevision.code === 0
|
|
166
|
+
? endingRevision.stdout.trim().slice(0, 128)
|
|
167
|
+
: null;
|
|
168
|
+
if (startingHead !== endingHead) {
|
|
133
169
|
throw new Error(
|
|
134
|
-
"
|
|
170
|
+
"The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
|
|
135
171
|
);
|
|
136
172
|
}
|
|
137
173
|
return {
|
|
138
174
|
report_markdown: report,
|
|
139
|
-
observed_head_revision:
|
|
140
|
-
revision.code === 0 ? revision.stdout.trim().slice(0, 128) : null,
|
|
175
|
+
observed_head_revision: endingHead,
|
|
141
176
|
};
|
|
142
177
|
}
|
|
143
178
|
|
|
144
179
|
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
145
|
-
const
|
|
180
|
+
const execution = connectorExecution(assignment);
|
|
146
181
|
const controller = launchCodexProcess(
|
|
147
182
|
config.workspace,
|
|
148
|
-
|
|
149
|
-
|
|
183
|
+
execution.prompt,
|
|
184
|
+
execution.sandboxMode,
|
|
150
185
|
callbacks,
|
|
151
186
|
);
|
|
152
187
|
callbacks.onProcess?.(controller.child);
|
|
@@ -158,6 +193,82 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
|
158
193
|
return { content, model: "codex-cli" };
|
|
159
194
|
}
|
|
160
195
|
|
|
196
|
+
export async function inspectCodexCli(workspace = process.cwd()) {
|
|
197
|
+
const command =
|
|
198
|
+
process.env.CODEX_BIN ||
|
|
199
|
+
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
200
|
+
const result = await runCodexCommand(command, ["--version"], workspace);
|
|
201
|
+
if (result.code !== 0) {
|
|
202
|
+
throw new Error(
|
|
203
|
+
"Codex CLI is unavailable. Install it with `npm install -g @openai/codex@latest`, run `codex login`, then restart this connector.",
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
const version = result.stdout.trim().slice(0, 100);
|
|
207
|
+
if (!version) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Codex CLI returned no version. Reinstall @openai/codex, then restart this connector.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return { command, version };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function codexFailureMessage(output, code) {
|
|
216
|
+
if (/requires a newer version of Codex/i.test(output)) {
|
|
217
|
+
return (
|
|
218
|
+
"The configured model requires a newer Codex CLI. " +
|
|
219
|
+
"Run `npm install -g @openai/codex@latest`, verify with `codex --version`, " +
|
|
220
|
+
"then restart the EngineerOS connector and retry the assessment."
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
if (/not logged in|login required|authentication required/i.test(output)) {
|
|
224
|
+
return "Codex CLI is not authenticated. Run `codex login`, then restart the EngineerOS connector.";
|
|
225
|
+
}
|
|
226
|
+
return `Codex exited with code ${code}. ${output.slice(-1_000)}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export function assessmentProgressMessage(event) {
|
|
230
|
+
if (!event || typeof event !== "object") return null;
|
|
231
|
+
if (event.type === "turn.started")
|
|
232
|
+
return "Codex started analyzing repository evidence";
|
|
233
|
+
if (event.type === "item.started") {
|
|
234
|
+
if (event.item?.type === "command_execution") {
|
|
235
|
+
const command = compactCommand(event.item.command);
|
|
236
|
+
return command
|
|
237
|
+
? `Inspecting with ${command}`
|
|
238
|
+
: "Inspecting repository files";
|
|
239
|
+
}
|
|
240
|
+
if (event.item?.type === "mcp_tool_call")
|
|
241
|
+
return "Consulting a connected analysis tool";
|
|
242
|
+
if (event.item?.type === "web_search")
|
|
243
|
+
return "Checking an external technical reference";
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
247
|
+
return "Assessment report prepared";
|
|
248
|
+
}
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function compactCommand(command) {
|
|
253
|
+
const value = Array.isArray(command)
|
|
254
|
+
? command.join(" ")
|
|
255
|
+
: String(command || "");
|
|
256
|
+
const compact = value.replace(/\s+/g, " ").trim();
|
|
257
|
+
return compact.length > 96 ? `${compact.slice(0, 93)}...` : compact;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function connectorExecution(assignment) {
|
|
261
|
+
const prompt = assignment?.prompt_markdown;
|
|
262
|
+
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
263
|
+
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|
|
264
|
+
}
|
|
265
|
+
const sandboxMode = assignment?.sandbox_mode;
|
|
266
|
+
if (!new Set(["read-only", "workspace-write"]).has(sandboxMode)) {
|
|
267
|
+
throw new Error("EngineerOS assignment has an unsupported sandbox_mode.");
|
|
268
|
+
}
|
|
269
|
+
return { prompt, sandboxMode };
|
|
270
|
+
}
|
|
271
|
+
|
|
161
272
|
export async function stopProcess(child) {
|
|
162
273
|
if (!child || child.exitCode !== null) return;
|
|
163
274
|
if (process.platform === "win32") {
|
|
@@ -278,24 +389,11 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
|
|
|
278
389
|
return target;
|
|
279
390
|
}
|
|
280
391
|
|
|
281
|
-
function launchCodex(workspace, packet, callbacks) {
|
|
282
|
-
const prompt = `${packet}\n\n## EngineerOS execution instruction\n\nImplement this frozen Goal completely in the current run workspace. Run the required verification. Do not commit, push, or modify files outside this workspace. End with a concise result and verification summary.\n`;
|
|
283
|
-
return launchCodexProcess(workspace, prompt, "workspace-write", callbacks);
|
|
284
|
-
}
|
|
285
|
-
|
|
286
392
|
function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
287
393
|
const command =
|
|
288
394
|
process.env.CODEX_BIN ||
|
|
289
395
|
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
290
|
-
const args = [
|
|
291
|
-
"exec",
|
|
292
|
-
"--json",
|
|
293
|
-
"--sandbox",
|
|
294
|
-
sandbox,
|
|
295
|
-
"-C",
|
|
296
|
-
workspace,
|
|
297
|
-
"-",
|
|
298
|
-
];
|
|
396
|
+
const args = ["exec", "--json", "--sandbox", sandbox, "-C", workspace, "-"];
|
|
299
397
|
const child = spawn(command, args, {
|
|
300
398
|
cwd: workspace,
|
|
301
399
|
env: process.env,
|
|
@@ -344,15 +442,31 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
344
442
|
child.once("error", reject);
|
|
345
443
|
child.once("close", (code) => {
|
|
346
444
|
if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
|
|
347
|
-
else
|
|
348
|
-
reject(
|
|
349
|
-
new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),
|
|
350
|
-
);
|
|
445
|
+
else reject(new Error(codexFailureMessage(output, code)));
|
|
351
446
|
});
|
|
352
447
|
});
|
|
353
448
|
return { child, completed };
|
|
354
449
|
}
|
|
355
450
|
|
|
451
|
+
function runCodexCommand(command, args, cwd) {
|
|
452
|
+
return new Promise((resolve, reject) => {
|
|
453
|
+
const child = spawn(command, args, {
|
|
454
|
+
cwd,
|
|
455
|
+
env: process.env,
|
|
456
|
+
shell: process.platform === "win32",
|
|
457
|
+
windowsHide: true,
|
|
458
|
+
});
|
|
459
|
+
let stdout = "";
|
|
460
|
+
let stderr = "";
|
|
461
|
+
child.stdout.setEncoding("utf8");
|
|
462
|
+
child.stderr.setEncoding("utf8");
|
|
463
|
+
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
464
|
+
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
465
|
+
child.once("error", reject);
|
|
466
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
356
470
|
async function changedFilePaths(workspace) {
|
|
357
471
|
const tracked = await run(
|
|
358
472
|
"git",
|
|
@@ -447,7 +561,12 @@ async function workspaceFiles(root, current = root) {
|
|
|
447
561
|
|
|
448
562
|
function isShareablePath(relative) {
|
|
449
563
|
const normalized = normalizePath(relative);
|
|
450
|
-
if (
|
|
564
|
+
if (
|
|
565
|
+
!normalized ||
|
|
566
|
+
normalized === ".." ||
|
|
567
|
+
normalized.startsWith("../") ||
|
|
568
|
+
path.isAbsolute(normalized)
|
|
569
|
+
) {
|
|
451
570
|
return false;
|
|
452
571
|
}
|
|
453
572
|
const parts = normalized.split("/");
|
|
@@ -477,7 +596,9 @@ function isShareablePath(relative) {
|
|
|
477
596
|
|
|
478
597
|
function isCodeBearing(relative) {
|
|
479
598
|
const name = path.posix.basename(relative).toLowerCase();
|
|
480
|
-
return
|
|
599
|
+
return (
|
|
600
|
+
CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name))
|
|
601
|
+
);
|
|
481
602
|
}
|
|
482
603
|
|
|
483
604
|
async function worktreeBase(workspace, requested) {
|