@engineeros/connector 0.15.1 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,9 @@ 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. Connector `0.15.0` uses half of the locally available CPU parallelism by default, capped at eight assessment workers, and supports an explicit `--assessment-workers` override from 1 to 32. It atomically spools each completed Markdown report under `.engineeros/assessments/<run-id>` before delivery. ACP agents such as OpenCode use isolated stage sessions within one persistent coding-agent process. The connector owns the worker pool and sends one aggregate heartbeat containing every active worker's phase, progress, last activity, and event count; report content is no longer streamed over the WebSocket or written into backend assessment JSON. The backend validates stage checkpoints so dependency fan-out remains authoritative, while the connector retains the reports and reuses them after reconnect without rerunning completed agents. Capability detail and project-specific compliance controls fan out after their catalogs. Final synthesis reads the connector-local Markdown files, then one completed bundle is revalidated, persisted, and published as canonical artifacts. The connector removes its spool only after EngineerOS acknowledges the complete bundle. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. 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 tracked workspace source.
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. Connector `0.15.1` uses half of the locally available CPU parallelism by default, capped at eight assessment workers, and supports an explicit `--assessment-workers` override from 1 to 32. It atomically spools each completed Markdown report under `.engineeros/assessments/<run-id>` before delivery. ACP agents such as OpenCode use isolated stage sessions within one persistent coding-agent process. The connector owns the worker pool and sends one aggregate heartbeat containing every active worker's phase, progress, last activity, and event count; report content is no longer streamed over the WebSocket or written into backend assessment JSON. The backend validates stage checkpoints so dependency fan-out remains authoritative, while the connector retains the reports and reuses them after reconnect without rerunning completed agents. Capability detail and project-specific compliance controls fan out after their catalogs. Final synthesis reads the connector-local Markdown files, then one completed bundle is revalidated, persisted, and published as canonical artifacts. The connector removes its spool only after EngineerOS acknowledges the complete bundle. Compliance findings describe repository-verifiable engineering readiness and missing external context, never legal compliance or certification. 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 applies separate bounded structure, semantic, and corrected-response formatting repairs before failing a stage. Assessment cannot modify tracked workspace source.
51
+
52
+ Every backend `422` content-validation response is returned to the stage agent with the exact failed item. Distinct validation failures continue through correction and resubmission until the complete stage is accepted. Three repetitions of the same unresolved validation error stop the automatic loop while preserving the latest local report for retry. Authentication, cancellation, source drift, and transport failures remain operational errors rather than agent-correction prompts.
51
53
 
52
54
  Override the automatic assessment worker count when pairing (saved for that workspace) or starting the connector (for that process only):
53
55
 
@@ -27,6 +27,7 @@ import {
27
27
  requeueInterruptedAssessment,
28
28
  stopProcess,
29
29
  submitAssessmentResultWithRetry,
30
+ submitAssessmentWithValidationRepair,
30
31
  takeWorkspaceAssessmentWave,
31
32
  workspaceAssessmentWorkerLimit,
32
33
  workspaceSnapshot,
@@ -721,6 +722,46 @@ async function executeAssessment(assignment, assessmentState) {
721
722
  );
722
723
  }
723
724
  }, 15_000);
725
+ const assessmentAgentCallbacks = {
726
+ onController: (controller) => {
727
+ if (isActiveAssessment()) assessmentState.controller = controller;
728
+ },
729
+ onProcess: (child) => {
730
+ if (isActiveAssessment()) {
731
+ assessmentState.child = child;
732
+ agentProcessStarted = true;
733
+ if (assessmentState.phase !== "correcting") {
734
+ assessmentState.phase = "running";
735
+ reportProgress("Connected agent process started");
736
+ }
737
+ assessmentState.lastActivityAt = Date.now();
738
+ }
739
+ },
740
+ onEvent: (event) => {
741
+ assessmentState.lastActivityAt = Date.now();
742
+ assessmentState.eventCount += 1;
743
+ const message = assessmentProgressMessage(event);
744
+ if (message) reportProgress(message);
745
+ },
746
+ };
747
+ const attachStoredCorrection = (candidate, preparedAssignment) =>
748
+ attachAssessmentRejectionCorrection(
749
+ candidate,
750
+ preparedAssignment,
751
+ config,
752
+ assessmentAgentCallbacks,
753
+ {
754
+ draftPath: path
755
+ .join(
756
+ ".engineeros",
757
+ "assessments",
758
+ String(assessmentId),
759
+ candidate.report_file,
760
+ )
761
+ .split(path.sep)
762
+ .join("/"),
763
+ },
764
+ );
724
765
  try {
725
766
  if (assignment.assessment_mode === "incremental") {
726
767
  progress = 15;
@@ -736,63 +777,14 @@ async function executeAssessment(assignment, assessmentState) {
736
777
  stage,
737
778
  );
738
779
  if (result) {
739
- attachAssessmentRejectionCorrection(
740
- result,
780
+ attachStoredCorrection(result, preparedAssignment);
781
+ reportProgress("Recovered completed stage report from connector storage");
782
+ } else {
783
+ result = await executeWorkspaceAssessment(
741
784
  preparedAssignment,
742
785
  config,
743
- {
744
- onController: (controller) => {
745
- if (isActiveAssessment()) assessmentState.controller = controller;
746
- },
747
- onProcess: (child) => {
748
- if (isActiveAssessment()) {
749
- assessmentState.child = child;
750
- agentProcessStarted = true;
751
- assessmentState.phase = "correcting";
752
- assessmentState.lastActivityAt = Date.now();
753
- }
754
- },
755
- onEvent: (event) => {
756
- assessmentState.lastActivityAt = Date.now();
757
- assessmentState.eventCount += 1;
758
- const message = assessmentProgressMessage(event);
759
- if (message) reportProgress(message);
760
- },
761
- },
762
- {
763
- draftPath: path
764
- .join(
765
- ".engineeros",
766
- "assessments",
767
- String(assessmentId),
768
- result.report_file,
769
- )
770
- .split(path.sep)
771
- .join("/"),
772
- },
786
+ assessmentAgentCallbacks,
773
787
  );
774
- reportProgress("Recovered completed stage report from connector storage");
775
- } else {
776
- result = await executeWorkspaceAssessment(preparedAssignment, config, {
777
- onController: (controller) => {
778
- if (isActiveAssessment()) assessmentState.controller = controller;
779
- },
780
- onProcess: (child) => {
781
- if (isActiveAssessment()) {
782
- assessmentState.child = child;
783
- agentProcessStarted = true;
784
- assessmentState.phase = "running";
785
- assessmentState.lastActivityAt = Date.now();
786
- reportProgress("Connected agent process started");
787
- }
788
- },
789
- onEvent: (event) => {
790
- assessmentState.lastActivityAt = Date.now();
791
- assessmentState.eventCount += 1;
792
- const message = assessmentProgressMessage(event);
793
- if (message) reportProgress(message);
794
- },
795
- });
796
788
  await persistAssessmentStageResult(
797
789
  config.workspace,
798
790
  assessmentId,
@@ -837,44 +829,53 @@ async function executeAssessment(assignment, assessmentState) {
837
829
  );
838
830
  },
839
831
  });
840
- const deliveryPayload =
841
- stage === "synthesis"
842
- ? await assessmentCompletionBundle(
832
+ const repairedDelivery = await submitAssessmentWithValidationRepair(
833
+ result,
834
+ {
835
+ submit: deliverResult,
836
+ payloadFor: (candidate) =>
837
+ stage === "synthesis"
838
+ ? assessmentCompletionBundle(
839
+ config.workspace,
840
+ assessmentId,
841
+ candidate,
842
+ )
843
+ : assessmentCheckpointPayload(candidate),
844
+ rejectionFor: (response) =>
845
+ describeRejectedResponse(response, "the assessment"),
846
+ correct: async (candidate, rejection) => {
847
+ agentCompleted = false;
848
+ assessmentState.phase = "correcting";
849
+ assessmentState.lastMessage = "Correcting a rejected stage result";
850
+ if (!candidate.correctAfterRejection) {
851
+ throw new Error(
852
+ `${rejection} The connector could not start the required agent correction.`,
853
+ );
854
+ }
855
+ return candidate.correctAfterRejection(rejection);
856
+ },
857
+ onCorrected: async (candidate) => {
858
+ await removeAssessmentStageResult(
843
859
  config.workspace,
844
860
  assessmentId,
845
- result,
846
- )
847
- : assessmentCheckpointPayload(result);
848
- let response = await deliverResult(deliveryPayload);
849
- if (response.status === 422 && result.correctAfterRejection) {
850
- const rejection = await describeRejectedResponse(
851
- response,
852
- "the assessment",
853
- );
854
- agentCompleted = false;
855
- assessmentState.phase = "correcting";
856
- assessmentState.lastMessage = "Correcting a rejected stage result";
857
- result = await result.correctAfterRejection(rejection);
858
- await removeAssessmentStageResult(config.workspace, assessmentId, stage);
859
- await persistAssessmentStageResult(
860
- config.workspace,
861
- assessmentId,
862
- preparedAssignment,
863
- result,
864
- );
865
- agentCompleted = true;
866
- assessmentState.phase = "delivering";
867
- assessmentState.lastMessage = "Delivering corrected stage result";
868
- response = await deliverResult(
869
- stage === "synthesis"
870
- ? await assessmentCompletionBundle(
871
- config.workspace,
872
- assessmentId,
873
- result,
874
- )
875
- : assessmentCheckpointPayload(result),
876
- );
877
- }
861
+ stage,
862
+ );
863
+ const persisted = await persistAssessmentStageResult(
864
+ config.workspace,
865
+ assessmentId,
866
+ preparedAssignment,
867
+ candidate,
868
+ );
869
+ attachStoredCorrection(persisted, preparedAssignment);
870
+ agentCompleted = true;
871
+ assessmentState.phase = "delivering";
872
+ assessmentState.lastMessage = "Delivering corrected stage result";
873
+ return persisted;
874
+ },
875
+ },
876
+ );
877
+ result = repairedDelivery.result;
878
+ const response = repairedDelivery.response;
878
879
  if (!response.ok) {
879
880
  throw new Error(
880
881
  await describeRejectedResponse(response, "the assessment"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
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
@@ -33,6 +33,7 @@ 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
35
  export const ASSESSMENT_RESULT_TIMEOUT_MS = 120_000;
36
+ export const MAX_REPEATED_ASSESSMENT_VALIDATION_FAILURES = 3;
36
37
  export const MAX_ASSESSMENT_WORKERS = 32;
37
38
  export const MAX_AUTOMATIC_ASSESSMENT_WORKERS = 8;
38
39
  const EXCLUDED_DIRECTORIES = new Set([
@@ -466,12 +467,10 @@ export async function executeWorkspaceAssessment(
466
467
  };
467
468
  };
468
469
  const result = await buildResult(completed, recovered.report);
469
- if (!recovered.correctionUsed) {
470
- attachAssessmentRejectionCorrection(result, assignment, config, callbacks, {
471
- previousSessionId: completed.sessionId,
472
- buildResult,
473
- });
474
- }
470
+ attachAssessmentRejectionCorrection(result, assignment, config, callbacks, {
471
+ previousSessionId: completed.sessionId,
472
+ buildResult,
473
+ });
475
474
  return result;
476
475
  }
477
476
 
@@ -503,23 +502,40 @@ export function attachAssessmentRejectionCorrection(
503
502
  `The authoritative draft is stored at \`${draftPath}\`. Read that file before making the requested correction.`,
504
503
  ].join("\n")
505
504
  : "";
506
- const controller = launchAgentProcess(
507
- config.workspace,
508
- `${assessmentRejectionCorrectionPrompt(validationMessage, execution.requiredOutputHeading)}${draftInstruction}`,
509
- execution.sandboxMode,
510
- config,
511
- callbacks,
512
- execution.profile,
505
+ const correctionPrompt = `${assessmentRejectionCorrectionPrompt(validationMessage, execution.requiredOutputHeading)}${draftInstruction}`;
506
+ const launchCorrection = (prompt, sessionId) => {
507
+ const controller = launchAgentProcess(
508
+ config.workspace,
509
+ prompt,
510
+ execution.sandboxMode,
511
+ config,
512
+ callbacks,
513
+ execution.profile,
514
+ sessionId,
515
+ assessmentAcpContext,
516
+ );
517
+ callbacks.onController?.(controller);
518
+ callbacks.onProcess?.(controller.child);
519
+ return controller;
520
+ };
521
+ let corrected = await launchCorrection(
522
+ correctionPrompt,
513
523
  previousSessionId,
514
- assessmentAcpContext,
515
- );
516
- callbacks.onController?.(controller);
517
- callbacks.onProcess?.(controller.child);
518
- const corrected = await controller.completed;
519
- const correctedReport = normalizeAgentStructuredOutput(
520
- corrected.finalMessage,
521
- execution.requiredOutputHeading,
522
- );
524
+ ).completed;
525
+ const recovered = await recoverAssessmentStageOutput({
526
+ completed: corrected,
527
+ prompt: correctionPrompt,
528
+ requiredOutputHeading: execution.requiredOutputHeading,
529
+ retry: async (formatPrompt, sessionId) => {
530
+ callbacks.onEvent?.({
531
+ type: "assessment.output_correction",
532
+ message: "The Agent is formatting the corrected stage report",
533
+ });
534
+ return launchCorrection(formatPrompt, sessionId).completed;
535
+ },
536
+ });
537
+ corrected = recovered.completed;
538
+ const correctedReport = recovered.report;
523
539
  if (buildResult) {
524
540
  return buildResult(
525
541
  {
@@ -1174,6 +1190,36 @@ export async function submitAssessmentResultWithRetry(
1174
1190
  );
1175
1191
  }
1176
1192
 
1193
+ export async function submitAssessmentWithValidationRepair(
1194
+ initialResult,
1195
+ {
1196
+ submit,
1197
+ payloadFor,
1198
+ rejectionFor,
1199
+ correct,
1200
+ onCorrected = async () => {},
1201
+ maxRepeatedFailures = MAX_REPEATED_ASSESSMENT_VALIDATION_FAILURES,
1202
+ },
1203
+ ) {
1204
+ let result = initialResult;
1205
+ let response = await submit(await payloadFor(result));
1206
+ const rejectionCounts = new Map();
1207
+ while (response.status === 422) {
1208
+ const rejection = await rejectionFor(response);
1209
+ const rejectionCount = (rejectionCounts.get(rejection) || 0) + 1;
1210
+ rejectionCounts.set(rejection, rejectionCount);
1211
+ if (rejectionCount > maxRepeatedFailures) {
1212
+ throw new Error(
1213
+ `${rejection} The Agent repeated this unresolved validation failure ${maxRepeatedFailures} times; the connector retained the latest local report for retry.`,
1214
+ );
1215
+ }
1216
+ result = await correct(result, rejection);
1217
+ result = (await onCorrected(result, rejection)) || result;
1218
+ response = await submit(await payloadFor(result));
1219
+ }
1220
+ return { result, response };
1221
+ }
1222
+
1177
1223
  function isTransientAssessmentResultResponse(response) {
1178
1224
  return (
1179
1225
  response.status === 408 ||