@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 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 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.16.0` starts assessments with three parallel workers and accepts the run's UI-selected limit from three through eight for every coding-agent provider. It atomically spools each completed Markdown report and each active stage's resumable agent-session checkpoint under `.engineeros/assessments/<run-id>`. 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. 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.
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
- promptStreamEvent,
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
- assessmentCheckpointPayload,
38
- assessmentCompletionBundle,
39
- clearAssessmentSpool,
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
- .join(
794
- ".engineeros",
795
- "assessments",
796
- String(assessmentId),
797
- candidate.report_file,
798
- )
799
- .split(path.sep)
800
- .join("/"),
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
- const previousSessionId = await loadAssessmentStageSession(
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 (previousSessionId) {
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 clearAssessmentSpool(config.workspace, assessmentId);
934
- console.log(
935
- `Workspace assessment ${assessmentId} was accepted by EngineerOS.`,
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
- if (isActiveAssessment()) {
945
- failureReported = sendAssessmentMessage({
946
- type: "workspace.assessment.failed",
947
- assessment_id: assessmentId,
948
- stage,
949
- message,
950
- });
951
- }
952
- console.error(message);
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(`Restarting interrupted assessment stage ${stage}.`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@engineeros/connector",
3
- "version": "0.16.0",
3
+ "version": "0.16.1",
4
4
  "description": "Connect a local coding agent to EngineerOS, using ACP when supported.",
5
5
  "private": false,
6
6
  "type": "module",
@@ -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, assessmentId) {
16
- return path.join(
17
- path.resolve(workspace),
18
- ".engineeros",
19
- "assessments",
20
- String(assessmentId),
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
- stage: result.stage,
39
- report_file: reportName,
40
- report_sha256: sha256(report),
41
- observed_head_revision: result.observed_head_revision ?? null,
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.stage !== stage) {
69
- throw new Error(
70
- `Assessment spool metadata does not match stage '${stage}'.`,
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(workspace, assessmentId) {
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(prompt, previousSessionId);
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 (