@engineeros/connector 0.14.2 → 0.14.4

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 any combination of fifteen brownfield assessment domains, collectively covering 133 baseline checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Every domain uses a detailed structured-Markdown contract. EngineerOS stores each completed stage, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. Connector `0.14.2` runs native Codex stages concurrently and runs ACP agents such as OpenCode one stage at a time because every ACP stage owns a separate local process. It also throttles repetitive progress transitions so long assessments do not saturate the backend with database writes. Capability detail and project-specific compliance controls fan out after their catalogs, and final synthesis waits for every selected result. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. Each result, live output, validation correction, and retry remains independently durable. The connector prints each worker's safe activity and elapsed time every 30 seconds, stops a worker that produces no agent activity for ten minutes instead of waiting indefinitely, and gives an incomplete report one bounded correction turn instead of repeatedly restarting it. 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 any combination of fifteen brownfield assessment domains, collectively covering 133 baseline checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Every domain uses a detailed structured-Markdown contract. EngineerOS stores each completed stage, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. Connector `0.14.4` runs up to three independent assessment stages for every supported coding-agent protocol, including ACP agents such as OpenCode. The connector owns the worker pool and sends one aggregate heartbeat containing every active worker's phase, progress, last activity, and event count; workers no longer compete to send individual WebSocket progress heartbeats. Live output remains coalesced separately because it is durable report content. Capability detail and project-specific compliance controls fan out after their catalogs, and final synthesis waits for every selected result. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. Each result, live output, validation correction, and retry remains independently durable. The connector prints each worker's safe activity and elapsed time every 30 seconds, stops a worker that produces no agent activity for ten minutes instead of waiting indefinitely, and gives an incomplete report one bounded correction turn instead of repeatedly restarting it. 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,6 +12,9 @@ import {
12
12
  workspaceUrl,
13
13
  } from "../src/config.mjs";
14
14
  import {
15
+ ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
16
+ ASSESSMENT_RESULT_TIMEOUT_MS,
17
+ assessmentWorkerSnapshots,
15
18
  assessmentInactivityFailure,
16
19
  assessmentProgressMessage,
17
20
  assessmentStreamDelta,
@@ -20,15 +23,14 @@ import {
20
23
  executeAssignment,
21
24
  executeConnectedPrompt,
22
25
  executeWorkspaceAssessment,
23
- inspectCodingAgent,
24
- promptStreamEvent,
25
- requeueInterruptedAssessment,
26
- shouldReportAssessmentProgress,
27
- stopProcess,
28
- submitAssessmentResultWithRetry,
29
- takeWorkspaceAssessmentWave,
30
- workspaceAssessmentWorkerLimit,
31
- workspaceSnapshot,
26
+ inspectCodingAgent,
27
+ promptStreamEvent,
28
+ requeueInterruptedAssessment,
29
+ stopProcess,
30
+ submitAssessmentResultWithRetry,
31
+ takeWorkspaceAssessmentWave,
32
+ workspaceAssessmentWorkerLimit,
33
+ workspaceSnapshot,
32
34
  } from "../src/runner.mjs";
33
35
  import { disposeAcpRuntimes } from "../src/acp-client.mjs";
34
36
  import {
@@ -199,15 +201,13 @@ firstMessage.capabilities = capabilities;
199
201
 
200
202
  let stopped = false;
201
203
  let active = null;
202
- const activeAssessments = new Map();
203
- const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
204
- codingAgent.protocol,
205
- );
206
- if (ASSESSMENT_WORKER_LIMIT === 1) {
207
- console.log(
208
- "ACP assessments run one stage at a time to keep the local agent and EngineerOS responsive.",
209
- );
210
- }
204
+ const activeAssessments = new Map();
205
+ const ASSESSMENT_WORKER_LIMIT = workspaceAssessmentWorkerLimit(
206
+ codingAgent.protocol,
207
+ );
208
+ console.log(
209
+ `Assessment supervisor can run up to ${ASSESSMENT_WORKER_LIMIT} independent ${codingAgent.name} workers.`,
210
+ );
211
211
  const available = [];
212
212
  const assessments = [];
213
213
  const activePrompts = new Map();
@@ -354,11 +354,11 @@ async function connect() {
354
354
  socket.addEventListener("close", (event) => {
355
355
  clearConnectionWatchdog?.();
356
356
  clearInterval(pingTimer);
357
- for (const promptState of activePrompts.values())
358
- void cancelPrompt(promptState);
359
- for (const assessmentState of activeAssessments.values()) {
360
- if (event.code === 4001) void stopProcess(assessmentState.child);
361
- }
357
+ for (const promptState of activePrompts.values())
358
+ void cancelPrompt(promptState);
359
+ for (const assessmentState of activeAssessments.values()) {
360
+ if (event.code === 4001) void stopProcess(assessmentState.child);
361
+ }
362
362
  if (connectionRejected) {
363
363
  stopped = true;
364
364
  console.error(
@@ -421,7 +421,7 @@ async function submitWorkspaceSnapshot() {
421
421
  `Inventoried ${snapshot.total_file_count.toLocaleString()} safe file(s).`,
422
422
  );
423
423
  console.log(
424
- `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
424
+ `Uploaded ${snapshot.evidence_file_count.toLocaleString()} prioritized source file(s).`,
425
425
  );
426
426
  console.log(
427
427
  `Excluded ${snapshot.excluded_file_count.toLocaleString()} sensitive or generated file(s).`,
@@ -453,16 +453,29 @@ async function submitWorkspaceSnapshot() {
453
453
 
454
454
  function startPings() {
455
455
  clearInterval(pingTimer);
456
- pingTimer = setInterval(() => {
457
- if (socket.readyState === WebSocket.OPEN) {
458
- socket.send(
459
- JSON.stringify({
460
- type: "ping",
461
- active_run_id: active?.kind === "goal" ? active.runId : null,
462
- }),
463
- );
456
+ sendPing();
457
+ pingTimer = setInterval(sendPing, 10_000);
458
+ }
459
+
460
+ function sendPing() {
461
+ if (socket.readyState !== WebSocket.OPEN) return;
462
+ try {
463
+ socket.send(
464
+ JSON.stringify({
465
+ type: "ping",
466
+ active_run_id: active?.kind === "goal" ? active.runId : null,
467
+ assessment_workers: assessmentWorkerSnapshots(activeAssessments),
468
+ }),
469
+ );
470
+ } catch (error) {
471
+ lastConnectionError = protocolFailureMessage(error);
472
+ try {
473
+ socket.close();
474
+ } catch {
475
+ // The reconnect timer below owns recovery.
464
476
  }
465
- }, 10_000);
477
+ scheduleReconnect();
478
+ }
466
479
  }
467
480
 
468
481
  function sendPromptEvent(promptId, event) {
@@ -491,13 +504,20 @@ function pump() {
491
504
  kind: "assessment",
492
505
  key: assessmentKey,
493
506
  runId: assessment.assessment_id,
494
- stage: assessment.stage,
495
- child: null,
496
- resumeAssignment: null,
497
- };
507
+ stage: assessment.stage,
508
+ child: null,
509
+ resumeAssignment: null,
510
+ phase: "starting",
511
+ progressPercent: 5,
512
+ lastMessage: `Starting ${assessment.stage} assessment stage`,
513
+ startedAt: Date.now(),
514
+ lastActivityAt: Date.now(),
515
+ eventCount: 0,
516
+ };
498
517
  activeAssessments.set(assessmentKey, assessmentState);
499
518
  void executeAssessment(assessment, assessmentState);
500
519
  }
520
+ if (wave.length > 0) sendPing();
501
521
  if (activeAssessments.size > 0 || assessments.length > 0) return;
502
522
  const runId = available.shift();
503
523
  if (!runId) return;
@@ -610,12 +630,9 @@ async function executeAssessment(assignment, assessmentState) {
610
630
  const assessmentId = assignment.assessment_id;
611
631
  const stage = assignment.stage;
612
632
  console.log(`Agent is running assessment stage ${stage} (${assessmentId}).`);
613
- const stageStartedAt = Date.now();
614
- let lastAgentActivityAt = stageStartedAt;
615
- let lastAgentStatus = "Starting the connected agent";
616
- let agentEventCount = 0;
617
- let agentProcessStarted = false;
618
- let agentCompleted = false;
633
+ const stageStartedAt = assessmentState.startedAt;
634
+ let agentProcessStarted = false;
635
+ let agentCompleted = false;
619
636
  let statusTicks = 0;
620
637
  let progress = 5;
621
638
  let outputBuffer = "";
@@ -623,31 +640,28 @@ async function executeAssessment(assignment, assessmentState) {
623
640
  let outputTimer;
624
641
  let accepted = false;
625
642
  let failureReported = false;
626
- let inactivityFailure = null;
627
- let lastReportedMilestone = null;
628
- let lastProgressMessage = null;
629
- let lastProgressPercent = -1;
630
- let lastProgressSentAt = 0;
631
- const creditedMilestones = new Set();
632
- const isActiveAssessment = () =>
633
- activeAssessments.get(assessmentState.key) === assessmentState;
634
- const sendAssessmentMessage = (payload) => {
635
- if (socket.readyState !== WebSocket.OPEN) return false;
636
- try {
637
- socket.send(JSON.stringify(payload));
638
- return true;
639
- } catch (error) {
640
- lastConnectionError = protocolFailureMessage(error);
641
- try {
642
- socket.close();
643
- } catch {
644
- // The reconnect timer below owns recovery.
645
- }
646
- scheduleReconnect();
647
- return false;
648
- }
649
- };
650
- const reportProgress = (message, { milestone = true } = {}) => {
643
+ let inactivityFailure = null;
644
+ let lastReportedMilestone = null;
645
+ const creditedMilestones = new Set();
646
+ const isActiveAssessment = () =>
647
+ activeAssessments.get(assessmentState.key) === assessmentState;
648
+ const sendAssessmentMessage = (payload) => {
649
+ if (socket.readyState !== WebSocket.OPEN) return false;
650
+ try {
651
+ socket.send(JSON.stringify(payload));
652
+ return true;
653
+ } catch (error) {
654
+ lastConnectionError = protocolFailureMessage(error);
655
+ try {
656
+ socket.close();
657
+ } catch {
658
+ // The reconnect timer below owns recovery.
659
+ }
660
+ scheduleReconnect();
661
+ return false;
662
+ }
663
+ };
664
+ const reportProgress = (message, { milestone = true } = {}) => {
651
665
  if (!isActiveAssessment()) return;
652
666
  if (milestone && lastReportedMilestone === message) return;
653
667
  if (milestone) {
@@ -656,38 +670,11 @@ async function executeAssessment(assignment, assessmentState) {
656
670
  creditedMilestones.add(message);
657
671
  progress = Math.min(90, progress + 10);
658
672
  }
659
- lastAgentStatus = message;
660
673
  console.log(`[assessment:${stage}] ${message}.`);
661
674
  }
662
- const boundedMessage = String(message).slice(0, 500);
663
- const now = Date.now();
664
- if (
665
- !shouldReportAssessmentProgress(
666
- {
667
- message: boundedMessage,
668
- progressPercent: progress,
669
- lastMessage: lastProgressMessage,
670
- lastProgressPercent,
671
- lastSentAt: lastProgressSentAt,
672
- },
673
- { now },
674
- )
675
- ) {
676
- return;
677
- }
678
- if (
679
- sendAssessmentMessage({
680
- type: "workspace.assessment.progress",
681
- assessment_id: assessmentId,
682
- stage,
683
- progress_percent: progress,
684
- message: boundedMessage,
685
- })
686
- ) {
687
- lastProgressMessage = boundedMessage;
688
- lastProgressPercent = progress;
689
- lastProgressSentAt = now;
690
- }
675
+ const boundedMessage = String(message).slice(0, 500);
676
+ assessmentState.progressPercent = progress;
677
+ assessmentState.lastMessage = boundedMessage;
691
678
  };
692
679
  const flushOutput = () => {
693
680
  if (outputTimer) clearTimeout(outputTimer);
@@ -700,14 +687,14 @@ async function executeAssessment(assignment, assessmentState) {
700
687
  !isActiveAssessment()
701
688
  ) {
702
689
  return;
703
- }
704
- for (let offset = 0; offset < output.length; offset += 50_000) {
705
- sendAssessmentMessage({
706
- type: "workspace.assessment.output",
707
- assessment_id: assessmentId,
708
- stage,
709
- delta: output.slice(offset, offset + 50_000),
710
- });
690
+ }
691
+ for (let offset = 0; offset < output.length; offset += 50_000) {
692
+ sendAssessmentMessage({
693
+ type: "workspace.assessment.output",
694
+ assessment_id: assessmentId,
695
+ stage,
696
+ delta: output.slice(offset, offset + 50_000),
697
+ });
711
698
  }
712
699
  };
713
700
  const reportOutput = (delta) => {
@@ -719,16 +706,19 @@ async function executeAssessment(assignment, assessmentState) {
719
706
  if (outputBuffer.length >= 50_000) {
720
707
  flushOutput();
721
708
  } else if (!outputTimer) {
722
- outputTimer = setTimeout(flushOutput, 500);
709
+ outputTimer = setTimeout(
710
+ flushOutput,
711
+ ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
712
+ );
723
713
  }
724
714
  };
725
715
  reportProgress(`Starting ${stage} assessment stage`);
726
716
  const heartbeat = setInterval(() => {
727
717
  const now = Date.now();
728
- const inactiveMs = now - lastAgentActivityAt;
729
- const nextInactivityFailure = agentProcessStarted && !agentCompleted
730
- ? assessmentInactivityFailure(stage, inactiveMs)
731
- : null;
718
+ const inactiveMs = now - assessmentState.lastActivityAt;
719
+ const nextInactivityFailure = agentProcessStarted && !agentCompleted
720
+ ? assessmentInactivityFailure(stage, inactiveMs)
721
+ : null;
732
722
  if (!inactivityFailure && nextInactivityFailure) {
733
723
  inactivityFailure = nextInactivityFailure;
734
724
  console.error(`[assessment:${stage}] ${inactivityFailure}`);
@@ -736,42 +726,45 @@ async function executeAssessment(assignment, assessmentState) {
736
726
  return;
737
727
  }
738
728
  if (inactivityFailure) return;
739
- reportProgress("Assessment in progress", { milestone: false });
740
729
  statusTicks += 1;
741
730
  if (statusTicks % 2 === 0) {
742
731
  console.log(
743
732
  `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
744
- `${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
745
- `(${agentEventCount.toLocaleString()} events).`,
733
+ `${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
734
+ `(${assessmentState.eventCount.toLocaleString()} events).`,
746
735
  );
747
736
  }
748
737
  }, 15_000);
749
738
  try {
750
- if (assignment.assessment_mode === "incremental") {
751
- progress = 15;
752
- reportProgress("Calculating changed files and affected behavior");
739
+ if (assignment.assessment_mode === "incremental") {
740
+ progress = 15;
741
+ reportProgress("Calculating changed files and affected behavior");
753
742
  }
754
- let result = await executeWorkspaceAssessment(assignment, config, {
743
+ let result = await executeWorkspaceAssessment(assignment, config, {
755
744
  onProcess: (child) => {
756
745
  if (isActiveAssessment()) {
757
746
  assessmentState.child = child;
758
747
  agentProcessStarted = true;
759
- lastAgentActivityAt = Date.now();
748
+ assessmentState.phase = "running";
749
+ assessmentState.lastActivityAt = Date.now();
760
750
  reportProgress("Connected agent process started");
761
751
  }
762
752
  },
763
753
  onEvent: (event) => {
764
- lastAgentActivityAt = Date.now();
765
- agentEventCount += 1;
754
+ assessmentState.lastActivityAt = Date.now();
755
+ assessmentState.eventCount += 1;
766
756
  reportOutput(assessmentStreamDelta(event));
767
757
  const message = assessmentProgressMessage(event);
768
- if (message) reportProgress(message);
769
- },
770
- });
771
- agentCompleted = true;
772
- flushOutput();
773
- const submitResult = (payload) =>
774
- fetch(
758
+ if (message) reportProgress(message);
759
+ },
760
+ });
761
+ agentCompleted = true;
762
+ assessmentState.phase = "delivering";
763
+ assessmentState.progressPercent = 95;
764
+ assessmentState.lastMessage = "Delivering completed stage result";
765
+ flushOutput();
766
+ const submitResult = (payload) =>
767
+ fetch(
775
768
  assessmentResultUrl(
776
769
  config.server_url,
777
770
  config.connector_id,
@@ -779,38 +772,42 @@ async function executeAssessment(assignment, assessmentState) {
779
772
  ),
780
773
  {
781
774
  method: "POST",
782
- headers: {
783
- "Content-Type": "application/json",
784
- Authorization: `Bearer ${config.token}`,
785
- },
786
- body: JSON.stringify(payload),
787
- signal: AbortSignal.timeout(30_000),
788
- },
789
- );
790
- const deliverResult = (payload) =>
791
- submitAssessmentResultWithRetry(payload, submitResult, {
792
- isActive: () => isActiveAssessment() && !stopped,
793
- onRetry: (error, delayMs) => {
794
- reportProgress("Completed result is waiting for EngineerOS", {
795
- milestone: false,
796
- });
797
- console.warn(
798
- `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
799
- `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
800
- );
801
- },
802
- });
803
- let response = await deliverResult(result);
804
- if (response.status === 422 && result.correctAfterRejection) {
805
- const rejection = await describeRejectedResponse(
806
- response,
807
- "the assessment",
808
- );
809
- agentCompleted = false;
810
- result = await result.correctAfterRejection(rejection);
811
- agentCompleted = true;
812
- flushOutput();
813
- response = await deliverResult(result);
775
+ headers: {
776
+ "Content-Type": "application/json",
777
+ Authorization: `Bearer ${config.token}`,
778
+ },
779
+ body: JSON.stringify(payload),
780
+ signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
781
+ },
782
+ );
783
+ const deliverResult = (payload) =>
784
+ submitAssessmentResultWithRetry(payload, submitResult, {
785
+ isActive: () => isActiveAssessment() && !stopped,
786
+ onRetry: (error, delayMs) => {
787
+ reportProgress("Completed result is waiting for EngineerOS", {
788
+ milestone: false,
789
+ });
790
+ console.warn(
791
+ `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
792
+ `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
793
+ );
794
+ },
795
+ });
796
+ let response = await deliverResult(result);
797
+ if (response.status === 422 && result.correctAfterRejection) {
798
+ const rejection = await describeRejectedResponse(
799
+ response,
800
+ "the assessment",
801
+ );
802
+ agentCompleted = false;
803
+ assessmentState.phase = "correcting";
804
+ assessmentState.lastMessage = "Correcting a rejected stage result";
805
+ result = await result.correctAfterRejection(rejection);
806
+ agentCompleted = true;
807
+ assessmentState.phase = "delivering";
808
+ assessmentState.lastMessage = "Delivering corrected stage result";
809
+ flushOutput();
810
+ response = await deliverResult(result);
814
811
  }
815
812
  if (!response.ok) {
816
813
  throw new Error(
@@ -822,14 +819,14 @@ async function executeAssessment(assignment, assessmentState) {
822
819
  } catch (error) {
823
820
  flushOutput();
824
821
  const message = inactivityFailure || protocolFailureMessage(error);
825
- if (isActiveAssessment()) {
826
- failureReported = sendAssessmentMessage({
827
- type: "workspace.assessment.failed",
828
- assessment_id: assessmentId,
829
- stage,
830
- message,
831
- });
832
- }
822
+ if (isActiveAssessment()) {
823
+ failureReported = sendAssessmentMessage({
824
+ type: "workspace.assessment.failed",
825
+ assessment_id: assessmentId,
826
+ stage,
827
+ message,
828
+ });
829
+ }
833
830
  console.error(message);
834
831
  } finally {
835
832
  if (outputTimer) clearTimeout(outputTimer);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
package/src/runner.mjs CHANGED
@@ -32,6 +32,9 @@ const MAX_INVENTORY_FILES = 100_000;
32
32
  const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
33
33
  const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
34
34
  export const ASSESSMENT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000;
35
+ export const ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS = 15_000;
36
+ export const ASSESSMENT_RESULT_TIMEOUT_MS = 120_000;
37
+ export const MAX_ASSESSMENT_WORKERS = 3;
35
38
  const EXCLUDED_DIRECTORIES = new Set([
36
39
  ".agents",
37
40
  ".claude",
@@ -697,16 +700,16 @@ export function codexFailureMessage(output, code) {
697
700
 
698
701
  export function assessmentProgressMessage(event) {
699
702
  if (!event || typeof event !== "object") return null;
700
- if (event.type === "assessment.output_correction")
701
- return "Completing the required stage report structure";
702
- if (event.type === "agent.connected")
703
- return "Connected agent is ready to inspect the workspace";
703
+ if (event.type === "assessment.output_correction")
704
+ return "Completing the required stage report structure";
705
+ if (event.type === "agent.connected")
706
+ return "Connected agent is ready to inspect the workspace";
704
707
  if (event.type === "acp.plan")
705
708
  return "Organizing the repository assessment plan";
706
- if (event.type === "acp.agent_thought_chunk")
707
- return "Reasoning through the current implementation";
708
- if (event.type === "acp.agent_message_chunk")
709
- return "Drafting the stage report";
709
+ if (event.type === "acp.agent_thought_chunk")
710
+ return "Reasoning through the current implementation";
711
+ if (event.type === "acp.agent_message_chunk")
712
+ return "Drafting the stage report";
710
713
  if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
711
714
  return assessmentCommandMilestone(event.update?.title);
712
715
  }
@@ -824,7 +827,7 @@ function assessmentCommandMilestone(command) {
824
827
  ? command.join(" ")
825
828
  : String(command || "");
826
829
  const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
827
- if (!normalized) return "Inspecting workspace source";
830
+ if (!normalized) return "Inspecting workspace source";
828
831
  if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
829
832
  return "Comparing Git history and workspace changes";
830
833
  }
@@ -930,9 +933,9 @@ export function assessmentStageCorrectionPrompt(
930
933
  "## Incomplete Stage Output Recovery",
931
934
  "",
932
935
  "The previous turn ended without a complete stage deliverable. Return the entire structured Markdown stage report now.",
933
- "Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
936
+ "Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
934
937
  `The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
935
- "Follow every section, inspection, and completeness rule in the Assignment.",
938
+ "Follow every section, inspection, and completeness rule in the Assignment.",
936
939
  "Do not return progress commentary, an explanation of the correction, or a code fence.",
937
940
  ].join("\n");
938
941
  }
@@ -992,7 +995,7 @@ export function enqueueWorkspaceAssessment(
992
995
  return "queued";
993
996
  }
994
997
 
995
- export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
998
+ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
996
999
  const available = Math.max(0, limit - activeCount);
997
1000
  if (!available || !queue.length) return [];
998
1001
  if (queue[0]?.parallelizable !== true) {
@@ -1005,36 +1008,37 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
1005
1008
  ) {
1006
1009
  wave.push(queue.shift());
1007
1010
  }
1008
- return wave;
1009
- }
1010
-
1011
- export function workspaceAssessmentWorkerLimit(protocol) {
1012
- return protocol === "acp" ? 1 : 3;
1013
- }
1014
-
1015
- export function shouldReportAssessmentProgress(
1016
- {
1017
- message,
1018
- progressPercent,
1019
- lastMessage,
1020
- lastProgressPercent,
1021
- lastSentAt,
1022
- },
1023
- { now = Date.now(), minimumIntervalMs = 2_000 } = {},
1024
- ) {
1025
- if (!message) return false;
1026
- if (message === lastMessage && progressPercent <= lastProgressPercent) {
1027
- return false;
1028
- }
1029
- return (
1030
- progressPercent > lastProgressPercent ||
1031
- now - lastSentAt >= minimumIntervalMs
1032
- );
1033
- }
1034
-
1035
- export function requeueInterruptedAssessment(
1036
- queue,
1037
- assessmentState,
1011
+ return wave;
1012
+ }
1013
+
1014
+ export function workspaceAssessmentWorkerLimit() {
1015
+ return MAX_ASSESSMENT_WORKERS;
1016
+ }
1017
+
1018
+ export function assessmentWorkerSnapshots(activeAssessments) {
1019
+ return [...activeAssessments.values()]
1020
+ .slice(0, MAX_ASSESSMENT_WORKERS)
1021
+ .map((state) => ({
1022
+ assessment_id: String(state.runId),
1023
+ stage: String(state.stage).slice(0, 96),
1024
+ phase: state.phase,
1025
+ progress_percent: Math.max(
1026
+ 0,
1027
+ Math.min(95, Number(state.progressPercent) || 0),
1028
+ ),
1029
+ message: String(state.lastMessage || "Assessment worker is starting").slice(
1030
+ 0,
1031
+ 500,
1032
+ ),
1033
+ started_at: new Date(state.startedAt).toISOString(),
1034
+ last_activity_at: new Date(state.lastActivityAt).toISOString(),
1035
+ event_count: Math.max(0, Number(state.eventCount) || 0),
1036
+ }));
1037
+ }
1038
+
1039
+ export function requeueInterruptedAssessment(
1040
+ queue,
1041
+ assessmentState,
1038
1042
  { accepted, failureReported },
1039
1043
  ) {
1040
1044
  if (accepted || failureReported || !assessmentState?.resumeAssignment) {
@@ -1047,52 +1051,52 @@ export function requeueInterruptedAssessment(
1047
1051
  assessmentState.resumeAssignment,
1048
1052
  { front: true },
1049
1053
  ) === "queued"
1050
- );
1051
- }
1052
-
1053
- export async function submitAssessmentResultWithRetry(
1054
- payload,
1055
- submit,
1056
- {
1057
- isActive = () => true,
1058
- onRetry = () => {},
1059
- wait = (milliseconds) =>
1060
- new Promise((resolve) => setTimeout(resolve, milliseconds)),
1061
- initialDelayMs = 1_000,
1062
- maxDelayMs = 30_000,
1063
- } = {},
1064
- ) {
1065
- let retryDelayMs = initialDelayMs;
1066
- while (isActive()) {
1067
- let retryFailure;
1068
- try {
1069
- const response = await submit(payload);
1070
- if (!isTransientAssessmentResultResponse(response)) return response;
1071
- await response.body?.cancel?.();
1072
- retryFailure = new Error(
1073
- `EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
1074
- );
1075
- } catch (error) {
1076
- retryFailure = error;
1077
- }
1078
- if (!isActive()) break;
1079
- onRetry(retryFailure, retryDelayMs);
1080
- await wait(retryDelayMs);
1081
- retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
1082
- }
1083
- throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
1084
- }
1085
-
1086
- function isTransientAssessmentResultResponse(response) {
1087
- return (
1088
- response.status === 408 ||
1089
- response.status === 425 ||
1090
- response.status === 429 ||
1091
- response.status >= 500
1092
- );
1093
- }
1094
-
1095
- export function connectorExecution(assignment, options = {}) {
1054
+ );
1055
+ }
1056
+
1057
+ export async function submitAssessmentResultWithRetry(
1058
+ payload,
1059
+ submit,
1060
+ {
1061
+ isActive = () => true,
1062
+ onRetry = () => {},
1063
+ wait = (milliseconds) =>
1064
+ new Promise((resolve) => setTimeout(resolve, milliseconds)),
1065
+ initialDelayMs = 1_000,
1066
+ maxDelayMs = 30_000,
1067
+ } = {},
1068
+ ) {
1069
+ let retryDelayMs = initialDelayMs;
1070
+ while (isActive()) {
1071
+ let retryFailure;
1072
+ try {
1073
+ const response = await submit(payload);
1074
+ if (!isTransientAssessmentResultResponse(response)) return response;
1075
+ await response.body?.cancel?.();
1076
+ retryFailure = new Error(
1077
+ `EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
1078
+ );
1079
+ } catch (error) {
1080
+ retryFailure = error;
1081
+ }
1082
+ if (!isActive()) break;
1083
+ onRetry(retryFailure, retryDelayMs);
1084
+ await wait(retryDelayMs);
1085
+ retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
1086
+ }
1087
+ throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
1088
+ }
1089
+
1090
+ function isTransientAssessmentResultResponse(response) {
1091
+ return (
1092
+ response.status === 408 ||
1093
+ response.status === 425 ||
1094
+ response.status === 429 ||
1095
+ response.status >= 500
1096
+ );
1097
+ }
1098
+
1099
+ export function connectorExecution(assignment, options = {}) {
1096
1100
  const rawPrompt = assignment?.prompt_markdown;
1097
1101
  if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
1098
1102
  throw new Error("EngineerOS assignment is missing prompt_markdown.");