@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 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
- event.message || event.item?.text || event.type || "Codex is assessing the workspace";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.4.0",
3
+ "version": "0.4.4",
4
4
  "description": "Connect a local Codex CLI workspace to EngineerOS over an outbound WebSocket.",
5
5
  "private": false,
6
6
  "type": "module",
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 { lstat, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
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 = launchCodex(
98
+ const controller = launchCodexProcess(
91
99
  runWorkspace,
92
- assignment.packet_markdown,
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(assignment, config, callbacks) {
116
- const revision = await run(
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 instruction = `${assignment.prompt_markdown}\n\n## EngineerOS execution instruction\n\nAssess the current workspace at the requested inventory revision ${assignment.source_revision}. Remain read-only. Return only the required structured Markdown assessment as your final answer.\n`;
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
- instruction,
126
- "read-only",
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.startsWith("# Workspace Assessment")) {
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
- "Codex completed without returning the required structured Workspace Assessment.",
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 instruction = `${assignment.prompt_markdown}\n\n## EngineerOS connector instruction\n\nAnswer this project prompt from the connected workspace. Remain read-only and return only the requested response as your final answer.\n`;
180
+ const execution = connectorExecution(assignment);
146
181
  const controller = launchCodexProcess(
147
182
  config.workspace,
148
- instruction,
149
- "read-only",
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 (!normalized || normalized === ".." || normalized.startsWith("../") || path.isAbsolute(normalized)) {
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 CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name));
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) {