approval-md 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (266) hide show
  1. package/README.md +584 -553
  2. package/SPEC.md +42 -13
  3. package/dist/src/adapters/agentmail.d.ts +426 -0
  4. package/dist/src/adapters/agentmail.js +2 -2
  5. package/dist/src/adapters/conformance.d.ts +149 -0
  6. package/dist/src/adapters/contract.d.ts +628 -0
  7. package/dist/src/adapters/contract.js +110 -16
  8. package/dist/src/adapters/contract.js.map +1 -1
  9. package/dist/src/adapters/email.d.ts +324 -0
  10. package/dist/src/adapters/env-passphrase.d.ts +93 -0
  11. package/dist/src/adapters/public.d.ts +11 -0
  12. package/dist/src/adapters/public.js +11 -0
  13. package/dist/src/adapters/public.js.map +1 -0
  14. package/dist/src/adapters/registry.d.ts +59 -0
  15. package/dist/src/adapters/registry.js +2 -1
  16. package/dist/src/adapters/registry.js.map +1 -1
  17. package/dist/src/adapters/smtp.d.ts +213 -0
  18. package/dist/src/adapters/vault-provider.d.ts +114 -0
  19. package/dist/src/adapters/vault-provider.js +3 -3
  20. package/dist/src/adapters/zzz.d.ts +66 -0
  21. package/dist/src/adapters/zzz.js +299 -0
  22. package/dist/src/adapters/zzz.js.map +1 -0
  23. package/dist/src/channels/batch.d.ts +109 -0
  24. package/dist/src/channels/cli.d.ts +193 -0
  25. package/dist/src/channels/conformance.d.ts +92 -0
  26. package/dist/src/channels/contract.d.ts +623 -0
  27. package/dist/src/channels/payload-view.d.ts +35 -0
  28. package/dist/src/channels/render-queue.d.ts +149 -0
  29. package/dist/src/channels/tagging.d.ts +196 -0
  30. package/dist/src/channels/telegram.d.ts +1832 -0
  31. package/dist/src/channels/web.d.ts +341 -0
  32. package/dist/src/cli/adapter.d.ts +90 -0
  33. package/dist/src/cli/adapter.js +25 -15
  34. package/dist/src/cli/adapter.js.map +1 -1
  35. package/dist/src/cli/amend.d.ts +59 -0
  36. package/dist/src/cli/args.d.ts +43 -0
  37. package/dist/src/cli/attest.d.ts +41 -0
  38. package/dist/src/cli/audit-card.d.ts +62 -0
  39. package/dist/src/cli/audit.d.ts +59 -0
  40. package/dist/src/cli/channel-telegram.d.ts +806 -0
  41. package/dist/src/cli/channel-web.d.ts +131 -0
  42. package/dist/src/cli/channel.d.ts +71 -0
  43. package/dist/src/cli/checkpoint-tap.d.ts +169 -0
  44. package/dist/src/cli/codex.d.ts +2 -0
  45. package/dist/src/cli/codex.js +172 -0
  46. package/dist/src/cli/codex.js.map +1 -0
  47. package/dist/src/cli/coverage.d.ts +61 -0
  48. package/dist/src/cli/daemon.d.ts +120 -0
  49. package/dist/src/cli/doctor.d.ts +129 -0
  50. package/dist/src/cli/doctor.js +119 -5
  51. package/dist/src/cli/doctor.js.map +1 -1
  52. package/dist/src/cli/env.d.ts +65 -0
  53. package/dist/src/cli/execute.d.ts +202 -0
  54. package/dist/src/cli/exit-codes.d.ts +73 -0
  55. package/dist/src/cli/feedback.d.ts +60 -0
  56. package/dist/src/cli/gate-window.d.ts +40 -0
  57. package/dist/src/cli/gate.d.ts +68 -0
  58. package/dist/src/cli/git-scope.d.ts +190 -0
  59. package/dist/src/cli/gloss-attach.d.ts +85 -0
  60. package/dist/src/cli/gloss-codex-child.d.ts +9 -0
  61. package/dist/src/cli/gloss-codex.d.ts +24 -0
  62. package/dist/src/cli/gloss-options.d.ts +42 -0
  63. package/dist/src/cli/gloss.d.ts +265 -0
  64. package/dist/src/cli/help.d.ts +103 -0
  65. package/dist/src/cli/help.js +173 -51
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +78 -0
  68. package/dist/src/cli/hook-codex.js +167 -0
  69. package/dist/src/cli/hook-codex.js.map +1 -0
  70. package/dist/src/cli/hook.d.ts +331 -0
  71. package/dist/src/cli/hook.js +186 -80
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.d.ts +35 -0
  74. package/dist/src/cli/init.d.ts +84 -0
  75. package/dist/src/cli/init.js +2 -2
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.d.ts +23 -0
  78. package/dist/src/cli/journal.d.ts +41 -0
  79. package/dist/src/cli/log-advance.d.ts +287 -0
  80. package/dist/src/cli/log-advance.js +102 -11
  81. package/dist/src/cli/log-advance.js.map +1 -1
  82. package/dist/src/cli/log-anchor.d.ts +176 -0
  83. package/dist/src/cli/log-checkpoint.d.ts +22 -0
  84. package/dist/src/cli/log-sync.d.ts +243 -0
  85. package/dist/src/cli/log-verbs.d.ts +16 -0
  86. package/dist/src/cli/log-verbs.js +7 -1
  87. package/dist/src/cli/log-verbs.js.map +1 -1
  88. package/dist/src/cli/long-help.d.ts +70 -0
  89. package/dist/src/cli/main.d.ts +77 -0
  90. package/dist/src/cli/main.js +155 -5
  91. package/dist/src/cli/main.js.map +1 -1
  92. package/dist/src/cli/mcp.d.ts +52 -0
  93. package/dist/src/cli/paths.d.ts +56 -0
  94. package/dist/src/cli/payload.d.ts +58 -0
  95. package/dist/src/cli/policy.d.ts +43 -0
  96. package/dist/src/cli/preflight.d.ts +363 -0
  97. package/dist/src/cli/preflight.js +294 -7
  98. package/dist/src/cli/preflight.js.map +1 -1
  99. package/dist/src/cli/progress.d.ts +78 -0
  100. package/dist/src/cli/prompt.d.ts +209 -0
  101. package/dist/src/cli/quickstart.d.ts +46 -0
  102. package/dist/src/cli/quickstart.js +297 -0
  103. package/dist/src/cli/quickstart.js.map +1 -0
  104. package/dist/src/cli/records.d.ts +34 -0
  105. package/dist/src/cli/render.d.ts +22 -0
  106. package/dist/src/cli/sandbox.d.ts +51 -0
  107. package/dist/src/cli/scaffold.d.ts +79 -0
  108. package/dist/src/cli/setup-adapter.d.ts +137 -0
  109. package/dist/src/cli/setup-adapter.js +38 -4
  110. package/dist/src/cli/setup-adapter.js.map +1 -1
  111. package/dist/src/cli/setup-channel.d.ts +117 -0
  112. package/dist/src/cli/setup-checkpoint.d.ts +57 -0
  113. package/dist/src/cli/setup-common.d.ts +275 -0
  114. package/dist/src/cli/setup-flow.d.ts +287 -0
  115. package/dist/src/cli/setup-service.d.ts +96 -0
  116. package/dist/src/cli/setup.d.ts +202 -0
  117. package/dist/src/cli/style.d.ts +320 -0
  118. package/dist/src/cli/token.d.ts +39 -0
  119. package/dist/src/cli/up.d.ts +155 -0
  120. package/dist/src/cli/up.js +4 -2
  121. package/dist/src/cli/up.js.map +1 -1
  122. package/dist/src/cli/usage.d.ts +37 -0
  123. package/dist/src/cli/values.d.ts +40 -0
  124. package/dist/src/cli/vault.d.ts +59 -0
  125. package/dist/src/cli/vault.js +2 -2
  126. package/dist/src/cli/vault.js.map +1 -1
  127. package/dist/src/cli/verb-registry.d.ts +76 -0
  128. package/dist/src/cli/verb-registry.js +176 -8
  129. package/dist/src/cli/verb-registry.js.map +1 -1
  130. package/dist/src/cli/wordmark.d.ts +31 -0
  131. package/dist/src/cli/wordmark.js +2 -2
  132. package/dist/src/codex/doctor.d.ts +13 -0
  133. package/dist/src/codex/doctor.js +41 -0
  134. package/dist/src/codex/doctor.js.map +1 -0
  135. package/dist/src/codex/manifest.d.ts +49 -0
  136. package/dist/src/codex/manifest.js +103 -0
  137. package/dist/src/codex/manifest.js.map +1 -0
  138. package/dist/src/codex/templates.d.ts +41 -0
  139. package/dist/src/codex/templates.js +319 -0
  140. package/dist/src/codex/templates.js.map +1 -0
  141. package/dist/src/codex/trust.d.ts +19 -0
  142. package/dist/src/codex/trust.js +183 -0
  143. package/dist/src/codex/trust.js.map +1 -0
  144. package/dist/src/codex/workspace-plan.d.ts +131 -0
  145. package/dist/src/codex/workspace-plan.js +561 -0
  146. package/dist/src/codex/workspace-plan.js.map +1 -0
  147. package/dist/src/core/actor.d.ts +2 -0
  148. package/dist/src/core/actor.js +5 -0
  149. package/dist/src/core/actor.js.map +1 -0
  150. package/dist/src/core/advance-cycle.d.ts +170 -0
  151. package/dist/src/core/agents-md.d.ts +276 -0
  152. package/dist/src/core/apply-patch.d.ts +49 -0
  153. package/dist/src/core/apply-patch.js +266 -0
  154. package/dist/src/core/apply-patch.js.map +1 -0
  155. package/dist/src/core/attest.d.ts +420 -0
  156. package/dist/src/core/attest.js +13 -1
  157. package/dist/src/core/attest.js.map +1 -1
  158. package/dist/src/core/audit.d.ts +492 -0
  159. package/dist/src/core/budgets.d.ts +238 -0
  160. package/dist/src/core/checkpoint.d.ts +500 -0
  161. package/dist/src/core/child-env.d.ts +88 -0
  162. package/dist/src/core/clock.d.ts +52 -0
  163. package/dist/src/core/command-class.d.ts +543 -0
  164. package/dist/src/core/command-class.js +43 -8
  165. package/dist/src/core/command-class.js.map +1 -1
  166. package/dist/src/core/coverage-sources/adapter.d.ts +40 -0
  167. package/dist/src/core/coverage-sources/gh.d.ts +48 -0
  168. package/dist/src/core/coverage-sources/git.d.ts +101 -0
  169. package/dist/src/core/coverage.d.ts +217 -0
  170. package/dist/src/core/credential-spec.d.ts +72 -0
  171. package/dist/src/core/dark-session.d.ts +331 -0
  172. package/dist/src/core/decision-refusal.d.ts +185 -0
  173. package/dist/src/core/env-file.d.ts +450 -0
  174. package/dist/src/core/execute.d.ts +858 -0
  175. package/dist/src/core/execute.js +44 -6
  176. package/dist/src/core/execute.js.map +1 -1
  177. package/dist/src/core/frontmatter.d.ts +78 -0
  178. package/dist/src/core/gate-window.d.ts +312 -0
  179. package/dist/src/core/gate.d.ts +1364 -0
  180. package/dist/src/core/gate.js +68 -13
  181. package/dist/src/core/gate.js.map +1 -1
  182. package/dist/src/core/git-run.d.ts +73 -0
  183. package/dist/src/core/harness-version.d.ts +157 -0
  184. package/dist/src/core/harness-version.js +2 -1
  185. package/dist/src/core/harness-version.js.map +1 -1
  186. package/dist/src/core/harness-wait.d.ts +55 -0
  187. package/dist/src/core/head-retry.d.ts +107 -0
  188. package/dist/src/core/instance.d.ts +253 -0
  189. package/dist/src/core/intake-limits.d.ts +247 -0
  190. package/dist/src/core/jcs.d.ts +52 -0
  191. package/dist/src/core/journal.d.ts +144 -0
  192. package/dist/src/core/live-draw.d.ts +436 -0
  193. package/dist/src/core/log-reconcile.d.ts +89 -0
  194. package/dist/src/core/log-subscribe.d.ts +36 -0
  195. package/dist/src/core/log-subscribe.js +162 -0
  196. package/dist/src/core/log-subscribe.js.map +1 -0
  197. package/dist/src/core/log.d.ts +278 -0
  198. package/dist/src/core/loop.d.ts +274 -0
  199. package/dist/src/core/loop.js +11 -0
  200. package/dist/src/core/loop.js.map +1 -1
  201. package/dist/src/core/md-fence.d.ts +41 -0
  202. package/dist/src/core/money.d.ts +147 -0
  203. package/dist/src/core/payload-census.d.ts +74 -0
  204. package/dist/src/core/payload-store.d.ts +175 -0
  205. package/dist/src/core/payload.d.ts +71 -0
  206. package/dist/src/core/policy-diff.d.ts +292 -0
  207. package/dist/src/core/policy-diff.js +27 -4
  208. package/dist/src/core/policy-diff.js.map +1 -1
  209. package/dist/src/core/policy-expectations.d.ts +199 -0
  210. package/dist/src/core/policy-explain.d.ts +150 -0
  211. package/dist/src/core/policy-explain.js +31 -3
  212. package/dist/src/core/policy-explain.js.map +1 -1
  213. package/dist/src/core/policy-load.d.ts +527 -0
  214. package/dist/src/core/policy-load.js +15 -3
  215. package/dist/src/core/policy-load.js.map +1 -1
  216. package/dist/src/core/policy-match.d.ts +281 -0
  217. package/dist/src/core/policy-match.js +20 -9
  218. package/dist/src/core/policy-match.js.map +1 -1
  219. package/dist/src/core/policy-proposal.d.ts +265 -0
  220. package/dist/src/core/prompt-layout.d.ts +221 -0
  221. package/dist/src/core/protected-path-guard.d.ts +453 -0
  222. package/dist/src/core/protected-path-guard.js +514 -35
  223. package/dist/src/core/protected-path-guard.js.map +1 -1
  224. package/dist/src/core/registration.d.ts +25 -0
  225. package/dist/src/core/reindex.d.ts +99 -0
  226. package/dist/src/core/sampler.d.ts +313 -0
  227. package/dist/src/core/sandbox.d.ts +290 -0
  228. package/dist/src/core/seal.d.ts +165 -0
  229. package/dist/src/core/state.d.ts +505 -0
  230. package/dist/src/core/task-file.d.ts +185 -0
  231. package/dist/src/core/telegram-config.d.ts +93 -0
  232. package/dist/src/core/token.d.ts +409 -0
  233. package/dist/src/core/token.js +21 -38
  234. package/dist/src/core/token.js.map +1 -1
  235. package/dist/src/core/validate.d.ts +138 -0
  236. package/dist/src/core/values.d.ts +137 -0
  237. package/dist/src/core/vault.d.ts +291 -0
  238. package/dist/src/core/verified-snapshot.d.ts +204 -0
  239. package/dist/src/core/verify.d.ts +336 -0
  240. package/dist/src/core/version.d.ts +8 -0
  241. package/dist/src/core/wysiwys.d.ts +370 -0
  242. package/dist/src/daemon/advance-child.d.ts +39 -0
  243. package/dist/src/daemon/advance.d.ts +466 -0
  244. package/dist/src/daemon/audit.d.ts +87 -0
  245. package/dist/src/daemon/daemon.d.ts +1180 -0
  246. package/dist/src/daemon/dark-session.d.ts +64 -0
  247. package/dist/src/daemon/draw-child.d.ts +36 -0
  248. package/dist/src/daemon/draw.d.ts +154 -0
  249. package/dist/src/daemon/git-evidence.d.ts +173 -0
  250. package/dist/src/daemon/git-evidence.js +1 -1
  251. package/dist/src/daemon/projection.d.ts +180 -0
  252. package/dist/src/daemon/prune.d.ts +207 -0
  253. package/dist/src/mcp/http.d.ts +113 -0
  254. package/dist/src/mcp/server.d.ts +265 -0
  255. package/dist/src/mcp/server.js +9 -1
  256. package/dist/src/mcp/server.js.map +1 -1
  257. package/docs/adapter-api.md +106 -0
  258. package/docs/cli-reference.md +389 -36
  259. package/docs/codex-enforced-session.md +30 -0
  260. package/package.json +12 -2
  261. package/schema/codex-instance.schema.json +82 -0
  262. package/schema/event.schema.json +2 -1
  263. package/schema/fixtures/codex-instance/invalid/unpinned-codex-version.json +40 -0
  264. package/schema/fixtures/codex-instance/valid/canonical.json +40 -0
  265. package/schema/policy.schema.json +21 -1
  266. package/templates/codex/README.md +9 -0
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Prompt layout: which INFORMATIONAL rows a channel puts in front of an
3
+ * approver, as a policy decision rather than a code decision (APRV-218).
4
+ *
5
+ * SPEC.md §5.2 gains `channels.<name>.prompt`; SPEC.md §10.3 already says a
6
+ * channel holds no truth, and this module is what keeps that so — the block
7
+ * decides what is SHOWN, never what is true, and every field it can reach was
8
+ * already on the `ChannelRequest` and already in `--json`, `approval queue` and
9
+ * the web page. Nothing here reads the log, and no key added here lets a
10
+ * channel learn anything about it.
11
+ *
12
+ * ## Why this is a policy key at all
13
+ *
14
+ * The Telegram prompt was slimmed deliberately: APRV-143 dropped the `ttl` row
15
+ * because the `waiting … expires HH:MM UTC` line already states the deadline,
16
+ * and APRV-163 dropped six bookkeeping rows and made three health rows render
17
+ * only when abnormal. That layout fits ONE operator's workflow. Another wants
18
+ * the budget line on every prompt, or the TTL as a duration, or the task id
19
+ * always visible. Those are preferences about a screen, and preferences about a
20
+ * screen belong in the policy file beside the other things an operator chooses,
21
+ * not in a `git blame` argument about a default.
22
+ *
23
+ * ## What a layout may NOT do
24
+ *
25
+ * A layout chooses among rows the approver may read. It cannot touch what the
26
+ * approver SIGNS. Three things are therefore out of its reach entirely:
27
+ *
28
+ * 1. **The canonical block** (SPEC.md §9's "what you see is what you sign") is
29
+ * not a row. It carries the payload bytes, the renderer version, the class,
30
+ * the kind and the bound `payload sha256`, it is rendered verbatim, and no
31
+ * key in this module can reorder, shorten, or remove it.
32
+ * 2. **The computed/claimed split.** A row's side of the boundary is a property
33
+ * of the field (`TaggedField.kind`), not of the layout. `rows` reorders; it
34
+ * cannot move a claimed line into the computed block, because a channel
35
+ * partitions by kind AFTER it applies the order. That is the property
36
+ * APRV-144's CLAIMED heading exists for, and a test pins it.
37
+ * 3. **{@link REQUIRED_PROMPT_ROWS}**, the rows the contract marks as required
38
+ * for a decision. Naming one in `hide` is a policy that fails to load.
39
+ *
40
+ * The buttons are not a row either, for the same reason the block is not: a
41
+ * prompt with no way to answer it is not a prompt.
42
+ *
43
+ * ## Fail directions
44
+ *
45
+ * Soft on ABSENCE, closed on INVALIDITY, which is the split every other policy
46
+ * key in this runtime keeps. No `prompt` block, or a policy that did not load,
47
+ * means today's rows: a layout is not a permission, and an unrelated typo in a
48
+ * class rule must not silently redecorate a phone screen. An unknown row name
49
+ * or a required row in `hide` is a statement the runtime cannot honour, so the
50
+ * WHOLE policy fails schema validation and every class resolves to `manual` —
51
+ * the operator repairs the file, and until they do the gate is at its
52
+ * strictest.
53
+ *
54
+ * Pure: no clock, no IO, no environment. It sits in `core/` rather than in
55
+ * `channels/` for `core/telegram-config.ts`'s reason — more than one layer asks
56
+ * "which rows does this policy want?" and none of them may answer differently —
57
+ * and it imports nothing from `channels/`, so `tests/layering.test.ts` stays
58
+ * true. The row names below are the `ChannelRequest` member names spelled as
59
+ * plain strings for exactly that reason.
60
+ */
61
+ import type { Policy, PolicyLoadResult } from "./policy-load.js";
62
+ import type { ValidationError } from "./validate.js";
63
+ /**
64
+ * The row vocabulary: every `ChannelRequest` member a channel renders as a row.
65
+ *
66
+ * Closed, and closed for `delivery`'s reason (APRV-216): a name this runtime
67
+ * cannot place is a row the author believes is in force that nothing reads, and
68
+ * guessing at it would be the silent no-op the whole policy schema is shaped to
69
+ * prevent.
70
+ *
71
+ * `fullPayload` is deliberately ABSENT. The canonical block is not a row.
72
+ */
73
+ export declare const PROMPT_ROWS: readonly ["action_key", "task", "class", "command_breakdown", "protected_path", "policy_diff", "policy_load", "autonomy", "provenance", "state", "requested_ts", "waiting", "ttl_remaining_ms", "payload_hash", "attestation", "budgets", "chain", "token_delivery", "est_cost_usd", "gloss", "summary", "rationale", "confidence"];
74
+ /** One row name from {@link PROMPT_ROWS}. */
75
+ export type PromptRow = (typeof PROMPT_ROWS)[number];
76
+ /** Whether a string is a row name this runtime knows how to place. */
77
+ export declare function isPromptRow(value: unknown): value is PromptRow;
78
+ /**
79
+ * The rows an operator may reorder but never remove.
80
+ *
81
+ * Each is required for a DECISION rather than for a screen. `action_key`
82
+ * identifies which request the gesture answers; `class` is the resolution the
83
+ * whole gate turns on; `command_breakdown` and `protected_path` are APRV-144
84
+ * and APRV-143's answer to "what does this command actually do, and which
85
+ * protected path earned the class", derived from the bound bytes by the same
86
+ * classifier the hook decided with; `policy_diff` and `policy_load` are what
87
+ * SPEC.md §10.3 requires of an attestation prompt, where "a prompt carrying
88
+ * only a hash asks a human to sign for sixty-four characters and is a
89
+ * conformance failure".
90
+ *
91
+ * `payload_hash` is NOT here, and its absence is not an oversight. The bound
92
+ * hash is stated inside the canonical block on every channel, and the block is
93
+ * out of a layout's reach, so an operator who hides the row removes a
94
+ * duplicate rather than the binding. Telegram's default layout already does
95
+ * exactly that (APRV-163).
96
+ *
97
+ * A required row a request does not CARRY renders nothing, as it does today.
98
+ * Requirement is about what a policy may instruct, not about what a particular
99
+ * request happens to hold.
100
+ */
101
+ export declare const REQUIRED_PROMPT_ROWS: readonly PromptRow[];
102
+ /**
103
+ * How a row renders when nothing in the policy says otherwise.
104
+ *
105
+ * - `always` — on every prompt the request carries the field for.
106
+ * - `abnormal` — only when the value is the reason to look. APRV-163's
107
+ * argument: a row that says "everything is fine" on every ordinary request is
108
+ * a row a reader learns to skip, and the skipping does not stop on the one
109
+ * request where it says something else.
110
+ * - `off` — the channel does not render it by default. The field still travels
111
+ * on the request, so `--json`, `approval queue` and the web page carry it.
112
+ */
113
+ export type RowVisibility = "always" | "abnormal" | "off";
114
+ /** A resolved layout: the order rows render in, and each row's visibility. */
115
+ export interface PromptLayout {
116
+ /** Every row in {@link PROMPT_ROWS}, in render order. */
117
+ order: readonly PromptRow[];
118
+ /** Visibility per row. Total: every row name has an entry. */
119
+ visibility: Readonly<Record<PromptRow, RowVisibility>>;
120
+ }
121
+ /** The channels this runtime ships (SPEC.md §10.3). */
122
+ export declare const PROMPT_CHANNELS: readonly ["cli", "web", "telegram"];
123
+ /** One of the three shipped channel names. */
124
+ export type PromptChannel = (typeof PROMPT_CHANNELS)[number];
125
+ /**
126
+ * Telegram's default layout: the slimmed phone prompt, exactly as APRV-143 and
127
+ * APRV-163 left it.
128
+ *
129
+ * The ORDER of the rows that render is load-bearing and must not drift: it is
130
+ * the sequence a reader's eye has learned. The `off` rows are interleaved where
131
+ * they belong if an operator turns them on, which costs nothing while they are
132
+ * off and saves an operator from writing a `rows` list to get a sensible
133
+ * position.
134
+ *
135
+ * `action_key` is `off` here because Telegram renders it STRUCTURALLY, as the
136
+ * message's second line in a `<code>` span. It is required, so it cannot be
137
+ * hidden, and it is not a bullet, so promoting it changes nothing.
138
+ */
139
+ export declare const TELEGRAM_PROMPT_LAYOUT: PromptLayout;
140
+ /**
141
+ * The CLI channel's default layout: every row the request carries.
142
+ *
143
+ * A terminal has room, and a one-shot rendering an operator asked for by typing
144
+ * a verb is not the place to economise on lines the way a push notification is.
145
+ */
146
+ export declare const CLI_PROMPT_LAYOUT: PromptLayout;
147
+ /** The web channel's default layout. Same reasoning as the CLI's: a page scrolls. */
148
+ export declare const WEB_PROMPT_LAYOUT: PromptLayout;
149
+ /** Default layout per shipped channel. What an absent `prompt` block means. */
150
+ export declare const DEFAULT_PROMPT_LAYOUTS: Readonly<Record<PromptChannel, PromptLayout>>;
151
+ /**
152
+ * A `channels.<name>.prompt` block as a policy may write it.
153
+ *
154
+ * Three keys, and each does ONE thing, because a key that both orders and
155
+ * hides would leave an operator guessing which of the two a short list meant:
156
+ *
157
+ * - `rows` — ORDER ONLY. The rows it names render in that order, ahead of every
158
+ * row it does not name; those keep their default relative order behind them.
159
+ * It is never a whitelist, so a `ChannelRequest` widened by a later task
160
+ * cannot silently lose a field to a list written before that field existed —
161
+ * the same property `orderedFields` has held since APRV-23.
162
+ * - `always` — visibility UP. A row that is `abnormal` or `off` by default
163
+ * renders on every prompt.
164
+ * - `hide` — visibility DOWN. A row never renders. Refused for
165
+ * {@link REQUIRED_PROMPT_ROWS}.
166
+ *
167
+ * `always` and `hide` naming the same row is refused rather than resolved by
168
+ * precedence: a policy that says both things about one row has an author who
169
+ * believes one of them, and picking for them is the guess this schema does not
170
+ * make.
171
+ */
172
+ export interface PromptBlock {
173
+ rows?: PromptRow[];
174
+ always?: PromptRow[];
175
+ hide?: PromptRow[];
176
+ }
177
+ /**
178
+ * The layout in force for `channel`.
179
+ *
180
+ * Fail-soft in the same direction as `telegramDeliveryFor`: a policy that did
181
+ * not load declares nothing, an absent block declares nothing, and a layout is
182
+ * not a permission. Anything structurally wrong that reaches here (a hand-built
183
+ * load result, a key from a later version) falls back to the default rather
184
+ * than being guessed at — a LOADED policy cannot carry such a block, because
185
+ * {@link promptBlockErrors} refuses it at load.
186
+ */
187
+ export declare function promptLayoutFor(load: PolicyLoadResult, channel: string): PromptLayout;
188
+ /**
189
+ * Apply a block to a layout. Pure, total, and exported so a caller holding a
190
+ * block already can resolve it without going back through a policy file, which
191
+ * is what the tests do and what a future `approval policy explain` of a channel
192
+ * would need. Nothing outside the tests calls it today, and that is fine: the
193
+ * split keeps {@link promptLayoutFor} to one job, reading the block.
194
+ */
195
+ export declare function applyPromptBlock(base: PromptLayout, block: PromptBlock): PromptLayout;
196
+ /** The machine-readable reasons a `prompt` block refuses (APRV-218). */
197
+ export declare const PROMPT_BLOCK_ERROR_KEYWORDS: readonly [
198
+ /** A row name this runtime cannot place. */
199
+ "prompt-row-unknown",
200
+ /** {@link REQUIRED_PROMPT_ROWS} named in `hide`. */
201
+ "prompt-row-required",
202
+ /** `rows`, `always` or `hide` is not an array of strings. */
203
+ "prompt-block-shape",
204
+ /** One row named by both `always` and `hide`. */
205
+ "prompt-row-conflict",
206
+ /** A key the `prompt` block does not define. */
207
+ "prompt-key-unknown"];
208
+ /**
209
+ * Validate every `channels.<name>.prompt` in a policy. Empty means clean.
210
+ *
211
+ * This runs on top of the JSON Schema rather than instead of it, and the
212
+ * duplication is deliberate. The schema types the three channels this runtime
213
+ * ships and closes the row enum there, which is where SPEC.md §8's
214
+ * validate-at-the-write-boundary rule wants it; but `channels` admits UNKNOWN
215
+ * channel names as free-form objects on purpose, so a third-party transport
216
+ * plugin does not invalidate a whole policy, and a `prompt` block written under
217
+ * such a name would otherwise reach a renderer unchecked. Both nets return the
218
+ * same verdict — the policy does not load, everything is `manual` — so an
219
+ * operator never has to know which one caught them.
220
+ */
221
+ export declare function promptBlockErrors(policy: Policy): ValidationError[];
@@ -0,0 +1,453 @@
1
+ /**
2
+ * The protected-path guard (APRV-151): CI asks the log, not the session.
3
+ *
4
+ * ## The hole this closes
5
+ *
6
+ * `approval hook claude-code` classifies a file-tool call before it applies,
7
+ * and when the target is a protected path the answer is `policy.edit` and the
8
+ * phone buzzes. That control is only as good as the wiring: a session whose
9
+ * PreToolUse hook never loaded edits SPEC.md with no prompt, no denial, and —
10
+ * this is the part that hurts — no refused-request record either. Nothing in
11
+ * the log says the edit happened, because the thing that would have written
12
+ * the record is the thing that did not run. Two incidents (2026-08-29 SPEC.md
13
+ * in worktree `aprv-145-land`, 2026-08-30 `.github/workflows/ci.yml` in
14
+ * `agent-a3f5d255372d43ac0`) are the same shape.
15
+ *
16
+ * A detector that asked the session whether it was hooked would be asking the
17
+ * party under oversight to report its own oversight, which SPEC.md §11 rules
18
+ * out on principle. So this one never talks to a session. It takes two commits,
19
+ * asks git which protected paths changed between them, and requires — for each
20
+ * one — exact evidence in the committed hash-chained log that either a human
21
+ * granted that edit or the runtime recorded that policy authorized its
22
+ * execution. A change with no evidence fails the pull request. Session wiring
23
+ * is not an input.
24
+ *
25
+ * ## What counts as evidence
26
+ *
27
+ * Evidence is about the CHANGE, not about the path (APRV-202). The guard reads
28
+ * the blob at both commits, reduces the difference to the lines this pull
29
+ * request adds and removes, and requires every one of them to trace back to
30
+ * bound material in an authorization record. A path authorized last Tuesday
31
+ * and edited again today has evidence naming it and none covering today's
32
+ * lines, and that fails. Naming remains necessary; it stopped being sufficient.
33
+ *
34
+ * Four verdicts pass:
35
+ *
36
+ * 1. `attested` — the policy file, and the gate's ORGANS. `approval policy
37
+ * amend --commit` appends `policy.updated` carrying `{policy_path, sha256}`,
38
+ * the SHA-256 of the policy bytes a human attested. So for `APPROVAL.md` the
39
+ * guard does not look for a grant at all: it hashes the file's bytes AT THE
40
+ * HEAD COMMIT and requires that exact digest in the log. This is the
41
+ * strongest match in the system — content-level, not path-level — and it is
42
+ * why amendment PRs pass without a `policy.edit` grant, which they would
43
+ * never have.
44
+ *
45
+ * Since APRV-272 the same verdict covers the gate's organs, the harness
46
+ * files that install the hook (`.claude/settings.json` and kin), on a
47
+ * `gate.organ.attested` record carrying that path and that digest. They need
48
+ * it for a stronger reason than the policy file does: an organ is
49
+ * `policy.core`, `policy.core` is human-only, and the gate mints NOTHING for
50
+ * a human-only class — so `granted-file` and `granted-command` cannot exist
51
+ * for one however correctly a human edited it, and before this the guard
52
+ * could only ever fail such a change (PR #300). The path is part of the
53
+ * match: a digest attested for one organ is not evidence for another, which
54
+ * is why the organ record carries a whole relative path where the policy
55
+ * record carries a basename.
56
+ * 2. `policy-authorized-file` — an exact Edit or Write whose verified
57
+ * `execution.started` is preceded by its unique matching registration and no
58
+ * approval request. The registration, start and recomputed stored payload
59
+ * must agree on task, action, class and hash. The start must precede the
60
+ * change and the recorded class must be the class this path is routed to.
61
+ * This records authorization to execute, not successful completion; the
62
+ * exact hunk checks below establish whether those bytes landed.
63
+ * 3. `granted-file` — a file-tool edit. The hook binds the CHANGE rather than
64
+ * the touch (APRV-124), so the bound material carries `file` plus the exact
65
+ * edit: `{before, after}` for an Edit, `{content}` for a Write. That is
66
+ * HUNK-level evidence, and it is used as such. The granted `after` bytes
67
+ * have to occur verbatim in the blob at head before any added line is
68
+ * credited to them, and the granted `before` bytes have to occur in the blob
69
+ * at base before any removed line is. A granted edit whose after-state is not
70
+ * in head is a grant for something that did not land, and covers nothing.
71
+ * Some payloads carry the `{input}` fallback shape instead, which describes
72
+ * no bytes; those name the path and cover nothing.
73
+ * 4. `granted-command` — a shell edit. The bound material is `{command, cwd}`
74
+ * or `{argv, cwd}`, and the guard re-runs the runtime's own
75
+ * {@link classifyCommand} over it, requiring a segment that classifies as a
76
+ * granting class BECAUSE of a word naming this path. A mention is not a
77
+ * grant: `cat SPEC.md` is `read.shell` and proves nothing, and the first
78
+ * draft of this module, which substring-matched, accepted `hook classify --
79
+ * vi SPEC.md` as evidence for a later SPEC.md edit.
80
+ *
81
+ * A command payload cannot describe hunks: `node scripts/apply.mjs` names
82
+ * the file it will rewrite and says nothing about the bytes. So this kind is
83
+ * attributed rather than covered, and three things all have to hold:
84
+ *
85
+ * - The write lands on THIS checkout's copy of the path. The payload's `cwd`
86
+ * joined with the repository-relative path has to be exactly the word the
87
+ * classifier matched (see {@link commandTargetsPath}). Three of the grants
88
+ * that would otherwise have carried PR #187's SPEC.md change were dry runs
89
+ * into `$SCRATCH/dry/SPEC.md`.
90
+ * - The grant was SPENT: an `execution.started` for its `action_key`, and
91
+ * its `execution.completed` when the log has one. A grant nobody spent
92
+ * authorized a command that never ran.
93
+ * - That run sits within {@link DEFAULT_COMMAND_ATTRIBUTION_MS} of the
94
+ * commit AND does not start after it. A command's effect follows its own
95
+ * `execution.started`, so a run four hours after the commit did not write
96
+ * it — a real batch, on 2026-09-02, that a symmetric window would have
97
+ * credited with PR #187's changes.
98
+ *
99
+ * The finding names the run it attributed the change to, so a reader can
100
+ * check the attribution rather than take it. It stays weaker than
101
+ * `granted-file` because time is a weaker link than bytes: everything the
102
+ * approved run wrote to that path in its window is carried by it.
103
+ *
104
+ * There is deliberately no class-level pass. A `policy.edit` grant that exists
105
+ * in the window but names some other file is not evidence that anybody saw
106
+ * THIS edit, and accepting it would let one approved edit launder every other
107
+ * edit in the same window. Class-level grants appear in the failure detail as
108
+ * diagnosis, never as a verdict.
109
+ *
110
+ * ## Grants go stale
111
+ *
112
+ * Naming the path is necessary and not sufficient, because grants accumulate
113
+ * forever. Run without a recency rule against the real log, this guard passed a
114
+ * SPEC.md edit made on 2026-08-29 on the strength of a `git add SPEC.md`
115
+ * granted on 2026-08-20: once any edit to a path has ever been approved, every
116
+ * later edit to that path would inherit the approval. So evidence must also sit
117
+ * within {@link DEFAULT_LOOKBACK_MS} of the commit that introduced the change,
118
+ * on either side of it. Either side, because both orderings are real: a grant
119
+ * shortly BEFORE the commit is the ordinary case, and a grant shortly after is
120
+ * the grant-follows-write anomaly (APRV-117/150 adjacent) — a defect in its own
121
+ * right, but a complete consent trail all the same, and not this guard's to
122
+ * adjudicate.
123
+ *
124
+ * That bound used to be the guard's weakest joint: a repeat edit to the same
125
+ * path inside the window inherited the earlier grant, and the guard passed
126
+ * PR #187, #196 and #207 on grants that authorized some earlier edit. APRV-202
127
+ * closes it, and the window is now the cheap pre-filter in front of the real
128
+ * question. A grant inside the window still has to cover the lines: an
129
+ * uncovered change fails `uncovered-hunk` however fresh the grants naming its
130
+ * path are. Attestation is exempt from the bound, because it matches CONTENT: bytes that
131
+ * hash to an attested digest are the attested bytes whenever they were signed.
132
+ * And when git cannot date the change at all, no bound is applied rather than a
133
+ * weaker one invented: see `changeTsFor` on {@link GuardInput} for why a bound
134
+ * against the head commit would have been theatre. The finding says which of
135
+ * the two it got, every time.
136
+ *
137
+ * ## How a hunk is decided to be covered
138
+ *
139
+ * The unit is a line of text. `added` is the multiset of lines the head blob
140
+ * has and the base blob does not; `removed` is the converse. A line is covered
141
+ * when its exact text appears in the granted material of some in-window grant
142
+ * (in `after`/`content` for an added line, in `before` for a removed one) and
143
+ * that material is anchored to the blob it claims, as described above. Coverage
144
+ * may be assembled from several grants, because one pull request may carry
145
+ * several approved edits to one file; the finding names every contributing
146
+ * grant and puts the strongest and nearest at the head.
147
+ *
148
+ * Three properties of that choice are worth stating, because each is a limit:
149
+ *
150
+ * - A blob that differs while its line multiset does not is a REORDERING, and
151
+ * it is reported as one uncovered hunk rather than as no change. A rule that
152
+ * compared multisets alone would pass a rewrite that only moved paragraphs.
153
+ * - Blank and whitespace-only lines neither need coverage nor give it. They
154
+ * carry no content, and treating them as material would let one granted edit
155
+ * containing an empty line cover every blank line added anywhere.
156
+ * - Coverage is by line text, not by position. An added line whose exact text
157
+ * appears in some granted edit counts as covered even if it landed somewhere
158
+ * else in the file. Tightening that to positions would trade a narrow
159
+ * laundering channel (repeating a line the human already approved) for false
160
+ * failures on every rebase and re-indent, and the first is the cheaper loss.
161
+ *
162
+ * ## The evidence surface is not a protected write surface
163
+ *
164
+ * `.approval/` is protected wherever it sits, and the daemon appends to it
165
+ * every time anything is approved — so a records / log-advance pull request
166
+ * changes `.approval/log/events.jsonl`, the payload store beside it, and the
167
+ * regenerated `QUEUE.md`. Requiring a grant for those would require a grant for
168
+ * the evidence, which is circular and would make it impossible to land the very
169
+ * commits this guard reads. {@link EXEMPT_PREFIXES} names that surface, and
170
+ * nothing else under `.approval/` is exempt: the vault, the environment map and
171
+ * anything else that lands there is still a protected write.
172
+ *
173
+ * ## The lag, and the ordering rule it implies
174
+ *
175
+ * The log on `main` trails the primary checkout's live log; advances land
176
+ * periodically as records pull requests. A grant made this morning may not be
177
+ * on `main` yet, and this module can only see the records its caller hands it.
178
+ * That is not a bug to paper over, it is an ordering rule, and every failure
179
+ * states it: **the log advance carrying the grant must be pushed to a records
180
+ * branch or merged to main before or with the protected-path pull request.**
181
+ * Each failure also names the window it searched (the seq and timestamp range
182
+ * of the records it was given) so the reader can tell "the grant is not there"
183
+ * from "the grant is newer than this log".
184
+ *
185
+ * A records branch counts because the caller may read further along the same
186
+ * chain than the head commit does (APRV-260): `scripts/protected-path-guard.mjs`
187
+ * takes the freshest committed copy that carries head's own last record at
188
+ * head's index, so an advance that is pushed but not yet merged is already
189
+ * evidence. Nothing here changes: this module still reads only the verified
190
+ * records it was handed, and the window it reports is the window it searched.
191
+ *
192
+ * ## Fail closed
193
+ *
194
+ * A missing log, a log that does not pass chain verification, and a protected
195
+ * path with no evidence are all failures, each with its own code. Records that
196
+ * have not passed verification are never read for evidence (SPEC.md §11.1
197
+ * invariant 1): the caller hands this module the verified records or none.
198
+ * This module appends nothing, reads no clock, and performs no IO of its own —
199
+ * git plumbing and file reads live in the caller.
200
+ */
201
+ import { type ProtectedPathEntry } from "./command-class.js";
202
+ import type { EventRecord } from "./log.js";
203
+ /**
204
+ * Classes whose grant authorizes a protected-path write.
205
+ *
206
+ * `policy.edit` is what the hook and this repository's policy use.
207
+ * `policy.core` is accepted alongside it because SPEC.md §7's taxonomy admits
208
+ * a stricter sibling and a policy that routed the highest-value edits there
209
+ * should not lose its evidence.
210
+ */
211
+ export declare const GRANTING_CLASSES: readonly string[];
212
+ /**
213
+ * Does a grant of this class authorize a protected-path write? (APRV-266.)
214
+ *
215
+ * The named classes above, plus any `policy.edit` sub-class a policy routes a
216
+ * path to. Both directions of the cross-check matter and both are accepted:
217
+ *
218
+ * - A grant of the ROUTED class is the ordinary case once a policy adopts
219
+ * routing. `policy.edit.spec` is the class the hook asked about and the class
220
+ * the human decided, so it is the class the record carries.
221
+ * - A grant of `policy.edit` ITSELF is accepted for a path now routed, because
222
+ * a routing is a policy edit and the two are not synchronized: a grant taken
223
+ * under yesterday's string-only policy, or under the daemon's own fallback
224
+ * when the policy would not load, names `policy.edit` for a path today's
225
+ * policy routes. Refusing it would make adopting a routing retroactively
226
+ * invalidate evidence that was correct when it was taken.
227
+ *
228
+ * What this does NOT do is loosen the guard: the class only opens the door, and
229
+ * the naming test — the grant's own material has to name THIS path, and since
230
+ * APRV-202 has to cover the actual hunks — is unchanged and is what decides.
231
+ * A sub-class name is not authority over anything; the namespace is closed to
232
+ * `policy.edit.*` in the classifier, so no grant of `log.mutate` or
233
+ * `policy.core` can be manufactured by naming one (SPEC.md §11.1 invariant 9).
234
+ */
235
+ export declare function isGrantingClass(actionClass: string): boolean;
236
+ /**
237
+ * The daemon's own append surface: evidence, not a protected write.
238
+ *
239
+ * Repository-relative, `/`-separated prefixes. A changed path equal to one of
240
+ * these, or under one of the directory ones, is skipped before any evidence is
241
+ * sought. See the module note for why this carve-out is narrow on purpose.
242
+ */
243
+ export declare const EXEMPT_PREFIXES: readonly string[];
244
+ /** Every way this guard can refuse, as stable codes. */
245
+ export declare const GUARD_FAILURE_CODES: readonly [
246
+ /** The log blob does not exist at the head commit at all. */
247
+ "log-missing",
248
+ /** The log exists and does not pass chain verification. */
249
+ "log-unverified",
250
+ /** A protected path changed and nothing in the log is evidence for it. */
251
+ "no-evidence",
252
+ /**
253
+ * Grants DO name this path, inside the window, and some added or removed line
254
+ * of this change traces to none of their bound material (APRV-202).
255
+ *
256
+ * Distinct from `no-evidence` on purpose, because the two ask different
257
+ * things of the reader. `no-evidence` says nobody approved anything about
258
+ * this file and the question is whether the hook fired at all.
259
+ * `uncovered-hunk` says somebody approved something about this file and it
260
+ * was not this, which is the repeat-edit shape: the grant is real, the
261
+ * consent trail for THESE bytes is missing, and the fix is to take the
262
+ * change to the gate rather than to hunt for a lost record.
263
+ */
264
+ "uncovered-hunk",
265
+ /**
266
+ * The blobs at base and head could not be read for this path (git could not
267
+ * show them, or they are binary), so no coverage could be established.
268
+ * Failing is the fail-closed direction: a change the guard cannot read is not
269
+ * a change it has checked.
270
+ */
271
+ "change-unreadable"];
272
+ export type GuardFailureCode = (typeof GUARD_FAILURE_CODES)[number];
273
+ /** How the verified log and bound material authorize this exact path. */
274
+ export type EvidenceKind = "attested" | "policy-authorized-file" | "granted-file" | "granted-command";
275
+ /**
276
+ * How far from the commit that introduced a change a grant may sit and still be
277
+ * evidence for it: seven days, either side. See the module note.
278
+ */
279
+ export declare const DEFAULT_LOOKBACK_MS: number;
280
+ /**
281
+ * How far from the change commit a granted command's RUN may sit and still be
282
+ * the run that produced it: six hours, either side.
283
+ *
284
+ * Deliberately much tighter than {@link DEFAULT_LOOKBACK_MS}, because it is
285
+ * carrying much more weight. A file grant is checked against the bytes, so the
286
+ * recency bound is only a sanity rail around a content match. A command grant
287
+ * has no bytes to check, so time is the whole attribution, and a week of it
288
+ * would re-open exactly the hole this closes: every later edit to a path some
289
+ * approved script once wrote would inherit that script's grant. Six hours is
290
+ * about a working session, which is the unit of "this run produced this
291
+ * commit"; a batch that legitimately takes longer than that is asked for a
292
+ * fresh approval, which costs one tap.
293
+ */
294
+ export declare const DEFAULT_COMMAND_ATTRIBUTION_MS: number;
295
+ /** Fail-closed resource bounds for exact protected-edit reconstruction. */
296
+ export declare const EXACT_REPLAY_MAX_CANDIDATES = 128;
297
+ export declare const EXACT_REPLAY_MAX_STATES = 2048;
298
+ export declare const EXACT_REPLAY_MAX_EXAMINED_BYTES: number;
299
+ export interface GuardFinding {
300
+ /** The changed path, repository-relative. */
301
+ path: string;
302
+ ok: boolean;
303
+ /** Present on a pass. */
304
+ evidence?: EvidenceKind;
305
+ /** The log record that is the evidence, on a pass. */
306
+ seq?: number;
307
+ ts?: string;
308
+ actor?: string;
309
+ /**
310
+ * Every grant that covered part of this change, in report order, strongest
311
+ * and nearest first. `seq` is the head of this list.
312
+ */
313
+ coveredBy?: readonly number[];
314
+ /** Present on a failure. */
315
+ code?: GuardFailureCode;
316
+ /**
317
+ * On `uncovered-hunk`, a sample of the lines that traced to no granted
318
+ * material, `+` for added and `-` for removed.
319
+ */
320
+ uncovered?: readonly string[];
321
+ /** Prose a reader can act on, on either outcome. */
322
+ detail: string;
323
+ }
324
+ /** A protected path's bytes at both ends of the range the guard is checking. */
325
+ export interface ChangeBlobs {
326
+ /** The blob at base, or `null` when this pull request adds the file. */
327
+ base: string | null;
328
+ /** The blob at head, or `null` when this pull request deletes it. */
329
+ head: string | null;
330
+ }
331
+ /** The window of log the guard could see, for the failure messages. */
332
+ export interface LogWindow {
333
+ /** Lowest and highest `seq` in the log at head, or `null` for an empty log. */
334
+ firstSeq: number | null;
335
+ lastSeq: number | null;
336
+ firstTs: string | null;
337
+ lastTs: string | null;
338
+ /** The commit range the caller diffed. */
339
+ base: string;
340
+ head: string;
341
+ }
342
+ export interface GuardInput {
343
+ /** Repository-relative paths that differ between base and head, deletions included. */
344
+ changedPaths: readonly string[];
345
+ /**
346
+ * Records from the log at HEAD that have passed chain verification.
347
+ * `null` means the log could not be verified or could not be read; pair it
348
+ * with `logStatus` so the guard can say which.
349
+ */
350
+ records: readonly EventRecord[] | null;
351
+ logStatus: "ok" | "missing" | "unverified";
352
+ /** Why the log did not verify, when `logStatus` is not `ok`. */
353
+ logDetail?: string;
354
+ /** `policy.protected_paths` from the policy, widening the built-in set. */
355
+ policyProtectedPaths: readonly ProtectedPathEntry[];
356
+ /**
357
+ * SHA-256 of the policy file's bytes at the head commit, or `null` when the
358
+ * head tree carries no policy file. Only used for the `attested` verdict.
359
+ */
360
+ policySha256AtHead: string | null;
361
+ /** The policy file's repository-relative path, e.g. `APPROVAL.md`. */
362
+ policyPath: string;
363
+ /**
364
+ * SHA-256 of one GATE ORGAN's bytes at the head commit, or `null` when the
365
+ * head tree does not carry that path (APRV-272).
366
+ *
367
+ * The per-path counterpart of {@link policySha256AtHead}, computed the same
368
+ * way and from the same place — the blob at the head COMMIT, never the
369
+ * working tree. Only used for the `attested` verdict on an organ.
370
+ *
371
+ * OPTIONAL, and a caller that omits it gets no organ verdict at all rather
372
+ * than a weaker one: an organ change then falls through to the grant search
373
+ * and fails like any other unevidenced protected path. That is the
374
+ * fail-closed direction, and it is what keeps a caller written before this
375
+ * field existed correct rather than newly permissive.
376
+ *
377
+ * A DELETED organ resolves to `null` here, so removing one cannot pass by
378
+ * attestation: there are no bytes at head for a human to have signed. That is
379
+ * deliberate — the repair for a deletion is a grant or a human's own commit
380
+ * outside a pull request, not a record about bytes that no longer exist.
381
+ */
382
+ organSha256AtHead?: (path: string) => string | null;
383
+ /**
384
+ * Resolve bound material from the committed payload store, by hash.
385
+ * Returns `null` when the head tree does not carry that payload.
386
+ */
387
+ payloadFor: (hash: string) => unknown | null;
388
+ /**
389
+ * The author timestamp of the newest commit in `base..head` that touched this
390
+ * path, as an ISO-8601 instant, or `null` when git could not say.
391
+ *
392
+ * With no anchor — `null`, or a value that does not parse — NO recency bound
393
+ * is applied to this path, and the finding says so in its own text rather
394
+ * than reporting a bound it did not enforce.
395
+ *
396
+ * That is stated plainly because it is the accepting direction and it would
397
+ * be easy to dress up. The alternative considered was to bound the grant
398
+ * against the head commit instead, and it was rejected as theatre: every
399
+ * record in the log AT head is already before head by construction, so the
400
+ * rule would pass everything it was asked about while reading like a check.
401
+ * A guard that reports a bound it cannot enforce is worse than one that
402
+ * admits it has none, because only the first kind gets trusted.
403
+ *
404
+ * In practice the anchor is missing only when git cannot date a path it just
405
+ * reported in the diff, which is a broken-git condition rather than an
406
+ * attacker-reachable one; refusing on it would fire only on that breakage.
407
+ * The path-level evidence requirement is unaffected and still holds.
408
+ */
409
+ changeTsFor: (path: string) => string | null;
410
+ /**
411
+ * The path's bytes at base and at head, or `null` when they could not be
412
+ * read (git could not show them, or the blob is binary).
413
+ *
414
+ * This is what makes the guard's question "was THIS change approved" rather
415
+ * than "was this path approved once" (APRV-202). `null` fails the path with
416
+ * `change-unreadable` rather than falling back to the path-level rule: the
417
+ * fallback is the hole.
418
+ */
419
+ blobsFor: (path: string) => ChangeBlobs | null;
420
+ /** Override {@link DEFAULT_LOOKBACK_MS}. */
421
+ lookbackMs?: number;
422
+ /** Override {@link DEFAULT_COMMAND_ATTRIBUTION_MS}. */
423
+ commandAttributionMs?: number;
424
+ window: LogWindow;
425
+ }
426
+ export interface GuardReport {
427
+ ok: boolean;
428
+ /** Every protected path that changed, in the order git reported them. */
429
+ findings: readonly GuardFinding[];
430
+ /** Changed paths skipped as the daemon's own append surface. */
431
+ exempt: readonly string[];
432
+ window: LogWindow;
433
+ }
434
+ /** Is this changed path the daemon's own evidence surface? */
435
+ export declare function isExemptPath(path: string): boolean;
436
+ /**
437
+ * Is this changed path one whose edit requires a human decision?
438
+ *
439
+ * The guarded set is exactly the hook's ({@link isProtectedPath}, built-ins
440
+ * plus `policy.protected_paths`) minus the evidence surface. Sharing the
441
+ * predicate is the point: a CI guard whose idea of "protected" drifted from the
442
+ * hook's would fail the changes the hook already gated and pass the ones it
443
+ * would have caught.
444
+ */
445
+ export declare function isGuardedPath(path: string, policyProtectedPaths: readonly ProtectedPathEntry[]): boolean;
446
+ /**
447
+ * Evaluate a candidate against the committed log. Pure: no IO, no clock.
448
+ *
449
+ * @see GuardInput for what the caller has to gather.
450
+ */
451
+ export declare function evaluateProtectedPaths(input: GuardInput): GuardReport;
452
+ /** The report as the lines CI prints. Pure. */
453
+ export declare function renderGuardReport(report: GuardReport): string;