@engineeros/connector 0.13.0 → 0.13.2

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
@@ -47,7 +47,7 @@ npx --yes @engineeros/connector@latest pair PAIRING-CODE --url https://your-engi
47
47
 
48
48
  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.
49
49
 
50
- From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select Architecture, Capabilities, Quality, or any combination when only part of the workspace needs reassessment. EngineerOS stores every completed assessment stage before starting the next one, resumes the unfinished stage after a connector restart or retry, and then synthesizes the selected results into System State. Capabilities are assessed one discovered capability at a time so a large repository does not depend on one unbounded agent turn. Assessment cannot modify the workspace.
50
+ From **Project steering -> Workspace**, run the workspace assessment to use the connected agent subscription already authenticated on that computer. Select Architecture, Capabilities, Quality, or any combination when only part of the workspace needs reassessment. EngineerOS stores every completed assessment stage before starting the next one, resumes the unfinished stage after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. The connector prints the current safe activity and elapsed time every 30 seconds, stops a stage that produces no agent activity for ten minutes instead of waiting indefinitely, and gives an incomplete final report one bounded correction turn instead of repeatedly restarting the stage. Capabilities are assessed one discovered capability at a time so a large repository does not depend on one unbounded agent turn. Assessment cannot modify the workspace.
51
51
 
52
52
  After onboarding, every project prompt is routed to this connection. Copilot, shaping, planning, architecture, and experience generation use the connected agent subscription and workspace context. Interactive prompts run independently from assessments and Goal scheduling. 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.
53
53
 
@@ -12,14 +12,17 @@ import {
12
12
  workspaceUrl,
13
13
  } from "../src/config.mjs";
14
14
  import {
15
+ assessmentInactivityFailure,
15
16
  assessmentProgressMessage,
16
17
  assessmentStreamDelta,
17
18
  applyAcceptedChange,
19
+ enqueueWorkspaceAssessment,
18
20
  executeAssignment,
19
21
  executeConnectedPrompt,
20
22
  executeWorkspaceAssessment,
21
23
  inspectCodingAgent,
22
24
  promptStreamEvent,
25
+ requeueInterruptedAssessment,
23
26
  stopProcess,
24
27
  workspaceSnapshot,
25
28
  } from "../src/runner.mjs";
@@ -275,16 +278,15 @@ async function connect() {
275
278
  return;
276
279
  }
277
280
  if (message.type === "workspace.assessment") {
278
- if (
279
- (active?.runId !== message.assessment_id ||
280
- active?.stage !== message.stage) &&
281
- !assessments.some(
282
- (candidate) =>
283
- candidate.assessment_id === message.assessment_id &&
284
- candidate.stage === message.stage,
285
- )
286
- ) {
287
- assessments.push(message);
281
+ const disposition = enqueueWorkspaceAssessment(
282
+ assessments,
283
+ active,
284
+ message,
285
+ );
286
+ if (disposition === "deferred") {
287
+ console.log(
288
+ `Assessment stage ${message.stage} is still active after reconnect; preserving the replay until it finishes.`,
289
+ );
288
290
  }
289
291
  pump();
290
292
  return;
@@ -337,8 +339,9 @@ async function connect() {
337
339
  clearInterval(pingTimer);
338
340
  for (const promptState of activePrompts.values())
339
341
  void cancelPrompt(promptState);
340
- if (event.code === 4001 && active?.kind === "assessment") {
341
- void stopProcess(active.child);
342
+ if (active?.kind === "assessment") {
343
+ active.transportInterrupted = true;
344
+ if (event.code === 4001) void stopProcess(active.child);
342
345
  }
343
346
  if (connectionRejected) {
344
347
  stopped = true;
@@ -463,13 +466,16 @@ function pump() {
463
466
  if (active || socket.readyState !== WebSocket.OPEN) return;
464
467
  const assessment = assessments.shift();
465
468
  if (assessment) {
466
- active = {
469
+ const assessmentState = {
467
470
  kind: "assessment",
468
471
  runId: assessment.assessment_id,
469
472
  stage: assessment.stage,
470
473
  child: null,
474
+ resumeAssignment: null,
475
+ transportInterrupted: false,
471
476
  };
472
- void executeAssessment(assessment);
477
+ active = assessmentState;
478
+ void executeAssessment(assessment, assessmentState);
473
479
  return;
474
480
  }
475
481
  const runId = available.shift();
@@ -579,24 +585,38 @@ async function cancelPrompt(promptState) {
579
585
  await stopProcess(promptState.child);
580
586
  }
581
587
 
582
- async function executeAssessment(assignment) {
588
+ async function executeAssessment(assignment, assessmentState) {
583
589
  const assessmentId = assignment.assessment_id;
584
590
  const stage = assignment.stage;
585
591
  console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
586
- let progress = 10;
592
+ const stageStartedAt = Date.now();
593
+ let lastAgentActivityAt = stageStartedAt;
594
+ let lastAgentStatus = "Starting the connected agent";
595
+ let agentEventCount = 0;
596
+ let agentProcessStarted = false;
597
+ let statusTicks = 0;
598
+ let progress = 5;
587
599
  let outputBuffer = "";
588
600
  let outputLength = 0;
589
601
  let outputTimer;
590
- const reportedMilestones = new Set();
602
+ let accepted = false;
603
+ let failureReported = false;
604
+ let inactivityFailure = null;
605
+ let lastReportedMilestone = null;
606
+ const creditedMilestones = new Set();
591
607
  const reportProgress = (message, { milestone = true } = {}) => {
592
- if (
593
- socket.readyState !== WebSocket.OPEN ||
594
- active?.runId !== assessmentId ||
595
- active?.stage !== stage
596
- )
597
- return;
598
- if (milestone && reportedMilestones.has(message)) return;
599
- if (milestone) reportedMilestones.add(message);
608
+ if (active !== assessmentState) return;
609
+ if (milestone && lastReportedMilestone === message) return;
610
+ if (milestone) {
611
+ lastReportedMilestone = message;
612
+ if (!creditedMilestones.has(message)) {
613
+ creditedMilestones.add(message);
614
+ progress = Math.min(90, progress + 10);
615
+ }
616
+ lastAgentStatus = message;
617
+ console.log(`[assessment:${stage}] ${message}.`);
618
+ }
619
+ if (socket.readyState !== WebSocket.OPEN) return;
600
620
  socket.send(
601
621
  JSON.stringify({
602
622
  type: "workspace.assessment.progress",
@@ -615,8 +635,7 @@ async function executeAssessment(assignment) {
615
635
  if (
616
636
  !output ||
617
637
  socket.readyState !== WebSocket.OPEN ||
618
- active?.runId !== assessmentId ||
619
- active?.stage !== stage
638
+ active !== assessmentState
620
639
  ) {
621
640
  return;
622
641
  }
@@ -645,8 +664,27 @@ async function executeAssessment(assignment) {
645
664
  };
646
665
  reportProgress(`Starting ${stage} assessment stage`);
647
666
  const heartbeat = setInterval(() => {
648
- progress = Math.min(90, progress + 5);
667
+ const now = Date.now();
668
+ const inactiveMs = now - lastAgentActivityAt;
669
+ const nextInactivityFailure = agentProcessStarted
670
+ ? assessmentInactivityFailure(stage, inactiveMs)
671
+ : null;
672
+ if (!inactivityFailure && nextInactivityFailure) {
673
+ inactivityFailure = nextInactivityFailure;
674
+ console.error(`[assessment:${stage}] ${inactivityFailure}`);
675
+ void stopProcess(assessmentState.child);
676
+ return;
677
+ }
678
+ if (inactivityFailure) return;
649
679
  reportProgress("Assessment in progress", { milestone: false });
680
+ statusTicks += 1;
681
+ if (statusTicks % 2 === 0) {
682
+ console.log(
683
+ `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
684
+ `${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
685
+ `(${agentEventCount.toLocaleString()} events).`,
686
+ );
687
+ }
650
688
  }, 15_000);
651
689
  try {
652
690
  if (assignment.assessment_mode === "incremental") {
@@ -655,11 +693,16 @@ async function executeAssessment(assignment) {
655
693
  }
656
694
  const result = await executeWorkspaceAssessment(assignment, config, {
657
695
  onProcess: (child) => {
658
- if (active?.runId === assessmentId && active?.stage === stage) {
659
- active.child = child;
696
+ if (active === assessmentState) {
697
+ assessmentState.child = child;
698
+ agentProcessStarted = true;
699
+ lastAgentActivityAt = Date.now();
700
+ reportProgress("Connected agent process started");
660
701
  }
661
702
  },
662
703
  onEvent: (event) => {
704
+ lastAgentActivityAt = Date.now();
705
+ agentEventCount += 1;
663
706
  reportOutput(assessmentStreamDelta(event));
664
707
  const message = assessmentProgressMessage(event);
665
708
  if (message) reportProgress(message);
@@ -682,11 +725,16 @@ async function executeAssessment(assignment) {
682
725
  await describeRejectedResponse(response, "the assessment"),
683
726
  );
684
727
  }
728
+ accepted = true;
685
729
  console.log(`Workspace assessment stage ${stage} was accepted by EngineerOS.`);
686
730
  } catch (error) {
687
731
  flushOutput();
688
- const message = protocolFailureMessage(error);
689
- if (socket.readyState === WebSocket.OPEN) {
732
+ const message = inactivityFailure || protocolFailureMessage(error);
733
+ if (
734
+ !assessmentState.transportInterrupted &&
735
+ active === assessmentState &&
736
+ socket.readyState === WebSocket.OPEN
737
+ ) {
690
738
  socket.send(
691
739
  JSON.stringify({
692
740
  type: "workspace.assessment.failed",
@@ -695,16 +743,34 @@ async function executeAssessment(assignment) {
695
743
  message,
696
744
  }),
697
745
  );
746
+ failureReported = true;
698
747
  }
699
748
  console.error(message);
700
749
  } finally {
701
750
  if (outputTimer) clearTimeout(outputTimer);
702
751
  clearInterval(heartbeat);
703
- active = null;
704
- pump();
752
+ if (active === assessmentState) {
753
+ active = null;
754
+ const replayQueued = requeueInterruptedAssessment(
755
+ assessments,
756
+ assessmentState,
757
+ { accepted, failureReported },
758
+ );
759
+ if (replayQueued) {
760
+ console.log(`Restarting interrupted assessment stage ${stage}.`);
761
+ }
762
+ pump();
763
+ }
705
764
  }
706
765
  }
707
766
 
767
+ function formatAssessmentDuration(milliseconds) {
768
+ const totalSeconds = Math.max(0, Math.floor(milliseconds / 1_000));
769
+ const minutes = Math.floor(totalSeconds / 60);
770
+ const seconds = totalSeconds % 60;
771
+ return minutes ? `${minutes}m ${seconds}s` : `${seconds}s`;
772
+ }
773
+
708
774
  async function execute(assignment) {
709
775
  const runId = assignment.run_id;
710
776
  console.log(`Running Goal ${runId} with ${codingAgent.name}.`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.13.0",
3
+ "version": "0.13.2",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -5,7 +5,7 @@ import * as acp from "@agentclientprotocol/sdk";
5
5
  const RUNTIME_IDLE_MS = 30 * 60 * 1_000;
6
6
  const runtimes = new Map();
7
7
 
8
- export function permissionOutcome(options = [], allow = false) {
8
+ export function permissionOutcome(options = [], allow = false) {
9
9
  if (!allow) return { outcome: { outcome: "cancelled" } };
10
10
  const selected =
11
11
  options.find((option) => option.kind === "allow_once") ??
@@ -13,9 +13,9 @@ export function permissionOutcome(options = [], allow = false) {
13
13
  return selected
14
14
  ? { outcome: { outcome: "selected", optionId: selected.optionId } }
15
15
  : { outcome: { outcome: "cancelled" } };
16
- }
17
-
18
- export function launchAcpAgent(
16
+ }
17
+
18
+ export function launchAcpAgent(
19
19
  workspace,
20
20
  prompt,
21
21
  config,
@@ -39,7 +39,7 @@ export function launchAcpAgent(
39
39
  if (runtimeKey) runtimes.set(runtimeKey, runtime);
40
40
  }
41
41
  const sessionKey = options.sessionKey || `isolated-${crypto.randomUUID()}`;
42
- const completed = runtime
42
+ const completed = runtime
43
43
  .prompt({
44
44
  sessionKey,
45
45
  prompt,
@@ -49,9 +49,10 @@ export function launchAcpAgent(
49
49
  callbacks,
50
50
  })
51
51
  .finally(async () => {
52
- if (!persistent) await runtime.dispose();
53
- });
54
- return {
52
+ if (!persistent) await runtime.dispose();
53
+ });
54
+ runtime.child.engineerOsCancel = () => runtime.dispose();
55
+ return {
55
56
  child: runtime.child,
56
57
  completed,
57
58
  cancel: () => runtime.cancel(sessionKey),
@@ -117,18 +118,19 @@ class AcpRuntime {
117
118
  this.stderr = "";
118
119
  this.disposed = false;
119
120
  this.idleTimer = null;
120
- this.child = spawn(
121
- config.agent_command,
122
- Array.isArray(config.agent_args) ? config.agent_args : [],
123
- {
124
- cwd: workspace,
125
- env: { ...process.env, ...(config.agent_env || {}) },
126
- shell:
127
- process.platform === "win32" &&
128
- /\.(cmd|bat)$/i.test(config.agent_command),
129
- stdio: ["pipe", "pipe", "pipe"],
130
- },
131
- );
121
+ this.child = spawn(
122
+ config.agent_command,
123
+ Array.isArray(config.agent_args) ? config.agent_args : [],
124
+ {
125
+ cwd: workspace,
126
+ env: { ...process.env, ...(config.agent_env || {}) },
127
+ windowsHide: true,
128
+ shell:
129
+ process.platform === "win32" &&
130
+ /\.(cmd|bat)$/i.test(config.agent_command),
131
+ stdio: ["pipe", "pipe", "pipe"],
132
+ },
133
+ );
132
134
  this.child.stderr.setEncoding("utf8");
133
135
  this.child.stderr.on("data", (chunk) => {
134
136
  this.stderr = `${this.stderr}${chunk}`.slice(-4_000);
@@ -141,10 +141,15 @@ export function normalizeAgentStructuredOutput(
141
141
  if (!content) {
142
142
  throw new Error("Agent completed without returning a response.");
143
143
  }
144
- const firstSection = {
145
- "# Workspace Assessment": "## Executive Summary",
146
- "# Workspace Assessment Delta": "## Updated Executive Summary",
147
- }[outputHeading];
144
+ const firstSection = {
145
+ "# Workspace Assessment": "## Executive Summary",
146
+ "# Workspace Assessment Delta": "## Updated Executive Summary",
147
+ "# Assessment Stage: Architecture": "## Architecture Summary",
148
+ "# Assessment Stage: Capability Catalog": "## Capability Catalog",
149
+ "# Assessment Stage: Capabilities": "## Observed Capabilities",
150
+ "# Assessment Stage: Quality": "## Quality Summary",
151
+ "# Assessment Stage: Synthesis": "## Executive Summary",
152
+ }[outputHeading];
148
153
  if (
149
154
  firstSection &&
150
155
  (content.startsWith(firstSection) ||
@@ -152,12 +157,34 @@ export function normalizeAgentStructuredOutput(
152
157
  ) {
153
158
  return `${outputHeading}\n\n${content}`;
154
159
  }
155
- const lines = content.split(/\r?\n/);
156
- const headingIndex = lines.findIndex(
157
- (line) => line.trim() === outputHeading,
158
- );
159
- const preamble =
160
- headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
160
+ const lines = content.split(/\r?\n/);
161
+ const headingIndex = lines.findIndex(
162
+ (line) => line.trim() === outputHeading,
163
+ );
164
+ const firstSectionIndex = firstSection
165
+ ? lines.findIndex((line) => line.trim() === firstSection)
166
+ : -1;
167
+ if (
168
+ headingIndex < 0 &&
169
+ outputHeading.startsWith("# Assessment Stage:") &&
170
+ firstSectionIndex >= 0
171
+ ) {
172
+ const preamble = lines.slice(0, firstSectionIndex).join("\n");
173
+ if (preamble.length > 2_000) {
174
+ throw new Error(
175
+ `Agent response contains too much text before the required section '${firstSection}'.`,
176
+ );
177
+ }
178
+ const normalized = lines.slice(firstSectionIndex).join("\n").trim();
179
+ if (/```/.test(preamble) || /(?:^|\n)```\s*$/.test(normalized)) {
180
+ throw new Error(
181
+ `Agent response must return '${outputHeading}' as plain Markdown, without a code fence.`,
182
+ );
183
+ }
184
+ return `${outputHeading}\n\n${normalized}`;
185
+ }
186
+ const preamble =
187
+ headingIndex >= 0 ? lines.slice(0, headingIndex).join("\n") : content;
161
188
  if (headingIndex < 0) {
162
189
  throw new Error(
163
190
  `Agent response is missing the required heading '${outputHeading}'.`,
package/src/runner.mjs CHANGED
@@ -28,9 +28,10 @@ const MAX_ARCHIVE_BYTES = 25_000_000;
28
28
  const MAX_EVIDENCE_BYTES = 24_000_000;
29
29
  const MAX_SHAREABLE_FILE_BYTES = 5 * 1024 * 1024;
30
30
  const MAX_EVIDENCE_FILES = 5_000;
31
- const MAX_INVENTORY_FILES = 100_000;
32
- const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
33
- const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
31
+ const MAX_INVENTORY_FILES = 100_000;
32
+ const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
33
+ const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
34
+ export const ASSESSMENT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000;
34
35
  const EXCLUDED_DIRECTORIES = new Set([
35
36
  ".agents",
36
37
  ".claude",
@@ -394,11 +395,31 @@ export async function executeWorkspaceAssessment(
394
395
  execution.profile,
395
396
  );
396
397
  callbacks.onProcess?.(controller.child);
397
- const completed = await controller.completed;
398
- const report = normalizeAgentStructuredOutput(
399
- completed.finalMessage,
400
- execution.requiredOutputHeading,
401
- );
398
+ let completed = await controller.completed;
399
+ const recovered = await recoverAssessmentStageOutput({
400
+ completed,
401
+ prompt,
402
+ requiredOutputHeading: execution.requiredOutputHeading,
403
+ retry: async (correctionPrompt, previousSessionId) => {
404
+ callbacks.onEvent?.({
405
+ type: "assessment.output_correction",
406
+ message: "The Agent is completing the required stage report structure",
407
+ });
408
+ const correction = launchAgentProcess(
409
+ config.workspace,
410
+ correctionPrompt,
411
+ execution.sandboxMode,
412
+ config,
413
+ callbacks,
414
+ execution.profile,
415
+ previousSessionId,
416
+ );
417
+ callbacks.onProcess?.(correction.child);
418
+ return correction.completed;
419
+ },
420
+ });
421
+ completed = recovered.completed;
422
+ const report = recovered.report;
402
423
  const endingRevision = await run(
403
424
  "git",
404
425
  ["rev-parse", "HEAD"],
@@ -635,10 +656,23 @@ export function codexFailureMessage(output, code) {
635
656
  return `Codex exited with code ${code}. ${output.slice(-1_000)}`;
636
657
  }
637
658
 
638
- export function assessmentProgressMessage(event) {
639
- if (!event || typeof event !== "object") return null;
640
- if (event.type === "turn.started")
641
- return "Reviewing repository structure and current Git state";
659
+ export function assessmentProgressMessage(event) {
660
+ if (!event || typeof event !== "object") return null;
661
+ if (event.type === "assessment.output_correction")
662
+ return "Completing the required stage report structure";
663
+ if (event.type === "agent.connected")
664
+ return "Connected agent is ready to inspect repository evidence";
665
+ if (event.type === "acp.plan")
666
+ return "Organizing the repository assessment plan";
667
+ if (event.type === "acp.agent_thought_chunk")
668
+ return "Reasoning through repository evidence";
669
+ if (event.type === "acp.agent_message_chunk")
670
+ return "Drafting the evidence-backed stage report";
671
+ if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
672
+ return assessmentCommandMilestone(event.update?.title);
673
+ }
674
+ if (event.type === "turn.started")
675
+ return "Reviewing repository structure and current Git state";
642
676
  if (event.type === "item.started") {
643
677
  if (event.item?.type === "command_execution") {
644
678
  return assessmentCommandMilestone(event.item.command);
@@ -652,8 +686,16 @@ export function assessmentProgressMessage(event) {
652
686
  if (event.type === "item.completed" && event.item?.type === "agent_message") {
653
687
  return "Synthesizing findings and highest-return actions";
654
688
  }
655
- return null;
656
- }
689
+ return null;
690
+ }
691
+
692
+ export function assessmentInactivityFailure(stage, inactiveMs) {
693
+ if (inactiveMs < ASSESSMENT_INACTIVITY_TIMEOUT_MS) return null;
694
+ return (
695
+ `The connected agent produced no activity for 10 minutes during ${stage}. ` +
696
+ "The stage was stopped instead of waiting indefinitely. Retry it after checking the agent terminal."
697
+ );
698
+ }
657
699
 
658
700
  export function promptProgressMessage(event) {
659
701
  if (!event || typeof event !== "object") return null;
@@ -781,10 +823,141 @@ export function workspaceAssessmentExecution(assignment) {
781
823
  return {
782
824
  ...connectorExecution(assignment, { requiredOutputHeading }),
783
825
  requiredOutputHeading,
784
- };
785
- }
786
-
787
- export function connectorExecution(assignment, options = {}) {
826
+ };
827
+ }
828
+
829
+ export async function recoverAssessmentStageOutput({
830
+ completed,
831
+ prompt,
832
+ requiredOutputHeading,
833
+ retry,
834
+ }) {
835
+ try {
836
+ return {
837
+ completed,
838
+ report: normalizeAgentStructuredOutput(
839
+ completed.finalMessage,
840
+ requiredOutputHeading,
841
+ ),
842
+ };
843
+ } catch (error) {
844
+ if (!recoverableStructuredOutputFailure(error)) throw error;
845
+ }
846
+
847
+ const corrected = await retry(
848
+ assessmentStageCorrectionPrompt(prompt, requiredOutputHeading),
849
+ completed.sessionId,
850
+ );
851
+ try {
852
+ return {
853
+ completed: {
854
+ ...corrected,
855
+ usage: mergeTokenUsage(completed.usage, corrected.usage),
856
+ },
857
+ report: normalizeAgentStructuredOutput(
858
+ corrected.finalMessage,
859
+ requiredOutputHeading,
860
+ ),
861
+ };
862
+ } catch (error) {
863
+ throw new Error(
864
+ `Agent did not return the required stage report after one automatic correction attempt. ${error.message}`,
865
+ { cause: error },
866
+ );
867
+ }
868
+ }
869
+
870
+ export function assessmentStageCorrectionPrompt(
871
+ originalPrompt,
872
+ requiredOutputHeading,
873
+ ) {
874
+ return [
875
+ String(originalPrompt || "").trim(),
876
+ "",
877
+ "## Incomplete Stage Output Recovery",
878
+ "",
879
+ "The previous turn ended without a complete stage deliverable. Return the entire structured Markdown stage report now.",
880
+ "Use repository evidence already inspected in the previous turn when it is available; inspect only what remains necessary.",
881
+ `The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
882
+ "Follow every section, evidence, and completeness rule in the Assignment.",
883
+ "Do not return progress commentary, an explanation of the correction, or a code fence.",
884
+ ].join("\n");
885
+ }
886
+
887
+ function recoverableStructuredOutputFailure(error) {
888
+ const message = error instanceof Error ? error.message : "";
889
+ return (
890
+ message.startsWith("Agent completed") ||
891
+ message.startsWith("Agent response")
892
+ );
893
+ }
894
+
895
+ function mergeTokenUsage(first, second) {
896
+ const usages = [first, second].filter(
897
+ (usage) => usage && typeof usage === "object",
898
+ );
899
+ if (!usages.length) return null;
900
+ return Object.fromEntries(
901
+ [
902
+ "input_tokens",
903
+ "output_tokens",
904
+ "cache_read_tokens",
905
+ "cache_write_tokens",
906
+ "reasoning_tokens",
907
+ "total_tokens",
908
+ ].map((field) => [
909
+ field,
910
+ usages.reduce(
911
+ (total, usage) =>
912
+ total + (Number.isFinite(usage[field]) ? usage[field] : 0),
913
+ 0,
914
+ ),
915
+ ]),
916
+ );
917
+ }
918
+
919
+ export function enqueueWorkspaceAssessment(
920
+ queue,
921
+ activeAssessment,
922
+ assignment,
923
+ { front = false } = {},
924
+ ) {
925
+ const matches = (candidate) =>
926
+ candidate?.assessment_id === assignment?.assessment_id &&
927
+ candidate?.stage === assignment?.stage;
928
+ if (
929
+ activeAssessment?.kind === "assessment" &&
930
+ activeAssessment.runId === assignment?.assessment_id &&
931
+ activeAssessment.stage === assignment?.stage
932
+ ) {
933
+ activeAssessment.resumeAssignment = assignment;
934
+ return "deferred";
935
+ }
936
+ if (queue.some(matches)) return "duplicate";
937
+ if (front) queue.unshift(assignment);
938
+ else queue.push(assignment);
939
+ return "queued";
940
+ }
941
+
942
+ export function requeueInterruptedAssessment(
943
+ queue,
944
+ assessmentState,
945
+ { accepted, failureReported },
946
+ ) {
947
+ if (accepted || failureReported || !assessmentState?.resumeAssignment) {
948
+ return false;
949
+ }
950
+ return (
951
+ enqueueWorkspaceAssessment(
952
+ queue,
953
+ null,
954
+ assessmentState.resumeAssignment,
955
+ { front: true },
956
+ ) === "queued"
957
+ );
958
+ }
959
+
960
+ export function connectorExecution(assignment, options = {}) {
788
961
  const rawPrompt = assignment?.prompt_markdown;
789
962
  if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
790
963
  throw new Error("EngineerOS assignment is missing prompt_markdown.");
@@ -834,10 +1007,13 @@ export function connectorExecution(assignment, options = {}) {
834
1007
  };
835
1008
  }
836
1009
 
837
- export async function stopProcess(child) {
838
- if (!child || child.exitCode !== null) return;
839
- await child.engineerOsCancel?.();
840
- if (process.platform === "win32") {
1010
+ export async function stopProcess(child) {
1011
+ if (!child || child.exitCode !== null) return;
1012
+ if (typeof child.engineerOsCancel === "function") {
1013
+ await child.engineerOsCancel();
1014
+ return;
1015
+ }
1016
+ if (process.platform === "win32") {
841
1017
  await run(
842
1018
  "taskkill",
843
1019
  ["/pid", String(child.pid), "/t", "/f"],