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.
- package/README.md +584 -553
- package/SPEC.md +42 -13
- package/dist/src/adapters/agentmail.d.ts +426 -0
- package/dist/src/adapters/agentmail.js +2 -2
- package/dist/src/adapters/conformance.d.ts +149 -0
- package/dist/src/adapters/contract.d.ts +628 -0
- package/dist/src/adapters/contract.js +110 -16
- package/dist/src/adapters/contract.js.map +1 -1
- package/dist/src/adapters/email.d.ts +324 -0
- package/dist/src/adapters/env-passphrase.d.ts +93 -0
- package/dist/src/adapters/public.d.ts +11 -0
- package/dist/src/adapters/public.js +11 -0
- package/dist/src/adapters/public.js.map +1 -0
- package/dist/src/adapters/registry.d.ts +59 -0
- package/dist/src/adapters/registry.js +2 -1
- package/dist/src/adapters/registry.js.map +1 -1
- package/dist/src/adapters/smtp.d.ts +213 -0
- package/dist/src/adapters/vault-provider.d.ts +114 -0
- package/dist/src/adapters/vault-provider.js +3 -3
- package/dist/src/adapters/zzz.d.ts +66 -0
- package/dist/src/adapters/zzz.js +299 -0
- package/dist/src/adapters/zzz.js.map +1 -0
- package/dist/src/channels/batch.d.ts +109 -0
- package/dist/src/channels/cli.d.ts +193 -0
- package/dist/src/channels/conformance.d.ts +92 -0
- package/dist/src/channels/contract.d.ts +623 -0
- package/dist/src/channels/payload-view.d.ts +35 -0
- package/dist/src/channels/render-queue.d.ts +149 -0
- package/dist/src/channels/tagging.d.ts +196 -0
- package/dist/src/channels/telegram.d.ts +1832 -0
- package/dist/src/channels/web.d.ts +341 -0
- package/dist/src/cli/adapter.d.ts +90 -0
- package/dist/src/cli/adapter.js +25 -15
- package/dist/src/cli/adapter.js.map +1 -1
- package/dist/src/cli/amend.d.ts +59 -0
- package/dist/src/cli/args.d.ts +43 -0
- package/dist/src/cli/attest.d.ts +41 -0
- package/dist/src/cli/audit-card.d.ts +62 -0
- package/dist/src/cli/audit.d.ts +59 -0
- package/dist/src/cli/channel-telegram.d.ts +806 -0
- package/dist/src/cli/channel-web.d.ts +131 -0
- package/dist/src/cli/channel.d.ts +71 -0
- package/dist/src/cli/checkpoint-tap.d.ts +169 -0
- package/dist/src/cli/codex.d.ts +2 -0
- package/dist/src/cli/codex.js +172 -0
- package/dist/src/cli/codex.js.map +1 -0
- package/dist/src/cli/coverage.d.ts +61 -0
- package/dist/src/cli/daemon.d.ts +120 -0
- package/dist/src/cli/doctor.d.ts +129 -0
- package/dist/src/cli/doctor.js +119 -5
- package/dist/src/cli/doctor.js.map +1 -1
- package/dist/src/cli/env.d.ts +65 -0
- package/dist/src/cli/execute.d.ts +202 -0
- package/dist/src/cli/exit-codes.d.ts +73 -0
- package/dist/src/cli/feedback.d.ts +60 -0
- package/dist/src/cli/gate-window.d.ts +40 -0
- package/dist/src/cli/gate.d.ts +68 -0
- package/dist/src/cli/git-scope.d.ts +190 -0
- package/dist/src/cli/gloss-attach.d.ts +85 -0
- package/dist/src/cli/gloss-codex-child.d.ts +9 -0
- package/dist/src/cli/gloss-codex.d.ts +24 -0
- package/dist/src/cli/gloss-options.d.ts +42 -0
- package/dist/src/cli/gloss.d.ts +265 -0
- package/dist/src/cli/help.d.ts +103 -0
- package/dist/src/cli/help.js +173 -51
- package/dist/src/cli/help.js.map +1 -1
- package/dist/src/cli/hook-codex.d.ts +78 -0
- package/dist/src/cli/hook-codex.js +167 -0
- package/dist/src/cli/hook-codex.js.map +1 -0
- package/dist/src/cli/hook.d.ts +331 -0
- package/dist/src/cli/hook.js +186 -80
- package/dist/src/cli/hook.js.map +1 -1
- package/dist/src/cli/import.d.ts +35 -0
- package/dist/src/cli/init.d.ts +84 -0
- package/dist/src/cli/init.js +2 -2
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/instructions.d.ts +23 -0
- package/dist/src/cli/journal.d.ts +41 -0
- package/dist/src/cli/log-advance.d.ts +287 -0
- package/dist/src/cli/log-advance.js +102 -11
- package/dist/src/cli/log-advance.js.map +1 -1
- package/dist/src/cli/log-anchor.d.ts +176 -0
- package/dist/src/cli/log-checkpoint.d.ts +22 -0
- package/dist/src/cli/log-sync.d.ts +243 -0
- package/dist/src/cli/log-verbs.d.ts +16 -0
- package/dist/src/cli/log-verbs.js +7 -1
- package/dist/src/cli/log-verbs.js.map +1 -1
- package/dist/src/cli/long-help.d.ts +70 -0
- package/dist/src/cli/main.d.ts +77 -0
- package/dist/src/cli/main.js +155 -5
- package/dist/src/cli/main.js.map +1 -1
- package/dist/src/cli/mcp.d.ts +52 -0
- package/dist/src/cli/paths.d.ts +56 -0
- package/dist/src/cli/payload.d.ts +58 -0
- package/dist/src/cli/policy.d.ts +43 -0
- package/dist/src/cli/preflight.d.ts +363 -0
- package/dist/src/cli/preflight.js +294 -7
- package/dist/src/cli/preflight.js.map +1 -1
- package/dist/src/cli/progress.d.ts +78 -0
- package/dist/src/cli/prompt.d.ts +209 -0
- package/dist/src/cli/quickstart.d.ts +46 -0
- package/dist/src/cli/quickstart.js +297 -0
- package/dist/src/cli/quickstart.js.map +1 -0
- package/dist/src/cli/records.d.ts +34 -0
- package/dist/src/cli/render.d.ts +22 -0
- package/dist/src/cli/sandbox.d.ts +51 -0
- package/dist/src/cli/scaffold.d.ts +79 -0
- package/dist/src/cli/setup-adapter.d.ts +137 -0
- package/dist/src/cli/setup-adapter.js +38 -4
- package/dist/src/cli/setup-adapter.js.map +1 -1
- package/dist/src/cli/setup-channel.d.ts +117 -0
- package/dist/src/cli/setup-checkpoint.d.ts +57 -0
- package/dist/src/cli/setup-common.d.ts +275 -0
- package/dist/src/cli/setup-flow.d.ts +287 -0
- package/dist/src/cli/setup-service.d.ts +96 -0
- package/dist/src/cli/setup.d.ts +202 -0
- package/dist/src/cli/style.d.ts +320 -0
- package/dist/src/cli/token.d.ts +39 -0
- package/dist/src/cli/up.d.ts +155 -0
- package/dist/src/cli/up.js +4 -2
- package/dist/src/cli/up.js.map +1 -1
- package/dist/src/cli/usage.d.ts +37 -0
- package/dist/src/cli/values.d.ts +40 -0
- package/dist/src/cli/vault.d.ts +59 -0
- package/dist/src/cli/vault.js +2 -2
- package/dist/src/cli/vault.js.map +1 -1
- package/dist/src/cli/verb-registry.d.ts +76 -0
- package/dist/src/cli/verb-registry.js +176 -8
- package/dist/src/cli/verb-registry.js.map +1 -1
- package/dist/src/cli/wordmark.d.ts +31 -0
- package/dist/src/cli/wordmark.js +2 -2
- package/dist/src/codex/doctor.d.ts +13 -0
- package/dist/src/codex/doctor.js +41 -0
- package/dist/src/codex/doctor.js.map +1 -0
- package/dist/src/codex/manifest.d.ts +49 -0
- package/dist/src/codex/manifest.js +103 -0
- package/dist/src/codex/manifest.js.map +1 -0
- package/dist/src/codex/templates.d.ts +41 -0
- package/dist/src/codex/templates.js +319 -0
- package/dist/src/codex/templates.js.map +1 -0
- package/dist/src/codex/trust.d.ts +19 -0
- package/dist/src/codex/trust.js +183 -0
- package/dist/src/codex/trust.js.map +1 -0
- package/dist/src/codex/workspace-plan.d.ts +131 -0
- package/dist/src/codex/workspace-plan.js +561 -0
- package/dist/src/codex/workspace-plan.js.map +1 -0
- package/dist/src/core/actor.d.ts +2 -0
- package/dist/src/core/actor.js +5 -0
- package/dist/src/core/actor.js.map +1 -0
- package/dist/src/core/advance-cycle.d.ts +170 -0
- package/dist/src/core/agents-md.d.ts +276 -0
- package/dist/src/core/apply-patch.d.ts +49 -0
- package/dist/src/core/apply-patch.js +266 -0
- package/dist/src/core/apply-patch.js.map +1 -0
- package/dist/src/core/attest.d.ts +420 -0
- package/dist/src/core/attest.js +13 -1
- package/dist/src/core/attest.js.map +1 -1
- package/dist/src/core/audit.d.ts +492 -0
- package/dist/src/core/budgets.d.ts +238 -0
- package/dist/src/core/checkpoint.d.ts +500 -0
- package/dist/src/core/child-env.d.ts +88 -0
- package/dist/src/core/clock.d.ts +52 -0
- package/dist/src/core/command-class.d.ts +543 -0
- package/dist/src/core/command-class.js +43 -8
- package/dist/src/core/command-class.js.map +1 -1
- package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
- package/dist/src/core/coverage-sources/gh.d.ts +48 -0
- package/dist/src/core/coverage-sources/git.d.ts +101 -0
- package/dist/src/core/coverage.d.ts +217 -0
- package/dist/src/core/credential-spec.d.ts +72 -0
- package/dist/src/core/dark-session.d.ts +331 -0
- package/dist/src/core/decision-refusal.d.ts +185 -0
- package/dist/src/core/env-file.d.ts +450 -0
- package/dist/src/core/execute.d.ts +858 -0
- package/dist/src/core/execute.js +44 -6
- package/dist/src/core/execute.js.map +1 -1
- package/dist/src/core/frontmatter.d.ts +78 -0
- package/dist/src/core/gate-window.d.ts +312 -0
- package/dist/src/core/gate.d.ts +1364 -0
- package/dist/src/core/gate.js +68 -13
- package/dist/src/core/gate.js.map +1 -1
- package/dist/src/core/git-run.d.ts +73 -0
- package/dist/src/core/harness-version.d.ts +157 -0
- package/dist/src/core/harness-version.js +2 -1
- package/dist/src/core/harness-version.js.map +1 -1
- package/dist/src/core/harness-wait.d.ts +55 -0
- package/dist/src/core/head-retry.d.ts +107 -0
- package/dist/src/core/instance.d.ts +253 -0
- package/dist/src/core/intake-limits.d.ts +247 -0
- package/dist/src/core/jcs.d.ts +52 -0
- package/dist/src/core/journal.d.ts +144 -0
- package/dist/src/core/live-draw.d.ts +436 -0
- package/dist/src/core/log-reconcile.d.ts +89 -0
- package/dist/src/core/log-subscribe.d.ts +36 -0
- package/dist/src/core/log-subscribe.js +162 -0
- package/dist/src/core/log-subscribe.js.map +1 -0
- package/dist/src/core/log.d.ts +278 -0
- package/dist/src/core/loop.d.ts +274 -0
- package/dist/src/core/loop.js +11 -0
- package/dist/src/core/loop.js.map +1 -1
- package/dist/src/core/md-fence.d.ts +41 -0
- package/dist/src/core/money.d.ts +147 -0
- package/dist/src/core/payload-census.d.ts +74 -0
- package/dist/src/core/payload-store.d.ts +175 -0
- package/dist/src/core/payload.d.ts +71 -0
- package/dist/src/core/policy-diff.d.ts +292 -0
- package/dist/src/core/policy-diff.js +27 -4
- package/dist/src/core/policy-diff.js.map +1 -1
- package/dist/src/core/policy-expectations.d.ts +199 -0
- package/dist/src/core/policy-explain.d.ts +150 -0
- package/dist/src/core/policy-explain.js +31 -3
- package/dist/src/core/policy-explain.js.map +1 -1
- package/dist/src/core/policy-load.d.ts +527 -0
- package/dist/src/core/policy-load.js +15 -3
- package/dist/src/core/policy-load.js.map +1 -1
- package/dist/src/core/policy-match.d.ts +281 -0
- package/dist/src/core/policy-match.js +20 -9
- package/dist/src/core/policy-match.js.map +1 -1
- package/dist/src/core/policy-proposal.d.ts +265 -0
- package/dist/src/core/prompt-layout.d.ts +221 -0
- package/dist/src/core/protected-path-guard.d.ts +453 -0
- package/dist/src/core/protected-path-guard.js +514 -35
- package/dist/src/core/protected-path-guard.js.map +1 -1
- package/dist/src/core/registration.d.ts +25 -0
- package/dist/src/core/reindex.d.ts +99 -0
- package/dist/src/core/sampler.d.ts +313 -0
- package/dist/src/core/sandbox.d.ts +290 -0
- package/dist/src/core/seal.d.ts +165 -0
- package/dist/src/core/state.d.ts +505 -0
- package/dist/src/core/task-file.d.ts +185 -0
- package/dist/src/core/telegram-config.d.ts +93 -0
- package/dist/src/core/token.d.ts +409 -0
- package/dist/src/core/token.js +21 -38
- package/dist/src/core/token.js.map +1 -1
- package/dist/src/core/validate.d.ts +138 -0
- package/dist/src/core/values.d.ts +137 -0
- package/dist/src/core/vault.d.ts +291 -0
- package/dist/src/core/verified-snapshot.d.ts +204 -0
- package/dist/src/core/verify.d.ts +336 -0
- package/dist/src/core/version.d.ts +8 -0
- package/dist/src/core/wysiwys.d.ts +370 -0
- package/dist/src/daemon/advance-child.d.ts +39 -0
- package/dist/src/daemon/advance.d.ts +466 -0
- package/dist/src/daemon/audit.d.ts +87 -0
- package/dist/src/daemon/daemon.d.ts +1180 -0
- package/dist/src/daemon/dark-session.d.ts +64 -0
- package/dist/src/daemon/draw-child.d.ts +36 -0
- package/dist/src/daemon/draw.d.ts +154 -0
- package/dist/src/daemon/git-evidence.d.ts +173 -0
- package/dist/src/daemon/git-evidence.js +1 -1
- package/dist/src/daemon/projection.d.ts +180 -0
- package/dist/src/daemon/prune.d.ts +207 -0
- package/dist/src/mcp/http.d.ts +113 -0
- package/dist/src/mcp/server.d.ts +265 -0
- package/dist/src/mcp/server.js +9 -1
- package/dist/src/mcp/server.js.map +1 -1
- package/docs/adapter-api.md +106 -0
- package/docs/cli-reference.md +389 -36
- package/docs/codex-enforced-session.md +30 -0
- package/package.json +12 -2
- package/schema/codex-instance.schema.json +82 -0
- package/schema/event.schema.json +2 -1
- package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
- package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
- package/schema/policy.schema.json +21 -1
- 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;
|