@engineeros/connector 0.2.1 → 0.4.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 CHANGED
@@ -10,7 +10,11 @@ Create a connection command from **Project steering -> Connect workspace**, then
10
10
  npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engineeros.example --workspace . --onboard
11
11
  ```
12
12
 
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.
13
+ 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.
14
+
15
+ 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.
16
+
17
+ 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
18
 
15
19
  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
20
 
@@ -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.0",
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
@@ -112,6 +112,52 @@ export async function executeAssignment(assignment, config, callbacks) {
112
112
  };
113
113
  }
114
114
 
115
+ export async function executeWorkspaceAssessment(assignment, config, callbacks) {
116
+ const revision = await run(
117
+ "git",
118
+ ["rev-parse", "HEAD"],
119
+ config.workspace,
120
+ { allowFailure: true },
121
+ );
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`;
123
+ const controller = launchCodexProcess(
124
+ config.workspace,
125
+ instruction,
126
+ "read-only",
127
+ callbacks,
128
+ );
129
+ callbacks.onProcess?.(controller.child);
130
+ const completed = await controller.completed;
131
+ const report = completed.finalMessage.trim();
132
+ if (!report.startsWith("# Workspace Assessment")) {
133
+ throw new Error(
134
+ "Codex completed without returning the required structured Workspace Assessment.",
135
+ );
136
+ }
137
+ return {
138
+ report_markdown: report,
139
+ observed_head_revision:
140
+ revision.code === 0 ? revision.stdout.trim().slice(0, 128) : null,
141
+ };
142
+ }
143
+
144
+ 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`;
146
+ const controller = launchCodexProcess(
147
+ config.workspace,
148
+ instruction,
149
+ "read-only",
150
+ callbacks,
151
+ );
152
+ callbacks.onProcess?.(controller.child);
153
+ const completed = await controller.completed;
154
+ const content = completed.finalMessage.trim();
155
+ if (!content) {
156
+ throw new Error("Codex completed without returning a response.");
157
+ }
158
+ return { content, model: "codex-cli" };
159
+ }
160
+
115
161
  export async function stopProcess(child) {
116
162
  if (!child || child.exitCode !== null) return;
117
163
  if (process.platform === "win32") {
@@ -233,6 +279,11 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
233
279
  }
234
280
 
235
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
+ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
236
287
  const command =
237
288
  process.env.CODEX_BIN ||
238
289
  (process.platform === "win32" ? "codex.cmd" : "codex");
@@ -240,7 +291,7 @@ function launchCodex(workspace, packet, callbacks) {
240
291
  "exec",
241
292
  "--json",
242
293
  "--sandbox",
243
- "workspace-write",
294
+ sandbox,
244
295
  "-C",
245
296
  workspace,
246
297
  "-",
@@ -251,10 +302,10 @@ function launchCodex(workspace, packet, callbacks) {
251
302
  shell: process.platform === "win32",
252
303
  stdio: ["pipe", "pipe", "pipe"],
253
304
  });
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
305
  child.stdin.end(prompt);
256
306
  let output = "";
257
307
  let buffer = "";
308
+ let finalMessage = "";
258
309
  child.stdout.setEncoding("utf8");
259
310
  child.stdout.on("data", (chunk) => {
260
311
  output += chunk;
@@ -265,6 +316,13 @@ function launchCodex(workspace, packet, callbacks) {
265
316
  if (!line.trim()) continue;
266
317
  try {
267
318
  const event = JSON.parse(line);
319
+ if (
320
+ event.type === "item.completed" &&
321
+ event.item?.type === "agent_message" &&
322
+ typeof event.item.text === "string"
323
+ ) {
324
+ finalMessage = event.item.text;
325
+ }
268
326
  callbacks.onEvent?.(event);
269
327
  } catch {
270
328
  callbacks.onEvent?.({
@@ -285,7 +343,7 @@ function launchCodex(workspace, packet, callbacks) {
285
343
  const completed = new Promise((resolve, reject) => {
286
344
  child.once("error", reject);
287
345
  child.once("close", (code) => {
288
- if (code === 0) resolve(output.slice(-20_000));
346
+ if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
289
347
  else
290
348
  reject(
291
349
  new Error(`Codex exited with code ${code}. ${output.slice(-1_000)}`),