approval-md 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/README.md +85 -877
  2. package/SPEC.md +150 -19
  3. package/cli.js +66 -7
  4. package/dist/src/adapters/smtp.d.ts +18 -0
  5. package/dist/src/adapters/smtp.js +24 -3
  6. package/dist/src/adapters/smtp.js.map +1 -1
  7. package/dist/src/channels/contract.d.ts +53 -1
  8. package/dist/src/channels/contract.js +304 -7
  9. package/dist/src/channels/contract.js.map +1 -1
  10. package/dist/src/channels/relay-server.d.ts +90 -0
  11. package/dist/src/channels/relay-server.js +295 -0
  12. package/dist/src/channels/relay-server.js.map +1 -0
  13. package/dist/src/channels/relay.d.ts +273 -0
  14. package/dist/src/channels/relay.js +564 -0
  15. package/dist/src/channels/relay.js.map +1 -0
  16. package/dist/src/channels/tagging.js +6 -1
  17. package/dist/src/channels/tagging.js.map +1 -1
  18. package/dist/src/channels/telegram-webhook.d.ts +273 -0
  19. package/dist/src/channels/telegram-webhook.js +536 -0
  20. package/dist/src/channels/telegram-webhook.js.map +1 -0
  21. package/dist/src/channels/telegram.d.ts +394 -12
  22. package/dist/src/channels/telegram.js +621 -68
  23. package/dist/src/channels/telegram.js.map +1 -1
  24. package/dist/src/channels/web.d.ts +9 -0
  25. package/dist/src/channels/web.js +17 -0
  26. package/dist/src/channels/web.js.map +1 -1
  27. package/dist/src/cli/amend.js +564 -34
  28. package/dist/src/cli/amend.js.map +1 -1
  29. package/dist/src/cli/attest.d.ts +9 -0
  30. package/dist/src/cli/attest.js +177 -13
  31. package/dist/src/cli/attest.js.map +1 -1
  32. package/dist/src/cli/channel-relay.d.ts +108 -0
  33. package/dist/src/cli/channel-relay.js +334 -0
  34. package/dist/src/cli/channel-relay.js.map +1 -0
  35. package/dist/src/cli/channel-telegram-webhook.d.ts +273 -0
  36. package/dist/src/cli/channel-telegram-webhook.js +709 -0
  37. package/dist/src/cli/channel-telegram-webhook.js.map +1 -0
  38. package/dist/src/cli/channel-telegram.d.ts +347 -28
  39. package/dist/src/cli/channel-telegram.js +763 -28
  40. package/dist/src/cli/channel-telegram.js.map +1 -1
  41. package/dist/src/cli/channel.d.ts +10 -1
  42. package/dist/src/cli/channel.js +14 -1
  43. package/dist/src/cli/channel.js.map +1 -1
  44. package/dist/src/cli/codex-bridge.d.ts +819 -0
  45. package/dist/src/cli/codex-bridge.js +1607 -0
  46. package/dist/src/cli/codex-bridge.js.map +1 -0
  47. package/dist/src/cli/codex.d.ts +1 -1
  48. package/dist/src/cli/codex.js +304 -7
  49. package/dist/src/cli/codex.js.map +1 -1
  50. package/dist/src/cli/daemon.js +41 -6
  51. package/dist/src/cli/daemon.js.map +1 -1
  52. package/dist/src/cli/doctor.d.ts +63 -0
  53. package/dist/src/cli/doctor.js +1322 -122
  54. package/dist/src/cli/doctor.js.map +1 -1
  55. package/dist/src/cli/execute.d.ts +17 -1
  56. package/dist/src/cli/execute.js +366 -22
  57. package/dist/src/cli/execute.js.map +1 -1
  58. package/dist/src/cli/exit-codes.d.ts +12 -0
  59. package/dist/src/cli/exit-codes.js +13 -0
  60. package/dist/src/cli/exit-codes.js.map +1 -1
  61. package/dist/src/cli/gate.d.ts +2 -0
  62. package/dist/src/cli/gate.js +233 -2
  63. package/dist/src/cli/gate.js.map +1 -1
  64. package/dist/src/cli/help.d.ts +14 -2
  65. package/dist/src/cli/help.js +395 -82
  66. package/dist/src/cli/help.js.map +1 -1
  67. package/dist/src/cli/hook-codex.d.ts +49 -1
  68. package/dist/src/cli/hook-codex.js +60 -1
  69. package/dist/src/cli/hook-codex.js.map +1 -1
  70. package/dist/src/cli/hook.d.ts +828 -4
  71. package/dist/src/cli/hook.js +2972 -142
  72. package/dist/src/cli/hook.js.map +1 -1
  73. package/dist/src/cli/import.js +1 -1
  74. package/dist/src/cli/import.js.map +1 -1
  75. package/dist/src/cli/init.js +16 -7
  76. package/dist/src/cli/init.js.map +1 -1
  77. package/dist/src/cli/instructions.js +24 -3
  78. package/dist/src/cli/instructions.js.map +1 -1
  79. package/dist/src/cli/log-anchor.d.ts +55 -0
  80. package/dist/src/cli/log-anchor.js +199 -60
  81. package/dist/src/cli/log-anchor.js.map +1 -1
  82. package/dist/src/cli/main.d.ts +0 -12
  83. package/dist/src/cli/main.js +56 -3
  84. package/dist/src/cli/main.js.map +1 -1
  85. package/dist/src/cli/muse.d.ts +2 -0
  86. package/dist/src/cli/muse.js +62 -0
  87. package/dist/src/cli/muse.js.map +1 -0
  88. package/dist/src/cli/payload.js +79 -1
  89. package/dist/src/cli/payload.js.map +1 -1
  90. package/dist/src/cli/policy-apply.d.ts +195 -0
  91. package/dist/src/cli/policy-apply.js +573 -0
  92. package/dist/src/cli/policy-apply.js.map +1 -0
  93. package/dist/src/cli/policy.js +14 -1
  94. package/dist/src/cli/policy.js.map +1 -1
  95. package/dist/src/cli/preflight.d.ts +151 -13
  96. package/dist/src/cli/preflight.js +398 -41
  97. package/dist/src/cli/preflight.js.map +1 -1
  98. package/dist/src/cli/sandbox.js +17 -1
  99. package/dist/src/cli/sandbox.js.map +1 -1
  100. package/dist/src/cli/scaffold.d.ts +1 -1
  101. package/dist/src/cli/scaffold.js +7 -1
  102. package/dist/src/cli/scaffold.js.map +1 -1
  103. package/dist/src/cli/serve.d.ts +53 -0
  104. package/dist/src/cli/serve.js +270 -0
  105. package/dist/src/cli/serve.js.map +1 -0
  106. package/dist/src/cli/setup-channel.d.ts +9 -0
  107. package/dist/src/cli/setup-channel.js +28 -1
  108. package/dist/src/cli/setup-channel.js.map +1 -1
  109. package/dist/src/cli/setup-common.d.ts +3 -1
  110. package/dist/src/cli/setup-common.js +3 -2
  111. package/dist/src/cli/setup-common.js.map +1 -1
  112. package/dist/src/cli/setup.d.ts +2 -0
  113. package/dist/src/cli/setup.js +94 -2
  114. package/dist/src/cli/setup.js.map +1 -1
  115. package/dist/src/cli/up.js +134 -51
  116. package/dist/src/cli/up.js.map +1 -1
  117. package/dist/src/cli/values.js +3 -4
  118. package/dist/src/cli/values.js.map +1 -1
  119. package/dist/src/cli/verb-registry.d.ts +2 -0
  120. package/dist/src/cli/verb-registry.js +510 -47
  121. package/dist/src/cli/verb-registry.js.map +1 -1
  122. package/dist/src/cli/wordmark.d.ts +2 -2
  123. package/dist/src/cli/wordmark.js +2 -2
  124. package/dist/src/codex/broker.d.ts +229 -0
  125. package/dist/src/codex/broker.js +548 -0
  126. package/dist/src/codex/broker.js.map +1 -0
  127. package/dist/src/codex/runner.d.ts +178 -0
  128. package/dist/src/codex/runner.js +231 -0
  129. package/dist/src/codex/runner.js.map +1 -0
  130. package/dist/src/codex/serve.d.ts +56 -0
  131. package/dist/src/codex/serve.js +98 -0
  132. package/dist/src/codex/serve.js.map +1 -0
  133. package/dist/src/codex/workspace-commit.d.ts +219 -0
  134. package/dist/src/codex/workspace-commit.js +549 -0
  135. package/dist/src/codex/workspace-commit.js.map +1 -0
  136. package/dist/src/core/advance-cycle.d.ts +51 -0
  137. package/dist/src/core/advance-cycle.js +66 -2
  138. package/dist/src/core/advance-cycle.js.map +1 -1
  139. package/dist/src/core/agents-md.d.ts +20 -18
  140. package/dist/src/core/agents-md.js +33 -31
  141. package/dist/src/core/agents-md.js.map +1 -1
  142. package/dist/src/core/attest-bootstrap.d.ts +95 -0
  143. package/dist/src/core/attest-bootstrap.js +135 -0
  144. package/dist/src/core/attest-bootstrap.js.map +1 -0
  145. package/dist/src/core/attest.d.ts +234 -2
  146. package/dist/src/core/attest.js +342 -7
  147. package/dist/src/core/attest.js.map +1 -1
  148. package/dist/src/core/audit.d.ts +73 -3
  149. package/dist/src/core/audit.js +65 -5
  150. package/dist/src/core/audit.js.map +1 -1
  151. package/dist/src/core/channel-lease.d.ts +322 -0
  152. package/dist/src/core/channel-lease.js +663 -0
  153. package/dist/src/core/channel-lease.js.map +1 -0
  154. package/dist/src/core/channel-owner.d.ts +213 -0
  155. package/dist/src/core/channel-owner.js +358 -0
  156. package/dist/src/core/channel-owner.js.map +1 -0
  157. package/dist/src/core/command-class.d.ts +205 -0
  158. package/dist/src/core/command-class.js +1433 -28
  159. package/dist/src/core/command-class.js.map +1 -1
  160. package/dist/src/core/commit-guard.d.ts +272 -0
  161. package/dist/src/core/commit-guard.js +424 -0
  162. package/dist/src/core/commit-guard.js.map +1 -0
  163. package/dist/src/core/daemon-actor.d.ts +45 -0
  164. package/dist/src/core/daemon-actor.js +54 -0
  165. package/dist/src/core/daemon-actor.js.map +1 -0
  166. package/dist/src/core/daemon-host.d.ts +178 -0
  167. package/dist/src/core/daemon-host.js +228 -0
  168. package/dist/src/core/daemon-host.js.map +1 -0
  169. package/dist/src/core/daemon-identity.d.ts +238 -0
  170. package/dist/src/core/daemon-identity.js +252 -0
  171. package/dist/src/core/daemon-identity.js.map +1 -0
  172. package/dist/src/core/dark-session.d.ts +109 -8
  173. package/dist/src/core/dark-session.js +266 -82
  174. package/dist/src/core/dark-session.js.map +1 -1
  175. package/dist/src/core/decision-refusal.d.ts +23 -2
  176. package/dist/src/core/decision-refusal.js +24 -2
  177. package/dist/src/core/decision-refusal.js.map +1 -1
  178. package/dist/src/core/env-file.d.ts +5 -0
  179. package/dist/src/core/env-file.js +60 -1
  180. package/dist/src/core/env-file.js.map +1 -1
  181. package/dist/src/core/execute.d.ts +15 -2
  182. package/dist/src/core/execute.js +15 -2
  183. package/dist/src/core/execute.js.map +1 -1
  184. package/dist/src/core/gate.d.ts +392 -6
  185. package/dist/src/core/gate.js +852 -24
  186. package/dist/src/core/gate.js.map +1 -1
  187. package/dist/src/core/gesture-refusal.d.ts +166 -0
  188. package/dist/src/core/gesture-refusal.js +188 -0
  189. package/dist/src/core/gesture-refusal.js.map +1 -0
  190. package/dist/src/core/harness-version.d.ts +128 -4
  191. package/dist/src/core/harness-version.js +181 -12
  192. package/dist/src/core/harness-version.js.map +1 -1
  193. package/dist/src/core/harness-wait.d.ts +82 -3
  194. package/dist/src/core/harness-wait.js +89 -3
  195. package/dist/src/core/harness-wait.js.map +1 -1
  196. package/dist/src/core/instance.d.ts +59 -2
  197. package/dist/src/core/instance.js +113 -0
  198. package/dist/src/core/instance.js.map +1 -1
  199. package/dist/src/core/log-subscribe.d.ts +21 -0
  200. package/dist/src/core/log-subscribe.js +16 -6
  201. package/dist/src/core/log-subscribe.js.map +1 -1
  202. package/dist/src/core/log-write-layer.d.ts +55 -0
  203. package/dist/src/core/log-write-layer.js +91 -0
  204. package/dist/src/core/log-write-layer.js.map +1 -0
  205. package/dist/src/core/log.d.ts +89 -22
  206. package/dist/src/core/log.js +197 -19
  207. package/dist/src/core/log.js.map +1 -1
  208. package/dist/src/core/payload-census.d.ts +51 -0
  209. package/dist/src/core/payload-census.js +125 -2
  210. package/dist/src/core/payload-census.js.map +1 -1
  211. package/dist/src/core/payload-store.d.ts +31 -0
  212. package/dist/src/core/payload-store.js +113 -8
  213. package/dist/src/core/payload-store.js.map +1 -1
  214. package/dist/src/core/payload.d.ts +18 -3
  215. package/dist/src/core/payload.js +20 -4
  216. package/dist/src/core/payload.js.map +1 -1
  217. package/dist/src/core/policy-diff.d.ts +10 -0
  218. package/dist/src/core/policy-diff.js +10 -2
  219. package/dist/src/core/policy-diff.js.map +1 -1
  220. package/dist/src/core/policy-explain.d.ts +17 -1
  221. package/dist/src/core/policy-explain.js +48 -1
  222. package/dist/src/core/policy-explain.js.map +1 -1
  223. package/dist/src/core/policy-load.d.ts +65 -1
  224. package/dist/src/core/policy-load.js +21 -3
  225. package/dist/src/core/policy-load.js.map +1 -1
  226. package/dist/src/core/policy-match.d.ts +81 -0
  227. package/dist/src/core/policy-match.js +87 -0
  228. package/dist/src/core/policy-match.js.map +1 -1
  229. package/dist/src/core/policy-proposal.d.ts +75 -0
  230. package/dist/src/core/policy-proposal.js +110 -4
  231. package/dist/src/core/policy-proposal.js.map +1 -1
  232. package/dist/src/core/protected-path-guard.d.ts +117 -4
  233. package/dist/src/core/protected-path-guard.js +362 -48
  234. package/dist/src/core/protected-path-guard.js.map +1 -1
  235. package/dist/src/core/question-preempted.d.ts +141 -0
  236. package/dist/src/core/question-preempted.js +152 -0
  237. package/dist/src/core/question-preempted.js.map +1 -0
  238. package/dist/src/core/read-scope.d.ts +182 -0
  239. package/dist/src/core/read-scope.js +256 -0
  240. package/dist/src/core/read-scope.js.map +1 -0
  241. package/dist/src/core/run-payload.d.ts +160 -0
  242. package/dist/src/core/run-payload.js +304 -0
  243. package/dist/src/core/run-payload.js.map +1 -0
  244. package/dist/src/core/sandbox.d.ts +81 -0
  245. package/dist/src/core/sandbox.js +190 -1
  246. package/dist/src/core/sandbox.js.map +1 -1
  247. package/dist/src/core/sender-identity.d.ts +501 -0
  248. package/dist/src/core/sender-identity.js +609 -0
  249. package/dist/src/core/sender-identity.js.map +1 -0
  250. package/dist/src/core/shlex.d.ts +102 -0
  251. package/dist/src/core/shlex.js +159 -0
  252. package/dist/src/core/shlex.js.map +1 -0
  253. package/dist/src/core/state.d.ts +52 -1
  254. package/dist/src/core/state.js +52 -6
  255. package/dist/src/core/state.js.map +1 -1
  256. package/dist/src/core/telegram-config.d.ts +79 -0
  257. package/dist/src/core/telegram-config.js +108 -0
  258. package/dist/src/core/telegram-config.js.map +1 -1
  259. package/dist/src/core/values.d.ts +18 -8
  260. package/dist/src/core/values.js +36 -1
  261. package/dist/src/core/values.js.map +1 -1
  262. package/dist/src/core/verify.d.ts +23 -1
  263. package/dist/src/core/verify.js +20 -2
  264. package/dist/src/core/verify.js.map +1 -1
  265. package/dist/src/daemon/advance.d.ts +10 -0
  266. package/dist/src/daemon/advance.js +25 -4
  267. package/dist/src/daemon/advance.js.map +1 -1
  268. package/dist/src/daemon/audit.d.ts +80 -5
  269. package/dist/src/daemon/audit.js +126 -6
  270. package/dist/src/daemon/audit.js.map +1 -1
  271. package/dist/src/daemon/daemon.d.ts +174 -2
  272. package/dist/src/daemon/daemon.js +307 -7
  273. package/dist/src/daemon/daemon.js.map +1 -1
  274. package/dist/src/daemon/git-evidence.d.ts +2 -2
  275. package/dist/src/daemon/git-evidence.js +1 -1
  276. package/dist/src/daemon/projection.d.ts +13 -2
  277. package/dist/src/daemon/projection.js +39 -6
  278. package/dist/src/daemon/projection.js.map +1 -1
  279. package/dist/src/mcp/server.d.ts +39 -3
  280. package/dist/src/mcp/server.js +30 -3
  281. package/dist/src/mcp/server.js.map +1 -1
  282. package/dist/src/muse/server.d.ts +16 -0
  283. package/dist/src/muse/server.js +238 -0
  284. package/dist/src/muse/server.js.map +1 -0
  285. package/dist/src/serve/archive.d.ts +158 -0
  286. package/dist/src/serve/archive.js +354 -0
  287. package/dist/src/serve/archive.js.map +1 -0
  288. package/dist/src/serve/arguments.d.ts +93 -0
  289. package/dist/src/serve/arguments.js +228 -0
  290. package/dist/src/serve/arguments.js.map +1 -0
  291. package/dist/src/serve/credentials.d.ts +91 -0
  292. package/dist/src/serve/credentials.js +143 -0
  293. package/dist/src/serve/credentials.js.map +1 -0
  294. package/dist/src/serve/follow.d.ts +86 -0
  295. package/dist/src/serve/follow.js +101 -0
  296. package/dist/src/serve/follow.js.map +1 -0
  297. package/dist/src/serve/hook-thread.d.ts +134 -0
  298. package/dist/src/serve/hook-thread.js +333 -0
  299. package/dist/src/serve/hook-thread.js.map +1 -0
  300. package/dist/src/serve/hook-worker.d.ts +134 -0
  301. package/dist/src/serve/hook-worker.js +173 -0
  302. package/dist/src/serve/hook-worker.js.map +1 -0
  303. package/dist/src/serve/server.d.ts +412 -0
  304. package/dist/src/serve/server.js +1251 -0
  305. package/dist/src/serve/server.js.map +1 -0
  306. package/docs/README-extended.md +766 -0
  307. package/docs/cli-reference.md +2563 -73
  308. package/docs/codex-enforced-session.md +75 -2
  309. package/docs/codex-workspace-broker.md +118 -0
  310. package/package.json +6 -2
  311. package/schema/event.schema.json +595 -9
  312. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-false.json +20 -0
  313. package/schema/fixtures/event/invalid/approval-granted-sender-hashed-raw-id.json +20 -0
  314. package/schema/fixtures/event/invalid/audit-gesture-refused-human-actor.json +16 -0
  315. package/schema/fixtures/event/invalid/audit-gesture-refused-no-actor-no-sender.json +15 -0
  316. package/schema/fixtures/event/invalid/audit-gesture-refused-unknown-gesture.json +16 -0
  317. package/schema/fixtures/event/invalid/audit-question-preempted-agent-actor.json +16 -0
  318. package/schema/fixtures/event/invalid/audit-question-preempted-no-question-id.json +16 -0
  319. package/schema/fixtures/event/invalid/audit-question-preempted-unknown-source.json +15 -0
  320. package/schema/fixtures/event/invalid/daemon-id-malformed.json +15 -0
  321. package/schema/fixtures/event/invalid/execution-started-truncated-policy-sha256.json +18 -0
  322. package/schema/fixtures/event/invalid/gate-path-signed-off-absolute-path.json +14 -0
  323. package/schema/fixtures/event/invalid/gate-path-signed-off-agent-actor.json +14 -0
  324. package/schema/fixtures/event/invalid/gate-path-signed-off-missing-path.json +13 -0
  325. package/schema/fixtures/event/invalid/harness-cap-at-margin.json +16 -0
  326. package/schema/fixtures/event/invalid/harness-cap-fractional.json +16 -0
  327. package/schema/fixtures/event/invalid/harness-cap-negative.json +16 -0
  328. package/schema/fixtures/event/invalid/harness-cap-string.json +16 -0
  329. package/schema/fixtures/event/invalid/harness-cap-without-harness-execution.json +16 -0
  330. package/schema/fixtures/event/invalid/harness-cap-zero.json +16 -0
  331. package/schema/fixtures/event/valid/approval-granted-sender-hashed.json +20 -0
  332. package/schema/fixtures/event/valid/approval-requested-harness-cap.json +18 -0
  333. package/schema/fixtures/event/valid/audit-gesture-refused-review-note.json +21 -0
  334. package/schema/fixtures/event/valid/audit-gesture-refused-sender-key-unavailable.json +19 -0
  335. package/schema/fixtures/event/valid/audit-gesture-refused.json +19 -0
  336. package/schema/fixtures/event/valid/audit-question-preempted-no-verdict.json +16 -0
  337. package/schema/fixtures/event/valid/audit-question-preempted.json +20 -0
  338. package/schema/fixtures/event/valid/daemon-identity-declared.json +17 -0
  339. package/schema/fixtures/event/valid/daemon-identity.json +15 -0
  340. package/schema/fixtures/event/valid/execution-started-policy-sha256.json +18 -0
  341. package/schema/fixtures/event/valid/gate-path-signed-off.json +14 -0
  342. package/schema/fixtures/event/valid/harness-kind-claude-code.json +23 -0
  343. package/schema/fixtures/event/valid/harness-kind-codex.json +23 -0
  344. package/schema/fixtures/event/valid/harness-kind-cursor.json +23 -0
  345. package/schema/fixtures/event/valid/harness-kind-grok.json +23 -0
  346. package/schema/fixtures/event/valid/harness-kind-hermes.json +23 -0
  347. package/schema/fixtures/event/valid/harness-kind-muse.json +23 -0
  348. package/schema/fixtures/policy/invalid/daemons-malformed-id.json +10 -0
  349. package/schema/fixtures/policy/invalid/senders-half-keyed.json +20 -0
  350. package/schema/fixtures/policy/valid/canonical.json +1 -1
  351. package/schema/fixtures/policy/valid/daemons-allowlist.json +19 -0
  352. package/schema/fixtures/policy/valid/senders-keyed.json +24 -0
  353. package/schema/fixtures/policy-md/invalid/schema-invalid-agent-may-request.md +21 -0
  354. package/schema/fixtures/policy-md/valid/agent-may-request.md +24 -0
  355. package/schema/fixtures/policy-md/valid/canonical.md +1 -1
  356. package/schema/fixtures/policy-md/valid/with-values.md +5 -7
  357. package/schema/fixtures/values/invalid/class-shaped.json +1 -1
  358. package/schema/fixtures/values/invalid/duplicate-entry.json +1 -1
  359. package/schema/fixtures/values/invalid/non-string-item.json +1 -1
  360. package/schema/fixtures/values/invalid/over-cap.json +1 -1
  361. package/schema/fixtures/values/invalid/unknown-key.json +1 -1
  362. package/schema/fixtures/values/invalid/version-float.json +1 -0
  363. package/schema/fixtures/values/invalid/version-integer.json +1 -0
  364. package/schema/fixtures/values/invalid/version-wrong-string.json +1 -0
  365. package/schema/fixtures/values/valid/empty-lists.json +2 -3
  366. package/schema/fixtures/values/valid/full.json +5 -7
  367. package/schema/fixtures/values/valid/minimal.json +1 -1
  368. package/schema/fixtures/values-md/invalid/schema-invalid.md +5 -3
  369. package/schema/fixtures/values-md/invalid/two-blocks.md +3 -3
  370. package/schema/fixtures/values-md/invalid/unterminated.md +2 -2
  371. package/schema/fixtures/values-md/invalid/version-1.md +69 -0
  372. package/schema/fixtures/values-md/invalid/version-unquoted.md +64 -0
  373. package/schema/fixtures/values-md/invalid/yaml-error.md +2 -2
  374. package/schema/fixtures/values-md/valid/absent.md +1 -1
  375. package/schema/fixtures/values-md/valid/with-values.md +5 -7
  376. package/schema/policy.schema.json +100 -4
  377. package/schema/values.schema.json +7 -11
  378. package/schema/fixtures/values/invalid/version-string.json +0 -1
@@ -59,12 +59,14 @@ import { createHash } from "node:crypto";
59
59
  import { accessSync, constants, existsSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
60
60
  import { tmpdir } from "node:os";
61
61
  import { basename, dirname, isAbsolute, join, relative, resolve as resolvePathSegments, sep } from "node:path";
62
- import { HUMAN_ACTOR_ENV, appendAttestation, checkAttestation, policyFileHash, resolveHumanActor, } from "../core/attest.js";
62
+ import { HUMAN_ACTOR_ENV, appendAttestation, attestedPolicyPayloadHash, checkAttestation, policyBytesHash, resolveHumanActor, } from "../core/attest.js";
63
63
  import { compareChains } from "../core/log-reconcile.js";
64
64
  import { diffPolicies, renderDiff, SPEC_NAMESPACES } from "../core/policy-diff.js";
65
65
  import { checkPolicyExpectations, describeFailure, describePinChange, diffPinSources, expectationsFor, DOGFOOD_SUITE_BUILT, DOGFOOD_SUITE_SOURCE, EXPECTATIONS_MODULE, } from "../core/policy-expectations.js";
66
66
  import { loadPolicy, parseDuration, POLICY_FILENAMES, } from "../core/policy-load.js";
67
- import { proposalState, proposeAttestation, } from "../core/policy-proposal.js";
67
+ import { payloadHash } from "../core/payload.js";
68
+ import { payloadPath, payloadStoreDirFor } from "../core/payload-store.js";
69
+ import { proposalPayloadValue, proposalState, proposeAttestation, } from "../core/policy-proposal.js";
68
70
  import { readVerifiedRecords } from "../core/state.js";
69
71
  import { boolFlag, parseFlags, stringFlag } from "./args.js";
70
72
  import { EXIT_INTEGRITY, EXIT_IO, EXIT_OK, EXIT_TORN_TAIL, EXIT_USAGE, } from "./exit-codes.js";
@@ -83,6 +85,9 @@ const FLAGS = {
83
85
  "--require-load": "boolean",
84
86
  "--dry-run": "boolean",
85
87
  "--commit": "boolean",
88
+ // APRV-341: the whole ceremony in one word. Implies `--commit` and forces the
89
+ // branch flow, so the amendment reaches a protected trunk the only way it can.
90
+ "--pr": "boolean",
86
91
  "--no-publish": "boolean",
87
92
  "--branch": "string",
88
93
  "--direct": "boolean",
@@ -946,7 +951,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
946
951
  }
947
952
  const dryRun = boolFlag(parsed.flags, "--dry-run");
948
953
  const requireLoad = boolFlag(parsed.flags, "--require-load");
949
- const wantCommit = boolFlag(parsed.flags, "--commit");
954
+ // APRV-341. `--pr` is `--commit` plus "and finish the job": the branch flow,
955
+ // whatever the protection probe thinks, because the operator has said which
956
+ // ceremony they want. `--commit` alone keeps letting the probe decide.
957
+ const wantPr = boolFlag(parsed.flags, "--pr");
958
+ const wantCommit = boolFlag(parsed.flags, "--commit") || wantPr;
950
959
  // APRV-130: the ceremony publishes by default (push, and on a protected main
951
960
  // branch + push + PR). `--no-publish` is the operator who wants it to stop at
952
961
  // the commit, which is what `--commit` did before the publishing half existed.
@@ -960,6 +969,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
960
969
  if (branchFlag !== null && branchFlag.trim().length === 0) {
961
970
  return usageError(streams, json, "--branch expects a branch name");
962
971
  }
972
+ if (wantPr && forceDirect) {
973
+ return usageError(streams, json, "--pr and --direct ask for opposite ceremonies: --pr publishes through a branch and a pull request, --direct commits on the branch you are standing on. Pass one of them");
974
+ }
975
+ if (wantPr && noPublish) {
976
+ return usageError(streams, json, "--pr and --no-publish ask for opposite ceremonies: --pr opens the pull request, --no-publish stops at the commit. Pass one of them");
977
+ }
963
978
  // Identity first, before a byte is read. Asking a human to read a diff and
964
979
  // only then telling them their sign-off cannot be attributed wastes the one
965
980
  // resource this system spends.
@@ -1006,13 +1021,54 @@ export function commandPolicyAmend(argv, streams, cwd) {
1006
1021
  return refuse(streams, json, "io", policy.message, EXIT_IO);
1007
1022
  const policyPath = policy.path;
1008
1023
  let liveSha256;
1024
+ /**
1025
+ * The live bytes as text, for the payload file this ceremony will commit
1026
+ * (APRV-356).
1027
+ *
1028
+ * One read for both, on the rule `core/attest.ts` states for the same pair:
1029
+ * two reads of a file an operator may still be editing can disagree, and the
1030
+ * digest reported in the report has to be the digest of the bytes whose store
1031
+ * file the `git add` names.
1032
+ */
1033
+ let liveText;
1009
1034
  try {
1010
- liveSha256 = policyFileHash(policyPath);
1035
+ const liveBytes = readFileSync(policyPath);
1036
+ liveSha256 = policyBytesHash(liveBytes);
1037
+ liveText = liveBytes.toString("utf8");
1011
1038
  }
1012
1039
  catch (cause) {
1013
1040
  return refuse(streams, json, "io", `policy ${policyPath} could not be read: ${detail(cause)}`, EXIT_IO);
1014
1041
  }
1015
1042
  const logPath = resolvePath(stringFlag(parsed.flags, "--log"), DEFAULT_LOG_PATH, cwd);
1043
+ /**
1044
+ * The store file holding the attested policy text, absolute (APRV-356).
1045
+ *
1046
+ * The ceremony commit carries it beside the policy and the log, because a
1047
+ * committed log carrying a binding whose payload file was never committed is
1048
+ * a chain whose in-force bytes are unrecoverable to every reader of the
1049
+ * committed copy, which is the state this task exists to end. The CI
1050
+ * protected-path guard resolves payloads out of committed trees, so that copy
1051
+ * is the one it reads.
1052
+ *
1053
+ * Which file it is depends on which door the attestation comes through, and
1054
+ * both are addressable from the bytes on disk before anything is appended:
1055
+ *
1056
+ * - The HUMAN path appends the attestation here, and since APRV-356 that
1057
+ * record binds `{ text }` (`core/attest.ts`).
1058
+ * - The AGENT path proposes and waits for a tap. The phone's attestation
1059
+ * stores nothing, which APRV-356 left unchanged; what carries the text on
1060
+ * that chain is the `policy.proposed` this ceremony appends, binding
1061
+ * `{ policy_path, text }` (`core/policy-proposal.ts`).
1062
+ *
1063
+ * Addressed from `liveText` rather than read back from the appended record,
1064
+ * so it is a ceremony file from the start: `--dry-run` names it in the `git
1065
+ * add` it would run, and `--commit` counts it as its own rather than as a
1066
+ * stray in the index. An edit landing between here and the append moves the
1067
+ * hash, and the append refuses on the digest before this path is used.
1068
+ */
1069
+ const payloadFile = payloadPath(payloadStoreDirFor(logPath), agentActor === null
1070
+ ? attestedPolicyPayloadHash(liveText)
1071
+ : payloadHash(proposalPayloadValue(policyPath, liveText)));
1016
1072
  // (a-pre) The thirty-three seconds of silence, ended (APRV-167).
1017
1073
  //
1018
1074
  // Everything from here to the `Policy` block below is work the operator could
@@ -1044,6 +1100,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
1044
1100
  : EXIT_IO;
1045
1101
  return refuse(streams, json, read.code, read.message, exitCode);
1046
1102
  }
1103
+ /**
1104
+ * The amendment branch's name, `--branch` or the seq-derived default.
1105
+ *
1106
+ * Declared HERE rather than beside the flow selection below (APRV-420): the
1107
+ * no-op path needs it too, because a re-run's whole job is to find the branch
1108
+ * a previous run left standing.
1109
+ */
1110
+ const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
1047
1111
  const status = checkAttestation(read.records, policyPath);
1048
1112
  const attested = status.status === "attested"
1049
1113
  ? { sha256: status.sha256, seq: status.seq }
@@ -1053,7 +1117,27 @@ export function commandPolicyAmend(argv, streams, cwd) {
1053
1117
  // (a) Nothing to amend. A no-op ceremony is a SUCCESS, not an error: an
1054
1118
  // operator (or a script) that runs `amend` on an already-attested policy has
1055
1119
  // established exactly what they wanted to establish.
1120
+ //
1121
+ // APRV-420, one exception: an amendment branch already standing on origin.
1122
+ // The live policy does match its attestation, and saying only that leaves the
1123
+ // pull request carrying it open, unmergeable and unarmed, which is the state a
1124
+ // records advance landing first puts it in. So the re-run repairs it, or names
1125
+ // exactly what to run — never "nothing to amend" over a dirty amendment.
1056
1126
  if (status.status === "attested") {
1127
+ const noopRoot = repoRoot(dirname(policyPath));
1128
+ const repair = wantCommit && noopRoot !== null && hasOrigin(noopRoot)
1129
+ ? repairAmendBranch({
1130
+ root: noopRoot,
1131
+ policyPath,
1132
+ logPath,
1133
+ payloadFile,
1134
+ seq: status.seq,
1135
+ branch: branchName(String(status.seq)),
1136
+ // `--dry-run` and `--no-publish` mean the same thing here as
1137
+ // everywhere else in this verb: compute it, print it, run nothing.
1138
+ act: !dryRun && !noPublish,
1139
+ })
1140
+ : null;
1057
1141
  if (json) {
1058
1142
  emitReport(streams, {
1059
1143
  policyPath,
@@ -1067,12 +1151,24 @@ export function commandPolicyAmend(argv, streams, cwd) {
1067
1151
  noop: true,
1068
1152
  dryRun,
1069
1153
  aborted: false,
1154
+ repair,
1070
1155
  });
1071
1156
  }
1072
- else {
1157
+ else if (repair === null) {
1073
1158
  streams.out(`nothing to amend: ${relPath(policyPath, cwd)} already matches its attestation at seq ${status.seq} (sha256 ${shortHash(liveSha256)})\n`);
1074
1159
  }
1075
- return EXIT_OK;
1160
+ else {
1161
+ const headline = repair.state === "failed" ? st.glyph("fail") : st.glyph(repair.state === "owed" ? "point" : "ok");
1162
+ streams.out(`${headline} ${repair.branch}: ${repair.message}\n`);
1163
+ for (const step of repair.commands) {
1164
+ streams.err(` ${step.ok === null ? st.glyph("point") : step.ok ? st.glyph("ok") : st.glyph("fail")} ${step.command}\n`);
1165
+ }
1166
+ }
1167
+ // A repair that BROKE keeps the I/O exit, which is the split the rest of
1168
+ // this verb uses: the exit code speaks to scripts, the text to people.
1169
+ // `owed` is not a failure — it is `--dry-run` or `--no-publish` answering
1170
+ // the question they were asked — and `landed` and `repaired` are successes.
1171
+ return repair !== null && repair.state === "failed" ? EXIT_IO : EXIT_OK;
1076
1172
  }
1077
1173
  // (b) The baseline, and only a verifiable one. See the module header.
1078
1174
  progress.phase("recovering the attested baseline and diffing it against the live policy");
@@ -1108,8 +1204,11 @@ export function commandPolicyAmend(argv, streams, cwd) {
1108
1204
  const onProtectedDefault = probe.protection === "protected" &&
1109
1205
  probe.currentBranch !== null &&
1110
1206
  probe.currentBranch === probe.defaultBranch;
1111
- const useBranch = branchFlag !== null || (!forceDirect && onProtectedDefault);
1112
- const branchName = (seq) => branchFlag ?? `policy-amend-${seq}`;
1207
+ // APRV-341: `--pr` is a third way into the branch flow, and it is the
1208
+ // operator's own instruction rather than an inference from a probe. A probe
1209
+ // that cannot reach GitHub answers `unknown`, which used to mean "commit in
1210
+ // place"; an operator who typed `--pr` has said what they want either way.
1211
+ const useBranch = branchFlag !== null || wantPr || (!forceDirect && onProtectedDefault);
1113
1212
  // The direct flow's push is about to hit a protected branch. Say so before
1114
1213
  // the human types it, rather than after GitHub says it.
1115
1214
  const pushWarning = !useBranch && onProtectedDefault
@@ -1133,9 +1232,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
1133
1232
  */
1134
1233
  let pinsChange = null;
1135
1234
  if (wantCommit && !dryRun) {
1136
- const plan = planCommit(policyPath, logPath, useBranch ? { branch: branchFlag } : null);
1235
+ const plan = planCommit(policyPath, logPath, payloadFile, useBranch ? { branch: branchFlag } : null);
1137
1236
  if (!plan.ok) {
1138
- return refuse(streams, json, "commit-preconditions", plan.message, EXIT_USAGE);
1237
+ return refuse(streams, json, plan.code, plan.message, EXIT_USAGE);
1139
1238
  }
1140
1239
  commitPlan = plan.plan;
1141
1240
  const prepared = prepareBase({ root: commitPlan.root, policyArg: commitPlan.policyArg, logPath, progress }, probe, attested?.sha256 ?? null);
@@ -1211,7 +1310,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
1211
1310
  pinsChange = pinsChangeIn(amendRoot, policyPath, "HEAD");
1212
1311
  }
1213
1312
  /**
1214
- * The same command, with the two long absolute paths written the way the
1313
+ * The same command, with the long absolute paths written the way the
1215
1314
  * operator would type them.
1216
1315
  *
1217
1316
  * This is a HUMAN transform and nothing else: `--json`'s `git.commands` keeps
@@ -1224,7 +1323,9 @@ export function commandPolicyAmend(argv, streams, cwd) {
1224
1323
  .split(policyPath)
1225
1324
  .join(relPath(policyPath, cwd))
1226
1325
  .split(logPath)
1227
- .join(relPath(logPath, cwd));
1326
+ .join(relPath(logPath, cwd))
1327
+ .split(payloadFile)
1328
+ .join(relPath(payloadFile, cwd));
1228
1329
  return pinsChange === null
1229
1330
  ? shortened
1230
1331
  : shortened.split(pinsChange.path).join(relPath(pinsChange.path, cwd));
@@ -1242,8 +1343,13 @@ export function commandPolicyAmend(argv, streams, cwd) {
1242
1343
  };
1243
1344
  const summary = summarize(policyPath, diff, pinsChange);
1244
1345
  // APRV-274: the pins file is a member of the `git add` exactly when it moved,
1245
- // so a copied command lands the same three (or two) files the verb would.
1246
- const ceremonyFiles = [policyPath, logPath, ...(pinsChange === null ? [] : [pinsChange.path])];
1346
+ // so a copied command lands the same three (or four) files the verb would.
1347
+ const ceremonyFiles = [
1348
+ policyPath,
1349
+ logPath,
1350
+ ...(pinsChange === null ? [] : [pinsChange.path]),
1351
+ payloadFile,
1352
+ ];
1247
1353
  const commitCommands = (seq) => [
1248
1354
  `git add ${ceremonyFiles.join(" ")}`,
1249
1355
  `git commit -m ${JSON.stringify(`Policy: ${summary} (attested seq ${seq})`)}`,
@@ -1251,15 +1357,31 @@ export function commandPolicyAmend(argv, streams, cwd) {
1251
1357
  const gitCommands = (seq) => {
1252
1358
  // APRV-203: `--commit` runs none of these; it assembles the commit on the
1253
1359
  // remote's tip without a checkout. These are the HAND procedure, and they
1254
- // start where `--commit` starts: at the remote, so the branch is not built
1255
- // on a local trunk that has fallen behind.
1360
+ // start where `--commit` starts: with the checkout brought current, so the
1361
+ // branch is not built on a local trunk that has fallen behind.
1362
+ //
1363
+ // APRV-360: and they never switch branches. The form printed before this
1364
+ // opened with `git checkout -b policy-amend-<seq> origin/main`, which is
1365
+ // the shape that forked the log on 2026-09-16 and that refused outright on
1366
+ // 2026-09-18, when the primary's main was fourteen commits behind and the
1367
+ // switch would have overwritten QUEUE.md, the working log and six
1368
+ // payloads. A branch switch in the primary is never the right answer: the
1369
+ // daemon is appending to the working log while the operator reads this.
1370
+ //
1371
+ // What replaces it is what the 2026-09-18 recovery actually did. `approval
1372
+ // log sync` brings the checkout current (and refuses `log-diverged` rather
1373
+ // than fast-forwarding over a fork), the commit is made where the operator
1374
+ // is standing, and the branch is created on the REMOTE by refspec, so it
1375
+ // exists without any local ref ever being checked out.
1256
1376
  if (useBranch) {
1377
+ const branch = branchName(seq);
1378
+ const base = probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`;
1257
1379
  return [
1258
- "git fetch origin",
1259
- `git checkout -b ${branchName(seq)} origin/${probe.defaultBranch ?? "main"}`,
1380
+ "approval log sync",
1260
1381
  ...commitCommands(seq),
1261
- `git push -u origin ${branchName(seq)}`,
1262
- `gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))}`,
1382
+ `git push origin HEAD:refs/heads/${branch}`,
1383
+ `gh pr create --title ${JSON.stringify(prTitle(summary, seq))} --body ${JSON.stringify(prBody(seq))} --head ${branch}${base}`,
1384
+ `gh pr merge ${branch} --auto --merge`,
1263
1385
  ];
1264
1386
  }
1265
1387
  return amendRoot === null
@@ -1506,6 +1628,7 @@ export function commandPolicyAmend(argv, streams, cwd) {
1506
1628
  branch: null,
1507
1629
  pushed: false,
1508
1630
  prUrl: null,
1631
+ prUpdated: false,
1509
1632
  autoMerge: "not-attempted",
1510
1633
  steps: [],
1511
1634
  stoppedAt: null,
@@ -1596,6 +1719,14 @@ export function commandPolicyAmend(argv, streams, cwd) {
1596
1719
  const why = commandOutputLines(merge.stderr, merge.stdout)[0];
1597
1720
  note(`${prLabel(publishing.prUrl)} is open — merge it with a MERGE COMMIT when CI is green. auto-merge was not armed${why === undefined ? "" : `: ${why}`}`);
1598
1721
  };
1722
+ /**
1723
+ * Who publishes the log for this amendment (APRV-420).
1724
+ *
1725
+ * Settled at commit-assembly time against the base the commit is parented
1726
+ * on, and reported, so the pull request's file list is never a surprise. The
1727
+ * default is the behaviour that predates the task: this commit carries it.
1728
+ */
1729
+ let carriage = { carry: true, reason: null, publisher: null };
1599
1730
  if (commitPlan !== null && commitBase !== null) {
1600
1731
  // APRV-203. The commit is ASSEMBLED, never checked out: a scratch index is
1601
1732
  // filled from the remote's tree, the two ceremony files are laid over it
@@ -1605,6 +1736,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
1605
1736
  const message = `Policy: ${summary} (attested seq ${seq})`;
1606
1737
  /** `origin/main abc123`, or `HEAD abc123` where there is no remote. */
1607
1738
  const baseLabel = `${commitBase.remote === null ? "HEAD" : `${commitBase.remote}/${commitBase.branch}`} ${commitBase.sha.slice(0, 12)}`;
1739
+ // APRV-420: who publishes the log. Decided here, from the base this commit
1740
+ // is about to be parented on, so the answer is about the tree the commit
1741
+ // will actually sit against.
1742
+ carriage = decideLogCarriage(commitPlan.root, commitBase, logPath);
1608
1743
  progress.phase(`building the amendment commit on ${baseLabel} (nothing is checked out)`);
1609
1744
  const built = commitOnBase(commitPlan.root, {
1610
1745
  base: commitBase.sha,
@@ -1613,12 +1748,23 @@ export function commandPolicyAmend(argv, streams, cwd) {
1613
1748
  // keeps this ceremony from reverting a pins edit somebody else landed.
1614
1749
  paths: [
1615
1750
  commitPlan.policyArg,
1616
- commitPlan.logArg,
1751
+ // APRV-420: the log joins the commit only when this amendment is the
1752
+ // thing publishing it. When a records advance is, two commits carrying
1753
+ // the same appended records is a conflict on `events.jsonl` whichever
1754
+ // lands first, and the repair is a hand merge of the log.
1755
+ ...(carriage.carry ? [commitPlan.logArg] : []),
1617
1756
  ...(pinsChange === null ? [] : [pinsChange.arg]),
1757
+ // APRV-356: the attested policy text, so the commit carrying the
1758
+ // binding carries the bytes it binds. Without it the in-force policy is
1759
+ // unrecoverable for every reader of the committed copy, the CI guard
1760
+ // included.
1761
+ commitPlan.payloadArg,
1618
1762
  ],
1619
1763
  message,
1620
1764
  });
1621
1765
  progress.done();
1766
+ if (carriage.reason !== null)
1767
+ note(`${st.glyph("ok")} ${carriage.reason}`);
1622
1768
  if (!built.ok) {
1623
1769
  return gitFailed(built.step, built.quote.join(" | "), `the attestation was appended at seq ${seq}, but ${built.message}; the checkout is untouched, so run the printed commands by hand`, commands);
1624
1770
  }
@@ -1669,6 +1815,47 @@ export function commandPolicyAmend(argv, streams, cwd) {
1669
1815
  : `${commitSha.slice(0, 12)} on ${baseLabel} (held at ${anchor}; your checkout was not moved)`;
1670
1816
  /** `gh pr create …` as the operator would type it, for both flows. */
1671
1817
  const prCreateCommand = (head) => `gh pr create --title ${JSON.stringify(prTitle(summary, String(seq)))} --body ${JSON.stringify(prBody(String(seq)))} --head ${head}${probe.defaultBranch === null ? "" : ` --base ${probe.defaultBranch}`}`;
1818
+ /**
1819
+ * The open pull request for `head`, or `null` when there is none (APRV-341).
1820
+ *
1821
+ * Asked before `gh pr create`, because a second run of the ceremony onto a
1822
+ * branch that already carries one is the ordinary shape of "the ceremony
1823
+ * stopped half-way and I ran it again": `gh pr create` answers that with a
1824
+ * failure, and a failure there used to end the verb holding an attestation
1825
+ * it had already appended. A `gh` that cannot answer is `null`, which falls
1826
+ * through to `create` — the path that was there before this existed.
1827
+ */
1828
+ const openPrFor = (head) => {
1829
+ const listed = gh(["pr", "list", "--head", head, "--state", "open", "--json", "url"], commitPlan.root);
1830
+ if (!listed.ok)
1831
+ return null;
1832
+ const text = listed.stdout.trim();
1833
+ if (text.length === 0)
1834
+ return null;
1835
+ let rows;
1836
+ try {
1837
+ rows = JSON.parse(text);
1838
+ }
1839
+ catch {
1840
+ return null;
1841
+ }
1842
+ if (!Array.isArray(rows))
1843
+ return null;
1844
+ const first = rows[0];
1845
+ if (typeof first !== "object" || first === null)
1846
+ return null;
1847
+ const url = first.url;
1848
+ return typeof url === "string" && url.length > 0 ? url : null;
1849
+ };
1850
+ const prEditArgs = (head) => [
1851
+ "pr",
1852
+ "edit",
1853
+ head,
1854
+ "--title",
1855
+ prTitle(summary, String(seq)),
1856
+ "--body",
1857
+ prBody(String(seq)),
1858
+ ];
1672
1859
  const prCreateArgs = (head) => [
1673
1860
  "pr",
1674
1861
  "create",
@@ -1714,11 +1901,22 @@ export function commandPolicyAmend(argv, streams, cwd) {
1714
1901
  publishing.pushed = true;
1715
1902
  output = `${output}\n${`${push.stdout}${push.stderr}`.trim()}`.trim();
1716
1903
  if (ghAvailable(commitPlan.root)) {
1717
- const pr = gh(prCreateArgs(branch), commitPlan.root);
1718
- publishing.steps.push({ command: prCreateCommand(branch), ok: pr.ok });
1904
+ // APRV-341: open, or update the one that is already open for this
1905
+ // branch. A ceremony re-run onto a branch a pull request already stands
1906
+ // on is not an error to report, it is the state to bring up to date.
1907
+ const standing = openPrFor(branch);
1908
+ const pr = standing === null
1909
+ ? gh(prCreateArgs(branch), commitPlan.root)
1910
+ : gh(prEditArgs(branch), commitPlan.root);
1911
+ publishing.steps.push({
1912
+ command: standing === null ? prCreateCommand(branch) : `gh pr edit ${branch} --title … --body …`,
1913
+ ok: pr.ok,
1914
+ });
1719
1915
  if (!pr.ok) {
1720
1916
  const ghFailure = pr.stderr.trim() || pr.stdout.trim() || "gh did not run";
1721
- return stalled("pr-failed", "`gh pr create` failed; the branch is already on origin", pr, `the attestation was appended at seq ${seq}, committed on ${branch} and pushed, but \`gh pr create\` failed: ${ghFailure}; open the pull request by hand and merge it with a merge commit`, [
1917
+ return stalled("pr-failed", standing === null
1918
+ ? "`gh pr create` failed; the branch is already on origin"
1919
+ : "`gh pr edit` failed; the branch and its pull request are already on origin", pr, `the attestation was appended at seq ${seq}, committed on ${branch} and pushed, but \`gh pr create\` failed: ${ghFailure}; open the pull request by hand and merge it with a merge commit`, [
1722
1920
  `attestation appended at seq ${seq}: it is in the log, on disk`,
1723
1921
  `committed on ${branch} and PUSHED: origin has the branch`,
1724
1922
  "no pull request: origin's default branch still carries the previous policy",
@@ -1727,8 +1925,12 @@ export function commandPolicyAmend(argv, streams, cwd) {
1727
1925
  { command: `gh pr merge ${branch} --merge`, note: "or merge it in the web UI" },
1728
1926
  ], 0, [MERGE_COMMIT_LINE]);
1729
1927
  }
1730
- prUrl = lastUrl(pr.stdout);
1928
+ // `gh pr edit` prints the pull request's URL too, but the one this
1929
+ // ceremony already knows is the one it asked for; `create` has nothing
1930
+ // but its own output. Either way `null` is an honest answer.
1931
+ prUrl = standing ?? lastUrl(pr.stdout);
1731
1932
  publishing.prUrl = prUrl;
1933
+ publishing.prUpdated = standing !== null;
1732
1934
  publishing.complete = true;
1733
1935
  // APRV-130: the ceremony offers to finish the last step too.
1734
1936
  armAutoMerge(commitPlan.root, branch);
@@ -1865,7 +2067,10 @@ export function commandPolicyAmend(argv, streams, cwd) {
1865
2067
  // APRV-274: the headline names what the commit actually carries. A reader
1866
2068
  // who is told "the policy and the log" and finds a third file in the diff
1867
2069
  // has been told something false about the one commit that must not lie.
1868
- const carried = pinsChange === null ? "the policy and the log" : "the policy, the log and the pins";
2070
+ // APRV-356 added the fourth: the store file holding the attested bytes.
2071
+ const carried = pinsChange === null
2072
+ ? "the policy, the log and the attested policy text"
2073
+ : "the policy, the log, the pins and the attested policy text";
1869
2074
  const done = [
1870
2075
  branch === null
1871
2076
  ? `${st.glyph("ok")} committed ${carried} together:`
@@ -1958,6 +2163,8 @@ export function commandPolicyAmend(argv, streams, cwd) {
1958
2163
  * across two commits and a hand-run cherry-pick.
1959
2164
  */
1960
2165
  function planCommit(policyPath, logPath,
2166
+ /** The store file holding the attested policy text, absolute (APRV-356). */
2167
+ payloadFile,
1961
2168
  /**
1962
2169
  * The branch flow's preconditions, checked here for the same reason: an
1963
2170
  * `origin` that does not exist, or a branch name already taken, would fail
@@ -1971,11 +2178,16 @@ branchFlow) {
1971
2178
  if (root === null) {
1972
2179
  return {
1973
2180
  ok: false,
2181
+ code: "commit-preconditions",
1974
2182
  message: `--commit needs a git repository and ${policyPath} is not inside one; nothing was attested`,
1975
2183
  };
1976
2184
  }
1977
2185
  const policyArg = repoPath(root, policyPath);
1978
2186
  const logArg = repoPath(root, logPath);
2187
+ // APRV-356. The payload store is a sibling of the log directory, so a log
2188
+ // inside the repository puts this inside it too; the check below covers both
2189
+ // with one sentence rather than naming a path no operator chose.
2190
+ const payloadArg = repoPath(root, payloadFile);
1979
2191
  // APRV-274. `null` where these pins do not govern this policy, and where the
1980
2192
  // repository simply has no pins module: in both cases the ceremony's file set
1981
2193
  // is the two it always was.
@@ -1985,32 +2197,59 @@ branchFlow) {
1985
2197
  if (policyArg.startsWith("../") || logArg.startsWith("../")) {
1986
2198
  return {
1987
2199
  ok: false,
2200
+ code: "commit-preconditions",
1988
2201
  message: `--commit needs the policy (${policyPath}) and the log (${logPath}) inside the same repository (${root}); nothing was attested`,
1989
2202
  };
1990
2203
  }
1991
2204
  const status = git(["status", "--porcelain"], root);
1992
2205
  if (!status.ok) {
1993
- return { ok: false, message: `--commit could not read git status: ${status.stderr.trim()}` };
2206
+ return {
2207
+ ok: false,
2208
+ code: "commit-preconditions",
2209
+ message: `--commit could not read git status: ${status.stderr.trim()}`,
2210
+ };
1994
2211
  }
2212
+ const ceremonyFiles = [policyArg, logArg, payloadArg, ...(pinsArg === null ? [] : [pinsArg])];
2213
+ const carried = pinsArg === null
2214
+ ? "the policy, the log and the attested policy text"
2215
+ : `the policy, the log, the attested policy text and ${pinsArg}`;
1995
2216
  const strays = [];
2217
+ /** Ceremony files staged in one state and left in another (APRV-341). */
2218
+ const split = [];
1996
2219
  for (const line of status.stdout.split("\n")) {
1997
2220
  if (line.trim().length === 0)
1998
2221
  continue;
1999
2222
  const index = line[0] ?? " ";
2000
- // Only the INDEX column matters: an unstaged or untracked file elsewhere is
2001
- // not going into this commit, and refusing over it would make the verb
2002
- // unusable in any working repository.
2223
+ const worktree = line[1] ?? " ";
2224
+ const path = line.slice(3).trim();
2225
+ const ceremony = ceremonyFiles.includes(path);
2226
+ // A ceremony file the operator staged and then edited again. The commit is
2227
+ // assembled from the working tree, so the bytes it would carry are not the
2228
+ // bytes `git diff --cached` shows, and an amendment is the one commit that
2229
+ // may not be a surprise to the person who signed it.
2230
+ if (ceremony && index !== " " && index !== "?" && worktree === "M")
2231
+ split.push(path);
2232
+ // Only the INDEX column matters for everything else: an unstaged or
2233
+ // untracked file elsewhere is not going into this commit (the scratch index
2234
+ // lays exactly the ceremony paths over the remote's tree), and refusing
2235
+ // over one would make the verb unusable in any working repository.
2003
2236
  if (index === " " || index === "?")
2004
2237
  continue;
2005
- const path = line.slice(3).trim();
2006
- if (path === policyArg || path === logArg || path === pinsArg)
2238
+ if (ceremony)
2007
2239
  continue;
2008
2240
  strays.push(path);
2009
2241
  }
2242
+ if (split.length > 0) {
2243
+ return {
2244
+ ok: false,
2245
+ code: "dirty-tree",
2246
+ message: `--commit refuses: ${split.join(", ")} ${split.length === 1 ? "is" : "are"} staged in one state and modified again in the working tree. The amendment commit is assembled from the WORKING TREE, so it would carry bytes your \`git diff --cached\` does not show, and the one commit that may not surprise the person who signed it is this one. Stage the file as it stands (\`git add ${split.join(" ")}\`) or unstage it, then run this again. Nothing was attested`,
2247
+ };
2248
+ }
2010
2249
  if (strays.length > 0) {
2011
- const carried = pinsArg === null ? "the policy and the log" : `the policy, the log and ${pinsArg}`;
2012
2250
  return {
2013
2251
  ok: false,
2252
+ code: "staged-unrelated",
2014
2253
  message: `--commit refuses: the index carries ${strays.length} staged change(s) beyond ${carried} (${strays.join(", ")}). The amendment commit carries EXACTLY those files, so that "this commit is the amendment" stays true. Unstage them, or drop --commit and run the printed commands yourself. Nothing was attested`,
2015
2254
  };
2016
2255
  }
@@ -2019,6 +2258,7 @@ branchFlow) {
2019
2258
  if (!remote.ok) {
2020
2259
  return {
2021
2260
  ok: false,
2261
+ code: "commit-preconditions",
2022
2262
  message: `--commit on a branch needs an "origin" remote to push to, and ${root} has none (${remote.stderr.trim()}); pass --direct to commit in place, or add the remote. Nothing was attested`,
2023
2263
  };
2024
2264
  }
@@ -2027,12 +2267,13 @@ branchFlow) {
2027
2267
  if (exists.ok) {
2028
2268
  return {
2029
2269
  ok: false,
2270
+ code: "commit-preconditions",
2030
2271
  message: `--branch ${branchFlow.branch} already exists in ${root}; the amendment branch is created fresh so it carries exactly one commit. Pick another name. Nothing was attested`,
2031
2272
  };
2032
2273
  }
2033
2274
  }
2034
2275
  }
2035
- return { ok: true, plan: { root, policyArg, logArg, pinsArg } };
2276
+ return { ok: true, plan: { root, policyArg, logArg, pinsArg, payloadArg } };
2036
2277
  }
2037
2278
  /**
2038
2279
  * Fetch the remote and establish that this ceremony may be based on it (APRV-203).
@@ -2137,6 +2378,292 @@ function readLogText(path) {
2137
2378
  return "";
2138
2379
  }
2139
2380
  }
2381
+ /**
2382
+ * The open records-advance branch on `origin`, or `null`.
2383
+ *
2384
+ * `git ls-remote` rather than `gh`, deliberately: this is a question about refs
2385
+ * and it must answer the same way on a box with no GitHub CLI, no token and no
2386
+ * network beyond the remote itself. `approval log advance` names its branch
2387
+ * `records-log-<YYYY-MM-DD>` and falls back to `records-log-<date>-<n>`
2388
+ * (`cli/log-advance.ts`), and merged branches are deleted by the reconcile
2389
+ * script, so a branch matching that pattern on origin is a live advance.
2390
+ */
2391
+ function recordsPublisher(root) {
2392
+ const listed = git(["ls-remote", "--heads", "origin", "records-log-*"], root);
2393
+ if (!listed.ok)
2394
+ return null;
2395
+ for (const line of listed.stdout.split("\n")) {
2396
+ const [sha, ref] = line.trim().split(/\s+/u);
2397
+ if (ref === undefined || sha === undefined)
2398
+ continue;
2399
+ const name = ref.replace(/^refs\/heads\//u, "");
2400
+ if (name.startsWith("records-log-"))
2401
+ return { name, sha };
2402
+ }
2403
+ return null;
2404
+ }
2405
+ /**
2406
+ * Whether the records branch at `sha` already carries every record of the
2407
+ * working log, this attestation included.
2408
+ *
2409
+ * A branch pushed BEFORE the attestation was appended does not, and that is
2410
+ * the ordinary order (the cadence advance ran, then the human signed), so
2411
+ * saying "this attestation included" of it would be a claim the committed
2412
+ * log contradicts. What publishes the attestation then is the NEXT advance,
2413
+ * and the pull request's protected-path guard holds the policy change until
2414
+ * a records branch or main carries the record. `null` when the branch's log
2415
+ * could not be fetched or read, which the caller reports as unknown rather
2416
+ * than as either answer.
2417
+ */
2418
+ function publisherCarriesLog(root, sha, logArg, logPath) {
2419
+ const fetched = git(["fetch", "--quiet", "origin", sha], root);
2420
+ if (!fetched.ok)
2421
+ return null;
2422
+ const branchLog = showBlob(root, sha, logArg);
2423
+ if (branchLog === null)
2424
+ return null;
2425
+ const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `records branch ${sha.slice(0, 12)}:${logArg}`, text: branchLog.toString("utf8") });
2426
+ if (!compared.ok)
2427
+ return null;
2428
+ return compared.drift.relation === "equal" || compared.drift.relation === "behind";
2429
+ }
2430
+ /**
2431
+ * Decide who publishes the log for this amendment. See the block above.
2432
+ *
2433
+ * Fails toward CARRYING it: every answer this function cannot establish (a log
2434
+ * that does not verify, a blob it could not read, a remote it could not list)
2435
+ * leaves the amendment holding the log, which is the behaviour that predates
2436
+ * this task. A dirty pull request is a nuisance; an attestation that reaches
2437
+ * `main` in nobody's commit is a policy in force that the committed log does
2438
+ * not record, and between those two there is no contest.
2439
+ */
2440
+ function decideLogCarriage(root, base, logPath) {
2441
+ const logArg = repoPath(root, logPath);
2442
+ const baseLabel = base.remote === null ? base.branch : `${base.remote}/${base.branch}`;
2443
+ const baseLog = showBlob(root, base.sha, logArg);
2444
+ const compared = compareChains({ label: "the working log", text: readLogText(logPath) }, { label: `${baseLabel}:${logArg}`, text: baseLog === null ? "" : baseLog.toString("utf8") });
2445
+ if (compared.ok && (compared.drift.relation === "equal" || compared.drift.relation === "behind")) {
2446
+ return {
2447
+ carry: false,
2448
+ reason: `${baseLabel} already carries every record this attestation added, so this amendment does not touch the log`,
2449
+ publisher: null,
2450
+ };
2451
+ }
2452
+ const publisher = recordsPublisher(root);
2453
+ if (publisher !== null) {
2454
+ const carries = publisherCarriesLog(root, publisher.sha, logArg, logPath);
2455
+ return {
2456
+ carry: false,
2457
+ reason: carries === true
2458
+ ? `the records advance on ${publisher.name} publishes the whole log, this attestation included, so this amendment carries only the policy and cannot conflict with it`
2459
+ : `a records advance is live on ${publisher.name}${carries === null ? " (its log could not be read from here)" : ", pushed before this attestation was appended"}, so this amendment carries only the policy and cannot conflict with it; the attestation record lands with the next advance, and the pull request's protected-path check holds the policy change until a records branch or main carries it`,
2460
+ publisher: publisher.name,
2461
+ };
2462
+ }
2463
+ return { carry: true, reason: null, publisher: null };
2464
+ }
2465
+ /**
2466
+ * Repair the amendment pull request a previous run left standing (APRV-420).
2467
+ *
2468
+ * The second run of a ceremony that already attested reaches "nothing to
2469
+ * amend", which is true and useless: the live policy does match its
2470
+ * attestation, and the pull request carrying that attestation to the trunk is
2471
+ * open, unmergeable and unarmed. On 2026-09-21 the repair was a hand merge of
2472
+ * `events.jsonl` in a throwaway worktree, which is the work every idiom here
2473
+ * exists to remove.
2474
+ *
2475
+ * So the re-run rebuilds. The trunk's log is a superset of the amendment's by
2476
+ * construction — every record the amendment appended reaches the trunk through
2477
+ * a records advance as well — so a commit built fresh on the current trunk,
2478
+ * carrying only what the trunk still LACKS, is the merge without a merge.
2479
+ *
2480
+ * Two things this does not do. It does not check anything out: the rebuild is
2481
+ * `commitOnBase`'s scratch index, exactly as the first run's was, so a live
2482
+ * appender never has the log moved underneath it. And it does not touch a
2483
+ * branch it cannot account for: the remote tip must be a single commit on top
2484
+ * of an ancestor of the base, which is the shape this ceremony creates and
2485
+ * nothing else. Anything else is reported with its commands rather than
2486
+ * force-pushed over.
2487
+ *
2488
+ * Returns `null` when there is nothing standing, which leaves the caller's
2489
+ * "nothing to amend" exactly as it was.
2490
+ */
2491
+ function repairAmendBranch(input) {
2492
+ const { root, policyPath, logPath, payloadFile, seq, branch, act } = input;
2493
+ const listed = git(["ls-remote", "--heads", "origin", branch], root);
2494
+ const tipLine = listed.ok
2495
+ ? listed.stdout.split("\n").find((line) => line.trim().length > 0)
2496
+ : undefined;
2497
+ if (tipLine === undefined)
2498
+ return null;
2499
+ const commands = [];
2500
+ const stop = (state, message, extra = []) => {
2501
+ for (const command of extra)
2502
+ commands.push({ command, ok: null });
2503
+ return {
2504
+ branch,
2505
+ state,
2506
+ base: null,
2507
+ carried: [],
2508
+ commit: null,
2509
+ pushed: false,
2510
+ autoMerge: "not-attempted",
2511
+ commands,
2512
+ message,
2513
+ };
2514
+ };
2515
+ const probe = probeProtection(root);
2516
+ const baseBranch = probe.defaultBranch ?? probe.currentBranch;
2517
+ if (baseBranch === null) {
2518
+ return stop("failed", `${branch} is on origin and no branch could be resolved to rebuild it on (${probe.reason})`);
2519
+ }
2520
+ commands.push({ command: `git fetch origin ${baseBranch}`, ok: null });
2521
+ const fetched = fetchBase(root, "origin", baseBranch);
2522
+ if (!fetched.ok) {
2523
+ commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: false };
2524
+ return stop("failed", `${branch} is on origin and ${fetched.message}`);
2525
+ }
2526
+ commands[commands.length - 1] = { command: `git fetch origin ${baseBranch}`, ok: true };
2527
+ const base = { remote: "origin", branch: baseBranch, sha: fetched.sha };
2528
+ const baseLabel = `origin/${baseBranch}`;
2529
+ // Fetched rather than read off `ls-remote`, because the count below needs the
2530
+ // tip to be an OBJECT in this repository: a commit somebody else pushed to
2531
+ // the branch has never been near this checkout, and `rev-list` over a sha it
2532
+ // does not have answers nothing at all.
2533
+ commands.push({ command: `git fetch origin ${branch}`, ok: null });
2534
+ const fetchedTip = fetchBase(root, "origin", branch);
2535
+ if (!fetchedTip.ok) {
2536
+ commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: false };
2537
+ return stop("failed", `${branch} is on origin and ${fetchedTip.message}`);
2538
+ }
2539
+ commands[commands.length - 1] = { command: `git fetch origin ${branch}`, ok: true };
2540
+ const tipSha = fetchedTip.sha;
2541
+ // What the base still lacks. Each answer is read from the base's own tree,
2542
+ // so a record or a payload a records advance already landed is not carried
2543
+ // twice, which is the whole cause of the conflict this repairs.
2544
+ const policyArg = repoPath(root, policyPath);
2545
+ const payloadArg = repoPath(root, payloadFile);
2546
+ const carried = [];
2547
+ const reasons = [];
2548
+ const basePolicy = showBlob(root, base.sha, policyArg);
2549
+ const livePolicy = ((() => {
2550
+ try {
2551
+ return readFileSync(policyPath);
2552
+ }
2553
+ catch {
2554
+ return null;
2555
+ }
2556
+ })());
2557
+ if (livePolicy === null) {
2558
+ return stop("failed", `${policyPath} could not be read, so the rebuild has no policy bytes to carry`);
2559
+ }
2560
+ if (basePolicy === null || !basePolicy.equals(livePolicy)) {
2561
+ carried.push(policyArg);
2562
+ reasons.push(`the policy bytes (${baseLabel} does not carry them)`);
2563
+ }
2564
+ if (showBlob(root, base.sha, payloadArg) === null) {
2565
+ carried.push(payloadArg);
2566
+ reasons.push("the attested policy text");
2567
+ }
2568
+ const carriage = decideLogCarriage(root, base, logPath);
2569
+ if (carriage.carry) {
2570
+ carried.push(repoPath(root, logPath));
2571
+ reasons.push("the log, which nothing else is publishing");
2572
+ }
2573
+ else {
2574
+ reasons.push(`NOT the log: ${carriage.reason ?? "it is published elsewhere"}`);
2575
+ }
2576
+ const pins = pinsChangeIn(root, policyPath, base.sha);
2577
+ if (pins !== null) {
2578
+ carried.push(pins.arg);
2579
+ reasons.push(pins.arg);
2580
+ }
2581
+ if (carried.length === 0) {
2582
+ return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel} in full: the policy, its attested text and its records are all there, so ${branch} has nothing left to carry. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
2583
+ }
2584
+ // Only now that a rebuild is actually owed does the branch's SHAPE matter,
2585
+ // and it has to be the one shape this ceremony makes: one commit, on a parent
2586
+ // the base already contains. `rev-list --count base..tip` counts the commits
2587
+ // the tip has that the base does not, so anything above one is work this verb
2588
+ // did not put there and a force-push would lose it. Zero is not checked here,
2589
+ // because a branch the base already contains has nothing left to carry and
2590
+ // left through the `landed` exit above.
2591
+ const countRun = git(["rev-list", "--count", `${base.sha}..${tipSha}`], root);
2592
+ const count = countRun.ok ? Number(countRun.stdout.trim()) : Number.NaN;
2593
+ if (!Number.isInteger(count) || count > 1) {
2594
+ return stop("failed", `${branch} on origin carries ${Number.isInteger(count) ? String(count) : "an unknown number of"} commits that ${baseLabel} does not. The amendment ceremony builds exactly one, so this branch holds work this verb did not put there and it will not be rebuilt over. Rebase or merge it by hand`, [`git log --oneline ${base.sha.slice(0, 12)}..${tipSha.slice(0, 12)}`]);
2595
+ }
2596
+ const subject = git(["log", "-1", "--format=%s", tipSha], root);
2597
+ const message = subject.ok && subject.stdout.trim().length > 0
2598
+ ? subject.stdout.trim()
2599
+ : `Policy: amend ${policyArg} (attested seq ${seq})`;
2600
+ const rebuild = `git push origin +<rebuilt>:refs/heads/${branch}`;
2601
+ const built = commitOnBase(root, { base: base.sha, paths: carried, message });
2602
+ if (!built.ok) {
2603
+ commands.push({ command: rebuild, ok: null });
2604
+ return stop("failed", `${branch} could not be rebuilt on ${baseLabel}: ${built.message}`);
2605
+ }
2606
+ if (built.unchanged) {
2607
+ return stop("landed", `the amendment at seq ${seq} is already on ${baseLabel}: a commit carrying ${carried.join(", ")} over ${baseLabel} is the same tree. Close the pull request`, [`gh pr close ${branch} --delete-branch`]);
2608
+ }
2609
+ const commitSha = built.sha;
2610
+ const carriedNote = `rebuilt ${commitSha.slice(0, 12)} on ${baseLabel} ${base.sha.slice(0, 12)} carrying ${reasons.join(", ")}`;
2611
+ const push = `git push origin +${commitSha.slice(0, 12)}:refs/heads/${branch}`;
2612
+ if (!act) {
2613
+ commands.push({ command: push, ok: null });
2614
+ commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
2615
+ return {
2616
+ branch,
2617
+ state: "owed",
2618
+ base: base.sha,
2619
+ carried,
2620
+ commit: commitSha,
2621
+ pushed: false,
2622
+ autoMerge: "not-attempted",
2623
+ commands,
2624
+ message: `${carriedNote}; nothing was pushed, so the pull request is unchanged. Run the commands above to finish it`,
2625
+ };
2626
+ }
2627
+ // Force, and only here: the branch carries exactly the one commit this
2628
+ // ceremony put on it (checked above), and the whole repair is replacing that
2629
+ // commit with the same amendment parented on a base that has moved.
2630
+ const pushed = git(["push", "origin", `+${commitSha}:refs/heads/${branch}`], root);
2631
+ commands.push({ command: push, ok: pushed.ok });
2632
+ if (!pushed.ok) {
2633
+ commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
2634
+ return {
2635
+ branch,
2636
+ state: "failed",
2637
+ base: base.sha,
2638
+ carried,
2639
+ commit: commitSha,
2640
+ pushed: false,
2641
+ autoMerge: "not-attempted",
2642
+ commands,
2643
+ message: `${carriedNote}, and \`${push}\` was REJECTED: ${pushFailureText(pushed)}. The commit exists locally; push it and re-arm the merge`,
2644
+ };
2645
+ }
2646
+ let autoMerge = "not-attempted";
2647
+ if (ghAvailable(root)) {
2648
+ const armed = gh(["pr", "merge", branch, "--merge", "--auto"], root);
2649
+ commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: armed.ok });
2650
+ autoMerge = armed.ok ? "armed" : "refused";
2651
+ }
2652
+ else {
2653
+ commands.push({ command: `gh pr merge ${branch} --merge --auto`, ok: null });
2654
+ }
2655
+ return {
2656
+ branch,
2657
+ state: "repaired",
2658
+ base: base.sha,
2659
+ carried,
2660
+ commit: commitSha,
2661
+ pushed: true,
2662
+ autoMerge,
2663
+ commands,
2664
+ message: `${carriedNote}, and force-updated ${branch} on origin, so the pull request is mergeable again${autoMerge === "armed" ? " and armed" : ""}`,
2665
+ };
2666
+ }
2140
2667
  function loadSummary(load) {
2141
2668
  return load.ok
2142
2669
  ? { ok: true, code: null, message: null }
@@ -2166,6 +2693,9 @@ function emitReport(streams, report) {
2166
2693
  // APRV-109, additive and always present: `null` says the attestation was
2167
2694
  // performed at this terminal, an object says it was collected as a tap.
2168
2695
  proposal: report.proposal ?? null,
2696
+ // APRV-420, additive and always present: `null` says this run had an
2697
+ // amendment to make, or found no standing branch to repair.
2698
+ repair: report.repair ?? null,
2169
2699
  })}\n`);
2170
2700
  }
2171
2701
  //# sourceMappingURL=amend.js.map