approval-md 0.3.0 → 0.4.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 (226) hide show
  1. package/README.md +83 -914
  2. package/SPEC.md +98 -13
  3. package/cli.js +66 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +19 -0
  8. package/dist/src/channels/contract.js +105 -1
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.js +6 -1
  17. package/dist/src/channels/tagging.js.map +1 -1
  18. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  19. package/dist/src/channels/telegram-webhook.js +536 -0
  20. package/dist/src/channels/telegram-webhook.js.map +1 -0
  21. package/dist/src/channels/telegram.d.ts +271 -1
  22. package/dist/src/channels/telegram.js +403 -45
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/cli/amend.js +350 -4
  25. package/dist/src/cli/amend.js.map +1 -1
  26. package/dist/src/cli/attest.js +43 -6
  27. package/dist/src/cli/attest.js.map +1 -1
  28. package/dist/src/cli/channel-relay.d.ts +108 -0
  29. package/dist/src/cli/channel-relay.js +334 -0
  30. package/dist/src/cli/channel-relay.js.map +1 -0
  31. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  32. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  33. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  34. package/dist/src/cli/channel-telegram.d.ts +272 -26
  35. package/dist/src/cli/channel-telegram.js +478 -41
  36. package/dist/src/cli/channel-telegram.js.map +1 -1
  37. package/dist/src/cli/channel.d.ts +1 -1
  38. package/dist/src/cli/channel.js +5 -1
  39. package/dist/src/cli/channel.js.map +1 -1
  40. package/dist/src/cli/daemon.js +37 -5
  41. package/dist/src/cli/daemon.js.map +1 -1
  42. package/dist/src/cli/doctor.d.ts +63 -0
  43. package/dist/src/cli/doctor.js +856 -111
  44. package/dist/src/cli/doctor.js.map +1 -1
  45. package/dist/src/cli/execute.d.ts +17 -1
  46. package/dist/src/cli/execute.js +341 -20
  47. package/dist/src/cli/execute.js.map +1 -1
  48. package/dist/src/cli/exit-codes.d.ts +12 -0
  49. package/dist/src/cli/exit-codes.js +13 -0
  50. package/dist/src/cli/exit-codes.js.map +1 -1
  51. package/dist/src/cli/gate.d.ts +2 -0
  52. package/dist/src/cli/gate.js +233 -2
  53. package/dist/src/cli/gate.js.map +1 -1
  54. package/dist/src/cli/help.d.ts +9 -1
  55. package/dist/src/cli/help.js +246 -38
  56. package/dist/src/cli/help.js.map +1 -1
  57. package/dist/src/cli/hook.d.ts +371 -3
  58. package/dist/src/cli/hook.js +1926 -44
  59. package/dist/src/cli/hook.js.map +1 -1
  60. package/dist/src/cli/init.js +16 -7
  61. package/dist/src/cli/init.js.map +1 -1
  62. package/dist/src/cli/instructions.js +24 -3
  63. package/dist/src/cli/instructions.js.map +1 -1
  64. package/dist/src/cli/log-anchor.d.ts +55 -0
  65. package/dist/src/cli/log-anchor.js +199 -60
  66. package/dist/src/cli/log-anchor.js.map +1 -1
  67. package/dist/src/cli/main.d.ts +0 -12
  68. package/dist/src/cli/main.js +51 -0
  69. package/dist/src/cli/main.js.map +1 -1
  70. package/dist/src/cli/muse.d.ts +2 -0
  71. package/dist/src/cli/muse.js +62 -0
  72. package/dist/src/cli/muse.js.map +1 -0
  73. package/dist/src/cli/payload.js +79 -1
  74. package/dist/src/cli/payload.js.map +1 -1
  75. package/dist/src/cli/scaffold.js +6 -0
  76. package/dist/src/cli/scaffold.js.map +1 -1
  77. package/dist/src/cli/serve.d.ts +53 -0
  78. package/dist/src/cli/serve.js +270 -0
  79. package/dist/src/cli/serve.js.map +1 -0
  80. package/dist/src/cli/up.js +21 -2
  81. package/dist/src/cli/up.js.map +1 -1
  82. package/dist/src/cli/verb-registry.d.ts +2 -0
  83. package/dist/src/cli/verb-registry.js +337 -39
  84. package/dist/src/cli/verb-registry.js.map +1 -1
  85. package/dist/src/cli/wordmark.d.ts +2 -2
  86. package/dist/src/cli/wordmark.js +2 -2
  87. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  88. package/dist/src/core/attest-bootstrap.js +135 -0
  89. package/dist/src/core/attest-bootstrap.js.map +1 -0
  90. package/dist/src/core/attest.d.ts +20 -3
  91. package/dist/src/core/attest.js +25 -0
  92. package/dist/src/core/attest.js.map +1 -1
  93. package/dist/src/core/audit.d.ts +55 -3
  94. package/dist/src/core/audit.js +52 -5
  95. package/dist/src/core/audit.js.map +1 -1
  96. package/dist/src/core/channel-lease.d.ts +322 -0
  97. package/dist/src/core/channel-lease.js +663 -0
  98. package/dist/src/core/channel-lease.js.map +1 -0
  99. package/dist/src/core/command-class.d.ts +51 -0
  100. package/dist/src/core/command-class.js +760 -8
  101. package/dist/src/core/command-class.js.map +1 -1
  102. package/dist/src/core/daemon-host.d.ts +178 -0
  103. package/dist/src/core/daemon-host.js +228 -0
  104. package/dist/src/core/daemon-host.js.map +1 -0
  105. package/dist/src/core/daemon-identity.d.ts +238 -0
  106. package/dist/src/core/daemon-identity.js +252 -0
  107. package/dist/src/core/daemon-identity.js.map +1 -0
  108. package/dist/src/core/gate.d.ts +306 -5
  109. package/dist/src/core/gate.js +772 -24
  110. package/dist/src/core/gate.js.map +1 -1
  111. package/dist/src/core/harness-version.d.ts +128 -4
  112. package/dist/src/core/harness-version.js +179 -12
  113. package/dist/src/core/harness-version.js.map +1 -1
  114. package/dist/src/core/harness-wait.d.ts +82 -3
  115. package/dist/src/core/harness-wait.js +89 -3
  116. package/dist/src/core/harness-wait.js.map +1 -1
  117. package/dist/src/core/log-subscribe.d.ts +21 -0
  118. package/dist/src/core/log-subscribe.js +16 -6
  119. package/dist/src/core/log-subscribe.js.map +1 -1
  120. package/dist/src/core/log-write-layer.d.ts +55 -0
  121. package/dist/src/core/log-write-layer.js +91 -0
  122. package/dist/src/core/log-write-layer.js.map +1 -0
  123. package/dist/src/core/log.d.ts +50 -21
  124. package/dist/src/core/log.js +197 -19
  125. package/dist/src/core/log.js.map +1 -1
  126. package/dist/src/core/payload-census.d.ts +51 -0
  127. package/dist/src/core/payload-census.js +125 -2
  128. package/dist/src/core/payload-census.js.map +1 -1
  129. package/dist/src/core/payload-store.d.ts +31 -0
  130. package/dist/src/core/payload-store.js +113 -8
  131. package/dist/src/core/payload-store.js.map +1 -1
  132. package/dist/src/core/payload.d.ts +18 -3
  133. package/dist/src/core/payload.js +20 -4
  134. package/dist/src/core/payload.js.map +1 -1
  135. package/dist/src/core/policy-diff.d.ts +10 -0
  136. package/dist/src/core/policy-diff.js +10 -2
  137. package/dist/src/core/policy-diff.js.map +1 -1
  138. package/dist/src/core/policy-explain.d.ts +7 -1
  139. package/dist/src/core/policy-explain.js +16 -1
  140. package/dist/src/core/policy-explain.js.map +1 -1
  141. package/dist/src/core/policy-load.d.ts +24 -0
  142. package/dist/src/core/policy-load.js.map +1 -1
  143. package/dist/src/core/policy-match.d.ts +38 -0
  144. package/dist/src/core/policy-match.js +35 -0
  145. package/dist/src/core/policy-match.js.map +1 -1
  146. package/dist/src/core/policy-proposal.d.ts +23 -0
  147. package/dist/src/core/policy-proposal.js +8 -2
  148. package/dist/src/core/policy-proposal.js.map +1 -1
  149. package/dist/src/core/read-scope.d.ts +10 -0
  150. package/dist/src/core/read-scope.js +4 -0
  151. package/dist/src/core/read-scope.js.map +1 -1
  152. package/dist/src/core/run-payload.d.ts +160 -0
  153. package/dist/src/core/run-payload.js +304 -0
  154. package/dist/src/core/run-payload.js.map +1 -0
  155. package/dist/src/core/sender-identity.d.ts +35 -10
  156. package/dist/src/core/sender-identity.js +48 -11
  157. package/dist/src/core/sender-identity.js.map +1 -1
  158. package/dist/src/core/state.d.ts +52 -1
  159. package/dist/src/core/state.js +52 -6
  160. package/dist/src/core/state.js.map +1 -1
  161. package/dist/src/core/telegram-config.d.ts +79 -0
  162. package/dist/src/core/telegram-config.js +108 -0
  163. package/dist/src/core/telegram-config.js.map +1 -1
  164. package/dist/src/core/verify.d.ts +23 -1
  165. package/dist/src/core/verify.js +20 -2
  166. package/dist/src/core/verify.js.map +1 -1
  167. package/dist/src/daemon/audit.d.ts +80 -5
  168. package/dist/src/daemon/audit.js +126 -6
  169. package/dist/src/daemon/audit.js.map +1 -1
  170. package/dist/src/daemon/daemon.d.ts +174 -2
  171. package/dist/src/daemon/daemon.js +298 -7
  172. package/dist/src/daemon/daemon.js.map +1 -1
  173. package/dist/src/daemon/git-evidence.d.ts +2 -2
  174. package/dist/src/daemon/git-evidence.js +1 -1
  175. package/dist/src/daemon/projection.d.ts +13 -2
  176. package/dist/src/daemon/projection.js +39 -6
  177. package/dist/src/daemon/projection.js.map +1 -1
  178. package/dist/src/mcp/server.d.ts +39 -3
  179. package/dist/src/mcp/server.js +22 -3
  180. package/dist/src/mcp/server.js.map +1 -1
  181. package/dist/src/muse/server.d.ts +16 -0
  182. package/dist/src/muse/server.js +238 -0
  183. package/dist/src/muse/server.js.map +1 -0
  184. package/dist/src/serve/archive.d.ts +158 -0
  185. package/dist/src/serve/archive.js +354 -0
  186. package/dist/src/serve/archive.js.map +1 -0
  187. package/dist/src/serve/arguments.d.ts +93 -0
  188. package/dist/src/serve/arguments.js +228 -0
  189. package/dist/src/serve/arguments.js.map +1 -0
  190. package/dist/src/serve/credentials.d.ts +91 -0
  191. package/dist/src/serve/credentials.js +143 -0
  192. package/dist/src/serve/credentials.js.map +1 -0
  193. package/dist/src/serve/follow.d.ts +86 -0
  194. package/dist/src/serve/follow.js +101 -0
  195. package/dist/src/serve/follow.js.map +1 -0
  196. package/dist/src/serve/hook-thread.d.ts +134 -0
  197. package/dist/src/serve/hook-thread.js +333 -0
  198. package/dist/src/serve/hook-thread.js.map +1 -0
  199. package/dist/src/serve/hook-worker.d.ts +134 -0
  200. package/dist/src/serve/hook-worker.js +173 -0
  201. package/dist/src/serve/hook-worker.js.map +1 -0
  202. package/dist/src/serve/server.d.ts +412 -0
  203. package/dist/src/serve/server.js +1251 -0
  204. package/dist/src/serve/server.js.map +1 -0
  205. package/docs/README-extended.md +766 -0
  206. package/docs/cli-reference.md +1633 -43
  207. package/package.json +4 -2
  208. package/schema/event.schema.json +59 -2
  209. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  210. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  211. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  212. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  213. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  214. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  215. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  216. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  217. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  218. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  219. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  220. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  221. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  222. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  223. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  224. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  225. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  226. package/schema/policy.schema.json +50 -6
@@ -104,6 +104,12 @@ import { type PruneReason } from "./prune.js";
104
104
  export declare const DAEMON_ACTOR = "system:daemon";
105
105
  /** Backlog.md's conventional task folder, relative to the working directory. */
106
106
  export { DEFAULT_TASKS_DIR } from "../core/registration.js";
107
+ /**
108
+ * Forget every deferral. Exported for tests, used nowhere else: the memory is
109
+ * output bookkeeping, so a suite that shares a process needs to be able to start
110
+ * from nothing and to exercise what a restarted daemon does.
111
+ */
112
+ export declare function resetDriftDeferrals(): void;
107
113
  /** How often the daemon looks, absent any watcher event. */
108
114
  export declare const DEFAULT_INTERVAL_MS = 30000;
109
115
  /** How long a burst of watcher events is allowed to settle before a tick. */
@@ -203,6 +209,18 @@ export type DaemonEvent = {
203
209
  * a person.
204
210
  */
205
211
  dangling_advances?: string[];
212
+ /**
213
+ * Which daemon instance this run is (APRV-383), and `null` when the id it
214
+ * was launched with is unusable. Additive, like every other growth of this
215
+ * union.
216
+ *
217
+ * On the FIRST line for the reason `read_proof` and `draw` are: it is the
218
+ * name every record this run appends will carry, a tenant reading a hosted
219
+ * log identifies the process by it, and it is not something an operator
220
+ * should have to ask the process about. `null` is the state in which this
221
+ * run will refuse every append, which the warning beside it says in words.
222
+ */
223
+ daemon?: string | null;
206
224
  } | {
207
225
  event: "drift";
208
226
  task: string;
@@ -216,6 +234,18 @@ export type DaemonEvent = {
216
234
  * appeared on all of them would change a shape supervisors already parse.
217
235
  */
218
236
  reason?: "envelope-missing";
237
+ /**
238
+ * This append is the one an earlier tick of THIS run deferred (APRV-403).
239
+ * Present only when it is true, so the shape a supervisor already parses is
240
+ * unchanged for every other drift record.
241
+ *
242
+ * It closes a `drift-deferred` warning, exactly as `sampled.retry` closes a
243
+ * `sample-deferred` one: the pair is what tells an operator that the
244
+ * transient failure they read resolved, and which record resolved it. A
245
+ * fresh process makes the same append and claims no retry, because this
246
+ * loop says only what it witnessed.
247
+ */
248
+ retry?: true;
219
249
  } | {
220
250
  /**
221
251
  * A task file's `state:` was rewritten to match the log (SPEC.md §6.3).
@@ -252,6 +282,18 @@ export type DaemonEvent = {
252
282
  seq: number;
253
283
  /** `seq` of the `execution.started` record the sample named. */
254
284
  subject_seq: number;
285
+ /**
286
+ * This append is the one an earlier tick of THIS run deferred (APRV-381).
287
+ * Present only when it is true, so the shape a supervisor already parses is
288
+ * unchanged for every other sample.
289
+ *
290
+ * It closes a `sample-deferred` warning: the pair is what tells an operator
291
+ * that the transient failure they read resolved, and which sample resolved
292
+ * it. Absent on a run that did not itself defer the sample, including a
293
+ * fresh process picking up a deferral the previous one printed, because this
294
+ * loop says only what it witnessed.
295
+ */
296
+ retry?: true;
255
297
  } | {
256
298
  /**
257
299
  * A payload's bytes were removed under `payload_retention` (APRV-41,
@@ -551,6 +593,19 @@ export declare const DAEMON_WARNING_CODES: readonly [
551
593
  * repair, and the next tick asks again.
552
594
  */
553
595
  "anchor-behind",
596
+ /**
597
+ * The working log changed between this tick's opening read and the anchor
598
+ * check's own read of the file, so the check read it again (APRV-389).
599
+ *
600
+ * A line rather than a stop, and rather than a silence. `approval log sync`
601
+ * and a records pull both rewrite `events.jsonl` under a running daemon
602
+ * (baseline, fast-forward, restore), and this loop reads without the append
603
+ * lock, so a tick can hold a view the file has already moved past. That is a
604
+ * fact about reads, never a fork: the anchor comparison is made again from the
605
+ * file and the verdict beside this line is the second read's. The line is the
606
+ * one an operator acts on when the writer was not a sync they ran.
607
+ */
608
+ "anchor-reread",
554
609
  /**
555
610
  * `audit.checkpoint_every` says a human-signed checkpoint is due and the log
556
611
  * carries none that recent (APRV-220). A WARNING and never a stop, at every
@@ -566,7 +621,54 @@ export declare const DAEMON_WARNING_CODES: readonly [
566
621
  * while it stands, and that is a thing to know rather than to discover from a
567
622
  * month of taps.
568
623
  */
569
- "draw-unavailable"];
624
+ "draw-unavailable",
625
+ /**
626
+ * This daemon's own id is not one the attested policy's `daemons` list admits,
627
+ * or the id it was launched with is not a usable id at all (APRV-383).
628
+ *
629
+ * A warning rather than a stop, and the distinction matters: the refusal itself
630
+ * happens at the write boundary, one append at a time, so the loop keeps
631
+ * reading, keeps rendering the queue and keeps reporting, and writes nothing.
632
+ * The line exists because an operator should learn that from the first tick
633
+ * rather than from a day of `append-refused` messages. The repair is a line in
634
+ * a policy a human re-attests, or a launch environment fixed, and neither is
635
+ * something this process can do for itself.
636
+ */
637
+ "daemon-identity-refused",
638
+ /**
639
+ * An `audit.sampled` append met a held lock or a moved head, so the sample was
640
+ * postponed to the next tick (APRV-381). Appended to this union, so no existing
641
+ * entry changed meaning.
642
+ *
643
+ * Distinct from `append-refused` because the operator's next move is different:
644
+ * `append-refused` on a sample is a fact about the record or the file and wants
645
+ * a person, and this is contention between writers that the next sweep resolves
646
+ * by re-deriving the sample from the log. It is still a warning rather than a
647
+ * silence, because a tick that keeps deferring is a log under contention and
648
+ * that is a thing to know, and the `sampled` line that closes it carries
649
+ * `retry`.
650
+ */
651
+ "sample-deferred",
652
+ /**
653
+ * An `envelope.drift` append met a held lock or a moved head, so the record was
654
+ * postponed to the next tick (APRV-403). Appended to this union, so no existing
655
+ * entry changed meaning.
656
+ *
657
+ * The same split APRV-381 made for `audit.sampled`, for the same reason and
658
+ * from the same closed list of transient codes. Found while landing that task:
659
+ * the end-to-end test that holds the append lockfile for a whole tick showed
660
+ * the drift scan printing `append-refused … (lock-timeout)`, which is true and
661
+ * reads as a lost record, while the next tick re-derived the drift from the
662
+ * verified log and appended it. An operator's next move differs between the
663
+ * two: `append-refused` on a drift record is a fact about the record or the
664
+ * file and wants a person, and this is contention between writers that the
665
+ * scan resolves by itself.
666
+ *
667
+ * Still a warning rather than a silence: a scan that keeps deferring is a log
668
+ * under contention, which is a thing to know. The `drift` line that closes it
669
+ * carries `retry`.
670
+ */
671
+ "drift-deferred"];
570
672
  export type DaemonWarningCode = (typeof DAEMON_WARNING_CODES)[number];
571
673
  /** Where daemon output goes. Injected, so the loop itself writes to nothing. */
572
674
  export interface DaemonSink {
@@ -838,6 +940,16 @@ export declare class Daemon {
838
940
  private previousSelfWrites;
839
941
  private settle;
840
942
  private finished;
943
+ /**
944
+ * What this process resolved as its own daemon id (APRV-383), kept so the
945
+ * `started` line can name it and the startup check can say what is wrong with
946
+ * it. The id in FORCE lives in `core/daemon-identity.ts`'s process state, which
947
+ * is what the write boundary reads; this field is the resolution that produced
948
+ * it, and nothing reads it to decide anything.
949
+ */
950
+ private readonly identity;
951
+ /** Has the unlisted-id warning already been said this run? */
952
+ private reportedIdentityRefusal;
841
953
  /** Whether {@link DaemonOptions.draw} actually bound (APRV-208). */
842
954
  private drawServing;
843
955
  constructor(options: DaemonOptions);
@@ -1025,6 +1137,23 @@ export declare class Daemon {
1025
1137
  */
1026
1138
  private read;
1027
1139
  private readOnce;
1140
+ /** Where this daemon's policy is, in `loadPolicy`'s own vocabulary. */
1141
+ private policyWhere;
1142
+ /**
1143
+ * Put the attested policy's `daemons` allowlist in force for this tick
1144
+ * (APRV-383).
1145
+ *
1146
+ * Called once per tick, immediately after the opening verified read and before
1147
+ * any sweep that appends. It never widens: a resolution that fails sets nothing,
1148
+ * so whatever was last resolved stays in force, and a run whose policy was never
1149
+ * attested is unrestricted exactly as every run before this key existed was.
1150
+ *
1151
+ * The warning is said ONCE per run. A line every thirty seconds forever stops
1152
+ * being read (the `reportedDangling` set above exists for the same reason), and
1153
+ * the write boundary is where the refusal actually happens: every refused append
1154
+ * carries its own `append-refused` line with the reason in it.
1155
+ */
1156
+ private refreshIdentity;
1028
1157
  /** The TTL in force right now, re-read every pass: policy files change. */
1029
1158
  private ttlMs;
1030
1159
  private gateOptions;
@@ -1045,8 +1174,46 @@ export declare class Daemon {
1045
1174
  * malformed file is not a *contradiction* of the log, it is a file the runtime
1046
1175
  * cannot read a claim out of at all, and inventing a `declared_state` for it
1047
1176
  * would put a fact in the log that nobody wrote.
1177
+ *
1178
+ * ## A deferred drift record is not a lost one (APRV-403)
1179
+ *
1180
+ * An append here can fail two ways that call for different things. A held
1181
+ * lockfile or a head that moved under the compare-and-append means the log was
1182
+ * not written and nothing about the file changed; the next tick re-reads the
1183
+ * folder, re-derives the disagreement from the verified log, and appends it.
1184
+ * A validation refusal, a canonicalization failure, a corrupt tail or an I/O
1185
+ * error is a fact about the record or the file that no retry repairs.
1186
+ *
1187
+ * Both used to read as `append-refused … was not appended`, which for the
1188
+ * first is true and reads as a loss. So the transient half now goes out as
1189
+ * {@link deferDrift}'s own `drift-deferred` line, naming the task and saying
1190
+ * that the scan retries, and the `drift` record that closes it carries
1191
+ * `retry`. Everything else keeps the form it had.
1192
+ *
1193
+ * Nothing about the APPEND changed, which is the part that matters for
1194
+ * SPEC.md §11.1 invariant 5: the record is still placed by `appendEvent`
1195
+ * against the `expectedHead` of a read taken immediately before it, and the
1196
+ * whole decision is still remade against that read. A `head-moved` here IS
1197
+ * compare-and-append working — another writer landed a record between this
1198
+ * scan's read and its append — and deferring is the only answer that does not
1199
+ * either write against a head this scan never saw or lose the observation.
1048
1200
  */
1049
1201
  private scanForDrift;
1202
+ /**
1203
+ * Say that an `envelope.drift` append did not happen YET, and remember it
1204
+ * (APRV-403).
1205
+ *
1206
+ * Two things this line has to do that `append-refused` did not. It names the
1207
+ * TASK KEY, so an operator can match the deferral to the record that closes
1208
+ * it, and it says in words that the next tick retries — because the sentence
1209
+ * it replaced ended at "was not appended", which is true and reads as a loss.
1210
+ *
1211
+ * The claim is safe to make because the scan carries nothing between ticks:
1212
+ * the next one re-reads the folder, re-derives the state from the verified
1213
+ * log, and finds the same disagreement, so the retry is a property of how the
1214
+ * scan is written rather than a promise this method is keeping.
1215
+ */
1216
+ private deferDrift;
1050
1217
  /** Every `*.md` under the task folder, sorted, non-recursive. */
1051
1218
  private taskFiles;
1052
1219
  private checkOneFile;
@@ -1116,7 +1283,7 @@ export declare class Daemon {
1116
1283
  * log, and produces bytes only through `core/task-file.ts`. A file can no more
1117
1284
  * teach the log a state than a screenshot can teach a database a row.
1118
1285
  *
1119
- * Four rules, each of which is a way of not making things worse:
1286
+ * Five rules, each of which is a way of not making things worse:
1120
1287
  *
1121
1288
  * 1. **Only files that already have an envelope.** `set-state` refuses
1122
1289
  * `no-envelope`, and that refusal is honoured silently: a task with no
@@ -1138,6 +1305,11 @@ export declare class Daemon {
1138
1305
  * envelope: each was warned about a few milliseconds ago by
1139
1306
  * {@link scanForDrift} over the same folder. Repeating it here would double
1140
1307
  * every line an operator reads without adding a fact.
1308
+ * 5. **No repair for a drift record that was deferred** (APRV-403). See the
1309
+ * check inside the loop: a file whose `envelope.drift` this tick could not
1310
+ * append is left exactly as it is, because repairing it would correct the
1311
+ * disagreement off the record and erase what the retry is supposed to
1312
+ * re-derive.
1141
1313
  *
1142
1314
  * Loop safety comes from the comparison, not from a remembered flag: the next
1143
1315
  * tick derives the same state from the same log, finds the file already