@engineeros/connector 0.15.0 → 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
 
@@ -13,6 +13,7 @@ import {
13
13
  } from "../src/config.mjs";
14
14
  import {
15
15
  ASSESSMENT_RESULT_TIMEOUT_MS,
16
+ attachAssessmentRejectionCorrection,
16
17
  assessmentWorkerSnapshots,
17
18
  assessmentInactivityFailure,
18
19
  assessmentProgressMessage,
@@ -26,6 +27,7 @@ import {
26
27
  requeueInterruptedAssessment,
27
28
  stopProcess,
28
29
  submitAssessmentResultWithRetry,
30
+ submitAssessmentWithValidationRepair,
29
31
  takeWorkspaceAssessmentWave,
30
32
  workspaceAssessmentWorkerLimit,
31
33
  workspaceSnapshot,
@@ -720,6 +722,46 @@ async function executeAssessment(assignment, assessmentState) {
720
722
  );
721
723
  }
722
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
+ );
723
765
  try {
724
766
  if (assignment.assessment_mode === "incremental") {
725
767
  progress = 15;
@@ -735,28 +777,14 @@ async function executeAssessment(assignment, assessmentState) {
735
777
  stage,
736
778
  );
737
779
  if (result) {
780
+ attachStoredCorrection(result, preparedAssignment);
738
781
  reportProgress("Recovered completed stage report from connector storage");
739
782
  } else {
740
- result = await executeWorkspaceAssessment(preparedAssignment, config, {
741
- onController: (controller) => {
742
- if (isActiveAssessment()) assessmentState.controller = controller;
743
- },
744
- onProcess: (child) => {
745
- if (isActiveAssessment()) {
746
- assessmentState.child = child;
747
- agentProcessStarted = true;
748
- assessmentState.phase = "running";
749
- assessmentState.lastActivityAt = Date.now();
750
- reportProgress("Connected agent process started");
751
- }
752
- },
753
- onEvent: (event) => {
754
- assessmentState.lastActivityAt = Date.now();
755
- assessmentState.eventCount += 1;
756
- const message = assessmentProgressMessage(event);
757
- if (message) reportProgress(message);
758
- },
759
- });
783
+ result = await executeWorkspaceAssessment(
784
+ preparedAssignment,
785
+ config,
786
+ assessmentAgentCallbacks,
787
+ );
760
788
  await persistAssessmentStageResult(
761
789
  config.workspace,
762
790
  assessmentId,
@@ -801,44 +829,53 @@ async function executeAssessment(assignment, assessmentState) {
801
829
  );
802
830
  },
803
831
  });
804
- const deliveryPayload =
805
- stage === "synthesis"
806
- ? 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(
807
859
  config.workspace,
808
860
  assessmentId,
809
- result,
810
- )
811
- : assessmentCheckpointPayload(result);
812
- let response = await deliverResult(deliveryPayload);
813
- if (response.status === 422 && result.correctAfterRejection) {
814
- const rejection = await describeRejectedResponse(
815
- response,
816
- "the assessment",
817
- );
818
- agentCompleted = false;
819
- assessmentState.phase = "correcting";
820
- assessmentState.lastMessage = "Correcting a rejected stage result";
821
- result = await result.correctAfterRejection(rejection);
822
- await removeAssessmentStageResult(config.workspace, assessmentId, stage);
823
- await persistAssessmentStageResult(
824
- config.workspace,
825
- assessmentId,
826
- preparedAssignment,
827
- result,
828
- );
829
- agentCompleted = true;
830
- assessmentState.phase = "delivering";
831
- assessmentState.lastMessage = "Delivering corrected stage result";
832
- response = await deliverResult(
833
- stage === "synthesis"
834
- ? await assessmentCompletionBundle(
835
- config.workspace,
836
- assessmentId,
837
- result,
838
- )
839
- : assessmentCheckpointPayload(result),
840
- );
841
- }
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;
842
879
  if (!response.ok) {
843
880
  throw new Error(
844
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.0",
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",
@@ -43,6 +43,7 @@ export async function persistAssessmentStageResult(
43
43
  change_impact_markdown: result.change_impact_markdown ?? null,
44
44
  model: result.model ?? null,
45
45
  usage: result.usage ?? null,
46
+ agent_session_id: result.agent_session_id ?? null,
46
47
  };
47
48
  await atomicWrite(path.join(directory, reportName), `${report}\n`);
48
49
  await atomicWrite(
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([
@@ -462,39 +463,116 @@ export async function executeWorkspaceAssessment(
462
463
  change_impact_markdown: changeImpact.markdown,
463
464
  model: turn.model ?? config.agent_protocol ?? "coding-agent",
464
465
  usage: turn.usage ?? null,
466
+ agent_session_id: turn.sessionId ?? null,
465
467
  };
466
468
  };
467
469
  const result = await buildResult(completed, recovered.report);
468
- if (!recovered.correctionUsed) {
469
- Object.defineProperty(result, "correctAfterRejection", {
470
- enumerable: false,
471
- value: async (validationMessage) => {
472
- callbacks.onEvent?.({
473
- type: "assessment.output_correction",
474
- message: "The Agent is correcting the rejected stage report",
475
- });
476
- const correction = launchAssessmentAgent(
477
- assessmentRejectionCorrectionPrompt(
478
- validationMessage,
479
- execution.requiredOutputHeading,
480
- ),
481
- completed.sessionId,
482
- );
483
- const corrected = await correction.completed;
484
- const correctedReport = normalizeAgentStructuredOutput(
485
- corrected.finalMessage,
486
- execution.requiredOutputHeading,
470
+ attachAssessmentRejectionCorrection(result, assignment, config, callbacks, {
471
+ previousSessionId: completed.sessionId,
472
+ buildResult,
473
+ });
474
+ return result;
475
+ }
476
+
477
+ export function attachAssessmentRejectionCorrection(
478
+ result,
479
+ assignment,
480
+ config,
481
+ callbacks,
482
+ { previousSessionId = result.agent_session_id, draftPath, buildResult } = {},
483
+ ) {
484
+ const execution = workspaceAssessmentExecution(assignment);
485
+ const assessmentAcpContext =
486
+ config.agent_protocol === "acp"
487
+ ? {
488
+ persistentAcp: true,
489
+ sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
490
+ }
491
+ : undefined;
492
+ Object.defineProperty(result, "correctAfterRejection", {
493
+ enumerable: false,
494
+ value: async (validationMessage) => {
495
+ callbacks.onEvent?.({
496
+ type: "assessment.output_correction",
497
+ message: "The Agent is correcting the rejected stage report",
498
+ });
499
+ const draftInstruction = draftPath
500
+ ? [
501
+ "",
502
+ `The authoritative draft is stored at \`${draftPath}\`. Read that file before making the requested correction.`,
503
+ ].join("\n")
504
+ : "";
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,
487
516
  );
517
+ callbacks.onController?.(controller);
518
+ callbacks.onProcess?.(controller.child);
519
+ return controller;
520
+ };
521
+ let corrected = await launchCorrection(
522
+ correctionPrompt,
523
+ previousSessionId,
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;
539
+ if (buildResult) {
488
540
  return buildResult(
489
541
  {
490
542
  ...corrected,
491
- usage: mergeTokenUsage(completed.usage, corrected.usage),
543
+ usage: mergeTokenUsage(result.usage, corrected.usage),
492
544
  },
493
545
  correctedReport,
494
546
  );
495
- },
496
- });
497
- }
547
+ }
548
+ const endingRevision = await run(
549
+ "git",
550
+ ["rev-parse", "HEAD"],
551
+ config.workspace,
552
+ { allowFailure: true },
553
+ );
554
+ const endingHead =
555
+ endingRevision.code === 0
556
+ ? endingRevision.stdout.trim().slice(0, 128)
557
+ : null;
558
+ if (
559
+ assignment.target_head_revision &&
560
+ endingHead !== assignment.target_head_revision
561
+ ) {
562
+ throw new Error(
563
+ "The Git commit changed before assessment correction. Refresh the workspace inventory and assess the new commit.",
564
+ );
565
+ }
566
+ return {
567
+ ...result,
568
+ report_markdown: correctedReport,
569
+ observed_head_revision: endingHead,
570
+ model: corrected.model ?? result.model,
571
+ usage: mergeTokenUsage(result.usage, corrected.usage),
572
+ agent_session_id: corrected.sessionId ?? previousSessionId ?? null,
573
+ };
574
+ },
575
+ });
498
576
  return result;
499
577
  }
500
578
 
@@ -1112,6 +1190,36 @@ export async function submitAssessmentResultWithRetry(
1112
1190
  );
1113
1191
  }
1114
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
+
1115
1223
  function isTransientAssessmentResultResponse(response) {
1116
1224
  return (
1117
1225
  response.status === 408 ||