@engineeros/connector 0.6.1 → 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 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
@@ -23,6 +23,7 @@ import {
23
23
  describeWebSocketError,
24
24
  startConnectionWatchdog,
25
25
  } from "../src/connection.mjs";
26
+ import { advertisedCapabilities } from "../src/capabilities.mjs";
26
27
 
27
28
  const { command, positional, flags } = parseArgs(process.argv.slice(2));
28
29
 
@@ -63,13 +64,7 @@ if (command === "pair") {
63
64
  type: "pair",
64
65
  pairing_code: pairingCode,
65
66
  name: config.name,
66
- capabilities: {
67
- agent_protocols: flags["agent-command"] ? ["acp"] : ["codex"],
68
- coding_agent: true,
69
- codex_cli: !flags["agent-command"],
70
- platform: process.platform,
71
- workspace_name: path.basename(config.workspace),
72
- },
67
+ capabilities: {},
73
68
  };
74
69
  } else if (command === "start") {
75
70
  config = await loadConfig(flags.workspace || process.cwd());
@@ -93,10 +88,8 @@ try {
93
88
  fail(error instanceof Error ? error.message : String(error));
94
89
  }
95
90
  console.log(`Using ${codingAgent.name} through ${codingAgent.protocol} (${codingAgent.version}).`);
96
- if (firstMessage.type === "pair") {
97
- firstMessage.capabilities.agent_name = codingAgent.name;
98
- firstMessage.capabilities.agent_version = codingAgent.version;
99
- }
91
+ const capabilities = advertisedCapabilities(config, codingAgent);
92
+ firstMessage.capabilities = capabilities;
100
93
 
101
94
  let stopped = false;
102
95
  let active = null;
@@ -153,6 +146,7 @@ async function connect() {
153
146
  type: "authenticate",
154
147
  connector_id: config.connector_id,
155
148
  token: config.token,
149
+ capabilities,
156
150
  };
157
151
  console.log(`Paired. Connector ${config.connector_id} is online.`);
158
152
  reconnectDelay = 1_000;
@@ -381,6 +375,11 @@ async function executePrompt(assignment) {
381
375
  if (active?.runId === promptId) active.child = child;
382
376
  },
383
377
  });
378
+ config = {
379
+ ...config,
380
+ sessions: { ...(config.sessions || {}), [result.sessionKey]: result.sessionId },
381
+ };
382
+ await saveConfig(config);
384
383
  if (active?.cancelled || socket.readyState !== WebSocket.OPEN) return;
385
384
  socket.send(
386
385
  JSON.stringify({
@@ -388,6 +387,7 @@ async function executePrompt(assignment) {
388
387
  prompt_id: promptId,
389
388
  content: result.content,
390
389
  model: result.model,
390
+ session_id: result.sessionId,
391
391
  }),
392
392
  );
393
393
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -16,7 +16,7 @@
16
16
  "scripts": {
17
17
  "start": "node ./bin/engineeros-connector.mjs",
18
18
  "test": "node --test --test-concurrency=1",
19
- "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
19
+ "type-check": "node --check ./bin/engineeros-connector.mjs && node --check ./src/capabilities.mjs && node --check ./src/connection.mjs && node --check ./src/runner.mjs"
20
20
  },
21
21
  "engines": {
22
22
  "node": ">=22"
@@ -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 { finalMessage, model: "acp-agent" };
71
+ return {
72
+ finalMessage,
73
+ model: "acp-agent",
74
+ sessionId: session.sessionId,
75
+ };
72
76
  }
73
77
  const update = message.update;
74
78
  if (
@@ -0,0 +1,27 @@
1
+ import path from "node:path";
2
+
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);
10
+ return {
11
+ agent_protocols: [codingAgent.protocol],
12
+ coding_agent: true,
13
+ codex_cli: codingAgent.protocol === "codex",
14
+ platform: process.platform,
15
+ workspace_name: path.basename(config.workspace),
16
+ agent_name: codingAgent.name,
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
+ },
26
+ };
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(workspace, baseRevision, headRevision) {
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 { applied: false, reason: "The connected workspace is not a Git repository." };
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) return { applied: true, revision: currentHead };
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(report, proofChecks, connectorId, runId) {
246
- if (!report?.trim()) throw new Error("Codex returned no connected verification report.");
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 = /^- Status:\s*(passed|failed)\s*$/im.exec(section)?.[1] ?? "failed";
252
- const exitValue = /^- Exit code:\s*(\d+|none)\s*$/im.exec(section)?.[1] ?? "none";
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: evidence || "The connected verifier did not provide evidence for this Proof.",
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(workspace, baseRevision, targetRevision) {
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
- ["-c", "core.quotepath=false", "ls-files", "--others", "--modified", "--deleted", "--exclude-standard"],
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 = [...new Set([
378
- ...pathLines(committedFiles.stdout),
379
- ...pathLines(workingFiles.stdout),
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("", "### Working tree", "", "```text", boundedText(status.stdout, 6_000), "```");
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("", "### Diff summary", "", "```text", boundedText(stat.stdout, 6_000), "```");
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 ? text : `${text.slice(0, maximum)}\n... truncated by EngineerOS`;
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 { content, model: completed.model ?? config.agent_protocol ?? "coding-agent" };
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("ACP requires --agent-command when pairing the connector.");
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
- return { prompt, sandboxMode };
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(workspace, prompt, sandbox, callbacks) {
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 args = ["exec", "--json", "--sandbox", sandbox, "-C", workspace, "-"];
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(workspace, prompt, sandbox, config, callbacks) {
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
- [...gitPathList(tracked.stdout), ...gitPathList(untracked.stdout)],
766
- ),
883
+ ...new Set([
884
+ ...gitPathList(tracked.stdout),
885
+ ...gitPathList(untracked.stdout),
886
+ ]),
767
887
  ].sort();
768
888
  }
769
889