@engineeros/connector 0.14.2 → 0.14.5

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.5` runs up to three independent assessment stages for every supported coding-agent protocol. ACP agents such as OpenCode use isolated stage sessions within one persistent coding-agent process, avoiding the CPU and memory spike from launching three full agent runtimes. 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} stages.`,
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,21 @@ 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
+ controller: null,
510
+ resumeAssignment: null,
511
+ phase: "starting",
512
+ progressPercent: 5,
513
+ lastMessage: `Starting ${assessment.stage} assessment stage`,
514
+ startedAt: Date.now(),
515
+ lastActivityAt: Date.now(),
516
+ eventCount: 0,
517
+ };
498
518
  activeAssessments.set(assessmentKey, assessmentState);
499
519
  void executeAssessment(assessment, assessmentState);
500
520
  }
521
+ if (wave.length > 0) sendPing();
501
522
  if (activeAssessments.size > 0 || assessments.length > 0) return;
502
523
  const runId = available.shift();
503
524
  if (!runId) return;
@@ -606,16 +627,21 @@ async function cancelPrompt(promptState) {
606
627
  await stopProcess(promptState.child);
607
628
  }
608
629
 
630
+ async function cancelAssessment(assessmentState) {
631
+ if (typeof assessmentState.controller?.cancel === "function") {
632
+ await assessmentState.controller.cancel();
633
+ return;
634
+ }
635
+ await stopProcess(assessmentState.child);
636
+ }
637
+
609
638
  async function executeAssessment(assignment, assessmentState) {
610
639
  const assessmentId = assignment.assessment_id;
611
640
  const stage = assignment.stage;
612
641
  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;
642
+ const stageStartedAt = assessmentState.startedAt;
643
+ let agentProcessStarted = false;
644
+ let agentCompleted = false;
619
645
  let statusTicks = 0;
620
646
  let progress = 5;
621
647
  let outputBuffer = "";
@@ -623,31 +649,28 @@ async function executeAssessment(assignment, assessmentState) {
623
649
  let outputTimer;
624
650
  let accepted = false;
625
651
  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 } = {}) => {
652
+ let inactivityFailure = null;
653
+ let lastReportedMilestone = null;
654
+ const creditedMilestones = new Set();
655
+ const isActiveAssessment = () =>
656
+ activeAssessments.get(assessmentState.key) === assessmentState;
657
+ const sendAssessmentMessage = (payload) => {
658
+ if (socket.readyState !== WebSocket.OPEN) return false;
659
+ try {
660
+ socket.send(JSON.stringify(payload));
661
+ return true;
662
+ } catch (error) {
663
+ lastConnectionError = protocolFailureMessage(error);
664
+ try {
665
+ socket.close();
666
+ } catch {
667
+ // The reconnect timer below owns recovery.
668
+ }
669
+ scheduleReconnect();
670
+ return false;
671
+ }
672
+ };
673
+ const reportProgress = (message, { milestone = true } = {}) => {
651
674
  if (!isActiveAssessment()) return;
652
675
  if (milestone && lastReportedMilestone === message) return;
653
676
  if (milestone) {
@@ -656,38 +679,11 @@ async function executeAssessment(assignment, assessmentState) {
656
679
  creditedMilestones.add(message);
657
680
  progress = Math.min(90, progress + 10);
658
681
  }
659
- lastAgentStatus = message;
660
682
  console.log(`[assessment:${stage}] ${message}.`);
661
683
  }
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
- }
684
+ const boundedMessage = String(message).slice(0, 500);
685
+ assessmentState.progressPercent = progress;
686
+ assessmentState.lastMessage = boundedMessage;
691
687
  };
692
688
  const flushOutput = () => {
693
689
  if (outputTimer) clearTimeout(outputTimer);
@@ -700,14 +696,14 @@ async function executeAssessment(assignment, assessmentState) {
700
696
  !isActiveAssessment()
701
697
  ) {
702
698
  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
- });
699
+ }
700
+ for (let offset = 0; offset < output.length; offset += 50_000) {
701
+ sendAssessmentMessage({
702
+ type: "workspace.assessment.output",
703
+ assessment_id: assessmentId,
704
+ stage,
705
+ delta: output.slice(offset, offset + 50_000),
706
+ });
711
707
  }
712
708
  };
713
709
  const reportOutput = (delta) => {
@@ -719,59 +715,68 @@ async function executeAssessment(assignment, assessmentState) {
719
715
  if (outputBuffer.length >= 50_000) {
720
716
  flushOutput();
721
717
  } else if (!outputTimer) {
722
- outputTimer = setTimeout(flushOutput, 500);
718
+ outputTimer = setTimeout(
719
+ flushOutput,
720
+ ASSESSMENT_OUTPUT_FLUSH_INTERVAL_MS,
721
+ );
723
722
  }
724
723
  };
725
724
  reportProgress(`Starting ${stage} assessment stage`);
726
725
  const heartbeat = setInterval(() => {
727
726
  const now = Date.now();
728
- const inactiveMs = now - lastAgentActivityAt;
729
- const nextInactivityFailure = agentProcessStarted && !agentCompleted
730
- ? assessmentInactivityFailure(stage, inactiveMs)
731
- : null;
727
+ const inactiveMs = now - assessmentState.lastActivityAt;
728
+ const nextInactivityFailure = agentProcessStarted && !agentCompleted
729
+ ? assessmentInactivityFailure(stage, inactiveMs)
730
+ : null;
732
731
  if (!inactivityFailure && nextInactivityFailure) {
733
732
  inactivityFailure = nextInactivityFailure;
734
733
  console.error(`[assessment:${stage}] ${inactivityFailure}`);
735
- void stopProcess(assessmentState.child);
734
+ void cancelAssessment(assessmentState);
736
735
  return;
737
736
  }
738
737
  if (inactivityFailure) return;
739
- reportProgress("Assessment in progress", { milestone: false });
740
738
  statusTicks += 1;
741
739
  if (statusTicks % 2 === 0) {
742
740
  console.log(
743
741
  `[assessment:${stage}] ${formatAssessmentDuration(now - stageStartedAt)} elapsed · ` +
744
- `${lastAgentStatus} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
745
- `(${agentEventCount.toLocaleString()} events).`,
742
+ `${assessmentState.lastMessage} · last agent activity ${formatAssessmentDuration(inactiveMs)} ago ` +
743
+ `(${assessmentState.eventCount.toLocaleString()} events).`,
746
744
  );
747
745
  }
748
746
  }, 15_000);
749
747
  try {
750
- if (assignment.assessment_mode === "incremental") {
751
- progress = 15;
752
- reportProgress("Calculating changed files and affected behavior");
748
+ if (assignment.assessment_mode === "incremental") {
749
+ progress = 15;
750
+ reportProgress("Calculating changed files and affected behavior");
753
751
  }
754
- let result = await executeWorkspaceAssessment(assignment, config, {
752
+ let result = await executeWorkspaceAssessment(assignment, config, {
753
+ onController: (controller) => {
754
+ if (isActiveAssessment()) assessmentState.controller = controller;
755
+ },
755
756
  onProcess: (child) => {
756
757
  if (isActiveAssessment()) {
757
758
  assessmentState.child = child;
758
759
  agentProcessStarted = true;
759
- lastAgentActivityAt = Date.now();
760
+ assessmentState.phase = "running";
761
+ assessmentState.lastActivityAt = Date.now();
760
762
  reportProgress("Connected agent process started");
761
763
  }
762
764
  },
763
765
  onEvent: (event) => {
764
- lastAgentActivityAt = Date.now();
765
- agentEventCount += 1;
766
+ assessmentState.lastActivityAt = Date.now();
767
+ assessmentState.eventCount += 1;
766
768
  reportOutput(assessmentStreamDelta(event));
767
769
  const message = assessmentProgressMessage(event);
768
- if (message) reportProgress(message);
769
- },
770
- });
771
- agentCompleted = true;
772
- flushOutput();
773
- const submitResult = (payload) =>
774
- fetch(
770
+ if (message) reportProgress(message);
771
+ },
772
+ });
773
+ agentCompleted = true;
774
+ assessmentState.phase = "delivering";
775
+ assessmentState.progressPercent = 95;
776
+ assessmentState.lastMessage = "Delivering completed stage result";
777
+ flushOutput();
778
+ const submitResult = (payload) =>
779
+ fetch(
775
780
  assessmentResultUrl(
776
781
  config.server_url,
777
782
  config.connector_id,
@@ -779,38 +784,42 @@ async function executeAssessment(assignment, assessmentState) {
779
784
  ),
780
785
  {
781
786
  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);
787
+ headers: {
788
+ "Content-Type": "application/json",
789
+ Authorization: `Bearer ${config.token}`,
790
+ },
791
+ body: JSON.stringify(payload),
792
+ signal: AbortSignal.timeout(ASSESSMENT_RESULT_TIMEOUT_MS),
793
+ },
794
+ );
795
+ const deliverResult = (payload) =>
796
+ submitAssessmentResultWithRetry(payload, submitResult, {
797
+ isActive: () => isActiveAssessment() && !stopped,
798
+ onRetry: (error, delayMs) => {
799
+ reportProgress("Completed result is waiting for EngineerOS", {
800
+ milestone: false,
801
+ });
802
+ console.warn(
803
+ `[assessment:${stage}] Completed result delivery failed: ${protocolFailureMessage(error)} ` +
804
+ `Retrying in ${Math.round(delayMs / 1_000)}s without rerunning the agent.`,
805
+ );
806
+ },
807
+ });
808
+ let response = await deliverResult(result);
809
+ if (response.status === 422 && result.correctAfterRejection) {
810
+ const rejection = await describeRejectedResponse(
811
+ response,
812
+ "the assessment",
813
+ );
814
+ agentCompleted = false;
815
+ assessmentState.phase = "correcting";
816
+ assessmentState.lastMessage = "Correcting a rejected stage result";
817
+ result = await result.correctAfterRejection(rejection);
818
+ agentCompleted = true;
819
+ assessmentState.phase = "delivering";
820
+ assessmentState.lastMessage = "Delivering corrected stage result";
821
+ flushOutput();
822
+ response = await deliverResult(result);
814
823
  }
815
824
  if (!response.ok) {
816
825
  throw new Error(
@@ -822,14 +831,14 @@ async function executeAssessment(assignment, assessmentState) {
822
831
  } catch (error) {
823
832
  flushOutput();
824
833
  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
- }
834
+ if (isActiveAssessment()) {
835
+ failureReported = sendAssessmentMessage({
836
+ type: "workspace.assessment.failed",
837
+ assessment_id: assessmentId,
838
+ stage,
839
+ message,
840
+ });
841
+ }
833
842
  console.error(message);
834
843
  } finally {
835
844
  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.5",
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",
@@ -354,6 +357,28 @@ export async function executeWorkspaceAssessment(
354
357
  callbacks,
355
358
  ) {
356
359
  const execution = workspaceAssessmentExecution(assignment);
360
+ const assessmentAcpContext =
361
+ config.agent_protocol === "acp"
362
+ ? {
363
+ persistentAcp: true,
364
+ sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
365
+ }
366
+ : undefined;
367
+ const launchAssessmentAgent = (turnPrompt, previousSessionId) => {
368
+ const controller = launchAgentProcess(
369
+ config.workspace,
370
+ turnPrompt,
371
+ execution.sandboxMode,
372
+ config,
373
+ callbacks,
374
+ execution.profile,
375
+ previousSessionId,
376
+ assessmentAcpContext,
377
+ );
378
+ callbacks.onController?.(controller);
379
+ callbacks.onProcess?.(controller.child);
380
+ return controller;
381
+ };
357
382
  const startingRevision = await run(
358
383
  "git",
359
384
  ["rev-parse", "HEAD"],
@@ -386,15 +411,7 @@ export async function executeWorkspaceAssessment(
386
411
  changeImpact.markdown,
387
412
  )
388
413
  : execution.prompt;
389
- const controller = launchAgentProcess(
390
- config.workspace,
391
- prompt,
392
- execution.sandboxMode,
393
- config,
394
- callbacks,
395
- execution.profile,
396
- );
397
- callbacks.onProcess?.(controller.child);
414
+ const controller = launchAssessmentAgent(prompt);
398
415
  let completed = await controller.completed;
399
416
  const recovered = await recoverAssessmentStageOutput({
400
417
  completed,
@@ -405,16 +422,10 @@ export async function executeWorkspaceAssessment(
405
422
  type: "assessment.output_correction",
406
423
  message: "The Agent is completing the required stage report structure",
407
424
  });
408
- const correction = launchAgentProcess(
409
- config.workspace,
425
+ const correction = launchAssessmentAgent(
410
426
  correctionPrompt,
411
- execution.sandboxMode,
412
- config,
413
- callbacks,
414
- execution.profile,
415
427
  previousSessionId,
416
428
  );
417
- callbacks.onProcess?.(correction.child);
418
429
  return correction.completed;
419
430
  },
420
431
  });
@@ -454,19 +465,13 @@ export async function executeWorkspaceAssessment(
454
465
  type: "assessment.output_correction",
455
466
  message: "The Agent is correcting the rejected stage report",
456
467
  });
457
- const correction = launchAgentProcess(
458
- config.workspace,
468
+ const correction = launchAssessmentAgent(
459
469
  assessmentRejectionCorrectionPrompt(
460
470
  validationMessage,
461
471
  execution.requiredOutputHeading,
462
472
  ),
463
- execution.sandboxMode,
464
- config,
465
- callbacks,
466
- execution.profile,
467
473
  completed.sessionId,
468
474
  );
469
- callbacks.onProcess?.(correction.child);
470
475
  const corrected = await correction.completed;
471
476
  const correctedReport = normalizeAgentStructuredOutput(
472
477
  corrected.finalMessage,
@@ -697,16 +702,16 @@ export function codexFailureMessage(output, code) {
697
702
 
698
703
  export function assessmentProgressMessage(event) {
699
704
  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";
705
+ if (event.type === "assessment.output_correction")
706
+ return "Completing the required stage report structure";
707
+ if (event.type === "agent.connected")
708
+ return "Connected agent is ready to inspect the workspace";
704
709
  if (event.type === "acp.plan")
705
710
  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";
711
+ if (event.type === "acp.agent_thought_chunk")
712
+ return "Reasoning through the current implementation";
713
+ if (event.type === "acp.agent_message_chunk")
714
+ return "Drafting the stage report";
710
715
  if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
711
716
  return assessmentCommandMilestone(event.update?.title);
712
717
  }
@@ -824,7 +829,7 @@ function assessmentCommandMilestone(command) {
824
829
  ? command.join(" ")
825
830
  : String(command || "");
826
831
  const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
827
- if (!normalized) return "Inspecting workspace source";
832
+ if (!normalized) return "Inspecting workspace source";
828
833
  if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
829
834
  return "Comparing Git history and workspace changes";
830
835
  }
@@ -930,9 +935,9 @@ export function assessmentStageCorrectionPrompt(
930
935
  "## Incomplete Stage Output Recovery",
931
936
  "",
932
937
  "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.",
938
+ "Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
934
939
  `The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
935
- "Follow every section, inspection, and completeness rule in the Assignment.",
940
+ "Follow every section, inspection, and completeness rule in the Assignment.",
936
941
  "Do not return progress commentary, an explanation of the correction, or a code fence.",
937
942
  ].join("\n");
938
943
  }
@@ -992,7 +997,7 @@ export function enqueueWorkspaceAssessment(
992
997
  return "queued";
993
998
  }
994
999
 
995
- export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
1000
+ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
996
1001
  const available = Math.max(0, limit - activeCount);
997
1002
  if (!available || !queue.length) return [];
998
1003
  if (queue[0]?.parallelizable !== true) {
@@ -1005,36 +1010,37 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
1005
1010
  ) {
1006
1011
  wave.push(queue.shift());
1007
1012
  }
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,
1013
+ return wave;
1014
+ }
1015
+
1016
+ export function workspaceAssessmentWorkerLimit() {
1017
+ return MAX_ASSESSMENT_WORKERS;
1018
+ }
1019
+
1020
+ export function assessmentWorkerSnapshots(activeAssessments) {
1021
+ return [...activeAssessments.values()]
1022
+ .slice(0, MAX_ASSESSMENT_WORKERS)
1023
+ .map((state) => ({
1024
+ assessment_id: String(state.runId),
1025
+ stage: String(state.stage).slice(0, 96),
1026
+ phase: state.phase,
1027
+ progress_percent: Math.max(
1028
+ 0,
1029
+ Math.min(95, Number(state.progressPercent) || 0),
1030
+ ),
1031
+ message: String(state.lastMessage || "Assessment worker is starting").slice(
1032
+ 0,
1033
+ 500,
1034
+ ),
1035
+ started_at: new Date(state.startedAt).toISOString(),
1036
+ last_activity_at: new Date(state.lastActivityAt).toISOString(),
1037
+ event_count: Math.max(0, Number(state.eventCount) || 0),
1038
+ }));
1039
+ }
1040
+
1041
+ export function requeueInterruptedAssessment(
1042
+ queue,
1043
+ assessmentState,
1038
1044
  { accepted, failureReported },
1039
1045
  ) {
1040
1046
  if (accepted || failureReported || !assessmentState?.resumeAssignment) {
@@ -1047,52 +1053,52 @@ export function requeueInterruptedAssessment(
1047
1053
  assessmentState.resumeAssignment,
1048
1054
  { front: true },
1049
1055
  ) === "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 = {}) {
1056
+ );
1057
+ }
1058
+
1059
+ export async function submitAssessmentResultWithRetry(
1060
+ payload,
1061
+ submit,
1062
+ {
1063
+ isActive = () => true,
1064
+ onRetry = () => {},
1065
+ wait = (milliseconds) =>
1066
+ new Promise((resolve) => setTimeout(resolve, milliseconds)),
1067
+ initialDelayMs = 1_000,
1068
+ maxDelayMs = 30_000,
1069
+ } = {},
1070
+ ) {
1071
+ let retryDelayMs = initialDelayMs;
1072
+ while (isActive()) {
1073
+ let retryFailure;
1074
+ try {
1075
+ const response = await submit(payload);
1076
+ if (!isTransientAssessmentResultResponse(response)) return response;
1077
+ await response.body?.cancel?.();
1078
+ retryFailure = new Error(
1079
+ `EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
1080
+ );
1081
+ } catch (error) {
1082
+ retryFailure = error;
1083
+ }
1084
+ if (!isActive()) break;
1085
+ onRetry(retryFailure, retryDelayMs);
1086
+ await wait(retryDelayMs);
1087
+ retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
1088
+ }
1089
+ throw new Error("Assessment result delivery stopped before EngineerOS accepted it.");
1090
+ }
1091
+
1092
+ function isTransientAssessmentResultResponse(response) {
1093
+ return (
1094
+ response.status === 408 ||
1095
+ response.status === 425 ||
1096
+ response.status === 429 ||
1097
+ response.status >= 500
1098
+ );
1099
+ }
1100
+
1101
+ export function connectorExecution(assignment, options = {}) {
1096
1102
  const rawPrompt = assignment?.prompt_markdown;
1097
1103
  if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
1098
1104
  throw new Error("EngineerOS assignment is missing prompt_markdown.");