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,543 @@
1
+ /**
2
+ * Shell command classification: a pure, deterministic map from a command line
3
+ * to the SPEC.md §7 side-effect classes it would produce (APRV-82).
4
+ *
5
+ * This is the input half of the Claude Code PreToolUse hook. The harness hands
6
+ * us a command string it is about to run; policy speaks in classes; something
7
+ * has to translate. That translation is the reviewable artifact of this task, so
8
+ * it lives in one file, is data-driven, and is exhaustively fixture-tested.
9
+ *
10
+ * Three properties are load-bearing.
11
+ *
12
+ * **Pure.** No filesystem, no clock, no environment, no network. The same string
13
+ * always yields the same answer, which is what makes the fixture table a real
14
+ * specification rather than a sample of observed behaviour.
15
+ *
16
+ * **Fail closed, in three named ways.** A construct whose effect cannot be read
17
+ * off the text is `opaque` (`bash -c`, `eval`, backticks, a tainting command
18
+ * substitution). A binary the table does not know is `unclassified`. A string
19
+ * the tokenizer cannot even split is `unparseable`. All three are refusals, and
20
+ * the hook denies on each: the alternative, guessing a class for a command we do
21
+ * not understand, would let the least legible command lines through the widest
22
+ * gap. Ambiguity inside a construct we DO understand resolves to the stricter
23
+ * class (`git push` with no refspec is `vcs.push.main`, an `rm` path holding an
24
+ * unexpanded `$VAR` is `files.delete.out_of_scope`).
25
+ *
26
+ * **Best effort, and honest about it.** This is a classifier over shell text,
27
+ * not a shell. It is a speed bump against an agent doing the wrong thing by
28
+ * mistake, in the SPEC.md §11 sense; it is not scheming-robust, and nothing here
29
+ * claims otherwise. The hard boundary stays where SPEC.md §10.4 puts it: the
30
+ * credentials in the vault and the token an adapter demands.
31
+ *
32
+ * Self-reported text is never read. The hook passes the command only; the
33
+ * harness's `description` field is authored by the very agent being gated
34
+ * (SPEC.md §11.1: self-reported fields never reduce scrutiny).
35
+ */
36
+ /** Why a command could not be classified. Each denies; none is a soft failure. */
37
+ export type ClassifierFailureCode = "unclassified" | "opaque" | "unparseable";
38
+ /** One command in a pipeline or list, with the class it resolved to. */
39
+ export interface ClassifiedSegment {
40
+ /** The segment's source text, as written. */
41
+ text: string;
42
+ /** The dotted side-effect class (SPEC.md §7). */
43
+ class: string;
44
+ /** Which rule decided it, for `hook classify` output and for tests. */
45
+ rule: string;
46
+ /**
47
+ * The protected path that selected the class, present only when one did
48
+ * (APRV-143).
49
+ *
50
+ * The protected classes (`policy.edit`, `policy.core`, `log.mutate`) are the
51
+ * classes a segment can take because of a *value* in it rather than because
52
+ * of its binary, and until this field the value was discarded the moment the
53
+ * rule fired: an approver was told the class and left to guess which of six
54
+ * arguments earned it. It is the word the classifier matched, verbatim, so a
55
+ * channel can name it without a second search of its own.
56
+ */
57
+ path?: string;
58
+ /**
59
+ * The sandbox wrapper this segment runs inside, when the classifier unwrapped
60
+ * one (APRV-193). Absent means "not wrapped, as far as this classifier can
61
+ * tell", which is the direction every caller must fail in: the marker is only
62
+ * ever read to RELAX a requirement, so an early return that omits it costs a
63
+ * refusal and never an authorization.
64
+ *
65
+ * `runtime` is `approval sandbox -- …`, whose profile this runtime writes.
66
+ * `external` is a hand-written `sandbox-exec -f <profile> …`, which is
67
+ * classified honestly (by its inner argv) and trusted for nothing: the
68
+ * profile is the caller's, and a profile a caller wrote can allow anything.
69
+ */
70
+ sandbox?: SandboxWrapper;
71
+ }
72
+ /**
73
+ * Who wrote the profile a wrapped command runs under (APRV-193).
74
+ *
75
+ * The distinction is the whole reason this is not a boolean. Classification
76
+ * treats the two identically — both are unwrapped to the inner argv, because a
77
+ * wrapper is a room and the class belongs to what runs in it. Enforcement does
78
+ * not: `APPROVAL_HOOK_REQUIRE_SANDBOX` is satisfied only by `runtime`, because
79
+ * `sandbox-exec -f /tmp/anything.sb` with a profile whose only line is
80
+ * `(allow default)` denies nothing at all, and a requirement a caller can meet
81
+ * by writing their own permission is not a requirement.
82
+ */
83
+ export type SandboxWrapper = "runtime" | "external";
84
+ export type CommandClassification = {
85
+ ok: true;
86
+ segments: ClassifiedSegment[];
87
+ classes: string[];
88
+ } | {
89
+ ok: false;
90
+ code: ClassifierFailureCode;
91
+ /** The segment (or whole command) that could not be read. */
92
+ segment: string;
93
+ detail: string;
94
+ };
95
+ /**
96
+ * The pass-through pseudo-class for the gate's own CLI.
97
+ *
98
+ * `approval …` is already the enforcement path — gating it with itself would
99
+ * either deadlock (the hook waiting on a decision that `approval grant` cannot
100
+ * deliver) or recurse. The hook allows this class without touching the log; it
101
+ * is never written to any envelope and no policy rule should name it.
102
+ */
103
+ export declare const GATE_SELF_CLASS = "gate.self";
104
+ /**
105
+ * The three classes a protected path can select (APRV-198).
106
+ *
107
+ * One class used to cover the whole protected surface, so a policy could not
108
+ * say "an agent may edit the prose that describes the gate, under sampling,
109
+ * and may never touch the gate itself". These are that sentence, in the order
110
+ * of decreasing consequence:
111
+ *
112
+ * - `log.mutate` — anything aimed at `.approval/log/`. The log is the truth;
113
+ * a write to it is not an edit of the rules, it is an edit of the record of
114
+ * what happened.
115
+ * - `policy.core` — the policy file itself and the rest of the gate's own
116
+ * directory (env, payload store, vault, keys, `QUEUE.md`), plus the harness
117
+ * files that install the hook. An agent that can write these can write
118
+ * itself out of the gate without the gate ever seeing it.
119
+ * - `policy.edit` — the prose and configuration ABOUT the gate: the agent
120
+ * instructions, CI and release configuration, and whatever paths the policy
121
+ * itself protects. Reviewable after the fact, and the only one of the three
122
+ * a policy can sensibly sample.
123
+ */
124
+ export type ProtectedPathClass = "log.mutate" | "policy.core" | "policy.edit";
125
+ /**
126
+ * The `policy.edit` sub-class namespace a `protected_paths` entry may route to
127
+ * (APRV-266).
128
+ *
129
+ * One protected surface with one autonomy was the shape until now, so a policy
130
+ * that wanted its specification sampled at one tenth and its release workflows
131
+ * gated every time had to choose one of those numbers for both. A routed entry
132
+ * says which sub-class a path family takes, and the sub-class is an ordinary
133
+ * §7 class with an ordinary policy line, so each family gets its own autonomy
134
+ * and its own live rate without any new grammar in `classes`.
135
+ *
136
+ * The namespace is closed to ONE extra segment under `policy.edit` and nothing
137
+ * else. A policy may not route a path to `policy.core`, to `log.mutate`, or to
138
+ * any class outside this namespace: those are the gate's own organs and the
139
+ * record of what happened, and §11.1 invariant 9 reserves them — a policy
140
+ * widening its own protected surface is naming prose and configuration, and
141
+ * mints no authority over anything else. `policy.schema.json` enforces the
142
+ * shape; this pattern is the same rule where the matcher can see it.
143
+ */
144
+ export declare const POLICY_EDIT_SUBCLASS: RegExp;
145
+ /**
146
+ * Sub-class names this spec reserves, with the meaning an implementation must
147
+ * give them (APRV-266).
148
+ *
149
+ * Reserved so that two policies written by two people mean the same thing by
150
+ * `policy.edit.ci`, and so a reader of somebody else's policy does not have to
151
+ * infer it. An author is free to mint their own word beside these — the pattern
152
+ * above admits any lowercase word — and a minted name carries only the meaning
153
+ * its own policy line gives it.
154
+ */
155
+ export declare const RESERVED_POLICY_EDIT_SUBCLASSES: Readonly<Record<string, string>>;
156
+ /**
157
+ * One `protected_paths` entry: a bare path, or a path routed to a sub-class.
158
+ *
159
+ * The bare string is the APRV-107 spelling and keeps its meaning exactly —
160
+ * `policy.edit`, as though the object form did not exist. Nothing about a
161
+ * string-only policy changes by a byte, which is the compatibility rule this
162
+ * type exists to state: `approval hook classify` over such a policy answers
163
+ * today's class with today's rule name.
164
+ */
165
+ export type ProtectedPathEntry = string | {
166
+ path: string;
167
+ class: string;
168
+ };
169
+ /**
170
+ * One entry of `policy.protected_paths`, pre-split (APRV-107).
171
+ *
172
+ * `directory` records the trailing `/` that distinguishes `design/` (a subtree)
173
+ * from `design` (a file named `design`).
174
+ */
175
+ interface ProtectedEntry {
176
+ segments: string[];
177
+ directory: boolean;
178
+ /**
179
+ * The class this entry routes to (APRV-266). `null` for a bare string entry,
180
+ * which means `policy.edit` and is matched in the built-in `policy.edit`
181
+ * tier exactly where it always was.
182
+ */
183
+ routed: string | null;
184
+ }
185
+ /**
186
+ * Read one `policy.protected_paths` entry, or `null` when it names nothing.
187
+ *
188
+ * The schema already rejects globs, absolute paths and `..` segments; this
189
+ * repeats the structural half of that check so a caller that skipped
190
+ * validation gets an entry that matches nothing rather than an entry that
191
+ * matches surprisingly. Pure, like everything else here: no resolution against
192
+ * a checkout, no disk.
193
+ *
194
+ * The same defensiveness covers the routed form (APRV-266): an object whose
195
+ * `class` is not a well-formed `policy.edit` sub-class matches NOTHING at all
196
+ * rather than falling back to `policy.edit`. A silent fallback would be the
197
+ * worst of the three available answers — the author would read their file and
198
+ * see a rate that is not the rate in force — and the loader refuses such a
199
+ * policy outright, so this branch is only ever reached by a caller that
200
+ * skipped validation.
201
+ */
202
+ export declare function parseProtectedEntry(entry: ProtectedPathEntry): ProtectedEntry | null;
203
+ /**
204
+ * Which protected class does this path name, if any? (APRV-198.)
205
+ *
206
+ * Deliberately name-based rather than location-based: the hook runs in whatever
207
+ * directory the harness is in, and a classifier that resolved paths against a
208
+ * checkout would answer differently in a worktree than in the primary. A
209
+ * false positive here costs one approval prompt; a false negative costs the
210
+ * property the whole file exists to defend.
211
+ *
212
+ * **The check order IS the precedence.** A path is answered by the strictest
213
+ * surface it names, `log.mutate` first, then `policy.core`, then
214
+ * `policy.edit`: `.approval/log/events.jsonl` is a log write and not merely an
215
+ * approval-home write, and a `policy.protected_paths` entry that happens to
216
+ * name a built-in surface cannot demote it, because the built-ins are matched
217
+ * before the policy's own list is read.
218
+ *
219
+ * `extra` carries `policy.protected_paths` (APRV-107). It is strictly
220
+ * ADDITIVE: the built-in set above is protected whatever a policy says, so a
221
+ * policy can widen the protected surface and can never narrow it, and every
222
+ * path it adds in the APRV-107 bare-string spelling lands on `policy.edit` —
223
+ * the reviewable class — because a policy widening its own surface is naming
224
+ * prose and configuration, not minting authority over the gate's organs. Still
225
+ * pure: the caller loads the policy, this function only matches segments.
226
+ *
227
+ * ## Routed entries (APRV-266)
228
+ *
229
+ * An entry written as `{path, class}` answers with the class it names, and the
230
+ * routed tier sits between the built-in `policy.core` tier and the built-in
231
+ * `policy.edit` tier. That position is the whole of the routing rule:
232
+ *
233
+ * - It is BELOW `log.mutate` and `policy.core`, so a routing can never reach
234
+ * the log or the gate's own organs. `{path: .approval/, class:
235
+ * policy.edit.home}` matches nothing, because tier 2 answered first — which
236
+ * is invariant 9's "no verb minting authority" holding at the one place a
237
+ * policy could otherwise have reached past it. The loader refuses such an
238
+ * entry outright rather than letting it sit inert.
239
+ * - It is ABOVE the built-in `policy.edit` set, so a routing CAN re-label a
240
+ * built-in `policy.edit` path: `{path: .github/workflows/, class:
241
+ * policy.edit.ci}` is exactly the sentence a project wants to write. What
242
+ * stops that from being a demotion is not this function but the load-time
243
+ * floor in `policy-load.ts`, which refuses a policy whose routing would
244
+ * resolve a built-in path below what the `policy.edit` line itself resolves
245
+ * to. The classifier stays pure: it reports the class the policy named and
246
+ * judges no autonomy.
247
+ *
248
+ * Among several routed entries matching one path, the MOST SPECIFIC wins (most
249
+ * segments), and declaration order breaks a tie. Most-specific is what a
250
+ * carve-out means — `design/` routed one way and `design/frozen/` another — and
251
+ * a tie is two entries of equal depth both claiming one path, which is the
252
+ * author's own ambiguity and is resolved the only way a pure function can:
253
+ * by the order they wrote them in.
254
+ *
255
+ * A string-only `extra` cannot reach the routed tier at all, so a policy that
256
+ * has not adopted the object form classifies byte for byte as it did before.
257
+ */
258
+ export declare function protectedPathClass(candidate: string, extra?: readonly ProtectedPathEntry[]): string | null;
259
+ /**
260
+ * The class a path takes from the BUILT-IN set alone, ignoring every policy
261
+ * entry (APRV-266).
262
+ *
263
+ * The load-time routing floor needs this and nothing else: "is the path this
264
+ * entry routes one the runtime protects on its own?" decides whether the floor
265
+ * applies to it, and asking {@link protectedPathClass} with the policy's own
266
+ * entries in hand would answer with the routing under test.
267
+ */
268
+ export declare function builtinProtectedPathClass(candidate: string): ProtectedPathClass | null;
269
+ /**
270
+ * Does this path name something only a human may write?
271
+ *
272
+ * The boolean face of {@link protectedPathClass}, kept because two callers
273
+ * (`core/wysiwys.ts`'s protected-path view and the hook's file-tool gate) ask
274
+ * whether a path is protected at all before they ask which surface it is.
275
+ */
276
+ export declare function isProtectedPath(candidate: string, extra?: readonly ProtectedPathEntry[]): boolean;
277
+ /**
278
+ * One path, in the spelling an organ attestation records (APRV-272).
279
+ *
280
+ * Segment-wise: separators collapse, `./` noise disappears, a trailing slash
281
+ * goes, and the result is joined with `/` whatever the caller's platform uses.
282
+ * Two spellings of one file therefore attest and match as one file, which is
283
+ * the property the guard needs, since git reports `.claude/settings.json` and a
284
+ * human at a terminal may type `./.claude/settings.json`.
285
+ *
286
+ * Pure and disk-free, like everything else in this file: it never resolves,
287
+ * never follows a link, and never asks whether the path exists.
288
+ */
289
+ export declare function normalizePathSpelling(candidate: string): string;
290
+ /**
291
+ * Is this path one of the gate's ORGANS — a `policy.core` surface a human can
292
+ * attest by content (APRV-272)?
293
+ *
294
+ * The organs are the harness files that install the hook: `.claude/settings*`
295
+ * and Cursor's `hooks.json`, `hooks/` and `agents/`. They are `policy.core`
296
+ * because an agent that could write them could write itself out of the gate,
297
+ * and `policy.core` is human-only, so the gate mints no record for them at all
298
+ * — which is exactly why the protected-path guard could never pass a hand-made
299
+ * edit to one, and why {@link normalizePathSpelling}-keyed attestation is the
300
+ * evidence for them.
301
+ *
302
+ * Two `policy.core` surfaces are deliberately NOT organs, and both keep their
303
+ * own rules:
304
+ *
305
+ * - The policy file, which has had content attestation since APRV-15 and whose
306
+ * attestation the gate reads on every operation. An organ record must never
307
+ * be able to stand in for it.
308
+ * - Everything under the approval home (`.approval/`): the payload store, the
309
+ * vault, the keys, the environment map, the queue. Those are the human's own
310
+ * ceremony surface, and the log directory under them is `log.mutate`, which
311
+ * is stricter still.
312
+ *
313
+ * The question is asked of the BUILT-IN set alone. A policy may not route any
314
+ * path to `policy.core` (§11.1 invariant 9 and the `policy.edit.*` namespace
315
+ * close that), so consulting the policy's entries here could only ever widen
316
+ * the set of files a human may attest by a routing the classifier already
317
+ * refuses to honor.
318
+ */
319
+ export declare function isGateOrganPath(candidate: string): boolean;
320
+ /**
321
+ * Environment variables whose NAME says they carry a secret.
322
+ *
323
+ * Prefix-matched, because the classifier reads command text and never an
324
+ * environment: it cannot know which `APPROVAL_*` holds a token, so it treats
325
+ * the family alike and lets the allowlist below carve out the runtime's own
326
+ * non-secret names. Erring wide costs one approval prompt.
327
+ *
328
+ * Exported since APRV-205: `core/child-env.ts` starves a spawned child of the
329
+ * same family, and two copies of this list would be one list that drifts.
330
+ *
331
+ * `AGENTMAIL_` joins the family with the AgentMail adapter (APRV-224). An
332
+ * AgentMail API key is a mailbox in one string, and the deployment the adapter
333
+ * assumes hands the agent a key that cannot send while the sending key waits in
334
+ * the vault (SPEC.md §10.4). A key of either half in a granted child's
335
+ * environment would undo that split, so the prefix is withheld like the rest.
336
+ * The adapter's own declared credentials are vault names (`agentmail.api_key`,
337
+ * `agentmail.inbox_id`), so nothing under this prefix passes through by
338
+ * declaration either.
339
+ */
340
+ export declare const SECRET_ENV_PREFIXES: readonly string[];
341
+ /**
342
+ * The runtime's own variables under those prefixes that hold no secret: an
343
+ * identity, a rendering switch, a path. Listed rather than pattern-matched so
344
+ * that adding one is a deliberate act with a reviewer.
345
+ */
346
+ export declare const NON_SECRET_ENV_NAMES: readonly string[];
347
+ /**
348
+ * Does this bare variable name name credential material?
349
+ *
350
+ * Exported since APRV-205 for the same reason the two lists are: the scrub that
351
+ * builds a granted child's environment asks exactly this question, of a real
352
+ * environment rather than of command text, and it must ask it the same way.
353
+ */
354
+ export declare function isSecretEnvName(name: string): boolean;
355
+ /**
356
+ * Facts about the machine the command will run on, resolved by the CALLER
357
+ * (APRV-267).
358
+ *
359
+ * The classifier is pure and stays pure. Some rules, though, turn on something
360
+ * no string can carry: whether a path names the agent's own scratch space. So
361
+ * the impure half is hoisted out of this file entirely — the caller resolves
362
+ * the roots, this file only compares path segments against them — and the
363
+ * shape is the one `protectedPaths` already established: an optional argument
364
+ * whose absence yields the strictly NARROWER answer. A caller that forgets it
365
+ * classifies every delete the way this file classified it before the field
366
+ * existed; it never invents an authorization.
367
+ *
368
+ * Every root must be ABSOLUTE and already resolved (symlinks followed) by the
369
+ * caller. This file does not touch the disk and cannot check either property,
370
+ * so a caller handing it a relative or unresolved root gets segment matching
371
+ * against exactly what it passed.
372
+ */
373
+ export interface ClassifierContext {
374
+ /**
375
+ * Roots under which a delete is the agent tidying after itself: the session
376
+ * scratchpad the harness allots, and the system temp directory.
377
+ *
378
+ * `src/cli/hook.ts` resolves these (`resolveScratchRoots`) and tightens the
379
+ * answer afterwards with the checks that need the disk — a symlink escaping
380
+ * the root, a git checkout living inside it. Nothing here is a grant on its
381
+ * own: a path under a root still has to survive that second pass.
382
+ */
383
+ scratchRoots?: readonly string[];
384
+ }
385
+ /** Everything a refinement needs: the binary and the words that followed it. */
386
+ interface RuleContext {
387
+ bin: string;
388
+ /** Words after the binary, quotes already removed. */
389
+ args: string[];
390
+ /** Words after the binary that are not flags. */
391
+ positionals: string[];
392
+ /** The matched subcommand (first positional), or `null`. */
393
+ sub: string | null;
394
+ /**
395
+ * Did any of those words come out of a command substitution? Its text is
396
+ * gone by the time a rule sees it, so a rule that reads its arguments closely
397
+ * (APRV-114's fetch refinement) needs to know that one of them is a hole.
398
+ */
399
+ substituted: boolean;
400
+ /** What the caller knows about the machine (APRV-267). Never read from here. */
401
+ context: ClassifierContext;
402
+ }
403
+ /**
404
+ * A refinement's answer: a class and the rule id that chose it, or a statement
405
+ * that this invocation cannot be read at all.
406
+ *
407
+ * The opaque arm is APRV-283's. A refinement that returns `null` is answered
408
+ * with the interpreter message (`runs inline source`), which is true of
409
+ * `node -e` and false of `find … -exec`, and a refusal that misdescribes the
410
+ * command it refuses sends the agent looking for a flag it did not pass. A
411
+ * refinement that has its own reason states it.
412
+ */
413
+ type Refinement = {
414
+ class: string;
415
+ rule: string;
416
+ } | {
417
+ opaque: string;
418
+ };
419
+ /**
420
+ * One row of the classification table.
421
+ *
422
+ * `bins` + `subs` is the match; `class` is the answer. A row with a `refine`
423
+ * looks at the flags before answering, and declares every class it can emit in
424
+ * `emits` so the table stays enumerable (the dogfood test reads that list).
425
+ */
426
+ export interface CommandRule {
427
+ /** Stable identifier, printed by `hook classify` and pinned by the fixtures. */
428
+ id: string;
429
+ bins: readonly string[];
430
+ /** Match only when the first positional is one of these. */
431
+ subs?: readonly string[];
432
+ class: string;
433
+ /** Additional classes a refinement may return. */
434
+ emits?: readonly string[];
435
+ /** Flag-sensitive answer; falls back to `class` when it returns `null`. */
436
+ refine?: (ctx: RuleContext) => Refinement | null;
437
+ }
438
+ /**
439
+ * The table.
440
+ *
441
+ * Order matters: the first row whose binary and subcommand match decides. Rows
442
+ * are grouped by binary, strictest interpretation first within a binary, and
443
+ * every class named here is one SPEC.md §7 declares (§7's developer-workstation
444
+ * namespaces, plus `read.shell` / `read.vcs.remote` / `read.web` under
445
+ * `read.*`), with one addition: the `log.*` namespace of the two verbs that
446
+ * move the log file, introduced by SPEC §10.1's APRV-125 amendment.
447
+ */
448
+ export declare const COMMAND_RULES: readonly CommandRule[];
449
+ /**
450
+ * The rules whose commands RUN CODE THIS RUNTIME DID NOT AUTHOR (APRV-193).
451
+ *
452
+ * Rule ids rather than classes, because the class does not separate them: `npm
453
+ * test`, `node build.mjs`, `tsc` and `mkdir` all resolve to
454
+ * `files.write.workspace`, and only the first three execute a file an agent may
455
+ * have written a minute ago. That is the whole distinction laundering turns on
456
+ * — the command's NAME stops describing its effect exactly when the effect is
457
+ * in a file the name does not mention — so it is drawn here, once, where a
458
+ * future rule's author will see it.
459
+ *
460
+ * Read by `APPROVAL_HOOK_REQUIRE_SANDBOX` (`src/cli/hook.ts`) and by nothing
461
+ * else. It grants nothing and denies nothing on its own: it says which commands
462
+ * the hook may be asked to require a sandbox for, and the requirement is off
463
+ * unless an operator turns it on.
464
+ */
465
+ export declare const CODE_EXECUTING_RULES: readonly string[];
466
+ /**
467
+ * Every class the table can emit, for docs and for the dogfood test.
468
+ *
469
+ * Fixed, and it does not include the `policy.edit` sub-classes (APRV-266): a
470
+ * routed class is emitted only because a particular policy named it, so the set
471
+ * of them is a property of that file rather than of this table. A reader
472
+ * asking "can the classifier ever emit this class?" of a routed name must ask
473
+ * it WITH the policy in hand — {@link emittableClass} is that question.
474
+ */
475
+ export declare const CLASSIFIER_CLASSES: readonly string[];
476
+ /**
477
+ * Can the classifier emit `actionClass` for a project whose policy carries
478
+ * these `protected_paths`? (APRV-266.)
479
+ *
480
+ * {@link CLASSIFIER_CLASSES} answers for the binary table, which is fixed. A
481
+ * routed class is not in that table and never will be: it exists because one
482
+ * policy wrote it beside one path, and the same name in another project's
483
+ * policy would be a different class over different files. So the reachability
484
+ * question — the one `core/policy-expectations.ts` asks of every class a policy
485
+ * declares, so that a policy line nobody can ever fire is caught at the
486
+ * ceremony rather than believed for a year — takes the policy's own entries.
487
+ *
488
+ * A routed name is reachable exactly when some entry routes to it. A
489
+ * `policy.edit.spec` rule in a policy whose `protected_paths` routes nothing to
490
+ * it is a line that will never fire, and saying so is the whole point.
491
+ */
492
+ export declare function emittableClass(actionClass: string, protectedPaths?: readonly ProtectedPathEntry[]): boolean;
493
+ /**
494
+ * Classify a shell command line into the classes it would produce.
495
+ *
496
+ * Every segment must classify: one unreadable segment refuses the whole
497
+ * command, because a command line's effect is the union of its parts and a
498
+ * partial answer would authorize the parts we happened to understand.
499
+ *
500
+ * `protectedPaths` is `policy.protected_paths` (APRV-107), added to the
501
+ * built-in protected set rather than replacing it. Omitting it classifies
502
+ * against the built-ins alone, which is the strictly narrower answer, so a
503
+ * caller that forgets it under-reports the protected classes rather than inventing an
504
+ * authorization; every enforcement path passes the loaded policy's list.
505
+ *
506
+ * Since APRV-266 an entry may be `{path, class}`, routing that path family to a
507
+ * `policy.edit` sub-class. The classifier stays what it was: the entry's class
508
+ * is DATA it copies out of the policy, matched by the same segment matcher as
509
+ * every other entry, so this resolves no autonomy at all.
510
+ *
511
+ * `context` (APRV-267) carries the machine facts a caller has resolved: today
512
+ * only `scratchRoots`. It behaves exactly as `protectedPaths` does: omitting it
513
+ * yields the strictly narrower answer, because every rule that reads it can only
514
+ * ever LOOSEN a class, and no rule reads it to loosen a protected or credential
515
+ * one.
516
+ */
517
+ export declare function classifyCommand(command: string, protectedPaths?: readonly ProtectedPathEntry[], context?: ClassifierContext): CommandClassification;
518
+ /** One segment's words, as the classifier's own tokenizer read them. */
519
+ export interface CommandSegmentWords {
520
+ /** The segment's source text, as written. */
521
+ text: string;
522
+ /** The binary, `VAR=value` prefixes already skipped, quotes already removed. */
523
+ bin: string;
524
+ /** Every word after the binary, flags included, in order. */
525
+ args: string[];
526
+ }
527
+ /**
528
+ * The words of each segment, from the SAME parse {@link classifyCommand} uses.
529
+ *
530
+ * Exported for the channel-side command breakdown (APRV-144): a prompt that
531
+ * says what a compound command does needs the verb and the arguments of each
532
+ * segment, and a display layer that re-split the string itself would be a
533
+ * second tokenizer, free to disagree with the one that chose the class. This
534
+ * runs {@link lex} — the tokenizer — and applies the same assignment-prefix
535
+ * skip `classifySegment` applies, and stops there: it classifies nothing and
536
+ * decides nothing.
537
+ *
538
+ * `null` when the tokenizer refuses the string, which is the same input
539
+ * `classifyCommand` answers `unparseable` for. Segments carrying no binary (a
540
+ * bare assignment, a lone redirection) are omitted: they have no verb to show.
541
+ */
542
+ export declare function commandSegmentWords(command: string): CommandSegmentWords[] | null;
543
+ export {};
@@ -240,6 +240,16 @@ export function protectedPathClass(candidate, extra = []) {
240
240
  if (next === "hooks.json" || next === "hooks" || next === "agents")
241
241
  return "policy.core";
242
242
  }
243
+ // Codex installs its hook through these configuration and script paths.
244
+ if (segment === ".codex") {
245
+ const next = segments[index + 1];
246
+ if (next === undefined ||
247
+ next === "config.toml" ||
248
+ next === "hooks.json" ||
249
+ next === "hooks" ||
250
+ segments.slice(index + 1).includes(".."))
251
+ return "policy.core";
252
+ }
243
253
  }
244
254
  // 3. The policy's own routed entries (APRV-266), above the built-in
245
255
  // `policy.edit` set so a routing can re-label one of those paths, and
@@ -868,27 +878,49 @@ function hasShortFlag(args, letters) {
868
878
  function isUnknownValue(word) {
869
879
  return word.includes("$") || word.includes("*") || word.includes("?") || word.startsWith("~");
870
880
  }
871
- /** `git push` — the three push classes turn on flags and refspecs. */
881
+ /** A bare release tag: the conventional `v` plus a semantic-version-shaped value. */
882
+ const V_PREFIXED_SEMVER = /^v(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/u;
883
+ /** Does one side of a push refspec explicitly name a tag? */
884
+ function isTagRef(name) {
885
+ if (name.startsWith("refs/tags/"))
886
+ return true;
887
+ if (name.startsWith("refs/heads/"))
888
+ return false;
889
+ return V_PREFIXED_SEMVER.test(name);
890
+ }
891
+ /** Does either source or destination of this refspec explicitly name a tag? */
892
+ function isTagRefspec(refspec) {
893
+ const colon = refspec.indexOf(":");
894
+ if (colon === -1)
895
+ return isTagRef(refspec);
896
+ return isTagRef(refspec.slice(0, colon)) || isTagRef(refspec.slice(colon + 1));
897
+ }
898
+ /** `git push` — force, release, trunk and branch classes turn on flags and refspecs. */
872
899
  function refineGitPush(ctx) {
873
900
  const args = ctx.args.slice(1);
874
- if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes"])) {
901
+ if (hasFlag(args, ["--force", "-f", "--force-with-lease", "--force-if-includes", "--mirror"])) {
875
902
  return { class: "vcs.history.rewrite", rule: "git-push-force" };
876
903
  }
877
904
  const positionals = args.filter((arg) => !isFlag(arg));
878
- // A deletion, or a push with no refspec at all: the destination is either the
879
- // trunk or unknown, and unknown resolves to the stricter class.
905
+ const refspecs = positionals.slice(1);
906
+ if (refspecs.some((refspec) => refspec.startsWith("+"))) {
907
+ return { class: "vcs.history.rewrite", rule: "git-push-force" };
908
+ }
909
+ if (hasFlag(args, ["--tags", "--follow-tags"]) ||
910
+ refspecs.some(isTagRefspec) ||
911
+ refspecs.some((word, index) => word === "tag" && index + 1 < refspecs.length)) {
912
+ return { class: "release.publish", rule: "git-push-tag" };
913
+ }
914
+ // A non-tag deletion, or a push with no refspec at all: the destination is
915
+ // either the trunk or unknown, and unknown resolves to the stricter class.
880
916
  if (hasFlag(args, ["--delete", "-d"])) {
881
917
  return { class: "vcs.push.main", rule: "git-push-delete" };
882
918
  }
883
- const refspecs = positionals.slice(1);
884
919
  if (refspecs.length === 0) {
885
920
  return { class: "vcs.push.main", rule: "git-push-implicit" };
886
921
  }
887
922
  let sawMain = false;
888
923
  for (const refspec of refspecs) {
889
- if (refspec.startsWith("+")) {
890
- return { class: "vcs.history.rewrite", rule: "git-push-force" };
891
- }
892
924
  const colon = refspec.indexOf(":");
893
925
  const destination = colon === -1 ? refspec : refspec.slice(colon + 1);
894
926
  // `:branch` (empty source) and `src:` (empty destination) both delete a
@@ -1401,6 +1433,9 @@ function isGateEntrypoint(path) {
1401
1433
  function refineApprovalVerb(positionals) {
1402
1434
  const verb = positionals[0];
1403
1435
  const sub = positionals[1];
1436
+ if (verb === "quickstart") {
1437
+ return { class: "policy.core", rule: "approval-quickstart" };
1438
+ }
1404
1439
  if (verb === "log") {
1405
1440
  if (sub === "sync")
1406
1441
  return { class: "log.sync", rule: "approval-log-sync" };