@engineeros/connector 0.16.0 → 0.16.1
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 +1 -1
- package/bin/engineeros-connector.mjs +101 -53
- package/package.json +1 -1
- package/src/assessment-spool.mjs +182 -39
- package/src/runner.mjs +42 -7
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
|
|
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 internal checks from product and architecture through security, privacy, project-specific compliance, delivery, reliability, governance, and modernization planning. Reports include only material conclusions and actions; routine healthy checklist rows are not published. Capability entries state what they own, what they link to, and what they depend on. Connector `0.16.1` starts assessments with three parallel workers and accepts the run's UI-selected limit from three through eight for every coding-agent provider. It keeps one local assessment area at `.engineeros/assessment`: the initial accepted run creates `assessment.md`, later code revisions create one consolidated file under `changes/`, and in-progress reports plus resumable agent-session checkpoints stay under `.work/` until final acceptance. 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 completed reports and reloads interrupted ACP or Codex sessions after reconnect when the coding agent supports session restoration. Transient ACP, app-server, provider-availability, and rate-limit failures receive two bounded attempts to continue the saved session without repeating completed inspection. A changed Git commit updates the same durable assessment, retains the prior commit in assessment history, and updates published artifacts in place. Connector reports and session checkpoints are tagged with their target commit so an earlier revision cannot leak into the update. If no assessment exists, EngineerOS creates the initial assessment identity. 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, published as canonical artifacts, and archived locally. 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
51
|
|
|
52
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.
|
|
53
53
|
|
|
@@ -21,10 +21,11 @@ import {
|
|
|
21
21
|
applyAcceptedChange,
|
|
22
22
|
enqueueWorkspaceAssessment,
|
|
23
23
|
executeAssignment,
|
|
24
|
-
executeConnectedPrompt,
|
|
25
|
-
executeWorkspaceAssessment,
|
|
26
|
-
inspectCodingAgent,
|
|
27
|
-
|
|
24
|
+
executeConnectedPrompt,
|
|
25
|
+
executeWorkspaceAssessment,
|
|
26
|
+
inspectCodingAgent,
|
|
27
|
+
isRecoverableAssessmentAgentFailure,
|
|
28
|
+
promptStreamEvent,
|
|
28
29
|
requeueInterruptedAssessment,
|
|
29
30
|
stopProcess,
|
|
30
31
|
submitAssessmentResultWithRetry,
|
|
@@ -33,10 +34,11 @@ import {
|
|
|
33
34
|
workspaceAssessmentWorkerLimit,
|
|
34
35
|
workspaceSnapshot,
|
|
35
36
|
} from "../src/runner.mjs";
|
|
36
|
-
import {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
import {
|
|
38
|
+
archiveCompletedAssessment,
|
|
39
|
+
assessmentCheckpointPayload,
|
|
40
|
+
assessmentCompletionBundle,
|
|
41
|
+
assessmentRunDirectory,
|
|
40
42
|
loadAssessmentStageSession,
|
|
41
43
|
loadAssessmentStageResult,
|
|
42
44
|
persistAssessmentStageResult,
|
|
@@ -689,9 +691,10 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
689
691
|
let agentCompleted = false;
|
|
690
692
|
let statusTicks = 0;
|
|
691
693
|
let progress = 5;
|
|
692
|
-
let accepted = false;
|
|
693
|
-
let failureReported = false;
|
|
694
|
-
let inactivityFailure = null;
|
|
694
|
+
let accepted = false;
|
|
695
|
+
let failureReported = false;
|
|
696
|
+
let inactivityFailure = null;
|
|
697
|
+
let checkpointSessionId = null;
|
|
695
698
|
let lastReportedMilestone = null;
|
|
696
699
|
const creditedMilestones = new Set();
|
|
697
700
|
const isActiveAssessment = () =>
|
|
@@ -754,11 +757,16 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
754
757
|
const assessmentAgentCallbacks = {
|
|
755
758
|
onSession: async (sessionId) => {
|
|
756
759
|
if (!isActiveAssessment()) return;
|
|
760
|
+
checkpointSessionId = String(sessionId);
|
|
757
761
|
await persistAssessmentStageSession(
|
|
758
762
|
config.workspace,
|
|
759
763
|
assessmentId,
|
|
760
764
|
stage,
|
|
761
765
|
sessionId,
|
|
766
|
+
{
|
|
767
|
+
sourceRevision: assignment.source_revision,
|
|
768
|
+
targetHeadRevision: assignment.target_head_revision,
|
|
769
|
+
},
|
|
762
770
|
);
|
|
763
771
|
},
|
|
764
772
|
onController: (controller) => {
|
|
@@ -789,15 +797,16 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
789
797
|
config,
|
|
790
798
|
assessmentAgentCallbacks,
|
|
791
799
|
{
|
|
792
|
-
draftPath: path
|
|
793
|
-
.
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
.
|
|
800
|
+
draftPath: path
|
|
801
|
+
.relative(
|
|
802
|
+
config.workspace,
|
|
803
|
+
path.join(
|
|
804
|
+
assessmentRunDirectory(config.workspace, assessmentId),
|
|
805
|
+
candidate.report_file,
|
|
806
|
+
),
|
|
807
|
+
)
|
|
808
|
+
.split(path.sep)
|
|
809
|
+
.join("/"),
|
|
801
810
|
},
|
|
802
811
|
);
|
|
803
812
|
try {
|
|
@@ -810,27 +819,35 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
810
819
|
assignment,
|
|
811
820
|
);
|
|
812
821
|
let result = await loadAssessmentStageResult(
|
|
813
|
-
config.workspace,
|
|
814
|
-
assessmentId,
|
|
815
|
-
stage,
|
|
816
|
-
|
|
822
|
+
config.workspace,
|
|
823
|
+
assessmentId,
|
|
824
|
+
stage,
|
|
825
|
+
{
|
|
826
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
827
|
+
expectedSourceRevision: assignment.source_revision,
|
|
828
|
+
},
|
|
829
|
+
);
|
|
817
830
|
if (result) {
|
|
818
831
|
attachStoredCorrection(result, preparedAssignment);
|
|
819
832
|
reportProgress("Recovered completed stage report from connector storage");
|
|
820
833
|
} else {
|
|
821
|
-
|
|
834
|
+
checkpointSessionId = await loadAssessmentStageSession(
|
|
822
835
|
config.workspace,
|
|
823
836
|
assessmentId,
|
|
824
837
|
stage,
|
|
838
|
+
{
|
|
839
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
840
|
+
expectedSourceRevision: assignment.source_revision,
|
|
841
|
+
},
|
|
825
842
|
);
|
|
826
|
-
if (
|
|
843
|
+
if (checkpointSessionId) {
|
|
827
844
|
reportProgress("Restoring interrupted agent session");
|
|
828
845
|
}
|
|
829
846
|
result = await executeWorkspaceAssessment(
|
|
830
847
|
preparedAssignment,
|
|
831
848
|
config,
|
|
832
849
|
assessmentAgentCallbacks,
|
|
833
|
-
{ previousSessionId },
|
|
850
|
+
{ previousSessionId: checkpointSessionId },
|
|
834
851
|
);
|
|
835
852
|
await persistAssessmentStageResult(
|
|
836
853
|
config.workspace,
|
|
@@ -882,11 +899,12 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
882
899
|
submit: deliverResult,
|
|
883
900
|
payloadFor: (candidate) =>
|
|
884
901
|
stage === "synthesis"
|
|
885
|
-
? assessmentCompletionBundle(
|
|
886
|
-
config.workspace,
|
|
887
|
-
assessmentId,
|
|
888
|
-
candidate,
|
|
889
|
-
|
|
902
|
+
? assessmentCompletionBundle(
|
|
903
|
+
config.workspace,
|
|
904
|
+
assessmentId,
|
|
905
|
+
candidate,
|
|
906
|
+
{ expectedSourceRevision: assignment.source_revision },
|
|
907
|
+
)
|
|
890
908
|
: assessmentCheckpointPayload(candidate),
|
|
891
909
|
rejectionFor: (response) =>
|
|
892
910
|
describeRejectedResponse(response, "the assessment"),
|
|
@@ -928,28 +946,56 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
928
946
|
await describeRejectedResponse(response, "the assessment"),
|
|
929
947
|
);
|
|
930
948
|
}
|
|
931
|
-
accepted = true;
|
|
932
|
-
if (stage === "synthesis") {
|
|
933
|
-
await
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
949
|
+
accepted = true;
|
|
950
|
+
if (stage === "synthesis") {
|
|
951
|
+
const archivePath = await archiveCompletedAssessment(
|
|
952
|
+
config.workspace,
|
|
953
|
+
assessmentId,
|
|
954
|
+
assignment,
|
|
955
|
+
result,
|
|
956
|
+
);
|
|
957
|
+
console.log(
|
|
958
|
+
`Workspace assessment ${assessmentId} was accepted by EngineerOS and saved to ${path.relative(config.workspace, archivePath)}.`,
|
|
959
|
+
);
|
|
937
960
|
} else {
|
|
938
961
|
console.log(
|
|
939
962
|
`Workspace assessment stage ${stage} was checkpointed by EngineerOS.`,
|
|
940
963
|
);
|
|
941
964
|
}
|
|
942
|
-
} catch (error) {
|
|
943
|
-
const message = inactivityFailure || protocolFailureMessage(error);
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
965
|
+
} catch (error) {
|
|
966
|
+
const message = inactivityFailure || protocolFailureMessage(error);
|
|
967
|
+
const resumeAttempts = Number(
|
|
968
|
+
assignment.connector_session_resume_attempts || 0,
|
|
969
|
+
);
|
|
970
|
+
const canResumeSession =
|
|
971
|
+
!inactivityFailure &&
|
|
972
|
+
checkpointSessionId &&
|
|
973
|
+
isRecoverableAssessmentAgentFailure(error) &&
|
|
974
|
+
resumeAttempts < 2;
|
|
975
|
+
if (isActiveAssessment() && canResumeSession) {
|
|
976
|
+
assessmentState.phase = "waiting";
|
|
977
|
+
assessmentState.lastMessage =
|
|
978
|
+
"Agent session interrupted; resuming checkpoint";
|
|
979
|
+
assessmentState.resumeAssignment = {
|
|
980
|
+
...assignment,
|
|
981
|
+
connector_session_resume_attempts: resumeAttempts + 1,
|
|
982
|
+
};
|
|
983
|
+
const delayMs = 1_000 * 2 ** resumeAttempts;
|
|
984
|
+
console.warn(
|
|
985
|
+
`[assessment:${stage}] Agent session interrupted: ${message} ` +
|
|
986
|
+
`Resuming session ${checkpointSessionId} in ${Math.round(delayMs / 1_000)}s ` +
|
|
987
|
+
`(${resumeAttempts + 1}/2).`,
|
|
988
|
+
);
|
|
989
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
990
|
+
} else if (isActiveAssessment()) {
|
|
991
|
+
failureReported = sendAssessmentMessage({
|
|
992
|
+
type: "workspace.assessment.failed",
|
|
993
|
+
assessment_id: assessmentId,
|
|
994
|
+
stage,
|
|
995
|
+
message,
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
if (!canResumeSession) console.error(message);
|
|
953
999
|
} finally {
|
|
954
1000
|
clearInterval(heartbeat);
|
|
955
1001
|
if (isActiveAssessment()) {
|
|
@@ -958,10 +1004,12 @@ async function executeAssessment(assignment, assessmentState) {
|
|
|
958
1004
|
assessments,
|
|
959
1005
|
assessmentState,
|
|
960
1006
|
{ accepted, failureReported },
|
|
961
|
-
);
|
|
962
|
-
if (replayQueued) {
|
|
963
|
-
console.log(
|
|
964
|
-
|
|
1007
|
+
);
|
|
1008
|
+
if (replayQueued) {
|
|
1009
|
+
console.log(
|
|
1010
|
+
`Resuming interrupted agent session for assessment stage ${stage} from its checkpoint.`,
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
965
1013
|
pump();
|
|
966
1014
|
}
|
|
967
1015
|
}
|
package/package.json
CHANGED
package/src/assessment-spool.mjs
CHANGED
|
@@ -12,14 +12,18 @@ import path from "node:path";
|
|
|
12
12
|
|
|
13
13
|
const LOCAL_REPORTS_MARKER = "<!-- ENGINEEROS_LOCAL_STAGE_REPORTS -->";
|
|
14
14
|
|
|
15
|
-
export function assessmentRunDirectory(workspace,
|
|
16
|
-
return path.join(
|
|
17
|
-
path.resolve(workspace),
|
|
18
|
-
".engineeros",
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
);
|
|
22
|
-
}
|
|
15
|
+
export function assessmentRunDirectory(workspace, _assessmentId) {
|
|
16
|
+
return path.join(
|
|
17
|
+
path.resolve(workspace),
|
|
18
|
+
".engineeros",
|
|
19
|
+
"assessment",
|
|
20
|
+
".work",
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function assessmentArchiveDirectory(workspace) {
|
|
25
|
+
return path.join(path.resolve(workspace), ".engineeros", "assessment");
|
|
26
|
+
}
|
|
23
27
|
|
|
24
28
|
export async function persistAssessmentStageResult(
|
|
25
29
|
workspace,
|
|
@@ -33,12 +37,14 @@ export async function persistAssessmentStageResult(
|
|
|
33
37
|
const basename = assessmentStageBasename(result.stage);
|
|
34
38
|
const reportName = `${basename}.md`;
|
|
35
39
|
const metadataName = `${basename}.result.json`;
|
|
36
|
-
const report = String(result.report_markdown).trim();
|
|
37
|
-
const metadata = {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const report = String(result.report_markdown).trim();
|
|
41
|
+
const metadata = {
|
|
42
|
+
assessment_id: assessmentId,
|
|
43
|
+
stage: result.stage,
|
|
44
|
+
report_file: reportName,
|
|
45
|
+
report_sha256: sha256(report),
|
|
46
|
+
source_revision: assignment.source_revision ?? null,
|
|
47
|
+
observed_head_revision: result.observed_head_revision ?? null,
|
|
42
48
|
changed_files: result.changed_files ?? [],
|
|
43
49
|
change_impact_markdown: result.change_impact_markdown ?? null,
|
|
44
50
|
model: result.model ?? null,
|
|
@@ -54,22 +60,38 @@ export async function persistAssessmentStageResult(
|
|
|
54
60
|
}
|
|
55
61
|
|
|
56
62
|
export async function loadAssessmentStageResult(
|
|
57
|
-
workspace,
|
|
58
|
-
assessmentId,
|
|
59
|
-
stage,
|
|
60
|
-
|
|
63
|
+
workspace,
|
|
64
|
+
assessmentId,
|
|
65
|
+
stage,
|
|
66
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
67
|
+
) {
|
|
61
68
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
62
69
|
const metadataPath = path.join(
|
|
63
70
|
directory,
|
|
64
71
|
`${assessmentStageBasename(stage)}.result.json`,
|
|
65
72
|
);
|
|
66
73
|
try {
|
|
67
|
-
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
68
|
-
if (metadata.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const metadata = JSON.parse(await readFile(metadataPath, "utf8"));
|
|
75
|
+
if (metadata.assessment_id !== assessmentId) {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
if (metadata.stage !== stage) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Assessment spool metadata does not match stage '${stage}'.`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
expectedHeadRevision &&
|
|
85
|
+
metadata.observed_head_revision !== expectedHeadRevision
|
|
86
|
+
) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
expectedSourceRevision &&
|
|
91
|
+
metadata.source_revision !== expectedSourceRevision
|
|
92
|
+
) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
73
95
|
const expectedReportFile = `${assessmentStageBasename(stage)}.md`;
|
|
74
96
|
if (metadata.report_file !== expectedReportFile) {
|
|
75
97
|
throw new Error(
|
|
@@ -96,6 +118,7 @@ export async function persistAssessmentStageSession(
|
|
|
96
118
|
assessmentId,
|
|
97
119
|
stage,
|
|
98
120
|
sessionId,
|
|
121
|
+
{ sourceRevision, targetHeadRevision } = {},
|
|
99
122
|
) {
|
|
100
123
|
const normalizedSessionId = String(sessionId || "").trim();
|
|
101
124
|
if (!normalizedSessionId) {
|
|
@@ -104,8 +127,11 @@ export async function persistAssessmentStageSession(
|
|
|
104
127
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
105
128
|
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
106
129
|
const checkpoint = {
|
|
130
|
+
assessment_id: assessmentId,
|
|
107
131
|
stage,
|
|
108
132
|
agent_session_id: normalizedSessionId,
|
|
133
|
+
source_revision: String(sourceRevision || "").trim() || null,
|
|
134
|
+
target_head_revision: String(targetHeadRevision || "").trim() || null,
|
|
109
135
|
};
|
|
110
136
|
await atomicWrite(
|
|
111
137
|
assessmentStageSessionPath(directory, stage),
|
|
@@ -118,17 +144,33 @@ export async function loadAssessmentStageSession(
|
|
|
118
144
|
workspace,
|
|
119
145
|
assessmentId,
|
|
120
146
|
stage,
|
|
147
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
121
148
|
) {
|
|
122
149
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
123
150
|
try {
|
|
124
151
|
const checkpoint = JSON.parse(
|
|
125
152
|
await readFile(assessmentStageSessionPath(directory, stage), "utf8"),
|
|
126
153
|
);
|
|
154
|
+
if (checkpoint.assessment_id !== assessmentId) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
127
157
|
if (checkpoint.stage !== stage) {
|
|
128
158
|
throw new Error(
|
|
129
159
|
`Assessment session checkpoint does not match stage '${stage}'.`,
|
|
130
160
|
);
|
|
131
161
|
}
|
|
162
|
+
if (
|
|
163
|
+
expectedHeadRevision &&
|
|
164
|
+
checkpoint.target_head_revision !== expectedHeadRevision
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
if (
|
|
169
|
+
expectedSourceRevision &&
|
|
170
|
+
checkpoint.source_revision !== expectedSourceRevision
|
|
171
|
+
) {
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
132
174
|
const sessionId = String(checkpoint.agent_session_id || "").trim();
|
|
133
175
|
if (!sessionId) {
|
|
134
176
|
throw new Error(
|
|
@@ -163,7 +205,11 @@ export async function removeAssessmentStageResult(
|
|
|
163
205
|
);
|
|
164
206
|
}
|
|
165
207
|
|
|
166
|
-
export async function assessmentStageResults(
|
|
208
|
+
export async function assessmentStageResults(
|
|
209
|
+
workspace,
|
|
210
|
+
assessmentId,
|
|
211
|
+
{ expectedHeadRevision, expectedSourceRevision } = {},
|
|
212
|
+
) {
|
|
167
213
|
const directory = assessmentRunDirectory(workspace, assessmentId);
|
|
168
214
|
let entries;
|
|
169
215
|
try {
|
|
@@ -179,11 +225,12 @@ export async function assessmentStageResults(workspace, assessmentId) {
|
|
|
179
225
|
const metadata = JSON.parse(
|
|
180
226
|
await readFile(path.join(directory, entry), "utf8"),
|
|
181
227
|
);
|
|
182
|
-
return loadAssessmentStageResult(
|
|
183
|
-
workspace,
|
|
184
|
-
assessmentId,
|
|
185
|
-
metadata.stage,
|
|
186
|
-
|
|
228
|
+
return loadAssessmentStageResult(
|
|
229
|
+
workspace,
|
|
230
|
+
assessmentId,
|
|
231
|
+
metadata.stage,
|
|
232
|
+
{ expectedHeadRevision, expectedSourceRevision },
|
|
233
|
+
);
|
|
187
234
|
}),
|
|
188
235
|
);
|
|
189
236
|
return results
|
|
@@ -195,7 +242,10 @@ export async function prepareLocalAssessmentAssignment(workspace, assignment) {
|
|
|
195
242
|
const prompt = String(assignment.prompt_markdown || "");
|
|
196
243
|
if (!prompt.includes(LOCAL_REPORTS_MARKER)) return assignment;
|
|
197
244
|
const reports = (
|
|
198
|
-
await assessmentStageResults(workspace, assignment.assessment_id
|
|
245
|
+
await assessmentStageResults(workspace, assignment.assessment_id, {
|
|
246
|
+
expectedHeadRevision: assignment.target_head_revision,
|
|
247
|
+
expectedSourceRevision: assignment.source_revision,
|
|
248
|
+
})
|
|
199
249
|
).filter((result) => result.stage !== "synthesis");
|
|
200
250
|
if (!reports.length) {
|
|
201
251
|
throw new Error(
|
|
@@ -227,19 +277,71 @@ export async function prepareLocalAssessmentAssignment(workspace, assignment) {
|
|
|
227
277
|
};
|
|
228
278
|
}
|
|
229
279
|
|
|
230
|
-
export async function assessmentCompletionBundle(
|
|
231
|
-
workspace,
|
|
232
|
-
assessmentId,
|
|
233
|
-
synthesisResult,
|
|
234
|
-
|
|
280
|
+
export async function assessmentCompletionBundle(
|
|
281
|
+
workspace,
|
|
282
|
+
assessmentId,
|
|
283
|
+
synthesisResult,
|
|
284
|
+
{ expectedSourceRevision } = {},
|
|
285
|
+
) {
|
|
235
286
|
const reports = (
|
|
236
|
-
await assessmentStageResults(workspace, assessmentId
|
|
287
|
+
await assessmentStageResults(workspace, assessmentId, {
|
|
288
|
+
expectedHeadRevision: synthesisResult.observed_head_revision,
|
|
289
|
+
expectedSourceRevision,
|
|
290
|
+
})
|
|
237
291
|
).filter((result) => result.stage !== "synthesis");
|
|
238
292
|
return {
|
|
239
293
|
...assessmentResultPayload(synthesisResult),
|
|
240
294
|
reports: reports.map(assessmentResultPayload),
|
|
241
|
-
};
|
|
242
|
-
}
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export async function archiveCompletedAssessment(
|
|
299
|
+
workspace,
|
|
300
|
+
assessmentId,
|
|
301
|
+
assignment,
|
|
302
|
+
synthesisResult,
|
|
303
|
+
) {
|
|
304
|
+
const bundle = await assessmentCompletionBundle(
|
|
305
|
+
workspace,
|
|
306
|
+
assessmentId,
|
|
307
|
+
synthesisResult,
|
|
308
|
+
{ expectedSourceRevision: assignment.source_revision },
|
|
309
|
+
);
|
|
310
|
+
const archiveDirectory = assessmentArchiveDirectory(workspace);
|
|
311
|
+
const isUpdate = assignment.is_assessment_update === true;
|
|
312
|
+
const target = isUpdate
|
|
313
|
+
? path.join(
|
|
314
|
+
archiveDirectory,
|
|
315
|
+
"changes",
|
|
316
|
+
`${assessmentChangeName(assignment, synthesisResult, assessmentId)}.md`,
|
|
317
|
+
)
|
|
318
|
+
: path.join(archiveDirectory, "assessment.md");
|
|
319
|
+
const heading = isUpdate
|
|
320
|
+
? "# Workspace Assessment Change Log"
|
|
321
|
+
: "# Workspace Assessment";
|
|
322
|
+
const stageReports = [...bundle.reports, assessmentResultPayload(synthesisResult)]
|
|
323
|
+
.map(
|
|
324
|
+
(report) =>
|
|
325
|
+
`## ${assessmentStageTitle(report.stage, report.report_markdown)}\n\n${nestedStageReport(report.report_markdown)}`,
|
|
326
|
+
)
|
|
327
|
+
.join("\n\n");
|
|
328
|
+
const content = [
|
|
329
|
+
heading,
|
|
330
|
+
"",
|
|
331
|
+
`- Inventory revision: \`${assignment.source_revision || "not available"}\``,
|
|
332
|
+
`- Git commit: \`${synthesisResult.observed_head_revision || "not available"}\``,
|
|
333
|
+
"",
|
|
334
|
+
stageReports,
|
|
335
|
+
"",
|
|
336
|
+
].join("\n");
|
|
337
|
+
await mkdir(path.dirname(target), { recursive: true, mode: 0o700 });
|
|
338
|
+
await atomicWrite(target, content);
|
|
339
|
+
await rm(assessmentRunDirectory(workspace, assessmentId), {
|
|
340
|
+
recursive: true,
|
|
341
|
+
force: true,
|
|
342
|
+
});
|
|
343
|
+
return target;
|
|
344
|
+
}
|
|
243
345
|
|
|
244
346
|
export function assessmentCheckpointPayload(result) {
|
|
245
347
|
return {
|
|
@@ -286,6 +388,47 @@ function assessmentStageBasename(stage) {
|
|
|
286
388
|
return `${slug}-${sha256(String(stage)).slice(0, 12)}`;
|
|
287
389
|
}
|
|
288
390
|
|
|
391
|
+
function assessmentStageTitle(stage, markdown) {
|
|
392
|
+
const stageKey = String(stage || "");
|
|
393
|
+
let namedStage = null;
|
|
394
|
+
let label = "";
|
|
395
|
+
if (stageKey.startsWith("capability:")) {
|
|
396
|
+
label = "Capability";
|
|
397
|
+
namedStage = String(markdown || "").match(/^### Capability:\s*(.+?)\s*$/m);
|
|
398
|
+
} else if (stageKey.startsWith("compliance:")) {
|
|
399
|
+
label = "Compliance";
|
|
400
|
+
namedStage = String(markdown || "").match(
|
|
401
|
+
/^### Compliance Framework:\s*(.+?)\s*$/m,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (namedStage) {
|
|
405
|
+
return `${label} — ${namedStage[1].trim()}`;
|
|
406
|
+
}
|
|
407
|
+
return String(stage || "assessment")
|
|
408
|
+
.replace(/[:_-]+/g, " ")
|
|
409
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function nestedStageReport(markdown) {
|
|
413
|
+
return String(markdown || "")
|
|
414
|
+
.trim()
|
|
415
|
+
.replace(/^# Assessment Stage:[^\n]*\n+/i, "")
|
|
416
|
+
.replace(/^\s*-\s+State:\s*.*\n?/gm, "")
|
|
417
|
+
.replace(/^(#{2,5}) /gm, "#$1 ");
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function assessmentChangeName(assignment, synthesisResult, assessmentId) {
|
|
421
|
+
const headRevision = String(synthesisResult.observed_head_revision || "")
|
|
422
|
+
.trim()
|
|
423
|
+
.toLowerCase();
|
|
424
|
+
if (/^[a-f0-9]{7,64}$/.test(headRevision)) {
|
|
425
|
+
return headRevision.slice(0, 16);
|
|
426
|
+
}
|
|
427
|
+
return sha256(
|
|
428
|
+
String(assignment.source_revision || assessmentId),
|
|
429
|
+
).slice(0, 16);
|
|
430
|
+
}
|
|
431
|
+
|
|
289
432
|
function assessmentStageSessionPath(directory, stage) {
|
|
290
433
|
return path.join(directory, `${assessmentStageBasename(stage)}.session.json`);
|
|
291
434
|
}
|
package/src/runner.mjs
CHANGED
|
@@ -416,13 +416,16 @@ export async function executeWorkspaceAssessment(
|
|
|
416
416
|
assignment.target_head_revision,
|
|
417
417
|
)
|
|
418
418
|
: { changedFiles: [], markdown: null };
|
|
419
|
-
const prompt = changeImpact.markdown
|
|
420
|
-
? execution.prompt.replace(
|
|
421
|
-
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
422
|
-
changeImpact.markdown,
|
|
423
|
-
)
|
|
424
|
-
: execution.prompt;
|
|
425
|
-
const controller = launchAssessmentAgent(
|
|
419
|
+
const prompt = changeImpact.markdown
|
|
420
|
+
? execution.prompt.replace(
|
|
421
|
+
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
422
|
+
changeImpact.markdown,
|
|
423
|
+
)
|
|
424
|
+
: execution.prompt;
|
|
425
|
+
const controller = launchAssessmentAgent(
|
|
426
|
+
previousSessionId ? assessmentContinuationPrompt(prompt) : prompt,
|
|
427
|
+
previousSessionId,
|
|
428
|
+
);
|
|
426
429
|
let completed = await controller.completed;
|
|
427
430
|
const recovered = await recoverAssessmentStageOutput({
|
|
428
431
|
completed,
|
|
@@ -1100,6 +1103,38 @@ export function takeWorkspaceAssessmentWave(queue, activeCount, limit) {
|
|
|
1100
1103
|
return wave;
|
|
1101
1104
|
}
|
|
1102
1105
|
|
|
1106
|
+
export function assessmentContinuationPrompt(originalPrompt) {
|
|
1107
|
+
return [
|
|
1108
|
+
"# Continue Interrupted EngineerOS Assessment",
|
|
1109
|
+
"",
|
|
1110
|
+
"Continue the existing assessment task in this agent session from its last completed step.",
|
|
1111
|
+
"Do not restart repository inspection or repeat work already present in the session context.",
|
|
1112
|
+
"Finish and return the complete structured Markdown report required by the original stage contract.",
|
|
1113
|
+
"If the provider could not restore prior context, use the original contract below to complete only the missing work.",
|
|
1114
|
+
"",
|
|
1115
|
+
"## Original Stage Contract",
|
|
1116
|
+
"",
|
|
1117
|
+
String(originalPrompt || "").trim(),
|
|
1118
|
+
].join("\n");
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
export function isRecoverableAssessmentAgentFailure(error) {
|
|
1122
|
+
const message = String(error?.message || error || "").toLowerCase();
|
|
1123
|
+
return [
|
|
1124
|
+
"acp connection closed",
|
|
1125
|
+
"acp agent ended the task with an error",
|
|
1126
|
+
"app-server connection closed",
|
|
1127
|
+
"app server connection closed",
|
|
1128
|
+
"connection reset",
|
|
1129
|
+
"econnreset",
|
|
1130
|
+
"broken pipe",
|
|
1131
|
+
"service unavailable",
|
|
1132
|
+
"too many requests",
|
|
1133
|
+
"rate limit",
|
|
1134
|
+
"overloaded",
|
|
1135
|
+
].some((fragment) => message.includes(fragment));
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1103
1138
|
export function workspaceAssessmentWorkerLimit(value) {
|
|
1104
1139
|
const parsed = Number(value ?? DEFAULT_ASSESSMENT_WORKERS);
|
|
1105
1140
|
if (
|