@engineeros/connector 0.2.1 → 0.4.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 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:
@@ -10,7 +15,11 @@ Create a connection command from **Project steering -> Connect workspace**, then
10
15
  npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engineeros.example --workspace . --onboard
11
16
  ```
12
17
 
13
- The connector uploads a bounded ZIP snapshot for assessment, then stays online for rescans and Goal Runs. Onboarding never executes repository code. It excludes known secrets, dependency directories, build output, compiled binaries, files larger than 5 MB, agent-tool caches, Git metadata, and connector state before upload.
18
+ The connector uploads a bounded ZIP snapshot for a safe file inventory, then stays online for deep assessments, rescans, and Goal Runs. Inventory never executes repository code. It excludes known secrets, dependency directories, build output, compiled binaries, files larger than 5 MB, agent-tool caches, Git metadata, and connector state before upload.
19
+
20
+ From **Project steering -> Workspace**, choose **Assess with Codex** to run a read-only engineering assessment using the Codex CLI subscription already authenticated on that computer. EngineerOS stores the source-cited findings in System State and promotes the highest-return corrective action in Steering. Assessment cannot modify the workspace.
21
+
22
+ After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the Codex CLI subscription and connected workspace context. Prompt runs are read-only; only an explicitly registered Goal Run receives workspace-write access. If the connector is offline, EngineerOS asks the user to reconnect instead of silently switching models.
14
23
 
15
24
  An empty or document-only folder establishes a greenfield baseline. A code-bearing folder is assessed as brownfield. Use **Rescan** in Steering after the local workspace changes.
16
25
 
@@ -2,6 +2,7 @@
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import {
5
+ assessmentResultUrl,
5
6
  loadConfig,
6
7
  resultUrl,
7
8
  saveConfig,
@@ -10,6 +11,8 @@ import {
10
11
  } from "../src/config.mjs";
11
12
  import {
12
13
  executeAssignment,
14
+ executeConnectedPrompt,
15
+ executeWorkspaceAssessment,
13
16
  stopProcess,
14
17
  workspaceSnapshot,
15
18
  } from "../src/runner.mjs";
@@ -73,6 +76,8 @@ if (command === "pair") {
73
76
  let stopped = false;
74
77
  let active = null;
75
78
  const available = [];
79
+ const assessments = [];
80
+ const prompts = [];
76
81
  let socket;
77
82
  let pingTimer;
78
83
  let reconnectDelay = 1_000;
@@ -126,6 +131,37 @@ async function connect() {
126
131
  void submitWorkspaceSnapshot();
127
132
  return;
128
133
  }
134
+ if (message.type === "workspace.assessment") {
135
+ if (
136
+ active?.runId !== message.assessment_id &&
137
+ !assessments.some((candidate) => candidate.assessment_id === message.assessment_id)
138
+ ) {
139
+ assessments.push(message);
140
+ }
141
+ pump();
142
+ return;
143
+ }
144
+ if (message.type === "prompt.execute") {
145
+ if (
146
+ active?.runId !== message.prompt_id &&
147
+ !prompts.some((candidate) => candidate.prompt_id === message.prompt_id)
148
+ ) {
149
+ prompts.push(message);
150
+ }
151
+ pump();
152
+ return;
153
+ }
154
+ if (message.type === "prompt.cancel") {
155
+ const queued = prompts.findIndex(
156
+ (candidate) => candidate.prompt_id === message.prompt_id,
157
+ );
158
+ if (queued >= 0) prompts.splice(queued, 1);
159
+ if (active?.kind === "prompt" && active.runId === message.prompt_id) {
160
+ active.cancelled = true;
161
+ await stopProcess(active.child);
162
+ }
163
+ return;
164
+ }
129
165
  if (message.type === "run.available") {
130
166
  if (!available.includes(message.run_id)) available.push(message.run_id);
131
167
  pump();
@@ -162,6 +198,10 @@ async function connect() {
162
198
  });
163
199
  socket.addEventListener("close", () => {
164
200
  clearInterval(pingTimer);
201
+ if (active?.kind === "prompt") {
202
+ active.cancelled = true;
203
+ void stopProcess(active.child);
204
+ }
165
205
  if (connectionRejected) {
166
206
  stopped = true;
167
207
  console.error(
@@ -226,7 +266,7 @@ function startPings() {
226
266
  socket.send(
227
267
  JSON.stringify({
228
268
  type: "ping",
229
- active_run_id: active?.runId ?? null,
269
+ active_run_id: active?.kind === "goal" ? active.runId : null,
230
270
  }),
231
271
  );
232
272
  }
@@ -235,9 +275,30 @@ function startPings() {
235
275
 
236
276
  function pump() {
237
277
  if (active || socket.readyState !== WebSocket.OPEN) return;
278
+ const prompt = prompts.shift();
279
+ if (prompt) {
280
+ active = {
281
+ kind: "prompt",
282
+ runId: prompt.prompt_id,
283
+ child: null,
284
+ cancelled: false,
285
+ };
286
+ void executePrompt(prompt);
287
+ return;
288
+ }
289
+ const assessment = assessments.shift();
290
+ if (assessment) {
291
+ active = {
292
+ kind: "assessment",
293
+ runId: assessment.assessment_id,
294
+ child: null,
295
+ };
296
+ void executeAssessment(assessment);
297
+ return;
298
+ }
238
299
  const runId = available.shift();
239
300
  if (!runId) return;
240
- active = { runId, child: null, cancelled: false };
301
+ active = { kind: "goal", runId, child: null, cancelled: false };
241
302
  socket.send(
242
303
  JSON.stringify({
243
304
  type: "run.claim",
@@ -251,6 +312,109 @@ function pump() {
251
312
  );
252
313
  }
253
314
 
315
+ async function executePrompt(assignment) {
316
+ const promptId = assignment.prompt_id;
317
+ console.log(`Answering ${assignment.purpose || "project"} prompt with Codex CLI.`);
318
+ try {
319
+ const result = await executeConnectedPrompt(assignment, config, {
320
+ onProcess: (child) => {
321
+ if (active?.runId === promptId) active.child = child;
322
+ },
323
+ });
324
+ if (active?.cancelled || socket.readyState !== WebSocket.OPEN) return;
325
+ socket.send(
326
+ JSON.stringify({
327
+ type: "prompt.completed",
328
+ prompt_id: promptId,
329
+ content: result.content,
330
+ model: result.model,
331
+ }),
332
+ );
333
+ } catch (error) {
334
+ if (!active?.cancelled && socket.readyState === WebSocket.OPEN) {
335
+ socket.send(
336
+ JSON.stringify({
337
+ type: "prompt.failed",
338
+ prompt_id: promptId,
339
+ message: error instanceof Error ? error.message : String(error),
340
+ }),
341
+ );
342
+ }
343
+ if (!active?.cancelled) {
344
+ console.error(error instanceof Error ? error.message : String(error));
345
+ }
346
+ } finally {
347
+ active = null;
348
+ pump();
349
+ }
350
+ }
351
+
352
+ async function executeAssessment(assignment) {
353
+ const assessmentId = assignment.assessment_id;
354
+ console.log(`Assessing workspace with Codex CLI (${assessmentId}).`);
355
+ let progress = 10;
356
+ const reportProgress = (message) => {
357
+ if (socket.readyState !== WebSocket.OPEN || active?.runId !== assessmentId) return;
358
+ socket.send(
359
+ JSON.stringify({
360
+ type: "workspace.assessment.progress",
361
+ assessment_id: assessmentId,
362
+ progress_percent: progress,
363
+ message: String(message).slice(0, 500),
364
+ }),
365
+ );
366
+ };
367
+ reportProgress("Codex is inspecting the workspace");
368
+ const heartbeat = setInterval(() => {
369
+ progress = Math.min(90, progress + 5);
370
+ reportProgress("Codex is building the evidence-backed assessment");
371
+ }, 15_000);
372
+ try {
373
+ const result = await executeWorkspaceAssessment(assignment, config, {
374
+ onProcess: (child) => {
375
+ if (active?.runId === assessmentId) active.child = child;
376
+ },
377
+ onEvent: (event) => {
378
+ const message =
379
+ event.message || event.item?.text || event.type || "Codex is assessing the workspace";
380
+ reportProgress(message);
381
+ },
382
+ });
383
+ const response = await fetch(
384
+ assessmentResultUrl(config.server_url, config.connector_id, assessmentId),
385
+ {
386
+ method: "POST",
387
+ headers: {
388
+ "Content-Type": "application/json",
389
+ Authorization: `Bearer ${config.token}`,
390
+ },
391
+ body: JSON.stringify(result),
392
+ },
393
+ );
394
+ if (!response.ok) {
395
+ throw new Error(
396
+ `EngineerOS rejected the assessment (${response.status}): ${await response.text()}`,
397
+ );
398
+ }
399
+ console.log("Codex workspace assessment is current in EngineerOS.");
400
+ } catch (error) {
401
+ if (socket.readyState === WebSocket.OPEN) {
402
+ socket.send(
403
+ JSON.stringify({
404
+ type: "workspace.assessment.failed",
405
+ assessment_id: assessmentId,
406
+ message: error instanceof Error ? error.message : String(error),
407
+ }),
408
+ );
409
+ }
410
+ console.error(error instanceof Error ? error.message : String(error));
411
+ } finally {
412
+ clearInterval(heartbeat);
413
+ active = null;
414
+ pump();
415
+ }
416
+ }
417
+
254
418
  async function execute(assignment) {
255
419
  const runId = assignment.run_id;
256
420
  console.log(`Running Goal ${runId} with Codex CLI.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.2.1",
3
+ "version": "0.4.1",
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/config.mjs CHANGED
@@ -38,6 +38,13 @@ export function workspaceUrl(websocketUrl, connectorId) {
38
38
  return url.toString();
39
39
  }
40
40
 
41
+ export function assessmentResultUrl(websocketUrl, connectorId, assessmentId) {
42
+ const url = new URL(websocketUrl);
43
+ url.protocol = url.protocol === "wss:" ? "https:" : "http:";
44
+ url.pathname = `/api/v1/agent-connectors/${connectorId}/assessment/${assessmentId}/result`;
45
+ return url.toString();
46
+ }
47
+
41
48
  export async function loadConfig(workspace = process.cwd()) {
42
49
  try {
43
50
  return JSON.parse(await readFile(configPath(workspace), "utf8"));
package/src/runner.mjs CHANGED
@@ -82,14 +82,16 @@ const CODE_MARKERS = new Set([
82
82
  ]);
83
83
 
84
84
  export async function executeAssignment(assignment, config, callbacks) {
85
+ const execution = connectorExecution(assignment);
85
86
  const runWorkspace = await prepareRunWorkspace(
86
87
  config.workspace,
87
88
  assignment.run_id,
88
89
  assignment.base_revision,
89
90
  );
90
- const controller = launchCodex(
91
+ const controller = launchCodexProcess(
91
92
  runWorkspace,
92
- assignment.packet_markdown,
93
+ execution.prompt,
94
+ execution.sandboxMode,
93
95
  callbacks,
94
96
  );
95
97
  callbacks.onProcess?.(controller.child);
@@ -112,6 +114,60 @@ export async function executeAssignment(assignment, config, callbacks) {
112
114
  };
113
115
  }
114
116
 
117
+ export async function executeWorkspaceAssessment(assignment, config, callbacks) {
118
+ const execution = connectorExecution(assignment);
119
+ const revision = await run(
120
+ "git",
121
+ ["rev-parse", "HEAD"],
122
+ config.workspace,
123
+ { allowFailure: true },
124
+ );
125
+ const controller = launchCodexProcess(
126
+ config.workspace,
127
+ execution.prompt,
128
+ execution.sandboxMode,
129
+ callbacks,
130
+ );
131
+ callbacks.onProcess?.(controller.child);
132
+ const completed = await controller.completed;
133
+ const report = completed.finalMessage.trim();
134
+ if (!report) throw new Error("Codex completed without returning a response.");
135
+ return {
136
+ report_markdown: report,
137
+ observed_head_revision:
138
+ revision.code === 0 ? revision.stdout.trim().slice(0, 128) : null,
139
+ };
140
+ }
141
+
142
+ export async function executeConnectedPrompt(assignment, config, callbacks) {
143
+ const execution = connectorExecution(assignment);
144
+ const controller = launchCodexProcess(
145
+ config.workspace,
146
+ execution.prompt,
147
+ execution.sandboxMode,
148
+ callbacks,
149
+ );
150
+ callbacks.onProcess?.(controller.child);
151
+ const completed = await controller.completed;
152
+ const content = completed.finalMessage.trim();
153
+ if (!content) {
154
+ throw new Error("Codex completed without returning a response.");
155
+ }
156
+ return { content, model: "codex-cli" };
157
+ }
158
+
159
+ export function connectorExecution(assignment) {
160
+ const prompt = assignment?.prompt_markdown;
161
+ if (typeof prompt !== "string" || !prompt.trim()) {
162
+ throw new Error("EngineerOS assignment is missing prompt_markdown.");
163
+ }
164
+ const sandboxMode = assignment?.sandbox_mode;
165
+ if (!new Set(["read-only", "workspace-write"]).has(sandboxMode)) {
166
+ throw new Error("EngineerOS assignment has an unsupported sandbox_mode.");
167
+ }
168
+ return { prompt, sandboxMode };
169
+ }
170
+
115
171
  export async function stopProcess(child) {
116
172
  if (!child || child.exitCode !== null) return;
117
173
  if (process.platform === "win32") {
@@ -232,7 +288,7 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
232
288
  return target;
233
289
  }
234
290
 
235
- function launchCodex(workspace, packet, callbacks) {
291
+ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
236
292
  const command =
237
293
  process.env.CODEX_BIN ||
238
294
  (process.platform === "win32" ? "codex.cmd" : "codex");
@@ -240,7 +296,7 @@ function launchCodex(workspace, packet, callbacks) {
240
296
  "exec",
241
297
  "--json",
242
298
  "--sandbox",
243
- "workspace-write",
299
+ sandbox,
244
300
  "-C",
245
301
  workspace,
246
302
  "-",
@@ -251,10 +307,10 @@ function launchCodex(workspace, packet, callbacks) {
251
307
  shell: process.platform === "win32",
252
308
  stdio: ["pipe", "pipe", "pipe"],
253
309
  });
254
- 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`;
255
310
  child.stdin.end(prompt);
256
311
  let output = "";
257
312
  let buffer = "";
313
+ let finalMessage = "";
258
314
  child.stdout.setEncoding("utf8");
259
315
  child.stdout.on("data", (chunk) => {
260
316
  output += chunk;
@@ -265,6 +321,13 @@ function launchCodex(workspace, packet, callbacks) {
265
321
  if (!line.trim()) continue;
266
322
  try {
267
323
  const event = JSON.parse(line);
324
+ if (
325
+ event.type === "item.completed" &&
326
+ event.item?.type === "agent_message" &&
327
+ typeof event.item.text === "string"
328
+ ) {
329
+ finalMessage = event.item.text;
330
+ }
268
331
  callbacks.onEvent?.(event);
269
332
  } catch {
270
333
  callbacks.onEvent?.({
@@ -285,7 +348,7 @@ function launchCodex(workspace, packet, callbacks) {
285
348
  const completed = new Promise((resolve, reject) => {
286
349
  child.once("error", reject);
287
350
  child.once("close", (code) => {
288
- if (code === 0) resolve(output.slice(-20_000));
351
+ if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
289
352
  else
290
353
  reject(
291
354
  new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),