@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 +10 -1
- package/bin/engineeros-connector.mjs +166 -2
- package/package.json +1 -1
- package/src/config.mjs +7 -0
- package/src/runner.mjs +69 -6
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
|
|
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
|
|
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
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 =
|
|
91
|
+
const controller = launchCodexProcess(
|
|
91
92
|
runWorkspace,
|
|
92
|
-
|
|
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
|
|
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
|
-
|
|
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)}`),
|