blun-king-cli 9.1.567 → 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 +7 -56
  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,445 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { replaceFileWithRetry } from "./filesystem-retry.js";
5
+ import { withOwnedFileLock } from "./owned-file-lock.js";
6
+ import { canonicalPath, projectStateDir } from "./paths.js";
7
+ import { commandFromInput, deliveryToolActions, isSuccessMarkerClause } from "./delivery-command-actions.js";
8
+ import { deliveryWriteIdDigest } from "./delivery-premortem-binding.js";
9
+
10
+ export { deliveryToolActions } from "./delivery-command-actions.js";
11
+
12
+ export const DELIVERY_VERIFICATION_SCHEMA = "agentspine.delivery-verification/v1";
13
+ const DIGEST_RE = /^[a-f0-9]{64}$/;
14
+ const MAX_STATE_BYTES = 64 * 1024;
15
+ const MAX_RECENT_EVENTS = 32;
16
+ const MAX_PENDING_WRITE_INTENTS = 32;
17
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
18
+ const TEST_PROOF_GUIDANCE = "AgentSpine saw a test command without verified success. Use structured exit_code 0, "
19
+ + "or append && node -e \"console.log('AGENTSPINE_TEST_OK')\" and keep that marker as the final output line.";
20
+
21
+ function sha256(value) {
22
+ return createHash("sha256").update(value).digest("hex");
23
+ }
24
+
25
+ function stateMaterial(state) {
26
+ return {
27
+ schema: state.schema,
28
+ laneDigest: state.laneDigest,
29
+ revision: state.revision,
30
+ lastWrite: state.lastWrite,
31
+ lastTest: state.lastTest,
32
+ verifiedWriteDigest: state.verifiedWriteDigest,
33
+ pauseEventDigest: state.pauseEventDigest,
34
+ conflict: state.conflict,
35
+ pendingWriteIntents: state.pendingWriteIntents,
36
+ recentEvents: state.recentEvents,
37
+ authority: "verification-state-only"
38
+ };
39
+ }
40
+
41
+ function legacyStateMaterial(state) {
42
+ const material = stateMaterial(state);
43
+ delete material.pendingWriteIntents;
44
+ return material;
45
+ }
46
+
47
+ function seal(state) {
48
+ state.integrityDigest = sha256(JSON.stringify(stateMaterial(state)));
49
+ return state;
50
+ }
51
+
52
+ function emptyState(laneDigest) {
53
+ return seal({
54
+ schema: DELIVERY_VERIFICATION_SCHEMA,
55
+ laneDigest,
56
+ revision: 0,
57
+ lastWrite: null,
58
+ lastTest: null,
59
+ verifiedWriteDigest: null,
60
+ pauseEventDigest: null,
61
+ conflict: null,
62
+ pendingWriteIntents: [],
63
+ recentEvents: [],
64
+ integrityDigest: null,
65
+ authority: "verification-state-only"
66
+ });
67
+ }
68
+
69
+ function validReceipt(receipt) {
70
+ return receipt === null || (receipt && typeof receipt === "object" && !Array.isArray(receipt)
71
+ && DIGEST_RE.test(receipt.eventDigest || "") && typeof receipt.toolName === "string"
72
+ && receipt.toolName.length <= 128 && typeof receipt.at === "string"
73
+ && Number.isFinite(new Date(receipt.at).getTime())
74
+ && (receipt.family === null || typeof receipt.family === "string"));
75
+ }
76
+
77
+ function normalizeState(value, laneDigest) {
78
+ const pendingWriteIntents = value?.pendingWriteIntents ?? [];
79
+ const legacyIntegrity = value?.pendingWriteIntents === undefined
80
+ && value?.integrityDigest === sha256(JSON.stringify(legacyStateMaterial(value)));
81
+ if (!(value && value.schema === DELIVERY_VERIFICATION_SCHEMA && value.laneDigest === laneDigest
82
+ && Number.isInteger(value.revision) && value.revision >= 0
83
+ && validReceipt(value.lastWrite) && validReceipt(value.lastTest)
84
+ && (value.verifiedWriteDigest === null || DIGEST_RE.test(value.verifiedWriteDigest))
85
+ && (value.pauseEventDigest === null || DIGEST_RE.test(value.pauseEventDigest))
86
+ && (value.conflict === null || DIGEST_RE.test(value.conflict))
87
+ && Array.isArray(pendingWriteIntents) && pendingWriteIntents.length <= MAX_PENDING_WRITE_INTENTS
88
+ && pendingWriteIntents.every((item) => validReceipt(item) && DIGEST_RE.test(item.idDigest || "")
89
+ && DIGEST_RE.test(item.intentDigest || ""))
90
+ && Array.isArray(value.recentEvents) && value.recentEvents.length <= MAX_RECENT_EVENTS
91
+ && value.recentEvents.every((item) => DIGEST_RE.test(item?.idDigest || "")
92
+ && DIGEST_RE.test(item?.eventDigest || "")
93
+ && (item.intentDigest === undefined || item.intentDigest === null || DIGEST_RE.test(item.intentDigest)))
94
+ && value.authority === "verification-state-only"
95
+ && DIGEST_RE.test(value.integrityDigest || "")
96
+ && (value.integrityDigest === sha256(JSON.stringify(stateMaterial(value))) || legacyIntegrity))) {
97
+ throw new Error("delivery verification state failed integrity validation");
98
+ }
99
+ return value.pendingWriteIntents === undefined ? seal({ ...value, pendingWriteIntents }) : value;
100
+ }
101
+
102
+ function timestamp(value) {
103
+ const date = new Date(value || Date.now());
104
+ return Number.isFinite(date.getTime()) ? date.toISOString() : new Date().toISOString();
105
+ }
106
+
107
+ function scopeValue(scope, name) {
108
+ const value = scope?.[name];
109
+ return typeof value === "string" && value ? value : "";
110
+ }
111
+
112
+ function exactGatewayAttempt(scope) {
113
+ const value = scope?.gatewayAttempt;
114
+ return Number.isSafeInteger(value) && value > 0 ? String(value) : "";
115
+ }
116
+
117
+ function laneMaterial({ host, sessionId, scope }) {
118
+ const taskBound = scopeValue(scope, "entityId") && scopeValue(scope, "projectId")
119
+ && scopeValue(scope, "currentTaskId");
120
+ if (taskBound) {
121
+ const queueId = scopeValue(scope, "queueId");
122
+ const gatewayAttempt = exactGatewayAttempt(scope);
123
+ if (queueId && gatewayAttempt) {
124
+ return ["delivery", host || "unknown", scopeValue(scope, "entityId"),
125
+ scopeValue(scope, "tenantId"), scopeValue(scope, "groupId"), scopeValue(scope, "projectId"),
126
+ scopeValue(scope, "currentTaskId"), queueId, gatewayAttempt, scopeValue(scope, "goalId"),
127
+ scopeValue(scope, "goalStepId"), scopeValue(scope, "planDefinitionsDigest")].join("\0");
128
+ }
129
+ const values = ["task", host || "unknown", scopeValue(scope, "entityId"),
130
+ scopeValue(scope, "projectId"), scopeValue(scope, "currentTaskId")];
131
+ if (typeof sessionId === "string" && sessionId) values.push(sessionId);
132
+ return values.join("\0");
133
+ }
134
+ return typeof sessionId === "string" && sessionId ? ["session", host || "unknown", sessionId].join("\0") : null;
135
+ }
136
+
137
+ export function deliveryActorSession(input) {
138
+ const hostSession = input?.session_id ?? input?.sessionId;
139
+ if (typeof hostSession !== "string" || !ID_RE.test(hostSession)) return null;
140
+ const turn = input?.turn_id ?? input?.turnId;
141
+ if (turn !== null && turn !== undefined && turn !== ""
142
+ && (typeof turn !== "string" || !ID_RE.test(turn))) throw new Error("turnId is invalid");
143
+ return hostSession;
144
+ }
145
+
146
+ function eventIdentity(input, actions, success) {
147
+ const supplied = input.tool_use_id ?? input.event_id ?? input.hook_event_id;
148
+ const idDigest = sha256(typeof supplied === "string" && supplied
149
+ ? supplied : JSON.stringify([input.timestamp || null, input.tool_name || null, input.tool_input || input.tool_args || null]));
150
+ return {
151
+ idDigest,
152
+ eventDigest: sha256(JSON.stringify([idDigest, input.tool_name || null, actions, Boolean(success)]))
153
+ };
154
+ }
155
+
156
+ function stableToolIdentity(input, actions) {
157
+ const idDigest = deliveryWriteIdDigest(input);
158
+ return {
159
+ idDigest,
160
+ intentDigest: sha256(JSON.stringify([
161
+ idDigest, input.tool_name || null, actions, input.tool_input ?? input.tool_args ?? null
162
+ ]))
163
+ };
164
+ }
165
+
166
+ function resultValues(input) {
167
+ return [input?.tool_result, input?.tool_response, input?.result].filter((value) => value !== undefined);
168
+ }
169
+
170
+ function markerOutputTexts(value) {
171
+ if (typeof value === "string") return [value];
172
+ if (Array.isArray(value)) return value.flatMap(markerOutputTexts);
173
+ if (!value || typeof value !== "object") return [];
174
+ return ["stdout", "output", "text", "content", "stderr"].flatMap((key) => markerOutputTexts(value[key]));
175
+ }
176
+
177
+ function hasBoundSuccessMarker(input) {
178
+ const command = commandFromInput(input);
179
+ const parts = command.split(/\s*&&\s*/);
180
+ if (parts.length < 2 || !isSuccessMarkerClause(parts.at(-1))) return false;
181
+ const output = resultValues(input).flatMap(markerOutputTexts).join("\n").trimEnd();
182
+ return output.split(/\r?\n/).at(-1)?.trim() === "AGENTSPINE_TEST_OK";
183
+ }
184
+
185
+ export function deliverySuccessEvidence(input) {
186
+ if (input?.success === false || input?.is_error === true || input?.tool_error) return false;
187
+ if (resultValues(input).some((value) => value && typeof value === "object"
188
+ && (value.isError === true || value.exit_code > 0 || value.exitCode > 0))) return false;
189
+ // Host transport success means only that the tool invocation returned. A
190
+ // test is verified by its own structured process exit or the bound marker.
191
+ return resultValues(input).some((value) => value && typeof value === "object"
192
+ && (value.exit_code === 0 || value.exitCode === 0))
193
+ || hasBoundSuccessMarker(input);
194
+ }
195
+
196
+ async function statePaths(root, laneDigest) {
197
+ const directory = join(await projectStateDir(await canonicalPath(root)), "delivery-verification");
198
+ await mkdir(directory, { recursive: true, mode: 0o700 });
199
+ const path = join(directory, `${laneDigest}.json`);
200
+ return { path, lockPath: `${path}.lock` };
201
+ }
202
+
203
+ async function readState(path, laneDigest) {
204
+ try {
205
+ const content = await readFile(path, "utf8");
206
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("delivery verification state exceeds 64 KiB");
207
+ return normalizeState(JSON.parse(content), laneDigest);
208
+ } catch (error) {
209
+ if (error.code === "ENOENT") return emptyState(laneDigest);
210
+ if (error instanceof SyntaxError) throw new Error("delivery verification state is not valid JSON");
211
+ throw error;
212
+ }
213
+ }
214
+
215
+ async function saveState(path, state, assertOwned) {
216
+ seal(state);
217
+ const content = `${JSON.stringify(state, null, 2)}\n`;
218
+ if (Buffer.byteLength(content) > MAX_STATE_BYTES) throw new Error("delivery verification state exceeds 64 KiB");
219
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
220
+ try {
221
+ await writeFile(temporary, content, { mode: 0o600 });
222
+ await assertOwned();
223
+ await replaceFileWithRetry(temporary, path);
224
+ } finally {
225
+ await unlink(temporary).catch((error) => { if (error.code !== "ENOENT") throw error; });
226
+ }
227
+ }
228
+
229
+ function receipt(input, eventDigest, family = null) {
230
+ return {
231
+ eventDigest,
232
+ toolName: String(input.tool_name || "unknown").slice(0, 128),
233
+ family,
234
+ at: timestamp(input.timestamp)
235
+ };
236
+ }
237
+
238
+ function pendingIntentReceipt(input, identity) {
239
+ return {
240
+ ...receipt(input, identity.intentDigest),
241
+ idDigest: identity.idDigest,
242
+ intentDigest: identity.intentDigest
243
+ };
244
+ }
245
+
246
+ function unresolvedIntentReason(state) {
247
+ const tools = [...new Set(state.pendingWriteIntents.map((item) => item.toolName))].sort();
248
+ return `AgentSpine cannot accept this delivery: ${state.pendingWriteIntents.length} write intent(s) `
249
+ + `(${tools.join(", ")}) have no auditable PostToolUse result. Run a successful test after the intended mutation. `
250
+ + `If the hook supplies no success field, append && node -e \"console.log('AGENTSPINE_TEST_OK')\"; `
251
+ + `the marker must be the final output line.`;
252
+ }
253
+
254
+ export async function recordDeliveryWriteIntent({ root, host, sessionId, scope, input }) {
255
+ const material = laneMaterial({ host, sessionId, scope });
256
+ const actions = deliveryToolActions(input);
257
+ if (!material || !actions.some((item) => item.kind === "write")) {
258
+ return { status: "not-applicable", blocked: false };
259
+ }
260
+ const identity = stableToolIdentity(input, actions);
261
+ const laneDigest = sha256(material);
262
+ const paths = await statePaths(root, laneDigest);
263
+ try {
264
+ return await withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
265
+ const state = await readState(paths.path, laneDigest);
266
+ const resolved = state.recentEvents.find((item) => item.idDigest === identity.idDigest);
267
+ if (resolved) {
268
+ if (resolved.intentDigest && resolved.intentDigest !== identity.intentDigest) {
269
+ state.conflict = identity.idDigest;
270
+ state.revision += 1;
271
+ await saveState(paths.path, state, assertOwned);
272
+ return { status: "conflict", blocked: true,
273
+ reason: "AgentSpine write tool_use_id was reused with a different mutation." };
274
+ }
275
+ return { status: "already-resolved", blocked: false };
276
+ }
277
+ const pending = state.pendingWriteIntents.find((item) => item.idDigest === identity.idDigest);
278
+ if (pending) {
279
+ if (pending.intentDigest !== identity.intentDigest) {
280
+ state.conflict = identity.idDigest;
281
+ state.revision += 1;
282
+ await saveState(paths.path, state, assertOwned);
283
+ return { status: "conflict", blocked: true,
284
+ reason: "AgentSpine write tool_use_id was reused with a different mutation." };
285
+ }
286
+ return { status: "duplicate-intent", blocked: false };
287
+ }
288
+ if (state.pendingWriteIntents.length >= MAX_PENDING_WRITE_INTENTS) {
289
+ state.conflict = sha256(`pending-write-intent-overflow\0${laneDigest}`);
290
+ state.revision += 1;
291
+ await saveState(paths.path, state, assertOwned);
292
+ return { status: "conflict", blocked: true,
293
+ reason: `AgentSpine cannot track more than ${MAX_PENDING_WRITE_INTENTS} concurrent write intents.` };
294
+ }
295
+ state.pendingWriteIntents.push(pendingIntentReceipt(input, identity));
296
+ // A resumed mutation makes a prior paused Stop event stale. Keep the
297
+ // invalidation in the same locked state transition as the intent so an
298
+ // identical later completion event cannot bypass the pending-test gate.
299
+ state.pauseEventDigest = null;
300
+ state.revision += 1;
301
+ await saveState(paths.path, state, assertOwned);
302
+ return { status: "intent-recorded", blocked: false, pending: true };
303
+ });
304
+ } catch (error) {
305
+ return { status: "degraded", blocked: false, reason: error.message };
306
+ }
307
+ }
308
+
309
+ export async function recordDeliveryToolUse({ root, host, sessionId, scope, input, success }) {
310
+ const material = laneMaterial({ host, sessionId, scope });
311
+ const actions = deliveryToolActions(input);
312
+ if (!material || !actions.length) return { status: "not-applicable", blocked: false };
313
+ const laneDigest = sha256(material);
314
+ const paths = await statePaths(root, laneDigest);
315
+ try {
316
+ return await withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
317
+ const state = await readState(paths.path, laneDigest);
318
+ const identity = eventIdentity(input, actions, success);
319
+ const stableIdentity = stableToolIdentity(input, actions);
320
+ const known = state.recentEvents.find((item) => item.idDigest === identity.idDigest);
321
+ if (known) {
322
+ if (known.eventDigest !== identity.eventDigest) {
323
+ state.conflict = identity.idDigest;
324
+ state.revision += 1;
325
+ await saveState(paths.path, state, assertOwned);
326
+ return { status: "conflict", blocked: false };
327
+ }
328
+ return { status: "duplicate", blocked: false };
329
+ }
330
+ const writes = actions.some((item) => item.kind === "write");
331
+ if (writes) {
332
+ const pending = state.pendingWriteIntents.find((item) => item.idDigest === stableIdentity.idDigest);
333
+ if (pending && pending.intentDigest !== stableIdentity.intentDigest) {
334
+ state.conflict = stableIdentity.idDigest;
335
+ state.revision += 1;
336
+ await saveState(paths.path, state, assertOwned);
337
+ return { status: "conflict", blocked: false };
338
+ }
339
+ state.pendingWriteIntents = state.pendingWriteIntents
340
+ .filter((item) => item.idDigest !== stableIdentity.idDigest);
341
+ }
342
+ for (const action of actions) {
343
+ if (action.kind === "write") {
344
+ state.lastWrite = receipt(input, identity.eventDigest);
345
+ state.pauseEventDigest = null;
346
+ } else if (success) {
347
+ if (state.pendingWriteIntents.length) {
348
+ const intendedWrite = state.pendingWriteIntents.at(-1);
349
+ state.lastWrite = {
350
+ eventDigest: intendedWrite.intentDigest, toolName: intendedWrite.toolName,
351
+ family: null, at: intendedWrite.at
352
+ };
353
+ state.pendingWriteIntents = [];
354
+ state.pauseEventDigest = null;
355
+ }
356
+ state.lastTest = receipt(input, identity.eventDigest, action.family);
357
+ state.verifiedWriteDigest = state.lastWrite?.eventDigest || null;
358
+ } else {
359
+ // A later failed or unverified test cannot inherit an earlier success.
360
+ // Preserve that receipt as history, but require fresh post-write evidence.
361
+ state.verifiedWriteDigest = null;
362
+ }
363
+ }
364
+ state.recentEvents.push({ ...identity, intentDigest: stableIdentity.intentDigest });
365
+ state.recentEvents = state.recentEvents.slice(-MAX_RECENT_EVENTS);
366
+ state.revision += 1;
367
+ await saveState(paths.path, state, assertOwned);
368
+ return {
369
+ status: actions.some((item) => item.kind === "write") ? "write-recorded" : success ? "test-recorded" : "test-failed",
370
+ blocked: false,
371
+ ...(!writes && !success ? { reason: TEST_PROOF_GUIDANCE } : {}),
372
+ pending: Boolean(state.lastWrite && state.verifiedWriteDigest !== state.lastWrite.eventDigest)
373
+ };
374
+ });
375
+ } catch (error) {
376
+ return { status: "degraded", blocked: false, reason: error.message };
377
+ }
378
+ }
379
+
380
+ export async function recordDeliveryPause({ root, host, sessionId, scope, eventId }) {
381
+ const material = laneMaterial({ host, sessionId, scope });
382
+ if (!material) {
383
+ return { status: "paused-job", blocked: false };
384
+ }
385
+ const laneDigest = sha256(material);
386
+ const paths = await statePaths(root, laneDigest);
387
+ try {
388
+ return await withOwnedFileLock(paths.lockPath, async ({ assertOwned }) => {
389
+ const state = await readState(paths.path, laneDigest);
390
+ if (state.pendingWriteIntents.length) {
391
+ return { status: "blocked", blocked: true, reason: unresolvedIntentReason(state) };
392
+ }
393
+ // A pause may deduplicate only this exact Stop event. A session identity is
394
+ // deliberately insufficient because it could bypass a later completion.
395
+ const pauseEventDigest = typeof eventId === "string" && eventId ? sha256(`event\0${eventId}`) : null;
396
+ if (state.pauseEventDigest !== pauseEventDigest) {
397
+ state.pauseEventDigest = pauseEventDigest;
398
+ state.revision += 1;
399
+ await saveState(paths.path, state, assertOwned);
400
+ }
401
+ return { status: "paused-job", blocked: false };
402
+ });
403
+ } catch (error) {
404
+ return { status: "degraded-pause", blocked: false, reason: error.message };
405
+ }
406
+ }
407
+
408
+ export async function verifyDeliveryStop({ root, host, sessionId, scope, eventId = null }) {
409
+ const material = laneMaterial({ host, sessionId, scope });
410
+ if (!material) return { status: "not-applicable", blocked: false };
411
+ const laneDigest = sha256(material);
412
+ const paths = await statePaths(root, laneDigest);
413
+ try {
414
+ return await withOwnedFileLock(paths.lockPath, async () => {
415
+ const state = await readState(paths.path, laneDigest);
416
+ const pauseEventDigest = typeof eventId === "string" && eventId ? sha256(`event\0${eventId}`) : null;
417
+ if (state.conflict) {
418
+ return { status: "blocked", blocked: true,
419
+ reason: "AgentSpine cannot verify tool history because one tool delivery ID had conflicting results." };
420
+ }
421
+ if (state.pendingWriteIntents.length) {
422
+ return { status: "blocked", blocked: true, reason: unresolvedIntentReason(state) };
423
+ }
424
+ if (pauseEventDigest && state.pauseEventDigest === pauseEventDigest) {
425
+ return { status: "paused-job", blocked: false, stateDigest: state.integrityDigest };
426
+ }
427
+ if (state.lastWrite && state.verifiedWriteDigest !== state.lastWrite.eventDigest) {
428
+ return { status: "blocked", blocked: true,
429
+ reason: "AgentSpine cannot accept this delivery: run a successful test after the latest write "
430
+ + `(node --test, npm test, npm run check, or pytest). ${TEST_PROOF_GUIDANCE}` };
431
+ }
432
+ return { status: state.lastWrite ? "verified" : "no-write", blocked: false,
433
+ testFamily: state.lastTest?.family || null, stateDigest: state.integrityDigest };
434
+ });
435
+ } catch (error) {
436
+ return { status: "blocked", blocked: true,
437
+ reason: `AgentSpine cannot verify post-write tests: ${String(error.message).slice(0, 400)}` };
438
+ }
439
+ }
440
+
441
+ export async function deliveryVerificationPath({ root, host, sessionId, scope }) {
442
+ const material = laneMaterial({ host, sessionId, scope });
443
+ if (!material) return null;
444
+ return (await statePaths(root, sha256(material))).path;
445
+ }
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { lstat, opendir, readFile, realpath, stat } from "node:fs/promises";
3
3
  import { basename, dirname, extname, isAbsolute, join, relative, resolve, sep } from "node:path";
4
+ import { recordHookScanAudit } from "./hook-audit.js";
5
+ import { skippableTraversalError } from "./source-tree-scan.js";
4
6
 
5
7
  const SKIP_DIRS = new Set([
6
8
  ".git", ".hg", ".svn", ".next", ".nuxt", ".turbo", ".venv",
@@ -137,16 +139,36 @@ function isInside(parent, child) {
137
139
  return value === "" || (!isAbsolute(value) && !value.startsWith(`..${sep}`) && value !== "..");
138
140
  }
139
141
 
140
- async function walk(directory, found, excludedRoots) {
142
+ async function walk(directory, found, excludedRoots, env) {
143
+ let stream;
144
+ try {
145
+ stream = await opendir(directory);
146
+ } catch (error) {
147
+ if (!skippableTraversalError(error)) throw error;
148
+ await recordHookScanAudit({
149
+ event: "CatalogScan", phase: "document-discovery", error,
150
+ path: error.path || directory, operation: "opendir", env
151
+ });
152
+ return;
153
+ }
141
154
  const entries = [];
142
- for await (const entry of await opendir(directory)) entries.push(entry);
155
+ try {
156
+ for await (const entry of stream) entries.push(entry);
157
+ } catch (error) {
158
+ if (!skippableTraversalError(error)) throw error;
159
+ await recordHookScanAudit({
160
+ event: "CatalogScan", phase: "document-discovery", error,
161
+ path: error.path || directory, operation: "readdir", env
162
+ });
163
+ return;
164
+ }
143
165
  entries.sort((a, b) => a.name.localeCompare(b.name));
144
166
  for (const entry of entries) {
145
167
  if (entry.isSymbolicLink()) continue;
146
168
  const fullPath = join(directory, entry.name);
147
169
  if (entry.isDirectory()) {
148
170
  if (!SKIP_DIRS.has(entry.name) && !excludedRoots.some((root) => isInside(root, fullPath))) {
149
- await walk(fullPath, found, excludedRoots);
171
+ await walk(fullPath, found, excludedRoots, env);
150
172
  }
151
173
  } else if (entry.isFile() && extname(entry.name).toLowerCase() === ".md") {
152
174
  found.push(fullPath);
@@ -154,7 +176,7 @@ async function walk(directory, found, excludedRoots) {
154
176
  }
155
177
  }
156
178
 
157
- export async function discoverDocuments(root, { excludeRoots = [] } = {}) {
179
+ export async function discoverDocuments(root, { excludeRoots = [], env = process.env } = {}) {
158
180
  const files = [];
159
181
  // Canonicalize existing exclusion roots just like the scanned root. On macOS
160
182
  // temporary directories commonly cross /var -> /private/var, while Windows
@@ -167,7 +189,7 @@ export async function discoverDocuments(root, { excludeRoots = [] } = {}) {
167
189
  throw error;
168
190
  }
169
191
  }))).filter((path) => isInside(root, path));
170
- await walk(root, files, excludedRoots);
192
+ await walk(root, files, excludedRoots, env);
171
193
  const documents = new Array(files.length);
172
194
  let cursor = 0;
173
195
  async function worker() {
@@ -16,9 +16,11 @@ export function isTransientLockMetadataError(error, platform = process.platform)
16
16
  export async function replaceFileWithRetry(temporary, target, options = {}) {
17
17
  const renameFile = options.renameFile || rename;
18
18
  const wait = options.wait || delay;
19
+ const beforeAttempt = options.beforeAttempt || null;
19
20
  const platform = options.platform || process.platform;
20
21
  const maxRetries = options.maxRetries ?? 7;
21
22
  for (let attempt = 0; ; attempt += 1) {
23
+ await beforeAttempt?.(attempt);
22
24
  try {
23
25
  await renameFile(temporary, target);
24
26
  return;
@@ -0,0 +1,68 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as goalPremortem from "./gateway-premortem.js";
3
+
4
+ export const GATEWAY_POLICY_SCHEMA = "agentspine.gateway-policy/v2";
5
+ export const GATEWAY_RUNTIME_SCHEMA = "agentspine.gateway-runtime/v1";
6
+ export const GATEWAY_EVENT_SCHEMA = "agentspine.gateway-event/v1";
7
+ export const GOAL_PLAN_SCHEMA = "agentspine.goal-plan/v1";
8
+ export { KNOWLEDGE_GAP_SCHEMA } from "./knowledge-evidence.js";
9
+ export const EXECUTION_OUTCOME_SCHEMA = "agentspine.execution-outcome/v1";
10
+ export const EXECUTION_ATTEMPT_SCHEMA = "agentspine.execution-attempt/v1";
11
+ export const STRATEGY_TRANSFER_PROOF_SCHEMA = "agentspine.strategy-transfer-proof/v1";
12
+
13
+ export const CONFIRMATION = "local-owner-confirmed";
14
+ export const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
15
+ export const ROUTE_RE = /^-?[A-Za-z0-9][A-Za-z0-9:_.@/+~-]{0,255}$/;
16
+ export const GOAL_STATUSES = new Set(["active", "blocked", "completed", "cancelled"]);
17
+ export const PLAN_STEP_STATUSES = new Set(["pending", "active", "blocked", "completed", "cancelled"]);
18
+ export const QUEUE_STATUSES = new Set(["pending", "leased", "awaiting-delivery", "completed", "blocked", "dead-letter", "cancelled"]);
19
+ export const OUTBOX_STATUSES = new Set(["prepared", "sending", "delivered", "failed", "dead-letter", "delivery-unknown", "acknowledged"]);
20
+ export const WAKE_KINDS = new Set(["direct-message", "deadline", "promise", "resolved-blocker", "assignment", "follow-up", "relationship"]);
21
+ export const PRIORITY = { "direct-message": 100, deadline: 90, promise: 90, "resolved-blocker": 80, assignment: 70, "follow-up": 60, relationship: 50 };
22
+ export const SECRET_RE = /-----BEGIN [A-Z ]*PRIVATE KEY-----|\b(?:sk|gh[opusu])_[A-Za-z0-9_-]{20,}\b|\bBearer\s+[A-Za-z0-9._~+/-]{20,}|\b(?:api[-_ ]?key|token|password|secret)\s*[:=]\s*\S{8,}/i;
23
+ export const SECRET_KEY_RE = /"(?:api[-_ ]?key|token|password|secret|credential)"\s*:/i;
24
+ export const AUTHORITY_RE = /\b(?:permission|rights?|roles?|owner|trusted|delegat|authorized|approval|production|payment|spending|tool capability|send capability)\b/i;
25
+ export const HEALTH_VALUES = new Set(["stopped", "running", "unknown", "healthy", "degraded", "failed"]);
26
+ export const METRIC_OPERATORS = new Set(["gte", "lte", "eq"]);
27
+
28
+ export function emptyPolicy(root) {
29
+ return { schema: GATEWAY_POLICY_SCHEMA, root, revision: 0, enabled: false, killSwitch: false, goals: [], history: [],
30
+ premortemContractRegistry: goalPremortem.emptyGoalPremortemRegistry(root) };
31
+ }
32
+
33
+ export function emptyRuntime(root) {
34
+ return { schema: GATEWAY_RUNTIME_SCHEMA, root, revision: 0, queue: [], lanes: [], outbox: [], receipts: [], history: [],
35
+ health: { gateway: "stopped", adapter: "unknown", scheduler: "unknown", queue: "healthy", worker: "unknown", host: "unknown", lastTickAt: null, lastReconciledAt: null } };
36
+ }
37
+
38
+ export function exactId(value, field, nullable = false) {
39
+ if ((value === null || value === undefined || value === "") && nullable) return null;
40
+ if (typeof value !== "string" || !ID_RE.test(value) || value.includes("*")) throw new Error(field + " must be an exact stable ID without wildcards");
41
+ return value;
42
+ }
43
+
44
+ export function safeText(value, field, maximum = 1000) {
45
+ if (typeof value !== "string" || !value.trim()) throw new Error(field + " is required");
46
+ const text = value.trim().replace(/\s+/g, " ").slice(0, maximum);
47
+ if (SECRET_RE.test(text)) throw new Error(field + " appears to contain a secret");
48
+ return text;
49
+ }
50
+
51
+ export function timestamp(value = new Date()) {
52
+ const date = value instanceof Date ? value : new Date(value);
53
+ if (!Number.isFinite(date.getTime())) throw new Error("timestamp is invalid");
54
+ return date.toISOString();
55
+ }
56
+
57
+ export function sha256(value) { return createHash("sha256").update(value).digest("hex"); }
58
+
59
+ export function safeCheckpoint(value) {
60
+ if (value === null || value === undefined) return null;
61
+ let content;
62
+ try { content = JSON.stringify(value); } catch { throw new Error("checkpoint must be JSON serializable"); }
63
+ if (!content || Buffer.byteLength(content) > 16384) throw new Error("checkpoint exceeds 16 KiB");
64
+ if (SECRET_RE.test(content) || SECRET_KEY_RE.test(content) || AUTHORITY_RE.test(content)) {
65
+ throw new Error("checkpoint contains secret- or authority-shaped content");
66
+ }
67
+ return JSON.parse(content);
68
+ }