@engineeros/connector 0.13.3 → 0.14.0

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 any combination of fifteen brownfield evidence 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.0` runs up to three dependency-ready read-only assessment workers concurrently; 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 evidence, 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
 
@@ -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
  }
@@ -693,7 +708,7 @@ async function executeAssessment(assignment, assessmentState) {
693
708
  }
694
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();
@@ -747,7 +762,7 @@ async function executeAssessment(assignment, assessmentState) {
747
762
  const message = inactivityFailure || protocolFailureMessage(error);
748
763
  if (
749
764
  !assessmentState.transportInterrupted &&
750
- active === assessmentState &&
765
+ isActiveAssessment() &&
751
766
  socket.readyState === WebSocket.OPEN
752
767
  ) {
753
768
  socket.send(
@@ -764,8 +779,8 @@ async function executeAssessment(assignment, assessmentState) {
764
779
  } finally {
765
780
  if (outputTimer) clearTimeout(outputTimer);
766
781
  clearInterval(heartbeat);
767
- if (active === assessmentState) {
768
- active = null;
782
+ if (isActiveAssessment()) {
783
+ activeAssessments.delete(assessmentState.key);
769
784
  const replayQueued = requeueInterruptedAssessment(
770
785
  assessments,
771
786
  assessmentState,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.13.3",
3
+ "version": "0.14.0",
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
@@ -845,18 +845,10 @@ function assessmentCommandMilestone(command) {
845
845
 
846
846
  export function workspaceAssessmentExecution(assignment) {
847
847
  const stage = assignment?.stage;
848
- const requiredOutputHeading =
849
- stage === "synthesis"
850
- ? "# Assessment Stage: Synthesis"
851
- : stage === "capability_catalog"
852
- ? "# Assessment Stage: Capability Catalog"
853
- : String(stage || "").startsWith("capability:")
854
- ? "# Assessment Stage: Capabilities"
855
- : `# Assessment Stage: ${String(stage || "").replace(/^./, (value) => value.toUpperCase())}`;
856
- if (
857
- !new Set(["architecture", "capability_catalog", "quality", "synthesis"]).has(stage) &&
858
- !String(stage || "").startsWith("capability:")
859
- ) {
848
+ const requiredOutputHeading = String(
849
+ assignment?.required_output_heading || "",
850
+ ).trim();
851
+ if (!stage || !requiredOutputHeading.startsWith("# Assessment Stage: ")) {
860
852
  throw new Error("EngineerOS assessment assignment has an unsupported stage.");
861
853
  }
862
854
  return {
@@ -1000,6 +992,22 @@ export function enqueueWorkspaceAssessment(
1000
992
  return "queued";
1001
993
  }
1002
994
 
995
+ export function takeWorkspaceAssessmentWave(queue, activeCount, limit = 3) {
996
+ const available = Math.max(0, limit - activeCount);
997
+ if (!available || !queue.length) return [];
998
+ if (queue[0]?.parallelizable !== true) {
999
+ return activeCount === 0 ? queue.splice(0, 1) : [];
1000
+ }
1001
+ const wave = [];
1002
+ while (
1003
+ wave.length < available &&
1004
+ queue[0]?.parallelizable === true
1005
+ ) {
1006
+ wave.push(queue.shift());
1007
+ }
1008
+ return wave;
1009
+ }
1010
+
1003
1011
  export function requeueInterruptedAssessment(
1004
1012
  queue,
1005
1013
  assessmentState,