@engineeros/connector 0.13.2 → 0.13.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 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.
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, resumes unfinished work after a connector restart, retry, or temporary connection loss, and then synthesizes the selected results into System State. After the capability catalog is accepted, the connector runs up to three isolated read-only capability workers concurrently; 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
 
@@ -24,6 +24,7 @@ import {
24
24
  promptStreamEvent,
25
25
  requeueInterruptedAssessment,
26
26
  stopProcess,
27
+ takeWorkspaceAssessmentWave,
27
28
  workspaceSnapshot,
28
29
  } from "../src/runner.mjs";
29
30
  import { disposeAcpRuntimes } from "../src/acp-client.mjs";
@@ -195,6 +196,8 @@ firstMessage.capabilities = capabilities;
195
196
 
196
197
  let stopped = false;
197
198
  let active = null;
199
+ const activeAssessments = new Map();
200
+ const ASSESSMENT_WORKER_LIMIT = 3;
198
201
  const available = [];
199
202
  const assessments = [];
200
203
  const activePrompts = new Map();
@@ -212,7 +215,10 @@ process.on("SIGINT", async () => {
212
215
  clearConnectionWatchdog?.();
213
216
  clearTimeout(reconnectTimer);
214
217
  clearInterval(pingTimer);
215
- await stopProcess(active?.child);
218
+ await Promise.all([
219
+ stopProcess(active?.child),
220
+ ...[...activeAssessments.values()].map((state) => stopProcess(state.child)),
221
+ ]);
216
222
  await Promise.all([...activePrompts.values()].map(cancelPrompt));
217
223
  await disposeAcpRuntimes();
218
224
  socket?.close();
@@ -278,9 +284,10 @@ async function connect() {
278
284
  return;
279
285
  }
280
286
  if (message.type === "workspace.assessment") {
287
+ const assessmentKey = `${message.assessment_id}:${message.stage}`;
281
288
  const disposition = enqueueWorkspaceAssessment(
282
289
  assessments,
283
- active,
290
+ activeAssessments.get(assessmentKey),
284
291
  message,
285
292
  );
286
293
  if (disposition === "deferred") {
@@ -339,9 +346,9 @@ async function connect() {
339
346
  clearInterval(pingTimer);
340
347
  for (const promptState of activePrompts.values())
341
348
  void cancelPrompt(promptState);
342
- if (active?.kind === "assessment") {
343
- active.transportInterrupted = true;
344
- if (event.code === 4001) void stopProcess(active.child);
349
+ for (const assessmentState of activeAssessments.values()) {
350
+ assessmentState.transportInterrupted = true;
351
+ if (event.code === 4001) void stopProcess(assessmentState.child);
345
352
  }
346
353
  if (connectionRejected) {
347
354
  stopped = true;
@@ -463,21 +470,27 @@ function sendPromptEvent(promptId, event) {
463
470
  }
464
471
 
465
472
  function pump() {
466
- if (active || socket.readyState !== WebSocket.OPEN) return;
467
- const assessment = assessments.shift();
468
- if (assessment) {
473
+ if (socket.readyState !== WebSocket.OPEN || active) return;
474
+ const wave = takeWorkspaceAssessmentWave(
475
+ assessments,
476
+ activeAssessments.size,
477
+ ASSESSMENT_WORKER_LIMIT,
478
+ );
479
+ for (const assessment of wave) {
480
+ const assessmentKey = `${assessment.assessment_id}:${assessment.stage}`;
469
481
  const assessmentState = {
470
482
  kind: "assessment",
483
+ key: assessmentKey,
471
484
  runId: assessment.assessment_id,
472
485
  stage: assessment.stage,
473
486
  child: null,
474
487
  resumeAssignment: null,
475
488
  transportInterrupted: false,
476
489
  };
477
- active = assessmentState;
490
+ activeAssessments.set(assessmentKey, assessmentState);
478
491
  void executeAssessment(assessment, assessmentState);
479
- return;
480
492
  }
493
+ if (activeAssessments.size > 0 || assessments.length > 0) return;
481
494
  const runId = available.shift();
482
495
  if (!runId) return;
483
496
  active = { kind: "goal", runId, child: null, cancelled: false };
@@ -604,8 +617,10 @@ async function executeAssessment(assignment, assessmentState) {
604
617
  let inactivityFailure = null;
605
618
  let lastReportedMilestone = null;
606
619
  const creditedMilestones = new Set();
620
+ const isActiveAssessment = () =>
621
+ activeAssessments.get(assessmentState.key) === assessmentState;
607
622
  const reportProgress = (message, { milestone = true } = {}) => {
608
- if (active !== assessmentState) return;
623
+ if (!isActiveAssessment()) return;
609
624
  if (milestone && lastReportedMilestone === message) return;
610
625
  if (milestone) {
611
626
  lastReportedMilestone = message;
@@ -635,7 +650,7 @@ async function executeAssessment(assignment, assessmentState) {
635
650
  if (
636
651
  !output ||
637
652
  socket.readyState !== WebSocket.OPEN ||
638
- active !== assessmentState
653
+ !isActiveAssessment()
639
654
  ) {
640
655
  return;
641
656
  }
@@ -691,9 +706,9 @@ async function executeAssessment(assignment, assessmentState) {
691
706
  progress = 15;
692
707
  reportProgress("Calculating changed files and affected evidence");
693
708
  }
694
- const result = await executeWorkspaceAssessment(assignment, config, {
709
+ let result = await executeWorkspaceAssessment(assignment, config, {
695
710
  onProcess: (child) => {
696
- if (active === assessmentState) {
711
+ if (isActiveAssessment()) {
697
712
  assessmentState.child = child;
698
713
  agentProcessStarted = true;
699
714
  lastAgentActivityAt = Date.now();
@@ -709,17 +724,32 @@ async function executeAssessment(assignment, assessmentState) {
709
724
  },
710
725
  });
711
726
  flushOutput();
712
- const response = await fetch(
713
- assessmentResultUrl(config.server_url, config.connector_id, assessmentId),
714
- {
715
- method: "POST",
716
- headers: {
717
- "Content-Type": "application/json",
718
- Authorization: `Bearer ${config.token}`,
727
+ const submitResult = (payload) =>
728
+ fetch(
729
+ assessmentResultUrl(
730
+ config.server_url,
731
+ config.connector_id,
732
+ assessmentId,
733
+ ),
734
+ {
735
+ method: "POST",
736
+ headers: {
737
+ "Content-Type": "application/json",
738
+ Authorization: `Bearer ${config.token}`,
739
+ },
740
+ body: JSON.stringify(payload),
719
741
  },
720
- body: JSON.stringify(result),
721
- },
722
- );
742
+ );
743
+ let response = await submitResult(result);
744
+ if (response.status === 422 && result.correctAfterRejection) {
745
+ const rejection = await describeRejectedResponse(
746
+ response,
747
+ "the assessment",
748
+ );
749
+ result = await result.correctAfterRejection(rejection);
750
+ flushOutput();
751
+ response = await submitResult(result);
752
+ }
723
753
  if (!response.ok) {
724
754
  throw new Error(
725
755
  await describeRejectedResponse(response, "the assessment"),
@@ -732,7 +762,7 @@ async function executeAssessment(assignment, assessmentState) {
732
762
  const message = inactivityFailure || protocolFailureMessage(error);
733
763
  if (
734
764
  !assessmentState.transportInterrupted &&
735
- active === assessmentState &&
765
+ isActiveAssessment() &&
736
766
  socket.readyState === WebSocket.OPEN
737
767
  ) {
738
768
  socket.send(
@@ -749,8 +779,8 @@ async function executeAssessment(assignment, assessmentState) {
749
779
  } finally {
750
780
  if (outputTimer) clearTimeout(outputTimer);
751
781
  clearInterval(heartbeat);
752
- if (active === assessmentState) {
753
- active = null;
782
+ if (isActiveAssessment()) {
783
+ activeAssessments.delete(assessmentState.key);
754
784
  const replayQueued = requeueInterruptedAssessment(
755
785
  assessments,
756
786
  assessmentState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.13.2",
3
+ "version": "0.13.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
@@ -419,31 +419,70 @@ export async function executeWorkspaceAssessment(
419
419
  },
420
420
  });
421
421
  completed = recovered.completed;
422
- const report = recovered.report;
423
- const endingRevision = await run(
424
- "git",
425
- ["rev-parse", "HEAD"],
426
- config.workspace,
427
- { allowFailure: true },
428
- );
429
- const endingHead =
430
- endingRevision.code === 0
431
- ? endingRevision.stdout.trim().slice(0, 128)
432
- : null;
433
- if (startingHead !== endingHead) {
434
- throw new Error(
435
- "The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
436
- );
437
- }
438
- return {
439
- stage: assignment.stage,
440
- report_markdown: report,
441
- observed_head_revision: endingHead,
442
- changed_files: changeImpact.changedFiles,
443
- change_impact_markdown: changeImpact.markdown,
444
- model: completed.model ?? config.agent_protocol ?? "coding-agent",
445
- usage: completed.usage ?? null,
446
- };
422
+ const buildResult = async (turn, report) => {
423
+ const endingRevision = await run(
424
+ "git",
425
+ ["rev-parse", "HEAD"],
426
+ config.workspace,
427
+ { allowFailure: true },
428
+ );
429
+ const endingHead =
430
+ endingRevision.code === 0
431
+ ? endingRevision.stdout.trim().slice(0, 128)
432
+ : null;
433
+ if (startingHead !== endingHead) {
434
+ throw new Error(
435
+ "The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
436
+ );
437
+ }
438
+ return {
439
+ stage: assignment.stage,
440
+ report_markdown: report,
441
+ observed_head_revision: endingHead,
442
+ changed_files: changeImpact.changedFiles,
443
+ change_impact_markdown: changeImpact.markdown,
444
+ model: turn.model ?? config.agent_protocol ?? "coding-agent",
445
+ usage: turn.usage ?? null,
446
+ };
447
+ };
448
+ const result = await buildResult(completed, recovered.report);
449
+ if (!recovered.correctionUsed) {
450
+ Object.defineProperty(result, "correctAfterRejection", {
451
+ enumerable: false,
452
+ value: async (validationMessage) => {
453
+ callbacks.onEvent?.({
454
+ type: "assessment.output_correction",
455
+ message: "The Agent is correcting the rejected stage report",
456
+ });
457
+ const correction = launchAgentProcess(
458
+ config.workspace,
459
+ assessmentRejectionCorrectionPrompt(
460
+ validationMessage,
461
+ execution.requiredOutputHeading,
462
+ ),
463
+ execution.sandboxMode,
464
+ config,
465
+ callbacks,
466
+ execution.profile,
467
+ completed.sessionId,
468
+ );
469
+ callbacks.onProcess?.(correction.child);
470
+ const corrected = await correction.completed;
471
+ const correctedReport = normalizeAgentStructuredOutput(
472
+ corrected.finalMessage,
473
+ execution.requiredOutputHeading,
474
+ );
475
+ return buildResult(
476
+ {
477
+ ...corrected,
478
+ usage: mergeTokenUsage(completed.usage, corrected.usage),
479
+ },
480
+ correctedReport,
481
+ );
482
+ },
483
+ });
484
+ }
485
+ return result;
447
486
  }
448
487
 
449
488
  export async function workspaceChangeImpact(
@@ -835,6 +874,7 @@ export async function recoverAssessmentStageOutput({
835
874
  try {
836
875
  return {
837
876
  completed,
877
+ correctionUsed: false,
838
878
  report: normalizeAgentStructuredOutput(
839
879
  completed.finalMessage,
840
880
  requiredOutputHeading,
@@ -854,6 +894,7 @@ export async function recoverAssessmentStageOutput({
854
894
  ...corrected,
855
895
  usage: mergeTokenUsage(completed.usage, corrected.usage),
856
896
  },
897
+ correctionUsed: true,
857
898
  report: normalizeAgentStructuredOutput(
858
899
  corrected.finalMessage,
859
900
  requiredOutputHeading,
@@ -867,6 +908,26 @@ export async function recoverAssessmentStageOutput({
867
908
  }
868
909
  }
869
910
 
911
+ export function assessmentRejectionCorrectionPrompt(
912
+ validationMessage,
913
+ requiredOutputHeading,
914
+ ) {
915
+ return [
916
+ "## Rejected Stage Output Recovery",
917
+ "",
918
+ "EngineerOS rejected the previous stage report because it was incomplete or violated the structured Markdown contract:",
919
+ "",
920
+ String(validationMessage || "The stage report was rejected.").trim(),
921
+ "",
922
+ "Treat the previous stage report as the authoritative draft.",
923
+ "Copy every valid section and block unchanged; repair only the incomplete or invalid entries identified by EngineerOS validation.",
924
+ "Do not re-inspect the repository or replace valid evidence unless the validation error requires it.",
925
+ "Return the entire corrected structured Markdown stage report so EngineerOS can validate it atomically, not only the repaired fragment or missing tail.",
926
+ `The first non-whitespace line must be exactly: ${requiredOutputHeading}`,
927
+ "Do not return progress commentary, an explanation of the correction, or a code fence.",
928
+ ].join("\n");
929
+ }
930
+
870
931
  export function assessmentStageCorrectionPrompt(
871
932
  originalPrompt,
872
933
  requiredOutputHeading,
@@ -939,6 +1000,22 @@ export function enqueueWorkspaceAssessment(
939
1000
  return "queued";
940
1001
  }
941
1002
 
1003
+ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
1004
+ const available = Math.max(0, limit - activeCount);
1005
+ if (!available || !queue.length) return [];
1006
+ if (!String(queue[0]?.stage || "").startsWith("capability:")) {
1007
+ return activeCount === 0 ? queue.splice(0, 1) : [];
1008
+ }
1009
+ const wave = [];
1010
+ while (
1011
+ wave.length < available &&
1012
+ String(queue[0]?.stage || "").startsWith("capability:")
1013
+ ) {
1014
+ wave.push(queue.shift());
1015
+ }
1016
+ return wave;
1017
+ }
1018
+
942
1019
  export function requeueInterruptedAssessment(
943
1020
  queue,
944
1021
  assessmentState,