approval-md 0.0.1 → 0.2.0

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 (722) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +940 -4
  4. package/SPEC.md +476 -34
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.d.ts +426 -0
  7. package/dist/src/adapters/agentmail.js +1200 -0
  8. package/dist/src/adapters/agentmail.js.map +1 -0
  9. package/dist/src/adapters/conformance.d.ts +149 -0
  10. package/dist/src/adapters/conformance.js +461 -0
  11. package/dist/src/adapters/conformance.js.map +1 -0
  12. package/dist/src/adapters/contract.d.ts +628 -0
  13. package/dist/src/adapters/contract.js +1035 -0
  14. package/dist/src/adapters/contract.js.map +1 -0
  15. package/dist/src/adapters/email.d.ts +324 -0
  16. package/dist/src/adapters/email.js +749 -0
  17. package/dist/src/adapters/email.js.map +1 -0
  18. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  19. package/dist/src/adapters/env-passphrase.js +132 -0
  20. package/dist/src/adapters/env-passphrase.js.map +1 -0
  21. package/dist/src/adapters/public.d.ts +11 -0
  22. package/dist/src/adapters/public.js +11 -0
  23. package/dist/src/adapters/public.js.map +1 -0
  24. package/dist/src/adapters/registry.d.ts +59 -0
  25. package/dist/src/adapters/registry.js +77 -0
  26. package/dist/src/adapters/registry.js.map +1 -0
  27. package/dist/src/adapters/smtp.d.ts +213 -0
  28. package/dist/src/adapters/smtp.js +499 -0
  29. package/dist/src/adapters/smtp.js.map +1 -0
  30. package/dist/src/adapters/vault-provider.d.ts +114 -0
  31. package/dist/src/adapters/vault-provider.js +161 -0
  32. package/dist/src/adapters/vault-provider.js.map +1 -0
  33. package/dist/src/adapters/zzz.d.ts +66 -0
  34. package/dist/src/adapters/zzz.js +299 -0
  35. package/dist/src/adapters/zzz.js.map +1 -0
  36. package/dist/src/channels/batch.d.ts +109 -0
  37. package/dist/src/channels/batch.js +121 -0
  38. package/dist/src/channels/batch.js.map +1 -0
  39. package/dist/src/channels/cli.d.ts +193 -0
  40. package/dist/src/channels/cli.js +468 -0
  41. package/dist/src/channels/cli.js.map +1 -0
  42. package/dist/src/channels/conformance.d.ts +92 -0
  43. package/dist/src/channels/conformance.js +445 -0
  44. package/dist/src/channels/conformance.js.map +1 -0
  45. package/dist/src/channels/contract.d.ts +623 -0
  46. package/dist/src/channels/contract.js +494 -0
  47. package/dist/src/channels/contract.js.map +1 -0
  48. package/dist/src/channels/payload-view.d.ts +35 -0
  49. package/dist/src/channels/payload-view.js +43 -0
  50. package/dist/src/channels/payload-view.js.map +1 -0
  51. package/dist/src/channels/render-queue.d.ts +149 -0
  52. package/dist/src/channels/render-queue.js +564 -0
  53. package/dist/src/channels/render-queue.js.map +1 -0
  54. package/dist/src/channels/tagging.d.ts +196 -0
  55. package/dist/src/channels/tagging.js +723 -0
  56. package/dist/src/channels/tagging.js.map +1 -0
  57. package/dist/src/channels/telegram.d.ts +1832 -0
  58. package/dist/src/channels/telegram.js +3190 -0
  59. package/dist/src/channels/telegram.js.map +1 -0
  60. package/dist/src/channels/web.d.ts +341 -0
  61. package/dist/src/channels/web.js +903 -0
  62. package/dist/src/channels/web.js.map +1 -0
  63. package/dist/src/cli/adapter.d.ts +90 -0
  64. package/dist/src/cli/adapter.js +288 -0
  65. package/dist/src/cli/adapter.js.map +1 -0
  66. package/dist/src/cli/amend.d.ts +59 -0
  67. package/dist/src/cli/amend.js +2171 -0
  68. package/dist/src/cli/amend.js.map +1 -0
  69. package/dist/src/cli/args.d.ts +43 -0
  70. package/dist/src/cli/args.js +86 -0
  71. package/dist/src/cli/args.js.map +1 -0
  72. package/dist/src/cli/attest.d.ts +41 -0
  73. package/dist/src/cli/attest.js +307 -0
  74. package/dist/src/cli/attest.js.map +1 -0
  75. package/dist/src/cli/audit-card.d.ts +62 -0
  76. package/dist/src/cli/audit-card.js +201 -0
  77. package/dist/src/cli/audit-card.js.map +1 -0
  78. package/dist/src/cli/audit.d.ts +59 -0
  79. package/dist/src/cli/audit.js +460 -0
  80. package/dist/src/cli/audit.js.map +1 -0
  81. package/dist/src/cli/channel-telegram.d.ts +806 -0
  82. package/dist/src/cli/channel-telegram.js +2063 -0
  83. package/dist/src/cli/channel-telegram.js.map +1 -0
  84. package/dist/src/cli/channel-web.d.ts +131 -0
  85. package/dist/src/cli/channel-web.js +357 -0
  86. package/dist/src/cli/channel-web.js.map +1 -0
  87. package/dist/src/cli/channel.d.ts +71 -0
  88. package/dist/src/cli/channel.js +438 -0
  89. package/dist/src/cli/channel.js.map +1 -0
  90. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  91. package/dist/src/cli/checkpoint-tap.js +238 -0
  92. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  93. package/dist/src/cli/codex.d.ts +2 -0
  94. package/dist/src/cli/codex.js +172 -0
  95. package/dist/src/cli/codex.js.map +1 -0
  96. package/dist/src/cli/coverage.d.ts +61 -0
  97. package/dist/src/cli/coverage.js +343 -0
  98. package/dist/src/cli/coverage.js.map +1 -0
  99. package/dist/src/cli/daemon.d.ts +120 -0
  100. package/dist/src/cli/daemon.js +631 -0
  101. package/dist/src/cli/daemon.js.map +1 -0
  102. package/dist/src/cli/doctor.d.ts +129 -0
  103. package/dist/src/cli/doctor.js +2762 -0
  104. package/dist/src/cli/doctor.js.map +1 -0
  105. package/dist/src/cli/env.d.ts +65 -0
  106. package/dist/src/cli/env.js +302 -0
  107. package/dist/src/cli/env.js.map +1 -0
  108. package/dist/src/cli/execute.d.ts +202 -0
  109. package/dist/src/cli/execute.js +1682 -0
  110. package/dist/src/cli/execute.js.map +1 -0
  111. package/dist/src/cli/exit-codes.d.ts +73 -0
  112. package/dist/src/cli/exit-codes.js +82 -0
  113. package/dist/src/cli/exit-codes.js.map +1 -0
  114. package/dist/src/cli/feedback.d.ts +60 -0
  115. package/dist/src/cli/feedback.js +205 -0
  116. package/dist/src/cli/feedback.js.map +1 -0
  117. package/dist/src/cli/gate-window.d.ts +40 -0
  118. package/dist/src/cli/gate-window.js +294 -0
  119. package/dist/src/cli/gate-window.js.map +1 -0
  120. package/dist/src/cli/gate.d.ts +68 -0
  121. package/dist/src/cli/gate.js +557 -0
  122. package/dist/src/cli/gate.js.map +1 -0
  123. package/dist/src/cli/git-scope.d.ts +190 -0
  124. package/dist/src/cli/git-scope.js +295 -0
  125. package/dist/src/cli/git-scope.js.map +1 -0
  126. package/dist/src/cli/gloss-attach.d.ts +85 -0
  127. package/dist/src/cli/gloss-attach.js +107 -0
  128. package/dist/src/cli/gloss-attach.js.map +1 -0
  129. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  130. package/dist/src/cli/gloss-codex-child.js +149 -0
  131. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  132. package/dist/src/cli/gloss-codex.d.ts +24 -0
  133. package/dist/src/cli/gloss-codex.js +255 -0
  134. package/dist/src/cli/gloss-codex.js.map +1 -0
  135. package/dist/src/cli/gloss-options.d.ts +42 -0
  136. package/dist/src/cli/gloss-options.js +79 -0
  137. package/dist/src/cli/gloss-options.js.map +1 -0
  138. package/dist/src/cli/gloss.d.ts +265 -0
  139. package/dist/src/cli/gloss.js +362 -0
  140. package/dist/src/cli/gloss.js.map +1 -0
  141. package/dist/src/cli/help.d.ts +103 -0
  142. package/dist/src/cli/help.js +2339 -0
  143. package/dist/src/cli/help.js.map +1 -0
  144. package/dist/src/cli/hook-codex.d.ts +78 -0
  145. package/dist/src/cli/hook-codex.js +167 -0
  146. package/dist/src/cli/hook-codex.js.map +1 -0
  147. package/dist/src/cli/hook.d.ts +331 -0
  148. package/dist/src/cli/hook.js +2849 -0
  149. package/dist/src/cli/hook.js.map +1 -0
  150. package/dist/src/cli/import.d.ts +35 -0
  151. package/dist/src/cli/import.js +175 -0
  152. package/dist/src/cli/import.js.map +1 -0
  153. package/dist/src/cli/init.d.ts +84 -0
  154. package/dist/src/cli/init.js +336 -0
  155. package/dist/src/cli/init.js.map +1 -0
  156. package/dist/src/cli/instructions.d.ts +23 -0
  157. package/dist/src/cli/instructions.js +262 -0
  158. package/dist/src/cli/instructions.js.map +1 -0
  159. package/dist/src/cli/journal.d.ts +41 -0
  160. package/dist/src/cli/journal.js +238 -0
  161. package/dist/src/cli/journal.js.map +1 -0
  162. package/dist/src/cli/log-advance.d.ts +287 -0
  163. package/dist/src/cli/log-advance.js +840 -0
  164. package/dist/src/cli/log-advance.js.map +1 -0
  165. package/dist/src/cli/log-anchor.d.ts +176 -0
  166. package/dist/src/cli/log-anchor.js +387 -0
  167. package/dist/src/cli/log-anchor.js.map +1 -0
  168. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  169. package/dist/src/cli/log-checkpoint.js +128 -0
  170. package/dist/src/cli/log-checkpoint.js.map +1 -0
  171. package/dist/src/cli/log-sync.d.ts +243 -0
  172. package/dist/src/cli/log-sync.js +849 -0
  173. package/dist/src/cli/log-sync.js.map +1 -0
  174. package/dist/src/cli/log-verbs.d.ts +16 -0
  175. package/dist/src/cli/log-verbs.js +360 -0
  176. package/dist/src/cli/log-verbs.js.map +1 -0
  177. package/dist/src/cli/long-help.d.ts +70 -0
  178. package/dist/src/cli/long-help.js +148 -0
  179. package/dist/src/cli/long-help.js.map +1 -0
  180. package/dist/src/cli/main.d.ts +77 -0
  181. package/dist/src/cli/main.js +1206 -0
  182. package/dist/src/cli/main.js.map +1 -0
  183. package/dist/src/cli/mcp.d.ts +52 -0
  184. package/dist/src/cli/mcp.js +306 -0
  185. package/dist/src/cli/mcp.js.map +1 -0
  186. package/dist/src/cli/paths.d.ts +56 -0
  187. package/dist/src/cli/paths.js +79 -0
  188. package/dist/src/cli/paths.js.map +1 -0
  189. package/dist/src/cli/payload.d.ts +58 -0
  190. package/dist/src/cli/payload.js +253 -0
  191. package/dist/src/cli/payload.js.map +1 -0
  192. package/dist/src/cli/policy.d.ts +43 -0
  193. package/dist/src/cli/policy.js +229 -0
  194. package/dist/src/cli/policy.js.map +1 -0
  195. package/dist/src/cli/preflight.d.ts +363 -0
  196. package/dist/src/cli/preflight.js +1175 -0
  197. package/dist/src/cli/preflight.js.map +1 -0
  198. package/dist/src/cli/progress.d.ts +78 -0
  199. package/dist/src/cli/progress.js +112 -0
  200. package/dist/src/cli/progress.js.map +1 -0
  201. package/dist/src/cli/prompt.d.ts +209 -0
  202. package/dist/src/cli/prompt.js +312 -0
  203. package/dist/src/cli/prompt.js.map +1 -0
  204. package/dist/src/cli/quickstart.d.ts +46 -0
  205. package/dist/src/cli/quickstart.js +297 -0
  206. package/dist/src/cli/quickstart.js.map +1 -0
  207. package/dist/src/cli/records.d.ts +34 -0
  208. package/dist/src/cli/records.js +66 -0
  209. package/dist/src/cli/records.js.map +1 -0
  210. package/dist/src/cli/render.d.ts +22 -0
  211. package/dist/src/cli/render.js +132 -0
  212. package/dist/src/cli/render.js.map +1 -0
  213. package/dist/src/cli/sandbox.d.ts +51 -0
  214. package/dist/src/cli/sandbox.js +150 -0
  215. package/dist/src/cli/sandbox.js.map +1 -0
  216. package/dist/src/cli/scaffold.d.ts +79 -0
  217. package/dist/src/cli/scaffold.js +137 -0
  218. package/dist/src/cli/scaffold.js.map +1 -0
  219. package/dist/src/cli/setup-adapter.d.ts +137 -0
  220. package/dist/src/cli/setup-adapter.js +509 -0
  221. package/dist/src/cli/setup-adapter.js.map +1 -0
  222. package/dist/src/cli/setup-channel.d.ts +117 -0
  223. package/dist/src/cli/setup-channel.js +635 -0
  224. package/dist/src/cli/setup-channel.js.map +1 -0
  225. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  226. package/dist/src/cli/setup-checkpoint.js +196 -0
  227. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  228. package/dist/src/cli/setup-common.d.ts +275 -0
  229. package/dist/src/cli/setup-common.js +376 -0
  230. package/dist/src/cli/setup-common.js.map +1 -0
  231. package/dist/src/cli/setup-flow.d.ts +287 -0
  232. package/dist/src/cli/setup-flow.js +476 -0
  233. package/dist/src/cli/setup-flow.js.map +1 -0
  234. package/dist/src/cli/setup-service.d.ts +96 -0
  235. package/dist/src/cli/setup-service.js +308 -0
  236. package/dist/src/cli/setup-service.js.map +1 -0
  237. package/dist/src/cli/setup.d.ts +202 -0
  238. package/dist/src/cli/setup.js +473 -0
  239. package/dist/src/cli/setup.js.map +1 -0
  240. package/dist/src/cli/style.d.ts +320 -0
  241. package/dist/src/cli/style.js +469 -0
  242. package/dist/src/cli/style.js.map +1 -0
  243. package/dist/src/cli/token.d.ts +39 -0
  244. package/dist/src/cli/token.js +274 -0
  245. package/dist/src/cli/token.js.map +1 -0
  246. package/dist/src/cli/up.d.ts +155 -0
  247. package/dist/src/cli/up.js +849 -0
  248. package/dist/src/cli/up.js.map +1 -0
  249. package/dist/src/cli/usage.d.ts +37 -0
  250. package/dist/src/cli/usage.js +91 -0
  251. package/dist/src/cli/usage.js.map +1 -0
  252. package/dist/src/cli/values.d.ts +40 -0
  253. package/dist/src/cli/values.js +189 -0
  254. package/dist/src/cli/values.js.map +1 -0
  255. package/dist/src/cli/vault.d.ts +59 -0
  256. package/dist/src/cli/vault.js +362 -0
  257. package/dist/src/cli/vault.js.map +1 -0
  258. package/dist/src/cli/verb-registry.d.ts +76 -0
  259. package/dist/src/cli/verb-registry.js +2341 -0
  260. package/dist/src/cli/verb-registry.js.map +1 -0
  261. package/dist/src/cli/wordmark.d.ts +31 -0
  262. package/dist/src/cli/wordmark.js +52 -0
  263. package/dist/src/cli/wordmark.js.map +1 -0
  264. package/dist/src/codex/doctor.d.ts +13 -0
  265. package/dist/src/codex/doctor.js +41 -0
  266. package/dist/src/codex/doctor.js.map +1 -0
  267. package/dist/src/codex/manifest.d.ts +49 -0
  268. package/dist/src/codex/manifest.js +103 -0
  269. package/dist/src/codex/manifest.js.map +1 -0
  270. package/dist/src/codex/templates.d.ts +41 -0
  271. package/dist/src/codex/templates.js +319 -0
  272. package/dist/src/codex/templates.js.map +1 -0
  273. package/dist/src/codex/trust.d.ts +19 -0
  274. package/dist/src/codex/trust.js +183 -0
  275. package/dist/src/codex/trust.js.map +1 -0
  276. package/dist/src/codex/workspace-plan.d.ts +131 -0
  277. package/dist/src/codex/workspace-plan.js +561 -0
  278. package/dist/src/codex/workspace-plan.js.map +1 -0
  279. package/dist/src/core/actor.d.ts +2 -0
  280. package/dist/src/core/actor.js +5 -0
  281. package/dist/src/core/actor.js.map +1 -0
  282. package/dist/src/core/advance-cycle.d.ts +170 -0
  283. package/dist/src/core/advance-cycle.js +200 -0
  284. package/dist/src/core/advance-cycle.js.map +1 -0
  285. package/dist/src/core/agents-md.d.ts +276 -0
  286. package/dist/src/core/agents-md.js +747 -0
  287. package/dist/src/core/agents-md.js.map +1 -0
  288. package/dist/src/core/apply-patch.d.ts +49 -0
  289. package/dist/src/core/apply-patch.js +266 -0
  290. package/dist/src/core/apply-patch.js.map +1 -0
  291. package/dist/src/core/attest.d.ts +420 -0
  292. package/dist/src/core/attest.js +589 -0
  293. package/dist/src/core/attest.js.map +1 -0
  294. package/dist/src/core/audit.d.ts +492 -0
  295. package/dist/src/core/audit.js +882 -0
  296. package/dist/src/core/audit.js.map +1 -0
  297. package/dist/src/core/budgets.d.ts +238 -0
  298. package/dist/src/core/budgets.js +449 -0
  299. package/dist/src/core/budgets.js.map +1 -0
  300. package/dist/src/core/checkpoint.d.ts +500 -0
  301. package/dist/src/core/checkpoint.js +738 -0
  302. package/dist/src/core/checkpoint.js.map +1 -0
  303. package/dist/src/core/child-env.d.ts +88 -0
  304. package/dist/src/core/child-env.js +86 -0
  305. package/dist/src/core/child-env.js.map +1 -0
  306. package/dist/src/core/clock.d.ts +52 -0
  307. package/dist/src/core/clock.js +43 -0
  308. package/dist/src/core/clock.js.map +1 -0
  309. package/dist/src/core/command-class.d.ts +543 -0
  310. package/dist/src/core/command-class.js +2356 -0
  311. package/dist/src/core/command-class.js.map +1 -0
  312. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  313. package/dist/src/core/coverage-sources/adapter.js +71 -0
  314. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  315. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  316. package/dist/src/core/coverage-sources/gh.js +136 -0
  317. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  318. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  319. package/dist/src/core/coverage-sources/git.js +269 -0
  320. package/dist/src/core/coverage-sources/git.js.map +1 -0
  321. package/dist/src/core/coverage.d.ts +217 -0
  322. package/dist/src/core/coverage.js +337 -0
  323. package/dist/src/core/coverage.js.map +1 -0
  324. package/dist/src/core/credential-spec.d.ts +72 -0
  325. package/dist/src/core/credential-spec.js +23 -0
  326. package/dist/src/core/credential-spec.js.map +1 -0
  327. package/dist/src/core/dark-session.d.ts +331 -0
  328. package/dist/src/core/dark-session.js +714 -0
  329. package/dist/src/core/dark-session.js.map +1 -0
  330. package/dist/src/core/decision-refusal.d.ts +185 -0
  331. package/dist/src/core/decision-refusal.js +265 -0
  332. package/dist/src/core/decision-refusal.js.map +1 -0
  333. package/dist/src/core/env-file.d.ts +450 -0
  334. package/dist/src/core/env-file.js +837 -0
  335. package/dist/src/core/env-file.js.map +1 -0
  336. package/dist/src/core/execute.d.ts +858 -0
  337. package/dist/src/core/execute.js +1271 -0
  338. package/dist/src/core/execute.js.map +1 -0
  339. package/dist/src/core/frontmatter.d.ts +78 -0
  340. package/dist/src/core/frontmatter.js +100 -0
  341. package/dist/src/core/frontmatter.js.map +1 -0
  342. package/dist/src/core/gate-window.d.ts +312 -0
  343. package/dist/src/core/gate-window.js +506 -0
  344. package/dist/src/core/gate-window.js.map +1 -0
  345. package/dist/src/core/gate.d.ts +1364 -0
  346. package/dist/src/core/gate.js +3002 -0
  347. package/dist/src/core/gate.js.map +1 -0
  348. package/dist/src/core/git-run.d.ts +73 -0
  349. package/dist/src/core/git-run.js +93 -0
  350. package/dist/src/core/git-run.js.map +1 -0
  351. package/dist/src/core/harness-version.d.ts +157 -0
  352. package/dist/src/core/harness-version.js +211 -0
  353. package/dist/src/core/harness-version.js.map +1 -0
  354. package/dist/src/core/harness-wait.d.ts +55 -0
  355. package/dist/src/core/harness-wait.js +58 -0
  356. package/dist/src/core/harness-wait.js.map +1 -0
  357. package/dist/src/core/head-retry.d.ts +107 -0
  358. package/dist/src/core/head-retry.js +121 -0
  359. package/dist/src/core/head-retry.js.map +1 -0
  360. package/dist/src/core/instance.d.ts +253 -0
  361. package/dist/src/core/instance.js +319 -0
  362. package/dist/src/core/instance.js.map +1 -0
  363. package/dist/src/core/intake-limits.d.ts +247 -0
  364. package/dist/src/core/intake-limits.js +350 -0
  365. package/dist/src/core/intake-limits.js.map +1 -0
  366. package/dist/src/core/jcs.d.ts +52 -0
  367. package/dist/src/core/jcs.js +132 -0
  368. package/dist/src/core/jcs.js.map +1 -0
  369. package/dist/src/core/journal.d.ts +144 -0
  370. package/dist/src/core/journal.js +200 -0
  371. package/dist/src/core/journal.js.map +1 -0
  372. package/dist/src/core/live-draw.d.ts +436 -0
  373. package/dist/src/core/live-draw.js +703 -0
  374. package/dist/src/core/live-draw.js.map +1 -0
  375. package/dist/src/core/log-reconcile.d.ts +89 -0
  376. package/dist/src/core/log-reconcile.js +136 -0
  377. package/dist/src/core/log-reconcile.js.map +1 -0
  378. package/dist/src/core/log-subscribe.d.ts +36 -0
  379. package/dist/src/core/log-subscribe.js +162 -0
  380. package/dist/src/core/log-subscribe.js.map +1 -0
  381. package/dist/src/core/log.d.ts +278 -0
  382. package/dist/src/core/log.js +546 -0
  383. package/dist/src/core/log.js.map +1 -0
  384. package/dist/src/core/loop.d.ts +274 -0
  385. package/dist/src/core/loop.js +487 -0
  386. package/dist/src/core/loop.js.map +1 -0
  387. package/dist/src/core/md-fence.d.ts +41 -0
  388. package/dist/src/core/md-fence.js +74 -0
  389. package/dist/src/core/md-fence.js.map +1 -0
  390. package/dist/src/core/money.d.ts +147 -0
  391. package/dist/src/core/money.js +195 -0
  392. package/dist/src/core/money.js.map +1 -0
  393. package/dist/src/core/payload-census.d.ts +74 -0
  394. package/dist/src/core/payload-census.js +146 -0
  395. package/dist/src/core/payload-census.js.map +1 -0
  396. package/dist/src/core/payload-store.d.ts +175 -0
  397. package/dist/src/core/payload-store.js +340 -0
  398. package/dist/src/core/payload-store.js.map +1 -0
  399. package/dist/src/core/payload.d.ts +71 -0
  400. package/dist/src/core/payload.js +80 -0
  401. package/dist/src/core/payload.js.map +1 -0
  402. package/dist/src/core/policy-diff.d.ts +292 -0
  403. package/dist/src/core/policy-diff.js +588 -0
  404. package/dist/src/core/policy-diff.js.map +1 -0
  405. package/dist/src/core/policy-expectations.d.ts +199 -0
  406. package/dist/src/core/policy-expectations.js +394 -0
  407. package/dist/src/core/policy-expectations.js.map +1 -0
  408. package/dist/src/core/policy-explain.d.ts +150 -0
  409. package/dist/src/core/policy-explain.js +258 -0
  410. package/dist/src/core/policy-explain.js.map +1 -0
  411. package/dist/src/core/policy-load.d.ts +527 -0
  412. package/dist/src/core/policy-load.js +536 -0
  413. package/dist/src/core/policy-load.js.map +1 -0
  414. package/dist/src/core/policy-match.d.ts +281 -0
  415. package/dist/src/core/policy-match.js +478 -0
  416. package/dist/src/core/policy-match.js.map +1 -0
  417. package/dist/src/core/policy-proposal.d.ts +265 -0
  418. package/dist/src/core/policy-proposal.js +458 -0
  419. package/dist/src/core/policy-proposal.js.map +1 -0
  420. package/dist/src/core/prompt-layout.d.ts +221 -0
  421. package/dist/src/core/prompt-layout.js +422 -0
  422. package/dist/src/core/prompt-layout.js.map +1 -0
  423. package/dist/src/core/protected-path-guard.d.ts +453 -0
  424. package/dist/src/core/protected-path-guard.js +1566 -0
  425. package/dist/src/core/protected-path-guard.js.map +1 -0
  426. package/dist/src/core/registration.d.ts +25 -0
  427. package/dist/src/core/registration.js +39 -0
  428. package/dist/src/core/registration.js.map +1 -0
  429. package/dist/src/core/reindex.d.ts +99 -0
  430. package/dist/src/core/reindex.js +336 -0
  431. package/dist/src/core/reindex.js.map +1 -0
  432. package/dist/src/core/sampler.d.ts +313 -0
  433. package/dist/src/core/sampler.js +388 -0
  434. package/dist/src/core/sampler.js.map +1 -0
  435. package/dist/src/core/sandbox.d.ts +290 -0
  436. package/dist/src/core/sandbox.js +424 -0
  437. package/dist/src/core/sandbox.js.map +1 -0
  438. package/dist/src/core/seal.d.ts +165 -0
  439. package/dist/src/core/seal.js +290 -0
  440. package/dist/src/core/seal.js.map +1 -0
  441. package/dist/src/core/state.d.ts +505 -0
  442. package/dist/src/core/state.js +1009 -0
  443. package/dist/src/core/state.js.map +1 -0
  444. package/dist/src/core/task-file.d.ts +185 -0
  445. package/dist/src/core/task-file.js +464 -0
  446. package/dist/src/core/task-file.js.map +1 -0
  447. package/dist/src/core/telegram-config.d.ts +93 -0
  448. package/dist/src/core/telegram-config.js +114 -0
  449. package/dist/src/core/telegram-config.js.map +1 -0
  450. package/dist/src/core/token.d.ts +409 -0
  451. package/dist/src/core/token.js +561 -0
  452. package/dist/src/core/token.js.map +1 -0
  453. package/dist/src/core/validate.d.ts +138 -0
  454. package/dist/src/core/validate.js +0 -0
  455. package/dist/src/core/validate.js.map +1 -0
  456. package/dist/src/core/values.d.ts +137 -0
  457. package/dist/src/core/values.js +153 -0
  458. package/dist/src/core/values.js.map +1 -0
  459. package/dist/src/core/vault.d.ts +291 -0
  460. package/dist/src/core/vault.js +612 -0
  461. package/dist/src/core/vault.js.map +1 -0
  462. package/dist/src/core/verified-snapshot.d.ts +204 -0
  463. package/dist/src/core/verified-snapshot.js +506 -0
  464. package/dist/src/core/verified-snapshot.js.map +1 -0
  465. package/dist/src/core/verify.d.ts +336 -0
  466. package/dist/src/core/verify.js +549 -0
  467. package/dist/src/core/verify.js.map +1 -0
  468. package/dist/src/core/version.d.ts +8 -0
  469. package/dist/src/core/version.js +9 -0
  470. package/dist/src/core/version.js.map +1 -0
  471. package/dist/src/core/wysiwys.d.ts +370 -0
  472. package/dist/src/core/wysiwys.js +728 -0
  473. package/dist/src/core/wysiwys.js.map +1 -0
  474. package/dist/src/daemon/advance-child.d.ts +39 -0
  475. package/dist/src/daemon/advance-child.js +78 -0
  476. package/dist/src/daemon/advance-child.js.map +1 -0
  477. package/dist/src/daemon/advance.d.ts +466 -0
  478. package/dist/src/daemon/advance.js +849 -0
  479. package/dist/src/daemon/advance.js.map +1 -0
  480. package/dist/src/daemon/audit.d.ts +87 -0
  481. package/dist/src/daemon/audit.js +90 -0
  482. package/dist/src/daemon/audit.js.map +1 -0
  483. package/dist/src/daemon/daemon.d.ts +1180 -0
  484. package/dist/src/daemon/daemon.js +1988 -0
  485. package/dist/src/daemon/daemon.js.map +1 -0
  486. package/dist/src/daemon/dark-session.d.ts +64 -0
  487. package/dist/src/daemon/dark-session.js +119 -0
  488. package/dist/src/daemon/dark-session.js.map +1 -0
  489. package/dist/src/daemon/draw-child.d.ts +36 -0
  490. package/dist/src/daemon/draw-child.js +132 -0
  491. package/dist/src/daemon/draw-child.js.map +1 -0
  492. package/dist/src/daemon/draw.d.ts +154 -0
  493. package/dist/src/daemon/draw.js +458 -0
  494. package/dist/src/daemon/draw.js.map +1 -0
  495. package/dist/src/daemon/git-evidence.d.ts +173 -0
  496. package/dist/src/daemon/git-evidence.js +345 -0
  497. package/dist/src/daemon/git-evidence.js.map +1 -0
  498. package/dist/src/daemon/projection.d.ts +180 -0
  499. package/dist/src/daemon/projection.js +233 -0
  500. package/dist/src/daemon/projection.js.map +1 -0
  501. package/dist/src/daemon/prune.d.ts +207 -0
  502. package/dist/src/daemon/prune.js +376 -0
  503. package/dist/src/daemon/prune.js.map +1 -0
  504. package/dist/src/mcp/http.d.ts +113 -0
  505. package/dist/src/mcp/http.js +343 -0
  506. package/dist/src/mcp/http.js.map +1 -0
  507. package/dist/src/mcp/server.d.ts +265 -0
  508. package/dist/src/mcp/server.js +602 -0
  509. package/dist/src/mcp/server.js.map +1 -0
  510. package/docs/adapter-api.md +106 -0
  511. package/docs/cli-reference.md +5716 -0
  512. package/docs/codex-enforced-session.md +30 -0
  513. package/package.json +53 -4
  514. package/schema/.gitkeep +0 -0
  515. package/schema/LICENSE +117 -0
  516. package/schema/codex-instance.schema.json +82 -0
  517. package/schema/envelope.schema.json +137 -0
  518. package/schema/event.schema.json +1811 -0
  519. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  520. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  521. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  522. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  523. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  524. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  525. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  526. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  527. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  528. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  529. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  530. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  531. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  532. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  533. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  534. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  535. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  536. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  537. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  538. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  539. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  540. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  541. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  542. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  543. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  544. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  545. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  546. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  547. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  548. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  549. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  550. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  551. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  552. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  553. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  554. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  555. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  556. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  557. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  558. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  559. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  560. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  561. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  562. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  563. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  564. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  565. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  566. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  567. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  568. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  569. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  570. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  571. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  572. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  573. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  574. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  575. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  576. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  577. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  578. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  579. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  580. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  581. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  582. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  583. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  584. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  585. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  586. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  587. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  588. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  589. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  590. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  591. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  592. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  593. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  594. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  595. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  596. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  597. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  598. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  599. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  600. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  601. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  602. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  603. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  604. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  605. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  606. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  607. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  608. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  609. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  610. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  611. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  612. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  613. package/schema/fixtures/event/valid/execution-started.json +14 -0
  614. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  615. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  616. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  617. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  618. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  619. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  620. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  621. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  622. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  623. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  624. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  625. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  626. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  627. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  628. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  629. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  630. package/schema/fixtures/event/valid/spec-example.json +15 -0
  631. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  632. package/schema/fixtures/event/valid/task-registered.json +14 -0
  633. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  634. package/schema/fixtures/hash/known-answer.json +74 -0
  635. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  636. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  637. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  638. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  639. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  640. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  641. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  642. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  643. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  644. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  645. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  646. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  647. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  648. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  649. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  650. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  651. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  652. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  653. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  654. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  655. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  656. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  657. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  658. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  659. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  660. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  661. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  662. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  663. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  664. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  665. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  666. package/schema/fixtures/policy/valid/canonical.json +47 -0
  667. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  668. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  669. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  670. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  671. package/schema/fixtures/policy/valid/human-only.json +9 -0
  672. package/schema/fixtures/policy/valid/minimal.json +6 -0
  673. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  674. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  675. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  676. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  677. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  678. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  679. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  680. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  681. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  682. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  683. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  684. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  685. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  686. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  687. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  688. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  689. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  690. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  691. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  692. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  693. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  694. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  695. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  696. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  697. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  698. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  699. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  700. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  701. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  702. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  703. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  704. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  705. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  706. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  707. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  708. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  709. package/schema/fixtures/values/invalid/version-string.json +1 -0
  710. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  711. package/schema/fixtures/values/valid/full.json +20 -0
  712. package/schema/fixtures/values/valid/minimal.json +1 -0
  713. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  714. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  715. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  716. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  717. package/schema/fixtures/values-md/valid/absent.md +50 -0
  718. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  719. package/schema/policy.schema.json +501 -0
  720. package/schema/sample-record.schema.json +26 -0
  721. package/schema/values.schema.json +55 -0
  722. package/templates/codex/README.md +9 -0
@@ -0,0 +1,1988 @@
1
+ /**
2
+ * `approvald` — the daemon loop of SPEC.md §10.2 (APRV-39).
3
+ *
4
+ * > "`approvald` watches the backlog folder and the log: validates new/changed
5
+ * > envelopes, applies policy, dispatches channel notifications, expires TTLs,
6
+ * > samples supervised actions for audit, re-renders projections, and
7
+ * > (optionally) polls upstream sources."
8
+ *
9
+ * This module is the loop's core: watch, envelope drift, TTL sweep, projection
10
+ * write-back, queue regeneration, and loop-escalation surfacing. Channel dispatch
11
+ * belongs to the channel verbs (APRV-23/25/26), audit sampling to APRV-40, and
12
+ * payload-retention pruning to APRV-41; each is its own task and none of them is
13
+ * smuggled in here.
14
+ *
15
+ * ## Drift, then repair (SPEC.md §6.3, §10.2, APRV-62)
16
+ *
17
+ * A task file's `state:` is a projection and the log is the truth, so the two
18
+ * halves of that sentence are two steps of one tick. The drift scan runs first
19
+ * and appends `envelope.drift` for every file whose claim the log contradicts;
20
+ * the write-back pass runs after every append this tick could make and rewrites
21
+ * those files through `core/task-file.ts` so the projection matches the log
22
+ * again. Write-back never appends and never precedes an append: it only copies a
23
+ * fact the log already carries into a file that disagreed with it.
24
+ *
25
+ * So a drift record marks the moment a file was found wrong **and fixed**, not a
26
+ * standing disagreement. That reading is what makes the records worth watching:
27
+ * a file that keeps drifting after repair is a file some other writer is fighting
28
+ * the daemon over, and the repeated records are how an operator sees it. One
29
+ * record per transition is the healthy shape (the log moved, the file caught up);
30
+ * a run of identical records against an unmoving log is not.
31
+ *
32
+ * ## It decides nothing of its own
33
+ *
34
+ * Every judgement the daemon makes is one some other module already owns:
35
+ *
36
+ * - approval state per action is `core/state.ts`'s `requestState`, rolled up to
37
+ * the task by `daemon/projection.ts` and never re-derived;
38
+ * - expiry is `core/gate.ts`'s `expire`, the system verb, which re-reads the log,
39
+ * re-judges the TTL, and refuses anything that is not a live lapsed request;
40
+ * - the queue is `channels/render-queue.ts`'s `writeQueue`, the same renderer
41
+ * `approval render` calls, writing the same file the same atomic way;
42
+ * - loop escalation is `core/loop.ts`'s projection, which the gate and the
43
+ * executor already enforce. The daemon **surfaces** it and enforces nothing.
44
+ *
45
+ * What is new here is scheduling: when to look, how often, and how not to append
46
+ * the same fact twice.
47
+ *
48
+ * ## Watching, and why correctness never depends on it
49
+ *
50
+ * `fs.watch` is bursty, coalescing, and platform-dependent: one editor save can
51
+ * produce three events or one, a rename can arrive as a delete plus a create,
52
+ * and on some filesystems nothing arrives at all. So the watcher is treated as a
53
+ * **latency optimization and nothing else**. Every tick re-scans the task folder
54
+ * and re-derives everything from the verified log, and a periodic tick runs on
55
+ * `intervalMs` whether or not any watcher ever fires. A daemon whose watchers all
56
+ * failed to attach is a slower daemon, never a wrong one — which is also what
57
+ * makes the behavior testable without depending on any platform's watch
58
+ * semantics.
59
+ *
60
+ * Watch events are debounced (`debounceMs`) so a burst collapses into one tick.
61
+ * Ticks are synchronous end to end, so they cannot interleave.
62
+ *
63
+ * Which of those events are acted on, and which are dropped as this loop's own
64
+ * hand or as bookkeeping nobody reads, is {@link Daemon.attachWatchers}.
65
+ * `traceWatch` prints the decision for every event, and the `tick` line's
66
+ * `woke_by` names the one that opened the window it closed (APRV-230): a tick is
67
+ * the daemon's unit of work, and one nobody asked for costs exactly as much as
68
+ * one somebody did.
69
+ *
70
+ * ## Single writer, in intent only
71
+ *
72
+ * CLAUDE.md's rule is that the daemon is the sole writer while it runs. That is
73
+ * an operational stance, not a lock this module takes: the CLI verbs remain
74
+ * appendable at any moment, `core/log.ts`'s advisory lockfile serializes the
75
+ * writes, and every append here passes `expectedHead` so a check made against one
76
+ * log cannot land on another (compare-and-append, SPEC.md §11.1 invariant 5).
77
+ *
78
+ * The daemon therefore **tolerates external appends by re-reading**: a
79
+ * `head-moved` refusal is reported and dropped, never retried in place, because
80
+ * the next tick re-derives the whole question from the log as it now is. It holds
81
+ * no lock of its own and leaves no lockfile behind — the only lockfile in this
82
+ * system is the one `appendEvent` creates and releases inside a single call.
83
+ *
84
+ * ## Fail closed, loudly
85
+ *
86
+ * A log that does not verify stops the daemon rather than degrading it. Nothing
87
+ * may be appended onto a chain that does not verify, projections built from one
88
+ * would be screenshots of something nobody should read, and a daemon that kept
89
+ * running while reporting corruption would train an operator to ignore it.
90
+ */
91
+ import { watch } from "node:fs";
92
+ import { readFileSync, readdirSync, statSync } from "node:fs";
93
+ import { basename, dirname, isAbsolute, join, relative } from "node:path";
94
+ import { writeQueue } from "../channels/render-queue.js";
95
+ import { tick as readClock } from "../core/clock.js";
96
+ import { parseFrontmatter, readTaskFile } from "../core/frontmatter.js";
97
+ import { expire } from "../core/gate.js";
98
+ import { appendEvent } from "../core/log.js";
99
+ import { loopEscalation } from "../core/loop.js";
100
+ import { payloadHash } from "../core/payload.js";
101
+ import { loadPolicy } from "../core/policy-load.js";
102
+ import { rewriteTaskFile, writeTaskFileAtomic } from "../core/task-file.js";
103
+ import { processReadCache, readVerifiedRecords, } from "../core/state.js";
104
+ import { validate } from "../core/validate.js";
105
+ import { publishedState } from "../cli/log-advance.js";
106
+ import { isAdvanceBookkeeping } from "../core/advance-cycle.js";
107
+ import { repoRoot } from "../cli/git-scope.js";
108
+ import { checkLogAnchor, resolveAnchor } from "../cli/log-anchor.js";
109
+ import { checkLogCheckpoints, checkpointPolicyOf, } from "../core/checkpoint.js";
110
+ import { authorizeAdvance, proveDanglingAdvancesFor, runAdvanceAsync, runAdvanceSync, settleAdvanceFinish, sweepDanglingAdvances, sweepIsClear, unreconciledRefusal, } from "./advance.js";
111
+ import { sweepAuditSampling } from "./audit.js";
112
+ import { sweepDarkSessions, } from "./dark-session.js";
113
+ import { prunePayloads } from "./prune.js";
114
+ import { driftAlreadyLogged, lapsedRequests, latestRegistration, taskEnvelopeState, } from "./projection.js";
115
+ /**
116
+ * SPEC.md §8: runtime-originated events carry a `system:` actor. The daemon is
117
+ * the runtime, and `envelope.drift` is its own event — distinct from
118
+ * `system:gate`, which `core/gate.ts` stamps on the expiries it appends, so a
119
+ * reader can tell which part of the runtime spoke.
120
+ */
121
+ export const DAEMON_ACTOR = "system:daemon";
122
+ /** Backlog.md's conventional task folder, relative to the working directory. */
123
+ export { DEFAULT_TASKS_DIR } from "../core/registration.js";
124
+ /** How often the daemon looks, absent any watcher event. */
125
+ export const DEFAULT_INTERVAL_MS = 30_000;
126
+ /** How long a burst of watcher events is allowed to settle before a tick. */
127
+ export const DEFAULT_DEBOUNCE_MS = 250;
128
+ /**
129
+ * Why a watcher event did not schedule a tick (APRV-230).
130
+ *
131
+ * Machine-readable and a closed union, for the reason {@link
132
+ * DAEMON_WARNING_CODES} is: an operator counting phantom ticks groups the trace
133
+ * by this field, and a free-text sentence would make that a grep instead of a
134
+ * count.
135
+ */
136
+ export const WATCH_IGNORE_REASONS = [
137
+ /** A task file this daemon's own write-back pass placed (APRV-211). */
138
+ "self-write",
139
+ /** The `.<name>.tmp-<pid>-<n>` file this process writes a task file through. */
140
+ "own-temp",
141
+ /** A bookkeeping file: see {@link bookkeepingKind}. */
142
+ "bookkeeping",
143
+ /** Something in the log's directory that is not the log (the snapshot). */
144
+ "not-the-log",
145
+ ];
146
+ /**
147
+ * What kind of bookkeeping file `name` is, or `null` when it is not one.
148
+ *
149
+ * The files that live in these two directories without ever being their
150
+ * subject: the append lockfile every writer in this runtime creates and removes
151
+ * (`<log>.lock`, `core/log.ts`), the swap, autosave, backup and lock files an
152
+ * editor scatters beside a file it is editing, and macOS's own `.DS_Store` and
153
+ * AppleDouble residue. Each of these is a filesystem event about how a change
154
+ * was made and never about the change; a tick scheduled for one re-derives an
155
+ * answer nothing has moved (APRV-230, and the lockfile pair is two events per
156
+ * append from every writer on the machine).
157
+ *
158
+ * Safe for exactly the reason {@link Daemon.attachWatchers} documents: the
159
+ * watcher is a latency optimization and correctness never depended on it
160
+ * (SPEC.md §10.2), so the worst an over-eager name here can cost is one
161
+ * `--interval` of latency. Nothing in this list can name a Backlog.md task file,
162
+ * which is `<id> - <slug>.md`.
163
+ */
164
+ export function bookkeepingKind(name) {
165
+ // The append lockfile, and any other `.lock` a tool leaves in these folders.
166
+ if (name.endsWith(".lock"))
167
+ return "lockfile";
168
+ // vim: `.task-042.md.swp`, `.task-042.md.swo`, …
169
+ if (/\.sw[a-p]$/u.test(name))
170
+ return "swap";
171
+ // emacs: `#task-042.md#` (autosave) and `.#task-042.md` (the lock symlink).
172
+ if (/^#.+#$/u.test(name) || name.startsWith(".#"))
173
+ return "editor-lock";
174
+ // Backups: `task-042.md~`, and JetBrains' write-through temporaries.
175
+ if (name.endsWith("~") || /___jb_(?:tmp|old)___$/u.test(name))
176
+ return "backup";
177
+ // macOS: the Finder's own index, and AppleDouble sidecars.
178
+ if (name === ".DS_Store" || name.startsWith("._"))
179
+ return "macos";
180
+ return null;
181
+ }
182
+ /**
183
+ * Why the daemon complained without stopping. Machine-readable and distinct, per
184
+ * SPEC.md §11.1 invariant 6, and a closed union for the same reason the gate's
185
+ * refusal codes are: a supervisor that branches on them needs them stable.
186
+ */
187
+ export const DAEMON_WARNING_CODES = [
188
+ /** A task file could not be read (permissions, a vanished file). */
189
+ "task-unreadable",
190
+ /** A task file's frontmatter does not parse. */
191
+ "frontmatter-invalid",
192
+ /** The `approval:` envelope failed `envelope.schema.json`. */
193
+ "envelope-invalid",
194
+ /** The frontmatter carries no usable `id`, so no drift can be keyed to it. */
195
+ "task-id-missing",
196
+ /** The task folder could not be listed; the next tick tries again. */
197
+ "tasks-dir-unreadable",
198
+ /** An append was refused. The next tick re-derives and may try again. */
199
+ "append-refused",
200
+ /** The TTL sweep's `expire` refused for a reason other than a race. */
201
+ "expire-refused",
202
+ /** The queue could not be written. The log is untouched. */
203
+ "render-failed",
204
+ /** A watcher could not attach; the periodic tick covers the folder anyway. */
205
+ "watch-unavailable",
206
+ /**
207
+ * A payload-retention prune did not complete (APRV-41). The store keeps the
208
+ * file, and the next tick re-derives; nothing is ever deleted unlogged.
209
+ */
210
+ "prune-refused",
211
+ /**
212
+ * The projection write-back was refused by the writer, or the rewritten bytes
213
+ * could not be placed (APRV-62). The file is left exactly as it was and the
214
+ * log is untouched; the message carries `core/task-file.ts`'s own code.
215
+ */
216
+ "write-back-refused",
217
+ /**
218
+ * A cadence advance did not publish (APRV-204): the gate sent it to a human,
219
+ * refused it, or the verb itself failed. Nothing was committed, the outcome
220
+ * is on the `advance` line beside this warning, and the next tick tries
221
+ * again — the cadence interval is the retry bound, so there is no hot loop.
222
+ */
223
+ "advance-refused",
224
+ /**
225
+ * A dark-session sweep (APRV-192) found git activity it could not judge, or
226
+ * could not append the observation it did reach. Uncertainty is reported as
227
+ * uncertainty and never as a pass; nothing is escalated on it, because a
228
+ * detector reports and the gate decides.
229
+ */
230
+ "dark-session-undetermined",
231
+ /**
232
+ * The working log is a strict PREFIX of its committed copy (APRV-219): the
233
+ * anchor carries records this file does not. Not a divergence — the two are
234
+ * one chain and this checkout is behind it — and not silence either, because
235
+ * an append onto the shorter chain forks it. `approval log sync` is the
236
+ * repair, and the next tick asks again.
237
+ */
238
+ "anchor-behind",
239
+ /**
240
+ * `audit.checkpoint_every` says a human-signed checkpoint is due and the log
241
+ * carries none that recent (APRV-220). A WARNING and never a stop, at every
242
+ * layer: a human who has been away is not a forger, and a daemon that stopped
243
+ * for want of a tap is a daemon whose operator turns the check off.
244
+ */
245
+ "checkpoint-due",
246
+ /**
247
+ * The live-draw socket could not be served (APRV-208). Nothing is degraded:
248
+ * every asker fails closed to a human decision, which is exactly what happens
249
+ * on a machine where no daemon runs at all. It is a warning rather than a
250
+ * silence because the operator's `supervised-live` classes are gating at 100%
251
+ * while it stands, and that is a thing to know rather than to discover from a
252
+ * month of taps.
253
+ */
254
+ "draw-unavailable",
255
+ ];
256
+ function errorMessage(cause) {
257
+ return cause instanceof Error ? cause.message : String(cause);
258
+ }
259
+ /**
260
+ * The Backlog.md board key a task file's name begins with (`task-3 - Slug.md`).
261
+ *
262
+ * Used for one question only (APRV-63): a file that has lost its frontmatter
263
+ * entirely leaves no id anywhere, so the name is the only handle left with
264
+ * which to ask the log whether this task ever registered anything. The answer,
265
+ * and the id every record is written under, come from the log.
266
+ */
267
+ function taskIdFromFileName(path) {
268
+ const match = /^([A-Za-z][A-Za-z0-9_]*-\d+)/u.exec(basename(path));
269
+ return match?.[1] ?? null;
270
+ }
271
+ /**
272
+ * The daemon.
273
+ *
274
+ * One instance owns one working set (a log, a task folder, a queue file) and one
275
+ * set of timers. {@link run} resolves when the loop stops; {@link stop} is what a
276
+ * signal handler calls.
277
+ */
278
+ export class Daemon {
279
+ options;
280
+ watchers = [];
281
+ watchAttempted = false;
282
+ watching = false;
283
+ interval = null;
284
+ debounce = null;
285
+ ticking = false;
286
+ ticks = 0;
287
+ drifts = 0;
288
+ expiries = 0;
289
+ renders = 0;
290
+ lastRender = null;
291
+ /** Epoch ms of the last advance ATTEMPT, refusals included (APRV-204). */
292
+ lastAdvanceAt = null;
293
+ lastAdvance = null;
294
+ /** How many substantive records were owed at the last attempt. */
295
+ lastAdvanceOwed = null;
296
+ /**
297
+ * Where the owed span ENDED at the last attempt (APRV-233).
298
+ *
299
+ * The count trigger measures against this inside the interval, so records an
300
+ * attempt has already tried to publish are not counted a second time towards
301
+ * publishing them again.
302
+ */
303
+ lastAdvanceSpanEnd = null;
304
+ /**
305
+ * The advance whose git work is still running in a child (APRV-211).
306
+ *
307
+ * One slot, and a tick that finds it taken makes no attempt at all: two
308
+ * advances against one log would race for the append lock and for the records
309
+ * branch, and the second would have nothing to publish anyway.
310
+ */
311
+ advanceInFlight = null;
312
+ /**
313
+ * An advance outcome this process observed and could not record (APRV-233).
314
+ *
315
+ * The 2026-09-02 residue: a hook's record landed between `recordFinish`'s
316
+ * read and its append, the bounded retry was spent, and the execution stayed
317
+ * open. The outcome is a fact this process holds and the log does not, so it
318
+ * is carried here and settled at the top of the next tick, before any trigger
319
+ * is looked at. Nothing else may advance while it stands.
320
+ */
321
+ pendingAdvanceFinish = null;
322
+ /**
323
+ * The dangling advance cycles this process has already reported (APRV-264).
324
+ *
325
+ * A SET rather than one key: on 2026-09-05 five of them stood at once, and a
326
+ * single slot meant the operator was told about one, then about the next only
327
+ * after they had closed the first by hand. Reported once per key — the
328
+ * started line counts as the report for anything the startup listing found,
329
+ * so the first tick does not say it a second time — and never once per tick,
330
+ * because a warning printed every thirty seconds forever stops being read.
331
+ */
332
+ reportedDangling = new Set();
333
+ /** Epoch ms of the last dark-session sweep (APRV-192); `null` before the first. */
334
+ lastDarkSweepAt = null;
335
+ reportedEscalations = new Set();
336
+ /** Verified reads made during the current tick (APRV-211). Reset at tick start. */
337
+ reads = 0;
338
+ /** Did any of this tick's own reads hash the whole prefix (APRV-217)? */
339
+ fullReproofThisTick = false;
340
+ /** This tick's anchor comparison (APRV-219), or `null` when it made none. */
341
+ anchorThisTick = null;
342
+ /** This tick's checkpoint check (APRV-220), or `null` when it made none. */
343
+ checkpointsThisTick = null;
344
+ /**
345
+ * The watcher event that opened the current debounce window (APRV-230), or
346
+ * `null` when nothing has woken this daemon since the last tick consumed it.
347
+ * Read once at the top of a tick and cleared there, so a periodic tick that
348
+ * happens to run first takes the attribution with the work.
349
+ */
350
+ pendingWake = null;
351
+ /** Basenames {@link writeBack} placed this tick, so the watcher can ignore them. */
352
+ selfWrites = new Set();
353
+ /** The previous tick's, kept one generation: watch events arrive after the write. */
354
+ previousSelfWrites = new Set();
355
+ settle = null;
356
+ finished = false;
357
+ /** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
358
+ drawServing = false;
359
+ constructor(options) {
360
+ this.options = options;
361
+ }
362
+ /** Run until stopped (or, with `once`, for exactly one tick). */
363
+ run() {
364
+ return new Promise((resolve) => {
365
+ this.settle = resolve;
366
+ // The cadence clock starts when the daemon does (APRV-204), so a restart
367
+ // does not publish a records branch the moment it comes up: a daemon
368
+ // restarted in a loop would otherwise open a pull request per restart.
369
+ // The RECORD-COUNT trigger is unaffected, which is what keeps a busy
370
+ // repository from waiting out an interval it does not need to.
371
+ const started = Date.parse(readClock(this.clockOptions()));
372
+ this.lastAdvanceAt = Number.isNaN(started) ? 0 : started;
373
+ if (!this.options.once)
374
+ this.attachWatchers();
375
+ // APRV-208. Before the `started` line, so that line can say truthfully
376
+ // whether this run is answering draws. A refusal is reported and the loop
377
+ // continues: askers fail closed to a human, which is where they were
378
+ // going with no daemon at all.
379
+ let drawPath = null;
380
+ if (this.options.draw !== undefined) {
381
+ const served = this.options.draw.start();
382
+ if (served.ok) {
383
+ drawPath = served.path;
384
+ this.drawServing = true;
385
+ }
386
+ else {
387
+ this.warn("draw-unavailable", `live draws are not being served (${served.reason}): ${served.detail}. Every supervised-live action gates to a human until this is fixed.`);
388
+ }
389
+ }
390
+ this.emit({
391
+ event: "started",
392
+ log: this.display(this.options.logPath),
393
+ tasks: this.display(this.options.tasksDir),
394
+ queue: this.display(this.options.queuePath),
395
+ interval_ms: this.options.intervalMs,
396
+ debounce_ms: this.options.debounceMs,
397
+ watching: this.watching,
398
+ read_proof: this.options.readProof?.mode ?? "full",
399
+ // APRV-219. RESOLVED here and COMPARED by the first tick, which runs a
400
+ // line below and always re-proves in full: the started line's job is to
401
+ // name the witness this run holds itself to, and resolving it costs two
402
+ // git reads and no log read at all.
403
+ anchor: this.resolveAnchorForReport(),
404
+ draw: drawPath,
405
+ // APRV-264. LISTED here, SWEPT by the first tick, which runs a line
406
+ // below: the started line's job is to name what is blocking the
407
+ // cadence, and listing costs one verified read and one git read where
408
+ // appending five outcome records before the process has said hello
409
+ // would be a write nobody asked for yet. Seeded into `reportedDangling`
410
+ // so the tick that immediately follows does not repeat it.
411
+ dangling_advances: this.listDanglingAdvancesAtStartup(),
412
+ });
413
+ const outcome = this.tick();
414
+ if (outcome !== null) {
415
+ this.finish(outcome);
416
+ return;
417
+ }
418
+ if (this.options.once) {
419
+ this.finish({ kind: "stopped", reason: "once" });
420
+ return;
421
+ }
422
+ this.interval = setInterval(() => {
423
+ const periodic = this.tick();
424
+ if (periodic !== null)
425
+ this.finish(periodic);
426
+ }, this.options.intervalMs);
427
+ });
428
+ }
429
+ /** Stop cleanly: timers cleared, watchers closed, nothing half-written. */
430
+ stop(reason) {
431
+ this.finish({ kind: "stopped", reason });
432
+ }
433
+ // -------------------------------------------------------------------------
434
+ // Scheduling
435
+ // -------------------------------------------------------------------------
436
+ finish(outcome) {
437
+ if (this.finished)
438
+ return;
439
+ this.finished = true;
440
+ if (this.interval !== null)
441
+ clearInterval(this.interval);
442
+ this.interval = null;
443
+ if (this.debounce !== null)
444
+ clearTimeout(this.debounce);
445
+ this.debounce = null;
446
+ for (const watcher of this.watchers) {
447
+ try {
448
+ watcher.close();
449
+ }
450
+ catch {
451
+ // A watcher that is already closed, or whose directory vanished, has
452
+ // nothing left to release. Shutdown must not fail on it.
453
+ }
454
+ }
455
+ this.watchers.length = 0;
456
+ // APRV-208. The socket goes with the process that served it: a socket file
457
+ // outliving its server is a hook connecting to nothing, which is a slower
458
+ // road to the same gated verdict but a confusing one. Closed before the
459
+ // shutdown flush, because nothing in the flush answers draws.
460
+ if (this.drawServing && this.options.draw !== undefined) {
461
+ this.drawServing = false;
462
+ try {
463
+ this.options.draw.close();
464
+ }
465
+ catch {
466
+ // A server that will not close cannot stop the daemon from stopping.
467
+ }
468
+ }
469
+ // The shutdown flush (APRV-204). A clean stop with unpublished records
470
+ // publishes them before it goes: the daemon is the log's writer, and a
471
+ // writer that exits leaving hours of records on nobody's branch hands the
472
+ // problem back to whoever has to remember. Only on a CLEAN stop — the three
473
+ // failure outcomes are all "this log is not fit to be committed from", and
474
+ // the advance would refuse them a second time anyway.
475
+ //
476
+ // Synchronous, inside the shutdown path, before the `stopped` line: the
477
+ // advance is `spawnSync` throughout, so there is nothing to await and
478
+ // nothing that could outlive the process.
479
+ if (outcome.kind === "stopped") {
480
+ // An advance whose child is still running (APRV-211). The flush below
481
+ // declines to start a second one, and this says so rather than letting a
482
+ // dangling `execution.started` be discovered later by a reader of the log:
483
+ // the child settles and the execution is closed if this process outlives
484
+ // it, and does not if it does not.
485
+ if (this.advanceInFlight !== null) {
486
+ this.warn("advance-refused", "an advance was still running in a child when the daemon stopped; its outcome is recorded only if this process outlives it, and `approval status` shows the execution as open until then");
487
+ }
488
+ try {
489
+ this.advanceIfDue(true);
490
+ }
491
+ catch (cause) {
492
+ // A flush that throws must not stop the daemon from stopping.
493
+ this.warn("advance-refused", `the shutdown flush threw: ${errorMessage(cause)}`);
494
+ }
495
+ }
496
+ this.emit({
497
+ event: "stopped",
498
+ reason: outcome.kind === "stopped" ? outcome.reason : outcome.kind,
499
+ ticks: this.ticks,
500
+ drift: this.drifts,
501
+ expired: this.expiries,
502
+ renders: this.renders,
503
+ });
504
+ const settle = this.settle;
505
+ this.settle = null;
506
+ if (settle !== null)
507
+ settle(outcome);
508
+ }
509
+ /**
510
+ * Attach watchers to the task folder and the log's directory.
511
+ *
512
+ * The log's *directory* rather than the log file: an append to a file is
513
+ * observable either way, but a log that does not exist yet cannot be watched at
514
+ * all, and a rename (which `writeQueue` and any future rotation perform) leaves
515
+ * a file watcher pointed at an inode nobody writes to again.
516
+ *
517
+ * Failure is a warning, never fatal — see the module header on why the periodic
518
+ * tick makes watching optional.
519
+ *
520
+ * ## Ignoring the daemon's own hand (APRV-211)
521
+ *
522
+ * Two of the files in these directories are written by this loop itself: the
523
+ * verified-head snapshot beside the log (`verified-head.json` and its temp
524
+ * file, published on every clean read) and the task files {@link writeBack}
525
+ * repairs. A watcher that fires on those schedules a tick whose only cause was
526
+ * the previous tick, and the daemon wakes itself forever: measured at 18 ticks
527
+ * in 45 seconds against a ten-minute interval, with no other writer.
528
+ *
529
+ * So the log-directory watcher schedules only for the log file itself (or for
530
+ * an event that names no file, which is the platform saying "something here
531
+ * changed" and must still be believed), and the tasks watcher ignores the
532
+ * basenames this daemon just placed.
533
+ *
534
+ * ## Bookkeeping, nobody's hand (APRV-230)
535
+ *
536
+ * The self-wake was not the whole of it. Both directories also carry files
537
+ * that no reader ever reads: the append lockfile every writer in this runtime
538
+ * creates and removes around each append, and the swap, autosave and backup
539
+ * files an editor scatters beside a task file. Those are events about how a
540
+ * change was made, and a tick scheduled for one re-derives an answer nothing
541
+ * has moved. {@link bookkeepingKind} names them and they are dropped here, in
542
+ * both directories.
543
+ *
544
+ * What is left unattributed on purpose is a platform event that names no file.
545
+ * That is the platform saying "something in this directory changed" and
546
+ * declining to say what, and the log itself is one of the things it might
547
+ * have been; believing it costs a tick and doubting it could cost an append's
548
+ * latency. {@link DaemonOptions.traceWatch} makes every one of these decisions
549
+ * visible, which is how the remaining wake sources get counted rather than
550
+ * guessed at.
551
+ *
552
+ * This is safe for exactly the reason stated in the module header: correctness
553
+ * never depended on the watcher. Every tick re-scans the folder and re-derives
554
+ * everything from the verified log, and the periodic tick runs regardless
555
+ * (SPEC.md §10.2). The worst an over-eager filter can cost is latency on a
556
+ * change that arrives inside the same window as one of the daemon's own
557
+ * writes, and the next periodic tick collects it.
558
+ */
559
+ attachWatchers() {
560
+ if (this.watchAttempted)
561
+ return;
562
+ this.watchAttempted = true;
563
+ const logName = basename(this.options.logPath);
564
+ const ownTempFile = new RegExp(`^\\..*\\.tmp-${String(process.pid)}-\\d+$`, "u");
565
+ /** Decide one watcher event, report it under the trace, and act on it. */
566
+ const handle = (source, type, raw) => {
567
+ const file = raw === null || raw === undefined || raw === "" ? null : raw;
568
+ const reason = this.classifyWatchEvent(source, file, logName, ownTempFile);
569
+ if (this.options.traceWatch === true) {
570
+ this.emit({
571
+ event: "watch",
572
+ watcher: source,
573
+ type,
574
+ file,
575
+ action: reason === null ? "scheduled" : "ignored",
576
+ reason,
577
+ });
578
+ }
579
+ if (reason === null)
580
+ this.schedule({ source, file });
581
+ };
582
+ const triggers = {
583
+ tasks: (type, name) => {
584
+ handle("tasks", type, name);
585
+ },
586
+ log: (type, name) => {
587
+ handle("log", type, name);
588
+ },
589
+ };
590
+ for (const [label, dir] of [
591
+ ["tasks", this.options.tasksDir],
592
+ ["log", dirname(this.options.logPath)],
593
+ ]) {
594
+ try {
595
+ const watcher = watch(dir, { persistent: true }, triggers[label]);
596
+ watcher.on("error", () => {
597
+ // A watcher that errors (its directory was removed, the platform ran
598
+ // out of handles) is simply dropped. The periodic tick continues to
599
+ // cover everything it was watching.
600
+ });
601
+ this.watchers.push(watcher);
602
+ this.watching = true;
603
+ }
604
+ catch (cause) {
605
+ this.warn("watch-unavailable", `the ${label} directory ${this.display(dir)} could not be watched (${errorMessage(cause)}); the daemon still re-scans it every ${String(this.options.intervalMs)}ms, so this costs latency and never correctness`);
606
+ }
607
+ }
608
+ }
609
+ /**
610
+ * Whether one watcher event schedules a tick, and if not, why not (APRV-230).
611
+ *
612
+ * Pure, apart from reading the two sets of basenames this loop's own
613
+ * write-back placed. `null` means "schedule"; every other answer is a reason
614
+ * an operator can count.
615
+ */
616
+ classifyWatchEvent(source, file, logName, ownTempFile) {
617
+ // A platform that will not say which file moved is saying "something in
618
+ // here changed", and one of the things it might be is the log: believe it.
619
+ if (file === null)
620
+ return null;
621
+ if (source === "log") {
622
+ if (file === logName)
623
+ return null;
624
+ // The lockfile first, so the trace names what it actually was rather than
625
+ // lumping it in with the verified-head snapshot beside it.
626
+ if (bookkeepingKind(file) !== null)
627
+ return "bookkeeping";
628
+ return "not-the-log";
629
+ }
630
+ if (this.selfWrites.has(file) || this.previousSelfWrites.has(file))
631
+ return "self-write";
632
+ // `core/task-file.ts` places a file through `.<name>.tmp-<pid>-<n>` in the
633
+ // same directory, and that temp file's create and rename are two more
634
+ // events about a write this process made.
635
+ if (ownTempFile.test(file))
636
+ return "own-temp";
637
+ if (bookkeepingKind(file) !== null)
638
+ return "bookkeeping";
639
+ return null;
640
+ }
641
+ /** Coalesce a burst of watcher events into one tick. */
642
+ schedule(wake) {
643
+ if (this.finished)
644
+ return;
645
+ // The event that OPENED the window is the one that woke the daemon
646
+ // (APRV-230); the ones behind it in the same burst only move the deadline,
647
+ // and the tick they share is attributed to the first of them.
648
+ this.pendingWake ??= wake;
649
+ if (this.debounce !== null)
650
+ clearTimeout(this.debounce);
651
+ this.debounce = setTimeout(() => {
652
+ this.debounce = null;
653
+ const outcome = this.tick();
654
+ if (outcome !== null)
655
+ this.finish(outcome);
656
+ }, this.options.debounceMs);
657
+ }
658
+ // -------------------------------------------------------------------------
659
+ // One tick
660
+ // -------------------------------------------------------------------------
661
+ /**
662
+ * One full pass: drift scan, TTL sweep, write-back, escalation surfacing,
663
+ * queue render.
664
+ *
665
+ * Returns `null` to continue, or the outcome that must stop the loop. Every
666
+ * step re-reads the verified log rather than sharing one snapshot across the
667
+ * pass, because each append invalidates the head the next one would build on
668
+ * and because an external writer may have moved the log in between. Reading is
669
+ * O(n) per call at v0.1 (`core/state.ts` documents the deferral); the head
670
+ * cache is APRV-43, and correctness comes first.
671
+ */
672
+ tick() {
673
+ if (this.ticking)
674
+ return null;
675
+ this.ticking = true;
676
+ const startedAt = performance.now();
677
+ const phases = {
678
+ drift: 0,
679
+ ttl: 0,
680
+ audit: 0,
681
+ dark: 0,
682
+ prune: 0,
683
+ write_back: 0,
684
+ advance: 0,
685
+ escalations: 0,
686
+ render: 0,
687
+ };
688
+ /** Time `step`, add it to `phase`, and hand back what it returned. */
689
+ const timed = (phase, step) => {
690
+ const from = performance.now();
691
+ try {
692
+ return step();
693
+ }
694
+ finally {
695
+ phases[phase] += performance.now() - from;
696
+ }
697
+ };
698
+ // What woke this tick (APRV-230), taken exactly once: the next watcher
699
+ // event opens a fresh window, and a tick that ran on the interval reports
700
+ // the wake it happened to cover rather than leaving it for a second tick.
701
+ const woke = this.pendingWake;
702
+ this.pendingWake = null;
703
+ try {
704
+ this.ticks += 1;
705
+ this.reads = 0;
706
+ this.fullReproofThisTick = false;
707
+ this.anchorThisTick = null;
708
+ this.checkpointsThisTick = null;
709
+ // One generation of the daemon's own task-file writes is kept, because a
710
+ // watch event arrives after the write that caused it and often after the
711
+ // tick that made it has ended.
712
+ this.previousSelfWrites = this.selfWrites;
713
+ this.selfWrites = new Set();
714
+ // Late-attaching watchers: a log directory (or a task folder) created after
715
+ // startup becomes watchable, and the operator gets the latency back.
716
+ if (!this.options.once && !this.watching) {
717
+ this.watchAttempted = false;
718
+ this.attachWatchers();
719
+ }
720
+ const opening = this.read();
721
+ if (!opening.ok)
722
+ return this.fatal(opening);
723
+ // The anchor check (APRV-219), on the full re-proof cadence and on the
724
+ // first tick, which is always a cold walk. Placed immediately after the
725
+ // opening read and before any append this tick could make: a log that
726
+ // contradicts its own committed copy is not fit to be appended to, and
727
+ // the sweeps below all append.
728
+ if (this.fullReproofThisTick) {
729
+ const anchor = this.compareToAnchor(opening.records);
730
+ if (anchor !== null) {
731
+ if (anchor.status === "diverged") {
732
+ return { kind: "anchor-diverged", message: anchor.message };
733
+ }
734
+ if (anchor.status === "behind") {
735
+ this.warn("anchor-behind", `${anchor.detail}. Nothing was appended onto the shorter chain by this check; run \`approval log sync\` before this daemon writes anything`);
736
+ }
737
+ this.anchorThisTick =
738
+ anchor.status === "skip"
739
+ ? { status: "skip", rev: null, seq: null }
740
+ : { status: anchor.status, rev: anchor.anchor.rev, seq: anchor.anchor.head.seq };
741
+ }
742
+ // The second witness (APRV-220), on the same cadence and immediately
743
+ // after the first. A log whose own signed checkpoints contradict it is
744
+ // no more fit to append to than one whose committed copy does, and the
745
+ // sweeps below all append. Independent of the anchor in both
746
+ // directions: a skip on one never excuses the other.
747
+ const checkpoints = this.checkCheckpoints(opening.records);
748
+ if (checkpoints !== null) {
749
+ if (checkpoints.status === "refused") {
750
+ return { kind: "checkpoint-invalid", message: checkpoints.message };
751
+ }
752
+ if (checkpoints.status === "skip") {
753
+ this.checkpointsThisTick = { status: "skip", verified: 0, keys: 0, due: false };
754
+ }
755
+ else {
756
+ // APRV-257. The warning and the `due` flag are one fact read once:
757
+ // `checkpoints.warning` is non-null exactly when
758
+ // `core/checkpoint.ts`'s due-ness rule says a checkpoint is owed,
759
+ // which is the same rule the dispatch cycle enqueues its prompt
760
+ // from. A `warn`, deliberately, and there is no branch below it
761
+ // that could become a `fatal`: a human who has been away is not
762
+ // tampering.
763
+ if (checkpoints.warning !== null)
764
+ this.warn("checkpoint-due", checkpoints.warning);
765
+ this.checkpointsThisTick = {
766
+ status: "pass",
767
+ verified: checkpoints.checkpoints.length,
768
+ keys: checkpoints.keys,
769
+ due: checkpoints.warning !== null,
770
+ };
771
+ }
772
+ }
773
+ }
774
+ const drift = timed("drift", () => this.scanForDrift());
775
+ if (drift.stop !== null)
776
+ return drift.stop;
777
+ const expired = timed("ttl", () => this.sweepTtl());
778
+ if (expired.stop !== null)
779
+ return expired.stop;
780
+ // Audit sampling (APRV-40, SPEC.md §5.2/§10.2). Placed before the closing
781
+ // read and the render so a sample appended here is counted by this tick's
782
+ // head and shows up in this tick's `audit_backlog`. It decides nothing:
783
+ // `daemon/audit.ts` re-derives eligibility from the verified log and every
784
+ // append is a compare-and-append.
785
+ timed("audit", () => sweepAuditSampling({
786
+ logPath: this.options.logPath,
787
+ policy: this.options.policy,
788
+ cwd: this.options.cwd,
789
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
790
+ ...(this.options.clock === undefined ? {} : { clock: this.options.clock }),
791
+ warn: (message) => this.warn("append-refused", message),
792
+ // One line per sample appended (APRV-57). The sweep names no event; it
793
+ // hands back what it wrote and the loop says it in the loop's own words.
794
+ sampled: (sample) => this.emit({
795
+ event: "sampled",
796
+ action_key: sample.candidate.actionKey,
797
+ task: sample.candidate.task,
798
+ seq: sample.record.seq,
799
+ subject_seq: sample.candidate.seq,
800
+ }),
801
+ }));
802
+ // The dark-session sweep (APRV-192), on its own cadence. Placed with the
803
+ // audit sweep because it is the same kind of thing — a detective control
804
+ // that re-derives its whole question from the verified log and the world,
805
+ // appends what is new, and changes no verdict. It runs BEFORE the prune so
806
+ // that an observation it appends is counted by this tick's closing head.
807
+ timed("dark", () => {
808
+ this.sweepDark();
809
+ });
810
+ // Payload retention (APRV-41), after the TTL sweep so a request expired on
811
+ // this tick is judged against the record the sweep just wrote. The pruner
812
+ // owns the rule, the append and the unlink; the daemon owns only the
813
+ // scheduling, which is the one thing `daemon/prune.ts` deliberately lacks.
814
+ timed("prune", () => {
815
+ this.prune();
816
+ });
817
+ // Projection write-back (SPEC.md §6.3, APRV-62). Last, because it copies
818
+ // the log into the files and every append this tick can make has now been
819
+ // made: a request expired by the sweep above is reflected on disk by this
820
+ // same tick rather than surfacing as drift on the next one. It appends
821
+ // nothing itself, so its position cannot affect any record.
822
+ const wrote = timed("write_back", () => this.writeBack());
823
+ if (wrote !== null)
824
+ return wrote;
825
+ // The cadence advance (APRV-204), after every append this tick can make
826
+ // and before the closing read, so the head this tick reports is the head
827
+ // the advance published against. It appends through the gate rather than
828
+ // through this loop, and everything it appends is picked up by the read
829
+ // below like any other writer's.
830
+ timed("advance", () => {
831
+ this.advanceIfDue(false);
832
+ });
833
+ const closing = this.read();
834
+ if (!closing.ok)
835
+ return this.fatal(closing);
836
+ const escalated = timed("escalations", () => this.surfaceEscalations(closing.records));
837
+ timed("render", () => {
838
+ this.render();
839
+ });
840
+ this.options.gitEvidence?.commit(closing.head);
841
+ this.emit({
842
+ event: "tick",
843
+ n: this.ticks,
844
+ head: closing.head === null ? null : closing.head.seq,
845
+ drift: drift.appended,
846
+ expired: expired.appended,
847
+ escalated,
848
+ ms: Math.round((performance.now() - startedAt) * 10) / 10,
849
+ reads: this.reads,
850
+ reproof: this.fullReproofThisTick ? "full" : "incremental",
851
+ ...(this.anchorThisTick === null ? {} : { anchor: this.anchorThisTick }),
852
+ ...(this.checkpointsThisTick === null ? {} : { checkpoints: this.checkpointsThisTick }),
853
+ woke_by: woke === null ? "interval" : woke.source,
854
+ ...(woke === null || woke.file === null ? {} : { woke_file: woke.file }),
855
+ phases: {
856
+ drift: Math.round(phases.drift * 10) / 10,
857
+ ttl: Math.round(phases.ttl * 10) / 10,
858
+ audit: Math.round(phases.audit * 10) / 10,
859
+ dark: Math.round(phases.dark * 10) / 10,
860
+ prune: Math.round(phases.prune * 10) / 10,
861
+ write_back: Math.round(phases.write_back * 10) / 10,
862
+ advance: Math.round(phases.advance * 10) / 10,
863
+ escalations: Math.round(phases.escalations * 10) / 10,
864
+ render: Math.round(phases.render * 10) / 10,
865
+ },
866
+ });
867
+ return null;
868
+ }
869
+ finally {
870
+ this.ticking = false;
871
+ }
872
+ }
873
+ // -------------------------------------------------------------------------
874
+ // The dark-session sweep (APRV-192)
875
+ // -------------------------------------------------------------------------
876
+ /**
877
+ * Ask git what happened, and the log whether it was told.
878
+ *
879
+ * On its own interval rather than every tick: the tick is 30 seconds by
880
+ * default and a `git log` per worktree at that rate is work spent to re-read
881
+ * an unchanged answer. The interval is a floor and never a ceiling — a sweep
882
+ * missed because the daemon was down is simply made by the next one, since
883
+ * the sweep holds no cursor and re-derives its whole question from the window
884
+ * it is given.
885
+ *
886
+ * The daemon owns the SCHEDULING and nothing else, which is the division the
887
+ * drift scan, the TTL sweep and the audit sweep already keep.
888
+ */
889
+ sweepDark() {
890
+ const watch = this.options.darkSessions;
891
+ if (watch === undefined)
892
+ return;
893
+ const now = Date.parse(readClock(this.clockOptions()));
894
+ if (this.lastDarkSweepAt !== null &&
895
+ !Number.isNaN(now) &&
896
+ now - this.lastDarkSweepAt < watch.intervalMs) {
897
+ return;
898
+ }
899
+ this.lastDarkSweepAt = Number.isNaN(now) ? 0 : now;
900
+ const read = this.read();
901
+ const result = sweepDarkSessions({
902
+ logPath: this.options.logPath,
903
+ root: this.options.cwd,
904
+ policy: this.options.policy,
905
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
906
+ ...(this.options.clock === undefined ? {} : { clock: this.options.clock }),
907
+ ...(this.options.observeGit === undefined ? {} : { observe: this.options.observeGit }),
908
+ windowMs: watch.windowMs,
909
+ records: read.ok ? read.records : null,
910
+ ...(read.ok ? {} : { logDetail: read.message }),
911
+ });
912
+ for (const entry of result.appended) {
913
+ this.emit({
914
+ event: "dark_session",
915
+ verdict: "dark",
916
+ subject: entry.finding.subject,
917
+ branch: entry.finding.branch,
918
+ code: entry.finding.code ?? "no-records",
919
+ commits: entry.finding.commits,
920
+ seq: entry.seq,
921
+ already_recorded: false,
922
+ message: entry.finding.detail,
923
+ });
924
+ }
925
+ for (const finding of result.repeated) {
926
+ this.emit({
927
+ event: "dark_session",
928
+ verdict: "dark",
929
+ subject: finding.subject,
930
+ branch: finding.branch,
931
+ code: finding.code ?? "no-records",
932
+ commits: finding.commits,
933
+ seq: null,
934
+ already_recorded: true,
935
+ message: finding.detail,
936
+ });
937
+ }
938
+ for (const finding of result.undetermined) {
939
+ this.emit({
940
+ event: "dark_session",
941
+ verdict: "undetermined",
942
+ subject: finding.subject,
943
+ branch: finding.branch,
944
+ code: finding.code ?? "git-unavailable",
945
+ commits: finding.commits,
946
+ seq: null,
947
+ already_recorded: false,
948
+ message: finding.detail,
949
+ });
950
+ this.warn("dark-session-undetermined", `the dark-session sweep could not establish ${finding.subject} (${finding.code ?? "?"}): ${finding.detail}`);
951
+ }
952
+ for (const message of result.refusals)
953
+ this.warn("append-refused", message);
954
+ }
955
+ // -------------------------------------------------------------------------
956
+ // The cadence advance (APRV-204)
957
+ // -------------------------------------------------------------------------
958
+ /**
959
+ * Advance the log if the cadence says it is due, or if this is the flush.
960
+ *
961
+ * The trigger, in one place: enough SUBSTANTIVE records have accrued
962
+ * (`afterRecords`), or the interval has elapsed since the last attempt and at
963
+ * least one substantive record is owed. "Substantive" excludes the advance
964
+ * cycle's own bookkeeping — see `daemon/advance.ts` on why counting it would
965
+ * make an idle repository advance forever.
966
+ *
967
+ * The last-attempt clock is set for every attempt, successful or not, which
968
+ * is what keeps a refusal off the hot path: a gate that says no costs one
969
+ * attempt per interval and no more.
970
+ *
971
+ * The flush ignores the interval and the count, and only the interval and the
972
+ * count: it still asks the gate, and it still does nothing when nothing is
973
+ * owed.
974
+ */
975
+ /**
976
+ * The advance cycles the started line names, and nothing else (APRV-264).
977
+ *
978
+ * Reads and proves; appends nothing. The first tick runs a moment later and
979
+ * sweeps for real, so anything provable here is closed before an operator has
980
+ * finished reading the line — which is why the line carries only what is NOT
981
+ * provable, the part that needs a person. Every key it names is marked
982
+ * reported, so the sweep that follows does not warn about it again.
983
+ */
984
+ listDanglingAdvancesAtStartup() {
985
+ const cadence = this.options.advance;
986
+ if (cadence === undefined)
987
+ return [];
988
+ const read = this.read();
989
+ if (!read.ok)
990
+ return [];
991
+ let outstanding;
992
+ try {
993
+ outstanding = proveDanglingAdvancesFor(this.advanceInputFor(cadence), read.records)
994
+ .filter((entry) => entry.provenBy === null)
995
+ .map((entry) => entry.actionKey);
996
+ }
997
+ catch {
998
+ // A git read that throws (no repository, an unreadable object store) is
999
+ // not a reason for the daemon to fail to start. The tick reports it.
1000
+ return [];
1001
+ }
1002
+ for (const key of outstanding)
1003
+ this.reportedDangling.add(key);
1004
+ return outstanding;
1005
+ }
1006
+ /** Everything `daemon/advance.ts` needs, built from this daemon's options. */
1007
+ advanceInputFor(cadence) {
1008
+ return {
1009
+ logPath: this.options.logPath,
1010
+ cwd: this.options.cwd,
1011
+ policy: this.options.policy,
1012
+ cadence,
1013
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1014
+ ...(this.options.clock === undefined ? {} : { clock: this.options.clock }),
1015
+ ...(this.options.today === undefined ? {} : { today: this.options.today }),
1016
+ ...(this.options.advanceRunner === undefined ? {} : { runner: this.options.advanceRunner }),
1017
+ };
1018
+ }
1019
+ advanceIfDue(flush) {
1020
+ const cadence = this.options.advance;
1021
+ if (cadence === undefined)
1022
+ return;
1023
+ // An advance is already running in a child (APRV-211). Its records are not
1024
+ // on a branch yet and its execution is not closed yet, so a second attempt
1025
+ // now would ask about work that is already authorised and already moving.
1026
+ if (this.advanceInFlight !== null)
1027
+ return;
1028
+ const read = this.read();
1029
+ if (!read.ok)
1030
+ return;
1031
+ const root = this.options.cwd;
1032
+ const today = this.options.today ?? readClock(this.clockOptions());
1033
+ const input = this.advanceInputFor(cadence);
1034
+ // APRV-233, first: an outcome this process observed and could not record.
1035
+ // It is settled BEFORE any trigger is evaluated, on the head as it stands
1036
+ // now, and this tick does nothing else either way — a tick that both closed
1037
+ // an old cycle and opened a new one would be reasoning about a log it read
1038
+ // before it wrote to it.
1039
+ if (this.pendingAdvanceFinish !== null) {
1040
+ const settled = settleAdvanceFinish(input, this.pendingAdvanceFinish);
1041
+ if (settled.ok) {
1042
+ this.pendingAdvanceFinish = null;
1043
+ this.emit({
1044
+ event: "advance",
1045
+ outcome: "nothing-owed",
1046
+ records_pending: 0,
1047
+ records_branch: null,
1048
+ range: null,
1049
+ commit: null,
1050
+ pr_url: null,
1051
+ pr_created: false,
1052
+ auto_merge: null,
1053
+ auto_merge_note: null,
1054
+ rebuilt: false,
1055
+ rebuilt_on: null,
1056
+ code: "advance-settled",
1057
+ message: settled.message,
1058
+ flush,
1059
+ });
1060
+ }
1061
+ else {
1062
+ this.warn("advance-refused", settled.message);
1063
+ }
1064
+ return;
1065
+ }
1066
+ // APRV-233, and the same rule for cycles this process does not remember:
1067
+ // an advance whose outcome nobody recorded is reconciled from the git
1068
+ // evidence before anything else is attempted, and refused (never re-run)
1069
+ // when the evidence is not there. On 2026-09-02 the absence of this made
1070
+ // the next tick's authorization reach `startExecution` on the open key and
1071
+ // come back `already-executed`, which reported a failure and fixed nothing.
1072
+ //
1073
+ // APRV-264 widened it from one cycle to all of them. On 2026-09-05 five
1074
+ // stood at once and this closed one per tick behind a refusal that stopped
1075
+ // the cadence, so the operator repaired the pile by hand instead.
1076
+ const sweep = sweepDanglingAdvances(input, read.records);
1077
+ if (!sweepIsClear(sweep)) {
1078
+ for (const closed of sweep.settled) {
1079
+ this.emit({
1080
+ event: "advance",
1081
+ outcome: "nothing-owed",
1082
+ records_pending: 0,
1083
+ records_branch: null,
1084
+ range: null,
1085
+ commit: null,
1086
+ pr_url: null,
1087
+ pr_created: false,
1088
+ auto_merge: null,
1089
+ auto_merge_note: null,
1090
+ rebuilt: false,
1091
+ rebuilt_on: null,
1092
+ code: "advance-reconciled",
1093
+ message: closed.message,
1094
+ flush,
1095
+ });
1096
+ }
1097
+ if (sweep.outstanding.length > 0) {
1098
+ // Once per key, not once per tick, and one line for all of them: the
1099
+ // repair is the same command whichever key an operator reads first, so
1100
+ // they are named together under it. The set is what makes it once —
1101
+ // a key already named on the started line or by an earlier tick is not
1102
+ // named again until it goes away and comes back.
1103
+ const unreported = sweep.outstanding.filter((entry) => !this.reportedDangling.has(entry.actionKey));
1104
+ if (unreported.length > 0) {
1105
+ for (const entry of sweep.outstanding)
1106
+ this.reportedDangling.add(entry.actionKey);
1107
+ this.warn("advance-refused", `${unreconciledRefusal(sweep.outstanding.map((entry) => entry.actionKey))} ${sweep.outstanding
1108
+ .map((entry) => entry.message)
1109
+ .join(" ")}`);
1110
+ }
1111
+ }
1112
+ return;
1113
+ }
1114
+ this.reportedDangling.clear();
1115
+ const state = publishedState(root, this.options.logPath, read.records, cadence, today);
1116
+ if (state.substantive === 0)
1117
+ return;
1118
+ // The flush does not re-ask a question this process just asked. A tick
1119
+ // whose advance was gated or refused leaves a request in the queue; a flush
1120
+ // a moment later, against the same owed records, would put a SECOND
1121
+ // identical question in front of the same human. Measured in SUBSTANTIVE
1122
+ // records rather than in head seq, because the gated attempt's own request
1123
+ // moved the head and answered nothing. The retry is the next tick's
1124
+ // business, and the next daemon's.
1125
+ if (flush &&
1126
+ this.lastAdvance !== null &&
1127
+ this.lastAdvance.outcome !== "advanced" &&
1128
+ this.lastAdvanceOwed !== null &&
1129
+ state.substantive <= this.lastAdvanceOwed) {
1130
+ return;
1131
+ }
1132
+ const now = Date.parse(readClock(this.clockOptions()));
1133
+ const elapsed = this.lastAdvanceAt === null || Number.isNaN(now) || now - this.lastAdvanceAt >= cadence.intervalMs;
1134
+ // APRV-233, second: an advance that ALREADY HAPPENED does not get made
1135
+ // again inside the interval, and the record-count trigger does not run
1136
+ // around the interval for a span an earlier attempt already carried.
1137
+ //
1138
+ // The 2026-09-02 shape. The advance pushed `records-log-2026-09-02` and its
1139
+ // `execution.completed` lost the append race, so the only thing left saying
1140
+ // that a branch had just been pushed was an in-process clock — and the
1141
+ // count trigger, alone among the two, never consulted it. Ticks two, five
1142
+ // and eight each pushed the same branch again, ninety seconds apart, under
1143
+ // a fifteen-minute interval (the three-tick spacing is the one in-flight
1144
+ // slot: the two ticks in between found a child still running).
1145
+ //
1146
+ // An advance cycle still open in the log has already returned above, so
1147
+ // what is left is the trigger itself: inside the interval, the count
1148
+ // trigger fires only on records this process has not already attempted to
1149
+ // publish. `afterRecords` is the busy-hour trigger and it keeps working;
1150
+ // what it no longer does is count the same owed span over and over, which
1151
+ // is how four fresh records re-pushed the branch every ninety seconds while
1152
+ // the published head stood still.
1153
+ const fresh = this.lastAdvanceSpanEnd === null
1154
+ ? state.substantive
1155
+ : read.records.filter((record) => record.seq > Math.max(state.publishedSeq, this.lastAdvanceSpanEnd ?? 0) &&
1156
+ !isAdvanceBookkeeping(record)).length;
1157
+ if (!flush && fresh < cadence.afterRecords && !elapsed)
1158
+ return;
1159
+ this.lastAdvanceAt = Number.isNaN(now) ? 0 : now;
1160
+ this.lastAdvanceOwed = state.substantive;
1161
+ this.lastAdvanceSpanEnd = state.substantiveSeq;
1162
+ // The gate, always here: the `supervised-live` draw reads a secret that
1163
+ // `core/child-env.ts` strips from every child, so authorization cannot
1164
+ // leave this process (APRV-205, APRV-211).
1165
+ const auth = authorizeAdvance(input, read.records);
1166
+ if (!auth.authorized) {
1167
+ this.reportAdvance(auth.attempt, flush);
1168
+ return;
1169
+ }
1170
+ // The git side effect, which may leave. A blocking `git push` on this stack
1171
+ // is a Telegram callback answered past its window (APRV-211), so the
1172
+ // periodic tick hands the verb to a child and returns to the loop. The
1173
+ // shutdown flush and `--once` keep the synchronous path: the first has no
1174
+ // loop left to return to, and the second is a process that exits at the end
1175
+ // of this tick, where an advance settling afterwards would be an advance
1176
+ // nobody recorded.
1177
+ if (flush || this.options.once === true) {
1178
+ this.reportAdvance(runAdvanceSync(input, auth), flush);
1179
+ return;
1180
+ }
1181
+ this.advanceInFlight = runAdvanceAsync(input, auth)
1182
+ .then((attempt) => {
1183
+ this.reportAdvance(attempt, flush);
1184
+ })
1185
+ .catch((cause) => {
1186
+ this.warn("advance-refused", `the advance child could not be settled: ${errorMessage(cause)}`);
1187
+ })
1188
+ .finally(() => {
1189
+ this.advanceInFlight = null;
1190
+ });
1191
+ }
1192
+ /** Record and report one finished attempt, from either runner. */
1193
+ reportAdvance(attempt, flush) {
1194
+ this.lastAdvance = attempt;
1195
+ // APRV-233. Held whatever the outcome was, and before the early return: an
1196
+ // outcome nobody recorded is the one thing this loop must not forget.
1197
+ this.pendingAdvanceFinish = attempt.pendingFinish;
1198
+ if (attempt.outcome === "nothing-owed" && attempt.pendingFinish === null)
1199
+ return;
1200
+ this.emit({
1201
+ event: "advance",
1202
+ outcome: attempt.outcome,
1203
+ records_pending: attempt.recordsPending,
1204
+ records_branch: attempt.recordsBranch,
1205
+ range: attempt.range,
1206
+ commit: attempt.commit,
1207
+ pr_url: attempt.prUrl,
1208
+ pr_created: attempt.prCreated,
1209
+ auto_merge: attempt.autoMerge,
1210
+ auto_merge_note: attempt.autoMergeNote,
1211
+ rebuilt: attempt.rebuilt,
1212
+ rebuilt_on: attempt.rebuiltOn,
1213
+ code: attempt.code,
1214
+ message: attempt.message,
1215
+ flush,
1216
+ });
1217
+ if (attempt.outcome !== "advanced") {
1218
+ this.warn("advance-refused", `the cadence advance did not publish (${attempt.outcome}${attempt.code === null ? "" : `, ${attempt.code}`}): ${attempt.message}`);
1219
+ }
1220
+ }
1221
+ /** The clock this loop reads, as the options every core writer takes it. */
1222
+ clockOptions() {
1223
+ return this.options.clock === undefined ? {} : { clock: this.options.clock };
1224
+ }
1225
+ /** The last attempt this process made, for a caller that wants to assert on it. */
1226
+ lastAdvanceAttempt() {
1227
+ return this.lastAdvance;
1228
+ }
1229
+ // -------------------------------------------------------------------------
1230
+ // Log anchoring (APRV-219)
1231
+ // -------------------------------------------------------------------------
1232
+ /** Is the anchor check on for this run? On unless the operator turned it off. */
1233
+ anchorEnabled() {
1234
+ return this.options.anchor?.enabled !== false;
1235
+ }
1236
+ /** The anchor options this loop passes down, assembled once and identically. */
1237
+ anchorWhere() {
1238
+ const anchor = this.options.anchor;
1239
+ return {
1240
+ ...(anchor?.rev === undefined ? {} : { rev: anchor.rev }),
1241
+ ...(anchor?.remote === undefined ? {} : { remote: anchor.remote }),
1242
+ ...(anchor?.base === undefined ? {} : { base: anchor.base }),
1243
+ ...(this.options.today === undefined ? {} : { today: this.options.today }),
1244
+ };
1245
+ }
1246
+ /**
1247
+ * Which committed copy this run is anchored against, for the `started` line.
1248
+ *
1249
+ * Resolution only: it asks git which revs carry a copy of the log and which
1250
+ * of them reaches furthest, and compares nothing. The first tick, a line
1251
+ * below the `started` emit, makes the comparison — and always in full, since
1252
+ * a cold walk is a full re-proof by construction.
1253
+ */
1254
+ resolveAnchorForReport() {
1255
+ if (!this.anchorEnabled()) {
1256
+ return { rev: null, seq: null, reason: "the anchor check is disabled for this run" };
1257
+ }
1258
+ const root = repoRoot(dirname(this.options.logPath));
1259
+ if (root === null) {
1260
+ return {
1261
+ rev: null,
1262
+ seq: null,
1263
+ reason: `${this.display(this.options.logPath)} is not inside a git repository`,
1264
+ };
1265
+ }
1266
+ const resolved = resolveAnchor(root, this.options.logPath, {
1267
+ ...this.anchorWhere(),
1268
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1269
+ });
1270
+ return resolved.ok
1271
+ ? { rev: resolved.anchor.rev, seq: resolved.anchor.head.seq, reason: null }
1272
+ : { rev: null, seq: null, reason: resolved.reason };
1273
+ }
1274
+ /**
1275
+ * Compare the verified working records against the committed copy.
1276
+ *
1277
+ * `null` when the check is off. Everything else — including "there is no
1278
+ * committed copy" — comes back as an {@link AnchorCheck} the caller reports,
1279
+ * because a check that could not look must never be read as a check that
1280
+ * looked and was satisfied.
1281
+ */
1282
+ compareToAnchor(records) {
1283
+ if (!this.anchorEnabled())
1284
+ return null;
1285
+ return checkLogAnchor({
1286
+ logPath: this.options.logPath,
1287
+ records,
1288
+ ...this.anchorWhere(),
1289
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1290
+ });
1291
+ }
1292
+ // -------------------------------------------------------------------------
1293
+ // Human-signed checkpoints (APRV-220)
1294
+ // -------------------------------------------------------------------------
1295
+ /**
1296
+ * Demand every checkpoint inside the verified records.
1297
+ *
1298
+ * `null` when the check is off. Everything else — including "no key is
1299
+ * configured" — comes back as a {@link CheckpointCheck} the caller reports,
1300
+ * because a check that could not look must never be read as a check that
1301
+ * looked and was satisfied.
1302
+ *
1303
+ * The policy is read here, once per comparison rather than once per run: the
1304
+ * keys are the human's and the human may add one while this loop is running,
1305
+ * and a daemon holding a key list from startup would keep refusing a
1306
+ * checkpoint the operator had already authorized by editing the policy.
1307
+ */
1308
+ checkCheckpoints(records) {
1309
+ if (this.options.checkpoints?.enabled === false)
1310
+ return null;
1311
+ const configured = checkpointPolicyOf(this.options.policy, this.options.schemaDir);
1312
+ return checkLogCheckpoints({
1313
+ records,
1314
+ publicKeys: configured.publicKeys,
1315
+ checkpointEveryMs: configured.checkpointEveryMs,
1316
+ keysUnavailable: configured.unloadable,
1317
+ ...(this.options.clock === undefined
1318
+ ? {}
1319
+ : { now: Date.parse(this.options.clock()) }),
1320
+ });
1321
+ }
1322
+ fatal(read) {
1323
+ switch (read.code) {
1324
+ case "log-unreadable":
1325
+ return { kind: "log-unreadable", message: read.message };
1326
+ case "log-torn-tail":
1327
+ return { kind: "log-torn-tail", message: read.message };
1328
+ case "log-corrupt":
1329
+ return { kind: "log-corrupt", message: read.message };
1330
+ }
1331
+ }
1332
+ /**
1333
+ * The verified log, and — since APRV-188 — the publication of what was
1334
+ * verified.
1335
+ *
1336
+ * The daemon holds a warm {@link VerifiedReadCache} and re-verifies only the
1337
+ * appended tail on every tick. Every hook process, by contrast, starts with an
1338
+ * empty cache and walks the whole chain before it may decide anything. So on
1339
+ * each clean read this loop publishes a verified-head snapshot beside the log:
1340
+ * an endorsement of the exact bytes it just walked, which the next hook
1341
+ * process re-proves for itself (one SHA-256) instead of re-walking. See
1342
+ * `core/verified-snapshot.ts` for what that endorsement claims and what a
1343
+ * reader still checks.
1344
+ *
1345
+ * The publication rides on the read rather than following it, so the bytes
1346
+ * endorsed are the bytes verified: a publisher that re-read the file to hash
1347
+ * it could endorse a digest of bytes nobody walked.
1348
+ */
1349
+ read() {
1350
+ this.reads += 1;
1351
+ // APRV-217. Measured around THIS read, so the tick line reports the path
1352
+ // the loop's own reads took: other readers in the same process (the queue
1353
+ // renderer) have their own answer and their own line to be judged on.
1354
+ const fullBefore = processReadCache.stats.fullReproofs;
1355
+ try {
1356
+ return this.readOnce();
1357
+ }
1358
+ finally {
1359
+ if (processReadCache.stats.fullReproofs > fullBefore)
1360
+ this.fullReproofThisTick = true;
1361
+ }
1362
+ }
1363
+ readOnce() {
1364
+ return readVerifiedRecords(this.options.logPath, {
1365
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1366
+ publishSnapshot: this.options.snapshot !== false,
1367
+ // APRV-217. Absent on the options means absent here, which the read cache
1368
+ // reads as `full`: the daemon asks for the cheaper proof only when an
1369
+ // operator's policy or flag said so.
1370
+ ...(this.options.readProof === undefined ? {} : { readProof: this.options.readProof }),
1371
+ });
1372
+ }
1373
+ /** The TTL in force right now, re-read every pass: policy files change. */
1374
+ ttlMs() {
1375
+ const where = this.options.policy.file !== undefined
1376
+ ? { file: this.options.policy.file }
1377
+ : { dir: this.options.policy.dir ?? this.options.cwd };
1378
+ if (this.options.schemaDir !== undefined)
1379
+ where.schemaDir = this.options.schemaDir;
1380
+ const load = loadPolicy(where);
1381
+ // Fail closed exactly as the gate does: an unloadable policy declares no
1382
+ // TTL, so nothing lapses and nothing is expired on its behalf.
1383
+ return load.ok ? load.durations.approvalTtlMs : null;
1384
+ }
1385
+ gateOptions() {
1386
+ const options = { policy: this.options.policy };
1387
+ if (this.options.schemaDir !== undefined)
1388
+ options.schemaDir = this.options.schemaDir;
1389
+ if (this.options.clock !== undefined)
1390
+ options.clock = this.options.clock;
1391
+ return options;
1392
+ }
1393
+ // -------------------------------------------------------------------------
1394
+ // Envelope drift (SPEC.md §6.3)
1395
+ // -------------------------------------------------------------------------
1396
+ /**
1397
+ * Read every task file, compare its claimed `state:` against the log, and
1398
+ * append `envelope.drift` for each file that contradicts it.
1399
+ *
1400
+ * §6.3: "`state` is a projection of log events; the file is updated by the
1401
+ * daemon after the event is appended, never the reverse. A file edit that
1402
+ * contradicts the log is itself logged (`envelope.drift`) and surfaced."
1403
+ *
1404
+ * This scan **only records**. The repair is {@link writeBack}, later in the
1405
+ * same tick: the disagreement is written to the log first and copied into the
1406
+ * file second, in that order, so nothing is ever corrected off the record. A
1407
+ * drift record therefore names a moment, not a standing condition.
1408
+ *
1409
+ * A schema-invalid envelope is warned about and skipped, not logged as drift: a
1410
+ * malformed file is not a *contradiction* of the log, it is a file the runtime
1411
+ * cannot read a claim out of at all, and inventing a `declared_state` for it
1412
+ * would put a fact in the log that nobody wrote.
1413
+ */
1414
+ scanForDrift() {
1415
+ // ONE verified read for the whole scan (APRV-211). The scan asks the same
1416
+ // question of every file — "what does the log say about this task?" — and
1417
+ // asking it per file re-verified and re-walked the log once per task file:
1418
+ // 210 reads a tick in this repository, 45% of a three-second tick.
1419
+ //
1420
+ // The read below is the DECISION's evidence. It never becomes an append's:
1421
+ // a file the decision finds in drift is re-derived against a fresh read
1422
+ // immediately before the append, and that fresh head is the `expectedHead`
1423
+ // the append is compared against (SPEC.md §11.1 invariant 5, unchanged).
1424
+ // Deciding from a slightly older log can therefore only cost a decision that
1425
+ // the fresh derivation then declines to act on; it can never place a record
1426
+ // against a head it did not see.
1427
+ const scan = this.read();
1428
+ if (!scan.ok)
1429
+ return { appended: 0, stop: this.fatal(scan) };
1430
+ let appended = 0;
1431
+ for (const file of this.taskFiles()) {
1432
+ const outcome = this.checkOneFile(file, scan);
1433
+ if (outcome.stop !== null)
1434
+ return { appended, stop: outcome.stop };
1435
+ if (outcome.appended)
1436
+ appended += 1;
1437
+ }
1438
+ return { appended, stop: null };
1439
+ }
1440
+ /** Every `*.md` under the task folder, sorted, non-recursive. */
1441
+ taskFiles() {
1442
+ let entries;
1443
+ try {
1444
+ entries = readdirSync(this.options.tasksDir, { withFileTypes: true });
1445
+ }
1446
+ catch (cause) {
1447
+ const code = cause.code;
1448
+ if (code !== "ENOENT") {
1449
+ this.warn("tasks-dir-unreadable", `task folder ${this.display(this.options.tasksDir)} could not be listed: ${errorMessage(cause)}; the next tick tries again`);
1450
+ }
1451
+ return [];
1452
+ }
1453
+ return entries
1454
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
1455
+ .map((entry) => join(this.options.tasksDir, entry.name))
1456
+ .sort();
1457
+ }
1458
+ checkOneFile(file, scan) {
1459
+ const read = readTaskFile(file);
1460
+ if (!read.ok) {
1461
+ if (read.code === "no-frontmatter") {
1462
+ // SPEC.md §6: a task with no envelope is valid markdown. Silent by
1463
+ // design — unless the log says this task once declared actions, which
1464
+ // makes it a loss rather than an absence (APRV-63). The file name is
1465
+ // the only id such a file leaves; the log decides whether it means
1466
+ // anything.
1467
+ const hint = taskIdFromFileName(file);
1468
+ if (hint === null)
1469
+ return { appended: false, stop: null };
1470
+ return this.reportEnvelopeLoss(file, hint, true, "no-frontmatter", scan);
1471
+ }
1472
+ this.warn(read.code === "io" ? "task-unreadable" : "frontmatter-invalid", `${this.display(file)}: ${read.message}`);
1473
+ return { appended: false, stop: null };
1474
+ }
1475
+ const envelope = read.data["approval"];
1476
+ if (envelope === undefined) {
1477
+ // Frontmatter, no `approval:` key. Ordinary for a task that never had
1478
+ // one; envelope loss for a task the log registered (APRV-63).
1479
+ const declaredId = read.data["id"];
1480
+ const id = typeof declaredId === "string" && declaredId.length > 0
1481
+ ? declaredId
1482
+ : taskIdFromFileName(file);
1483
+ if (id === null)
1484
+ return { appended: false, stop: null };
1485
+ return this.reportEnvelopeLoss(file, id, typeof declaredId !== "string" || declaredId.length === 0, "no-approval-key", scan);
1486
+ }
1487
+ const id = read.data["id"];
1488
+ if (typeof id !== "string" || id.length === 0) {
1489
+ this.warn("task-id-missing", `${this.display(file)} carries an approval: envelope but no usable \`id\`; the task id is the key every log record is written under, so drift cannot be attributed to this file`);
1490
+ return { appended: false, stop: null };
1491
+ }
1492
+ // The read boundary, deliberately (APRV-148). This scan validates envelopes
1493
+ // it will never register: a pre-APRV-121 envelope carrying numeric monetary
1494
+ // fields was valid when its task was registered and must keep validating
1495
+ // here, or drift and loss detection turn silently off for exactly the
1496
+ // historical artifacts APRV-121's compatibility rule protects. `approval
1497
+ // register` still validates at the strict write boundary.
1498
+ const validation = validate("envelope", envelope, this.options.schemaDir === undefined
1499
+ ? { mode: "historical" }
1500
+ : { schemaDir: this.options.schemaDir, mode: "historical" });
1501
+ if (!validation.ok) {
1502
+ this.warn("envelope-invalid", `${this.display(file)}: the approval: envelope failed envelope.schema.json (${validation.errors.length} error(s), first: ${validation.errors[0]?.message ?? "unknown"}); nothing was appended, because a malformed envelope makes no claim the log could contradict`);
1503
+ return { appended: false, stop: null };
1504
+ }
1505
+ const ts = readClock(this.options.clock === undefined ? {} : { clock: this.options.clock });
1506
+ const ttlMs = this.ttlMs();
1507
+ const declaredRaw = envelope.state;
1508
+ const declaredState = typeof declaredRaw === "string" ? declaredRaw : null;
1509
+ const envelopeDigest = digestOf(envelope);
1510
+ // The decision, from the scan's read.
1511
+ const decided = taskEnvelopeState(scan.records, id, ts, ttlMs);
1512
+ if (declaredState === decided.state)
1513
+ return { appended: false, stop: null };
1514
+ if (driftAlreadyLogged(scan.records, id, {
1515
+ declaredState,
1516
+ derivedState: decided.state,
1517
+ envelopeDigest,
1518
+ })) {
1519
+ return { appended: false, stop: null };
1520
+ }
1521
+ // Re-read immediately before appending, so the head this append is compared
1522
+ // against is the head the RECORDED fact was derived from. The whole decision
1523
+ // is remade against those fresh records: a log that moved between the scan
1524
+ // and here may have removed the drift (someone decided the request, another
1525
+ // writer recorded the same drift), and a record about a disagreement that no
1526
+ // longer exists is a record nobody wrote.
1527
+ const records = this.read();
1528
+ if (!records.ok)
1529
+ return { appended: false, stop: this.fatal(records) };
1530
+ const projection = taskEnvelopeState(records.records, id, ts, ttlMs);
1531
+ if (declaredState === projection.state)
1532
+ return { appended: false, stop: null };
1533
+ const facts = {
1534
+ declaredState,
1535
+ derivedState: projection.state,
1536
+ envelopeDigest,
1537
+ };
1538
+ if (driftAlreadyLogged(records.records, id, facts))
1539
+ return { appended: false, stop: null };
1540
+ const payload = {
1541
+ file: this.display(file),
1542
+ declared_state: declaredState,
1543
+ derived_state: facts.derivedState,
1544
+ registered: projection.registered,
1545
+ reason: "state-mismatch",
1546
+ };
1547
+ if (facts.envelopeDigest !== null)
1548
+ payload["envelope_sha256"] = facts.envelopeDigest;
1549
+ const result = appendEvent(this.options.logPath, {
1550
+ ts,
1551
+ event: "envelope.drift",
1552
+ actor: DAEMON_ACTOR,
1553
+ task: id,
1554
+ payload,
1555
+ }, {
1556
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1557
+ expectedHead: records.head,
1558
+ });
1559
+ if (!result.ok) {
1560
+ this.warn("append-refused", `envelope.drift for ${id} was not appended (${result.error.code}): ${result.error.message}`);
1561
+ return { appended: false, stop: null };
1562
+ }
1563
+ this.drifts += 1;
1564
+ this.emit({
1565
+ event: "drift",
1566
+ task: id,
1567
+ file: this.display(file),
1568
+ declared_state: declaredState,
1569
+ derived_state: facts.derivedState,
1570
+ seq: result.record.seq,
1571
+ });
1572
+ return { appended: true, stop: null };
1573
+ }
1574
+ /**
1575
+ * A task file with no envelope whose task the log registered: the envelope was
1576
+ * lost (APRV-63, the defense half of APRV-60).
1577
+ *
1578
+ * Recorded as `envelope.drift` with `payload.reason: "envelope-missing"` —
1579
+ * the same event type, because it is the same §6.3 question ("the file and
1580
+ * the log disagree"), and a distinct reason, because the answer is different:
1581
+ * a state mismatch is an edit to reconcile, a missing envelope is a deletion
1582
+ * to restore. `declared_state` is `null` because the file makes no claim at
1583
+ * all, and `envelope_sha256` is absent because there is no envelope to digest.
1584
+ *
1585
+ * **Nothing is repaired.** The registration in the log holds every action the
1586
+ * envelope declared, so a writer *could* re-emit it — and that would turn a
1587
+ * projection into a source, which is the one thing the log's authority rests
1588
+ * on not happening. The daemon reports; a human restores by hand.
1589
+ *
1590
+ * `loose` says the id came from the file name rather than from frontmatter;
1591
+ * it relaxes only the *matching*, and the record is written under the id the
1592
+ * log itself holds.
1593
+ */
1594
+ reportEnvelopeLoss(file, id, loose, kind, scan) {
1595
+ // The decision, from the scan's read (APRV-211). This is the path the vast
1596
+ // majority of task files take — a plain Backlog.md task the log never
1597
+ // registered — and it used to cost one full verified read per file.
1598
+ const scanned = latestRegistration(scan.records, id, loose);
1599
+ if (scanned === null) {
1600
+ // The log has never heard of this task. SPEC.md §6: a task with no
1601
+ // envelope is valid markdown, and this one is exactly that.
1602
+ return { appended: false, stop: null };
1603
+ }
1604
+ const ts = readClock(this.options.clock === undefined ? {} : { clock: this.options.clock });
1605
+ const ttlMs = this.ttlMs();
1606
+ // Re-read immediately before appending, exactly as the mismatch path does:
1607
+ // the head this append is compared against is the head the recorded fact was
1608
+ // derived from, and the whole decision is remade against it.
1609
+ const records = this.read();
1610
+ if (!records.ok)
1611
+ return { appended: false, stop: this.fatal(records) };
1612
+ const registration = latestRegistration(records.records, id, loose);
1613
+ if (registration === null)
1614
+ return { appended: false, stop: null };
1615
+ const task = registration.task;
1616
+ if (typeof task !== "string" || task.length === 0)
1617
+ return { appended: false, stop: null };
1618
+ const projection = taskEnvelopeState(records.records, task, ts, ttlMs);
1619
+ const facts = {
1620
+ declaredState: null,
1621
+ derivedState: projection.state,
1622
+ envelopeDigest: null,
1623
+ reason: "envelope-missing",
1624
+ };
1625
+ if (driftAlreadyLogged(records.records, task, facts))
1626
+ return { appended: false, stop: null };
1627
+ const payload = {
1628
+ file: this.display(file),
1629
+ declared_state: null,
1630
+ derived_state: facts.derivedState,
1631
+ registered: projection.registered,
1632
+ reason: "envelope-missing",
1633
+ missing: kind,
1634
+ registered_seq: registration.seq,
1635
+ };
1636
+ const result = appendEvent(this.options.logPath, { ts, event: "envelope.drift", actor: DAEMON_ACTOR, task, payload }, {
1637
+ ...(this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir }),
1638
+ expectedHead: records.head,
1639
+ });
1640
+ if (!result.ok) {
1641
+ this.warn("append-refused", `envelope.drift (envelope-missing) for ${task} was not appended (${result.error.code}): ${result.error.message}`);
1642
+ return { appended: false, stop: null };
1643
+ }
1644
+ this.drifts += 1;
1645
+ this.emit({
1646
+ event: "drift",
1647
+ task,
1648
+ file: this.display(file),
1649
+ declared_state: null,
1650
+ derived_state: facts.derivedState,
1651
+ seq: result.record.seq,
1652
+ reason: "envelope-missing",
1653
+ });
1654
+ return { appended: true, stop: null };
1655
+ }
1656
+ // -------------------------------------------------------------------------
1657
+ // TTL sweep (SPEC.md §5.2, §6.3)
1658
+ // -------------------------------------------------------------------------
1659
+ /**
1660
+ * Append `approval.expired` for every live request whose TTL has lapsed.
1661
+ *
1662
+ * The sweep changes no verdict. `core/gate.ts` already judges the TTL lazily at
1663
+ * decision time, so a late grant is refused with or without an expiry record;
1664
+ * what the sweep adds is *visibility* — the queue, the index, and anyone
1665
+ * reading the log see a terminal fact rather than a request that looks live and
1666
+ * is not.
1667
+ *
1668
+ * Idempotent three ways over, and none of them is a remembered flag:
1669
+ *
1670
+ * - with lazy expiry, because `expire` refuses `already-decided` for anything a
1671
+ * human (or the lazy path) already settled;
1672
+ * - with itself, because the candidate list is re-derived from the verified log
1673
+ * each sweep and an expired request no longer appears in it;
1674
+ * - across restarts, because the daemon carries no state between runs at all.
1675
+ *
1676
+ * A `head-moved` refusal is expected traffic, not a fault: a CLI verb decided
1677
+ * the same request between this candidate list and this append. It is reported
1678
+ * at `debug` weight (a single warning line) and the next tick re-derives.
1679
+ */
1680
+ sweepTtl() {
1681
+ const records = this.read();
1682
+ if (!records.ok)
1683
+ return { appended: 0, stop: this.fatal(records) };
1684
+ const ts = readClock(this.options.clock === undefined ? {} : { clock: this.options.clock });
1685
+ const candidates = lapsedRequests(records.records, ts, this.ttlMs());
1686
+ let appended = 0;
1687
+ for (const candidate of candidates) {
1688
+ const result = expire(this.options.logPath, candidate.actionKey, this.gateOptions());
1689
+ if (!result.ok) {
1690
+ // `not-expired` and `already-decided` mean the log moved under the
1691
+ // candidate list — someone decided it, or the clock the gate read differs
1692
+ // by a hair from the one this list was built with. Neither is an error,
1693
+ // and neither is retried here.
1694
+ this.warn("expire-refused", `approval.expired for ${candidate.actionKey} was not appended (${result.code}): ${result.message}`);
1695
+ continue;
1696
+ }
1697
+ appended += 1;
1698
+ this.expiries += 1;
1699
+ this.emit({
1700
+ event: "expired",
1701
+ action_key: candidate.actionKey,
1702
+ task: candidate.task,
1703
+ seq: result.record.seq,
1704
+ });
1705
+ }
1706
+ return { appended, stop: null };
1707
+ }
1708
+ // -------------------------------------------------------------------------
1709
+ // Payload retention (amended SPEC.md §5.2, APRV-41)
1710
+ // -------------------------------------------------------------------------
1711
+ /**
1712
+ * Hand one pass to `daemon/prune.ts` and surface whatever it could not do.
1713
+ *
1714
+ * The daemon adds nothing to the rule: with `payload_retention` absent the
1715
+ * pass is a no-op, and with it present the pruner appends `payload.pruned`
1716
+ * before every unlink and re-derives the whole question from the verified log.
1717
+ * Warnings never stop the loop — a store that could not be pruned is a store
1718
+ * holding more evidence than the policy asked it to, which is the safe side.
1719
+ */
1720
+ prune() {
1721
+ const options = {
1722
+ logPath: this.options.logPath,
1723
+ // The same resolution `ttlMs` uses: an unset directory means the daemon's
1724
+ // own working directory, never the process's.
1725
+ policy: this.options.policy.file !== undefined
1726
+ ? { file: this.options.policy.file }
1727
+ : { dir: this.options.policy.dir ?? this.options.cwd },
1728
+ };
1729
+ if (this.options.schemaDir !== undefined)
1730
+ options.schemaDir = this.options.schemaDir;
1731
+ if (this.options.clock !== undefined)
1732
+ options.clock = this.options.clock;
1733
+ const report = prunePayloads(options);
1734
+ // Successes first, so the narrative reads in the order the pass ran them: a
1735
+ // prune that appended and could not unlink is a warning below, never a line
1736
+ // here (APRV-57).
1737
+ for (const done of report.pruned) {
1738
+ this.emit({
1739
+ event: "pruned",
1740
+ payload_hash: done.candidate.hash,
1741
+ reason: done.candidate.reason,
1742
+ action_key: done.candidate.actionKey,
1743
+ task: done.candidate.task,
1744
+ seq: done.seq,
1745
+ });
1746
+ }
1747
+ for (const warning of report.warnings) {
1748
+ this.warn("prune-refused", `${warning.code}: ${warning.message}`);
1749
+ }
1750
+ }
1751
+ // -------------------------------------------------------------------------
1752
+ // Projection write-back (SPEC.md §6.3, §10.2, APRV-62)
1753
+ // -------------------------------------------------------------------------
1754
+ /**
1755
+ * Rewrite every task file whose `state:` disagrees with the log, so the
1756
+ * projection says what the log says.
1757
+ *
1758
+ * §6.3: "`state` is a projection of log events; the file is updated by the
1759
+ * daemon after the event is appended, never the reverse." Both halves are
1760
+ * enforced structurally here. *After the event*: this runs at the end of the
1761
+ * tick, when the drift scan and the TTL sweep have appended everything they
1762
+ * are going to. *Never the reverse*: this method appends nothing at all, reads
1763
+ * the state it writes from `daemon/projection.ts`'s rollup over the verified
1764
+ * log, and produces bytes only through `core/task-file.ts`. A file can no more
1765
+ * teach the log a state than a screenshot can teach a database a row.
1766
+ *
1767
+ * Four rules, each of which is a way of not making things worse:
1768
+ *
1769
+ * 1. **Only files that already have an envelope.** `set-state` refuses
1770
+ * `no-envelope`, and that refusal is honoured silently: a task with no
1771
+ * `approval:` key is a plain Backlog.md task (SPEC.md §6 requires tolerating
1772
+ * it), and a daemon that gave one an envelope would be enrolling a task
1773
+ * nobody enrolled. The register path is where an envelope comes from.
1774
+ * 2. **No write when the bytes would not change.** The writer reports
1775
+ * `changed`, and the bytes are compared besides. An unnecessary write moves
1776
+ * an mtime, which wakes the watcher, which schedules a tick — a loop that
1777
+ * costs nothing but looks exactly like one that does not terminate.
1778
+ * 3. **A refusal leaves the file alone.** Anything the round-trip writer will
1779
+ * not do — corrupt YAML, an `approval:` key that is not a mapping, a
1780
+ * self-check that failed — becomes one `write-back-refused` warning carrying
1781
+ * the writer's own code. Nothing partial is ever written, because
1782
+ * `rewriteTaskFile` produces bytes or a refusal and `writeTaskFileAtomic`
1783
+ * renames a complete temp file into place.
1784
+ * 4. **Silence where the drift scan already spoke.** An unreadable file, a
1785
+ * frontmatter that does not parse, a missing `id`, a schema-invalid
1786
+ * envelope: each was warned about a few milliseconds ago by
1787
+ * {@link scanForDrift} over the same folder. Repeating it here would double
1788
+ * every line an operator reads without adding a fact.
1789
+ *
1790
+ * Loop safety comes from the comparison, not from a remembered flag: the next
1791
+ * tick derives the same state from the same log, finds the file already
1792
+ * declaring it, and does nothing — no drift, no write, no event. A file that
1793
+ * *keeps* needing repair is being rewritten by something else, and the drift
1794
+ * records are the trail of that fight.
1795
+ */
1796
+ writeBack() {
1797
+ const files = this.taskFiles();
1798
+ if (files.length === 0)
1799
+ return null;
1800
+ const records = this.read();
1801
+ if (!records.ok)
1802
+ return this.fatal(records);
1803
+ const ts = readClock(this.options.clock === undefined ? {} : { clock: this.options.clock });
1804
+ const ttlMs = this.ttlMs();
1805
+ const rewriteOptions = this.options.schemaDir === undefined ? {} : { schemaDir: this.options.schemaDir };
1806
+ for (const file of files) {
1807
+ let text;
1808
+ try {
1809
+ text = readFileSync(file, "utf8");
1810
+ }
1811
+ catch {
1812
+ // Warned by the drift scan, or a file that vanished between the two
1813
+ // passes. Either way the next tick re-scans.
1814
+ continue;
1815
+ }
1816
+ const parsed = parseFrontmatter(text);
1817
+ if (!parsed.ok)
1818
+ continue;
1819
+ const envelope = parsed.data["approval"];
1820
+ if (typeof envelope !== "object" || envelope === null || Array.isArray(envelope))
1821
+ continue;
1822
+ const id = parsed.data["id"];
1823
+ if (typeof id !== "string" || id.length === 0)
1824
+ continue;
1825
+ // Read boundary, as in the drift scan above (APRV-148): a historical
1826
+ // envelope the scan just read a claim out of must also be repairable, or
1827
+ // the drift it records stands forever.
1828
+ const validation = validate("envelope", envelope, this.options.schemaDir === undefined
1829
+ ? { mode: "historical" }
1830
+ : { schemaDir: this.options.schemaDir, mode: "historical" });
1831
+ if (!validation.ok)
1832
+ continue;
1833
+ const declaredRaw = envelope.state;
1834
+ const declaredState = typeof declaredRaw === "string" ? declaredRaw : null;
1835
+ const derived = taskEnvelopeState(records.records, id, ts, ttlMs).state;
1836
+ if (declaredState === derived)
1837
+ continue;
1838
+ const rewritten = rewriteTaskFile(text, { kind: "set-state", state: derived }, rewriteOptions);
1839
+ if (!rewritten.ok) {
1840
+ if (rewritten.code === "no-envelope")
1841
+ continue;
1842
+ this.warn("write-back-refused", `${this.display(file)}: the state: line could not be rewritten to ${derived} (${rewritten.code}): ${rewritten.message} The file is exactly as it was, the log is unchanged, and the envelope.drift record stands.`);
1843
+ continue;
1844
+ }
1845
+ if (!rewritten.changed || rewritten.bytes === text)
1846
+ continue;
1847
+ const written = writeTaskFileAtomic(file, rewritten.bytes);
1848
+ if (!written.ok) {
1849
+ this.warn("write-back-refused", `${this.display(file)}: the rewritten task file could not be placed (${written.code}): ${written.message}`);
1850
+ continue;
1851
+ }
1852
+ // The watcher is about to see this file change; the change was ours
1853
+ // (APRV-211). Recorded by basename because that is what `fs.watch` reports.
1854
+ this.selfWrites.add(basename(file));
1855
+ this.emit({
1856
+ event: "write_back",
1857
+ task: id,
1858
+ file: this.display(file),
1859
+ from: declaredState,
1860
+ to: derived,
1861
+ bytes: written.bytes,
1862
+ });
1863
+ }
1864
+ return null;
1865
+ }
1866
+ // -------------------------------------------------------------------------
1867
+ // Loop escalation (SPEC.md §10.2)
1868
+ // -------------------------------------------------------------------------
1869
+ /**
1870
+ * Report tasks that loop safety has escalated to manual, and tasks that have
1871
+ * come back.
1872
+ *
1873
+ * The projection is `core/loop.ts`'s and the enforcement is the gate's and the
1874
+ * executor's; this makes the state *visible* to whoever is watching the daemon,
1875
+ * which is the one thing neither of them does. `approval status` reports the
1876
+ * same set, from the same projection, for an operator who is not.
1877
+ *
1878
+ * Reported on change rather than every tick: a standing escalation restated
1879
+ * every interval is noise that trains an operator to scroll past it.
1880
+ */
1881
+ surfaceEscalations(records) {
1882
+ const current = new Set();
1883
+ for (const state of loopEscalation(records)) {
1884
+ if (!state.escalated)
1885
+ continue;
1886
+ current.add(state.task);
1887
+ if (!this.reportedEscalations.has(state.task)) {
1888
+ this.emit({
1889
+ event: "escalated",
1890
+ task: state.task,
1891
+ consecutive_failures: state.consecutiveFailures,
1892
+ });
1893
+ }
1894
+ }
1895
+ for (const task of this.reportedEscalations) {
1896
+ if (!current.has(task))
1897
+ this.emit({ event: "escalation_cleared", task });
1898
+ }
1899
+ this.reportedEscalations = current;
1900
+ return current.size;
1901
+ }
1902
+ // -------------------------------------------------------------------------
1903
+ // The queue projection (SPEC.md §9.1)
1904
+ // -------------------------------------------------------------------------
1905
+ /**
1906
+ * Regenerate `QUEUE.md` through the real renderer.
1907
+ *
1908
+ * Never partial: `writeQueue` writes a temp file and renames it, so a reader
1909
+ * sees either the previous queue or the new one and never a half-written file,
1910
+ * and a crashed daemon leaves no torn queue behind.
1911
+ *
1912
+ * The file is rewritten every tick, because TTL countdowns move even when the
1913
+ * log does not. The *event* is emitted only when the summary changes, so a
1914
+ * standing queue does not fill an operator's terminal with identical lines.
1915
+ */
1916
+ render() {
1917
+ const now = readClock(this.options.clock === undefined ? {} : { clock: this.options.clock });
1918
+ const result = writeQueue(this.options.logPath, this.options.queuePath, { policy: this.options.policy }, now);
1919
+ if (!result.ok) {
1920
+ this.warn("render-failed", `${this.display(this.options.queuePath)} was not regenerated (${result.code}): ${result.message}`);
1921
+ return;
1922
+ }
1923
+ this.renders += 1;
1924
+ const summary = {
1925
+ pending: result.pending,
1926
+ skipped: result.skipped,
1927
+ auditBacklog: result.auditBacklog,
1928
+ head: result.head === null ? null : result.head.seq,
1929
+ };
1930
+ const previous = this.lastRender;
1931
+ this.lastRender = summary;
1932
+ if (previous !== null &&
1933
+ previous.pending === summary.pending &&
1934
+ previous.skipped === summary.skipped &&
1935
+ previous.auditBacklog === summary.auditBacklog &&
1936
+ previous.head === summary.head) {
1937
+ return;
1938
+ }
1939
+ this.emit({
1940
+ event: "rendered",
1941
+ path: this.display(this.options.queuePath),
1942
+ bytes: result.bytes,
1943
+ pending: result.pending,
1944
+ skipped: result.skipped,
1945
+ audit_backlog: result.auditBacklog,
1946
+ });
1947
+ }
1948
+ // -------------------------------------------------------------------------
1949
+ // Output helpers
1950
+ // -------------------------------------------------------------------------
1951
+ emit(event) {
1952
+ this.options.sink.emit(event);
1953
+ }
1954
+ warn(code, message) {
1955
+ this.emit({ event: "warning", code, message });
1956
+ }
1957
+ /** A path as the operator typed it: relative to cwd when it is inside it. */
1958
+ display(path) {
1959
+ const rel = relative(this.options.cwd, path);
1960
+ return rel.length > 0 && !rel.startsWith("..") && !isAbsolute(rel) ? rel : path;
1961
+ }
1962
+ }
1963
+ /**
1964
+ * SHA-256 over the RFC 8785 form of the envelope, part of the drift dedupe key.
1965
+ *
1966
+ * `null` when the value cannot be canonicalized. A YAML mapping that survived the
1967
+ * hardened parser and `envelope.schema.json` always can, so this is a backstop
1968
+ * rather than a live branch — and a backstop that degrades to "dedupe on the
1969
+ * state pair alone" rather than to a thrown error inside a watcher callback.
1970
+ */
1971
+ function digestOf(envelope) {
1972
+ try {
1973
+ return payloadHash(envelope);
1974
+ }
1975
+ catch {
1976
+ return null;
1977
+ }
1978
+ }
1979
+ /** Does this path exist and is it a directory? Used by the verb's preflight. */
1980
+ export function isDirectory(path) {
1981
+ try {
1982
+ return statSync(path).isDirectory();
1983
+ }
1984
+ catch {
1985
+ return false;
1986
+ }
1987
+ }
1988
+ //# sourceMappingURL=daemon.js.map