approval-md 0.1.0 → 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 (266) hide show
  1. package/README.md +584 -553
  2. package/SPEC.md +42 -13
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +623 -0
  27. package/dist/src/channels/payload-view.d.ts +35 -0
  28. package/dist/src/channels/render-queue.d.ts +149 -0
  29. package/dist/src/channels/tagging.d.ts +196 -0
  30. package/dist/src/channels/telegram.d.ts +1832 -0
  31. package/dist/src/channels/web.d.ts +341 -0
  32. package/dist/src/cli/adapter.d.ts +90 -0
  33. package/dist/src/cli/adapter.js +25 -15
  34. package/dist/src/cli/adapter.js.map +1 -1
  35. package/dist/src/cli/amend.d.ts +59 -0
  36. package/dist/src/cli/args.d.ts +43 -0
  37. package/dist/src/cli/attest.d.ts +41 -0
  38. package/dist/src/cli/audit-card.d.ts +62 -0
  39. package/dist/src/cli/audit.d.ts +59 -0
  40. package/dist/src/cli/channel-telegram.d.ts +806 -0
  41. package/dist/src/cli/channel-web.d.ts +131 -0
  42. package/dist/src/cli/channel.d.ts +71 -0
  43. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  44. package/dist/src/cli/codex.d.ts +2 -0
  45. package/dist/src/cli/codex.js +172 -0
  46. package/dist/src/cli/codex.js.map +1 -0
  47. package/dist/src/cli/coverage.d.ts +61 -0
  48. package/dist/src/cli/daemon.d.ts +120 -0
  49. package/dist/src/cli/doctor.d.ts +129 -0
  50. package/dist/src/cli/doctor.js +119 -5
  51. package/dist/src/cli/doctor.js.map +1 -1
  52. package/dist/src/cli/env.d.ts +65 -0
  53. package/dist/src/cli/execute.d.ts +202 -0
  54. package/dist/src/cli/exit-codes.d.ts +73 -0
  55. package/dist/src/cli/feedback.d.ts +60 -0
  56. package/dist/src/cli/gate-window.d.ts +40 -0
  57. package/dist/src/cli/gate.d.ts +68 -0
  58. package/dist/src/cli/git-scope.d.ts +190 -0
  59. package/dist/src/cli/gloss-attach.d.ts +85 -0
  60. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  61. package/dist/src/cli/gloss-codex.d.ts +24 -0
  62. package/dist/src/cli/gloss-options.d.ts +42 -0
  63. package/dist/src/cli/gloss.d.ts +265 -0
  64. package/dist/src/cli/help.d.ts +103 -0
  65. package/dist/src/cli/help.js +173 -51
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +78 -0
  68. package/dist/src/cli/hook-codex.js +167 -0
  69. package/dist/src/cli/hook-codex.js.map +1 -0
  70. package/dist/src/cli/hook.d.ts +331 -0
  71. package/dist/src/cli/hook.js +186 -80
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.d.ts +35 -0
  74. package/dist/src/cli/init.d.ts +84 -0
  75. package/dist/src/cli/init.js +2 -2
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.d.ts +23 -0
  78. package/dist/src/cli/journal.d.ts +41 -0
  79. package/dist/src/cli/log-advance.d.ts +287 -0
  80. package/dist/src/cli/log-advance.js +102 -11
  81. package/dist/src/cli/log-advance.js.map +1 -1
  82. package/dist/src/cli/log-anchor.d.ts +176 -0
  83. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  84. package/dist/src/cli/log-sync.d.ts +243 -0
  85. package/dist/src/cli/log-verbs.d.ts +16 -0
  86. package/dist/src/cli/log-verbs.js +7 -1
  87. package/dist/src/cli/log-verbs.js.map +1 -1
  88. package/dist/src/cli/long-help.d.ts +70 -0
  89. package/dist/src/cli/main.d.ts +77 -0
  90. package/dist/src/cli/main.js +155 -5
  91. package/dist/src/cli/main.js.map +1 -1
  92. package/dist/src/cli/mcp.d.ts +52 -0
  93. package/dist/src/cli/paths.d.ts +56 -0
  94. package/dist/src/cli/payload.d.ts +58 -0
  95. package/dist/src/cli/policy.d.ts +43 -0
  96. package/dist/src/cli/preflight.d.ts +363 -0
  97. package/dist/src/cli/preflight.js +294 -7
  98. package/dist/src/cli/preflight.js.map +1 -1
  99. package/dist/src/cli/progress.d.ts +78 -0
  100. package/dist/src/cli/prompt.d.ts +209 -0
  101. package/dist/src/cli/quickstart.d.ts +46 -0
  102. package/dist/src/cli/quickstart.js +297 -0
  103. package/dist/src/cli/quickstart.js.map +1 -0
  104. package/dist/src/cli/records.d.ts +34 -0
  105. package/dist/src/cli/render.d.ts +22 -0
  106. package/dist/src/cli/sandbox.d.ts +51 -0
  107. package/dist/src/cli/scaffold.d.ts +79 -0
  108. package/dist/src/cli/setup-adapter.d.ts +137 -0
  109. package/dist/src/cli/setup-adapter.js +38 -4
  110. package/dist/src/cli/setup-adapter.js.map +1 -1
  111. package/dist/src/cli/setup-channel.d.ts +117 -0
  112. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  113. package/dist/src/cli/setup-common.d.ts +275 -0
  114. package/dist/src/cli/setup-flow.d.ts +287 -0
  115. package/dist/src/cli/setup-service.d.ts +96 -0
  116. package/dist/src/cli/setup.d.ts +202 -0
  117. package/dist/src/cli/style.d.ts +320 -0
  118. package/dist/src/cli/token.d.ts +39 -0
  119. package/dist/src/cli/up.d.ts +155 -0
  120. package/dist/src/cli/up.js +4 -2
  121. package/dist/src/cli/up.js.map +1 -1
  122. package/dist/src/cli/usage.d.ts +37 -0
  123. package/dist/src/cli/values.d.ts +40 -0
  124. package/dist/src/cli/vault.d.ts +59 -0
  125. package/dist/src/cli/vault.js +2 -2
  126. package/dist/src/cli/vault.js.map +1 -1
  127. package/dist/src/cli/verb-registry.d.ts +76 -0
  128. package/dist/src/cli/verb-registry.js +176 -8
  129. package/dist/src/cli/verb-registry.js.map +1 -1
  130. package/dist/src/cli/wordmark.d.ts +31 -0
  131. package/dist/src/cli/wordmark.js +2 -2
  132. package/dist/src/codex/doctor.d.ts +13 -0
  133. package/dist/src/codex/doctor.js +41 -0
  134. package/dist/src/codex/doctor.js.map +1 -0
  135. package/dist/src/codex/manifest.d.ts +49 -0
  136. package/dist/src/codex/manifest.js +103 -0
  137. package/dist/src/codex/manifest.js.map +1 -0
  138. package/dist/src/codex/templates.d.ts +41 -0
  139. package/dist/src/codex/templates.js +319 -0
  140. package/dist/src/codex/templates.js.map +1 -0
  141. package/dist/src/codex/trust.d.ts +19 -0
  142. package/dist/src/codex/trust.js +183 -0
  143. package/dist/src/codex/trust.js.map +1 -0
  144. package/dist/src/codex/workspace-plan.d.ts +131 -0
  145. package/dist/src/codex/workspace-plan.js +561 -0
  146. package/dist/src/codex/workspace-plan.js.map +1 -0
  147. package/dist/src/core/actor.d.ts +2 -0
  148. package/dist/src/core/actor.js +5 -0
  149. package/dist/src/core/actor.js.map +1 -0
  150. package/dist/src/core/advance-cycle.d.ts +170 -0
  151. package/dist/src/core/agents-md.d.ts +276 -0
  152. package/dist/src/core/apply-patch.d.ts +49 -0
  153. package/dist/src/core/apply-patch.js +266 -0
  154. package/dist/src/core/apply-patch.js.map +1 -0
  155. package/dist/src/core/attest.d.ts +420 -0
  156. package/dist/src/core/attest.js +13 -1
  157. package/dist/src/core/attest.js.map +1 -1
  158. package/dist/src/core/audit.d.ts +492 -0
  159. package/dist/src/core/budgets.d.ts +238 -0
  160. package/dist/src/core/checkpoint.d.ts +500 -0
  161. package/dist/src/core/child-env.d.ts +88 -0
  162. package/dist/src/core/clock.d.ts +52 -0
  163. package/dist/src/core/command-class.d.ts +543 -0
  164. package/dist/src/core/command-class.js +43 -8
  165. package/dist/src/core/command-class.js.map +1 -1
  166. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  167. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  168. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  169. package/dist/src/core/coverage.d.ts +217 -0
  170. package/dist/src/core/credential-spec.d.ts +72 -0
  171. package/dist/src/core/dark-session.d.ts +331 -0
  172. package/dist/src/core/decision-refusal.d.ts +185 -0
  173. package/dist/src/core/env-file.d.ts +450 -0
  174. package/dist/src/core/execute.d.ts +858 -0
  175. package/dist/src/core/execute.js +44 -6
  176. package/dist/src/core/execute.js.map +1 -1
  177. package/dist/src/core/frontmatter.d.ts +78 -0
  178. package/dist/src/core/gate-window.d.ts +312 -0
  179. package/dist/src/core/gate.d.ts +1364 -0
  180. package/dist/src/core/gate.js +68 -13
  181. package/dist/src/core/gate.js.map +1 -1
  182. package/dist/src/core/git-run.d.ts +73 -0
  183. package/dist/src/core/harness-version.d.ts +157 -0
  184. package/dist/src/core/harness-version.js +2 -1
  185. package/dist/src/core/harness-version.js.map +1 -1
  186. package/dist/src/core/harness-wait.d.ts +55 -0
  187. package/dist/src/core/head-retry.d.ts +107 -0
  188. package/dist/src/core/instance.d.ts +253 -0
  189. package/dist/src/core/intake-limits.d.ts +247 -0
  190. package/dist/src/core/jcs.d.ts +52 -0
  191. package/dist/src/core/journal.d.ts +144 -0
  192. package/dist/src/core/live-draw.d.ts +436 -0
  193. package/dist/src/core/log-reconcile.d.ts +89 -0
  194. package/dist/src/core/log-subscribe.d.ts +36 -0
  195. package/dist/src/core/log-subscribe.js +162 -0
  196. package/dist/src/core/log-subscribe.js.map +1 -0
  197. package/dist/src/core/log.d.ts +278 -0
  198. package/dist/src/core/loop.d.ts +274 -0
  199. package/dist/src/core/loop.js +11 -0
  200. package/dist/src/core/loop.js.map +1 -1
  201. package/dist/src/core/md-fence.d.ts +41 -0
  202. package/dist/src/core/money.d.ts +147 -0
  203. package/dist/src/core/payload-census.d.ts +74 -0
  204. package/dist/src/core/payload-store.d.ts +175 -0
  205. package/dist/src/core/payload.d.ts +71 -0
  206. package/dist/src/core/policy-diff.d.ts +292 -0
  207. package/dist/src/core/policy-diff.js +27 -4
  208. package/dist/src/core/policy-diff.js.map +1 -1
  209. package/dist/src/core/policy-expectations.d.ts +199 -0
  210. package/dist/src/core/policy-explain.d.ts +150 -0
  211. package/dist/src/core/policy-explain.js +31 -3
  212. package/dist/src/core/policy-explain.js.map +1 -1
  213. package/dist/src/core/policy-load.d.ts +527 -0
  214. package/dist/src/core/policy-load.js +15 -3
  215. package/dist/src/core/policy-load.js.map +1 -1
  216. package/dist/src/core/policy-match.d.ts +281 -0
  217. package/dist/src/core/policy-match.js +20 -9
  218. package/dist/src/core/policy-match.js.map +1 -1
  219. package/dist/src/core/policy-proposal.d.ts +265 -0
  220. package/dist/src/core/prompt-layout.d.ts +221 -0
  221. package/dist/src/core/protected-path-guard.d.ts +453 -0
  222. package/dist/src/core/protected-path-guard.js +514 -35
  223. package/dist/src/core/protected-path-guard.js.map +1 -1
  224. package/dist/src/core/registration.d.ts +25 -0
  225. package/dist/src/core/reindex.d.ts +99 -0
  226. package/dist/src/core/sampler.d.ts +313 -0
  227. package/dist/src/core/sandbox.d.ts +290 -0
  228. package/dist/src/core/seal.d.ts +165 -0
  229. package/dist/src/core/state.d.ts +505 -0
  230. package/dist/src/core/task-file.d.ts +185 -0
  231. package/dist/src/core/telegram-config.d.ts +93 -0
  232. package/dist/src/core/token.d.ts +409 -0
  233. package/dist/src/core/token.js +21 -38
  234. package/dist/src/core/token.js.map +1 -1
  235. package/dist/src/core/validate.d.ts +138 -0
  236. package/dist/src/core/values.d.ts +137 -0
  237. package/dist/src/core/vault.d.ts +291 -0
  238. package/dist/src/core/verified-snapshot.d.ts +204 -0
  239. package/dist/src/core/verify.d.ts +336 -0
  240. package/dist/src/core/version.d.ts +8 -0
  241. package/dist/src/core/wysiwys.d.ts +370 -0
  242. package/dist/src/daemon/advance-child.d.ts +39 -0
  243. package/dist/src/daemon/advance.d.ts +466 -0
  244. package/dist/src/daemon/audit.d.ts +87 -0
  245. package/dist/src/daemon/daemon.d.ts +1180 -0
  246. package/dist/src/daemon/dark-session.d.ts +64 -0
  247. package/dist/src/daemon/draw-child.d.ts +36 -0
  248. package/dist/src/daemon/draw.d.ts +154 -0
  249. package/dist/src/daemon/git-evidence.d.ts +173 -0
  250. package/dist/src/daemon/git-evidence.js +1 -1
  251. package/dist/src/daemon/projection.d.ts +180 -0
  252. package/dist/src/daemon/prune.d.ts +207 -0
  253. package/dist/src/mcp/http.d.ts +113 -0
  254. package/dist/src/mcp/server.d.ts +265 -0
  255. package/dist/src/mcp/server.js +9 -1
  256. package/dist/src/mcp/server.js.map +1 -1
  257. package/docs/adapter-api.md +106 -0
  258. package/docs/cli-reference.md +389 -36
  259. package/docs/codex-enforced-session.md +30 -0
  260. package/package.json +12 -2
  261. package/schema/codex-instance.schema.json +82 -0
  262. package/schema/event.schema.json +2 -1
  263. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  264. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  265. package/schema/policy.schema.json +21 -1
  266. package/templates/codex/README.md +9 -0
@@ -0,0 +1,1180 @@
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 { type Clock } from "../core/clock.js";
92
+ import { type AutoMergeState } from "../cli/log-advance.js";
93
+ import { type AdvanceAttempt, type AdvanceCadence, type AdvanceOutcome } from "./advance.js";
94
+ import { type DarkSessionSweepOptions, type DarkSessionWatch } from "./dark-session.js";
95
+ import type { DrawServeResult } from "./draw.js";
96
+ import type { GitEvidenceRecorder } from "./git-evidence.js";
97
+ import { type PruneReason } from "./prune.js";
98
+ /**
99
+ * SPEC.md §8: runtime-originated events carry a `system:` actor. The daemon is
100
+ * the runtime, and `envelope.drift` is its own event — distinct from
101
+ * `system:gate`, which `core/gate.ts` stamps on the expiries it appends, so a
102
+ * reader can tell which part of the runtime spoke.
103
+ */
104
+ export declare const DAEMON_ACTOR = "system:daemon";
105
+ /** Backlog.md's conventional task folder, relative to the working directory. */
106
+ export { DEFAULT_TASKS_DIR } from "../core/registration.js";
107
+ /** How often the daemon looks, absent any watcher event. */
108
+ export declare const DEFAULT_INTERVAL_MS = 30000;
109
+ /** How long a burst of watcher events is allowed to settle before a tick. */
110
+ export declare const DEFAULT_DEBOUNCE_MS = 250;
111
+ /** The two directories this loop watches, as they are named in its output. */
112
+ export type WatchSource = "tasks" | "log";
113
+ /**
114
+ * Why a watcher event did not schedule a tick (APRV-230).
115
+ *
116
+ * Machine-readable and a closed union, for the reason {@link
117
+ * DAEMON_WARNING_CODES} is: an operator counting phantom ticks groups the trace
118
+ * by this field, and a free-text sentence would make that a grep instead of a
119
+ * count.
120
+ */
121
+ export declare const WATCH_IGNORE_REASONS: readonly [
122
+ /** A task file this daemon's own write-back pass placed (APRV-211). */
123
+ "self-write",
124
+ /** The `.<name>.tmp-<pid>-<n>` file this process writes a task file through. */
125
+ "own-temp",
126
+ /** A bookkeeping file: see {@link bookkeepingKind}. */
127
+ "bookkeeping",
128
+ /** Something in the log's directory that is not the log (the snapshot). */
129
+ "not-the-log"];
130
+ export type WatchIgnoreReason = (typeof WATCH_IGNORE_REASONS)[number];
131
+ /**
132
+ * What kind of bookkeeping file `name` is, or `null` when it is not one.
133
+ *
134
+ * The files that live in these two directories without ever being their
135
+ * subject: the append lockfile every writer in this runtime creates and removes
136
+ * (`<log>.lock`, `core/log.ts`), the swap, autosave, backup and lock files an
137
+ * editor scatters beside a file it is editing, and macOS's own `.DS_Store` and
138
+ * AppleDouble residue. Each of these is a filesystem event about how a change
139
+ * was made and never about the change; a tick scheduled for one re-derives an
140
+ * answer nothing has moved (APRV-230, and the lockfile pair is two events per
141
+ * append from every writer on the machine).
142
+ *
143
+ * Safe for exactly the reason {@link Daemon.attachWatchers} documents: the
144
+ * watcher is a latency optimization and correctness never depended on it
145
+ * (SPEC.md §10.2), so the worst an over-eager name here can cost is one
146
+ * `--interval` of latency. Nothing in this list can name a Backlog.md task file,
147
+ * which is `<id> - <slug>.md`.
148
+ */
149
+ export declare function bookkeepingKind(name: string): string | null;
150
+ /**
151
+ * One line of daemon output. **Frozen shape** in the same sense every `--json`
152
+ * shape in this CLI is frozen: an operator's log pipeline and an agent's
153
+ * supervisor branch on `event`, so entries may be added and none may be
154
+ * repurposed. Each is printed as one JSON object per line under `--json`, and as
155
+ * one human sentence otherwise.
156
+ */
157
+ export type DaemonEvent = {
158
+ event: "started";
159
+ log: string;
160
+ tasks: string;
161
+ queue: string;
162
+ interval_ms: number;
163
+ debounce_ms: number;
164
+ watching: boolean;
165
+ /**
166
+ * Which prefix proof this run's verified reads run (APRV-217). Additive,
167
+ * like every other growth of this union. It is on the FIRST line the
168
+ * daemon prints because it is a configuration an operator has to be able
169
+ * to see without asking the process anything.
170
+ */
171
+ read_proof: "full" | "incremental";
172
+ /**
173
+ * The committed copy of the log this run is anchored against (APRV-219).
174
+ * Additive, like every other growth of this union, and on the first line
175
+ * for the reason `read_proof` is: which external witness a run holds
176
+ * itself to is not something an operator should have to ask the process
177
+ * about. `rev: null` is an honest "none was found", never a silent pass.
178
+ */
179
+ anchor: {
180
+ rev: string | null;
181
+ seq: number | null;
182
+ reason: string | null;
183
+ };
184
+ /**
185
+ * Where this run is answering live draws, or `null` when it is not
186
+ * (APRV-208). Additive, and on the first line for the reason `read_proof`
187
+ * is: whether `supervised-live` is actually live on this machine is a
188
+ * configuration an operator must be able to see without asking the process
189
+ * anything, and the difference between "sampled at 10%" and "gated at
190
+ * 100%" is exactly this field being a path rather than `null`.
191
+ */
192
+ draw?: string | null;
193
+ /**
194
+ * Advance cycles nobody closed that this checkout's refs cannot prove
195
+ * (APRV-264). Additive, like every other growth of this union.
196
+ *
197
+ * On the FIRST line because it is the one thing that will stop the
198
+ * cadence for the whole of this run and the one thing this process cannot
199
+ * fix: an operator who reads it here does not have to wait out a tick to
200
+ * find out that the advance is blocked, and they get the command that
201
+ * clears it. Empty on a healthy start, and empty when the startup sweep
202
+ * closed everything it found — what is here is precisely what is owed to
203
+ * a person.
204
+ */
205
+ dangling_advances?: string[];
206
+ } | {
207
+ event: "drift";
208
+ task: string;
209
+ file: string;
210
+ declared_state: string | null;
211
+ derived_state: string;
212
+ seq: number;
213
+ /**
214
+ * Why (APRV-63). Present only for `envelope-missing`: a `state-mismatch`
215
+ * line is what every `drift` line has always been, and a field that
216
+ * appeared on all of them would change a shape supervisors already parse.
217
+ */
218
+ reason?: "envelope-missing";
219
+ } | {
220
+ /**
221
+ * A task file's `state:` was rewritten to match the log (SPEC.md §6.3).
222
+ * Additive (APRV-62): the union grows, and no existing entry changes
223
+ * meaning. A rewritten file is a change to a human's working tree, so it
224
+ * gets a line of its own rather than hiding inside the drift record that
225
+ * preceded it.
226
+ */
227
+ event: "write_back";
228
+ task: string;
229
+ file: string;
230
+ /** The `state:` the file claimed, or `null` when it declared none. */
231
+ from: string | null;
232
+ /** The state the log implies, now on disk. */
233
+ to: string;
234
+ bytes: number;
235
+ } | {
236
+ event: "expired";
237
+ action_key: string;
238
+ task: string | null;
239
+ seq: number;
240
+ } | {
241
+ /**
242
+ * A supervised execution was drawn for retrospective review (APRV-40,
243
+ * SPEC.md §5.2). Additive (APRV-57): the union grows and no existing entry
244
+ * changes meaning. APRV-40 left successful samples visible only as a
245
+ * `rendered` backlog that grew, which tells an operator that *something*
246
+ * was sampled without telling them what; this names it.
247
+ */
248
+ event: "sampled";
249
+ action_key: string;
250
+ task: string | null;
251
+ /** `seq` of the appended `audit.sampled` record. */
252
+ seq: number;
253
+ /** `seq` of the `execution.started` record the sample named. */
254
+ subject_seq: number;
255
+ } | {
256
+ /**
257
+ * A payload's bytes were removed under `payload_retention` (APRV-41,
258
+ * amended SPEC.md §5.2). Additive (APRV-57), and emitted only for a prune
259
+ * that both appended its `payload.pruned` and unlinked the file: a prune
260
+ * that appended and could not unlink is already a `prune-refused` warning,
261
+ * and a crash-window completion appends nothing, so it has no `seq` to
262
+ * name and stays out of this line.
263
+ *
264
+ * No byte count: `daemon/prune.ts` unlinks by hash and never stats the
265
+ * file, and a size read here would be a fresh filesystem question asked
266
+ * after the answer stopped existing.
267
+ */
268
+ event: "pruned";
269
+ payload_hash: string;
270
+ reason: PruneReason;
271
+ /** The action whose terminal state released the bytes; `null` for an orphan. */
272
+ action_key: string | null;
273
+ task: string | null;
274
+ /** `seq` of the appended `payload.pruned` record. */
275
+ seq: number;
276
+ } | {
277
+ event: "rendered";
278
+ path: string;
279
+ bytes: number;
280
+ pending: number;
281
+ skipped: number;
282
+ audit_backlog: number;
283
+ } | {
284
+ /**
285
+ * The log was advanced onto a records branch, or an attempt to advance it
286
+ * ended some other way (APRV-204). Additive: the union grows and no
287
+ * existing entry changes meaning.
288
+ *
289
+ * One line per ATTEMPT, including the refused and gated ones, because the
290
+ * thing an operator needs to see is that the cadence is running and what
291
+ * it met — an advance that silently did not happen is the failure mode
292
+ * this whole feature exists to remove.
293
+ */
294
+ event: "advance";
295
+ outcome: AdvanceOutcome;
296
+ /** Records not yet on a records branch, at the moment of the attempt. */
297
+ records_pending: number;
298
+ records_branch: string | null;
299
+ /** The seq range this attempt published, or `null` when it published none. */
300
+ range: {
301
+ from: number;
302
+ to: number;
303
+ } | null;
304
+ commit: string | null;
305
+ pr_url: string | null;
306
+ /** True when this attempt opened the day's pull request rather than updating it. */
307
+ pr_created: boolean;
308
+ /**
309
+ * What became of the auto-merge arm on that pull request (APRV-284), and
310
+ * why when it was not armed. `null` when no pull request step ran.
311
+ *
312
+ * Reported because "the records are pushed" and "the records will land"
313
+ * are different facts, and only the second one means nobody has anything
314
+ * left to do.
315
+ */
316
+ auto_merge: AutoMergeState | null;
317
+ auto_merge_note: string | null;
318
+ /**
319
+ * True when the day's records branch was REBUILT on the base rather than
320
+ * stacked on its own tip, and the ref it was rebuilt on (APRV-234).
321
+ *
322
+ * A branch the trunk has moved under cannot be fast-forwarded into it,
323
+ * and a daemon that kept stacking on it produced a pull request only a
324
+ * hand merge could land. Rebuilding is the repair, and an operator reading
325
+ * this stream should not have to infer that it happened from a sha.
326
+ */
327
+ rebuilt: boolean;
328
+ rebuilt_on: string | null;
329
+ /** The refusal or failure code, when the outcome carries one. */
330
+ code: string | null;
331
+ message: string;
332
+ /** True when this attempt was the graceful-shutdown flush. */
333
+ flush: boolean;
334
+ } | {
335
+ /**
336
+ * One subject of a dark-session sweep (APRV-192). Additive: the union
337
+ * grows and no existing entry changes meaning.
338
+ *
339
+ * One line per subject that is NOT clean — dark, or undetermined —
340
+ * because the sweep's whole point is the thing nobody was told about, and
341
+ * a line per healthy worktree would bury it. A `dark` line names the
342
+ * `audit.dark_session` record it appended, or says the log already
343
+ * carried this observation; an `undetermined` line names what could not
344
+ * be established, which is never reported as a pass.
345
+ */
346
+ event: "dark_session";
347
+ verdict: "dark" | "undetermined";
348
+ /** The checkout: a worktree directory's name, or `primary`. */
349
+ subject: string;
350
+ branch: string | null;
351
+ code: string;
352
+ /** Commits observed on this subject inside the window. */
353
+ commits: number;
354
+ /** `seq` of the appended record, `null` when nothing was appended. */
355
+ seq: number | null;
356
+ /** True when a prior record already carried this observation key. */
357
+ already_recorded: boolean;
358
+ message: string;
359
+ } | {
360
+ event: "escalated";
361
+ task: string;
362
+ consecutive_failures: number;
363
+ } | {
364
+ event: "escalation_cleared";
365
+ task: string;
366
+ } | {
367
+ /**
368
+ * One filesystem watcher event, as the platform delivered it (APRV-230).
369
+ *
370
+ * Emitted only under {@link DaemonOptions.traceWatch}, because a busy
371
+ * checkout produces several of these per second and they are a diagnostic
372
+ * rather than a narrative. It is what the phantom-tick investigation had
373
+ * no way to ask: which directory fired, what the platform called the
374
+ * event, which file it named, and whether this loop acted on it.
375
+ *
376
+ * Report-only in the strongest sense: nothing downstream reads it, and
377
+ * turning the trace on changes no verdict and no schedule.
378
+ */
379
+ event: "watch";
380
+ /** Which watcher fired: the task folder, or the log's directory. */
381
+ watcher: WatchSource;
382
+ /** The platform's own event name (`rename`, `change`). */
383
+ type: string;
384
+ /** The file the platform named, or `null` when it named none. */
385
+ file: string | null;
386
+ /** Whether this event scheduled a tick. */
387
+ action: "scheduled" | "ignored";
388
+ /** Why it was ignored, or `null` when it scheduled. */
389
+ reason: WatchIgnoreReason | null;
390
+ } | {
391
+ event: "tick";
392
+ n: number;
393
+ head: number | null;
394
+ drift: number;
395
+ expired: number;
396
+ escalated: number;
397
+ /**
398
+ * What the tick cost (APRV-211). Additive, like every other growth of this
399
+ * union: the fields below were appended and nothing above them changed
400
+ * meaning. A tick is the daemon's unit of work and it was possible for one
401
+ * to pin a core for three seconds while every line it printed looked
402
+ * healthy; these three fields are how that is visible without a profiler.
403
+ */
404
+ /** Wall-clock duration of the whole tick, in milliseconds. */
405
+ ms: number;
406
+ /** Verified log reads this tick made. Bounded by structure, not by size. */
407
+ reads: number;
408
+ /**
409
+ * Which path this tick's reads took (APRV-217). Additive. `full` when any
410
+ * read this tick hashed the whole prefix — a full re-proof, a cadence
411
+ * boundary, a guard failure, or a cold walk — and `incremental` when
412
+ * every one of them was served from a carried hash state. Under
413
+ * `read_proof: full` it is `full` on every tick, which is the honest
414
+ * report: that is the path those reads took.
415
+ */
416
+ reproof: "full" | "incremental";
417
+ /**
418
+ * The anchor comparison this tick made, or `null` when it made none
419
+ * (APRV-219). Additive. The check rides the full re-proof cadence: a tick
420
+ * whose reads were all served from a carried hash state has re-proved
421
+ * nothing about the file's prefix and has nothing new to compare against
422
+ * a committed copy that cannot have changed since the last look.
423
+ */
424
+ anchor?: {
425
+ status: "pass" | "behind" | "skip";
426
+ rev: string | null;
427
+ seq: number | null;
428
+ };
429
+ /**
430
+ * The checkpoint check this tick made, or `null` when it made none
431
+ * (APRV-220). Additive, and on the same full re-proof cadence as the
432
+ * anchor for the same reason: a tick that re-proved nothing about the
433
+ * prefix has learned nothing new about the records inside it.
434
+ */
435
+ checkpoints?: {
436
+ status: "pass" | "skip";
437
+ verified: number;
438
+ keys: number;
439
+ /**
440
+ * Whether `audit.checkpoint_every` says one is DUE (APRV-257).
441
+ *
442
+ * The daemon's half of the cadence, and the same answer the channel
443
+ * prompt is enqueued from: both read `core/checkpoint.ts`'s one
444
+ * due-ness rule, so there is no arrangement in which this line says a
445
+ * checkpoint is owed and no prompt is offered, or the reverse. The
446
+ * ENQUEUE is the dispatch cycle's, which is where SPEC.md §10.2's
447
+ * dispatch job lives in this runtime (see `cli/channel-telegram.ts`'s
448
+ * module doc): the daemon holds no channel credential and reaches no
449
+ * network, and giving it either to send one prompt would undo that.
450
+ *
451
+ * Report-only, like everything else on this path. Nothing turns a
452
+ * `true` into a refusal; the outcome beside it is a `warn`, never a
453
+ * `fatal`.
454
+ */
455
+ due: boolean;
456
+ };
457
+ /**
458
+ * What woke this tick (APRV-230). Additive, and report-only.
459
+ *
460
+ * `log` and `tasks` name the watcher whose event opened the debounce
461
+ * window this tick closed; `interval` covers everything else (the
462
+ * periodic tick, the startup tick, and `--once`). The 2026-09-02 incident
463
+ * was diagnosed by counting ticks at an unchanged head, and this field is
464
+ * that count without a profiler: a tick that says `log` with no append
465
+ * behind it is a watcher event this daemon has not learned to attribute.
466
+ */
467
+ woke_by: WatchSource | "interval";
468
+ /**
469
+ * The file the waking event named, when it named one. Absent for an
470
+ * `interval` tick and for a platform event that named no file.
471
+ */
472
+ woke_file?: string;
473
+ /** Per-phase duration in milliseconds, in the order the tick runs them. */
474
+ phases: {
475
+ drift: number;
476
+ ttl: number;
477
+ audit: number;
478
+ dark: number;
479
+ prune: number;
480
+ write_back: number;
481
+ advance: number;
482
+ escalations: number;
483
+ render: number;
484
+ };
485
+ } | {
486
+ event: "warning";
487
+ code: DaemonWarningCode;
488
+ message: string;
489
+ } | {
490
+ event: "stopped";
491
+ reason: string;
492
+ ticks: number;
493
+ drift: number;
494
+ expired: number;
495
+ renders: number;
496
+ };
497
+ /**
498
+ * Why the daemon complained without stopping. Machine-readable and distinct, per
499
+ * SPEC.md §11.1 invariant 6, and a closed union for the same reason the gate's
500
+ * refusal codes are: a supervisor that branches on them needs them stable.
501
+ */
502
+ export declare const DAEMON_WARNING_CODES: readonly [
503
+ /** A task file could not be read (permissions, a vanished file). */
504
+ "task-unreadable",
505
+ /** A task file's frontmatter does not parse. */
506
+ "frontmatter-invalid",
507
+ /** The `approval:` envelope failed `envelope.schema.json`. */
508
+ "envelope-invalid",
509
+ /** The frontmatter carries no usable `id`, so no drift can be keyed to it. */
510
+ "task-id-missing",
511
+ /** The task folder could not be listed; the next tick tries again. */
512
+ "tasks-dir-unreadable",
513
+ /** An append was refused. The next tick re-derives and may try again. */
514
+ "append-refused",
515
+ /** The TTL sweep's `expire` refused for a reason other than a race. */
516
+ "expire-refused",
517
+ /** The queue could not be written. The log is untouched. */
518
+ "render-failed",
519
+ /** A watcher could not attach; the periodic tick covers the folder anyway. */
520
+ "watch-unavailable",
521
+ /**
522
+ * A payload-retention prune did not complete (APRV-41). The store keeps the
523
+ * file, and the next tick re-derives; nothing is ever deleted unlogged.
524
+ */
525
+ "prune-refused",
526
+ /**
527
+ * The projection write-back was refused by the writer, or the rewritten bytes
528
+ * could not be placed (APRV-62). The file is left exactly as it was and the
529
+ * log is untouched; the message carries `core/task-file.ts`'s own code.
530
+ */
531
+ "write-back-refused",
532
+ /**
533
+ * A cadence advance did not publish (APRV-204): the gate sent it to a human,
534
+ * refused it, or the verb itself failed. Nothing was committed, the outcome
535
+ * is on the `advance` line beside this warning, and the next tick tries
536
+ * again — the cadence interval is the retry bound, so there is no hot loop.
537
+ */
538
+ "advance-refused",
539
+ /**
540
+ * A dark-session sweep (APRV-192) found git activity it could not judge, or
541
+ * could not append the observation it did reach. Uncertainty is reported as
542
+ * uncertainty and never as a pass; nothing is escalated on it, because a
543
+ * detector reports and the gate decides.
544
+ */
545
+ "dark-session-undetermined",
546
+ /**
547
+ * The working log is a strict PREFIX of its committed copy (APRV-219): the
548
+ * anchor carries records this file does not. Not a divergence — the two are
549
+ * one chain and this checkout is behind it — and not silence either, because
550
+ * an append onto the shorter chain forks it. `approval log sync` is the
551
+ * repair, and the next tick asks again.
552
+ */
553
+ "anchor-behind",
554
+ /**
555
+ * `audit.checkpoint_every` says a human-signed checkpoint is due and the log
556
+ * carries none that recent (APRV-220). A WARNING and never a stop, at every
557
+ * layer: a human who has been away is not a forger, and a daemon that stopped
558
+ * for want of a tap is a daemon whose operator turns the check off.
559
+ */
560
+ "checkpoint-due",
561
+ /**
562
+ * The live-draw socket could not be served (APRV-208). Nothing is degraded:
563
+ * every asker fails closed to a human decision, which is exactly what happens
564
+ * on a machine where no daemon runs at all. It is a warning rather than a
565
+ * silence because the operator's `supervised-live` classes are gating at 100%
566
+ * while it stands, and that is a thing to know rather than to discover from a
567
+ * month of taps.
568
+ */
569
+ "draw-unavailable"];
570
+ export type DaemonWarningCode = (typeof DAEMON_WARNING_CODES)[number];
571
+ /** Where daemon output goes. Injected, so the loop itself writes to nothing. */
572
+ export interface DaemonSink {
573
+ emit(event: DaemonEvent): void;
574
+ }
575
+ export interface DaemonOptions {
576
+ /** The append-only log (SPEC.md §9). Read every tick, appended rarely. */
577
+ logPath: string;
578
+ /** The Backlog.md task folder to watch. */
579
+ tasksDir: string;
580
+ /** Where `QUEUE.md` is regenerated (SPEC.md §9.1). */
581
+ queuePath: string;
582
+ /** Policy location, with `loadPolicy`'s semantics. */
583
+ policy: {
584
+ dir?: string;
585
+ file?: string;
586
+ };
587
+ /** Schema directory, passed to validation and to every append. */
588
+ schemaDir?: string;
589
+ /** Directory paths in output are reported relative to this. */
590
+ cwd: string;
591
+ /** Periodic tick, in milliseconds. Also the poll-assisted watcher fallback. */
592
+ intervalMs: number;
593
+ /** Watcher debounce, in milliseconds. */
594
+ debounceMs: number;
595
+ /** Run exactly one tick and stop. The cron-shaped invocation, and the tests'. */
596
+ once: boolean;
597
+ /**
598
+ * Emit a `watch` line for every filesystem watcher event (APRV-230), ignored
599
+ * ones included. Off by default: it is a diagnostic an operator turns on to
600
+ * find out what is waking their daemon, and on a busy checkout it is several
601
+ * lines per second. It changes nothing else about the run.
602
+ */
603
+ traceWatch?: boolean;
604
+ /** The write-boundary clock, injected by tests (amended SPEC.md §8). */
605
+ clock?: Clock;
606
+ /**
607
+ * SPEC.md §8's optional git hardening (APRV-42), off unless the operator asked
608
+ * for it. When present, it is handed the verified head at the end of each
609
+ * tick; it decides everything else, reports through its own sink, and cannot
610
+ * change any verdict this loop reaches. See `daemon/git-evidence.ts`.
611
+ */
612
+ gitEvidence?: GitEvidenceRecorder;
613
+ /**
614
+ * The cadence advance (APRV-204), off unless the operator asked for it.
615
+ *
616
+ * Opt-in for the reason `gitEvidence` is: it pushes commits and opens pull
617
+ * requests on a remote, and a daemon that started doing that on an upgrade
618
+ * because a default changed under it would be the surprise this project
619
+ * exists to prevent. `approval daemon run --advance` turns it on.
620
+ */
621
+ advance?: AdvanceCadence;
622
+ /**
623
+ * The child that runs a periodic tick's advance (APRV-211). A test seam;
624
+ * production spawns `daemon/advance-child.js`. See {@link AdvanceInput.runner}.
625
+ */
626
+ advanceRunner?: {
627
+ command: string;
628
+ args: readonly string[];
629
+ };
630
+ /**
631
+ * The dark-session sweep (APRV-192), off unless the operator asked for it.
632
+ *
633
+ * Opt-in for the reason `gitEvidence` and `advance` are, though a milder one:
634
+ * it runs `git log` over every worktree of the checkout on a cadence, which
635
+ * on a large repository is real work, and a daemon that started doing it
636
+ * because a default moved under an operator would be the surprise this
637
+ * project exists to prevent. `approval daemon run --dark-sessions` turns it
638
+ * on. It is READ-ONLY against git and appends only its own observations.
639
+ */
640
+ darkSessions?: DarkSessionWatch;
641
+ /**
642
+ * Test seam for the sweep's observer: an answer that does not run git. The
643
+ * daemon never sets it, exactly as it never sets `today`.
644
+ */
645
+ observeGit?: DarkSessionSweepOptions["observe"];
646
+ /** The day the records branch is named for. Injected by tests. */
647
+ today?: string;
648
+ /**
649
+ * Publish a verified-head snapshot beside the log on every clean read
650
+ * (APRV-188). On unless explicitly set to `false`.
651
+ *
652
+ * On by default, unlike `gitEvidence` and `advance`, because it changes
653
+ * nothing outside this machine: the file is derived, local, byte-endorsing
654
+ * state that every reader re-proves and any reader may ignore. Turning it off
655
+ * costs hook latency and nothing else.
656
+ */
657
+ snapshot?: boolean;
658
+ /**
659
+ * Which prefix proof this loop's verified reads run (APRV-217).
660
+ *
661
+ * Absent means `full`: every read re-hashes the whole proved prefix, which is
662
+ * the behaviour of every release before this one. The CLI resolves it from
663
+ * the `daemon` policy block and the `--read-proof` family, flag first, and
664
+ * hands the answer down here so the loop itself reads no policy for it and
665
+ * cannot drift from the mode its `started` line printed.
666
+ */
667
+ readProof?: {
668
+ mode: "full" | "incremental";
669
+ everyReads: number;
670
+ afterMs: number;
671
+ };
672
+ /**
673
+ * The log-anchoring check (APRV-219). On unless explicitly disabled.
674
+ *
675
+ * On by default, unlike `gitEvidence` and `advance`, for the reason
676
+ * `snapshot` is: it READS git's object store and changes nothing anywhere,
677
+ * on this machine or any other. `rev` pins the anchor to one rev instead of
678
+ * the default resolution, which is what a test and an operator debugging a
679
+ * divergence both want.
680
+ */
681
+ anchor?: {
682
+ enabled?: boolean;
683
+ rev?: string;
684
+ remote?: string;
685
+ base?: string | null;
686
+ };
687
+ /**
688
+ * The human-signed checkpoint check (APRV-220). On unless explicitly
689
+ * disabled, for the reason the anchor is: it reads the log this loop has
690
+ * already verified plus the policy, and writes nothing anywhere.
691
+ */
692
+ checkpoints?: {
693
+ enabled?: boolean;
694
+ };
695
+ /**
696
+ * The live-draw server (APRV-208), or absent when this run answers no draws.
697
+ *
698
+ * Constructed by the CALLER, not here, and the reason is the sampling secret:
699
+ * the CLI resolves it from the environment the operator established (the one
700
+ * `eval "$(approval env)"` writes) and hands down a server that has closed
701
+ * over it. `DaemonOptions` therefore never carries a secret, this loop never
702
+ * sees one, and a daemon started in a shell where the secret does not resolve
703
+ * simply gets no server and every supervised-live action keeps gating, which
704
+ * is the behaviour of every release before this one.
705
+ *
706
+ * Typed structurally rather than as `DrawServer` so a test can inject one.
707
+ */
708
+ draw?: {
709
+ start(): DrawServeResult;
710
+ close(): void;
711
+ };
712
+ sink: DaemonSink;
713
+ }
714
+ /**
715
+ * How the loop ended.
716
+ *
717
+ * `stopped` is a clean shutdown (a signal, or `once` completing). The four
718
+ * failures mirror the CLI's frozen exit table exactly, so the verb maps them
719
+ * without inventing a code: an unreadable log is I/O, a torn tail is a crashed
720
+ * write, and a chain that does not verify is an integrity failure.
721
+ *
722
+ * `anchor-diverged` (APRV-219) is the fourth, and it is an integrity failure
723
+ * for the same reason `log-corrupt` is: the log this loop would append to is
724
+ * not the log somebody else already holds a committed copy of, and appending
725
+ * onto it would extend the wrong chain. It is a DISTINCT kind rather than a
726
+ * flavour of `log-corrupt` because the two say different things to whoever
727
+ * reads the stopped line — one means the file contradicts itself, the other
728
+ * means the file contradicts the record of it.
729
+ *
730
+ * `checkpoint-invalid` (APRV-220) is the fifth, and it is distinct from both
731
+ * for the same kind of reason: it means the file contradicts a signature a
732
+ * human made over it. The three failures name three different witnesses, and
733
+ * flattening them would leave the operator's first question — which witness
734
+ * disagrees? — answerable only by reading a message.
735
+ */
736
+ export type DaemonOutcome = {
737
+ kind: "stopped";
738
+ reason: string;
739
+ } | {
740
+ kind: "log-unreadable";
741
+ message: string;
742
+ } | {
743
+ kind: "log-torn-tail";
744
+ message: string;
745
+ } | {
746
+ kind: "log-corrupt";
747
+ message: string;
748
+ } | {
749
+ kind: "anchor-diverged";
750
+ message: string;
751
+ } | {
752
+ kind: "checkpoint-invalid";
753
+ message: string;
754
+ };
755
+ /**
756
+ * The daemon.
757
+ *
758
+ * One instance owns one working set (a log, a task folder, a queue file) and one
759
+ * set of timers. {@link run} resolves when the loop stops; {@link stop} is what a
760
+ * signal handler calls.
761
+ */
762
+ export declare class Daemon {
763
+ private readonly options;
764
+ private readonly watchers;
765
+ private watchAttempted;
766
+ private watching;
767
+ private interval;
768
+ private debounce;
769
+ private ticking;
770
+ private ticks;
771
+ private drifts;
772
+ private expiries;
773
+ private renders;
774
+ private lastRender;
775
+ /** Epoch ms of the last advance ATTEMPT, refusals included (APRV-204). */
776
+ private lastAdvanceAt;
777
+ private lastAdvance;
778
+ /** How many substantive records were owed at the last attempt. */
779
+ private lastAdvanceOwed;
780
+ /**
781
+ * Where the owed span ENDED at the last attempt (APRV-233).
782
+ *
783
+ * The count trigger measures against this inside the interval, so records an
784
+ * attempt has already tried to publish are not counted a second time towards
785
+ * publishing them again.
786
+ */
787
+ private lastAdvanceSpanEnd;
788
+ /**
789
+ * The advance whose git work is still running in a child (APRV-211).
790
+ *
791
+ * One slot, and a tick that finds it taken makes no attempt at all: two
792
+ * advances against one log would race for the append lock and for the records
793
+ * branch, and the second would have nothing to publish anyway.
794
+ */
795
+ private advanceInFlight;
796
+ /**
797
+ * An advance outcome this process observed and could not record (APRV-233).
798
+ *
799
+ * The 2026-09-02 residue: a hook's record landed between `recordFinish`'s
800
+ * read and its append, the bounded retry was spent, and the execution stayed
801
+ * open. The outcome is a fact this process holds and the log does not, so it
802
+ * is carried here and settled at the top of the next tick, before any trigger
803
+ * is looked at. Nothing else may advance while it stands.
804
+ */
805
+ private pendingAdvanceFinish;
806
+ /**
807
+ * The dangling advance cycles this process has already reported (APRV-264).
808
+ *
809
+ * A SET rather than one key: on 2026-09-05 five of them stood at once, and a
810
+ * single slot meant the operator was told about one, then about the next only
811
+ * after they had closed the first by hand. Reported once per key — the
812
+ * started line counts as the report for anything the startup listing found,
813
+ * so the first tick does not say it a second time — and never once per tick,
814
+ * because a warning printed every thirty seconds forever stops being read.
815
+ */
816
+ private reportedDangling;
817
+ /** Epoch ms of the last dark-session sweep (APRV-192); `null` before the first. */
818
+ private lastDarkSweepAt;
819
+ private reportedEscalations;
820
+ /** Verified reads made during the current tick (APRV-211). Reset at tick start. */
821
+ private reads;
822
+ /** Did any of this tick's own reads hash the whole prefix (APRV-217)? */
823
+ private fullReproofThisTick;
824
+ /** This tick's anchor comparison (APRV-219), or `null` when it made none. */
825
+ private anchorThisTick;
826
+ /** This tick's checkpoint check (APRV-220), or `null` when it made none. */
827
+ private checkpointsThisTick;
828
+ /**
829
+ * The watcher event that opened the current debounce window (APRV-230), or
830
+ * `null` when nothing has woken this daemon since the last tick consumed it.
831
+ * Read once at the top of a tick and cleared there, so a periodic tick that
832
+ * happens to run first takes the attribution with the work.
833
+ */
834
+ private pendingWake;
835
+ /** Basenames {@link writeBack} placed this tick, so the watcher can ignore them. */
836
+ private selfWrites;
837
+ /** The previous tick's, kept one generation: watch events arrive after the write. */
838
+ private previousSelfWrites;
839
+ private settle;
840
+ private finished;
841
+ /** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
842
+ private drawServing;
843
+ constructor(options: DaemonOptions);
844
+ /** Run until stopped (or, with `once`, for exactly one tick). */
845
+ run(): Promise<DaemonOutcome>;
846
+ /** Stop cleanly: timers cleared, watchers closed, nothing half-written. */
847
+ stop(reason: string): void;
848
+ private finish;
849
+ /**
850
+ * Attach watchers to the task folder and the log's directory.
851
+ *
852
+ * The log's *directory* rather than the log file: an append to a file is
853
+ * observable either way, but a log that does not exist yet cannot be watched at
854
+ * all, and a rename (which `writeQueue` and any future rotation perform) leaves
855
+ * a file watcher pointed at an inode nobody writes to again.
856
+ *
857
+ * Failure is a warning, never fatal — see the module header on why the periodic
858
+ * tick makes watching optional.
859
+ *
860
+ * ## Ignoring the daemon's own hand (APRV-211)
861
+ *
862
+ * Two of the files in these directories are written by this loop itself: the
863
+ * verified-head snapshot beside the log (`verified-head.json` and its temp
864
+ * file, published on every clean read) and the task files {@link writeBack}
865
+ * repairs. A watcher that fires on those schedules a tick whose only cause was
866
+ * the previous tick, and the daemon wakes itself forever: measured at 18 ticks
867
+ * in 45 seconds against a ten-minute interval, with no other writer.
868
+ *
869
+ * So the log-directory watcher schedules only for the log file itself (or for
870
+ * an event that names no file, which is the platform saying "something here
871
+ * changed" and must still be believed), and the tasks watcher ignores the
872
+ * basenames this daemon just placed.
873
+ *
874
+ * ## Bookkeeping, nobody's hand (APRV-230)
875
+ *
876
+ * The self-wake was not the whole of it. Both directories also carry files
877
+ * that no reader ever reads: the append lockfile every writer in this runtime
878
+ * creates and removes around each append, and the swap, autosave and backup
879
+ * files an editor scatters beside a task file. Those are events about how a
880
+ * change was made, and a tick scheduled for one re-derives an answer nothing
881
+ * has moved. {@link bookkeepingKind} names them and they are dropped here, in
882
+ * both directories.
883
+ *
884
+ * What is left unattributed on purpose is a platform event that names no file.
885
+ * That is the platform saying "something in this directory changed" and
886
+ * declining to say what, and the log itself is one of the things it might
887
+ * have been; believing it costs a tick and doubting it could cost an append's
888
+ * latency. {@link DaemonOptions.traceWatch} makes every one of these decisions
889
+ * visible, which is how the remaining wake sources get counted rather than
890
+ * guessed at.
891
+ *
892
+ * This is safe for exactly the reason stated in the module header: correctness
893
+ * never depended on the watcher. Every tick re-scans the folder and re-derives
894
+ * everything from the verified log, and the periodic tick runs regardless
895
+ * (SPEC.md §10.2). The worst an over-eager filter can cost is latency on a
896
+ * change that arrives inside the same window as one of the daemon's own
897
+ * writes, and the next periodic tick collects it.
898
+ */
899
+ private attachWatchers;
900
+ /**
901
+ * Whether one watcher event schedules a tick, and if not, why not (APRV-230).
902
+ *
903
+ * Pure, apart from reading the two sets of basenames this loop's own
904
+ * write-back placed. `null` means "schedule"; every other answer is a reason
905
+ * an operator can count.
906
+ */
907
+ private classifyWatchEvent;
908
+ /** Coalesce a burst of watcher events into one tick. */
909
+ private schedule;
910
+ /**
911
+ * One full pass: drift scan, TTL sweep, write-back, escalation surfacing,
912
+ * queue render.
913
+ *
914
+ * Returns `null` to continue, or the outcome that must stop the loop. Every
915
+ * step re-reads the verified log rather than sharing one snapshot across the
916
+ * pass, because each append invalidates the head the next one would build on
917
+ * and because an external writer may have moved the log in between. Reading is
918
+ * O(n) per call at v0.1 (`core/state.ts` documents the deferral); the head
919
+ * cache is APRV-43, and correctness comes first.
920
+ */
921
+ private tick;
922
+ /**
923
+ * Ask git what happened, and the log whether it was told.
924
+ *
925
+ * On its own interval rather than every tick: the tick is 30 seconds by
926
+ * default and a `git log` per worktree at that rate is work spent to re-read
927
+ * an unchanged answer. The interval is a floor and never a ceiling — a sweep
928
+ * missed because the daemon was down is simply made by the next one, since
929
+ * the sweep holds no cursor and re-derives its whole question from the window
930
+ * it is given.
931
+ *
932
+ * The daemon owns the SCHEDULING and nothing else, which is the division the
933
+ * drift scan, the TTL sweep and the audit sweep already keep.
934
+ */
935
+ private sweepDark;
936
+ /**
937
+ * Advance the log if the cadence says it is due, or if this is the flush.
938
+ *
939
+ * The trigger, in one place: enough SUBSTANTIVE records have accrued
940
+ * (`afterRecords`), or the interval has elapsed since the last attempt and at
941
+ * least one substantive record is owed. "Substantive" excludes the advance
942
+ * cycle's own bookkeeping — see `daemon/advance.ts` on why counting it would
943
+ * make an idle repository advance forever.
944
+ *
945
+ * The last-attempt clock is set for every attempt, successful or not, which
946
+ * is what keeps a refusal off the hot path: a gate that says no costs one
947
+ * attempt per interval and no more.
948
+ *
949
+ * The flush ignores the interval and the count, and only the interval and the
950
+ * count: it still asks the gate, and it still does nothing when nothing is
951
+ * owed.
952
+ */
953
+ /**
954
+ * The advance cycles the started line names, and nothing else (APRV-264).
955
+ *
956
+ * Reads and proves; appends nothing. The first tick runs a moment later and
957
+ * sweeps for real, so anything provable here is closed before an operator has
958
+ * finished reading the line — which is why the line carries only what is NOT
959
+ * provable, the part that needs a person. Every key it names is marked
960
+ * reported, so the sweep that follows does not warn about it again.
961
+ */
962
+ private listDanglingAdvancesAtStartup;
963
+ /** Everything `daemon/advance.ts` needs, built from this daemon's options. */
964
+ private advanceInputFor;
965
+ private advanceIfDue;
966
+ /** Record and report one finished attempt, from either runner. */
967
+ private reportAdvance;
968
+ /** The clock this loop reads, as the options every core writer takes it. */
969
+ private clockOptions;
970
+ /** The last attempt this process made, for a caller that wants to assert on it. */
971
+ lastAdvanceAttempt(): AdvanceAttempt | null;
972
+ /** Is the anchor check on for this run? On unless the operator turned it off. */
973
+ private anchorEnabled;
974
+ /** The anchor options this loop passes down, assembled once and identically. */
975
+ private anchorWhere;
976
+ /**
977
+ * Which committed copy this run is anchored against, for the `started` line.
978
+ *
979
+ * Resolution only: it asks git which revs carry a copy of the log and which
980
+ * of them reaches furthest, and compares nothing. The first tick, a line
981
+ * below the `started` emit, makes the comparison — and always in full, since
982
+ * a cold walk is a full re-proof by construction.
983
+ */
984
+ private resolveAnchorForReport;
985
+ /**
986
+ * Compare the verified working records against the committed copy.
987
+ *
988
+ * `null` when the check is off. Everything else — including "there is no
989
+ * committed copy" — comes back as an {@link AnchorCheck} the caller reports,
990
+ * because a check that could not look must never be read as a check that
991
+ * looked and was satisfied.
992
+ */
993
+ private compareToAnchor;
994
+ /**
995
+ * Demand every checkpoint inside the verified records.
996
+ *
997
+ * `null` when the check is off. Everything else — including "no key is
998
+ * configured" — comes back as a {@link CheckpointCheck} the caller reports,
999
+ * because a check that could not look must never be read as a check that
1000
+ * looked and was satisfied.
1001
+ *
1002
+ * The policy is read here, once per comparison rather than once per run: the
1003
+ * keys are the human's and the human may add one while this loop is running,
1004
+ * and a daemon holding a key list from startup would keep refusing a
1005
+ * checkpoint the operator had already authorized by editing the policy.
1006
+ */
1007
+ private checkCheckpoints;
1008
+ private fatal;
1009
+ /**
1010
+ * The verified log, and — since APRV-188 — the publication of what was
1011
+ * verified.
1012
+ *
1013
+ * The daemon holds a warm {@link VerifiedReadCache} and re-verifies only the
1014
+ * appended tail on every tick. Every hook process, by contrast, starts with an
1015
+ * empty cache and walks the whole chain before it may decide anything. So on
1016
+ * each clean read this loop publishes a verified-head snapshot beside the log:
1017
+ * an endorsement of the exact bytes it just walked, which the next hook
1018
+ * process re-proves for itself (one SHA-256) instead of re-walking. See
1019
+ * `core/verified-snapshot.ts` for what that endorsement claims and what a
1020
+ * reader still checks.
1021
+ *
1022
+ * The publication rides on the read rather than following it, so the bytes
1023
+ * endorsed are the bytes verified: a publisher that re-read the file to hash
1024
+ * it could endorse a digest of bytes nobody walked.
1025
+ */
1026
+ private read;
1027
+ private readOnce;
1028
+ /** The TTL in force right now, re-read every pass: policy files change. */
1029
+ private ttlMs;
1030
+ private gateOptions;
1031
+ /**
1032
+ * Read every task file, compare its claimed `state:` against the log, and
1033
+ * append `envelope.drift` for each file that contradicts it.
1034
+ *
1035
+ * §6.3: "`state` is a projection of log events; the file is updated by the
1036
+ * daemon after the event is appended, never the reverse. A file edit that
1037
+ * contradicts the log is itself logged (`envelope.drift`) and surfaced."
1038
+ *
1039
+ * This scan **only records**. The repair is {@link writeBack}, later in the
1040
+ * same tick: the disagreement is written to the log first and copied into the
1041
+ * file second, in that order, so nothing is ever corrected off the record. A
1042
+ * drift record therefore names a moment, not a standing condition.
1043
+ *
1044
+ * A schema-invalid envelope is warned about and skipped, not logged as drift: a
1045
+ * malformed file is not a *contradiction* of the log, it is a file the runtime
1046
+ * cannot read a claim out of at all, and inventing a `declared_state` for it
1047
+ * would put a fact in the log that nobody wrote.
1048
+ */
1049
+ private scanForDrift;
1050
+ /** Every `*.md` under the task folder, sorted, non-recursive. */
1051
+ private taskFiles;
1052
+ private checkOneFile;
1053
+ /**
1054
+ * A task file with no envelope whose task the log registered: the envelope was
1055
+ * lost (APRV-63, the defense half of APRV-60).
1056
+ *
1057
+ * Recorded as `envelope.drift` with `payload.reason: "envelope-missing"` —
1058
+ * the same event type, because it is the same §6.3 question ("the file and
1059
+ * the log disagree"), and a distinct reason, because the answer is different:
1060
+ * a state mismatch is an edit to reconcile, a missing envelope is a deletion
1061
+ * to restore. `declared_state` is `null` because the file makes no claim at
1062
+ * all, and `envelope_sha256` is absent because there is no envelope to digest.
1063
+ *
1064
+ * **Nothing is repaired.** The registration in the log holds every action the
1065
+ * envelope declared, so a writer *could* re-emit it — and that would turn a
1066
+ * projection into a source, which is the one thing the log's authority rests
1067
+ * on not happening. The daemon reports; a human restores by hand.
1068
+ *
1069
+ * `loose` says the id came from the file name rather than from frontmatter;
1070
+ * it relaxes only the *matching*, and the record is written under the id the
1071
+ * log itself holds.
1072
+ */
1073
+ private reportEnvelopeLoss;
1074
+ /**
1075
+ * Append `approval.expired` for every live request whose TTL has lapsed.
1076
+ *
1077
+ * The sweep changes no verdict. `core/gate.ts` already judges the TTL lazily at
1078
+ * decision time, so a late grant is refused with or without an expiry record;
1079
+ * what the sweep adds is *visibility* — the queue, the index, and anyone
1080
+ * reading the log see a terminal fact rather than a request that looks live and
1081
+ * is not.
1082
+ *
1083
+ * Idempotent three ways over, and none of them is a remembered flag:
1084
+ *
1085
+ * - with lazy expiry, because `expire` refuses `already-decided` for anything a
1086
+ * human (or the lazy path) already settled;
1087
+ * - with itself, because the candidate list is re-derived from the verified log
1088
+ * each sweep and an expired request no longer appears in it;
1089
+ * - across restarts, because the daemon carries no state between runs at all.
1090
+ *
1091
+ * A `head-moved` refusal is expected traffic, not a fault: a CLI verb decided
1092
+ * the same request between this candidate list and this append. It is reported
1093
+ * at `debug` weight (a single warning line) and the next tick re-derives.
1094
+ */
1095
+ private sweepTtl;
1096
+ /**
1097
+ * Hand one pass to `daemon/prune.ts` and surface whatever it could not do.
1098
+ *
1099
+ * The daemon adds nothing to the rule: with `payload_retention` absent the
1100
+ * pass is a no-op, and with it present the pruner appends `payload.pruned`
1101
+ * before every unlink and re-derives the whole question from the verified log.
1102
+ * Warnings never stop the loop — a store that could not be pruned is a store
1103
+ * holding more evidence than the policy asked it to, which is the safe side.
1104
+ */
1105
+ private prune;
1106
+ /**
1107
+ * Rewrite every task file whose `state:` disagrees with the log, so the
1108
+ * projection says what the log says.
1109
+ *
1110
+ * §6.3: "`state` is a projection of log events; the file is updated by the
1111
+ * daemon after the event is appended, never the reverse." Both halves are
1112
+ * enforced structurally here. *After the event*: this runs at the end of the
1113
+ * tick, when the drift scan and the TTL sweep have appended everything they
1114
+ * are going to. *Never the reverse*: this method appends nothing at all, reads
1115
+ * the state it writes from `daemon/projection.ts`'s rollup over the verified
1116
+ * log, and produces bytes only through `core/task-file.ts`. A file can no more
1117
+ * teach the log a state than a screenshot can teach a database a row.
1118
+ *
1119
+ * Four rules, each of which is a way of not making things worse:
1120
+ *
1121
+ * 1. **Only files that already have an envelope.** `set-state` refuses
1122
+ * `no-envelope`, and that refusal is honoured silently: a task with no
1123
+ * `approval:` key is a plain Backlog.md task (SPEC.md §6 requires tolerating
1124
+ * it), and a daemon that gave one an envelope would be enrolling a task
1125
+ * nobody enrolled. The register path is where an envelope comes from.
1126
+ * 2. **No write when the bytes would not change.** The writer reports
1127
+ * `changed`, and the bytes are compared besides. An unnecessary write moves
1128
+ * an mtime, which wakes the watcher, which schedules a tick — a loop that
1129
+ * costs nothing but looks exactly like one that does not terminate.
1130
+ * 3. **A refusal leaves the file alone.** Anything the round-trip writer will
1131
+ * not do — corrupt YAML, an `approval:` key that is not a mapping, a
1132
+ * self-check that failed — becomes one `write-back-refused` warning carrying
1133
+ * the writer's own code. Nothing partial is ever written, because
1134
+ * `rewriteTaskFile` produces bytes or a refusal and `writeTaskFileAtomic`
1135
+ * renames a complete temp file into place.
1136
+ * 4. **Silence where the drift scan already spoke.** An unreadable file, a
1137
+ * frontmatter that does not parse, a missing `id`, a schema-invalid
1138
+ * envelope: each was warned about a few milliseconds ago by
1139
+ * {@link scanForDrift} over the same folder. Repeating it here would double
1140
+ * every line an operator reads without adding a fact.
1141
+ *
1142
+ * Loop safety comes from the comparison, not from a remembered flag: the next
1143
+ * tick derives the same state from the same log, finds the file already
1144
+ * declaring it, and does nothing — no drift, no write, no event. A file that
1145
+ * *keeps* needing repair is being rewritten by something else, and the drift
1146
+ * records are the trail of that fight.
1147
+ */
1148
+ private writeBack;
1149
+ /**
1150
+ * Report tasks that loop safety has escalated to manual, and tasks that have
1151
+ * come back.
1152
+ *
1153
+ * The projection is `core/loop.ts`'s and the enforcement is the gate's and the
1154
+ * executor's; this makes the state *visible* to whoever is watching the daemon,
1155
+ * which is the one thing neither of them does. `approval status` reports the
1156
+ * same set, from the same projection, for an operator who is not.
1157
+ *
1158
+ * Reported on change rather than every tick: a standing escalation restated
1159
+ * every interval is noise that trains an operator to scroll past it.
1160
+ */
1161
+ private surfaceEscalations;
1162
+ /**
1163
+ * Regenerate `QUEUE.md` through the real renderer.
1164
+ *
1165
+ * Never partial: `writeQueue` writes a temp file and renames it, so a reader
1166
+ * sees either the previous queue or the new one and never a half-written file,
1167
+ * and a crashed daemon leaves no torn queue behind.
1168
+ *
1169
+ * The file is rewritten every tick, because TTL countdowns move even when the
1170
+ * log does not. The *event* is emitted only when the summary changes, so a
1171
+ * standing queue does not fill an operator's terminal with identical lines.
1172
+ */
1173
+ private render;
1174
+ private emit;
1175
+ private warn;
1176
+ /** A path as the operator typed it: relative to cwd when it is inside it. */
1177
+ private display;
1178
+ }
1179
+ /** Does this path exist and is it a directory? Used by the verb's preflight. */
1180
+ export declare function isDirectory(path: string): boolean;