@engineeros/connector 0.6.2 → 0.8.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 +12 -0
- package/bin/engineeros-connector.mjs +6 -0
- package/package.json +1 -1
- package/src/acp-client.mjs +5 -1
- package/src/capabilities.mjs +14 -0
- package/src/runner.mjs +148 -28
package/README.md
CHANGED
|
@@ -33,6 +33,8 @@ From **Project steering -> Workspace**, choose **Assess with Codex** to run a re
|
|
|
33
33
|
|
|
34
34
|
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.
|
|
35
35
|
|
|
36
|
+
Project prompts use resumable, purpose-specific coding-agent sessions. The connector keeps the external session identifiers in its local configuration, so Copilot and artifact conversations survive connector restarts. Changing the purpose, model, or reasoning effort starts a separate session. Goal implementation and verification remain isolated runs.
|
|
37
|
+
|
|
36
38
|
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.
|
|
37
39
|
|
|
38
40
|
## Reconnect
|
|
@@ -49,6 +51,16 @@ Keep the connector online to receive Goals assigned from EngineerOS. Each Goal r
|
|
|
49
51
|
|
|
50
52
|
Requirements: Node.js 22 or newer, Git, and an authenticated Codex CLI (`codex login`).
|
|
51
53
|
|
|
54
|
+
## Execution profiles
|
|
55
|
+
|
|
56
|
+
EngineerOS can set a workspace default model and reasoning effort, then override either value for an individual Goal. The same workspace default is used for assessments, Copilot, and generated artifacts. The connector passes the resolved values to Codex CLI and reports unsupported profiles instead of silently ignoring them.
|
|
57
|
+
|
|
58
|
+
Codex connectors advertise `gpt-5.6-sol` and `gpt-5.6-terra` by default. Override the choices shown in EngineerOS before starting the connector:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
ENGINEEROS_AGENT_MODELS="model-a,model-b" npx @engineeros/connector start --workspace .
|
|
62
|
+
```
|
|
63
|
+
|
|
52
64
|
## Codex CLI compatibility
|
|
53
65
|
|
|
54
66
|
The connector prints the exact Codex CLI version it will use before connecting. If the
|
|
@@ -375,6 +375,11 @@ async function executePrompt(assignment) {
|
|
|
375
375
|
if (active?.runId === promptId) active.child = child;
|
|
376
376
|
},
|
|
377
377
|
});
|
|
378
|
+
config = {
|
|
379
|
+
...config,
|
|
380
|
+
sessions: { ...(config.sessions || {}), [result.sessionKey]: result.sessionId },
|
|
381
|
+
};
|
|
382
|
+
await saveConfig(config);
|
|
378
383
|
if (active?.cancelled || socket.readyState !== WebSocket.OPEN) return;
|
|
379
384
|
socket.send(
|
|
380
385
|
JSON.stringify({
|
|
@@ -382,6 +387,7 @@ async function executePrompt(assignment) {
|
|
|
382
387
|
prompt_id: promptId,
|
|
383
388
|
content: result.content,
|
|
384
389
|
model: result.model,
|
|
390
|
+
session_id: result.sessionId,
|
|
385
391
|
}),
|
|
386
392
|
);
|
|
387
393
|
} catch (error) {
|
package/package.json
CHANGED
package/src/acp-client.mjs
CHANGED
|
@@ -68,7 +68,11 @@ export function launchAcpAgent(workspace, prompt, config, callbacks = {}) {
|
|
|
68
68
|
if (message.stopReason === "error") {
|
|
69
69
|
throw new Error("The ACP agent ended the task with an error.");
|
|
70
70
|
}
|
|
71
|
-
return {
|
|
71
|
+
return {
|
|
72
|
+
finalMessage,
|
|
73
|
+
model: "acp-agent",
|
|
74
|
+
sessionId: session.sessionId,
|
|
75
|
+
};
|
|
72
76
|
}
|
|
73
77
|
const update = message.update;
|
|
74
78
|
if (
|
package/src/capabilities.mjs
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
3
|
export function advertisedCapabilities(config, codingAgent) {
|
|
4
|
+
const configuredModels = String(
|
|
5
|
+
process.env.ENGINEEROS_AGENT_MODELS || "gpt-5.6-sol,gpt-5.6-terra",
|
|
6
|
+
)
|
|
7
|
+
.split(",")
|
|
8
|
+
.map((model) => model.trim())
|
|
9
|
+
.filter(Boolean);
|
|
4
10
|
return {
|
|
5
11
|
agent_protocols: [codingAgent.protocol],
|
|
6
12
|
coding_agent: true,
|
|
@@ -9,5 +15,13 @@ export function advertisedCapabilities(config, codingAgent) {
|
|
|
9
15
|
workspace_name: path.basename(config.workspace),
|
|
10
16
|
agent_name: codingAgent.name,
|
|
11
17
|
agent_version: codingAgent.version,
|
|
18
|
+
execution_profiles: {
|
|
19
|
+
model_selection: codingAgent.protocol === "codex",
|
|
20
|
+
models: codingAgent.protocol === "codex" ? configuredModels : [],
|
|
21
|
+
reasoning_efforts:
|
|
22
|
+
codingAgent.protocol === "codex"
|
|
23
|
+
? ["low", "medium", "high", "xhigh"]
|
|
24
|
+
: [],
|
|
25
|
+
},
|
|
12
26
|
};
|
|
13
27
|
}
|
package/src/runner.mjs
CHANGED
|
@@ -101,6 +101,7 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
101
101
|
execution.sandboxMode,
|
|
102
102
|
config,
|
|
103
103
|
callbacks,
|
|
104
|
+
execution.profile,
|
|
104
105
|
);
|
|
105
106
|
callbacks.onProcess?.(controller.child);
|
|
106
107
|
await controller.completed;
|
|
@@ -118,6 +119,7 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
118
119
|
"read-only",
|
|
119
120
|
config,
|
|
120
121
|
callbacks,
|
|
122
|
+
execution.profile,
|
|
121
123
|
);
|
|
122
124
|
callbacks.onProcess?.(verifier.child);
|
|
123
125
|
const verification = await verifier.completed;
|
|
@@ -138,12 +140,19 @@ export async function executeAssignment(assignment, config, callbacks) {
|
|
|
138
140
|
};
|
|
139
141
|
}
|
|
140
142
|
|
|
141
|
-
export async function applyAcceptedChange(
|
|
143
|
+
export async function applyAcceptedChange(
|
|
144
|
+
workspace,
|
|
145
|
+
baseRevision,
|
|
146
|
+
headRevision,
|
|
147
|
+
) {
|
|
142
148
|
const current = await run("git", ["rev-parse", "HEAD"], workspace, {
|
|
143
149
|
allowFailure: true,
|
|
144
150
|
});
|
|
145
151
|
if (current.code !== 0) {
|
|
146
|
-
return {
|
|
152
|
+
return {
|
|
153
|
+
applied: false,
|
|
154
|
+
reason: "The connected workspace is not a Git repository.",
|
|
155
|
+
};
|
|
147
156
|
}
|
|
148
157
|
const currentHead = current.stdout.trim();
|
|
149
158
|
const alreadyApplied = await run(
|
|
@@ -152,7 +161,8 @@ export async function applyAcceptedChange(workspace, baseRevision, headRevision)
|
|
|
152
161
|
workspace,
|
|
153
162
|
{ allowFailure: true },
|
|
154
163
|
);
|
|
155
|
-
if (alreadyApplied.code === 0)
|
|
164
|
+
if (alreadyApplied.code === 0)
|
|
165
|
+
return { applied: true, revision: currentHead };
|
|
156
166
|
const status = await run("git", ["status", "--porcelain"], workspace);
|
|
157
167
|
if (status.stdout.trim()) {
|
|
158
168
|
return {
|
|
@@ -242,14 +252,22 @@ Return structured Markdown only, with exactly one section per Proof:
|
|
|
242
252
|
Do not claim a Proof passed unless you observed it directly.`;
|
|
243
253
|
}
|
|
244
254
|
|
|
245
|
-
export function parseVerificationReport(
|
|
246
|
-
|
|
255
|
+
export function parseVerificationReport(
|
|
256
|
+
report,
|
|
257
|
+
proofChecks,
|
|
258
|
+
connectorId,
|
|
259
|
+
runId,
|
|
260
|
+
) {
|
|
261
|
+
if (!report?.trim())
|
|
262
|
+
throw new Error("Codex returned no connected verification report.");
|
|
247
263
|
return (proofChecks ?? []).map((_, index) => {
|
|
248
264
|
const start = new RegExp(`^## Proof ${index + 1}\\s*$`, "im").exec(report);
|
|
249
265
|
const tail = start ? report.slice(start.index + start[0].length) : "";
|
|
250
266
|
const section = tail.split(/^## Proof \d+\s*$/im)[0] ?? "";
|
|
251
|
-
const status =
|
|
252
|
-
|
|
267
|
+
const status =
|
|
268
|
+
/^- Status:\s*(passed|failed)\s*$/im.exec(section)?.[1] ?? "failed";
|
|
269
|
+
const exitValue =
|
|
270
|
+
/^- Exit code:\s*(\d+|none)\s*$/im.exec(section)?.[1] ?? "none";
|
|
253
271
|
const evidence = /^- Evidence:\s*(.+)$/im.exec(section)?.[1]?.trim();
|
|
254
272
|
return {
|
|
255
273
|
proof_index: index,
|
|
@@ -257,7 +275,9 @@ export function parseVerificationReport(report, proofChecks, connectorId, runId)
|
|
|
257
275
|
verifier_type: "agent_reported",
|
|
258
276
|
verifier_identity: "Connected Codex CLI verifier",
|
|
259
277
|
locator: `connector:${connectorId}/run:${runId}#proof-${index + 1}`,
|
|
260
|
-
output_excerpt:
|
|
278
|
+
output_excerpt:
|
|
279
|
+
evidence ||
|
|
280
|
+
"The connected verifier did not provide evidence for this Proof.",
|
|
261
281
|
exit_code: exitValue === "none" ? null : Number(exitValue),
|
|
262
282
|
};
|
|
263
283
|
});
|
|
@@ -307,6 +327,7 @@ export async function executeWorkspaceAssessment(
|
|
|
307
327
|
execution.sandboxMode,
|
|
308
328
|
config,
|
|
309
329
|
callbacks,
|
|
330
|
+
execution.profile,
|
|
310
331
|
);
|
|
311
332
|
callbacks.onProcess?.(controller.child);
|
|
312
333
|
const completed = await controller.completed;
|
|
@@ -335,7 +356,11 @@ export async function executeWorkspaceAssessment(
|
|
|
335
356
|
};
|
|
336
357
|
}
|
|
337
358
|
|
|
338
|
-
export async function workspaceChangeImpact(
|
|
359
|
+
export async function workspaceChangeImpact(
|
|
360
|
+
workspace,
|
|
361
|
+
baseRevision,
|
|
362
|
+
targetRevision,
|
|
363
|
+
) {
|
|
339
364
|
if (!baseRevision || !targetRevision) {
|
|
340
365
|
throw new Error(
|
|
341
366
|
"Incremental assessment requires both the previously assessed and current Git commits. Run a full assessment instead.",
|
|
@@ -361,7 +386,15 @@ export async function workspaceChangeImpact(workspace, baseRevision, targetRevis
|
|
|
361
386
|
);
|
|
362
387
|
const workingFiles = await run(
|
|
363
388
|
"git",
|
|
364
|
-
[
|
|
389
|
+
[
|
|
390
|
+
"-c",
|
|
391
|
+
"core.quotepath=false",
|
|
392
|
+
"ls-files",
|
|
393
|
+
"--others",
|
|
394
|
+
"--modified",
|
|
395
|
+
"--deleted",
|
|
396
|
+
"--exclude-standard",
|
|
397
|
+
],
|
|
365
398
|
workspace,
|
|
366
399
|
{ allowFailure: true },
|
|
367
400
|
);
|
|
@@ -374,10 +407,12 @@ export async function workspaceChangeImpact(workspace, baseRevision, targetRevis
|
|
|
374
407
|
workspace,
|
|
375
408
|
{ allowFailure: true },
|
|
376
409
|
);
|
|
377
|
-
const allChangedFiles = [
|
|
378
|
-
...
|
|
379
|
-
|
|
380
|
-
|
|
410
|
+
const allChangedFiles = [
|
|
411
|
+
...new Set([
|
|
412
|
+
...pathLines(committedFiles.stdout),
|
|
413
|
+
...pathLines(workingFiles.stdout),
|
|
414
|
+
]),
|
|
415
|
+
];
|
|
381
416
|
if (allChangedFiles.length > 500) {
|
|
382
417
|
throw new Error(
|
|
383
418
|
`This change affects ${allChangedFiles.length} paths, above the 500-path incremental limit. Run a full reassessment instead.`,
|
|
@@ -400,10 +435,24 @@ export async function workspaceChangeImpact(workspace, baseRevision, targetRevis
|
|
|
400
435
|
"```",
|
|
401
436
|
];
|
|
402
437
|
if (status.stdout.trim()) {
|
|
403
|
-
lines.push(
|
|
438
|
+
lines.push(
|
|
439
|
+
"",
|
|
440
|
+
"### Working tree",
|
|
441
|
+
"",
|
|
442
|
+
"```text",
|
|
443
|
+
boundedText(status.stdout, 6_000),
|
|
444
|
+
"```",
|
|
445
|
+
);
|
|
404
446
|
}
|
|
405
447
|
if (stat.stdout.trim()) {
|
|
406
|
-
lines.push(
|
|
448
|
+
lines.push(
|
|
449
|
+
"",
|
|
450
|
+
"### Diff summary",
|
|
451
|
+
"",
|
|
452
|
+
"```text",
|
|
453
|
+
boundedText(stat.stdout, 6_000),
|
|
454
|
+
"```",
|
|
455
|
+
);
|
|
407
456
|
}
|
|
408
457
|
return { changedFiles, markdown: lines.join("\n") };
|
|
409
458
|
}
|
|
@@ -418,17 +467,22 @@ function pathLines(output) {
|
|
|
418
467
|
|
|
419
468
|
function boundedText(value, maximum) {
|
|
420
469
|
const text = String(value || "").trim();
|
|
421
|
-
return text.length <= maximum
|
|
470
|
+
return text.length <= maximum
|
|
471
|
+
? text
|
|
472
|
+
: `${text.slice(0, maximum)}\n... truncated by EngineerOS`;
|
|
422
473
|
}
|
|
423
474
|
|
|
424
475
|
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
425
476
|
const execution = connectorExecution(assignment);
|
|
477
|
+
const previousSessionId = config.sessions?.[execution.sessionKey];
|
|
426
478
|
const controller = launchAgentProcess(
|
|
427
479
|
config.workspace,
|
|
428
480
|
execution.prompt,
|
|
429
481
|
execution.sandboxMode,
|
|
430
482
|
config,
|
|
431
483
|
callbacks,
|
|
484
|
+
execution.profile,
|
|
485
|
+
previousSessionId,
|
|
432
486
|
);
|
|
433
487
|
callbacks.onProcess?.(controller.child);
|
|
434
488
|
const completed = await controller.completed;
|
|
@@ -436,13 +490,20 @@ export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
|
436
490
|
if (!content) {
|
|
437
491
|
throw new Error("Codex completed without returning a response.");
|
|
438
492
|
}
|
|
439
|
-
return {
|
|
493
|
+
return {
|
|
494
|
+
content,
|
|
495
|
+
model: completed.model ?? config.agent_protocol ?? "coding-agent",
|
|
496
|
+
sessionId: completed.sessionId,
|
|
497
|
+
sessionKey: execution.sessionKey,
|
|
498
|
+
};
|
|
440
499
|
}
|
|
441
500
|
|
|
442
501
|
export async function inspectCodingAgent(config, workspace = process.cwd()) {
|
|
443
502
|
if (config.agent_protocol === "acp") {
|
|
444
503
|
if (!config.agent_command) {
|
|
445
|
-
throw new Error(
|
|
504
|
+
throw new Error(
|
|
505
|
+
"ACP requires --agent-command when pairing the connector.",
|
|
506
|
+
);
|
|
446
507
|
}
|
|
447
508
|
return {
|
|
448
509
|
protocol: "acp",
|
|
@@ -540,7 +601,30 @@ export function connectorExecution(assignment) {
|
|
|
540
601
|
if (!new Set(["read-only", "workspace-write"]).has(sandboxMode)) {
|
|
541
602
|
throw new Error("EngineerOS assignment has an unsupported sandbox_mode.");
|
|
542
603
|
}
|
|
543
|
-
|
|
604
|
+
const rawProfile = assignment?.execution_profile ?? {};
|
|
605
|
+
const model =
|
|
606
|
+
typeof rawProfile.model === "string" ? rawProfile.model.trim() : "";
|
|
607
|
+
const reasoningEffort = rawProfile.reasoning_effort;
|
|
608
|
+
if (
|
|
609
|
+
reasoningEffort &&
|
|
610
|
+
!new Set(["low", "medium", "high", "xhigh"]).has(reasoningEffort)
|
|
611
|
+
) {
|
|
612
|
+
throw new Error(
|
|
613
|
+
"EngineerOS assignment has an unsupported reasoning effort.",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
prompt,
|
|
618
|
+
sandboxMode,
|
|
619
|
+
sessionKey:
|
|
620
|
+
typeof assignment.session_key === "string" && assignment.session_key.trim()
|
|
621
|
+
? assignment.session_key.trim()
|
|
622
|
+
: `project-${String(assignment.purpose || "general").toLowerCase().replace(/[^a-z0-9]+/g, "-").slice(0, 80)}`,
|
|
623
|
+
profile: {
|
|
624
|
+
...(model ? { model } : {}),
|
|
625
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
626
|
+
},
|
|
627
|
+
};
|
|
544
628
|
}
|
|
545
629
|
|
|
546
630
|
export async function stopProcess(child) {
|
|
@@ -664,11 +748,28 @@ async function prepareRunWorkspace(workspace, runId, baseRevision) {
|
|
|
664
748
|
return target;
|
|
665
749
|
}
|
|
666
750
|
|
|
667
|
-
function launchCodexProcess(
|
|
751
|
+
function launchCodexProcess(
|
|
752
|
+
workspace,
|
|
753
|
+
prompt,
|
|
754
|
+
sandbox,
|
|
755
|
+
callbacks,
|
|
756
|
+
profile = {},
|
|
757
|
+
previousSessionId,
|
|
758
|
+
) {
|
|
668
759
|
const command =
|
|
669
760
|
process.env.CODEX_BIN ||
|
|
670
761
|
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
671
|
-
const
|
|
762
|
+
const profileArgs = [];
|
|
763
|
+
if (profile.model) profileArgs.push("--model", profile.model);
|
|
764
|
+
if (profile.reasoning_effort) {
|
|
765
|
+
profileArgs.push(
|
|
766
|
+
"--config",
|
|
767
|
+
`model_reasoning_effort=${JSON.stringify(profile.reasoning_effort)}`,
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
const args = previousSessionId
|
|
771
|
+
? ["exec", "resume", "--json", ...profileArgs, previousSessionId, "-"]
|
|
772
|
+
: ["exec", "--json", "--sandbox", sandbox, "-C", workspace, ...profileArgs, "-"];
|
|
672
773
|
const child = spawn(command, args, {
|
|
673
774
|
cwd: workspace,
|
|
674
775
|
env: process.env,
|
|
@@ -679,6 +780,7 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
679
780
|
let output = "";
|
|
680
781
|
let buffer = "";
|
|
681
782
|
let finalMessage = "";
|
|
783
|
+
let sessionId = previousSessionId || "";
|
|
682
784
|
child.stdout.setEncoding("utf8");
|
|
683
785
|
child.stdout.on("data", (chunk) => {
|
|
684
786
|
output += chunk;
|
|
@@ -689,6 +791,9 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
689
791
|
if (!line.trim()) continue;
|
|
690
792
|
try {
|
|
691
793
|
const event = JSON.parse(line);
|
|
794
|
+
if (event.type === "thread.started" && typeof event.thread_id === "string") {
|
|
795
|
+
sessionId = event.thread_id;
|
|
796
|
+
}
|
|
692
797
|
if (
|
|
693
798
|
event.type === "item.completed" &&
|
|
694
799
|
event.item?.type === "agent_message" &&
|
|
@@ -716,18 +821,32 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
|
|
|
716
821
|
const completed = new Promise((resolve, reject) => {
|
|
717
822
|
child.once("error", reject);
|
|
718
823
|
child.once("close", (code) => {
|
|
719
|
-
if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
|
|
824
|
+
if (code === 0 && sessionId) resolve({ output: output.slice(-20_000), finalMessage, sessionId });
|
|
825
|
+
else if (code === 0) reject(new Error("Codex completed without announcing a resumable session id."));
|
|
720
826
|
else reject(new Error(codexFailureMessage(output, code)));
|
|
721
827
|
});
|
|
722
828
|
});
|
|
723
829
|
return { child, completed };
|
|
724
830
|
}
|
|
725
831
|
|
|
726
|
-
function launchAgentProcess(
|
|
832
|
+
function launchAgentProcess(
|
|
833
|
+
workspace,
|
|
834
|
+
prompt,
|
|
835
|
+
sandbox,
|
|
836
|
+
config,
|
|
837
|
+
callbacks,
|
|
838
|
+
profile = {},
|
|
839
|
+
previousSessionId,
|
|
840
|
+
) {
|
|
727
841
|
if (config.agent_protocol === "acp") {
|
|
842
|
+
if (profile.model || profile.reasoning_effort) {
|
|
843
|
+
throw new Error(
|
|
844
|
+
"This ACP coding agent does not advertise execution-profile support.",
|
|
845
|
+
);
|
|
846
|
+
}
|
|
728
847
|
return launchAcpAgent(workspace, prompt, config, callbacks);
|
|
729
848
|
}
|
|
730
|
-
return launchCodexProcess(workspace, prompt, sandbox, callbacks);
|
|
849
|
+
return launchCodexProcess(workspace, prompt, sandbox, callbacks, profile, previousSessionId);
|
|
731
850
|
}
|
|
732
851
|
|
|
733
852
|
function runCodexCommand(command, args, cwd) {
|
|
@@ -761,9 +880,10 @@ async function changedFilePaths(workspace) {
|
|
|
761
880
|
workspace,
|
|
762
881
|
);
|
|
763
882
|
return [
|
|
764
|
-
...new Set(
|
|
765
|
-
|
|
766
|
-
|
|
883
|
+
...new Set([
|
|
884
|
+
...gitPathList(tracked.stdout),
|
|
885
|
+
...gitPathList(untracked.stdout),
|
|
886
|
+
]),
|
|
767
887
|
].sort();
|
|
768
888
|
}
|
|
769
889
|
|