blun-king-cli 9.1.566 → 9.1.569

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 (262) hide show
  1. package/agent-spine-plugin/.claude-plugin/marketplace.json +1 -1
  2. package/agent-spine-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-spine-plugin/.codex-plugin/plugin.json +2 -1
  4. package/agent-spine-plugin/CHANGELOG.md +1581 -0
  5. package/agent-spine-plugin/README.md +30 -4
  6. package/agent-spine-plugin/blun.plugin.json +3 -3
  7. package/agent-spine-plugin/docs/acceptance.md +61 -0
  8. package/agent-spine-plugin/docs/assignment-continuation.md +48 -0
  9. package/agent-spine-plugin/docs/host-integration.md +178 -0
  10. package/agent-spine-plugin/docs/preflight-recall.md +69 -0
  11. package/agent-spine-plugin/docs/preservation-contract.md +53 -0
  12. package/agent-spine-plugin/docs/quality-gates.md +50 -0
  13. package/agent-spine-plugin/docs/releasing.md +85 -0
  14. package/agent-spine-plugin/docs/session-timeline.md +251 -0
  15. package/agent-spine-plugin/docs/source-roots.md +113 -0
  16. package/agent-spine-plugin/docs/structured-completion.md +67 -0
  17. package/agent-spine-plugin/docs/world-model.md +94 -0
  18. package/agent-spine-plugin/hooks/codex.json +2 -2
  19. package/agent-spine-plugin/hooks/hooks.json +1 -1
  20. package/agent-spine-plugin/hooks/version.json +1 -1
  21. package/agent-spine-plugin/package.json +13 -3
  22. package/agent-spine-plugin/scripts/check-codex-install.js +226 -0
  23. package/agent-spine-plugin/scripts/check-hosts.js +14 -5
  24. package/agent-spine-plugin/scripts/check-install-hook.js +226 -0
  25. package/agent-spine-plugin/scripts/check-install-selfstarter.js +154 -0
  26. package/agent-spine-plugin/scripts/check-install.js +478 -0
  27. package/agent-spine-plugin/scripts/check-line-budget.js +58 -0
  28. package/agent-spine-plugin/scripts/check-syntax.js +29 -0
  29. package/agent-spine-plugin/scripts/github-actions.js +11 -0
  30. package/agent-spine-plugin/scripts/hermetic-process.js +183 -0
  31. package/agent-spine-plugin/scripts/release-check.js +145 -0
  32. package/agent-spine-plugin/scripts/run-acceptance.js +19 -0
  33. package/agent-spine-plugin/scripts/run-checks.js +47 -0
  34. package/agent-spine-plugin/scripts/run-tests-hermetic.js +89 -0
  35. package/agent-spine-plugin/skills/agent-spine/SKILL.md +16 -2
  36. package/agent-spine-plugin/spine-example/1-identity.md +12 -0
  37. package/agent-spine-plugin/spine-example/2-voice.md +6 -0
  38. package/agent-spine-plugin/spine-example/3-conduct.md +8 -0
  39. package/agent-spine-plugin/spine-example/4-history.md +4 -0
  40. package/agent-spine-plugin/src/cli-agent.js +296 -0
  41. package/agent-spine-plugin/src/cli-attention.js +95 -0
  42. package/agent-spine-plugin/src/cli-autonomy.js +36 -0
  43. package/agent-spine-plugin/src/cli-common.js +71 -0
  44. package/agent-spine-plugin/src/cli-continuity.js +116 -0
  45. package/agent-spine-plugin/src/cli-core.js +128 -0
  46. package/agent-spine-plugin/src/cli-diagnostics.js +291 -0
  47. package/agent-spine-plugin/src/cli-host.js +21 -0
  48. package/agent-spine-plugin/src/cli-learning.js +305 -0
  49. package/agent-spine-plugin/src/cli-premortem.js +16 -0
  50. package/agent-spine-plugin/src/cli-sharing.js +230 -0
  51. package/agent-spine-plugin/src/cli.js +40 -1350
  52. package/agent-spine-plugin/src/codex-reader-launcher.js +182 -0
  53. package/agent-spine-plugin/src/hook.js +185 -567
  54. package/agent-spine-plugin/src/index.js +16 -2
  55. package/agent-spine-plugin/src/lib/acceptance.js +113 -11
  56. package/agent-spine-plugin/src/lib/action-lesson-recall.js +53 -0
  57. package/agent-spine-plugin/src/lib/attention-context.js +167 -0
  58. package/agent-spine-plugin/src/lib/attention-events.js +113 -0
  59. package/agent-spine-plugin/src/lib/attention-privacy.js +72 -0
  60. package/agent-spine-plugin/src/lib/attention-schema.js +164 -0
  61. package/agent-spine-plugin/src/lib/attention-storage.js +93 -0
  62. package/agent-spine-plugin/src/lib/attention.js +9 -600
  63. package/agent-spine-plugin/src/lib/audit-premortem.js +223 -0
  64. package/agent-spine-plugin/src/lib/audit.js +56 -6
  65. package/agent-spine-plugin/src/lib/autonomy-policy.js +110 -0
  66. package/agent-spine-plugin/src/lib/autonomy-store.js +202 -0
  67. package/agent-spine-plugin/src/lib/autonomy.js +8 -0
  68. package/agent-spine-plugin/src/lib/briefing.js +67 -4
  69. package/agent-spine-plugin/src/lib/catalog-document-read.js +51 -0
  70. package/agent-spine-plugin/src/lib/catalog.js +1 -1
  71. package/agent-spine-plugin/src/lib/codex-installation.js +231 -0
  72. package/agent-spine-plugin/src/lib/codex-skill-installation.js +211 -0
  73. package/agent-spine-plugin/src/lib/context.js +3 -1
  74. package/agent-spine-plugin/src/lib/delivery-agent-usage.js +224 -0
  75. package/agent-spine-plugin/src/lib/delivery-assignment.js +220 -0
  76. package/agent-spine-plugin/src/lib/delivery-command-actions.js +453 -0
  77. package/agent-spine-plugin/src/lib/delivery-knowledge.js +78 -0
  78. package/agent-spine-plugin/src/lib/delivery-premortem-binding.js +107 -0
  79. package/agent-spine-plugin/src/lib/delivery-premortem-closure.js +171 -0
  80. package/agent-spine-plugin/src/lib/delivery-premortem-codec.js +45 -0
  81. package/agent-spine-plugin/src/lib/delivery-premortem-correction.js +101 -0
  82. package/agent-spine-plugin/src/lib/delivery-premortem-file.js +21 -0
  83. package/agent-spine-plugin/src/lib/delivery-premortem-index.js +493 -0
  84. package/agent-spine-plugin/src/lib/delivery-premortem-inspection.js +37 -0
  85. package/agent-spine-plugin/src/lib/delivery-premortem-recovery.js +120 -0
  86. package/agent-spine-plugin/src/lib/delivery-premortem-rejection.js +65 -0
  87. package/agent-spine-plugin/src/lib/delivery-premortem-results.js +28 -0
  88. package/agent-spine-plugin/src/lib/delivery-premortem-session-guard.js +46 -0
  89. package/agent-spine-plugin/src/lib/delivery-premortem-write-ledger.js +285 -0
  90. package/agent-spine-plugin/src/lib/delivery-premortem.js +499 -0
  91. package/agent-spine-plugin/src/lib/delivery-shell-heredoc.js +115 -0
  92. package/agent-spine-plugin/src/lib/delivery-shell-substitutions.js +111 -0
  93. package/agent-spine-plugin/src/lib/delivery-shell-wrapper.js +64 -0
  94. package/agent-spine-plugin/src/lib/delivery-target.js +73 -0
  95. package/agent-spine-plugin/src/lib/delivery-verification.js +445 -0
  96. package/agent-spine-plugin/src/lib/documents.js +27 -5
  97. package/agent-spine-plugin/src/lib/filesystem-retry.js +2 -0
  98. package/agent-spine-plugin/src/lib/gateway-common.js +68 -0
  99. package/agent-spine-plugin/src/lib/gateway-control.js +302 -0
  100. package/agent-spine-plugin/src/lib/gateway-delivery.js +103 -0
  101. package/agent-spine-plugin/src/lib/gateway-execution.js +350 -0
  102. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +185 -0
  103. package/agent-spine-plugin/src/lib/gateway-inspection.js +85 -0
  104. package/agent-spine-plugin/src/lib/gateway-knowledge.js +129 -0
  105. package/agent-spine-plugin/src/lib/gateway-policy-provenance.js +197 -0
  106. package/agent-spine-plugin/src/lib/gateway-premortem-disposition.js +82 -0
  107. package/agent-spine-plugin/src/lib/gateway-premortem.js +363 -0
  108. package/agent-spine-plugin/src/lib/gateway-runs.js +300 -0
  109. package/agent-spine-plugin/src/lib/gateway-runtime-identity.js +31 -0
  110. package/agent-spine-plugin/src/lib/gateway-runtime-records.js +21 -0
  111. package/agent-spine-plugin/src/lib/gateway-runtime.js +18 -1623
  112. package/agent-spine-plugin/src/lib/gateway-state-transaction.js +356 -0
  113. package/agent-spine-plugin/src/lib/gateway-state.js +342 -0
  114. package/agent-spine-plugin/src/lib/hook-artifact-guards.js +356 -0
  115. package/agent-spine-plugin/src/lib/hook-audit.js +16 -2
  116. package/agent-spine-plugin/src/lib/hook-briefing-use.js +68 -0
  117. package/agent-spine-plugin/src/lib/hook-context.js +424 -0
  118. package/agent-spine-plugin/src/lib/hook-final-message.js +26 -0
  119. package/agent-spine-plugin/src/lib/hook-input.js +27 -0
  120. package/agent-spine-plugin/src/lib/hook-output.js +140 -0
  121. package/agent-spine-plugin/src/lib/hook-premortem.js +257 -0
  122. package/agent-spine-plugin/src/lib/hook-process-advisory.js +37 -0
  123. package/agent-spine-plugin/src/lib/hook-protection.js +95 -0
  124. package/agent-spine-plugin/src/lib/hook-stop-verification.js +84 -0
  125. package/agent-spine-plugin/src/lib/hook-timeline.js +91 -0
  126. package/agent-spine-plugin/src/lib/identifier-analysis.js +446 -0
  127. package/agent-spine-plugin/src/lib/indexed-memory.js +23 -6
  128. package/agent-spine-plugin/src/lib/knowledge-evidence.js +431 -0
  129. package/agent-spine-plugin/src/lib/learning-applications.js +441 -0
  130. package/agent-spine-plugin/src/lib/learning-candidates.js +274 -0
  131. package/agent-spine-plugin/src/lib/learning-context.js +130 -0
  132. package/agent-spine-plugin/src/lib/learning-delivery-contracts.js +310 -0
  133. package/agent-spine-plugin/src/lib/learning-evaluation-contracts.js +350 -0
  134. package/agent-spine-plugin/src/lib/learning-evaluation-registration.js +372 -0
  135. package/agent-spine-plugin/src/lib/learning-evaluation-revocation.js +292 -0
  136. package/agent-spine-plugin/src/lib/learning-evidence-contracts.js +321 -0
  137. package/agent-spine-plugin/src/lib/learning-findings.js +458 -0
  138. package/agent-spine-plugin/src/lib/learning-measurement-contracts.js +285 -0
  139. package/agent-spine-plugin/src/lib/learning-measurements.js +265 -0
  140. package/agent-spine-plugin/src/lib/learning-outcome-contracts.js +165 -0
  141. package/agent-spine-plugin/src/lib/learning-outcomes.js +343 -0
  142. package/agent-spine-plugin/src/lib/learning-reconciliation.js +290 -0
  143. package/agent-spine-plugin/src/lib/learning-retry-contracts.js +188 -0
  144. package/agent-spine-plugin/src/lib/learning-schema.js +229 -0
  145. package/agent-spine-plugin/src/lib/learning-scope-targets.js +424 -0
  146. package/agent-spine-plugin/src/lib/learning-state-upgrade.js +477 -0
  147. package/agent-spine-plugin/src/lib/learning-status-configuration.js +476 -0
  148. package/agent-spine-plugin/src/lib/learning-storage.js +203 -0
  149. package/agent-spine-plugin/src/lib/learning-trial-recovery.js +218 -0
  150. package/agent-spine-plugin/src/lib/learning-validation-contracts.js +375 -0
  151. package/agent-spine-plugin/src/lib/learning-validation-renewal.js +298 -0
  152. package/agent-spine-plugin/src/lib/learning-validation-runtime.js +234 -0
  153. package/agent-spine-plugin/src/lib/learning.js +36 -6923
  154. package/agent-spine-plugin/src/lib/lesson-recall-session.js +172 -0
  155. package/agent-spine-plugin/src/lib/mcp-autonomy-tools.js +37 -0
  156. package/agent-spine-plugin/src/lib/mcp-delivery-completion.js +124 -0
  157. package/agent-spine-plugin/src/lib/mcp-delivery-tools.js +45 -0
  158. package/agent-spine-plugin/src/lib/mcp-premortem.js +68 -0
  159. package/agent-spine-plugin/src/lib/mcp-runtime.js +269 -0
  160. package/agent-spine-plugin/src/lib/mcp-source-context.js +51 -0
  161. package/agent-spine-plugin/src/lib/mcp-timeline-tools.js +164 -0
  162. package/agent-spine-plugin/src/lib/mcp-world-tools.js +52 -0
  163. package/agent-spine-plugin/src/lib/owned-file-lock.js +30 -10
  164. package/agent-spine-plugin/src/lib/project-portfolio.js +176 -0
  165. package/agent-spine-plugin/src/lib/selfstarter-core.js +382 -0
  166. package/agent-spine-plugin/src/lib/selfstarter-jobs.js +149 -0
  167. package/agent-spine-plugin/src/lib/selfstarter-lease.js +204 -0
  168. package/agent-spine-plugin/src/lib/selfstarter-policy.js +90 -0
  169. package/agent-spine-plugin/src/lib/selfstarter-workspace.js +111 -0
  170. package/agent-spine-plugin/src/lib/selfstarter.js +11 -911
  171. package/agent-spine-plugin/src/lib/session-timeline-auth.js +316 -0
  172. package/agent-spine-plugin/src/lib/session-timeline-codex.js +58 -0
  173. package/agent-spine-plugin/src/lib/session-timeline-contract.js +48 -0
  174. package/agent-spine-plugin/src/lib/session-timeline-enrollment-source.js +41 -0
  175. package/agent-spine-plugin/src/lib/session-timeline-enrollment-storage.js +132 -0
  176. package/agent-spine-plugin/src/lib/session-timeline-enrollment-transport.js +17 -0
  177. package/agent-spine-plugin/src/lib/session-timeline-enrollment.js +500 -0
  178. package/agent-spine-plugin/src/lib/session-timeline-event-extract.js +157 -0
  179. package/agent-spine-plugin/src/lib/session-timeline-host-origin.js +74 -0
  180. package/agent-spine-plugin/src/lib/session-timeline-host-receipt.js +117 -0
  181. package/agent-spine-plugin/src/lib/session-timeline-invocation.js +201 -0
  182. package/agent-spine-plugin/src/lib/session-timeline-king.js +79 -0
  183. package/agent-spine-plugin/src/lib/session-timeline-prior.js +59 -0
  184. package/agent-spine-plugin/src/lib/session-timeline-provider.js +34 -0
  185. package/agent-spine-plugin/src/lib/session-timeline-query.js +55 -0
  186. package/agent-spine-plugin/src/lib/session-timeline-results.js +31 -0
  187. package/agent-spine-plugin/src/lib/session-timeline-root.js +11 -0
  188. package/agent-spine-plugin/src/lib/session-timeline-search.js +82 -0
  189. package/agent-spine-plugin/src/lib/session-timeline-sid-acl.js +217 -0
  190. package/agent-spine-plugin/src/lib/session-timeline-source.js +83 -0
  191. package/agent-spine-plugin/src/lib/session-timeline-state.js +45 -0
  192. package/agent-spine-plugin/src/lib/session-timeline-transport.js +50 -0
  193. package/agent-spine-plugin/src/lib/session-timeline-windows-acl.js +148 -0
  194. package/agent-spine-plugin/src/lib/session-timeline.js +453 -0
  195. package/agent-spine-plugin/src/lib/source-roots.js +69 -136
  196. package/agent-spine-plugin/src/lib/source-tree-scan.js +178 -0
  197. package/agent-spine-plugin/src/lib/task-knowledge-context.js +78 -0
  198. package/agent-spine-plugin/src/lib/timeline-tool-guard.js +202 -0
  199. package/agent-spine-plugin/src/lib/world-knowledge.js +249 -0
  200. package/agent-spine-plugin/src/lib/world-model.js +278 -0
  201. package/agent-spine-plugin/src/mcp.js +20 -161
  202. package/agent-spine-plugin/src/version.js +1 -1
  203. package/agent-spine-plugin/src/worker.js +22 -5
  204. package/bin/agent-resume-snapshot.cjs +2 -2
  205. package/bin/agentspine-king-goal-inbox.mjs +111 -0
  206. package/bin/agentspine-king-goal-intake.mjs +106 -0
  207. package/bin/baseline-skill-performance-policy.cjs +1 -16
  208. package/bin/core-bootstrap.js +2 -0
  209. package/bin/curiosity-scout-policy.cjs +5 -1
  210. package/bin/input-draft-persistence.cjs +2 -2
  211. package/bin/king-tui-function-contract.json +33 -0
  212. package/bin/launcher-restart-policy.cjs +150 -0
  213. package/bin/launcher-runtime.js +56 -44
  214. package/bin/managed-context-startup-policy.cjs +27 -0
  215. package/bin/managed-plugin-selection.cjs +116 -0
  216. package/bin/mistake-relevance-policy.cjs +1 -1
  217. package/bin/observer-hooks.cjs +14 -0
  218. package/bin/oversized-context-offload-policy.cjs +86 -0
  219. package/bin/plugin-bootstrap.js +7 -40
  220. package/bin/proactive-compaction-policy.cjs +1 -1
  221. package/bin/provider-model-refresh-deadline.cjs +53 -0
  222. package/bin/provider-model-refresh-policy.cjs +107 -0
  223. package/bin/release-artifact-freeze-policy.cjs +30 -0
  224. package/bin/repeated-user-message-projection.cjs +3 -126
  225. package/bin/research-page-result.cjs +74 -0
  226. package/bin/runtime-exit-ledger.cjs +1 -0
  227. package/bin/session-compaction-policy.cjs +84 -0
  228. package/bin/skill-listing-performance-policy.cjs +2 -2
  229. package/bin/standard-tools-bootstrap.js +0 -37
  230. package/bin/subagent-skill-policy.cjs +3 -1
  231. package/bin/telegram-approval-relay.cjs +12 -7
  232. package/bin/telegram-private-conversation-policy.cjs +3 -2
  233. package/bin/telegram-queue-handoff-policy.cjs +24 -0
  234. package/bin/thinking-activity-status-policy.cjs +1 -1
  235. package/bin/thinking-only-guard.cjs +16 -12
  236. package/bin/tool-call-loop-policy.cjs +0 -2
  237. package/bin/tool-result-offload-policy.cjs +11 -2
  238. package/bin/tui-functional-contract.cjs +55 -0
  239. package/bin/update-notice.js +18 -14
  240. package/bin/user-message-offload-policy.cjs +1 -1
  241. package/bin/user-prompt-hook-origin-policy.cjs +34 -0
  242. package/bin/windows-node-crash-dump.cjs +110 -0
  243. package/blun.mjs +2517 -1048
  244. package/codebase-index/codebase_index.py +4 -3
  245. package/package.json +3 -17
  246. package/standard-skills/research-evidence/SKILL.md +39 -0
  247. package/standard-skills/research-evidence/references/evidence-format.md +82 -0
  248. package/standard-skills/research-evidence/scripts/evidence-collection.cjs +254 -0
  249. package/standard-skills/research-evidence/scripts/score-report.cjs +112 -0
  250. package/standard-skills/web-lesen/SKILL.md +37 -22
  251. package/standard-skills/web-lesen/scripts/crawl_public.py +376 -0
  252. package/telegram-plugin/DELIVERY.md +36 -0
  253. package/telegram-plugin/bin/telegram-approval-relay.cjs +13 -7
  254. package/telegram-plugin/bin/telegram-launcher-status-queue.cjs +122 -0
  255. package/telegram-plugin/bin/telegram-private-conversation-policy.cjs +3 -2
  256. package/telegram-plugin/bin/telegram-reply-parts.cjs +149 -0
  257. package/telegram-plugin/dist/bridge.mjs +11 -57
  258. package/telegram-plugin/dist/mcp-server.mjs +33 -4
  259. package/agent-spine-plugin/skill/SKILL.md +0 -76
  260. package/bin/mnemo-connect-heartbeat.cjs +0 -204
  261. package/bin/mnemo-tool-agent-policy.cjs +0 -22
  262. package/telegram-plugin/bin/telegram-mnemo-capture.cjs +0 -297
@@ -0,0 +1,493 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, readdir, rmdir, unlink, writeFile } from "node:fs/promises";
3
+ import { dirname, join } from "node:path";
4
+ import { replaceFileWithRetry } from "./filesystem-retry.js";
5
+ import { withOwnedFileLock } from "./owned-file-lock.js";
6
+ import { premortemMismatchError as mismatchError } from "./delivery-premortem-codec.js";
7
+
8
+ const SCHEMA = "agentspine.delivery-premortem-index/v1";
9
+ const SCOPE_SCHEMA = "agentspine.delivery-premortem-index-scope/v1";
10
+ const FINALIZATION_SCHEMA = "agentspine.delivery-premortem-index-finalization/v1";
11
+ const AUTHORITY = "context-only";
12
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
13
+ const MAX_POINTER_BYTES = 8 * 1024;
14
+ const MAX_LANES_PER_QUEUE = 64;
15
+ const MAX_AUDIT_SCOPES = 256;
16
+ const MAX_AUDIT_POINTERS = 256;
17
+ const KEYS = ["authority", "digest", "gatewayAttempt", "goalId", "goalStepId", "laneDigest",
18
+ "planDefinitionsDigest", "queueId", "schema", "scopeDigest", "sessionDigest"];
19
+ const FINALIZATION_KEYS = ["attachmentDigest", "authority", "bindingSummaryDigest", "digest",
20
+ "entityId", "gatewayAttempt", "goalId", "goalStepId", "groupId", "host", "laneDigest",
21
+ "planDefinitionsDigest", "pointerDigest", "projectId", "queueId", "schema", "scopeDigest", "status", "taskId"];
22
+
23
+ function canonical(value) {
24
+ if (value === null || typeof value !== "object") return JSON.stringify(value);
25
+ if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
26
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
27
+ }
28
+
29
+ function sha256(value) {
30
+ return createHash("sha256").update(typeof value === "string" ? value : canonical(value)).digest("hex");
31
+ }
32
+
33
+ function scopeMaterial(goalId, goalStepId, queueId, gatewayAttempt) {
34
+ return { schema: SCOPE_SCHEMA, goalId, goalStepId, queueId, gatewayAttempt };
35
+ }
36
+
37
+ function scopeDigest(goalId, goalStepId, queueId, gatewayAttempt) {
38
+ return sha256(scopeMaterial(goalId, goalStepId, queueId, gatewayAttempt));
39
+ }
40
+
41
+ export function premortemScopeDigest(goalId, goalStepId, queueId, gatewayAttempt) {
42
+ return scopeDigest(goalId, goalStepId, queueId, gatewayAttempt);
43
+ }
44
+
45
+ function indexRootForState(statePath) {
46
+ return join(dirname(dirname(statePath)), "delivery-premortem-index");
47
+ }
48
+
49
+ function finalizedError(message) {
50
+ const error = new Error(message);
51
+ error.code = "AGENTSPINE_PREMORTEM_FINALIZED";
52
+ return error;
53
+ }
54
+
55
+ function pointerFor(state) {
56
+ const { binding } = state;
57
+ const pointer = {
58
+ schema: SCHEMA,
59
+ scopeDigest: scopeDigest(binding.goalId, binding.goalStepId, binding.queueId,
60
+ binding.gatewayAttempt),
61
+ laneDigest: state.laneDigest,
62
+ sessionDigest: sha256(binding.sessionId),
63
+ goalId: binding.goalId,
64
+ goalStepId: binding.goalStepId,
65
+ queueId: binding.queueId,
66
+ gatewayAttempt: binding.gatewayAttempt,
67
+ planDefinitionsDigest: binding.planDefinitionsDigest,
68
+ authority: AUTHORITY
69
+ };
70
+ return { ...pointer, digest: sha256(pointer) };
71
+ }
72
+
73
+ function validId(value) {
74
+ return typeof value === "string" && value.length > 0 && value.length <= 512 && !/[\0\r\n]/.test(value);
75
+ }
76
+
77
+ function validPointer(value, expectedScope = null, expectedLane = null) {
78
+ if (!value || typeof value !== "object" || Array.isArray(value)
79
+ || canonical(Object.keys(value).sort()) !== canonical(KEYS)
80
+ || value.schema !== SCHEMA || value.authority !== AUTHORITY
81
+ || !DIGEST_RE.test(value.digest || "") || !DIGEST_RE.test(value.scopeDigest || "")
82
+ || !DIGEST_RE.test(value.laneDigest || "") || !DIGEST_RE.test(value.sessionDigest || "")
83
+ || !validId(value.goalId) || !validId(value.goalStepId)
84
+ || (value.queueId !== null && !validId(value.queueId))
85
+ || (value.gatewayAttempt !== null
86
+ && (!Number.isInteger(value.gatewayAttempt) || value.gatewayAttempt < 1 || value.queueId === null))
87
+ || (value.planDefinitionsDigest !== null && !DIGEST_RE.test(value.planDefinitionsDigest || ""))) return false;
88
+ const material = { ...value };
89
+ delete material.digest;
90
+ return value.digest === sha256(material)
91
+ && value.scopeDigest === scopeDigest(value.goalId, value.goalStepId, value.queueId,
92
+ value.gatewayAttempt)
93
+ && (expectedScope === null || value.scopeDigest === expectedScope)
94
+ && (expectedLane === null || value.laneDigest === expectedLane);
95
+ }
96
+
97
+ async function readPointer(path, expectedScope, expectedLane) {
98
+ const text = await readFile(path, "utf8");
99
+ if (Buffer.byteLength(text) > MAX_POINTER_BYTES) throw mismatchError("premortem index pointer exceeds 8 KiB");
100
+ let value;
101
+ try {
102
+ value = JSON.parse(text);
103
+ } catch {
104
+ throw new Error("premortem index pointer is not valid JSON");
105
+ }
106
+ if (!validPointer(value, expectedScope, expectedLane)) {
107
+ throw mismatchError("premortem index pointer failed integrity validation");
108
+ }
109
+ return value;
110
+ }
111
+
112
+ async function savePointer(path, pointer, assertOwned) {
113
+ const text = `${JSON.stringify(pointer, null, 2)}\n`;
114
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
115
+ try {
116
+ await writeFile(temporary, text, { mode: 0o600 });
117
+ await replaceFileWithRetry(temporary, path, { beforeAttempt: assertOwned });
118
+ await assertOwned();
119
+ } finally {
120
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; });
121
+ }
122
+ }
123
+
124
+ function finalizationFor(pointer, attachmentDigest, status, context, summaryDigests) {
125
+ const value = { schema: FINALIZATION_SCHEMA, scopeDigest: pointer.scopeDigest,
126
+ status, goalId: context.goalId, goalStepId: context.goalStepId, queueId: context.queueId,
127
+ gatewayAttempt: context.gatewayAttempt, planDefinitionsDigest: context.planDefinitionsDigest,
128
+ host: context.host, projectId: context.projectId, entityId: context.entityId ?? null,
129
+ groupId: context.groupId ?? null, taskId: context.taskId ?? null,
130
+ bindingSummaryDigest: sha256([...summaryDigests].sort()),
131
+ laneDigest: pointer.laneDigest, pointerDigest: pointer.digest, attachmentDigest,
132
+ authority: AUTHORITY };
133
+ return { ...value, digest: sha256(value) };
134
+ }
135
+
136
+ function readOnlyFinalizationFor(expectedScope, attachmentDigest, context, summaryDigests) {
137
+ return emptyScopeFinalizationFor(expectedScope, attachmentDigest, "read-only", context, summaryDigests);
138
+ }
139
+
140
+ function fencedFinalizationFor(expectedScope, context, summaryDigests) {
141
+ const attachmentDigest = sha256({ schema: FINALIZATION_SCHEMA, scopeDigest: expectedScope,
142
+ status: "fenced" });
143
+ return emptyScopeFinalizationFor(expectedScope, attachmentDigest, "fenced", context, summaryDigests);
144
+ }
145
+
146
+ function emptyScopeFinalizationFor(expectedScope, attachmentDigest, status, context, summaryDigests) {
147
+ const value = { schema: FINALIZATION_SCHEMA, scopeDigest: expectedScope,
148
+ status, goalId: context.goalId, goalStepId: context.goalStepId,
149
+ queueId: context.queueId, gatewayAttempt: context.gatewayAttempt,
150
+ planDefinitionsDigest: context.planDefinitionsDigest, host: context.host,
151
+ projectId: context.projectId, entityId: context.entityId ?? null, groupId: context.groupId ?? null,
152
+ taskId: null,
153
+ bindingSummaryDigest: sha256([...summaryDigests].sort()),
154
+ laneDigest: null, pointerDigest: null, attachmentDigest,
155
+ authority: AUTHORITY };
156
+ return { ...value, digest: sha256(value) };
157
+ }
158
+
159
+ function validFinalization(value, expectedScope = null) {
160
+ if (!value || typeof value !== "object" || Array.isArray(value)
161
+ || canonical(Object.keys(value).sort()) !== canonical(FINALIZATION_KEYS)
162
+ || value.schema !== FINALIZATION_SCHEMA || value.authority !== AUTHORITY
163
+ || !new Set(["closed", "read-only", "fenced"]).has(value.status)
164
+ || ![value.scopeDigest, value.planDefinitionsDigest, value.bindingSummaryDigest,
165
+ value.attachmentDigest, value.digest]
166
+ .every((digest) => DIGEST_RE.test(digest || ""))
167
+ || !validId(value.goalId) || !validId(value.goalStepId) || !validId(value.queueId)
168
+ || !Number.isInteger(value.gatewayAttempt) || value.gatewayAttempt < 1
169
+ || !validId(value.host) || !validId(value.projectId)
170
+ || (value.entityId !== null && !validId(value.entityId))
171
+ || (value.groupId !== null && !validId(value.groupId))
172
+ || (value.taskId !== null && !validId(value.taskId))
173
+ || value.scopeDigest !== scopeDigest(value.goalId, value.goalStepId,
174
+ value.queueId, value.gatewayAttempt)
175
+ || (value.status === "closed"
176
+ ? ![value.laneDigest, value.pointerDigest].every((digest) => DIGEST_RE.test(digest || ""))
177
+ : value.laneDigest !== null || value.pointerDigest !== null)
178
+ || (expectedScope !== null && value.scopeDigest !== expectedScope)) return false;
179
+ const material = { ...value };
180
+ delete material.digest;
181
+ return value.digest === sha256(material);
182
+ }
183
+
184
+ export function validPremortemScopeFinalizationContext(value, context) {
185
+ if (!context) return false;
186
+ const fields = ["goalId", "goalStepId", "queueId", "gatewayAttempt",
187
+ "planDefinitionsDigest", "host", "projectId"];
188
+ return validFinalization(value, scopeDigest(context.goalId, context.goalStepId,
189
+ context.queueId, context.gatewayAttempt))
190
+ && fields.every((field) => canonical(value[field]) === canonical(context[field]))
191
+ && value.entityId === (context.entityId ?? null) && value.groupId === (context.groupId ?? null)
192
+ && value.taskId === (context.taskId ?? null);
193
+ }
194
+
195
+ export function validPremortemScopeFinalization(value, context, bindingSummaryDigests = []) {
196
+ return validPremortemScopeFinalizationContext(value, context)
197
+ && Array.isArray(bindingSummaryDigests)
198
+ && bindingSummaryDigests.every((digest) => DIGEST_RE.test(digest || ""))
199
+ && value.bindingSummaryDigest === sha256([...bindingSummaryDigests].sort());
200
+ }
201
+
202
+ async function readFinalization(path, expectedScope) {
203
+ try {
204
+ const text = await readFile(path, "utf8");
205
+ if (Buffer.byteLength(text) > MAX_POINTER_BYTES) {
206
+ throw mismatchError("premortem scope finalization exceeds 8 KiB");
207
+ }
208
+ const value = JSON.parse(text);
209
+ if (!validFinalization(value, expectedScope)) {
210
+ throw mismatchError("premortem scope finalization failed integrity validation");
211
+ }
212
+ return value;
213
+ } catch (error) {
214
+ if (error.code === "ENOENT") return null;
215
+ throw error;
216
+ }
217
+ }
218
+
219
+ export async function registerPremortemLaneIndex({ statePath, state, commit = null, scopeCheck = null }) {
220
+ if (!state?.binding?.goalId || !state.binding.goalStepId) {
221
+ await commit?.();
222
+ return { status: "not-applicable" };
223
+ }
224
+ const pointer = pointerFor(state);
225
+ if (!validPointer(pointer, pointer.scopeDigest, pointer.laneDigest)) {
226
+ throw mismatchError("premortem index pointer failed integrity validation");
227
+ }
228
+ const indexRoot = indexRootForState(statePath);
229
+ const directory = join(indexRoot, pointer.scopeDigest);
230
+ await mkdir(indexRoot, { recursive: true, mode: 0o700 });
231
+ const path = join(directory, `${pointer.laneDigest}.json`);
232
+ const scopeLock = join(indexRoot, `${pointer.scopeDigest}.lock`);
233
+ await withOwnedFileLock(scopeLock, async ({ assertOwned: assertScopeOwned }) => {
234
+ await assertScopeOwned();
235
+ await mkdir(directory, { recursive: true, mode: 0o700 });
236
+ const finalizationPath = join(directory, "finalized.json");
237
+ if (await readFinalization(finalizationPath, pointer.scopeDigest)) {
238
+ throw finalizedError("premortem goal-step attempt was already finalized");
239
+ }
240
+ await scopeCheck?.();
241
+ await withOwnedFileLock(`${path}.lock`, async ({ assertOwned: assertPointerOwned }) => {
242
+ await assertScopeOwned();
243
+ await assertPointerOwned();
244
+ if (await readFinalization(finalizationPath, pointer.scopeDigest)) {
245
+ throw finalizedError("premortem goal-step attempt was already finalized");
246
+ }
247
+ let previous = null;
248
+ try {
249
+ previous = await readPointer(path, pointer.scopeDigest, pointer.laneDigest);
250
+ } catch (error) {
251
+ if (error.code !== "ENOENT") throw error;
252
+ }
253
+ if (previous && previous.digest !== pointer.digest) {
254
+ throw mismatchError("premortem index pointer conflicts with its lane");
255
+ }
256
+ const assertBothOwned = async () => {
257
+ await assertScopeOwned();
258
+ await assertPointerOwned();
259
+ };
260
+ if (!previous) await savePointer(path, pointer, assertBothOwned);
261
+ await commit?.({ assertOwned: assertBothOwned });
262
+ });
263
+ });
264
+ return { status: "indexed", pointer, path };
265
+ }
266
+
267
+ export async function finalizePremortemLaneIndex({ stateDirectory, goalId, goalStepId,
268
+ queueId = null, gatewayAttempt = null, laneDigest, attachmentDigest, context,
269
+ bindingSummaryDigests = [], commit = null }) {
270
+ return finalizePremortemScope({ stateDirectory, goalId, goalStepId, queueId,
271
+ gatewayAttempt, laneDigest, attachmentDigest, status: "closed", context,
272
+ bindingSummaryDigests, commit });
273
+ }
274
+
275
+ export async function finalizePremortemScope({ stateDirectory, goalId, goalStepId,
276
+ queueId = null, gatewayAttempt = null, laneDigest = null, attachmentDigest,
277
+ status, context, bindingSummaryDigests = [], commit = null, verifyEmpty = null }) {
278
+ const expected = scopeDigest(goalId, goalStepId, queueId, gatewayAttempt);
279
+ const indexRoot = join(dirname(stateDirectory), "delivery-premortem-index");
280
+ const directory = join(indexRoot, expected);
281
+ if (!context || canonical({ goalId, goalStepId, queueId, gatewayAttempt })
282
+ !== canonical({ goalId: context.goalId, goalStepId: context.goalStepId,
283
+ queueId: context.queueId, gatewayAttempt: context.gatewayAttempt })
284
+ || !Array.isArray(bindingSummaryDigests) || bindingSummaryDigests.length > MAX_LANES_PER_QUEUE
285
+ || bindingSummaryDigests.some((digest) => !DIGEST_RE.test(digest || ""))) {
286
+ throw mismatchError("premortem finalization context is invalid");
287
+ }
288
+ await mkdir(indexRoot, { recursive: true, mode: 0o700 });
289
+ return withOwnedFileLock(join(indexRoot, `${expected}.lock`), async ({ assertOwned, recovered }) => {
290
+ await assertOwned();
291
+ await mkdir(directory, { recursive: true, mode: 0o700 });
292
+ const path = join(directory, "finalized.json");
293
+ const previous = await readFinalization(path, expected);
294
+ if (previous?.status === "fenced") return { status: "mismatch", blocked: true,
295
+ reason: "Premortem scope was fenced after stale lock recovery.", finalization: previous };
296
+ if (!previous && recovered) {
297
+ const fenced = fencedFinalizationFor(expected, context, bindingSummaryDigests);
298
+ if (!validPremortemScopeFinalization(fenced, context, bindingSummaryDigests)) {
299
+ throw mismatchError("premortem scope fence evidence is invalid");
300
+ }
301
+ await assertOwned();
302
+ await savePointer(path, fenced, assertOwned);
303
+ return { status: "mismatch", blocked: true,
304
+ reason: "Premortem scope was fenced after stale lock recovery.", finalization: fenced };
305
+ }
306
+ const indexed = await lookupPremortemLaneIndex({ stateDirectory, goalId, goalStepId,
307
+ queueId, gatewayAttempt });
308
+ if (indexed.blocked || indexed.status === "degraded") return indexed;
309
+ const closed = status === "closed";
310
+ if (!new Set(["closed", "read-only"]).has(status)
311
+ || (closed && (indexed.pointers.length !== 1 || indexed.pointers[0].laneDigest !== laneDigest))
312
+ || (!closed && indexed.pointers.length !== 0)) {
313
+ return { status: "conflict", blocked: true,
314
+ reason: "Premortem goal-step attempt changed during finalization." };
315
+ }
316
+ if (!closed && verifyEmpty) {
317
+ const verified = await verifyEmpty();
318
+ if (!verified || verified.status !== "empty") return verified || { status: "degraded", blocked: false,
319
+ reason: "Premortem read-only orphan scan was inconclusive." };
320
+ }
321
+ const value = closed
322
+ ? finalizationFor(indexed.pointers[0], attachmentDigest, status, context, bindingSummaryDigests)
323
+ : readOnlyFinalizationFor(expected, attachmentDigest, context, bindingSummaryDigests);
324
+ if (!validPremortemScopeFinalization(value, context, bindingSummaryDigests)) {
325
+ throw mismatchError("premortem finalization evidence is invalid");
326
+ }
327
+ if (previous) {
328
+ if (previous.digest !== value.digest) {
329
+ throw mismatchError("premortem scope has conflicting finalization evidence");
330
+ }
331
+ return { status: "finalized", blocked: false, finalization: previous };
332
+ }
333
+ await assertOwned();
334
+ await commit?.({ assertOwned });
335
+ await savePointer(path, value, assertOwned);
336
+ return { status: "finalized", blocked: false, finalization: value };
337
+ });
338
+ }
339
+
340
+ export async function removePremortemLaneIndex({ statePath, state, commit = null }) {
341
+ if (!state?.binding?.goalId || !state.binding.goalStepId) {
342
+ await commit?.({ assertOwned: async () => {} });
343
+ return { status: "not-applicable" };
344
+ }
345
+ const pointer = pointerFor(state);
346
+ const indexRoot = indexRootForState(statePath);
347
+ const directory = join(indexRoot, pointer.scopeDigest);
348
+ const path = join(directory, `${pointer.laneDigest}.json`);
349
+ try {
350
+ await mkdir(indexRoot, { recursive: true, mode: 0o700 });
351
+ await withOwnedFileLock(join(indexRoot, `${pointer.scopeDigest}.lock`), async ({ assertOwned: assertScopeOwned }) => {
352
+ await mkdir(directory, { recursive: true, mode: 0o700 });
353
+ const finalizationPath = join(directory, "finalized.json");
354
+ await assertScopeOwned();
355
+ if (await readFinalization(finalizationPath, pointer.scopeDigest)) {
356
+ throw finalizedError("premortem goal-step attempt was already finalized");
357
+ }
358
+ await withOwnedFileLock(`${path}.lock`, async ({ assertOwned: assertPointerOwned }) => {
359
+ await assertScopeOwned();
360
+ await assertPointerOwned();
361
+ if (await readFinalization(finalizationPath, pointer.scopeDigest)) {
362
+ throw finalizedError("premortem goal-step attempt was already finalized");
363
+ }
364
+ try {
365
+ const previous = await readPointer(path, pointer.scopeDigest, pointer.laneDigest);
366
+ if (previous.digest !== pointer.digest) {
367
+ throw mismatchError("premortem index pointer conflicts with its lane");
368
+ }
369
+ await assertScopeOwned();
370
+ await assertPointerOwned();
371
+ await unlink(path);
372
+ } catch (error) {
373
+ if (error.code !== "ENOENT") throw error;
374
+ }
375
+ await commit?.({ assertOwned: async () => {
376
+ await assertScopeOwned();
377
+ await assertPointerOwned();
378
+ } });
379
+ });
380
+ await assertScopeOwned();
381
+ await rmdir(directory).catch((error) => {
382
+ if (!new Set(["ENOENT", "ENOTEMPTY", "EEXIST"]).has(error.code)) throw error;
383
+ });
384
+ });
385
+ return { status: "removed", path };
386
+ } catch (error) {
387
+ const verified = error.code === "AGENTSPINE_PREMORTEM_MISMATCH"
388
+ || error.code === "AGENTSPINE_PREMORTEM_FINALIZED";
389
+ return { status: verified ? (error.code === "AGENTSPINE_PREMORTEM_FINALIZED" ? "finalized" : "mismatch") : "degraded",
390
+ blocked: verified,
391
+ reason: String(error.message).slice(0, 400), path };
392
+ }
393
+ }
394
+
395
+ export async function lookupPremortemLaneIndex({ stateDirectory, goalId, goalStepId,
396
+ queueId = null, gatewayAttempt = null }) {
397
+ const expected = scopeDigest(goalId, goalStepId, queueId, gatewayAttempt);
398
+ const directory = join(dirname(stateDirectory), "delivery-premortem-index", expected);
399
+ let names, finalization;
400
+ try {
401
+ names = (await readdir(directory)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort();
402
+ finalization = await readFinalization(join(directory, "finalized.json"), expected);
403
+ } catch (error) {
404
+ if (error.code === "ENOENT") return { status: "unavailable", pointers: [], directory };
405
+ if (error.code === "AGENTSPINE_PREMORTEM_MISMATCH") return { status: "mismatch", blocked: true,
406
+ reason: String(error.message).slice(0, 400), pointers: [], directory };
407
+ return { status: "degraded", blocked: false,
408
+ reason: String(error.message).slice(0, 400), pointers: [], directory };
409
+ }
410
+ if (names.length > MAX_LANES_PER_QUEUE) return { status: "capacity", blocked: true,
411
+ reason: `premortem queue index exceeds ${MAX_LANES_PER_QUEUE} lanes`, pointers: [], directory };
412
+ try {
413
+ const pointers = [];
414
+ for (const name of names) pointers.push(await readPointer(join(directory, name), expected, name.slice(0, 64)));
415
+ if (finalization) {
416
+ if (finalization.status === "fenced") return { status: "mismatch", blocked: true,
417
+ reason: "premortem scope was fenced after stale lock recovery", pointers: [], directory };
418
+ const validClosed = finalization.status === "closed" && pointers.length === 1
419
+ && finalization.laneDigest === pointers[0].laneDigest
420
+ && finalization.pointerDigest === pointers[0].digest
421
+ && finalization.planDefinitionsDigest === pointers[0].planDefinitionsDigest;
422
+ const validReadOnly = finalization.status === "read-only" && pointers.length === 0;
423
+ if (!(validClosed || validReadOnly)) return { status: "mismatch", blocked: true,
424
+ reason: "premortem scope finalization conflicts with its index", pointers: [], directory };
425
+ return { status: "finalized", blocked: false, pointers, finalization, directory };
426
+ }
427
+ return { status: pointers.length ? "available" : "unavailable", pointers, finalization: null, directory };
428
+ } catch (error) {
429
+ if (error.code === "AGENTSPINE_PREMORTEM_MISMATCH") {
430
+ return { status: "mismatch", blocked: true,
431
+ reason: String(error.message).slice(0, 400), pointers: [], directory };
432
+ }
433
+ return { status: "degraded", blocked: false,
434
+ reason: String(error.message).slice(0, 400), pointers: [], directory };
435
+ }
436
+ }
437
+
438
+ export async function inspectPremortemLaneIndexes(stateDirectory) {
439
+ const directory = join(dirname(stateDirectory), "delivery-premortem-index");
440
+ const result = { directory, directories: [], paths: [], pointers: [], errors: [],
441
+ finalizations: [], tamperedPointers: [], tamperedFinalizations: [], truncations: [] };
442
+ let scopes;
443
+ try {
444
+ scopes = (await readdir(directory, { withFileTypes: true }))
445
+ .filter((entry) => entry.isDirectory() && DIGEST_RE.test(entry.name)).map((entry) => entry.name).sort();
446
+ } catch (error) {
447
+ if (error.code !== "ENOENT") result.errors.push({ path: directory, reason: String(error.message).slice(0, 400) });
448
+ return result;
449
+ }
450
+ if (scopes.length > MAX_AUDIT_SCOPES) result.truncations.push({ path: directory,
451
+ reason: `premortem index exceeds ${MAX_AUDIT_SCOPES} audit scopes` });
452
+ let remaining = MAX_AUDIT_POINTERS;
453
+ for (const scope of scopes.slice(0, MAX_AUDIT_SCOPES)) {
454
+ const scopePath = join(directory, scope);
455
+ result.directories.push(scopePath);
456
+ const finalizationPath = join(scopePath, "finalized.json");
457
+ try {
458
+ const finalization = await readFinalization(finalizationPath, scope);
459
+ if (finalization) {
460
+ result.paths.push(finalizationPath);
461
+ result.finalizations.push(finalization);
462
+ }
463
+ } catch (error) {
464
+ result.paths.push(finalizationPath);
465
+ result.errors.push({ path: finalizationPath, reason: String(error.message).slice(0, 400) });
466
+ if (error.code === "AGENTSPINE_PREMORTEM_MISMATCH") {
467
+ result.tamperedFinalizations.push(finalizationPath);
468
+ }
469
+ }
470
+ let names;
471
+ try {
472
+ names = (await readdir(scopePath)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort();
473
+ } catch (error) {
474
+ result.errors.push({ path: scopePath, reason: String(error.message).slice(0, 400) });
475
+ continue;
476
+ }
477
+ if (names.length > remaining) {
478
+ result.truncations.push({ path: scopePath, reason: `premortem index exceeds ${MAX_AUDIT_POINTERS} audit pointers` });
479
+ }
480
+ for (const name of names.slice(0, remaining)) {
481
+ const path = join(scopePath, name);
482
+ result.paths.push(path);
483
+ try {
484
+ result.pointers.push(await readPointer(path, scope, name.slice(0, 64)));
485
+ } catch (error) {
486
+ result.errors.push({ path, reason: String(error.message).slice(0, 400) });
487
+ if (error.code === "AGENTSPINE_PREMORTEM_MISMATCH") result.tamperedPointers.push(path);
488
+ }
489
+ }
490
+ remaining = Math.max(0, remaining - names.length);
491
+ }
492
+ return result;
493
+ }
@@ -0,0 +1,37 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+
4
+ export async function inspectActiveDeliveryPremortems({ stateDirectory, readState, maxFiles }) {
5
+ const result = { states: [], paths: [], errors: [], truncations: [], directory: stateDirectory };
6
+ try {
7
+ const names = (await readdir(stateDirectory)).filter((name) => /^[a-f0-9]{64}\.json$/.test(name)).sort();
8
+ if (names.length > maxFiles) result.truncations.push({ path: stateDirectory,
9
+ reason: `limited to ${maxFiles} state files` });
10
+ for (const name of names.slice(0, maxFiles)) {
11
+ const path = join(stateDirectory, name);
12
+ try {
13
+ const state = await readState(path, name.slice(0, 64));
14
+ if (!state) throw new Error("delivery premortem state disappeared during inspection");
15
+ result.states.push(structuredClone(state));
16
+ result.paths.push(path);
17
+ } catch (error) {
18
+ result.errors.push({ path, reason: String(error.message).slice(0, 400) });
19
+ }
20
+ }
21
+ } catch (error) {
22
+ result.errors.push({ path: stateDirectory, reason: String(error.message).slice(0, 400) });
23
+ }
24
+ return result;
25
+ }
26
+
27
+ export async function verifyEmptyPremortemGoalScope({ stateDirectory, readState, maxFiles, expected }) {
28
+ const inspected = await inspectActiveDeliveryPremortems({ stateDirectory, readState, maxFiles });
29
+ if (inspected.errors.length || inspected.truncations.length) return { status: "degraded", blocked: false,
30
+ reason: "premortem read-only orphan scan is uncertain" };
31
+ const found = inspected.states.some((state) => state.binding.goalId === expected.goalId
32
+ && state.binding.goalStepId === expected.goalStepId && state.binding.queueId === expected.queueId
33
+ && state.binding.gatewayAttempt === expected.gatewayAttempt);
34
+ return found ? { status: "mismatch", blocked: true,
35
+ reason: "A read-only premortem scope still has active state." }
36
+ : { status: "empty", blocked: false };
37
+ }
@@ -0,0 +1,120 @@
1
+ import { unlink } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { withOwnedFileLock } from "./owned-file-lock.js";
4
+ import { lookupPremortemLaneIndex,
5
+ registerPremortemLaneIndex } from "./delivery-premortem-index.js";
6
+ import { premortemSha256 as sha256 } from "./delivery-premortem-codec.js";
7
+
8
+ const MAX_RECOVERY_SNAPSHOTS = 2;
9
+
10
+ export async function recoverPremortemLaneIndex({ stateDirectory, state, readState }) {
11
+ const statePath = join(stateDirectory, `${state.laneDigest}.json`);
12
+ return withOwnedFileLock(`${statePath}.lock`, async () => {
13
+ const current = await readState(statePath, state.laneDigest);
14
+ if (!current) return { status: "disappeared" };
15
+ return registerPremortemLaneIndex({ statePath, state: current });
16
+ });
17
+ }
18
+
19
+ export async function removePremortemStateFile(path, assertLaneOwned, assertIndexOwned) {
20
+ await assertLaneOwned();
21
+ await assertIndexOwned();
22
+ await unlink(path);
23
+ }
24
+
25
+ async function resolvePointer({ stateDirectory, pointer, readState, expected }) {
26
+ const statePath = join(stateDirectory, `${pointer.laneDigest}.json`);
27
+ return withOwnedFileLock(`${statePath}.lock`, async () => {
28
+ const state = await readState(statePath, pointer.laneDigest);
29
+ if (!state) {
30
+ const refreshed = await lookupPremortemLaneIndex({ stateDirectory, ...expected });
31
+ const retained = refreshed.pointers?.some((item) => item.digest === pointer.digest);
32
+ if (retained) return { status: "mismatch", blocked: true,
33
+ reason: "A premortem index pointer has no state." };
34
+ if (refreshed.blocked || refreshed.status === "degraded") return refreshed;
35
+ return { status: "retry", blocked: false };
36
+ }
37
+ const binding = state.binding;
38
+ if (binding.goalId !== expected.goalId || binding.goalStepId !== expected.goalStepId
39
+ || binding.queueId !== expected.queueId || binding.gatewayAttempt !== expected.gatewayAttempt
40
+ || binding.planDefinitionsDigest !== pointer.planDefinitionsDigest
41
+ || sha256(binding.sessionId) !== pointer.sessionDigest) {
42
+ return { status: "mismatch", blocked: true,
43
+ reason: "The premortem index does not match its state." };
44
+ }
45
+ return { status: "resolved", blocked: false, state };
46
+ });
47
+ }
48
+
49
+ export async function resolvePremortemGoalScope({ stateDirectory, expected,
50
+ inspectStates, readState, removeReadOnlyState }) {
51
+ for (let attempt = 0; attempt < MAX_RECOVERY_SNAPSHOTS; attempt += 1) {
52
+ let indexed = await lookupPremortemLaneIndex({ stateDirectory, ...expected });
53
+ if (indexed.status === "capacity") {
54
+ const scanned = await inspectStates();
55
+ if (scanned.errors.length || scanned.truncations.length) return indexed;
56
+ const exact = scanned.states.filter((state) => state.binding.goalId === expected.goalId
57
+ && state.binding.goalStepId === expected.goalStepId
58
+ && state.binding.queueId === expected.queueId
59
+ && state.binding.gatewayAttempt === expected.gatewayAttempt);
60
+ let removed = false;
61
+ for (const state of exact.filter((item) => !item.firstWrite)) {
62
+ const result = await removeReadOnlyState(state);
63
+ removed ||= result.removed;
64
+ }
65
+ if (!removed) return indexed;
66
+ indexed = await lookupPremortemLaneIndex({ stateDirectory, ...expected });
67
+ }
68
+ if (indexed.blocked || indexed.status === "degraded") return indexed;
69
+ const scanned = await inspectStates();
70
+ if (indexed.status === "finalized") {
71
+ if (scanned.errors.length || scanned.truncations.length) {
72
+ return { status: "mismatch", blocked: true,
73
+ reason: "A finalized premortem scope cannot verify that no stale lane was published." };
74
+ }
75
+ const exact = scanned.states.filter((state) => state.binding.goalId === expected.goalId
76
+ && state.binding.goalStepId === expected.goalStepId
77
+ && state.binding.queueId === expected.queueId
78
+ && state.binding.gatewayAttempt === expected.gatewayAttempt);
79
+ const indexedLanes = new Set(indexed.pointers.map((pointer) => pointer.laneDigest));
80
+ const unexpected = indexed.finalization?.status === "read-only"
81
+ ? exact : exact.filter((state) => !indexedLanes.has(state.laneDigest));
82
+ if (unexpected.length) {
83
+ return { status: "mismatch", blocked: true,
84
+ reason: "A finalized premortem scope has a stale unindexed lane." };
85
+ }
86
+ } else {
87
+ if (scanned.errors.length || scanned.truncations.length) {
88
+ return { status: "degraded", blocked: false,
89
+ reason: "premortem orphan scan is uncertain" };
90
+ }
91
+ const exact = scanned.states.filter((state) => state.binding.goalId === expected.goalId
92
+ && state.binding.goalStepId === expected.goalStepId
93
+ && state.binding.queueId === expected.queueId
94
+ && state.binding.gatewayAttempt === expected.gatewayAttempt);
95
+ const indexedLanes = new Set(indexed.pointers.map((pointer) => pointer.laneDigest));
96
+ const orphaned = exact.filter((state) => !indexedLanes.has(state.laneDigest));
97
+ if (orphaned.length) {
98
+ for (const state of orphaned) {
99
+ await recoverPremortemLaneIndex({ stateDirectory, state, readState });
100
+ }
101
+ continue;
102
+ }
103
+ if (indexed.status === "unavailable") return indexed;
104
+ }
105
+ const states = [];
106
+ let retry = false;
107
+ for (const pointer of indexed.pointers) {
108
+ const resolved = await resolvePointer({ stateDirectory, pointer, readState, expected });
109
+ if (resolved.status === "retry") {
110
+ retry = true;
111
+ break;
112
+ }
113
+ if (resolved.status !== "resolved") return resolved;
114
+ states.push(resolved.state);
115
+ }
116
+ if (!retry) return { ...indexed, states };
117
+ }
118
+ return { status: "degraded", blocked: false,
119
+ reason: "premortem goal scope changed during bounded recovery" };
120
+ }