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,370 @@
1
+ /**
2
+ * WYSIWYS: the canonical rendering of a payload (APRV-119), and the structural
3
+ * views it is built from (APRV-100, APRV-124, APRV-126, APRV-144).
4
+ *
5
+ * ## What you see is what you sign
6
+ *
7
+ * The prompt a human approves is a deterministic function of the payload bytes
8
+ * and the action class, and of nothing else. {@link canonicalRender} is that
9
+ * function. Two channels, two runtimes, or two versions of this one cannot show
10
+ * two humans two different readings of the same payload without the difference
11
+ * being detectable: the rendering carries its own `display_hash`, the gate
12
+ * records that hash on `approval.requested`, and a rendering that disagrees is a
13
+ * rendering whose hash disagrees.
14
+ *
15
+ * The threat this closes is signoff social engineering. An approval surface that
16
+ * renders benign text while the hashed payload is malicious leaves the human
17
+ * signing blind. `payload_hash` binds the bytes; `display_hash` binds the
18
+ * reading of them.
19
+ *
20
+ * Four properties, and they are the whole module:
21
+ *
22
+ * 1. **Pure.** No clock, no locale, no environment, no randomness, no IO. The
23
+ * only inputs are the payload value and the class string, and
24
+ * `tests/wysiwys.test.ts` reads this file's own source and fails on a
25
+ * reference to any of them.
26
+ * 2. **Closed field set per kind.** A payload is recognised as one of four kinds
27
+ * (`command`, `file-change`, `email`, `opaque`) by its STRUCTURE, and each
28
+ * kind renders a fixed list of fields. A shape carrying one key its kind does
29
+ * not render is not that kind: it falls through to `opaque`, where the
30
+ * canonical JSON is shown whole. Nothing is hidden by being unrecognised.
31
+ * 3. **Absent renders explicitly.** A field the payload does not carry is
32
+ * printed as {@link ABSENT}, never omitted. An omitted line and a line whose
33
+ * value happens to be empty are different facts, and a reader who cannot tell
34
+ * them apart is reading a rendering that lost information.
35
+ * 4. **Claimed material stays outside.** Everything inside the canonical block
36
+ * is derived from the bound bytes. Summaries, cost estimates, rationale,
37
+ * confidence, and model-written glosses are rendered OUTSIDE it, under the
38
+ * channel's own claimed heading (SPEC.md §9).
39
+ *
40
+ * ## Why this lives in `src/core/`, not `src/channels/`
41
+ *
42
+ * It is deterministic core in the sense CLAUDE.md means: pure, exhaustively
43
+ * tested, and consulted by the gate. `core/gate.ts` computes `display_hash` at
44
+ * the write boundary from the same function every channel renders with, so the
45
+ * log states what rendering the approver was shown. A renderer under
46
+ * `src/channels/` would have to be imported BY core to do that, inverting the
47
+ * direction the codebase is built on. `channels/payload-view.ts` is the
48
+ * channel-side facade over this module and holds the one function that needs a
49
+ * channel type.
50
+ *
51
+ * ## The reading aids this absorbed (APRV-100, APRV-124, APRV-126)
52
+ *
53
+ * SPEC.md §10.4 requires a channel to present, for a `manual` action, "the full
54
+ * payload or a faithful rendering of it". Until now every channel used the one
55
+ * rendering `channels/tagging.ts` builds: pretty-printed JSON. That is faithful
56
+ * and it is exact, and for an email it is close to unreadable — the observed
57
+ * failure (2026-08-18, examples/email-demo.md) is a body arriving on a phone as
58
+ * a single line carrying literal `\n` sequences, which is precisely the text a
59
+ * human is being asked to take responsibility for.
60
+ *
61
+ * So this module adds a second rendering *on top of* the first, never instead
62
+ * of it:
63
+ *
64
+ * - A payload whose **structure** matches the email adapter's payload shape
65
+ * ({@link ../adapters/email.js}) is rendered field by field, with the body as
66
+ * the human will read it: real line breaks, no JSON escapes.
67
+ * - A payload whose structure is a **shell command** (APRV-126: the shape
68
+ * `cli/hook.ts` builds for a `Bash` tool call) is rendered as the command a
69
+ * shell would receive, over as many lines as it really has, with `cwd` on its
70
+ * own line beneath it. Same reason again: `"command": "gh pr create --body
71
+ * 'a\nb'"` on a phone is one paragraph of escaped JSON, and the observed
72
+ * failure (2026-08-20) is a human being asked to take responsibility for it.
73
+ * - A payload whose structure is a **file change** (APRV-124: the shape
74
+ * `cli/hook.ts` builds for an `Edit` or a `Write` tool call) is rendered as a
75
+ * diff, so the approver reads the change rather than the fact that a file was
76
+ * touched. Same reason as the email case: `"before": "a\nb"` on a phone is
77
+ * bytes nobody can check.
78
+ * - Every other shape falls to `opaque`, whose view IS the canonical JSON: the
79
+ * bytes whole, pretty-printed, exactly the rendering every payload had before
80
+ * the structural views existed.
81
+ *
82
+ * Three properties this file is careful about:
83
+ *
84
+ * 1. **Detection is structural.** Nothing here reads a self-declared "kind" or
85
+ * "type" field, because that field would be authored by the party under
86
+ * oversight, and a channel that lets an agent choose its own presentation
87
+ * has handed the agent the display. A payload is email-shaped when its keys
88
+ * and its value types say so, and for no other reason.
89
+ * 2. **Nothing is hidden.** The shape is accepted only when *every* key is one
90
+ * this module renders, so the field-by-field view never omits a byte of the
91
+ * payload — a `bcc` or a `content_type` that the reader could not see would
92
+ * be the same failure as a truncated payload, wearing a friendlier face.
93
+ * 3. **This is claimed content.** The payload is authored by the requesting
94
+ * agent. The block says so in its first line, and the computed binding (the
95
+ * `sha256` label each channel already prints around this region) stays where
96
+ * it is. Making the payload *legible* must not make it look *verified*.
97
+ * A `tool` or a `rule` value inside a file-change payload is rendered for
98
+ * the reader and is never what selects the rendering: the shape is, exactly
99
+ * as for an email.
100
+ * 4. **Two different byte strings never look the same.** A reading aid that
101
+ * interprets escape sequences has to answer the question it creates: if a
102
+ * real line break becomes a line break, what does the two-byte sequence
103
+ * backslash-`n` become? Rendering both as a line break would let an agent
104
+ * write one payload and have the approver read another. So the rendering is
105
+ * INJECTIVE by construction ({@link markEscapes}), and the property is
106
+ * tested by generating pairs of distinct byte strings.
107
+ * 5. **The view is the whole reading (APRV-162, `approval.md/wysiwys/2`).** A
108
+ * structured kind's view is the canonical rendering entire; no canonical-JSON
109
+ * appendix follows it. The completeness argument is property 2 above: kind
110
+ * detection is a closed field set, one unrecognised key sends the payload to
111
+ * `opaque` whose view is the whole JSON, so a structural view that renders at
112
+ * all renders every byte. The views therefore do not fold. A fold was
113
+ * survivable only while the appendix restated the hidden lines underneath it;
114
+ * with the appendix gone it would hide bytes from the only reading a human
115
+ * gets, which is the failure this module exists to remove.
116
+ *
117
+ * The output is plain text with real newlines. Escaping belongs to the channel:
118
+ * `telegram.ts` and `web.ts` each pass this through their own `escapeHtml` and
119
+ * their own `<pre>`, so the injection surface is exactly what it was before.
120
+ */
121
+ import { type ProtectedPathEntry } from "./command-class.js";
122
+ /**
123
+ * How a field the payload does not carry is rendered.
124
+ *
125
+ * Never an omission. A closed field set that silently drops its absent members
126
+ * is not a closed field set: the reader cannot tell "no `cc`" from "a `cc` this
127
+ * renderer does not know how to show", and those are the two cases the whole
128
+ * design exists to keep apart.
129
+ */
130
+ export declare const ABSENT = "(absent)";
131
+ /** The heading and delimiters. Exported because the tests pin them. */
132
+ export declare const EMAIL_VIEW_HEADING = "email \u2014 rendered field by field; every value below is CLAIMED, authored by the requesting party";
133
+ export declare const BODY_BEGIN = "--- body begins ---";
134
+ export declare const BODY_END = "--- body ends ---";
135
+ export declare const CANONICAL_JSON_HEADING = "--- the same bytes, canonical JSON ---";
136
+ /** One labelled line of the field-by-field view. */
137
+ export interface EmailViewField {
138
+ label: string;
139
+ /** The value as text. For `body`, this may contain newlines. */
140
+ text: string;
141
+ }
142
+ /**
143
+ * Recognise an email-shaped payload, structurally.
144
+ *
145
+ * Returns the fields in display order, or `null` when the value is any other
146
+ * shape — including an email-ish object carrying one key this module does not
147
+ * know how to show, which falls back to JSON rather than hiding it.
148
+ */
149
+ export declare function emailPayloadFields(value: unknown): EmailViewField[] | null;
150
+ /** The heading and delimiters of the diff view. Exported because tests pin them. */
151
+ export declare const EDIT_VIEW_HEADING = "file change \u2014 the change itself, not the touch; every value below is CLAIMED, authored by the requesting party";
152
+ export declare const DIFF_BEGIN = "--- change begins ---";
153
+ export declare const DIFF_END = "--- change ends ---";
154
+ /** The qualifier a proposal-tier touch renders (APRV-124). */
155
+ export declare const PROPOSAL_QUALIFIER = "this edit targets a file inside an AGENT WORKTREE: it is a branch PROPOSAL, not the live file. Merging it to the live checkout is a separate gated action.";
156
+ export declare const LIVE_QUALIFIER = "this edit targets the LIVE checkout, not a branch proposal.";
157
+ /** The qualifier a protected-name touch outside the gated checkout renders (APRV-161). */
158
+ export declare const ELSEWHERE_QUALIFIER = "this edit targets a file NAMED like a policy file, OUTSIDE the gated checkout: it is not the live policy. It gates because the name is protected wherever it sits.";
159
+ /** A file change, recognised structurally. */
160
+ export interface ChangeView {
161
+ /** Labelled single-line fields, in display order. */
162
+ labels: EmailViewField[];
163
+ /** The removed side, or `null` for a whole-file write. */
164
+ before: string | null;
165
+ /** The added side: the new text, or the whole new content. */
166
+ after: string;
167
+ }
168
+ /**
169
+ * Recognise a file-change payload, structurally.
170
+ *
171
+ * Accepted when the payload names a `file` and carries either both sides of an
172
+ * edit (`before` and `after`) or a whole-file `content`, and every other key is
173
+ * one this module renders. Anything else — including a payload that carries
174
+ * only `before`, where the reader would be shown half a change — is `null` and
175
+ * falls back to JSON.
176
+ */
177
+ export declare function changePayloadView(value: unknown): ChangeView | null;
178
+ /** The heading and delimiters of the command view. Exported because tests pin them. */
179
+ export declare const COMMAND_VIEW_HEADING = "command \u2014 rendered; the hash binds the RAW BYTES, not this view. Every value below is CLAIMED, authored by the requesting party";
180
+ export declare const COMMAND_BEGIN = "--- command begins ---";
181
+ export declare const COMMAND_END = "--- command ends ---";
182
+ /**
183
+ * The delimiters around a marked escape sequence.
184
+ *
185
+ * Guillemets rather than brackets: `[` and `]` are ordinary shell and regex
186
+ * characters, so a marker built from them would be indistinguishable from the
187
+ * command's own text at a glance, which is the failure this marker exists to
188
+ * prevent.
189
+ */
190
+ export declare const ESCAPE_OPEN = "\u00AB";
191
+ export declare const ESCAPE_CLOSE = "\u00BB";
192
+ /** The legend printed above every command block, so the marker needs no lore. */
193
+ export declare const ESCAPE_LEGEND = "escapes: \u00AB\\n\u00BB is the two LITERAL bytes backslash-n; a real line break is a line break";
194
+ /**
195
+ * One line of a command, with literal escape sequences marked.
196
+ *
197
+ * INJECTIVE, and the proof is short enough to keep here. The rendering is a
198
+ * left-to-right tokenizer over two tokens: a backslash followed by a letter in
199
+ * {@link MARKED_ESCAPES} becomes `«\c»`, and every other character is itself.
200
+ * A `«\c»` in the OUTPUT can therefore only have come from that first token,
201
+ * because a backslash followed by such a letter in the input is never emitted
202
+ * bare — so reading the output back left to right recovers the input exactly,
203
+ * and a left inverse is all injectivity needs.
204
+ *
205
+ * Real newlines are handled by the caller, which splits on them before calling
206
+ * this: a line break in the output comes from a line break in the input, and
207
+ * from nothing else.
208
+ */
209
+ export declare function markEscapes(line: string): string;
210
+ /** A shell command, recognised structurally. */
211
+ export interface CommandView {
212
+ /** The command, exactly as the payload carries it. */
213
+ command: string;
214
+ /** The working directory, or `null` when the payload names none. */
215
+ cwd: string | null;
216
+ }
217
+ /**
218
+ * Recognise a command payload, structurally.
219
+ *
220
+ * Accepted when the payload carries a string `command` and nothing this module
221
+ * cannot show. `cwd` is optional here even though `cli/hook.ts` always sets it:
222
+ * the question this answers is "will a human read this better as a command?",
223
+ * and a payload missing its directory reads better either way.
224
+ */
225
+ export declare function commandPayloadView(value: unknown): CommandView | null;
226
+ /**
227
+ * Where the exact bytes live, and how to get them back (APRV-126, APRV-162).
228
+ *
229
+ * Carried by every structural view, not the command view alone: with no
230
+ * canonical-JSON appendix underneath, this line is the reader's only route from
231
+ * a rendering back to the bytes it was derived from.
232
+ *
233
+ * The store is content-addressed by this very hash and re-verified on every
234
+ * read (`core/payload-store.ts`), so the line is an instruction, never a claim:
235
+ * following it produces the bytes or produces a refusal, and never something
236
+ * else wearing the same name.
237
+ */
238
+ export declare function rawBytesLine(hash: string): string;
239
+ /** What separates two segments of the breakdown. Exported: the tests pin it. */
240
+ export declare const BREAKDOWN_SEPARATOR = " \u00B7 ";
241
+ /** Characters one segment of the breakdown may take before it folds. */
242
+ export declare const BREAKDOWN_SEGMENT_BUDGET = 40;
243
+ /** Segments the breakdown shows before it says how many it did not. */
244
+ export declare const BREAKDOWN_MAX_SEGMENTS = 8;
245
+ /**
246
+ * What a compound command does, segment by segment (APRV-144).
247
+ *
248
+ * `git add … · git commit · git push origin main:records-… · gh pr create`.
249
+ *
250
+ * The observed complaint (Carter, 2026-08-25) is that the claimed summary of a
251
+ * shell action is `truncate(command, 160)`, which for a chained command is the
252
+ * first clause and a path prefix: the approver reads where the command starts
253
+ * and never what it ends by doing. This is the deterministic half of the
254
+ * answer. It is derived from {@link commandSegmentWords} — the classifier's own
255
+ * tokenizer, never a second one — so a channel showing it cannot describe a
256
+ * command differently from the module that chose its class.
257
+ *
258
+ * `null` for a string the tokenizer refuses (the same input the classifier
259
+ * answers `unparseable` for) and for one with no segment carrying a binary: an
260
+ * aid that cannot be derived is absent, never guessed.
261
+ */
262
+ export declare function commandBreakdown(command: string): string | null;
263
+ /** The path that made an action `policy.edit`, and the rule that matched it. */
264
+ export interface ProtectedPathView {
265
+ path: string;
266
+ rule: string;
267
+ }
268
+ /**
269
+ * Which protected path selected this payload's class, when one did (APRV-143).
270
+ *
271
+ * A prompt that says `class: policy.edit` and stops there tells the approver
272
+ * that *some* rule fired and leaves them to find the file. Both gated shapes
273
+ * can say which:
274
+ *
275
+ * - a shell payload is re-classified here, by the same
276
+ * {@link classifyCommand} the hook decided with, and the segment that took
277
+ * `policy.edit` carries the word it matched (`ClassifiedSegment.path`);
278
+ * - a file-tool payload names its target in `file`, and
279
+ * {@link isProtectedPath} is re-run over it rather than trusted: the answer
280
+ * is recomputed from the bound bytes, so this stays a computed field. The
281
+ * payload's own `rule` is used as the label only when it is one of the three
282
+ * the hook writes, which is what keeps the worktree-proposal and
283
+ * protected-name-elsewhere tiers legible (APRV-124, APRV-161).
284
+ *
285
+ * `extra` is `policy.protected_paths`, passed exactly as every enforcement path
286
+ * passes it; omitting it narrows the answer and never widens it.
287
+ */
288
+ export declare function protectedPathView(value: unknown, extra?: readonly ProtectedPathEntry[]): ProtectedPathView | null;
289
+ /**
290
+ * The renderer's identity, printed inside every canonical block.
291
+ *
292
+ * Inside the text, and therefore inside {@link CanonicalRendering.display_hash}:
293
+ * a version that rode alongside the hash rather than inside it would let two
294
+ * renderer versions produce the same digest for two different readings, which is
295
+ * the one thing the digest exists to make impossible. Any change to the bytes
296
+ * this module emits — a new field, a reworded heading, a line that used to be
297
+ * folded away — is a new version, and a reader comparing a stored
298
+ * `display_hash` against a re-render can see which renderer wrote it. A record
299
+ * written under an earlier version re-derives under the renderer its own hashed
300
+ * text names, never under this one.
301
+ *
302
+ * `/2` (APRV-162): the structural views render whole and carry no canonical-JSON
303
+ * appendix; `opaque` is unchanged, its view being that JSON.
304
+ */
305
+ export declare const CANONICAL_RENDERER_VERSION = "approval.md/wysiwys/2";
306
+ /**
307
+ * The `approval.requested` payload field carrying {@link
308
+ * CanonicalRendering.display_hash} (APRV-119).
309
+ *
310
+ * Written by the gate at the write boundary, exactly as `ts` and `policy_sha256`
311
+ * are, and for the same reason: the requesting party must not be able to name
312
+ * the rendering it claims a human was shown. {@link RequestInput} carries no
313
+ * field for it.
314
+ */
315
+ export declare const DISPLAY_HASH_FIELD = "display_hash";
316
+ /** The delimiters of the canonical block. Exported because the tests pin them. */
317
+ export declare const CANONICAL_BEGIN = "--- canonical rendering begins ---";
318
+ export declare const CANONICAL_END = "--- canonical rendering ends ---";
319
+ /** The heading of the `opaque` kind: no structural view, the bytes whole. */
320
+ export declare const OPAQUE_VIEW_HEADING = "payload \u2014 no structural view applies to this shape; every byte of it is in the canonical JSON below, and every value is CLAIMED, authored by the requesting party";
321
+ /**
322
+ * The kinds a payload can be rendered as.
323
+ *
324
+ * Closed, and decided by structure alone. `opaque` is not a failure: it is the
325
+ * kind whose closed field set is "the whole canonical JSON", which is the
326
+ * rendering every payload had before the structural views existed.
327
+ */
328
+ export declare const CANONICAL_KINDS: readonly ["command", "file-change", "email", "opaque"];
329
+ export type CanonicalKind = (typeof CANONICAL_KINDS)[number];
330
+ /** One canonical rendering: what the human reads, and the digest of it. */
331
+ export interface CanonicalRendering {
332
+ /** {@link CANONICAL_RENDERER_VERSION}, for a caller that wants it separately. */
333
+ version: string;
334
+ /** Which structural view was applied. */
335
+ kind: CanonicalKind;
336
+ /** The text, with real newlines. Escaping belongs to the channel. */
337
+ text: string;
338
+ /** SHA-256 (lowercase hex) over `text` as UTF-8. */
339
+ display_hash: string;
340
+ }
341
+ /**
342
+ * Render a payload the way every channel MUST present it (APRV-119).
343
+ *
344
+ * A pure function of `(payload, actionClass)`. Same arguments, byte-identical
345
+ * `text` and `display_hash`, in this process or another, today or next year
346
+ * under the same {@link CANONICAL_RENDERER_VERSION}.
347
+ *
348
+ * The block states its own renderer, class, kind and payload digest before it
349
+ * shows anything, so a reader who is handed the text alone can tell what
350
+ * produced it and what it binds to. Then the view for the kind, which is the
351
+ * whole reading: it renders every byte of the payload or the payload is
352
+ * `opaque` and the view is its JSON (APRV-162).
353
+ *
354
+ * Throws `JcsError` for a payload RFC 8785 cannot serialize (a cycle, a NaN).
355
+ * That is {@link payloadHash}'s contract and it is the right one here too: a
356
+ * payload that cannot be bound to must not acquire a plausible-looking rendering
357
+ * of itself. Every caller in this repository renders material that has already
358
+ * been hash-checked against the log's binding, so the throw is unreachable on
359
+ * the paths a human ever sees.
360
+ */
361
+ export declare function canonicalRender(payload: unknown, actionClass: string): CanonicalRendering;
362
+ /**
363
+ * The `display_hash` of a payload, or `null` when there is none to compute.
364
+ *
365
+ * The gate's entry point (`core/gate.ts`), where a payload that cannot be
366
+ * canonicalized must not abort a request that has already passed every check
367
+ * that matters. A missing `display_hash` costs a reader one cross-check; a
368
+ * throw here would cost them the request.
369
+ */
370
+ export declare function displayHashOf(payload: unknown, actionClass: string): string | null;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The daemon's advance, run in a child process (APRV-211).
3
+ *
4
+ * ## Why this file exists
5
+ *
6
+ * `approval up` runs the daemon loop and the Telegram listener in one process,
7
+ * and `cli/log-advance.ts` is `spawnSync` from end to end. So an advance run on
8
+ * the daemon's own stack blocks the loop for as long as `git fetch`, the
9
+ * scratch-index commit, `git push` and `gh pr create` take, and every callback
10
+ * that arrived meanwhile was answered past Telegram's window: the
11
+ * `answerCallbackQuery: HTTP 400`s Carter saw on 2026-09-02. Nothing that runs
12
+ * on that loop can fix it, because synchronous work does not yield. Another
13
+ * process can.
14
+ *
15
+ * ## What it is allowed to do, and what it is not
16
+ *
17
+ * It runs the verb. That is the entire remit.
18
+ *
19
+ * It does NOT touch the gate, and it could not if it tried: `core/child-env.ts`
20
+ * strips `APPROVAL_*` from a child's environment (APRV-205), which is where the
21
+ * `supervised-live` draw's secret lives, so a child that asked the gate would
22
+ * fail closed on every tick. The register/request/start half happens in the
23
+ * daemon before this is spawned and the `execution.completed`/`failed` is
24
+ * appended by the daemon after it exits. This process appends nothing, decides
25
+ * nothing, and holds no authority: if it were replaced wholesale by something
26
+ * hostile, the worst it could do is refuse to advance the log or report a
27
+ * failure that did not happen — it cannot authorise anything, because by the
28
+ * time it runs the authorisation is already in the log and already spent.
29
+ *
30
+ * ## The protocol
31
+ *
32
+ * One argument: the JSON `LogAdvanceOptions` subset the daemon chose. One line
33
+ * on stdout: the `LogAdvanceResult` verbatim, `{ok:true,report}` or
34
+ * `{ok:false,code,message}`. The parent VALIDATES that line rather than
35
+ * trusting it, and treats anything else as a failed advance with a
36
+ * machine-readable reason. Nothing is written to stdout but that line, which is
37
+ * why the verb is given no progress reporter.
38
+ */
39
+ export {};