@cassiomc1/forgeloop 1.14.0 → 2.0.0-rc.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.
Files changed (248) hide show
  1. package/CONTRIBUTING.md +11 -7
  2. package/DOCS_INDEX.md +2 -1
  3. package/ENG/c-development-eng.md +2 -2
  4. package/ENG/cpp-development-eng.md +2 -2
  5. package/ENG/dotnet-aspnetcore-development-eng.md +7 -1
  6. package/ENG/flutter-development-eng.md +4 -2
  7. package/ENG/go-development-eng.md +2 -2
  8. package/ENG/java-development-eng.md +3 -3
  9. package/ENG/nodejs-backend-development-eng.md +8 -4
  10. package/ENG/php-development-eng.md +3 -3
  11. package/ENG/python-development-eng.md +205 -0
  12. package/ENG/rust-development-eng.md +42 -1
  13. package/ENG/sql-development-eng.md +4 -4
  14. package/ENG/swift-development-eng.md +5 -4
  15. package/ENG/test-code-eng.md +1 -1
  16. package/ENG/typescript-development-eng.md +4 -4
  17. package/GUIDE_ROUTER.md +6 -0
  18. package/LICENSE +133 -21
  19. package/LOOP_SYSTEM_DESIGN.md +10 -10
  20. package/README.md +28 -39
  21. package/THIRD_PARTY_NOTICES.md +3 -4
  22. package/completions/_forgeloop +11 -3
  23. package/completions/forgeloop.bash +19 -3
  24. package/completions/forgeloop.fish +61 -1
  25. package/docs/AGENT_PROTOCOL_SUMMARY.md +50 -40
  26. package/docs/ARTIFACT_REFERENCE.md +50 -37
  27. package/docs/CLI_REFERENCE.md +147 -3
  28. package/docs/DOCUMENTATION_GUIDE.md +6 -4
  29. package/docs/GETTING_STARTED.md +3 -3
  30. package/docs/PACKAGE_CONTENTS.md +14 -13
  31. package/docs/RELEASE_CHECKLIST.md +81 -10
  32. package/docs/SELF_HOSTED_TESTS.md +121 -0
  33. package/docs/documentation-manifest.json +25 -12
  34. package/package.json +15 -10
  35. package/schemas/task-bundle.schema.json +7 -2
  36. package/scripts/generate-agent-protocol-summary.mjs +2 -2
  37. package/scripts/run-mcp-clean-tests.mjs +30 -0
  38. package/src/adapters/agent-browser/provider.js +14 -11
  39. package/src/adapters/emulated-services/provider.js +11 -4
  40. package/src/adapters/temporary-paths.js +18 -0
  41. package/src/cli.js +146 -210
  42. package/src/commands/action-show.js +5 -2
  43. package/src/commands/advance.js +5 -1
  44. package/src/commands/baseline.js +13 -11
  45. package/src/commands/checkpoint-revalidate.js +24 -8
  46. package/src/commands/complete.js +19 -3
  47. package/src/commands/contract-create.js +3 -1
  48. package/src/commands/doctor.js +36 -13
  49. package/src/commands/gate-record.js +20 -31
  50. package/src/commands/gate-revalidate.js +11 -13
  51. package/src/commands/handoff-list.js +6 -1
  52. package/src/commands/handoff-show.js +6 -1
  53. package/src/commands/migrate-protocol.js +2 -2
  54. package/src/commands/model-route.js +9 -1
  55. package/src/commands/next.js +7 -1
  56. package/src/commands/policy-diff.js +25 -4
  57. package/src/commands/progress.js +17 -11
  58. package/src/commands/record-diagnosis.js +3 -0
  59. package/src/commands/semantic-plan.js +26 -2
  60. package/src/commands/status.js +6 -1
  61. package/src/commands/storage-backup.js +15 -0
  62. package/src/commands/storage-migrate.js +5 -0
  63. package/src/commands/storage-migration-resume.js +5 -0
  64. package/src/commands/storage-migration-status.js +44 -0
  65. package/src/commands/storage-restore-resume.js +7 -0
  66. package/src/commands/storage-restore.js +8 -0
  67. package/src/commands/task-abandon.js +1 -1
  68. package/src/commands/task-list.js +8 -43
  69. package/src/commands/task-migrate.js +3 -3
  70. package/src/commands/task-recover.js +10 -2
  71. package/src/commands/task-scope.js +18 -2
  72. package/src/commands/task-show.js +16 -2
  73. package/src/commands/validate-protocol.js +29 -14
  74. package/src/commands/validate-receipt.js +8 -1
  75. package/src/config/guides.json +4 -0
  76. package/src/core/action-ledger-projection.js +10 -3
  77. package/src/core/actions.js +38 -75
  78. package/src/core/activation.js +11 -3
  79. package/src/core/approvals.js +17 -51
  80. package/src/core/artifact-registry.js +59 -2
  81. package/src/core/artifacts.js +81 -27
  82. package/src/core/attachment-paths.js +45 -0
  83. package/src/core/attestation-coverage.js +6 -1
  84. package/src/core/attestation-verifier.js +21 -2
  85. package/src/core/attestation.js +17 -6
  86. package/src/core/audit.js +6 -1
  87. package/src/core/bundles.js +160 -66
  88. package/src/core/checkpoint-revalidation.js +12 -10
  89. package/src/core/cli-command-definitions.js +109 -7
  90. package/src/core/code-manifest.js +9 -3
  91. package/src/core/command-executors.js +182 -126
  92. package/src/core/command-input.js +21 -1
  93. package/src/core/completion-artifacts.js +71 -29
  94. package/src/core/completion-ownership.js +19 -8
  95. package/src/core/completion-recovery-rebind.js +11 -1
  96. package/src/core/completion.js +60 -9
  97. package/src/core/conformance.js +2 -1
  98. package/src/core/continuity-lint.js +13 -2
  99. package/src/core/continuity-reconciliation.js +16 -3
  100. package/src/core/continuity.js +16 -8
  101. package/src/core/contract-bootstrap-recovery.js +42 -41
  102. package/src/core/contract-revision.js +8 -9
  103. package/src/core/decision/artifact.js +2 -1
  104. package/src/core/decision/events.js +9 -6
  105. package/src/core/decision/resolver.js +19 -11
  106. package/src/core/decision/service.js +63 -38
  107. package/src/core/decision/task-bindings.js +6 -1
  108. package/src/core/diagnosis-model.js +11 -0
  109. package/src/core/diagnosis.js +10 -1
  110. package/src/core/diagnostic-projection.js +29 -12
  111. package/src/core/diagnostic-record.js +28 -3
  112. package/src/core/efficiency.js +6 -1
  113. package/src/core/events.js +316 -97
  114. package/src/core/execution-prerequisites.js +22 -13
  115. package/src/core/execution.js +26 -40
  116. package/src/core/filesystem.js +11 -0
  117. package/src/core/gate-artifact.js +105 -19
  118. package/src/core/gate-provenance.js +6 -5
  119. package/src/core/handoff.js +26 -14
  120. package/src/core/history.js +39 -35
  121. package/src/core/hypothesis-projection.js +32 -0
  122. package/src/core/information-gain-projection.js +104 -100
  123. package/src/core/inspect.js +12 -3
  124. package/src/core/integration-invocation-policy.js +10 -0
  125. package/src/core/integration-resources.js +41 -34
  126. package/src/core/ledger-event-collection.js +189 -0
  127. package/src/core/ledger-relations.js +23 -0
  128. package/src/core/native-storage.js +12 -0
  129. package/src/core/next-action-continuity.js +31 -9
  130. package/src/core/next-action-phases.js +1 -1
  131. package/src/core/next-action-review-phase.js +2 -2
  132. package/src/core/next-action-verification-phase.js +2 -1
  133. package/src/core/next-action.js +21 -2
  134. package/src/core/phase.js +138 -93
  135. package/src/core/policy-engine.js +12 -13
  136. package/src/core/preflight-consistency.js +72 -59
  137. package/src/core/preflight-loaders.js +19 -12
  138. package/src/core/preflight-model.js +6 -1
  139. package/src/core/preflight.js +29 -12
  140. package/src/core/progress.js +21 -10
  141. package/src/core/protocol-info.js +3 -0
  142. package/src/core/protocol-migration.js +7 -6
  143. package/src/core/reconcile-closure.js +162 -24
  144. package/src/core/recovery-history.js +66 -47
  145. package/src/core/reflection.js +72 -77
  146. package/src/core/responsibility.js +25 -13
  147. package/src/core/route-artifact.js +13 -3
  148. package/src/core/runtime-context.js +5 -0
  149. package/src/core/schema-validation.js +8 -3
  150. package/src/core/settlement.js +12 -1
  151. package/src/core/signing/bundle-file.js +53 -0
  152. package/src/core/signing/bundle-reference.js +15 -0
  153. package/src/core/signing/sigstore.js +60 -33
  154. package/src/core/signing/statement-file.js +27 -0
  155. package/src/core/storage-runtime-registry.js +41 -0
  156. package/src/core/structural-quality/artifacts.js +20 -14
  157. package/src/core/structural-quality/service.js +81 -56
  158. package/src/core/target-layout.js +1 -1
  159. package/src/core/task-claim-state.js +52 -18
  160. package/src/core/task-command.js +21 -1
  161. package/src/core/task-conflict-inspection.js +112 -107
  162. package/src/core/task-descriptor.js +29 -22
  163. package/src/core/task-discovery.js +288 -90
  164. package/src/core/task-lock.js +66 -306
  165. package/src/core/task-migration-validation.js +5 -5
  166. package/src/core/task-migration.js +17 -287
  167. package/src/core/task-snapshot.js +51 -8
  168. package/src/core/task-storage-migration.js +18 -0
  169. package/src/core/templates.js +20 -8
  170. package/src/core/test-intelligence/prune.js +5 -1
  171. package/src/core/trace.js +154 -99
  172. package/src/core/trajectory-evaluation.js +15 -3
  173. package/src/core/trajectory-metrics.js +47 -32
  174. package/src/core/transaction.js +16 -288
  175. package/src/core/verification-scope.js +20 -12
  176. package/src/core/work-state.js +100 -59
  177. package/src/repository-index/binary-manager.js +7 -5
  178. package/src/storage/artifact-bytes.js +22 -0
  179. package/src/storage/attachment-binding.js +11 -0
  180. package/src/storage/attachment-files.js +82 -0
  181. package/src/storage/attachment-references.js +36 -0
  182. package/src/storage/backup.js +53 -0
  183. package/src/storage/capabilities.js +31 -0
  184. package/src/storage/connection-owner.js +119 -0
  185. package/src/storage/connection.js +376 -0
  186. package/src/storage/existing-project-scope.js +45 -0
  187. package/src/storage/export-destination.js +44 -0
  188. package/src/storage/exporter.js +284 -0
  189. package/src/storage/file-durability.js +46 -0
  190. package/src/storage/importer.js +461 -0
  191. package/src/storage/index.js +74 -0
  192. package/src/storage/leases.js +63 -0
  193. package/src/storage/ledger-event-snapshot.js +150 -0
  194. package/src/storage/ledger-relations.js +220 -0
  195. package/src/storage/legacy-maintenance-boundary.js +18 -0
  196. package/src/storage/legacy-signature.js +115 -0
  197. package/src/storage/maintenance-handoff.js +129 -0
  198. package/src/storage/maintenance-owner.js +42 -0
  199. package/src/storage/maintenance.js +310 -0
  200. package/src/storage/metadata-json.js +35 -0
  201. package/src/storage/migration-activate.js +59 -0
  202. package/src/storage/migration-archive.js +57 -0
  203. package/src/storage/migration-attachment-root.js +61 -0
  204. package/src/storage/migration-candidate.js +398 -0
  205. package/src/storage/migration-publication.js +143 -0
  206. package/src/storage/migration-rollback-admission.js +54 -0
  207. package/src/storage/migration-rollback-preparation.js +132 -0
  208. package/src/storage/migration-rollback-publication.js +183 -0
  209. package/src/storage/migration-rollback-release.js +66 -0
  210. package/src/storage/migration-rollback-source-stage.js +103 -0
  211. package/src/storage/migration-rollback.js +43 -0
  212. package/src/storage/migration-source-partition.js +56 -0
  213. package/src/storage/migration-source.js +255 -0
  214. package/src/storage/migration-terminal.js +39 -0
  215. package/src/storage/migration-validation.js +75 -0
  216. package/src/storage/migration.js +43 -0
  217. package/src/storage/operational-attachments.js +49 -0
  218. package/src/storage/operational-context.js +50 -0
  219. package/src/storage/project-backup.js +98 -0
  220. package/src/storage/project-bootstrap.js +24 -0
  221. package/src/storage/project-boundary.js +79 -0
  222. package/src/storage/project-read-snapshot.js +18 -0
  223. package/src/storage/project-replacement.js +137 -0
  224. package/src/storage/project-restore.js +203 -0
  225. package/src/storage/record-definitions.js +45 -0
  226. package/src/storage/repository.js +507 -0
  227. package/src/storage/restore-journal.js +62 -0
  228. package/src/storage/restore-layout.js +30 -0
  229. package/src/storage/restore-preparation.js +84 -0
  230. package/src/storage/restore-replacement-archive.js +162 -0
  231. package/src/storage/restore-replacement-owner.js +31 -0
  232. package/src/storage/restore-replacement-preparation.js +128 -0
  233. package/src/storage/restore-replacement.js +142 -0
  234. package/src/storage/restore-terminal.js +32 -0
  235. package/src/storage/runtime.js +31 -0
  236. package/src/storage/schema.js +214 -0
  237. package/src/storage/singleton-import.js +84 -0
  238. package/src/storage/snapshot-continuity.js +16 -0
  239. package/src/storage/snapshot.js +36 -0
  240. package/src/storage/storage-marker.js +79 -0
  241. package/src/storage/task-guards.js +107 -0
  242. package/src/storage/topology.js +42 -0
  243. package/src/storage/transaction.js +179 -0
  244. package/src/storage/unit-of-work.js +663 -0
  245. package/src/storage/windows-process-incarnation.js +206 -0
  246. package/.forgeloop/forgeloop.gitignore +0 -4
  247. package/LICENSE-DOCS.md +0 -13
  248. package/src/core/transaction-maintenance.js +0 -70
@@ -1,4 +1,5 @@
1
1
  import { ARTIFACT_PATHS, writeJsonArtifact } from "./artifacts.js";
2
+ import { withTaskTransaction } from "./transaction.js";
2
3
  import { readContract } from "./contract.js";
3
4
  import { assertRouteInvariants } from "./router.js";
4
5
  import { validateProfileSources } from "./profile.js";
@@ -35,13 +36,17 @@ const PREVIEW_DECISION_MAX_LENGTH = 240;
35
36
 
36
37
  export { validatePersistedPreflight } from "./preflight-model.js";
37
38
 
38
- export async function evaluatePreflight({ target, packageRoot, strict = false, taskId = null, contractPath = null, routePath = null, statePath = null } = {}) {
39
+ function preflightArtifactPath(explicitPath, taskId, kind) {
40
+ return explicitPath ?? (taskId ? taskArtifactPath(taskId, kind) : ARTIFACT_PATHS[kind]);
41
+ }
42
+
43
+ export async function evaluatePreflight({ target, packageRoot, strict = false, taskId = null, contractPath = null, routePath = null, statePath = null, readers = null } = {}) {
39
44
  const errors = [];
40
45
  const profile = await readProfile(target);
41
46
  const profileProvenance = await validateProfileSources(target, packageRoot);
42
47
  errors.push(...(profileProvenance.errors ?? []));
43
- const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath });
44
- const route = await loadRoute(target, packageRoot, errors, { taskId, routePath });
48
+ const contract = await loadContract(target, packageRoot, errors, { taskId, contractPath, readers });
49
+ const route = await loadRoute(target, packageRoot, errors, { taskId, routePath, readers });
45
50
  const config = await optionalConfig(target, packageRoot, errors);
46
51
  if (config.structuralQuality !== undefined) {
47
52
  try {
@@ -59,9 +64,9 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
59
64
  errors.push(issue("E_PROFILE_UNVERIFIED", "Strict preflight requires a verified project profile", [PROFILE_PATH]));
60
65
  }
61
66
 
62
- const contractRelPath = contractPath ?? (taskId ? taskArtifactPath(taskId, "contract") : ARTIFACT_PATHS.contract);
63
- const routeRelPath = routePath ?? (taskId ? taskArtifactPath(taskId, "route") : ARTIFACT_PATHS.route);
64
- const stateRelPath = statePath ?? (taskId ? taskArtifactPath(taskId, "state") : ARTIFACT_PATHS.state);
67
+ const contractRelPath = preflightArtifactPath(contractPath, taskId, "contract");
68
+ const routeRelPath = preflightArtifactPath(routePath, taskId, "route");
69
+ const stateRelPath = preflightArtifactPath(statePath, taskId, "state");
65
70
 
66
71
  const unresolvedDecisions = contract?.value?.unresolvedDecisions ?? [];
67
72
  if (unresolvedDecisions.length > 0) {
@@ -88,11 +93,11 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
88
93
  }
89
94
 
90
95
  const sources = await loadSources(target, contract, packageRoot, errors);
91
- const gates = await inspectGates(target, contract, route, packageRoot, errors, config, { taskId });
96
+ const gates = await inspectGates(target, contract, route, packageRoot, errors, config, { taskId, readers });
92
97
 
93
98
  let state = null;
94
99
  try {
95
- state = await readWorkState(target, { packageRoot, taskId, statePath });
100
+ state = await (readers?.readState ?? readWorkState)(target, { packageRoot, taskId, statePath });
96
101
  } catch (error) {
97
102
  errors.push(issue("E_STATE_INVALID", error.message, [stateRelPath]));
98
103
  }
@@ -135,7 +140,7 @@ export async function evaluatePreflight({ target, packageRoot, strict = false, t
135
140
  reasonCodes: [],
136
141
  next: config.structuralQuality?.mode === "gate" ? "CAPTURE_STRUCTURAL_QUALITY_BASELINE" : null,
137
142
  };
138
- if (effectiveTaskId !== "unknown") {
143
+ if (effectiveTaskId !== "unknown" && (!readers || structuralQuality.mode !== "off")) {
139
144
  try {
140
145
  const { projectStructuralQualityStatus } = await import("./structural-quality/service.js");
141
146
  structuralQuality = await projectStructuralQualityStatus({ target, packageRoot, taskId: effectiveTaskId });
@@ -183,7 +188,19 @@ export async function validateReadyProtocolConsistency(options = {}) {
183
188
  });
184
189
  }
185
190
 
186
- export async function runPreflight({
191
+ export async function runPreflight(options = {}) {
192
+ if (options.persist === false) return runPreflightInTransaction(options);
193
+ let taskId = options.taskId;
194
+ if (!taskId) {
195
+ const { target, packageRoot, strict, contractPath, routePath, statePath } = options;
196
+ const result = await evaluatePreflight({ target, packageRoot, strict, contractPath, routePath, statePath });
197
+ if (result.taskId !== "unknown") taskId = result.taskId;
198
+ }
199
+ if (!taskId) return runPreflightInTransaction(options);
200
+ return withTaskTransaction({ target: options.target, taskId, packageRoot: options.packageRoot, operation: "preflight" }, () => runPreflightInTransaction({ ...options, taskId }));
201
+ }
202
+
203
+ async function runPreflightInTransaction({
187
204
  target,
188
205
  packageRoot,
189
206
  strict = false,
@@ -198,7 +215,7 @@ export async function runPreflight({
198
215
  let result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
199
216
  if (!persist) return result;
200
217
 
201
- let ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
218
+ let ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, contractPath, routePath, statePath, eventsPath });
202
219
  assertPreflightResultPersistenceSafety(result);
203
220
  assertExistingReadyLifecycleCompatibility(ledger, result);
204
221
 
@@ -224,7 +241,7 @@ export async function runPreflight({
224
241
  result = await evaluatePreflight({ target, packageRoot, strict, taskId, contractPath, routePath, statePath });
225
242
  assertPreflightResultPersistenceSafety(result);
226
243
  assertExistingReadyLifecycleCompatibility(ledger, result);
227
- ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, eventsPath });
244
+ ledger = await assertPreflightPersistenceSafety(target, packageRoot, { taskId, contractPath, routePath, statePath, eventsPath });
228
245
  }
229
246
  }
230
247
 
@@ -1,8 +1,8 @@
1
- import { diagnosisEventsForTask } from "./diagnosis-model.js";
1
+ import { isLedgerEventCollection, ledgerEventsOfTypes } from "./ledger-event-collection.js";
2
+ import { latestDiagnosisForTask } from "./diagnosis-model.js";
2
3
  import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
3
4
  import {
4
- buildInformationGainProjection,
5
- evaluateStructuredDiagnosticStall,
5
+ summarizeInformationGain,
6
6
  } from "./information-gain-projection.js";
7
7
 
8
8
  export const PROGRESS_STATUS = Object.freeze({
@@ -27,6 +27,17 @@ export function diagnosisRequirements(diagnosis, checksById) {
27
27
  )].sort();
28
28
  }
29
29
 
30
+ function progressTaskEvents(events, taskId) {
31
+ if (Array.isArray(events)) return events.filter(event => !taskId || event.taskId === taskId);
32
+ return isLedgerEventCollection(events) ? events : [];
33
+ }
34
+
35
+ function* progressVerificationEvents(events, taskId) {
36
+ for (const event of ledgerEventsOfTypes(events, ["VERIFICATION_RECORDED"])) {
37
+ if (!taskId || event.taskId === taskId) yield event;
38
+ }
39
+ }
40
+
30
41
  export function evaluateProgress({ state, events = [] } = {}) {
31
42
  const signals = [];
32
43
  let status = PROGRESS_STATUS.ADVANCING;
@@ -35,10 +46,10 @@ export function evaluateProgress({ state, events = [] } = {}) {
35
46
  return { status, signals };
36
47
  }
37
48
 
38
- const taskEvents = Array.isArray(events) ? events.filter((e) => !state.taskId || e.taskId === state.taskId) : [];
39
- const diagEvents = diagnosisEventsForTask(taskEvents, state.taskId);
49
+ const taskEvents = progressTaskEvents(events, state.taskId);
50
+ const latestLegacy = latestDiagnosisForTask(taskEvents, state.taskId);
40
51
  const resolvedDiagnostic = resolveCurrentCycleDiagnostic(taskEvents, state.taskId, state.verificationCycle ?? null);
41
- const latestDiag = resolvedDiagnostic?.details ?? diagEvents.at(-1)?.details ?? null;
52
+ const latestDiag = resolvedDiagnostic?.details ?? latestLegacy?.details ?? null;
42
53
 
43
54
  let latestGainClassification = latestDiag?.informationGain ?? null;
44
55
  let structuredStall = null;
@@ -49,8 +60,8 @@ export function evaluateProgress({ state, events = [] } = {}) {
49
60
  )];
50
61
  }
51
62
  // Canonical structured-stall truth; compatibility classification is presentation only.
52
- structuredStall = evaluateStructuredDiagnosticStall(
53
- buildInformationGainProjection(taskEvents, state.taskId),
63
+ structuredStall = summarizeInformationGain(
64
+ taskEvents, state.taskId,
54
65
  { verificationCycle: state.verificationCycle ?? null },
55
66
  );
56
67
  latestDiag.effectiveInformationGain = !(structuredStall.stalled);
@@ -63,7 +74,7 @@ export function evaluateProgress({ state, events = [] } = {}) {
63
74
  if (check.id) checksById.set(check.id, check);
64
75
  if (check.checkId) checksById.set(check.checkId, check);
65
76
  }
66
- for (const event of taskEvents) {
77
+ for (const event of progressVerificationEvents(taskEvents, state.taskId)) {
67
78
  if (event.event === "VERIFICATION_RECORDED" && event.details) {
68
79
  if (event.details.id) checksById.set(event.details.id, event.details);
69
80
  if (event.details.checkId) checksById.set(event.details.checkId, event.details);
@@ -108,7 +119,7 @@ export function evaluateProgress({ state, events = [] } = {}) {
108
119
  }
109
120
 
110
121
  // Also extract from VERIFICATION_RECORDED events
111
- for (const event of taskEvents) {
122
+ for (const event of progressVerificationEvents(taskEvents, state.taskId)) {
112
123
  if (event.event === "VERIFICATION_RECORDED") {
113
124
  const details = event.details ?? {};
114
125
  if (details.status === "failed" || details.status === "blocked") {
@@ -7,6 +7,8 @@ import { VERIFICATION_ISOLATION_MODES } from "./verification-execution.js";
7
7
  import { PROVIDER_KINDS } from "../providers/capabilities.js";
8
8
  import { DECISION_DEFAULT_POLICY, DECISION_ENGINE_ID, PINNED_JEV_MODEL } from "./decision/constants.js";
9
9
 
10
+ import { getStorageCapabilities } from "../storage/capabilities.js";
11
+
10
12
  export const SCHEMA_COMPATIBILITY_POLICY = Object.freeze({
11
13
  protocolVersion: PROTOCOL_VERSION,
12
14
  schemaVersion: 1,
@@ -38,6 +40,7 @@ export function protocolInfo({ packageVersion = null } = {}) {
38
40
  writesSchemaVersions: schemaVersions,
39
41
  compatibility: SCHEMA_COMPATIBILITY_POLICY,
40
42
  features: {
43
+ operationalStorage: getStorageCapabilities(),
41
44
  semanticDecisionPlane: {
42
45
  version: 1,
43
46
  supported: true,
@@ -1,9 +1,10 @@
1
1
  import { E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED } from "./error-codes.js";
2
2
  import { PROTOCOL_VERSION } from "./protocol.js";
3
- import { detectLegacySingletonLayout, migrateLegacyLayout } from "./task-migration.js";
3
+ import { detectLegacySingletonLayout } from "./task-migration.js";
4
+ import { migrateLegacyTaskStorage } from "./task-storage-migration.js";
4
5
  import { getPackageRoot } from "./templates.js";
5
6
 
6
- function assertSupportedTarget(to) {
7
+ export function assertSupportedProtocolMigrationTarget(to) {
7
8
  if (to === undefined || to === null || to === "") {
8
9
  const error = new Error("migrate-protocol requires --to <protocolVersion>");
9
10
  error.code = E_PROTOCOL_MIGRATION_TARGET_UNSUPPORTED;
@@ -26,9 +27,9 @@ function assertSupportedTarget(to) {
26
27
  */
27
28
  export async function migrateProtocol(
28
29
  target,
29
- { to, dryRun = false, packageRoot = getPackageRoot() } = {},
30
+ { to, dryRun = false, packageRoot = getPackageRoot(), destination, writersQuiesced = false } = {},
30
31
  ) {
31
- assertSupportedTarget(to);
32
+ assertSupportedProtocolMigrationTarget(to);
32
33
  const legacy = await detectLegacySingletonLayout(target);
33
34
 
34
35
  if (!legacy.hasLegacy) {
@@ -43,7 +44,7 @@ export async function migrateProtocol(
43
44
  };
44
45
  }
45
46
 
46
- const result = await migrateLegacyLayout(target, { dryRun, packageRoot });
47
+ const result = await migrateLegacyTaskStorage(target, { dryRun, packageRoot, destination, writersQuiesced });
47
48
  return {
48
49
  ...result,
49
50
  fromProtocol: PROTOCOL_VERSION,
@@ -52,7 +53,7 @@ export async function migrateProtocol(
52
53
  actions: [{
53
54
  kind: "LEGACY_LAYOUT_MIGRATION",
54
55
  command: "task-migrate",
55
- receipt: ".forgeloop/task-state/<taskKey>/migration-receipt.json",
56
+ receipt: result.migrationReceipt ?? "<destination>/publication/publication-journal.json",
56
57
  artifacts: legacy.legacyFiles.map((item) => item.path),
57
58
  }],
58
59
  };
@@ -1,3 +1,4 @@
1
+ import { withTaskTransaction } from "./transaction.js";
1
2
  import { readContract } from "./contract.js";
2
3
  import { canonicalFingerprint, readJsonArtifact, writeJsonArtifact } from "./artifacts.js";
3
4
  import { appendProtocolEvent, validateCompletionRecoveryAuthorization, validateEventLedger } from "./events.js";
@@ -7,12 +8,74 @@ import { createReceipt } from "./receipt.js";
7
8
  import { currentRepositoryFingerprint } from "./repository.js";
8
9
  import { taskArtifactPath } from "./task-paths.js";
9
10
  import { resolveTaskClaimState } from "./task-claim-state.js";
11
+ import { DIAGNOSTIC_SCHEMA_VERSION, diagnosticSemanticFingerprint } from "./diagnostic-model.js";
10
12
  import { classifyLoadedWorkState, readWorkState, mutateWorkState } from "./work-state.js";
11
13
  import { classifyRequirement } from "./evidence-readiness.js";
14
+ import { assertDiagnosingToCorrectingTransition } from "./phase.js";
15
+ import { resolveCurrentCycleDiagnostic } from "./diagnostic-projection.js";
12
16
 
13
17
  export const RECONCILE_EVENT = "CHECKPOINT_RECONCILED";
14
18
 
15
- const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "REVIEWING"]);
19
+ /**
20
+ * Build the schema-valid rebound diagnostic case for a drifted DIAGNOSING task
21
+ * that entered the phase before its cycle recorded any diagnosis. The case
22
+ * binds every observation to the just-passed contract-bound evidence execution
23
+ * so the diagnosis carries explicit provenance instead of being manufactured
24
+ * by recovery: the hypothesis is the recovery statement, settled by the
25
+ * successful evidence, with the next safe action being the canonical advance.
26
+ */
27
+ function reconcileReboundDiagnosticCase({
28
+ taskId,
29
+ verificationCycle,
30
+ checkId,
31
+ requirement,
32
+ command,
33
+ executionId,
34
+ repositoryHead,
35
+ }) {
36
+ const statementLimit = 2000;
37
+ const observations = [{
38
+ id: "obs-reconcile-evidence",
39
+ kind: "CHECK_RESULT",
40
+ evidenceRef: checkId,
41
+ provenance: "FORGELOOP_EXECUTED",
42
+ statement: `Contract-bound evidence ${checkId} passed in the current repository (${repositoryHead}); execution ${executionId}: ${command}`.slice(0, statementLimit),
43
+ }];
44
+ const hypotheses = [{
45
+ id: "h-reconcile-rebind",
46
+ statement: `Repository moved after the task entered DIAGNOSING but before the cycle recorded any diagnosis; cycle ${verificationCycle} diagnosis is rebound from the passed contract-bound evidence requirement "${requirement}".`.slice(0, statementLimit),
47
+ contributorRefs: [],
48
+ evidenceRefs: [checkId],
49
+ settledBy: { type: "CHECK_STATUS", checkId, expectedStatus: "passed" },
50
+ status: "OPEN",
51
+ }];
52
+ const details = {
53
+ schemaVersion: DIAGNOSTIC_SCHEMA_VERSION,
54
+ verificationCycle,
55
+ diagnosticRevision: 1,
56
+ failureClass: "VERIFICATION_FAILURE",
57
+ observations,
58
+ contributors: [],
59
+ hypotheses,
60
+ nextSafeAction: {
61
+ statement: "Advance DIAGNOSING -> CORRECTING -> VERIFYING and continue the canonical verification pipeline in the reconciled repository.".slice(0, statementLimit),
62
+ },
63
+ };
64
+ details.diagnosticFingerprint = diagnosticSemanticFingerprint({
65
+ verificationCycle: details.verificationCycle,
66
+ failureClass: details.failureClass,
67
+ case_: details,
68
+ });
69
+ details.previousDiagnosticFingerprint = null;
70
+ return details;
71
+ }
72
+
73
+ // DIAGNOSING is reconcilable under the same diagnosis-boundary condition as
74
+ // CORRECTING: the active cycle must already carry an append-only diagnosis.
75
+ // Without it, drift entering DIAGNOSING before the cycle's first diagnosis is
76
+ // unrecoverable (reconcile-closure, task-recover, handoff, and every mutation
77
+ // all refuse), which stranded real drifted tasks on this exact edge.
78
+ const RECONCILABLE_PHASES = new Set(["EXECUTING", "VERIFYING", "DIAGNOSING", "CORRECTING", "REVIEWING"]);
16
79
 
17
80
  function reconcileError(code, message, artifacts = []) {
18
81
  const error = new Error(message);
@@ -100,6 +163,23 @@ async function validateReconciliationCheckpoint({
100
163
  );
101
164
  }
102
165
 
166
+ await assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel });
167
+
168
+ const reboundOrState = await rebindReconciliationState(target, {
169
+ packageRoot,
170
+ taskId,
171
+ state,
172
+ ledger,
173
+ authorityContext,
174
+ runtimeContext,
175
+ stateRel,
176
+ receiptRel,
177
+ });
178
+ return reboundOrState;
179
+ }
180
+
181
+ /** Fail closed unless the task claim is valid, mutable, and ACTIVE in this checkout. */
182
+ async function assertReconciliationClaimOwnership(target, { taskId, packageRoot, state, stateRel, eventsRel }) {
103
183
  const ownership = await resolveTaskClaimState(target, { taskId, packageRoot });
104
184
  if (!ownership.ownershipValid || !ownership.mutationAllowed || ownership.claimState !== "ACTIVE") {
105
185
  const first = ownership.ownershipErrors?.[0] ?? ownership.errors?.[0] ?? {};
@@ -109,18 +189,46 @@ async function validateReconciliationCheckpoint({
109
189
  [stateRel, eventsRel],
110
190
  );
111
191
  }
192
+ }
193
+
194
+ /** Handle the phase-bound diagnosis rebind gates for CORRECTING/DIAGNOSING and the REVIEWING recovery authorization.
195
+ * Returns the possibly-rebound state (or the original state when no rebind applies). */
196
+ async function rebindReconciliationState(target, {
197
+ packageRoot,
198
+ taskId,
199
+ state,
200
+ ledger,
201
+ authorityContext,
202
+ runtimeContext,
203
+ stateRel,
204
+ receiptRel,
205
+ }) {
206
+ if (state.phase === "CORRECTING" || state.phase === "DIAGNOSING") {
207
+ // The diagnosis boundary for CORRECTING stays absolute: recovery can never
208
+ // manufacture a diagnosis or authorize a retry the ordinary correction
209
+ // transition would refuse. A drifted DIAGNOSING task without a cycle
210
+ // diagnosis is the one legal exception: the drift struck between entering
211
+ // DIAGNOSING and recording the cycle's first diagnosis, and no other
212
+ // command can create either the diagnosis or the fresh checkpoint. Such a
213
+ // task reconciles only after contract-bound evidence passes, and the
214
+ // reconcile itself records the cycle diagnosis below (provenance bound to
215
+ // the passed evidence execution).
216
+ const cycleDiagnosis = resolveCurrentCycleDiagnostic(ledger.events, state.taskId, state.verificationCycle ?? 1);
217
+ if (state.phase === "DIAGNOSING" && !cycleDiagnosis) {
218
+ return { ...state, undiagnosedDiagnosingRebind: true };
219
+ }
220
+ assertDiagnosingToCorrectingTransition({
221
+ state,
222
+ events: ledger.events,
223
+ resolveDiagnosis: resolveCurrentCycleDiagnostic,
224
+ });
225
+ }
112
226
 
113
227
  if (state.phase !== "REVIEWING" || !Object.prototype.hasOwnProperty.call(state, "lastCompletionAttempt")) {
114
228
  return state;
115
229
  }
116
230
 
117
- const recovery = await authorizeCompletionRecoveryOrRebind({
118
- target,
119
- packageRoot,
120
- taskId,
121
- authorityContext,
122
- runtimeContext,
123
- });
231
+ const recovery = await authorizeCompletionRecoveryOrRebind({ target, packageRoot, taskId, authorityContext, runtimeContext });
124
232
  if (!recovery.recoveryAuth.authorized) {
125
233
  const first = recovery.recoveryAuth.errors?.[0] ?? {};
126
234
  throw reconcileError(
@@ -133,12 +241,12 @@ async function validateReconciliationCheckpoint({
133
241
  }
134
242
 
135
243
  /**
136
- * Canonical recovery for an EXECUTING, VERIFYING, or REVIEWING task whose
244
+ * Canonical recovery for an EXECUTING, VERIFYING, CORRECTING, or REVIEWING task whose
137
245
  * objective is already satisfied in the current repository but whose
138
246
  * work-state checkpoint is stale because the repository fingerprint moved.
139
247
  *
140
248
  * The command refreshes the checkpoint repository fingerprint only after:
141
- * - the task is EXECUTING, VERIFYING, or REVIEWING (with either a
249
+ * - the task is EXECUTING, VERIFYING, CORRECTING, or REVIEWING (with either a
142
250
  * persisted completion rejection or the narrow repository-only bootstrap
143
251
  * path),
144
252
  * - classification requires revalidation and the only drift is
@@ -152,7 +260,14 @@ async function validateReconciliationCheckpoint({
152
260
  * VERIFYING, prepare-completion, record-check, complete); claims are
153
261
  * released only by canonical COMPLETE.
154
262
  */
155
- export async function runReconcileClosure({
263
+ export async function runReconcileClosure(options = {}) {
264
+ if (typeof options.taskId !== "string" || !options.taskId.trim()) return reconcileSelectedClosure(options);
265
+ return withTaskTransaction({ target: options.target, packageRoot: options.packageRoot,
266
+ taskId: options.taskId, operation: "reconcile-closure", recordCommitEvent: true },
267
+ () => reconcileSelectedClosure(options));
268
+ }
269
+
270
+ async function reconcileSelectedClosure({
156
271
  target,
157
272
  packageRoot,
158
273
  taskId,
@@ -163,18 +278,7 @@ export async function runReconcileClosure({
163
278
  authorityContext,
164
279
  runtimeContext,
165
280
  } = {}) {
166
- if (typeof taskId !== "string" || !taskId.trim()) {
167
- throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
168
- }
169
- if (typeof checkId !== "string" || !checkId.trim()) {
170
- throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
171
- }
172
- if (typeof requirement !== "string" || !requirement.trim()) {
173
- throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
174
- }
175
- if (!Array.isArray(argv) || argv.length === 0) {
176
- throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
177
- }
281
+ requireReconcileInputs({ taskId, checkId, requirement, argv });
178
282
 
179
283
  const stateRel = taskArtifactPath(taskId, "state");
180
284
  const contractRel = taskArtifactPath(taskId, "contract");
@@ -188,7 +292,7 @@ export async function runReconcileClosure({
188
292
  if (!RECONCILABLE_PHASES.has(state.phase)) {
189
293
  throw reconcileError(
190
294
  "E_RECONCILE_PHASE_INVALID",
191
- `reconcile-closure supports EXECUTING, VERIFYING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
295
+ `reconcile-closure supports EXECUTING, VERIFYING, DIAGNOSING, CORRECTING, or REVIEWING tasks whose objective is already satisfied; found ${state.phase}`,
192
296
  [stateRel],
193
297
  );
194
298
  }
@@ -245,6 +349,24 @@ export async function runReconcileClosure({
245
349
  const repository = await currentRepositoryFingerprint(target);
246
350
  const previous = state.repositoryFingerprint;
247
351
 
352
+ const rebindDiagnosis = Boolean(state.undiagnosedDiagnosingRebind);
353
+ if (rebindDiagnosis) {
354
+ delete state.undiagnosedDiagnosingRebind;
355
+ await appendProtocolEvent(target, {
356
+ taskId,
357
+ event: "DIAGNOSTIC_CASE_RECORDED",
358
+ details: reconcileReboundDiagnosticCase({
359
+ taskId,
360
+ verificationCycle: state.verificationCycle ?? 1,
361
+ checkId,
362
+ requirement,
363
+ command: argv.join(" "),
364
+ executionId: execution.execution.executionId,
365
+ repositoryHead: repository.head,
366
+ }),
367
+ }, packageRoot, { taskId });
368
+ }
369
+
248
370
  await appendProtocolEvent(target, {
249
371
  taskId,
250
372
  event: RECONCILE_EVENT,
@@ -295,3 +417,19 @@ export async function runReconcileClosure({
295
417
  event: RECONCILE_EVENT,
296
418
  };
297
419
  }
420
+
421
+ /** Validate the reconcile arguments exactly as before; shared by the phase-independent input gate. */
422
+ function requireReconcileInputs({ taskId, checkId, requirement, argv } = {}) {
423
+ if (typeof taskId !== "string" || !taskId.trim()) {
424
+ throw reconcileError("E_TASK_REQUIRED", "reconcile-closure requires --task", []);
425
+ }
426
+ if (typeof checkId !== "string" || !checkId.trim()) {
427
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --id", []);
428
+ }
429
+ if (typeof requirement !== "string" || !requirement.trim()) {
430
+ throw reconcileError("E_RECONCILE_REQUIREMENT_UNKNOWN", "reconcile-closure requires --requirement", []);
431
+ }
432
+ if (!Array.isArray(argv) || argv.length === 0) {
433
+ throw reconcileError("E_RECONCILE_EVIDENCE_FAILED", "reconcile-closure requires -- followed by the evidence command argv", []);
434
+ }
435
+ }
@@ -1,3 +1,5 @@
1
+ import { ledgerEventsOfTypes } from "./ledger-event-collection.js";
2
+ import { ledgerRelationSet } from "./ledger-relations.js";
1
3
  import {
2
4
  LEGACY_RECOVERY_MIGRATION_EVENT,
3
5
  isLegacyRecoveryEventShape,
@@ -23,56 +25,65 @@ function recoveryIdOf(event) {
23
25
  : null;
24
26
  }
25
27
 
26
- export function classifyRecoveryHistory(events = []) {
28
+ function recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors) {
29
+ // The known legacy defect signature is never an owning recovery cycle by
30
+ // itself. It remains non-owning historical evidence; ownership comes
31
+ // exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
32
+ // which is validated (binding, uniqueness) by the ledger validator.
33
+ if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) return null;
34
+ // A migration event only counts as a canonical recovery cycle when it
35
+ // binds an actual legacy event present earlier in this ledger.
36
+ if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
37
+ && !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
38
+ && isLegacyRecoveryEventShape(candidate))) {
39
+ errors.push(recoveryError(
40
+ `Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
41
+ ));
42
+ return null;
43
+ }
44
+ const recoveryId = recoveryIdOf(event);
45
+ if (!recoveryId) {
46
+ errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
47
+ return null;
48
+ }
49
+ if (seenRecoveryIds.has(recoveryId)) {
50
+ errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
51
+ return null;
52
+ }
53
+ if (activeCycle) {
54
+ errors.push(recoveryError(
55
+ `Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
56
+ ));
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ recoveryId,
62
+ recoveryEventSeq: event.seq,
63
+ resumedEventSeq: null,
64
+ active: true,
65
+ event,
66
+ resumedEvent: null,
67
+ };
68
+ }
69
+
70
+ function classifyHistory(events, { retainCycles = true, onRecovery = null } = {}) {
27
71
  const recoveries = [];
28
72
  const completedRecoveries = [];
29
73
  const errors = [];
30
- const seenRecoveryIds = new Set();
74
+ const seenRecoveryIds = ledgerRelationSet();
31
75
  let activeCycle = null;
76
+ let completedRecoveryCount = 0;
32
77
 
33
- for (const event of events) {
78
+ const relevant = Array.isArray(events) ? events : ledgerEventsOfTypes(events, [...RECOVERY_EVENT_TYPES, "TASK_RECOVERY_RESUMED"]);
79
+ for (const event of relevant) {
34
80
  if (RECOVERY_EVENT_TYPES.has(event?.event)) {
35
- // The known legacy defect signature is never an owning recovery cycle by
36
- // itself. It remains non-owning historical evidence; ownership comes
37
- // exclusively from its valid LEGACY_RECOVERY_MIGRATION_RECORDED event,
38
- // which is validated (binding, uniqueness) by the ledger validator.
39
- if (!event.details?.recoveryId && isLegacyRecoveryEventShape(event)) continue;
40
- // A migration event only counts as a canonical recovery cycle when it
41
- // binds an actual legacy event present earlier in this ledger.
42
- if (event.event === LEGACY_RECOVERY_MIGRATION_EVENT
43
- && !events.some((candidate) => candidate.seq === event.details?.legacyEventSeq
44
- && isLegacyRecoveryEventShape(candidate))) {
45
- errors.push(recoveryError(
46
- `Legacy migration event at seq ${event?.seq ?? "unknown"} does not bind a legacy recovery event in this ledger`,
47
- ));
48
- continue;
49
- }
50
- const recoveryId = recoveryIdOf(event);
51
- if (!recoveryId) {
52
- errors.push(recoveryError(`Recovery event at seq ${event?.seq ?? "unknown"} has no recoveryId`));
53
- continue;
54
- }
55
- if (seenRecoveryIds.has(recoveryId)) {
56
- errors.push(recoveryError(`Recovery event at seq ${event.seq} reuses recovery id ${recoveryId}`));
57
- continue;
58
- }
59
- if (activeCycle) {
60
- errors.push(recoveryError(
61
- `Recovery ${recoveryId} was recorded while recovery ${activeCycle.recoveryId} is unresolved`,
62
- ));
63
- continue;
64
- }
65
-
66
- activeCycle = {
67
- recoveryId,
68
- recoveryEventSeq: event.seq,
69
- resumedEventSeq: null,
70
- active: true,
71
- event,
72
- resumedEvent: null,
73
- };
74
- seenRecoveryIds.add(recoveryId);
75
- recoveries.push(activeCycle);
81
+ const candidate = recoveryCandidate(event, events, seenRecoveryIds, activeCycle, errors);
82
+ if (!candidate) continue;
83
+ activeCycle = candidate;
84
+ seenRecoveryIds.add(activeCycle.recoveryId);
85
+ if (retainCycles) recoveries.push(activeCycle);
86
+ if (onRecovery) onRecovery(activeCycle);
76
87
  continue;
77
88
  }
78
89
 
@@ -95,23 +106,31 @@ export function classifyRecoveryHistory(events = []) {
95
106
  activeCycle.resumedEventSeq = event.seq;
96
107
  activeCycle.resumedEvent = event;
97
108
  activeCycle.active = false;
98
- completedRecoveries.push(activeCycle);
109
+ completedRecoveryCount += 1;
110
+ if (retainCycles) completedRecoveries.push(activeCycle);
99
111
  activeCycle = null;
100
112
  }
101
113
 
102
114
  const valid = errors.length === 0;
103
115
  return {
104
- recoveries,
116
+ ...(retainCycles ? { recoveries } : {}),
105
117
  activeRecoveryId: valid ? activeCycle?.recoveryId ?? null : null,
106
118
  activeRecovery: valid && activeCycle
107
119
  ? { recoveryId: activeCycle.recoveryId, event: activeCycle.event }
108
120
  : null,
109
- completedRecoveries,
121
+ ...(retainCycles ? { completedRecoveries } : { completedRecoveryCount }),
110
122
  valid,
111
123
  errors,
112
124
  };
113
125
  }
114
126
 
127
+ /** Public evidence keeps its historical arrays. Internal classification can stream claims. */
128
+ export function classifyRecoveryHistory(events = []) { return classifyHistory(events); }
129
+
130
+ export function summarizeRecoveryHistory(events, onRecovery) {
131
+ return classifyHistory(events, { retainCycles: false, onRecovery });
132
+ }
133
+
115
134
  export function resolveRecoveryHistory(events = []) {
116
135
  return classifyRecoveryHistory(events);
117
136
  }