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
@@ -1,565 +1,49 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash } from "node:crypto";
3
- import { isAbsolute, relative, resolve } from "node:path";
4
- import { recordAttentionEvent } from "./lib/attention.js";
3
+ import { resolve } from "node:path";
5
4
  import { catalogForStateRoot, saveCatalog } from "./lib/catalog.js";
6
5
  import { loadGraph } from "./lib/graph.js";
7
6
  import { canonicalPath } from "./lib/paths.js";
8
7
  import { resolveHostSourceCatalog } from "./lib/source-roots.js";
9
8
  import { sessionBriefing } from "./lib/briefing.js";
10
- import { captureContinuityPrompt, loadContinuity } from "./lib/continuity.js";
9
+ import { captureContinuityPrompt } from "./lib/continuity.js";
11
10
  import { recordLearningApplications, recordLearningDeliveries } from "./lib/learning.js";
12
- import {
13
- authorizeJobEffect, checkpointJobEffect, closeJobLease, resolveSessionJob, startOrResumeJob
14
- } from "./lib/selfstarter.js";
15
- import { claimChannelEvent } from "./lib/channel-runtime.js";
11
+ import { authorizeJobEffect, checkpointJobEffect, closeJobLease } from "./lib/selfstarter.js";
16
12
  import { syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
17
13
  import { captureMustRememberPrompt, recordPreflightFailure, runPreflight, verifyPreflightReceipt } from "./lib/preflight.js";
14
+ import { actionLessonRecall } from "./lib/action-lesson-recall.js";
15
+ import { captureSessionTimelineLifecycle, finalizeUserPromptSessionTimeline } from "./lib/hook-timeline.js";
16
+ import { timelineToolKind } from "./lib/mcp-timeline-tools.js";
17
+ import { emitTimelineToolGuard, runTimelineToolGuard } from "./lib/timeline-tool-guard.js";
18
+ import { readHookInput, SILENT_OVERSIZE_POST_TOOL_USE, SILENT_OVERSIZE_POST_TOOL_USE_ARG } from "./lib/hook-input.js";
18
19
  import { isMainModule } from "./lib/runtime.js";
19
- import { recordHookScanAudit } from "./lib/hook-audit.js";
20
-
21
- const MAX_STDIN_BYTES = 64 * 1024;
22
- const STANDARD_HOST_CONTEXT_BYTES = 9500;
23
- const MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES = 32 * 1024;
20
+ import { deliveryActorSession, deliverySuccessEvidence, recordDeliveryToolUse,
21
+ recordDeliveryWriteIntent } from "./lib/delivery-verification.js";
22
+ import {
23
+ blockPrompt, blockStop, blunRuntimeContext, blunRuntimeMessage, denyTool, hookOutput, lifecycleOutput
24
+ } from "./lib/hook-output.js";
25
+ import {
26
+ ATTENTION_WRITE_EVENTS, boundedId, captureAttentionLifecycle, hookDeliveryId, hostContextLimit, hostFromInput,
27
+ promptFromInput, renderContext, runtimeScope, selfstarterInput, selfstarterRootSkipped,
28
+ selfstarterScope, sessionId, startChannelEvent, startSelfstarter, toolResult, toolSucceeded
29
+ } from "./lib/hook-context.js";
30
+ import {
31
+ auditGuard, auditSkippedScans, candidatePaths, finishScanFailure, hookScanFailureFailsOpen,
32
+ isMutationTool, isScanFailOpenTool, shellTargetsProtected
33
+ } from "./lib/hook-protection.js";
34
+ import {
35
+ captureJavaScriptBeforeWrite, inspectWrittenJavaScript, verifyBaselineBeforeWrite, verifyDeliveredArtifacts
36
+ } from "./lib/hook-artifact-guards.js";
37
+ import { isPremortemWrite, prepareHookPremortem, recordHookPremortemWrite,
38
+ recordHookPremortemWriteIntent, verifyHookPremortemWrite } from "./lib/hook-premortem.js";
39
+ import { verifyHookStopContracts } from "./lib/hook-stop-verification.js";
40
+ import { processAdvisory } from "./lib/hook-process-advisory.js";
41
+ export { blunRuntimeContext, blunRuntimeMessage } from "./lib/hook-output.js";
24
42
  const CONTEXT_EVENTS = new Set(["SessionStart", "UserPromptSubmit", "PreCompact", "PostCompact"]);
25
43
  const KNOWN_EVENTS = new Set([
26
44
  ...CONTEXT_EVENTS, "InstructionsLoaded", "PreToolUse", "PostToolUse", "Stop", "SubagentStop"
27
45
  ]);
28
- const ID_RE = /^[A-Za-z0-9][A-Za-z0-9:_.@/-]{0,127}$/;
29
- const ATTENTION_WRITE_EVENTS = new Set(["UserPromptSubmit", "PostToolUse", "Stop", "SubagentStop"]);
30
- const SELFSTART_EVENTS = new Set(["SessionStart", "PostCompact"]);
31
- const SILENT_OVERSIZE_POST_TOOL_USE = Symbol("silent-oversize-post-tool-use");
32
- const SILENT_OVERSIZE_POST_TOOL_USE_ARG = "--silent-oversize-post-tool-use";
33
-
34
- async function readStdin({ silentOversizePostToolUse = false } = {}) {
35
- const chunks = [];
36
- let bytes = 0;
37
- let oversized = false;
38
- for await (const chunk of process.stdin) {
39
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
40
- const remaining = Math.max(0, MAX_STDIN_BYTES - bytes);
41
- if (remaining) chunks.push(buffer.subarray(0, remaining));
42
- bytes += buffer.length;
43
- if (bytes > MAX_STDIN_BYTES) oversized = true;
44
- }
45
- if (oversized) {
46
- if (silentOversizePostToolUse) return SILENT_OVERSIZE_POST_TOOL_USE;
47
- throw new Error("hook input exceeds the 64 KiB limit");
48
- }
49
- const value = Buffer.concat(chunks, bytes).toString("utf8");
50
- const parsed = value.trim() ? JSON.parse(value) : {};
51
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("hook input must be one JSON object");
52
- return parsed;
53
- }
54
-
55
- function boundedId(value, field) {
56
- if (value === null || value === undefined || value === "") return null;
57
- if (typeof value !== "string" || !ID_RE.test(value)) throw new Error(`${field} is invalid`);
58
- return value;
59
- }
60
-
61
- function promptFromInput(input) {
62
- for (const key of ["prompt", "user_prompt", "message", "input"]) {
63
- const value = input[key];
64
- if (typeof value === "string") return value;
65
- if (Array.isArray(value)) {
66
- const text = value
67
- .filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string")
68
- .map((part) => part.text)
69
- .join("\n")
70
- .trim();
71
- if (text) return text;
72
- }
73
- }
74
- return null;
75
- }
76
-
77
- function hostContextLimit(preflight) {
78
- return preflight?.receipt?.instructionBudget?.mode === "claude-required-overflow"
79
- ? MAX_CLAUDE_OVERFLOW_CONTEXT_BYTES
80
- : STANDARD_HOST_CONTEXT_BYTES;
81
- }
82
-
83
- function hostFromInput(input) {
84
- const explicit = input.host || input.provider || process.env.AGENTSPINE_HOST;
85
- if (["claude", "codex", "generic"].includes(explicit)) return explicit;
86
- if ((typeof input.model === "string" && input.model.trim()) || process.env.PLUGIN_ROOT || process.env.CODEX_HOME
87
- || process.env.BLUN_PLUGIN_ROOT || process.env.BLUN_HOME) return "codex";
88
- return "claude";
89
- }
90
-
91
- function gatewayEnvironmentContext(env = process.env) {
92
- if (env.AGENTSPINE_GATEWAY_CONTEXT !== "agentspine.gateway-start/v1") return null;
93
- return {
94
- entityId: boundedId(env.AGENTSPINE_ENTITY_ID, "AGENTSPINE_ENTITY_ID"),
95
- groupId: boundedId(env.AGENTSPINE_GROUP_ID, "AGENTSPINE_GROUP_ID"),
96
- projectId: boundedId(env.AGENTSPINE_PROJECT_ID, "AGENTSPINE_PROJECT_ID"),
97
- taskId: boundedId(env.AGENTSPINE_TASK_ID, "AGENTSPINE_TASK_ID"),
98
- eventId: boundedId(env.AGENTSPINE_CHANNEL_EVENT_ID, "AGENTSPINE_CHANNEL_EVENT_ID"),
99
- provider: boundedId(env.AGENTSPINE_CHANNEL_PROVIDER, "AGENTSPINE_CHANNEL_PROVIDER")
100
- };
101
- }
102
-
103
- async function runtimeScope(input, root, userStateRoot = null, catalog) {
104
- const { continuity: projectContinuity } = await loadContinuity(root, catalog);
105
- const userCatalog = userStateRoot && userStateRoot !== root
106
- ? catalogForStateRoot(catalog, userStateRoot) : catalog;
107
- const userContinuity = userStateRoot && userStateRoot !== root
108
- ? (await loadContinuity(userStateRoot, userCatalog)).continuity
109
- : projectContinuity;
110
- const continuity = {
111
- config: {
112
- ...projectContinuity.config,
113
- enabled: userContinuity.config.enabled,
114
- minConfidence: userContinuity.config.minConfidence,
115
- minDirectness: userContinuity.config.minDirectness,
116
- minEvidence: userContinuity.config.minEvidence,
117
- maxPromptBytes: userContinuity.config.maxPromptBytes,
118
- maxBriefingBytes: userContinuity.config.maxBriefingBytes,
119
- defaultEntityId: userContinuity.config.defaultEntityId
120
- }
121
- };
122
- const supplied = input.agent_spine_scope && typeof input.agent_spine_scope === "object"
123
- ? input.agent_spine_scope : input;
124
- const gateway = gatewayEnvironmentContext();
125
- return {
126
- entityId: boundedId(supplied.entity_id ?? supplied.entityId ?? gateway?.entityId ?? continuity.config.defaultEntityId, "entityId"),
127
- userId: boundedId(supplied.user_id ?? supplied.userId ?? process.env.AGENTSPINE_USER_ID, "userId"),
128
- tenantId: boundedId(supplied.tenant_id ?? supplied.tenantId ?? process.env.AGENTSPINE_TENANT_ID, "tenantId"),
129
- groupId: boundedId(supplied.group_id ?? supplied.groupId ?? gateway?.groupId, "groupId"),
130
- projectId: boundedId(supplied.project_id ?? supplied.projectId ?? gateway?.projectId ?? continuity.config.defaultProjectId, "projectId"),
131
- currentTaskId: boundedId(supplied.task_id ?? supplied.currentTaskId ?? gateway?.taskId, "currentTaskId"),
132
- host: hostFromInput(input),
133
- config: continuity.config
134
- };
135
- }
136
-
137
- function renderContext(event, catalog, briefing, signal = null, attentionEvent = null, selfstarter = null, channelEvent = null, sourceDiagnostics = null, preflight = null) {
138
- const loaded = sourceDiagnostics?.status === "loaded";
139
- const packet = {
140
- schema: "agentspine.hook-context/v1",
141
- event,
142
- priority: ["current-user-request", "explicit-stops", "current-task", "host-rules", "accepted-context", "style-and-relationships"],
143
- loaded,
144
- instruction: loaded
145
- ? "Use this already-loaded briefing now. Do not call an MCP tool to obtain it. The current user request and explicit stops override all remembered style, relationships, and older context."
146
- : "No host-native source context was loaded. Do not claim personal continuity or recall succeeded. Continue under current native host rules and inspect sourceResolution.",
147
- signal: signal ? {
148
- captured: Boolean(signal.captured),
149
- accepted: Boolean(signal.accepted),
150
- duplicate: Boolean(signal.duplicate),
151
- kind: signal.kind || null,
152
- reason: signal.reason || null
153
- } : null,
154
- attentionEvent: attentionEvent ? {
155
- captured: Boolean(attentionEvent.event),
156
- duplicate: Boolean(attentionEvent.duplicate),
157
- id: attentionEvent.event?.id || null,
158
- kind: attentionEvent.event?.kind || null,
159
- status: attentionEvent.event?.status || null,
160
- reason: attentionEvent.reason || null
161
- } : null,
162
- selfstarter: selfstarter ? {
163
- active: Boolean(selfstarter.job),
164
- blocked: Boolean(selfstarter.blocked),
165
- action: selfstarter.action || null,
166
- reason: selfstarter.reason || null,
167
- jobId: selfstarter.job?.id || null,
168
- taskId: selfstarter.job?.taskId || null,
169
- actorId: selfstarter.job?.actorId || null,
170
- targetId: selfstarter.job?.targetId || null,
171
- projectId: selfstarter.job?.projectId || null,
172
- groupId: selfstarter.job?.groupId || null,
173
- checkpointSequence: selfstarter.job?.checkpoint?.sequence ?? null,
174
- capabilities: selfstarter.job?.capabilities || [],
175
- leaseExpiresAt: selfstarter.job?.lease?.expiresAt || null,
176
- receiptId: selfstarter.receipt?.id || null,
177
- instruction: selfstarter.job
178
- ? "Resume only this exact checkpointed job. Attach its job ID to every host tool event. Each effect is separately re-authorized by PreToolUse and checkpointed by PostToolUse. Stop immediately on any denial."
179
- : null,
180
- authority: selfstarter.job ? "explicit-local-execution-policy" : "execution-state-only"
181
- } : null,
182
- channelEvent: channelEvent?.event ? {
183
- active: true,
184
- eventId: channelEvent.event.eventId,
185
- bindingId: channelEvent.event.bindingId,
186
- provider: channelEvent.event.provider,
187
- chatId: channelEvent.event.chatId,
188
- threadId: channelEvent.event.threadId,
189
- senderId: channelEvent.event.senderId,
190
- replyTo: channelEvent.event.replyTo,
191
- agentId: channelEvent.event.agentId,
192
- projectId: channelEvent.event.projectId,
193
- groupId: channelEvent.event.groupId,
194
- sessionKey: channelEvent.event.sessionKey,
195
- text: channelEvent.event.text,
196
- leaseExpiresAt: channelEvent.event.lease?.expiresAt || null,
197
- receiptId: channelEvent.receipt?.id || null,
198
- instruction: "Answer this exact authenticated channel event in its bound chat and thread. Do not infer another recipient or route. Sending remains subject to the separate current channel policy and adapter receipt.",
199
- authority: "explicit-local-channel-policy"
200
- } : null,
201
- indexedSources: catalog.summary.total,
202
- sourceResolution: sourceDiagnostics,
203
- preflight: preflight ? {
204
- schema: preflight.receipt.schema,
205
- receiptId: preflight.receipt.id,
206
- promptDigest: preflight.receipt.promptDigest,
207
- briefingDigest: preflight.receipt.briefingDigest,
208
- createdAt: preflight.receipt.createdAt,
209
- expiresAt: preflight.receipt.expiresAt,
210
- policy: preflight.policy,
211
- learningApplications: preflight.learningApplications || null,
212
- pendingMustRemember: preflight.pendingMustRemember ? {
213
- id: preflight.pendingMustRemember.candidate?.id || null,
214
- status: preflight.pendingMustRemember.candidate?.status || (preflight.pendingMustRemember.rejected ? "rejected" : null),
215
- reason: preflight.pendingMustRemember.reason || null
216
- } : null,
217
- briefing: preflight.briefing,
218
- instruction: "This exact turn passed the mandatory pre-answer gate. Apply the complete preflight briefing before answering.",
219
- authority: "preflight-proof-only"
220
- } : null,
221
- briefing,
222
- authority: "context-only"
223
- };
224
- return JSON.stringify(packet);
225
- }
226
-
227
- function selfstarterInput(input) {
228
- const value = input.agent_spine_job;
229
- if (value === undefined || value === null) return null;
230
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_job must be one object");
231
- return value;
232
- }
233
-
234
- function channelEventInput(input) {
235
- const gateway = gatewayEnvironmentContext();
236
- const value = input.agent_spine_channel_event ?? (gateway?.eventId && gateway?.provider
237
- ? { event_id: gateway.eventId, provider: gateway.provider } : null);
238
- if (value === undefined || value === null) return null;
239
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("agent_spine_channel_event must be one object");
240
- const unknown = Object.keys(value).filter((key) => !["event_id", "eventId", "provider"].includes(key));
241
- if (unknown.length) throw new Error(`agent_spine_channel_event contains unknown field: ${unknown.sort()[0]}`);
242
- return value;
243
- }
244
-
245
- function hookDeliveryId(input) {
246
- return boundedId(input.tool_use_id ?? input.event_id ?? input.hook_event_id, "toolUseId");
247
- }
248
-
249
- function sessionId(input) {
250
- return boundedId(input.session_id ?? input.sessionId, "sessionId");
251
- }
252
-
253
- async function startSelfstarter(input, event, root, scope, sourceDiagnostics) {
254
- if (!SELFSTART_EVENTS.has(event)) return null;
255
- const requested = selfstarterInput(input);
256
- if (selfstarterRootSkipped(sourceDiagnostics)) {
257
- if (requested) throw new Error("self-starter cannot use a user home or host profile as its workspace root");
258
- return null;
259
- }
260
- const session = sessionId(input);
261
- if (!scope.entityId || !scope.projectId || !session) {
262
- if (requested) throw new Error("self-starter start requires an exact actor, project, and host session");
263
- return null;
264
- }
265
- return startOrResumeJob({
266
- root, actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
267
- taskId: scope.currentTaskId, jobId: boundedId(requested?.job_id ?? requested?.jobId, "jobId"),
268
- host: scope.host, sessionId: session, now: input.timestamp || new Date()
269
- });
270
- }
271
-
272
- function selfstarterRootSkipped(sourceDiagnostics) {
273
- return ["skipped-unmarked-home", "skipped-home-root", "skipped-profile-root"]
274
- .includes(sourceDiagnostics?.projectTreeScan);
275
- }
276
-
277
- async function startChannelEvent(input, event, root, scope, catalog) {
278
- const requested = channelEventInput(input);
279
- if (!requested) return null;
280
- if (event !== "SessionStart") throw new Error("channel event claims are accepted only at SessionStart");
281
- const session = sessionId(input);
282
- if (!scope.entityId || !scope.projectId || !session) {
283
- throw new Error("channel event start requires an exact agent, project, and host session");
284
- }
285
- const workerId = `channel-worker:${createHash("sha256").update(`${scope.host}\0${session}`).digest("hex").slice(0, 24)}`;
286
- const claim = await claimChannelEvent({
287
- root, eventId: boundedId(requested.event_id ?? requested.eventId, "channelEventId"),
288
- agentId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
289
- provider: boundedId(requested.provider, "channelProvider"), workerId,
290
- now: input.timestamp || new Date(), catalog
291
- });
292
- if (!claim.event) throw new Error("the exact channel event is unavailable in this agent lane");
293
- return claim;
294
- }
295
-
296
- async function selfstarterScope(input, scope, root, action) {
297
- const requested = selfstarterInput(input);
298
- const hostSession = sessionId(input);
299
- const supplied = {
300
- actorId: scope.entityId, projectId: scope.projectId, groupId: scope.groupId,
301
- taskId: scope.currentTaskId, host: scope.host, sessionId: hostSession
302
- };
303
- if (requested) return { ...supplied, jobId: boundedId(requested.job_id ?? requested.jobId, "jobId") };
304
- if (!hostSession) return null;
305
- return resolveSessionJob({ root, ...supplied, action, now: input.timestamp || new Date() });
306
- }
307
-
308
- function toolSucceeded(input) {
309
- if (input.success === false || input.is_error === true || input.tool_error) return false;
310
- if (input.tool_result && typeof input.tool_result === "object" && input.tool_result.isError === true) return false;
311
- return true;
312
- }
313
-
314
- function toolResult(input) {
315
- return input.tool_result ?? input.tool_response ?? input.result ?? (input.tool_error ? { failed: true } : null);
316
- }
317
-
318
- function eventReceipt(input, event, scope, discriminator = "lifecycle") {
319
- const supplied = input.event_id ?? input.hook_event_id ?? input.tool_use_id ?? input.session_id;
320
- if (typeof supplied === "string" && ID_RE.test(supplied)) {
321
- return `receipt:${createHash("sha256").update(`${event}\0${supplied}\0${discriminator}`).digest("hex").slice(0, 24)}`;
322
- }
323
- const material = [event, scope.host, scope.entityId, scope.projectId, scope.currentTaskId, discriminator].join("\0");
324
- return `receipt:${createHash("sha256").update(material).digest("hex").slice(0, 24)}`;
325
- }
326
-
327
- function heartbeatReceipt(input, scope) {
328
- const at = new Date(input.timestamp || Date.now());
329
- if (!Number.isFinite(at.getTime())) throw new Error("heartbeat timestamp is invalid");
330
- const minute = at.toISOString().slice(0, 16);
331
- const material = [scope.host, scope.entityId, scope.groupId, scope.projectId, scope.currentTaskId, minute].join("\0");
332
- return `receipt:heartbeat:${createHash("sha256").update(material).digest("hex").slice(0, 20)}`;
333
- }
334
-
335
- function minimalAttentionSignal(prompt) {
336
- if (typeof prompt !== "string" || Buffer.byteLength(prompt) > 16384) return null;
337
- const rules = [
338
- { kind: "promise", re: /^(?:i promise(?: to)?|i will|ich verspreche(?:,)?|ich werde|prometo(?: que)?|voy a|jag lovar att|jag kommer att)\s+(.+)$/i, prefix: "Promise: " },
339
- { kind: "blocker", re: /^(?:blocker|blocked(?: by)?|i am blocked(?: by)?|ich bin blockiert(?: durch)?|blockiert durch|bloquead[oa](?: por)?|estoy bloquead[oa](?: por)?|blockerad(?: av)?|jag är blockerad(?: av)?)\s*[:,-]?\s*(.+)$/i, prefix: "Blocker: " }
340
- ];
341
- for (const rule of rules) {
342
- const match = prompt.trim().match(rule.re);
343
- if (!match) continue;
344
- const value = match[1].trim().replace(/\s+/g, " ").replace(/[.!?]+$/, "").slice(0, 220);
345
- if (value) return { kind: rule.kind, summary: `${rule.prefix}${value}` };
346
- }
347
- return null;
348
- }
349
-
350
- async function captureAttentionLifecycle(input, event, root, scope, catalog) {
351
- if (!ATTENTION_WRITE_EVENTS.has(event)) return null;
352
- const explicit = input.agent_spine_attention && typeof input.agent_spine_attention === "object"
353
- && !Array.isArray(input.agent_spine_attention) ? input.agent_spine_attention : null;
354
- let proposed = explicit;
355
- let automaticHeartbeat = false;
356
- if (!proposed && event === "UserPromptSubmit" && scope.config.enabled) {
357
- proposed = minimalAttentionSignal(promptFromInput(input));
358
- if (proposed && scope.groupId) return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
359
- }
360
- if (!proposed && event === "PostToolUse" && scope.currentTaskId) {
361
- proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "active" };
362
- automaticHeartbeat = true;
363
- }
364
- if (!proposed && ["Stop", "SubagentStop"].includes(event) && scope.currentTaskId) {
365
- proposed = { kind: "heartbeat", summary: "Work heartbeat recorded.", status: "stopped" };
366
- }
367
- if (!proposed) return null;
368
- if (event === "PostToolUse" && ![input.event_id, input.hook_event_id, input.tool_use_id].some((value) => typeof value === "string" && ID_RE.test(value))) {
369
- throw new Error("PostToolUse attention requires a stable host delivery ID");
370
- }
371
- if (!scope.entityId || !scope.projectId || !scope.currentTaskId) {
372
- if (explicit) throw new Error("lifecycle attention event is missing exact actor, project, or task scope");
373
- return { event: null, duplicate: false, reason: "missing-exact-scope" };
374
- }
375
- const privacy = proposed.privacy || "private";
376
- if (event === "UserPromptSubmit" && privacy === "group") {
377
- return { event: null, duplicate: false, reason: "rejected:group conversation events are never learned automatically" };
378
- }
379
- const discriminator = proposed.id || `${proposed.kind}:${proposed.status || ""}:${proposed.summary}`;
380
- return recordAttentionEvent({
381
- root,
382
- id: boundedId(proposed.id, "attentionEventId"),
383
- kind: proposed.kind,
384
- summary: proposed.summary,
385
- status: proposed.status || null,
386
- entityId: scope.entityId,
387
- groupId: privacy === "group" ? scope.groupId : null,
388
- projectId: scope.projectId,
389
- taskId: scope.currentTaskId,
390
- privacy,
391
- dueAt: proposed.due_at ?? proposed.dueAt ?? null,
392
- receiptId: automaticHeartbeat ? heartbeatReceipt(input, scope) : eventReceipt(input, event, scope, discriminator),
393
- host: scope.host,
394
- hookEvent: event,
395
- observedAt: input.timestamp || new Date(), catalog
396
- });
397
- }
398
-
399
- export function blunRuntimeContext(context) {
400
- const detailed = JSON.parse(context);
401
- const sourceResolution = detailed.sourceResolution ? {
402
- status: detailed.sourceResolution.status || null,
403
- reason: detailed.sourceResolution.reason || null
404
- } : null;
405
- const runtime = {
406
- schema: "agentspine.blun-runtime-context/v1",
407
- event: detailed.event,
408
- loaded: Boolean(detailed.loaded),
409
- failedClosed: detailed.failedClosed ? true : undefined,
410
- indexedSources: detailed.indexedSources || 0,
411
- sourceResolution,
412
- instruction: detailed.loaded
413
- ? "Detailed AgentSpine context is available on demand through session_briefing. Load it only when the current request needs continuity."
414
- : detailed.instruction,
415
- authority: "context-only"
416
- };
417
- if (detailed.signal && (detailed.signal.captured || detailed.signal.accepted || detailed.signal.reason)) {
418
- runtime.signal = detailed.signal;
419
- }
420
- if (detailed.attentionEvent
421
- && (detailed.attentionEvent.captured || detailed.attentionEvent.duplicate || detailed.attentionEvent.reason)) {
422
- runtime.attentionEvent = detailed.attentionEvent;
423
- }
424
- if (detailed.selfstarter && (detailed.selfstarter.active || detailed.selfstarter.blocked)) {
425
- runtime.selfstarter = detailed.selfstarter;
426
- }
427
- if (detailed.channelEvent?.active) runtime.channelEvent = detailed.channelEvent;
428
- return JSON.stringify(runtime);
429
- }
430
-
431
- export function blunRuntimeMessage(context) {
432
- const runtime = JSON.parse(blunRuntimeContext(context));
433
- const base = runtime.loaded
434
- ? `AgentSpine ready: ${runtime.indexedSources} sources indexed. Load detailed continuity only on demand through session_briefing.`
435
- : `AgentSpine unavailable${runtime.sourceResolution?.reason ? `: ${runtime.sourceResolution.reason}` : ""}. ${runtime.instruction}`;
436
- const active = {};
437
- if (runtime.signal && (runtime.signal.captured || runtime.signal.accepted
438
- || String(runtime.signal.reason || "").startsWith("rejected:"))) {
439
- active.signal = runtime.signal;
440
- }
441
- if (runtime.attentionEvent && (runtime.attentionEvent.captured || runtime.attentionEvent.duplicate
442
- || String(runtime.attentionEvent.reason || "").startsWith("rejected:"))) {
443
- active.attentionEvent = runtime.attentionEvent;
444
- }
445
- if (runtime.selfstarter) active.selfstarter = runtime.selfstarter;
446
- if (runtime.channelEvent) active.channelEvent = runtime.channelEvent;
447
- return Object.keys(active).length === 0
448
- ? base
449
- : `${base}\nActive AgentSpine runtime data: ${JSON.stringify(active)}`;
450
- }
451
-
452
- function hookOutput(event, context) {
453
- if (process.env.BLUN_PLUGIN_ROOT) {
454
- return { hookSpecificOutput: { hookEventName: event, message: blunRuntimeMessage(context) } };
455
- }
456
- return { hookSpecificOutput: { hookEventName: event, additionalContext: context } };
457
- }
458
-
459
- function candidatePaths(value, output = []) {
460
- if (!value) return output;
461
- if (typeof value === "string") {
462
- for (const match of value.matchAll(/^\*\*\* (?:Add|Update|Delete) File: (.+)$/gm)) output.push(match[1].trim());
463
- return output;
464
- }
465
- if (Array.isArray(value)) {
466
- for (const item of value) candidatePaths(item, output);
467
- return output;
468
- }
469
- for (const [key, item] of Object.entries(value)) {
470
- if (["path", "file_path", "target_file", "filename"].includes(key) && typeof item === "string") output.push(item);
471
- else candidatePaths(item, output);
472
- }
473
- return output;
474
- }
475
-
476
- function isMutationTool(name = "") {
477
- return /(^|__)(apply_patch|edit|write|delete|move|rename|bash|exec_command|shell)(_|$)/i.test(name);
478
- }
479
-
480
- function isScanFailOpenTool(name = "") {
481
- return /(^|__)(apply_patch|edit|write|bash|exec_command)(_|$)/i.test(name);
482
- }
483
-
484
- function filesystemScanError(error) {
485
- return Boolean(error && (error.code === "AGENTSPINE_SCAN_INCOMPLETE" || error.agentSpineScan === true));
486
- }
487
-
488
- export function hookScanFailureFailsOpen(eventOrError, _toolName, suppliedError) {
489
- const error = suppliedError ?? eventOrError;
490
- return filesystemScanError(error) || (["EPERM", "EACCES"].includes(error?.code)
491
- && ["opendir", "readdir", "scandir"].includes(error?.syscall));
492
- }
493
-
494
- async function auditSkippedScans(input, phase, skipped = []) {
495
- const item = skipped[0];
496
- if (!item) return;
497
- await recordHookScanAudit({
498
- event: "PreToolUse", toolName: input.tool_name || null, phase,
499
- error: { code: item.code, message: `${item.code}: ${item.operation} skipped ${item.path}` },
500
- path: item.path, operation: item.operation, now: input.timestamp || new Date()
501
- });
502
- }
503
-
504
- async function allowScanFailure(input, phase, error) {
505
- const event = input.hook_event_name || input.event_name || "unknown";
506
- await recordHookScanAudit({
507
- event, toolName: input.tool_name || null, phase, error,
508
- path: error?.path || input.cwd || process.cwd(), now: input.timestamp || new Date()
509
- });
510
- return { blocked: false, scanFailedOpen: true, event, phase, error: error.message };
511
- }
512
-
513
- async function finishScanFailure(input, payload, phase, error) {
514
- const allowed = await allowScanFailure(input, phase, error);
515
- if (payload) return allowed;
516
- process.stdout.write("{}\n");
517
- return undefined;
518
- }
519
-
520
- function stringValues(value, output = []) {
521
- if (typeof value === "string") output.push(value);
522
- else if (Array.isArray(value)) value.forEach((item) => stringValues(item, output));
523
- else if (value && typeof value === "object") Object.values(value).forEach((item) => stringValues(item, output));
524
- return output;
525
- }
526
-
527
- function shellTargetsProtected(input, documents, cwd, root) {
528
- if (!/(bash|exec_command|shell)/i.test(input.tool_name || "")) return null;
529
- const command = stringValues(input.tool_input || input.tool_args).join("\n").replaceAll("\\", "/");
530
- if (!/(?:^|[;&|\s])(?:rm|mv|cp|truncate|tee|sed\s+-i|perl\s+-i)\b|(?:^|[^>])>{1,2}(?!>)/i.test(command)) return null;
531
- for (const document of documents) {
532
- const forms = new Set([document.path.replaceAll("\\", "/"), document.relativePath]);
533
- for (const base of [cwd, root]) {
534
- const candidate = relative(base, document.path);
535
- if (candidate && !candidate.startsWith("..") && !isAbsolute(candidate)) forms.add(candidate.replaceAll("\\", "/"));
536
- }
537
- if ([...forms].some((form) => form && command.includes(form))) return document;
538
- }
539
- return null;
540
- }
541
-
542
- function deny(reason) {
543
- process.stdout.write(`${JSON.stringify({
544
- decision: "block",
545
- reason,
546
- hookSpecificOutput: {
547
- hookEventName: "PreToolUse",
548
- permissionDecision: "deny",
549
- permissionDecisionReason: reason
550
- }
551
- })}\n`);
552
- }
553
-
554
- function blockPrompt(reason) {
555
- process.stdout.write(`${JSON.stringify({
556
- decision: "block",
557
- reason,
558
- hookSpecificOutput: { hookEventName: "UserPromptSubmit", decision: "block", reason }
559
- })}\n`);
560
- }
561
-
562
- async function runHookCore(input, payload) {
46
+ async function runHookCore(input, payload, options) {
563
47
  if (!input || typeof input !== "object" || Array.isArray(input)) throw new Error("hook input must be one JSON object");
564
48
  const event = input.hook_event_name || input.event_name || "";
565
49
  if (!KNOWN_EVENTS.has(event)) throw new Error(`unsupported hook event: ${event || "missing"}`);
@@ -595,7 +79,7 @@ async function runHookCore(input, payload) {
595
79
  const reason = `AgentSpine source resolution failed closed: ${error.message}`;
596
80
  if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
597
81
  if (payload) return { blocked: true, failedClosed: true, reason };
598
- deny(reason);
82
+ denyTool(reason);
599
83
  return;
600
84
  }
601
85
  if (event === "UserPromptSubmit") {
@@ -616,16 +100,18 @@ async function runHookCore(input, payload) {
616
100
  }
617
101
  const root = resolvedSources.projectRoot;
618
102
  const catalog = resolvedSources.catalog;
619
- const catalogPath = await saveCatalog(catalog);
103
+ const catalogPath = await saveCatalog(catalog), sourceWarning = resolvedSources.diagnostics.warning || null;
620
104
  let scope = null;
621
105
  let selfstarter = null;
622
106
  let channelEvent = null;
623
107
  let learningDelivery = null;
624
-
108
+ let deliveryVerification = null;
109
+ let artifactGuard = null;
110
+ let premortem = null;
111
+ let lessonRecall = null;
625
112
  if (event === "PreToolUse" && isScanFailOpenTool(input.tool_name) && resolvedSources.diagnostics.skipped?.length) {
626
113
  await auditSkippedScans(input, "source-resolution", resolvedSources.diagnostics.skipped);
627
114
  }
628
-
629
115
  if (event === "PreToolUse" && isMutationTool(input.tool_name)) {
630
116
  const { graph } = await loadGraph(root, catalog);
631
117
  const inferredProtected = new Set(graph.annotations
@@ -659,11 +145,32 @@ async function runHookCore(input, payload) {
659
145
  const relativePath = shellHit?.relativePath || catalog.documents.find((doc) => resolve(doc.path) === hit)?.relativePath || hit;
660
146
  const reason = `AgentSpine protected source: ${relativePath}. Existing identity, rule, soul, and memory documents are read-only to agents.`;
661
147
  if (payload) return { blocked: true, reason };
662
- deny(reason);
148
+ denyTool(reason);
663
149
  return;
664
150
  }
665
151
  }
666
-
152
+ if (event === "PreToolUse") {
153
+ try {
154
+ artifactGuard = await verifyBaselineBeforeWrite({ input, cwd });
155
+ if (["no-stand", "no-baseline", "invalid-stand", "ambiguous-baseline"].includes(artifactGuard.status)) {
156
+ await auditGuard(input, "baseline-guard", artifactGuard, true);
157
+ }
158
+ if (artifactGuard.blocked) {
159
+ if (payload) return { blocked: true, reason: artifactGuard.reason, artifactGuard };
160
+ denyTool(artifactGuard.reason);
161
+ return;
162
+ }
163
+ } catch (error) {
164
+ artifactGuard = { status: "scan-failed-open", blocked: false, path: error.path || cwd, reason: error.message };
165
+ await auditGuard(input, "baseline-guard", artifactGuard);
166
+ }
167
+ try { await captureJavaScriptBeforeWrite({ input, cwd, root }); }
168
+ catch (error) {
169
+ await auditGuard(input, "identifier-before-state", {
170
+ status: "scan-failed-open", blocked: false, path: error.path || cwd, reason: error.message
171
+ });
172
+ }
173
+ }
667
174
  if (event === "PreToolUse" && !selfstarterRootSkipped(resolvedSources.diagnostics)) {
668
175
  try {
669
176
  scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
@@ -680,11 +187,101 @@ async function runHookCore(input, payload) {
680
187
  }
681
188
  const reason = `AgentSpine self-starter denied this effect: ${error.message}`;
682
189
  if (payload) return { blocked: true, reason, selfstarter: { allowed: false, reason: error.message } };
683
- deny(reason);
190
+ denyTool(reason);
684
191
  return;
685
192
  }
686
193
  }
194
+ if (event === "PreToolUse" && isPremortemWrite(input)) {
195
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
196
+ premortem = await verifyHookPremortemWrite({ input, root, scope });
197
+ if (premortem.blocked) {
198
+ if (premortem.status === "invalid-session") {
199
+ if (payload) return { blocked: true, reason: premortem.reason, premortem };
200
+ return denyTool(premortem.reason);
201
+ }
202
+ deliveryVerification = await recordDeliveryWriteIntent({
203
+ root, host: scope.host, sessionId: deliveryActorSession(input), scope, input
204
+ });
205
+ return processAdvisory(input, payload, root, scope, { premortem, deliveryVerification });
206
+ }
207
+ }
208
+ if (event === "PreToolUse") {
209
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
210
+ deliveryVerification = await recordDeliveryWriteIntent({
211
+ root, host: scope.host, sessionId: deliveryActorSession(input), scope, input
212
+ });
213
+ if (deliveryVerification.blocked) {
214
+ return processAdvisory(input, payload, root, scope, { deliveryVerification });
215
+ }
216
+ if (typeof options.afterDeliveryWriteIntent === "function") await options.afterDeliveryWriteIntent();
217
+ const premortemIntent = await recordHookPremortemWriteIntent({ input, root, scope });
218
+ if (premortemIntent.blocked) {
219
+ return processAdvisory(input, payload, root, scope, { deliveryVerification, premortem: premortemIntent });
220
+ }
221
+ premortem = { ...premortem, writeIntent: premortemIntent.status,
222
+ writeDigest: premortemIntent.writeDigest || null };
223
+ try { lessonRecall = actionLessonRecall({ catalog, event, input, scope }); }
224
+ catch (error) { lessonRecall = { status: "degraded", items: [], reason: error.message, authority: "context-only" }; }
225
+ }
226
+ if (["PostToolUse", "Stop", "SubagentStop"].includes(event)) {
227
+ scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
228
+ if (event === "PostToolUse") {
229
+ premortem = await recordHookPremortemWrite({
230
+ input, root, scope, success: toolSucceeded(input)
231
+ });
232
+ deliveryVerification = await recordDeliveryToolUse({
233
+ root, host: scope.host, sessionId: deliveryActorSession(input), scope,
234
+ input, success: deliverySuccessEvidence(input)
235
+ });
236
+ if (toolSucceeded(input)) {
237
+ try {
238
+ artifactGuard = await inspectWrittenJavaScript({ input, cwd, root });
239
+ } catch (error) {
240
+ artifactGuard = { status: "scan-failed-open", blocked: false, path: error.path || cwd, reason: error.message };
241
+ await auditGuard(input, "identifier-guard", artifactGuard);
242
+ }
243
+ }
244
+ } else {
245
+ const activeJob = selfstarterRootSkipped(resolvedSources.diagnostics)
246
+ ? null : await selfstarterScope(input, scope, root, "resume");
247
+ const pauseRequested = Boolean(activeJob && selfstarterInput(input)?.status !== "completed");
248
+ let contracts = await verifyHookStopContracts({
249
+ input,
250
+ root,
251
+ scope,
252
+ recordPause: pauseRequested
253
+ });
254
+ ({ deliveryVerification, premortem } = contracts);
255
+ if (contracts.blocked) {
256
+ return processAdvisory(input, payload, root, scope, { deliveryVerification, premortem });
257
+ }
687
258
 
259
+ try {
260
+ artifactGuard = await verifyDeliveredArtifacts({ input, cwd });
261
+ if (artifactGuard.blocked) {
262
+ return processAdvisory(input, payload, root, scope, { deliveryVerification, artifactGuard });
263
+ }
264
+ } catch (error) {
265
+ artifactGuard = { status: "scan-failed-open", blocked: false, path: error.path || cwd, reason: error.message };
266
+ await auditGuard(input, "delivery-artifact-guard", artifactGuard);
267
+ }
268
+ if (typeof options.afterArtifactVerification === "function") await options.afterArtifactVerification();
269
+ contracts = await verifyHookStopContracts({
270
+ input,
271
+ root,
272
+ scope,
273
+ recordPause: pauseRequested,
274
+ completionFence: !pauseRequested,
275
+ afterDeliveryVerification: options.afterFinalDeliveryVerification,
276
+ afterPremortemVerification: options.afterFinalPremortemVerification
277
+ });
278
+ ({ deliveryVerification, premortem } = contracts);
279
+ if (contracts.blocked) {
280
+ return processAdvisory(input, payload, root, scope,
281
+ { deliveryVerification, premortem, artifactGuard });
282
+ }
283
+ }
284
+ }
688
285
  let attentionEvent = null;
689
286
  if (ATTENTION_WRITE_EVENTS.has(event) && !CONTEXT_EVENTS.has(event)) {
690
287
  scope = await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
@@ -733,6 +330,11 @@ async function runHookCore(input, payload) {
733
330
  try {
734
331
  await syncPersonaRosterFromEnvironment({ root, env: process.env, now: input.timestamp || new Date(), catalog });
735
332
  scope ||= await runtimeScope(input, root, resolvedSources.userStateRoot, catalog);
333
+ if (event !== "UserPromptSubmit") {
334
+ try { resolvedSources.diagnostics.timeline = await captureSessionTimelineLifecycle({ root, event, input, scope,
335
+ hostHome: resolvedSources.hostHome, catalog }); }
336
+ catch (error) { resolvedSources.diagnostics.timeline = { status: "degraded", reason: error.message, authority: "context-only" }; }
337
+ }
736
338
  channelEvent = await startChannelEvent(input, event, root, scope, catalog);
737
339
  selfstarter = await startSelfstarter(input, event, root, scope, resolvedSources.diagnostics);
738
340
  if (selfstarter?.job && !scope.currentTaskId) scope.currentTaskId = selfstarter.job.taskId;
@@ -746,6 +348,7 @@ async function runHookCore(input, payload) {
746
348
  receipt: preflight.receipt, input, scope, resolvedSources, prompt,
747
349
  now: input.timestamp || new Date(), env: process.env
748
350
  })) throw new Error("newly created preflight receipt failed exact turn verification");
351
+ preflight.premortem = await prepareHookPremortem({ input, root, scope });
749
352
  preflight.pendingMustRemember = await captureMustRememberPrompt({ prompt, receipt: preflight.receipt, env: process.env });
750
353
  try {
751
354
  attentionEvent = await captureAttentionLifecycle(input, event, root, scope, catalog);
@@ -776,14 +379,20 @@ async function runHookCore(input, payload) {
776
379
  catalog, userStateRoot: resolvedSources.userStateRoot, sourceDiagnostics: resolvedSources.diagnostics,
777
380
  prompt: event === "UserPromptSubmit" ? promptFromInput(input) : null
778
381
  });
779
- let context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight);
382
+ if (event === "PostCompact") {
383
+ try { lessonRecall = actionLessonRecall({ catalog, event, input, scope }); }
384
+ catch (error) { lessonRecall = { status: "degraded", items: [], reason: error.message, authority: "context-only" }; }
385
+ }
386
+ let context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent, resolvedSources.diagnostics, preflight, lessonRecall);
780
387
  if (event === "UserPromptSubmit" && Buffer.byteLength(context) > hostContextLimit(preflight)) {
781
388
  throw new Error("mandatory preflight context exceeds the host hook injection limit");
782
389
  }
783
- if (event === "UserPromptSubmit" && !await verifyPreflightReceipt({
784
- receipt: preflight.receipt, input, scope, resolvedSources, prompt: promptFromInput(input),
785
- now: input.timestamp || new Date(), env: process.env, consume: true
786
- })) throw new Error("preflight receipt could not be consumed atomically for this exact turn");
390
+ if (event === "UserPromptSubmit") {
391
+ const { preflightConsumed, timeline } = await finalizeUserPromptSessionTimeline({ root, input, scope, resolvedSources, preflight,
392
+ prompt: promptFromInput(input), now: input.timestamp || new Date() });
393
+ if (!preflightConsumed) throw new Error("preflight receipt could not be consumed atomically for this exact turn");
394
+ resolvedSources.diagnostics.timeline = timeline;
395
+ }
787
396
  if (event === "UserPromptSubmit") {
788
397
  const activeCanaries = briefing.learning.filter((item) => ["active", "revalidating"].includes(item.outcomeStatus));
789
398
  if (!activeCanaries.length) {
@@ -815,15 +424,15 @@ async function runHookCore(input, payload) {
815
424
  };
816
425
  }
817
426
  const enriched = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
818
- resolvedSources.diagnostics, preflight);
427
+ resolvedSources.diagnostics, preflight, lessonRecall);
819
428
  if (Buffer.byteLength(enriched) <= hostContextLimit(preflight)) context = enriched;
820
429
  else if (preflight.learningApplications.status === "degraded") {
821
430
  preflight.learningApplications = null;
822
431
  context = renderContext(event, catalog, briefing, signal, attentionEvent, selfstarter, channelEvent,
823
- resolvedSources.diagnostics, preflight);
432
+ resolvedSources.diagnostics, preflight, lessonRecall);
824
433
  }
825
434
  }
826
- if (payload) return { blocked: false, context, briefing, preflight, signal, attentionEvent, channelEvent, catalogPath };
435
+ if (payload) return { blocked: false, context, briefing, preflight, signal, attentionEvent, channelEvent, lessonRecall, catalogPath };
827
436
  process.stdout.write(`${JSON.stringify(hookOutput(event, context))}\n`);
828
437
  return;
829
438
  } catch (error) {
@@ -851,17 +460,26 @@ async function runHookCore(input, payload) {
851
460
  return;
852
461
  }
853
462
  }
854
-
855
- if (payload) return { blocked: false, attentionEvent, selfstarter, learningDelivery };
856
- process.stdout.write("{}\n");
463
+ if (artifactGuard?.blocked) {
464
+ return processAdvisory(input, payload, root, scope, { artifactGuard,
465
+ attentionEvent, selfstarter, learningDelivery, deliveryVerification, premortem });
466
+ }
467
+ if (payload) return { blocked: false, sourceWarning, artifactGuard, attentionEvent, selfstarter,
468
+ learningDelivery, deliveryVerification, premortem, lessonRecall };
469
+ process.stdout.write(`${JSON.stringify(lifecycleOutput(event, artifactGuard, premortem, deliveryVerification, process.env, sourceWarning, lessonRecall))}\n`);
857
470
  }
858
471
 
859
472
  export async function runHook(payload = null, options = {}) {
860
473
  let input = payload;
861
474
  try {
862
- input ||= await readStdin(options);
475
+ input ||= await readHookInput(options);
863
476
  if (input === SILENT_OVERSIZE_POST_TOOL_USE) return;
864
- return await runHookCore(input, payload);
477
+ if (input.hook_event_name === "PreToolUse" && timelineToolKind(input.tool_name)) {
478
+ const result = await runTimelineToolGuard(input);
479
+ if (!payload) emitTimelineToolGuard(result);
480
+ return result;
481
+ }
482
+ return await runHookCore(input, payload, options);
865
483
  } catch (error) {
866
484
  if (!hookScanFailureFailsOpen(error) || input === SILENT_OVERSIZE_POST_TOOL_USE) throw error;
867
485
  return finishScanFailure(input, payload, "lifecycle", error);