@engineeros/connector 0.6.2 → 0.8.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
@@ -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
@@ -43,7 +43,7 @@ if (command === "pair") {
43
43
  const pairingCode = positional[0];
44
44
  if (!pairingCode)
45
45
  fail(
46
- "Usage: engineeros-connector pair CODE --url URL [--name NAME] [--workspace PATH]",
46
+ "Usage: engineeros-connector pair CODE --url URL [--name NAME] [--workspace PATH] [--skip-git-repo-check]",
47
47
  );
48
48
  const url = flags.url;
49
49
  if (!url) fail("Pairing requires --url with the EngineerOS backend address.");
@@ -56,6 +56,7 @@ if (command === "pair") {
56
56
  agent_command: flags["agent-command"] || null,
57
57
  agent_args: parseAgentArgs(flags["agent-args"]),
58
58
  agent_name: flags["agent-name"] || null,
59
+ skip_git_repo_check: flags["skip-git-repo-check"] === true,
59
60
  name:
60
61
  flags.name ||
61
62
  `${os.hostname()} - ${path.basename(path.resolve(flags.workspace || process.cwd()))}`,
@@ -375,6 +376,11 @@ async function executePrompt(assignment) {
375
376
  if (active?.runId === promptId) active.child = child;
376
377
  },
377
378
  });
379
+ config = {
380
+ ...config,
381
+ sessions: { ...(config.sessions || {}), [result.sessionKey]: result.sessionId },
382
+ };
383
+ await saveConfig(config);
378
384
  if (active?.cancelled || socket.readyState !== WebSocket.OPEN) return;
379
385
  socket.send(
380
386
  JSON.stringify({
@@ -382,6 +388,7 @@ async function executePrompt(assignment) {
382
388
  prompt_id: promptId,
383
389
  content: result.content,
384
390
  model: result.model,
391
+ session_id: result.sessionId,
385
392
  }),
386
393
  );
387
394
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.6.2",
3
+ "version": "0.8.1",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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 (
@@ -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(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,25 @@ 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
+ skipGitRepoCheck = false,
759
+ ) {
668
760
  const command =
669
761
  process.env.CODEX_BIN ||
670
762
  (process.platform === "win32" ? "codex.cmd" : "codex");
671
- const args = ["exec", "--json", "--sandbox", sandbox, "-C", workspace, "-"];
763
+ const args = codexExecutionArgs({
764
+ workspace,
765
+ sandbox,
766
+ profile,
767
+ previousSessionId,
768
+ skipGitRepoCheck,
769
+ });
672
770
  const child = spawn(command, args, {
673
771
  cwd: workspace,
674
772
  env: process.env,
@@ -679,6 +777,7 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
679
777
  let output = "";
680
778
  let buffer = "";
681
779
  let finalMessage = "";
780
+ let sessionId = previousSessionId || "";
682
781
  child.stdout.setEncoding("utf8");
683
782
  child.stdout.on("data", (chunk) => {
684
783
  output += chunk;
@@ -689,6 +788,9 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
689
788
  if (!line.trim()) continue;
690
789
  try {
691
790
  const event = JSON.parse(line);
791
+ if (event.type === "thread.started" && typeof event.thread_id === "string") {
792
+ sessionId = event.thread_id;
793
+ }
692
794
  if (
693
795
  event.type === "item.completed" &&
694
796
  event.item?.type === "agent_message" &&
@@ -716,18 +818,61 @@ function launchCodexProcess(workspace, prompt, sandbox, callbacks) {
716
818
  const completed = new Promise((resolve, reject) => {
717
819
  child.once("error", reject);
718
820
  child.once("close", (code) => {
719
- if (code === 0) resolve({ output: output.slice(-20_000), finalMessage });
821
+ if (code === 0 && sessionId) resolve({ output: output.slice(-20_000), finalMessage, sessionId });
822
+ else if (code === 0) reject(new Error("Codex completed without announcing a resumable session id."));
720
823
  else reject(new Error(codexFailureMessage(output, code)));
721
824
  });
722
825
  });
723
826
  return { child, completed };
724
827
  }
725
828
 
726
- function launchAgentProcess(workspace, prompt, sandbox, config, callbacks) {
829
+ export function codexExecutionArgs({
830
+ workspace,
831
+ sandbox,
832
+ profile = {},
833
+ previousSessionId,
834
+ skipGitRepoCheck = false,
835
+ }) {
836
+ const optionArgs = ["--json"];
837
+ if (skipGitRepoCheck) optionArgs.push("--skip-git-repo-check");
838
+ if (profile.model) optionArgs.push("--model", profile.model);
839
+ if (profile.reasoning_effort) {
840
+ optionArgs.push(
841
+ "--config",
842
+ `model_reasoning_effort=${JSON.stringify(profile.reasoning_effort)}`,
843
+ );
844
+ }
845
+ return previousSessionId
846
+ ? ["exec", "resume", ...optionArgs, previousSessionId, "-"]
847
+ : ["exec", ...optionArgs, "--sandbox", sandbox, "-C", workspace, "-"];
848
+ }
849
+
850
+ function launchAgentProcess(
851
+ workspace,
852
+ prompt,
853
+ sandbox,
854
+ config,
855
+ callbacks,
856
+ profile = {},
857
+ previousSessionId,
858
+ ) {
727
859
  if (config.agent_protocol === "acp") {
860
+ if (profile.model || profile.reasoning_effort) {
861
+ throw new Error(
862
+ "This ACP coding agent does not advertise execution-profile support.",
863
+ );
864
+ }
728
865
  return launchAcpAgent(workspace, prompt, config, callbacks);
729
866
  }
730
- return launchCodexProcess(workspace, prompt, sandbox, callbacks);
867
+ return launchCodexProcess(
868
+ workspace,
869
+ prompt,
870
+ sandbox,
871
+ callbacks,
872
+ profile,
873
+ previousSessionId,
874
+ config.skip_git_repo_check === true,
875
+ );
731
876
  }
732
877
 
733
878
  function runCodexCommand(command, args, cwd) {
@@ -761,9 +906,10 @@ async function changedFilePaths(workspace) {
761
906
  workspace,
762
907
  );
763
908
  return [
764
- ...new Set(
765
- [...gitPathList(tracked.stdout), ...gitPathList(untracked.stdout)],
766
- ),
909
+ ...new Set([
910
+ ...gitPathList(tracked.stdout),
911
+ ...gitPathList(untracked.stdout),
912
+ ]),
767
913
  ].sort();
768
914
  }
769
915