approval-md 0.0.1 → 0.1.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 (522) hide show
  1. package/LICENSE +176 -0
  2. package/NOTICE +5 -0
  3. package/README.md +909 -4
  4. package/SPEC.md +445 -32
  5. package/cli.js +29 -3
  6. package/dist/src/adapters/agentmail.js +1200 -0
  7. package/dist/src/adapters/agentmail.js.map +1 -0
  8. package/dist/src/adapters/conformance.js +461 -0
  9. package/dist/src/adapters/conformance.js.map +1 -0
  10. package/dist/src/adapters/contract.js +941 -0
  11. package/dist/src/adapters/contract.js.map +1 -0
  12. package/dist/src/adapters/email.js +749 -0
  13. package/dist/src/adapters/email.js.map +1 -0
  14. package/dist/src/adapters/env-passphrase.js +132 -0
  15. package/dist/src/adapters/env-passphrase.js.map +1 -0
  16. package/dist/src/adapters/registry.js +76 -0
  17. package/dist/src/adapters/registry.js.map +1 -0
  18. package/dist/src/adapters/smtp.js +499 -0
  19. package/dist/src/adapters/smtp.js.map +1 -0
  20. package/dist/src/adapters/vault-provider.js +161 -0
  21. package/dist/src/adapters/vault-provider.js.map +1 -0
  22. package/dist/src/channels/batch.js +121 -0
  23. package/dist/src/channels/batch.js.map +1 -0
  24. package/dist/src/channels/cli.js +468 -0
  25. package/dist/src/channels/cli.js.map +1 -0
  26. package/dist/src/channels/conformance.js +445 -0
  27. package/dist/src/channels/conformance.js.map +1 -0
  28. package/dist/src/channels/contract.js +494 -0
  29. package/dist/src/channels/contract.js.map +1 -0
  30. package/dist/src/channels/payload-view.js +43 -0
  31. package/dist/src/channels/payload-view.js.map +1 -0
  32. package/dist/src/channels/render-queue.js +564 -0
  33. package/dist/src/channels/render-queue.js.map +1 -0
  34. package/dist/src/channels/tagging.js +723 -0
  35. package/dist/src/channels/tagging.js.map +1 -0
  36. package/dist/src/channels/telegram.js +3190 -0
  37. package/dist/src/channels/telegram.js.map +1 -0
  38. package/dist/src/channels/web.js +903 -0
  39. package/dist/src/channels/web.js.map +1 -0
  40. package/dist/src/cli/adapter.js +278 -0
  41. package/dist/src/cli/adapter.js.map +1 -0
  42. package/dist/src/cli/amend.js +2171 -0
  43. package/dist/src/cli/amend.js.map +1 -0
  44. package/dist/src/cli/args.js +86 -0
  45. package/dist/src/cli/args.js.map +1 -0
  46. package/dist/src/cli/attest.js +307 -0
  47. package/dist/src/cli/attest.js.map +1 -0
  48. package/dist/src/cli/audit-card.js +201 -0
  49. package/dist/src/cli/audit-card.js.map +1 -0
  50. package/dist/src/cli/audit.js +460 -0
  51. package/dist/src/cli/audit.js.map +1 -0
  52. package/dist/src/cli/channel-telegram.js +2063 -0
  53. package/dist/src/cli/channel-telegram.js.map +1 -0
  54. package/dist/src/cli/channel-web.js +357 -0
  55. package/dist/src/cli/channel-web.js.map +1 -0
  56. package/dist/src/cli/channel.js +438 -0
  57. package/dist/src/cli/channel.js.map +1 -0
  58. package/dist/src/cli/checkpoint-tap.js +238 -0
  59. package/dist/src/cli/checkpoint-tap.js.map +1 -0
  60. package/dist/src/cli/coverage.js +343 -0
  61. package/dist/src/cli/coverage.js.map +1 -0
  62. package/dist/src/cli/daemon.js +631 -0
  63. package/dist/src/cli/daemon.js.map +1 -0
  64. package/dist/src/cli/doctor.js +2648 -0
  65. package/dist/src/cli/doctor.js.map +1 -0
  66. package/dist/src/cli/env.js +302 -0
  67. package/dist/src/cli/env.js.map +1 -0
  68. package/dist/src/cli/execute.js +1682 -0
  69. package/dist/src/cli/execute.js.map +1 -0
  70. package/dist/src/cli/exit-codes.js +82 -0
  71. package/dist/src/cli/exit-codes.js.map +1 -0
  72. package/dist/src/cli/feedback.js +205 -0
  73. package/dist/src/cli/feedback.js.map +1 -0
  74. package/dist/src/cli/gate-window.js +294 -0
  75. package/dist/src/cli/gate-window.js.map +1 -0
  76. package/dist/src/cli/gate.js +557 -0
  77. package/dist/src/cli/gate.js.map +1 -0
  78. package/dist/src/cli/git-scope.js +295 -0
  79. package/dist/src/cli/git-scope.js.map +1 -0
  80. package/dist/src/cli/gloss-attach.js +107 -0
  81. package/dist/src/cli/gloss-attach.js.map +1 -0
  82. package/dist/src/cli/gloss-codex-child.js +149 -0
  83. package/dist/src/cli/gloss-codex-child.js.map +1 -0
  84. package/dist/src/cli/gloss-codex.js +255 -0
  85. package/dist/src/cli/gloss-codex.js.map +1 -0
  86. package/dist/src/cli/gloss-options.js +79 -0
  87. package/dist/src/cli/gloss-options.js.map +1 -0
  88. package/dist/src/cli/gloss.js +362 -0
  89. package/dist/src/cli/gloss.js.map +1 -0
  90. package/dist/src/cli/help.js +2217 -0
  91. package/dist/src/cli/help.js.map +1 -0
  92. package/dist/src/cli/hook.js +2743 -0
  93. package/dist/src/cli/hook.js.map +1 -0
  94. package/dist/src/cli/import.js +175 -0
  95. package/dist/src/cli/import.js.map +1 -0
  96. package/dist/src/cli/init.js +336 -0
  97. package/dist/src/cli/init.js.map +1 -0
  98. package/dist/src/cli/instructions.js +262 -0
  99. package/dist/src/cli/instructions.js.map +1 -0
  100. package/dist/src/cli/journal.js +238 -0
  101. package/dist/src/cli/journal.js.map +1 -0
  102. package/dist/src/cli/log-advance.js +749 -0
  103. package/dist/src/cli/log-advance.js.map +1 -0
  104. package/dist/src/cli/log-anchor.js +387 -0
  105. package/dist/src/cli/log-anchor.js.map +1 -0
  106. package/dist/src/cli/log-checkpoint.js +128 -0
  107. package/dist/src/cli/log-checkpoint.js.map +1 -0
  108. package/dist/src/cli/log-sync.js +849 -0
  109. package/dist/src/cli/log-sync.js.map +1 -0
  110. package/dist/src/cli/log-verbs.js +354 -0
  111. package/dist/src/cli/log-verbs.js.map +1 -0
  112. package/dist/src/cli/long-help.js +148 -0
  113. package/dist/src/cli/long-help.js.map +1 -0
  114. package/dist/src/cli/main.js +1056 -0
  115. package/dist/src/cli/main.js.map +1 -0
  116. package/dist/src/cli/mcp.js +306 -0
  117. package/dist/src/cli/mcp.js.map +1 -0
  118. package/dist/src/cli/paths.js +79 -0
  119. package/dist/src/cli/paths.js.map +1 -0
  120. package/dist/src/cli/payload.js +253 -0
  121. package/dist/src/cli/payload.js.map +1 -0
  122. package/dist/src/cli/policy.js +229 -0
  123. package/dist/src/cli/policy.js.map +1 -0
  124. package/dist/src/cli/preflight.js +888 -0
  125. package/dist/src/cli/preflight.js.map +1 -0
  126. package/dist/src/cli/progress.js +112 -0
  127. package/dist/src/cli/progress.js.map +1 -0
  128. package/dist/src/cli/prompt.js +312 -0
  129. package/dist/src/cli/prompt.js.map +1 -0
  130. package/dist/src/cli/records.js +66 -0
  131. package/dist/src/cli/records.js.map +1 -0
  132. package/dist/src/cli/render.js +132 -0
  133. package/dist/src/cli/render.js.map +1 -0
  134. package/dist/src/cli/sandbox.js +150 -0
  135. package/dist/src/cli/sandbox.js.map +1 -0
  136. package/dist/src/cli/scaffold.js +137 -0
  137. package/dist/src/cli/scaffold.js.map +1 -0
  138. package/dist/src/cli/setup-adapter.js +475 -0
  139. package/dist/src/cli/setup-adapter.js.map +1 -0
  140. package/dist/src/cli/setup-channel.js +635 -0
  141. package/dist/src/cli/setup-channel.js.map +1 -0
  142. package/dist/src/cli/setup-checkpoint.js +196 -0
  143. package/dist/src/cli/setup-checkpoint.js.map +1 -0
  144. package/dist/src/cli/setup-common.js +376 -0
  145. package/dist/src/cli/setup-common.js.map +1 -0
  146. package/dist/src/cli/setup-flow.js +476 -0
  147. package/dist/src/cli/setup-flow.js.map +1 -0
  148. package/dist/src/cli/setup-service.js +308 -0
  149. package/dist/src/cli/setup-service.js.map +1 -0
  150. package/dist/src/cli/setup.js +473 -0
  151. package/dist/src/cli/setup.js.map +1 -0
  152. package/dist/src/cli/style.js +469 -0
  153. package/dist/src/cli/style.js.map +1 -0
  154. package/dist/src/cli/token.js +274 -0
  155. package/dist/src/cli/token.js.map +1 -0
  156. package/dist/src/cli/up.js +847 -0
  157. package/dist/src/cli/up.js.map +1 -0
  158. package/dist/src/cli/usage.js +91 -0
  159. package/dist/src/cli/usage.js.map +1 -0
  160. package/dist/src/cli/values.js +189 -0
  161. package/dist/src/cli/values.js.map +1 -0
  162. package/dist/src/cli/vault.js +362 -0
  163. package/dist/src/cli/vault.js.map +1 -0
  164. package/dist/src/cli/verb-registry.js +2173 -0
  165. package/dist/src/cli/verb-registry.js.map +1 -0
  166. package/dist/src/cli/wordmark.js +52 -0
  167. package/dist/src/cli/wordmark.js.map +1 -0
  168. package/dist/src/core/advance-cycle.js +200 -0
  169. package/dist/src/core/advance-cycle.js.map +1 -0
  170. package/dist/src/core/agents-md.js +747 -0
  171. package/dist/src/core/agents-md.js.map +1 -0
  172. package/dist/src/core/attest.js +577 -0
  173. package/dist/src/core/attest.js.map +1 -0
  174. package/dist/src/core/audit.js +882 -0
  175. package/dist/src/core/audit.js.map +1 -0
  176. package/dist/src/core/budgets.js +449 -0
  177. package/dist/src/core/budgets.js.map +1 -0
  178. package/dist/src/core/checkpoint.js +738 -0
  179. package/dist/src/core/checkpoint.js.map +1 -0
  180. package/dist/src/core/child-env.js +86 -0
  181. package/dist/src/core/child-env.js.map +1 -0
  182. package/dist/src/core/clock.js +43 -0
  183. package/dist/src/core/clock.js.map +1 -0
  184. package/dist/src/core/command-class.js +2321 -0
  185. package/dist/src/core/command-class.js.map +1 -0
  186. package/dist/src/core/coverage-sources/adapter.js +71 -0
  187. package/dist/src/core/coverage-sources/adapter.js.map +1 -0
  188. package/dist/src/core/coverage-sources/gh.js +136 -0
  189. package/dist/src/core/coverage-sources/gh.js.map +1 -0
  190. package/dist/src/core/coverage-sources/git.js +269 -0
  191. package/dist/src/core/coverage-sources/git.js.map +1 -0
  192. package/dist/src/core/coverage.js +337 -0
  193. package/dist/src/core/coverage.js.map +1 -0
  194. package/dist/src/core/credential-spec.js +23 -0
  195. package/dist/src/core/credential-spec.js.map +1 -0
  196. package/dist/src/core/dark-session.js +714 -0
  197. package/dist/src/core/dark-session.js.map +1 -0
  198. package/dist/src/core/decision-refusal.js +265 -0
  199. package/dist/src/core/decision-refusal.js.map +1 -0
  200. package/dist/src/core/env-file.js +837 -0
  201. package/dist/src/core/env-file.js.map +1 -0
  202. package/dist/src/core/execute.js +1233 -0
  203. package/dist/src/core/execute.js.map +1 -0
  204. package/dist/src/core/frontmatter.js +100 -0
  205. package/dist/src/core/frontmatter.js.map +1 -0
  206. package/dist/src/core/gate-window.js +506 -0
  207. package/dist/src/core/gate-window.js.map +1 -0
  208. package/dist/src/core/gate.js +2947 -0
  209. package/dist/src/core/gate.js.map +1 -0
  210. package/dist/src/core/git-run.js +93 -0
  211. package/dist/src/core/git-run.js.map +1 -0
  212. package/dist/src/core/harness-version.js +210 -0
  213. package/dist/src/core/harness-version.js.map +1 -0
  214. package/dist/src/core/harness-wait.js +58 -0
  215. package/dist/src/core/harness-wait.js.map +1 -0
  216. package/dist/src/core/head-retry.js +121 -0
  217. package/dist/src/core/head-retry.js.map +1 -0
  218. package/dist/src/core/instance.js +319 -0
  219. package/dist/src/core/instance.js.map +1 -0
  220. package/dist/src/core/intake-limits.js +350 -0
  221. package/dist/src/core/intake-limits.js.map +1 -0
  222. package/dist/src/core/jcs.js +132 -0
  223. package/dist/src/core/jcs.js.map +1 -0
  224. package/dist/src/core/journal.js +200 -0
  225. package/dist/src/core/journal.js.map +1 -0
  226. package/dist/src/core/live-draw.js +703 -0
  227. package/dist/src/core/live-draw.js.map +1 -0
  228. package/dist/src/core/log-reconcile.js +136 -0
  229. package/dist/src/core/log-reconcile.js.map +1 -0
  230. package/dist/src/core/log.js +546 -0
  231. package/dist/src/core/log.js.map +1 -0
  232. package/dist/src/core/loop.js +476 -0
  233. package/dist/src/core/loop.js.map +1 -0
  234. package/dist/src/core/md-fence.js +74 -0
  235. package/dist/src/core/md-fence.js.map +1 -0
  236. package/dist/src/core/money.js +195 -0
  237. package/dist/src/core/money.js.map +1 -0
  238. package/dist/src/core/payload-census.js +146 -0
  239. package/dist/src/core/payload-census.js.map +1 -0
  240. package/dist/src/core/payload-store.js +340 -0
  241. package/dist/src/core/payload-store.js.map +1 -0
  242. package/dist/src/core/payload.js +80 -0
  243. package/dist/src/core/payload.js.map +1 -0
  244. package/dist/src/core/policy-diff.js +565 -0
  245. package/dist/src/core/policy-diff.js.map +1 -0
  246. package/dist/src/core/policy-expectations.js +394 -0
  247. package/dist/src/core/policy-expectations.js.map +1 -0
  248. package/dist/src/core/policy-explain.js +230 -0
  249. package/dist/src/core/policy-explain.js.map +1 -0
  250. package/dist/src/core/policy-load.js +524 -0
  251. package/dist/src/core/policy-load.js.map +1 -0
  252. package/dist/src/core/policy-match.js +467 -0
  253. package/dist/src/core/policy-match.js.map +1 -0
  254. package/dist/src/core/policy-proposal.js +458 -0
  255. package/dist/src/core/policy-proposal.js.map +1 -0
  256. package/dist/src/core/prompt-layout.js +422 -0
  257. package/dist/src/core/prompt-layout.js.map +1 -0
  258. package/dist/src/core/protected-path-guard.js +1087 -0
  259. package/dist/src/core/protected-path-guard.js.map +1 -0
  260. package/dist/src/core/registration.js +39 -0
  261. package/dist/src/core/registration.js.map +1 -0
  262. package/dist/src/core/reindex.js +336 -0
  263. package/dist/src/core/reindex.js.map +1 -0
  264. package/dist/src/core/sampler.js +388 -0
  265. package/dist/src/core/sampler.js.map +1 -0
  266. package/dist/src/core/sandbox.js +424 -0
  267. package/dist/src/core/sandbox.js.map +1 -0
  268. package/dist/src/core/seal.js +290 -0
  269. package/dist/src/core/seal.js.map +1 -0
  270. package/dist/src/core/state.js +1009 -0
  271. package/dist/src/core/state.js.map +1 -0
  272. package/dist/src/core/task-file.js +464 -0
  273. package/dist/src/core/task-file.js.map +1 -0
  274. package/dist/src/core/telegram-config.js +114 -0
  275. package/dist/src/core/telegram-config.js.map +1 -0
  276. package/dist/src/core/token.js +578 -0
  277. package/dist/src/core/token.js.map +1 -0
  278. package/dist/src/core/validate.js +0 -0
  279. package/dist/src/core/validate.js.map +1 -0
  280. package/dist/src/core/values.js +153 -0
  281. package/dist/src/core/values.js.map +1 -0
  282. package/dist/src/core/vault.js +612 -0
  283. package/dist/src/core/vault.js.map +1 -0
  284. package/dist/src/core/verified-snapshot.js +506 -0
  285. package/dist/src/core/verified-snapshot.js.map +1 -0
  286. package/dist/src/core/verify.js +549 -0
  287. package/dist/src/core/verify.js.map +1 -0
  288. package/dist/src/core/version.js +9 -0
  289. package/dist/src/core/version.js.map +1 -0
  290. package/dist/src/core/wysiwys.js +728 -0
  291. package/dist/src/core/wysiwys.js.map +1 -0
  292. package/dist/src/daemon/advance-child.js +78 -0
  293. package/dist/src/daemon/advance-child.js.map +1 -0
  294. package/dist/src/daemon/advance.js +849 -0
  295. package/dist/src/daemon/advance.js.map +1 -0
  296. package/dist/src/daemon/audit.js +90 -0
  297. package/dist/src/daemon/audit.js.map +1 -0
  298. package/dist/src/daemon/daemon.js +1988 -0
  299. package/dist/src/daemon/daemon.js.map +1 -0
  300. package/dist/src/daemon/dark-session.js +119 -0
  301. package/dist/src/daemon/dark-session.js.map +1 -0
  302. package/dist/src/daemon/draw-child.js +132 -0
  303. package/dist/src/daemon/draw-child.js.map +1 -0
  304. package/dist/src/daemon/draw.js +458 -0
  305. package/dist/src/daemon/draw.js.map +1 -0
  306. package/dist/src/daemon/git-evidence.js +345 -0
  307. package/dist/src/daemon/git-evidence.js.map +1 -0
  308. package/dist/src/daemon/projection.js +233 -0
  309. package/dist/src/daemon/projection.js.map +1 -0
  310. package/dist/src/daemon/prune.js +376 -0
  311. package/dist/src/daemon/prune.js.map +1 -0
  312. package/dist/src/mcp/http.js +343 -0
  313. package/dist/src/mcp/http.js.map +1 -0
  314. package/dist/src/mcp/server.js +594 -0
  315. package/dist/src/mcp/server.js.map +1 -0
  316. package/docs/cli-reference.md +5363 -0
  317. package/package.json +43 -4
  318. package/schema/.gitkeep +0 -0
  319. package/schema/LICENSE +117 -0
  320. package/schema/envelope.schema.json +137 -0
  321. package/schema/event.schema.json +1810 -0
  322. package/schema/fixtures/envelope/invalid/action-missing-idempotency-key.json +15 -0
  323. package/schema/fixtures/envelope/invalid/action-unknown-class-format.json +14 -0
  324. package/schema/fixtures/envelope/invalid/confidence-out-of-range.json +11 -0
  325. package/schema/fixtures/envelope/invalid/est-cost-bare-number.json +14 -0
  326. package/schema/fixtures/envelope/invalid/est-cost-noncanonical-string.json +14 -0
  327. package/schema/fixtures/envelope/invalid/malformed-assignee.json +10 -0
  328. package/schema/fixtures/envelope/invalid/malformed-created-by.json +7 -0
  329. package/schema/fixtures/envelope/invalid/malformed-max-latency.json +11 -0
  330. package/schema/fixtures/envelope/invalid/malformed-payload-hash.json +14 -0
  331. package/schema/fixtures/envelope/invalid/max-cost-bare-number.json +11 -0
  332. package/schema/fixtures/envelope/invalid/missing-origin.json +3 -0
  333. package/schema/fixtures/envelope/invalid/negative-est-cost.json +14 -0
  334. package/schema/fixtures/envelope/invalid/unknown-state.json +7 -0
  335. package/schema/fixtures/envelope/invalid/unknown-top-level-field.json +8 -0
  336. package/schema/fixtures/envelope/valid/action-payload-hash.json +17 -0
  337. package/schema/fixtures/envelope/valid/actions-without-budget.json +18 -0
  338. package/schema/fixtures/envelope/valid/canonical.json +25 -0
  339. package/schema/fixtures/envelope/valid/minimal.json +7 -0
  340. package/schema/fixtures/envelope/valid/multi-action-executed.json +30 -0
  341. package/schema/fixtures/envelope/valid/record-write-stage.json +22 -0
  342. package/schema/fixtures/event/invalid/approval-granted-agent-actor.json +15 -0
  343. package/schema/fixtures/event/invalid/approval-granted-empty-batch-delivery-id.json +17 -0
  344. package/schema/fixtures/event/invalid/approval-granted-fifth-reaction.json +16 -0
  345. package/schema/fixtures/event/invalid/approval-granted-missing-actor.json +14 -0
  346. package/schema/fixtures/event/invalid/approval-requested-missing-action-key.json +14 -0
  347. package/schema/fixtures/event/invalid/approval-withdrawn-agent-policy-drift.json +15 -0
  348. package/schema/fixtures/event/invalid/approval-withdrawn-missing-reason.json +15 -0
  349. package/schema/fixtures/event/invalid/approval-withdrawn-system-actor.json +15 -0
  350. package/schema/fixtures/event/invalid/audit-decision-refused-human-actor.json +17 -0
  351. package/schema/fixtures/event/invalid/audit-decision-refused-missing-code.json +16 -0
  352. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +15 -0
  353. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +16 -0
  354. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +15 -0
  355. package/schema/fixtures/event/invalid/bad-actor-prefix.json +15 -0
  356. package/schema/fixtures/event/invalid/est-cost-bare-number.json +17 -0
  357. package/schema/fixtures/event/invalid/execution-completed-fabricated-exit-code.json +16 -0
  358. package/schema/fixtures/event/invalid/execution-completed-provider-ref-empty-id.json +18 -0
  359. package/schema/fixtures/event/invalid/execution-completed-provider-ref-extra-field.json +19 -0
  360. package/schema/fixtures/event/invalid/execution-completed-provider-ref-id-not-string.json +18 -0
  361. package/schema/fixtures/event/invalid/execution-completed-provider-ref-missing-adapter.json +17 -0
  362. package/schema/fixtures/event/invalid/execution-failed-open-reported-by.json +16 -0
  363. package/schema/fixtures/event/invalid/execution-indeterminate-open-reason.json +14 -0
  364. package/schema/fixtures/event/invalid/execution-reconciled-agent-actor.json +17 -0
  365. package/schema/fixtures/event/invalid/execution-started-negative-env-stripped.json +16 -0
  366. package/schema/fixtures/event/invalid/gate-bypassed-missing-opened-seq.json +15 -0
  367. package/schema/fixtures/event/invalid/gate-closed-non-integer-opened-seq.json +13 -0
  368. package/schema/fixtures/event/invalid/gate-opened-agent-actor.json +16 -0
  369. package/schema/fixtures/event/invalid/gate-organ-attested-absolute-path.json +14 -0
  370. package/schema/fixtures/event/invalid/gate-organ-attested-agent-actor.json +14 -0
  371. package/schema/fixtures/event/invalid/gate-organ-attested-missing-organ-path.json +13 -0
  372. package/schema/fixtures/event/invalid/harness-unknown-kind.json +18 -0
  373. package/schema/fixtures/event/invalid/harness-version-multiline.json +16 -0
  374. package/schema/fixtures/event/invalid/log-checkpoint-agent-actor.json +17 -0
  375. package/schema/fixtures/event/invalid/log-checkpoint-missing-signature.json +16 -0
  376. package/schema/fixtures/event/invalid/log-checkpoint-short-signed-hash.json +17 -0
  377. package/schema/fixtures/event/invalid/log-checkpoint-unknown-signature-alg.json +17 -0
  378. package/schema/fixtures/event/invalid/malformed-ts.json +15 -0
  379. package/schema/fixtures/event/invalid/missing-alg.json +14 -0
  380. package/schema/fixtures/event/invalid/missing-hash.json +14 -0
  381. package/schema/fixtures/event/invalid/non-integer-seq.json +15 -0
  382. package/schema/fixtures/event/invalid/payload-pruned-human-actor.json +14 -0
  383. package/schema/fixtures/event/invalid/payload-pruned-missing-hash.json +14 -0
  384. package/schema/fixtures/event/invalid/policy-declined-agent-actor.json +15 -0
  385. package/schema/fixtures/event/invalid/policy-proposed-missing-diff.json +19 -0
  386. package/schema/fixtures/event/invalid/policy-proposed-system-actor.json +26 -0
  387. package/schema/fixtures/event/invalid/short-hash.json +15 -0
  388. package/schema/fixtures/event/invalid/unknown-alg.json +15 -0
  389. package/schema/fixtures/event/invalid/unknown-event-type.json +15 -0
  390. package/schema/fixtures/event/invalid/unknown-top-level-field.json +16 -0
  391. package/schema/fixtures/event/valid/approval-expired.json +15 -0
  392. package/schema/fixtures/event/valid/approval-granted-batch.json +19 -0
  393. package/schema/fixtures/event/valid/approval-granted-reaction.json +16 -0
  394. package/schema/fixtures/event/valid/approval-granted.json +15 -0
  395. package/schema/fixtures/event/valid/approval-rejected.json +15 -0
  396. package/schema/fixtures/event/valid/approval-requested.json +19 -0
  397. package/schema/fixtures/event/valid/approval-revoked.json +15 -0
  398. package/schema/fixtures/event/valid/approval-withdrawn-policy-drift.json +17 -0
  399. package/schema/fixtures/event/valid/approval-withdrawn.json +16 -0
  400. package/schema/fixtures/event/valid/audit-decision-refused.json +20 -0
  401. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +17 -0
  402. package/schema/fixtures/event/valid/audit-reviewed.json +15 -0
  403. package/schema/fixtures/event/valid/audit-sampled.json +14 -0
  404. package/schema/fixtures/event/valid/budget-exceeded.json +21 -0
  405. package/schema/fixtures/event/valid/envelope-drift.json +16 -0
  406. package/schema/fixtures/event/valid/execution-completed-harness-report.json +16 -0
  407. package/schema/fixtures/event/valid/execution-completed-provider-ref.json +18 -0
  408. package/schema/fixtures/event/valid/execution-completed.json +15 -0
  409. package/schema/fixtures/event/valid/execution-failed-harness-report.json +16 -0
  410. package/schema/fixtures/event/valid/execution-failed.json +15 -0
  411. package/schema/fixtures/event/valid/execution-indeterminate.json +15 -0
  412. package/schema/fixtures/event/valid/execution-reconciled.json +17 -0
  413. package/schema/fixtures/event/valid/execution-started-env-stripped.json +17 -0
  414. package/schema/fixtures/event/valid/execution-started.json +14 -0
  415. package/schema/fixtures/event/valid/gate-bypassed-harness-version.json +18 -0
  416. package/schema/fixtures/event/valid/gate-bypassed.json +19 -0
  417. package/schema/fixtures/event/valid/gate-closed.json +14 -0
  418. package/schema/fixtures/event/valid/gate-opened.json +16 -0
  419. package/schema/fixtures/event/valid/gate-organ-attested.json +14 -0
  420. package/schema/fixtures/event/valid/genesis-null-prev.json +14 -0
  421. package/schema/fixtures/event/valid/log-checkpoint.json +17 -0
  422. package/schema/fixtures/event/valid/payload-pruned-orphan.json +13 -0
  423. package/schema/fixtures/event/valid/payload-pruned.json +17 -0
  424. package/schema/fixtures/event/valid/policy-declined.json +16 -0
  425. package/schema/fixtures/event/valid/policy-proposed.json +35 -0
  426. package/schema/fixtures/event/valid/policy-updated.json +14 -0
  427. package/schema/fixtures/event/valid/reconciliation-required.json +18 -0
  428. package/schema/fixtures/event/valid/reconciliation-satisfied.json +17 -0
  429. package/schema/fixtures/event/valid/route-accepted.json +15 -0
  430. package/schema/fixtures/event/valid/route-proposed.json +16 -0
  431. package/schema/fixtures/event/valid/spec-example.json +15 -0
  432. package/schema/fixtures/event/valid/task-registered-harness-version.json +23 -0
  433. package/schema/fixtures/event/valid/task-registered.json +14 -0
  434. package/schema/fixtures/hash/known-answer-pre-121.json +74 -0
  435. package/schema/fixtures/hash/known-answer.json +74 -0
  436. package/schema/fixtures/policy/invalid/bad-approval-ttl.json +7 -0
  437. package/schema/fixtures/policy/invalid/bad-web-port.json +4 -0
  438. package/schema/fixtures/policy/invalid/checkpoint-key-not-base64.json +6 -0
  439. package/schema/fixtures/policy/invalid/class-rule-missing-autonomy.json +9 -0
  440. package/schema/fixtures/policy/invalid/empty-class-key.json +6 -0
  441. package/schema/fixtures/policy/invalid/live-rate-on-human-only.json +7 -0
  442. package/schema/fixtures/policy/invalid/malformed-class-key.json +6 -0
  443. package/schema/fixtures/policy/invalid/missing-version.json +8 -0
  444. package/schema/fixtures/policy/invalid/negative-limit.json +9 -0
  445. package/schema/fixtures/policy/invalid/non-numeric-limit.json +9 -0
  446. package/schema/fixtures/policy/invalid/non-positive-max-pending.json +9 -0
  447. package/schema/fixtures/policy/invalid/on-expiry-grant.json +8 -0
  448. package/schema/fixtures/policy/invalid/payload-retention-bare-number.json +4 -0
  449. package/schema/fixtures/policy/invalid/payload-retention-compound.json +4 -0
  450. package/schema/fixtures/policy/invalid/payload-retention-fractional.json +4 -0
  451. package/schema/fixtures/policy/invalid/payload-retention-zero.json +4 -0
  452. package/schema/fixtures/policy/invalid/protected-paths-escape.json +4 -0
  453. package/schema/fixtures/policy/invalid/protected-paths-glob.json +4 -0
  454. package/schema/fixtures/policy/invalid/retro-rate-on-human-only.json +7 -0
  455. package/schema/fixtures/policy/invalid/retro-rate-on-manual.json +7 -0
  456. package/schema/fixtures/policy/invalid/retro-rate-zero.json +7 -0
  457. package/schema/fixtures/policy/invalid/sample-rate-too-high.json +5 -0
  458. package/schema/fixtures/policy/invalid/sampling-secret-env-empty.json +7 -0
  459. package/schema/fixtures/policy/invalid/sampling-secret-env-not-string.json +6 -0
  460. package/schema/fixtures/policy/invalid/skew-tolerance-compound.json +6 -0
  461. package/schema/fixtures/policy/invalid/unknown-autonomy.json +7 -0
  462. package/schema/fixtures/policy/invalid/unknown-class-rule-key.json +6 -0
  463. package/schema/fixtures/policy/invalid/unknown-top-level-key.json +7 -0
  464. package/schema/fixtures/policy/invalid/vault-passphrase-env-empty.json +6 -0
  465. package/schema/fixtures/policy/invalid/vault-passphrase-literal.json +6 -0
  466. package/schema/fixtures/policy/invalid/version-not-string.json +4 -0
  467. package/schema/fixtures/policy/valid/canonical.json +47 -0
  468. package/schema/fixtures/policy/valid/checkpoint-keys.json +18 -0
  469. package/schema/fixtures/policy/valid/class-approvers-limits.json +25 -0
  470. package/schema/fixtures/policy/valid/class-retro-rate.json +17 -0
  471. package/schema/fixtures/policy/valid/global-budgets.json +19 -0
  472. package/schema/fixtures/policy/valid/human-only.json +9 -0
  473. package/schema/fixtures/policy/valid/minimal.json +6 -0
  474. package/schema/fixtures/policy/valid/protected-paths.json +10 -0
  475. package/schema/fixtures/policy/valid/record-namespace.json +13 -0
  476. package/schema/fixtures/policy/valid/request-volume-limits.json +26 -0
  477. package/schema/fixtures/policy/valid/retention-and-sampling-secret.json +16 -0
  478. package/schema/fixtures/policy/valid/skew-tolerance.json +15 -0
  479. package/schema/fixtures/policy/valid/vault-passphrase-env.json +14 -0
  480. package/schema/fixtures/policy/valid/wildcards.json +15 -0
  481. package/schema/fixtures/policy-md/invalid/alias-bomb.md +15 -0
  482. package/schema/fixtures/policy-md/invalid/no-fence.md +7 -0
  483. package/schema/fixtures/policy-md/invalid/protected-route-not-a-subclass.md +16 -0
  484. package/schema/fixtures/policy-md/invalid/schema-invalid-autonomy.md +16 -0
  485. package/schema/fixtures/policy-md/invalid/schema-invalid-read-proof.md +17 -0
  486. package/schema/fixtures/policy-md/invalid/two-fences.md +19 -0
  487. package/schema/fixtures/policy-md/invalid/unclosed-fence.md +11 -0
  488. package/schema/fixtures/policy-md/invalid/wrong-info-string.md +11 -0
  489. package/schema/fixtures/policy-md/invalid/yaml-syntax-error.md +13 -0
  490. package/schema/fixtures/policy-md/precedence/both/APPROVAL.md +7 -0
  491. package/schema/fixtures/policy-md/precedence/both/APPROVALS.md +7 -0
  492. package/schema/fixtures/policy-md/precedence/fallback-only/APPROVALS.md +7 -0
  493. package/schema/fixtures/policy-md/valid/canonical.md +50 -0
  494. package/schema/fixtures/policy-md/valid/daemon-read-proof.md +18 -0
  495. package/schema/fixtures/policy-md/valid/minimal.md +3 -0
  496. package/schema/fixtures/policy-md/valid/prose-lookalikes.md +54 -0
  497. package/schema/fixtures/policy-md/valid/routed-protected-paths.md +49 -0
  498. package/schema/fixtures/policy-md/valid/with-values.md +79 -0
  499. package/schema/fixtures/sample-record/invalid/bad-date-time.json +4 -0
  500. package/schema/fixtures/sample-record/invalid/missing-required-field.json +3 -0
  501. package/schema/fixtures/sample-record/invalid/unknown-top-level-field.json +5 -0
  502. package/schema/fixtures/sample-record/invalid/wrong-type.json +4 -0
  503. package/schema/fixtures/sample-record/valid/minimal.json +4 -0
  504. package/schema/fixtures/sample-record/valid/with-note.json +5 -0
  505. package/schema/fixtures/values/invalid/class-shaped.json +9 -0
  506. package/schema/fixtures/values/invalid/duplicate-entry.json +4 -0
  507. package/schema/fixtures/values/invalid/non-string-item.json +4 -0
  508. package/schema/fixtures/values/invalid/over-cap.json +26 -0
  509. package/schema/fixtures/values/invalid/unknown-key.json +5 -0
  510. package/schema/fixtures/values/invalid/version-string.json +1 -0
  511. package/schema/fixtures/values/valid/empty-lists.json +7 -0
  512. package/schema/fixtures/values/valid/full.json +20 -0
  513. package/schema/fixtures/values/valid/minimal.json +1 -0
  514. package/schema/fixtures/values-md/invalid/schema-invalid.md +62 -0
  515. package/schema/fixtures/values-md/invalid/two-blocks.md +69 -0
  516. package/schema/fixtures/values-md/invalid/unterminated.md +61 -0
  517. package/schema/fixtures/values-md/invalid/yaml-error.md +63 -0
  518. package/schema/fixtures/values-md/valid/absent.md +50 -0
  519. package/schema/fixtures/values-md/valid/with-values.md +79 -0
  520. package/schema/policy.schema.json +481 -0
  521. package/schema/sample-record.schema.json +26 -0
  522. package/schema/values.schema.json +55 -0
@@ -0,0 +1,749 @@
1
+ /**
2
+ * `approval log advance` (APRV-125) — the commit-and-push half of the log
3
+ * ritual, as a verb.
4
+ *
5
+ * The APRV-92 flow, written down: the log's appends the remote does not have
6
+ * yet are gathered into a commit whose message names the seq range they cover,
7
+ * and pushed to a short-lived records branch that exists for it. Main is
8
+ * protected here, so the commit reaches it through a pull request, and the PR
9
+ * step is `--pr`, which runs the ordinary gated `gh` path. Since APRV-284 that
10
+ * step also ARMS the merge (`gh pr merge <branch> --merge --auto`, off with
11
+ * `--no-auto-merge`): a records pull request carries only evidence paths, so
12
+ * the click it was waiting for was never a review. {@link armAutoMerge} carries
13
+ * the reasoning and the guard that keeps it true.
14
+ *
15
+ * ## The three refusals that are the point
16
+ *
17
+ * **`log-advance-dirty-stage`.** The staged set must be EXACTLY the log, the
18
+ * queue projection, and the payload store — nothing else. The failure this
19
+ * prevents is the one CLAUDE.md's rule is written against: a log commit riding
20
+ * a branch that carries other work. A verb that ran `git add -A` and hoped
21
+ * would be a worse version of the hand-ritual it replaces, so anything else
22
+ * already staged is a refusal and not a thing to unstage. Unstaging someone
23
+ * else's work is not this verb's decision to make.
24
+ *
25
+ * **`log-advance-checkout-required`.** This verb never checks out anything. The
26
+ * checkout is the footgun: a branch switch with an uncommitted working log
27
+ * rewinds `events.jsonl` under whatever holds it open, which is fork 2 of
28
+ * 2026-08-20. It builds the commit with a scratch index and pushes THAT commit
29
+ * to a records branch by refspec, which moves no HEAD and touches no file.
30
+ *
31
+ * ## The base is the remote, not the local branch (APRV-203)
32
+ *
33
+ * The verb used to commit on the branch you were standing on, which made the
34
+ * operator responsible for that branch being current. A checkout whose local
35
+ * `main` was behind origin produced a records commit parented on a stale tip,
36
+ * carrying a tree that silently reverted everything main had merged since. So
37
+ * the verb fetches first and bases its commit on the remote's tip: `git
38
+ * read-tree` fills a scratch index from origin's tree, the log, the queue
39
+ * projection and the payload store are laid over it from the working tree, and
40
+ * `commit-tree` parents the result on origin. HEAD, the operator's index and
41
+ * every file in the working tree are exactly as they were found.
42
+ *
43
+ * **`log-advance-not-primary`.** Same rule as sync's: the committed log has one
44
+ * home, and it is not a worktree.
45
+ *
46
+ * ## One records branch, and one pull request, per day (APRV-204)
47
+ *
48
+ * The daemon advances on a cadence, so an advance is no longer a once-a-day
49
+ * ceremony and the day's records branch usually already exists. When it does,
50
+ * and its log is a prefix of the working log, the commit is parented on THAT
51
+ * branch rather than on the trunk: parenting every advance on the trunk makes
52
+ * the second push of the day a non-fast-forward of a branch a pull request is
53
+ * already open on. `--pr` asks whether an open pull request exists for the head
54
+ * branch before creating one, so a day gets a single pull request that later
55
+ * advances grow.
56
+ *
57
+ * A local branch that is AHEAD of origin with commits this verb did not make is
58
+ * not a refusal: the advance is based on origin either way, and those commits
59
+ * are simply not part of it. What IS refused is a working log that origin's log
60
+ * is not a prefix of, in either direction, because an advance is only ever the
61
+ * records origin does not have yet.
62
+ *
63
+ * ## This verb appends no event
64
+ *
65
+ * For the same reason `log sync` appends none: committing the file the log
66
+ * lives in is housekeeping on the container, not a decision about the world.
67
+ * The commit is already the record of itself, in git, where a reader can see
68
+ * exactly which bytes moved (SPEC §10.1, amended APRV-125).
69
+ */
70
+ import { existsSync, readFileSync } from "node:fs";
71
+ import { join } from "node:path";
72
+ import { isAdvanceBookkeeping } from "../core/advance-cycle.js";
73
+ import { withAppendLock } from "../core/log.js";
74
+ import { verify } from "../core/verify.js";
75
+ import { commitOnBase, currentBranch, failureText, fetchBase, gh, git, outputLines, primaryCheckout, repoPath, showBlob, } from "./git-scope.js";
76
+ import { anchorRevs, defaultRecordsBranch } from "./log-anchor.js";
77
+ import { compareChains } from "../core/log-reconcile.js";
78
+ import { silentProgress } from "./progress.js";
79
+ import { DEFAULT_LOG_PATH } from "./paths.js";
80
+ import { DEFAULT_QUEUE_PATH } from "./render.js";
81
+ /** SPEC §9: the payload store, the third and last path an advance may carry. */
82
+ const DEFAULT_PAYLOADS_DIR = ".approval/payloads";
83
+ /**
84
+ * The only paths an advance may stage, as git spells them.
85
+ *
86
+ * A closed list, checked in both directions: everything staged must be in it,
87
+ * and nothing outside it may be staged. Widening it is a reviewable diff.
88
+ */
89
+ export const ADVANCE_PATHS = [
90
+ DEFAULT_LOG_PATH,
91
+ DEFAULT_QUEUE_PATH,
92
+ DEFAULT_PAYLOADS_DIR,
93
+ ];
94
+ /** Machine-readable refusal codes. Frozen public API, printed in the help. */
95
+ export const LOG_ADVANCE_REFUSAL_CODES = [
96
+ "log-advance-not-primary",
97
+ "log-advance-no-branch",
98
+ "log-advance-dirty-stage",
99
+ "log-advance-checkout-required",
100
+ "log-advance-unverified",
101
+ "log-advance-locked",
102
+ "log-advance-fetch-failed",
103
+ "log-advance-behind-remote",
104
+ "log-advance-remote-diverged",
105
+ "log-advance-git-failed",
106
+ "log-advance-push-rejected",
107
+ "log-advance-pr-failed",
108
+ ];
109
+ function detail(cause) {
110
+ return cause instanceof Error ? cause.message : String(cause);
111
+ }
112
+ // Re-exported rather than defined here (APRV-219). The anchor check and this
113
+ // verb must name the day's records branch identically, and `cli/log-anchor.ts`
114
+ // is the module that owns which revs a committed copy of the log may live at.
115
+ export { defaultRecordsBranch };
116
+ /**
117
+ * Everything currently staged, as repo-relative paths.
118
+ *
119
+ * `--name-only --cached` is the whole index against HEAD; the `-z` form is used
120
+ * because a path with a space in it is a path, and a split on newlines would
121
+ * quietly mangle it.
122
+ */
123
+ function stagedPaths(root) {
124
+ const result = git(["diff", "--cached", "--name-only", "-z"], root);
125
+ if (!result.ok)
126
+ return { ok: false, message: failureText(result) };
127
+ const paths = result.stdout.split("\0").filter((entry) => entry.length > 0);
128
+ return { ok: true, paths };
129
+ }
130
+ /**
131
+ * Does `commit` already carry `candidate` in its history? (APRV-234)
132
+ *
133
+ * The question the day's records branch has to answer before another commit is
134
+ * stacked on it: a branch that no longer contains the trunk cannot be
135
+ * fast-forwarded into it, and everything built on it inherits that. Answered
136
+ * with `merge-base --is-ancestor`, whose exit status IS the answer; anything
137
+ * that is not a clean "yes" reads as "no", which is the direction that rebuilds
138
+ * rather than the direction that stacks on a base nobody checked.
139
+ */
140
+ function contains(root, commit, candidate) {
141
+ return git(["merge-base", "--is-ancestor", candidate, commit], root).ok;
142
+ }
143
+ /** Is `path` inside one of the three paths an advance is allowed to carry? */
144
+ function isAdvancePath(path) {
145
+ return ADVANCE_PATHS.some((allowed) => path === allowed || path.startsWith(`${allowed}/`));
146
+ }
147
+ /** `approval log advance`, as a function. The CLI wrapper formats its answer. */
148
+ export function logAdvance(options) {
149
+ const remote = options.remote ?? "origin";
150
+ const dryRun = options.dryRun === true;
151
+ const primary = primaryCheckout(options.cwd);
152
+ if (!primary.ok) {
153
+ return {
154
+ ok: false,
155
+ code: "log-advance-not-primary",
156
+ message: `log advance runs in the PRIMARY checkout only: ${primary.reason}. Log-touching commits are made where the log lives, never from a worktree. Run this in ${primary.primary ?? "the primary checkout"}.`,
157
+ };
158
+ }
159
+ const root = primary.root;
160
+ const logPath = join(root, DEFAULT_LOG_PATH);
161
+ const branch = currentBranch(root);
162
+ if (branch === null) {
163
+ return {
164
+ ok: false,
165
+ code: "log-advance-no-branch",
166
+ message: `HEAD in ${root} is detached, so there is no branch to commit on. This verb never checks anything out; check out the branch the log lives on yourself and run it again.`,
167
+ };
168
+ }
169
+ // The lock covers the READ of the log and nothing after it (APRV-233).
170
+ //
171
+ // It used to wrap this whole function, `git fetch`, `git push` and `gh pr
172
+ // create` included, and on 2026-09-02 that stalled every harness hook on the
173
+ // machine: each one asked for the append lock, waited the full two seconds
174
+ // and refused its command `append-failed`, for as long as each advance's
175
+ // network round-trips took. Nothing in the git half of the verb appends
176
+ // anything, so nothing in it needs the lock. What the lock IS for is the
177
+ // consistency of one read: the chain must verify, the seq range must be
178
+ // measured, and the bytes that get committed must be the bytes that were
179
+ // verified — so the log's exact content is pinned as a git blob HERE, under
180
+ // the lock, and the commit is assembled from that object rather than from a
181
+ // file another writer may have grown in the meantime.
182
+ (options.progress ?? silentProgress).phase("verifying the log chain before anything is committed from it");
183
+ const held = withAppendLock(logPath, () => snapshotUnderLock(root, logPath));
184
+ if (!held.ok) {
185
+ return {
186
+ ok: false,
187
+ code: held.error.code === "lock-timeout" ? "log-advance-locked" : "log-advance-git-failed",
188
+ message: held.error.code === "lock-timeout"
189
+ ? `${held.error.message}. Advance holds the append lock while it READS the chain, so the seq range it names is the range it commits. Wait for the append to finish and run this again.`
190
+ : held.error.message,
191
+ };
192
+ }
193
+ if (!held.value.ok)
194
+ return held.value;
195
+ return advanceOnSnapshot({ root, logPath, branch, remote, dryRun, options }, held.value);
196
+ }
197
+ /**
198
+ * Everything the advance must read from the log, read once and pinned.
199
+ *
200
+ * Short by design: a chain verify, an index read and one `git hash-object`, all
201
+ * local. Every writer on the machine waits on this and on nothing else.
202
+ */
203
+ function snapshotUnderLock(root, logPath) {
204
+ // 1. The chain, verified. A commit of a log that does not verify would be
205
+ // publishing the break.
206
+ const verified = verify(logPath);
207
+ if (verified.status !== "clean") {
208
+ return {
209
+ ok: false,
210
+ code: "log-advance-unverified",
211
+ message: verified.status === "torn-tail"
212
+ ? `${logPath} ends without a newline: the final record is truncated. Nothing is committed from a torn log; run \`approval log verify\`.`
213
+ : `${logPath} does not verify (${verified.reason}): ${verified.message}. Nothing is committed from a log that does not verify.`,
214
+ };
215
+ }
216
+ // 2. The staged set: exactly the three paths, and nothing else.
217
+ const staged = stagedPaths(root);
218
+ if (!staged.ok) {
219
+ return {
220
+ ok: false,
221
+ code: "log-advance-git-failed",
222
+ message: `the index could not be read: ${staged.message}`,
223
+ };
224
+ }
225
+ const offending = staged.paths.filter((path) => !isAdvancePath(path));
226
+ if (offending.length > 0) {
227
+ return {
228
+ ok: false,
229
+ code: "log-advance-dirty-stage",
230
+ offending,
231
+ message: `${String(offending.length)} path(s) are staged that an advance may not carry: ${offending.join(", ")}. An advance commits the log, the queue projection and the payload store, and nothing else — a log commit that rides other work is the thing the one-commit rule exists to forbid. Unstage them yourself (\`git restore --staged <path>\`) and run this again; this verb will not unstage anyone's work for them.`,
232
+ };
233
+ }
234
+ // 3. The bytes, pinned. `hash-object -w` writes the blob into the object
235
+ // store and answers its sha, so the commit built after the lock is
236
+ // released carries exactly what was verified above, however much the file
237
+ // has grown by then.
238
+ const text = textOfWorking(logPath);
239
+ const hashed = git(["hash-object", "-w", "--", repoPath(root, logPath)], root);
240
+ const blob = hashed.stdout.trim();
241
+ if (!hashed.ok || blob.length === 0) {
242
+ return {
243
+ ok: false,
244
+ code: "log-advance-git-failed",
245
+ message: `the working log could not be written to the object store: ${failureText(hashed)}; nothing was committed.`,
246
+ };
247
+ }
248
+ return { ok: true, text, blob, staged: staged.paths };
249
+ }
250
+ function advanceOnSnapshot(ctx, snapshot) {
251
+ const { root, logPath, branch, remote, dryRun, options } = ctx;
252
+ const progress = options.progress ?? silentProgress;
253
+ // 4. The base: the remote's tip, fetched by this verb rather than by the
254
+ // operator (APRV-203). Everything after this is measured against it.
255
+ const baseBranch = options.base ?? branch;
256
+ progress.phase(`fetching ${remote}/${baseBranch}: an advance is based on the remote, not on this checkout`);
257
+ const fetched = fetchBase(root, remote, baseBranch);
258
+ if (!fetched.ok) {
259
+ return {
260
+ ok: false,
261
+ code: "log-advance-fetch-failed",
262
+ message: `${fetched.message}. An advance bases its commit on ${remote}/${baseBranch}, so it cannot proceed without knowing where that is. Nothing was committed. Fix the remote (network, credentials, or a branch named something else — pass \`--base <branch>\`) and run this again.`,
263
+ quote: fetched.quote,
264
+ };
265
+ }
266
+ const baseSha = fetched.sha;
267
+ // 5. What is owed: the seq range between the committed log and the working
268
+ // head. Measured against the SNAPSHOT (APRV-233), which is the same bytes
269
+ // the commit will carry, so the range this names is the range it commits
270
+ // even though the lock is no longer held.
271
+ const workingText = snapshot.text;
272
+ const against = (sha, label) => {
273
+ const blob = showBlob(root, sha, repoPath(root, logPath));
274
+ const compared = compareChains({ label: `the working log ${logPath}`, text: workingText }, {
275
+ label: `the committed log ${label}:${repoPath(root, logPath)}`,
276
+ text: blob === null ? "" : blob.toString("utf8"),
277
+ });
278
+ if (!compared.ok) {
279
+ return { ok: false, code: "log-advance-unverified", message: compared.message };
280
+ }
281
+ if (compared.drift.relation === "diverged") {
282
+ return {
283
+ ok: false,
284
+ code: "log-advance-remote-diverged",
285
+ message: `the working log and ${label}'s log part at seq ${String(compared.drift.firstDivergentSeq)}: two chains, not one. Nothing is advanced over a fork, and nothing was committed. Run \`approval doctor\` for the log-drift report; hash chains do not merge, so which of the two is the log is a human decision.`,
286
+ };
287
+ }
288
+ if (compared.drift.relation === "behind") {
289
+ return {
290
+ ok: false,
291
+ code: "log-advance-behind-remote",
292
+ message: `${label} carries records this working log does not (its head is ${String(compared.drift.committedHead?.seq ?? 0)}, the working head is ${String(compared.drift.workingHead?.seq ?? 0)}). An advance publishes records the remote lacks, so there is nothing here to publish and committing would propose an older chain than the one already on the remote. Run \`approval log sync\` first, then run this again. Nothing was committed.`,
293
+ };
294
+ }
295
+ return { ok: true, drift: compared.drift };
296
+ };
297
+ const trunk = against(baseSha, `${remote}/${baseBranch}`);
298
+ if (!("drift" in trunk))
299
+ return trunk;
300
+ const recordsBranch = options.branch ?? defaultRecordsBranch(options.today ?? new Date().toISOString());
301
+ if (recordsBranch === "main" || recordsBranch === "master") {
302
+ return {
303
+ ok: false,
304
+ code: "log-advance-checkout-required",
305
+ message: `--branch ${recordsBranch} would push the advance straight at the trunk. An advance goes to a records branch and reaches the trunk through a pull request; pick another name.`,
306
+ };
307
+ }
308
+ // 4a. One records branch per day, updated rather than re-created (APRV-204).
309
+ //
310
+ // The day's branch usually already exists: the daemon advances on a cadence
311
+ // and every tick after the first would otherwise build ANOTHER commit on the
312
+ // trunk, which the remote rejects as a non-fast-forward of the branch a pull
313
+ // request is already open on. So when the records branch exists and its log
314
+ // is a prefix of the working log, THAT is the parent: the push fast-forwards
315
+ // and the open pull request grows by one commit.
316
+ //
317
+ // A fetch failure here is read as "no such branch", which is what it is
318
+ // whenever the trunk fetch a moment ago succeeded. The alternative — treating
319
+ // it as a network failure and refusing — would refuse every first advance of
320
+ // every day, since the branch genuinely does not exist yet.
321
+ //
322
+ // 4b. Unless the trunk has moved under the branch (APRV-234).
323
+ //
324
+ // Observed 2026-09-02: a ceremony commit landed on main carrying its own copy
325
+ // of the log, so `records-log-2026-09-02` no longer contained the trunk. The
326
+ // daemon went on stacking advance commits on the branch tip — six of them —
327
+ // each built on a base that was missing main's copy, and the pull request
328
+ // went DIRTY and stayed DIRTY. Nothing the cadence could do would clear it;
329
+ // it took a hand `git merge -X ours origin/main` by a person, which is
330
+ // precisely the tap this feature exists to remove.
331
+ //
332
+ // So: stack only while the branch still contains the base. When it does not,
333
+ // REBUILD — the same commit content, parented on the current trunk instead —
334
+ // and push it over the branch. That is sound here for a reason peculiar to
335
+ // this verb: the only paths it carries are the append-only log, its
336
+ // projection and its content-addressed payloads, and the trunk check above
337
+ // has already refused unless the working log is a SUPERSET of the trunk's
338
+ // (`log-advance-behind-remote` in one direction, `log-advance-remote-diverged`
339
+ // in the other). A rebuilt commit is therefore the trunk's own log plus the
340
+ // tail it lacks, never a revert of anything the trunk merged.
341
+ let parent = { ref: `${remote}/${baseBranch}`, sha: baseSha };
342
+ let drift = trunk.drift;
343
+ let reusedRecordsBranch = false;
344
+ let rebuilt = false;
345
+ let rebuiltOn;
346
+ const existing = fetchBase(root, remote, recordsBranch);
347
+ if (existing.ok) {
348
+ if (contains(root, existing.sha, baseSha)) {
349
+ const onBranch = against(existing.sha, `${remote}/${recordsBranch}`);
350
+ if (!("drift" in onBranch))
351
+ return onBranch;
352
+ parent = { ref: `${remote}/${recordsBranch}`, sha: existing.sha };
353
+ drift = onBranch.drift;
354
+ reusedRecordsBranch = true;
355
+ }
356
+ else {
357
+ // The branch exists and does not contain the trunk. Keep `parent` at the
358
+ // trunk and say so: the range is measured against the trunk's log, which
359
+ // is what `trunk.drift` already holds.
360
+ rebuilt = true;
361
+ rebuiltOn = { ref: `${remote}/${baseBranch}`, sha: baseSha };
362
+ }
363
+ }
364
+ const committedSeq = drift.committedHead?.seq ?? 0;
365
+ // `null` when there is nothing owed. A no-op advance is a SUCCESS, exactly as
366
+ // a no-op `policy amend` is: an operator who runs it on an already-committed
367
+ // log has established what they wanted to establish.
368
+ const range = drift.relation === "ahead"
369
+ ? { from: committedSeq + 1, to: drift.workingHead?.seq ?? committedSeq }
370
+ : null;
371
+ const message = range === null ? "" : advanceMessage(range, branch);
372
+ const carried = ADVANCE_PATHS.filter((path) => existsSync(join(root, path)));
373
+ const report = (over) => ({
374
+ root,
375
+ branch,
376
+ recordsBranch,
377
+ remote,
378
+ base: { branch: baseBranch, sha: baseSha },
379
+ parent: { ref: parent.ref, sha: parent.sha },
380
+ reusedRecordsBranch,
381
+ rebuilt,
382
+ ...(rebuiltOn === undefined ? {} : { rebuiltOn }),
383
+ prCreated: false,
384
+ autoMerge: null,
385
+ autoMergeNote: null,
386
+ range,
387
+ committedHead: drift.committedHead,
388
+ workingHead: drift.workingHead,
389
+ staged: carried,
390
+ message,
391
+ commit: null,
392
+ pushed: false,
393
+ prUrl: null,
394
+ dryRun,
395
+ ...over,
396
+ });
397
+ if (range === null || dryRun)
398
+ return { ok: true, report: report({ staged: range === null ? [] : carried }) };
399
+ // 5. Build the commit on the remote's tip, in a scratch index. Exactly those
400
+ // paths, laid over origin's tree; the operator's index is never touched
401
+ // and nothing is checked out.
402
+ progress.phase(`building the records commit on ${parent.ref} ${parent.sha.slice(0, 12)} (nothing is checked out)`);
403
+ const built = commitOnBase(root, {
404
+ base: parent.sha,
405
+ paths: carried,
406
+ message,
407
+ // APRV-233. The log goes in as the blob taken under the append lock, not as
408
+ // whatever the file holds now: the lock was released before the fetch, and
409
+ // a record appended since is a record this commit's message does not claim.
410
+ blobs: carried.includes(DEFAULT_LOG_PATH)
411
+ ? [{ path: repoPath(root, logPath), sha: snapshot.blob }]
412
+ : [],
413
+ });
414
+ if (!built.ok) {
415
+ return {
416
+ ok: false,
417
+ code: "log-advance-git-failed",
418
+ message: `${built.message}; nothing was committed and the checkout is untouched.`,
419
+ quote: built.quote,
420
+ };
421
+ }
422
+ if (built.unchanged) {
423
+ // The chains said records were owed and the trees say otherwise, which
424
+ // means the log file on the remote already carries these bytes under a
425
+ // different commit. Nothing to publish, and an empty commit would say
426
+ // otherwise.
427
+ return { ok: true, report: report({ commit: null, staged: [] }) };
428
+ }
429
+ const commit = built.sha;
430
+ // 6. Anchor the commit before pushing, so a rejected push loses nothing. A
431
+ // ref under `refs/approval/` rather than a branch: it keeps the object
432
+ // reachable without adding a branch nobody asked for, and it moves no HEAD.
433
+ const anchor = `refs/approval/advance/${recordsBranch}`;
434
+ git(["update-ref", anchor, commit], root);
435
+ // 7. Push THAT commit to the records branch, by refspec. A sha on the left
436
+ // moves no local ref and checks nothing out: the operator's branch stays
437
+ // exactly where they left it.
438
+ progress.phase(`pushing ${commit.slice(0, 12)} to ${remote} ${recordsBranch}`);
439
+ // A rebuilt commit does not descend from the branch it replaces, so it needs
440
+ // the `+`. It is not a history rewrite in the sense the rule forbids: the
441
+ // branch is a proposal nobody has merged, every record it carried is in the
442
+ // new commit's log, and the alternative is a pull request that cannot land.
443
+ const refspec = `${rebuilt ? "+" : ""}${commit}:refs/heads/${recordsBranch}`;
444
+ let target = recordsBranch;
445
+ let fallbackFrom = null;
446
+ let pushed = git(["push", remote, refspec], root);
447
+ if (!pushed.ok && rebuilt) {
448
+ // The branch cannot be updated in place: a protected-branch ruleset
449
+ // (GH006), or a pull request the merge queue has already taken. The records
450
+ // still have to reach a branch somebody can merge, so they get a fresh one
451
+ // and the report says which (APRV-234).
452
+ const rejection = failureText(pushed);
453
+ for (let n = 2; n <= 9 && !pushed.ok; n += 1) {
454
+ const alternate = `${recordsBranch}-${String(n)}`;
455
+ const attempt = git(["push", remote, `${commit}:refs/heads/${alternate}`], root);
456
+ if (attempt.ok) {
457
+ pushed = attempt;
458
+ fallbackFrom = recordsBranch;
459
+ target = alternate;
460
+ git(["update-ref", `refs/approval/advance/${alternate}`, commit], root);
461
+ progress.phase(`${recordsBranch} would not take the rebuilt commit (${rejection}); opened ${alternate} instead`);
462
+ }
463
+ }
464
+ }
465
+ if (!pushed.ok) {
466
+ return {
467
+ ok: false,
468
+ code: "log-advance-push-rejected",
469
+ message: `the advance is built on ${parent.ref} as ${commit.slice(0, 12)} and held at ${anchor}, but \`git push ${remote} ${refspec}\` was REJECTED: ${failureText(pushed)}. The commit exists and nothing was lost; push it when the remote will take it (\`git push ${remote} ${refspec}\`). Your checkout is exactly as you left it, on ${branch}.`,
470
+ quote: outputLines(pushed.stderr, pushed.stdout),
471
+ };
472
+ }
473
+ if (options.pr !== true) {
474
+ progress.done();
475
+ return {
476
+ ok: true,
477
+ report: report({ commit, pushed: true, recordsBranch: target, fallbackFrom }),
478
+ };
479
+ }
480
+ progress.phase(`opening or updating the pull request for ${target}`);
481
+ const pr = ghPullRequest(root, target, range);
482
+ if (!pr.ok) {
483
+ progress.done();
484
+ return {
485
+ ok: false,
486
+ code: "log-advance-pr-failed",
487
+ message: `the advance is committed and pushed to ${target}, but \`gh pr ${pr.step}\` failed: ${pr.message}. Open the pull request by hand and merge it with a merge commit.`,
488
+ };
489
+ }
490
+ // APRV-284. The arm, and never a refusal of the verb: the records are
491
+ // committed, pushed and open as a pull request whatever `gh` says next.
492
+ const arm = options.autoMerge === false
493
+ ? { state: "off", note: null }
494
+ : armAutoMerge(root, target, { baseSha, commit });
495
+ if (arm.state === "armed") {
496
+ progress.phase(`auto-merge armed on ${target}`);
497
+ }
498
+ else if (arm.note !== null) {
499
+ progress.phase(`auto-merge was not armed on ${target}: ${arm.note}`);
500
+ }
501
+ progress.done();
502
+ return {
503
+ ok: true,
504
+ report: report({
505
+ commit,
506
+ pushed: true,
507
+ recordsBranch: target,
508
+ fallbackFrom,
509
+ prUrl: pr.url,
510
+ prCreated: pr.created,
511
+ autoMerge: arm.state,
512
+ autoMergeNote: arm.note,
513
+ }),
514
+ };
515
+ }
516
+ /**
517
+ * The chain head of one git rev's copy of the log, or `null`.
518
+ *
519
+ * `null` covers three cases that mean the same thing here: no such rev, no such
520
+ * blob, and a copy that is not this chain (a fork, or a chain somehow ahead of
521
+ * the working log). None is evidence that any working record has been
522
+ * published, and treating any of them as evidence would UNDER-count what is
523
+ * owed, which is the direction that loses records.
524
+ */
525
+ function publishedHeadAt(root, logPath, workingText, rev) {
526
+ const blob = showBlob(root, rev, repoPath(root, logPath));
527
+ if (blob === null)
528
+ return null;
529
+ const compared = compareChains({ label: "the working log", text: workingText }, { label: rev, text: blob.toString("utf8") });
530
+ if (!compared.ok)
531
+ return null;
532
+ if (compared.drift.relation === "diverged" || compared.drift.relation === "behind")
533
+ return null;
534
+ return compared.drift.committedHead?.seq ?? 0;
535
+ }
536
+ /**
537
+ * How much of the working log is already on a records branch or the trunk.
538
+ *
539
+ * Reads git's object store and NEVER the network: this is asked on every daemon
540
+ * tick and by `approval doctor`, and a status question must not depend on a
541
+ * remote being reachable. The revs consulted are the local advance anchors
542
+ * (`refs/approval/advance/*`, which {@link logAdvance} writes before it pushes),
543
+ * the remote-tracking refs for the base branch and the day's records branch, and
544
+ * `HEAD`. The highest head among the copies that are a PREFIX of this chain
545
+ * wins; anything else is ignored, which can only make a caller advance less
546
+ * eagerly.
547
+ *
548
+ * It lives here rather than beside the daemon's cadence because `approval
549
+ * doctor` reports the same number and a CLI module may not import the daemon.
550
+ */
551
+ export function publishedState(root, logPath, records, where, today) {
552
+ const workingSeq = records.length === 0 ? 0 : (records[records.length - 1]?.seq ?? 0);
553
+ let workingText;
554
+ try {
555
+ workingText = readFileSync(logPath, "utf8");
556
+ }
557
+ catch {
558
+ workingText = "";
559
+ }
560
+ const revs = anchorRevs(root, { remote: where.remote, base: where.base, today });
561
+ let publishedSeq = 0;
562
+ let publishedRev = null;
563
+ for (const rev of revs) {
564
+ const head = publishedHeadAt(root, logPath, workingText, rev);
565
+ if (head !== null && head > publishedSeq) {
566
+ publishedSeq = head;
567
+ publishedRev = rev;
568
+ }
569
+ }
570
+ const unpublished = records.filter((record) => record.seq > publishedSeq);
571
+ const substantive = unpublished.filter((record) => !isAdvanceBookkeeping(record));
572
+ return {
573
+ publishedSeq,
574
+ workingSeq,
575
+ pending: unpublished.length,
576
+ substantive: substantive.length,
577
+ substantiveSeq: substantive[substantive.length - 1]?.seq ?? publishedSeq,
578
+ publishedRev,
579
+ revs,
580
+ };
581
+ }
582
+ /** The working log's text, or the empty string when there is no file. */
583
+ function textOfWorking(path) {
584
+ try {
585
+ return readFileSync(path, "utf8");
586
+ }
587
+ catch (cause) {
588
+ if (cause.code === "ENOENT")
589
+ return "";
590
+ throw new Error(`log ${path} could not be read: ${detail(cause)}`);
591
+ }
592
+ }
593
+ /** The canonical commit subject. It names the seq range, so the log is findable. */
594
+ export function advanceMessage(range, branch) {
595
+ const span = range.from === range.to ? `seq ${String(range.from)}` : `seq ${String(range.from)}..${String(range.to)}`;
596
+ return `Log advance: ${span} (${branch})`;
597
+ }
598
+ /** The first `http…` line of a `gh` run's output, which is how `gh` names a PR. */
599
+ function urlOf(text) {
600
+ return (text
601
+ .split("\n")
602
+ .map((line) => line.trim())
603
+ .filter((line) => line.startsWith("http"))
604
+ .pop() ?? null);
605
+ }
606
+ /**
607
+ * The day's pull request for the records branch: opened once, updated after.
608
+ *
609
+ * ONE PR PER DAY, not one per advance (APRV-204). The daemon advances on a
610
+ * cadence, so `gh pr create` on every run would either fail (a PR for the head
611
+ * branch already exists) or, on a remote that allows it, litter the repository
612
+ * with a pull request per tick. The push has already updated the branch by the
613
+ * time this runs, so an open pull request for that head IS the updated one and
614
+ * this function's whole job is to notice it.
615
+ *
616
+ * `gh pr list` is a read (`read.vcs.remote` in this project's own taxonomy) and
617
+ * asks nothing of the remote's state.
618
+ */
619
+ function ghPullRequest(root, recordsBranch, range) {
620
+ const listed = gh(["pr", "list", "--head", recordsBranch, "--state", "open", "--json", "url"], root);
621
+ if (!listed.ok) {
622
+ return { ok: false, step: "list", message: failureText(listed) };
623
+ }
624
+ const open = parsePrList(listed.stdout);
625
+ if (open.length > 0) {
626
+ return { ok: true, url: open[0] ?? null, created: false };
627
+ }
628
+ const title = `Log advance: ${range.from === range.to ? `seq ${String(range.from)}` : `seq ${String(range.from)}..${String(range.to)}`}`;
629
+ const body = "This branch carries the append-only log advance, its queue projection, and the payload files the records reference — one commit per advance, and nothing else. " +
630
+ "Merge with a MERGE COMMIT. Nothing else may ride this branch: a log commit alongside other work is what the one-commit rule forbids.";
631
+ const created = gh(["pr", "create", "--title", title, "--body", body, "--head", recordsBranch], root);
632
+ if (!created.ok) {
633
+ return { ok: false, step: "create", message: failureText(created) };
634
+ }
635
+ return { ok: true, url: urlOf(created.stdout), created: true };
636
+ }
637
+ /**
638
+ * Arm auto-merge on the day's records pull request (APRV-284).
639
+ *
640
+ * ## Why a records pull request may arm its own merge
641
+ *
642
+ * A records commit carries EXACTLY the log, `QUEUE.md` and
643
+ * `.approval/payloads/` (the `log-advance-dirty-stage` refusal is the other
644
+ * half of that rule), those three are the paths CI's protected-path guard
645
+ * exempts, and there is nothing in them a reviewer reviews: the log is
646
+ * append-only evidence and the queue is a projection of it. So a records pull
647
+ * request is the one shape in this repository that never needs a human to read
648
+ * it, and leaving it at CLEAN until somebody clicks is exactly the failure mode
649
+ * CLAUDE.md's workflow item 7 exists to remove.
650
+ *
651
+ * ## What the arm is, in this project's own taxonomy
652
+ *
653
+ * `gh pr merge <branch> --merge --auto` — the same command with the same class
654
+ * (`vcs.push.main`, `core/command-class.ts`'s `gh-pr-merge` row) a session runs
655
+ * to arm its own pull request. Nothing here mints a class, claims an exemption,
656
+ * or asks a second question: this runs inside whatever authorized the advance,
657
+ * which for the daemon is the `log.advance` grant APRV-204 opened and for a
658
+ * session is the harness hook's answer on the verb.
659
+ *
660
+ * ## The guard, and why the arm is not unconditional
661
+ *
662
+ * The reasoning above holds only while the branch really does carry nothing
663
+ * else. It is a branch on a shared remote, so a commit this verb did not make
664
+ * can be sitting on it, and the advance would fast-forward over that commit and
665
+ * then arm a merge for it. So the arm is preceded by a check of the pushed
666
+ * branch against the base it is measured from, and anything outside
667
+ * {@link ADVANCE_PATHS} WITHHOLDS the arm and names what it saw. Everything
668
+ * ambiguous withholds too: an unreadable diff, a base that could not be
669
+ * resolved. A withheld arm costs a human one click; an unconditional one would
670
+ * land whatever rode the branch.
671
+ *
672
+ * ## An arm `gh` says no to is not a failure
673
+ *
674
+ * Auto-merge disabled on the repository, a merge queue, a pull request already
675
+ * mergeable: `gh` refuses all three, and the advance is committed, pushed and
676
+ * open as a pull request regardless. The refusal is reported and the verb still
677
+ * succeeds — the same rule `cli/amend.ts`'s ceremony arm has followed since
678
+ * APRV-130.
679
+ */
680
+ function armAutoMerge(root, recordsBranch, span) {
681
+ const carried = branchDiffPaths(root, span.baseSha, span.commit);
682
+ if (carried === null) {
683
+ return {
684
+ state: "withheld",
685
+ note: `\`git diff --name-only ${span.baseSha.slice(0, 12)} ${span.commit.slice(0, 12)}\` could not be read, so what ${recordsBranch} carries is unknown`,
686
+ };
687
+ }
688
+ const foreign = carried.filter((path) => !isAdvancePath(path));
689
+ if (foreign.length > 0) {
690
+ return {
691
+ state: "withheld",
692
+ note: `${recordsBranch} carries paths an advance does not: ${foreign.join(", ")}. A records pull request arms its own merge because it carries only evidence; this one does not, so it wants a reviewer`,
693
+ };
694
+ }
695
+ const merged = gh(["pr", "merge", recordsBranch, "--merge", "--auto"], root);
696
+ if (merged.ok)
697
+ return { state: "armed", note: null };
698
+ return {
699
+ state: "refused",
700
+ note: outputLines(merged.stderr, merged.stdout)[0] ?? failureText(merged),
701
+ };
702
+ }
703
+ /**
704
+ * Every path `from..to` touches, or `null` when git would not say.
705
+ *
706
+ * `null` is not "nothing changed": an empty diff is `[]`, and the two must not
707
+ * collapse, because the caller treats the unknown as a reason to withhold and
708
+ * the empty as a branch carrying nothing foreign.
709
+ */
710
+ function branchDiffPaths(root, from, to) {
711
+ if (from.length === 0 || to.length === 0)
712
+ return null;
713
+ const diff = git(["diff", "--name-only", `${from}..${to}`], root);
714
+ if (!diff.ok)
715
+ return null;
716
+ return diff.stdout
717
+ .split("\n")
718
+ .map((line) => line.trim())
719
+ .filter((line) => line.length > 0);
720
+ }
721
+ /**
722
+ * The urls in `gh pr list --json url` output.
723
+ *
724
+ * Unparseable output answers "no open pull request", which is the conservative
725
+ * reading in exactly one direction: the verb then tries to create one, and a
726
+ * `gh pr create` that finds an existing PR fails loudly with the reason. The
727
+ * opposite default would silently skip opening the day's pull request.
728
+ */
729
+ function parsePrList(text) {
730
+ let parsed;
731
+ try {
732
+ parsed = JSON.parse(text.trim().length === 0 ? "[]" : text);
733
+ }
734
+ catch {
735
+ return [];
736
+ }
737
+ if (!Array.isArray(parsed))
738
+ return [];
739
+ const urls = [];
740
+ for (const entry of parsed) {
741
+ if (typeof entry !== "object" || entry === null)
742
+ continue;
743
+ const url = entry["url"];
744
+ if (typeof url === "string" && url.length > 0)
745
+ urls.push(url);
746
+ }
747
+ return urls;
748
+ }
749
+ //# sourceMappingURL=log-advance.js.map