approval-md 0.4.0 → 0.4.1

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 (107) hide show
  1. package/SPEC.md +3 -0
  2. package/cli.js +72 -4
  3. package/dist/src/channels/contract.d.ts +53 -0
  4. package/dist/src/channels/contract.js +56 -0
  5. package/dist/src/channels/contract.js.map +1 -1
  6. package/dist/src/channels/tagging.d.ts +25 -0
  7. package/dist/src/channels/tagging.js +57 -5
  8. package/dist/src/channels/tagging.js.map +1 -1
  9. package/dist/src/channels/telegram-minimal.d.ts +251 -0
  10. package/dist/src/channels/telegram-minimal.js +618 -0
  11. package/dist/src/channels/telegram-minimal.js.map +1 -0
  12. package/dist/src/channels/telegram.d.ts +454 -19
  13. package/dist/src/channels/telegram.js +931 -80
  14. package/dist/src/channels/telegram.js.map +1 -1
  15. package/dist/src/cli/amend.js +8 -1
  16. package/dist/src/cli/amend.js.map +1 -1
  17. package/dist/src/cli/audit-card.d.ts +1 -1
  18. package/dist/src/cli/audit-card.js +40 -1
  19. package/dist/src/cli/audit-card.js.map +1 -1
  20. package/dist/src/cli/audit.d.ts +7 -5
  21. package/dist/src/cli/audit.js +22 -10
  22. package/dist/src/cli/audit.js.map +1 -1
  23. package/dist/src/cli/channel-telegram-webhook.d.ts +2 -0
  24. package/dist/src/cli/channel-telegram-webhook.js +7 -0
  25. package/dist/src/cli/channel-telegram-webhook.js.map +1 -1
  26. package/dist/src/cli/channel-telegram.d.ts +75 -2
  27. package/dist/src/cli/channel-telegram.js +170 -15
  28. package/dist/src/cli/channel-telegram.js.map +1 -1
  29. package/dist/src/cli/execute.d.ts +12 -0
  30. package/dist/src/cli/execute.js +46 -1
  31. package/dist/src/cli/execute.js.map +1 -1
  32. package/dist/src/cli/help.d.ts +1 -0
  33. package/dist/src/cli/help.js +39 -18
  34. package/dist/src/cli/help.js.map +1 -1
  35. package/dist/src/cli/hook.d.ts +114 -3
  36. package/dist/src/cli/hook.js +468 -50
  37. package/dist/src/cli/hook.js.map +1 -1
  38. package/dist/src/cli/log-unlock.d.ts +23 -0
  39. package/dist/src/cli/log-unlock.js +109 -0
  40. package/dist/src/cli/log-unlock.js.map +1 -0
  41. package/dist/src/cli/main.js +23 -6
  42. package/dist/src/cli/main.js.map +1 -1
  43. package/dist/src/cli/up.js +5 -0
  44. package/dist/src/cli/up.js.map +1 -1
  45. package/dist/src/cli/verb-registry.js +1 -0
  46. package/dist/src/cli/verb-registry.js.map +1 -1
  47. package/dist/src/cli/wordmark.d.ts +2 -2
  48. package/dist/src/cli/wordmark.js +2 -2
  49. package/dist/src/core/audit.d.ts +98 -9
  50. package/dist/src/core/audit.js +363 -15
  51. package/dist/src/core/audit.js.map +1 -1
  52. package/dist/src/core/command-class.js +6 -0
  53. package/dist/src/core/command-class.js.map +1 -1
  54. package/dist/src/core/gate.d.ts +58 -1
  55. package/dist/src/core/gate.js +30 -3
  56. package/dist/src/core/gate.js.map +1 -1
  57. package/dist/src/core/log-lock.d.ts +435 -0
  58. package/dist/src/core/log-lock.js +1093 -0
  59. package/dist/src/core/log-lock.js.map +1 -0
  60. package/dist/src/core/log.d.ts +82 -3
  61. package/dist/src/core/log.js +285 -40
  62. package/dist/src/core/log.js.map +1 -1
  63. package/dist/src/core/policy-proposal.d.ts +17 -0
  64. package/dist/src/core/policy-proposal.js +29 -14
  65. package/dist/src/core/policy-proposal.js.map +1 -1
  66. package/dist/src/core/prompt-layout.d.ts +186 -1
  67. package/dist/src/core/prompt-layout.js +345 -3
  68. package/dist/src/core/prompt-layout.js.map +1 -1
  69. package/dist/src/core/sender-identity.d.ts +24 -1
  70. package/dist/src/core/sender-identity.js +31 -0
  71. package/dist/src/core/sender-identity.js.map +1 -1
  72. package/dist/src/core/validate.d.ts +20 -6
  73. package/dist/src/core/validate.js +0 -0
  74. package/dist/src/core/validate.js.map +1 -1
  75. package/dist/src/daemon/git-evidence.d.ts +2 -2
  76. package/dist/src/daemon/git-evidence.js +1 -1
  77. package/dist/src/serve/archive.d.ts +8 -0
  78. package/dist/src/serve/archive.js +11 -2
  79. package/dist/src/serve/archive.js.map +1 -1
  80. package/docs/README-extended.md +2 -0
  81. package/docs/cli-reference.md +507 -25
  82. package/package.json +2 -2
  83. package/schema/event.schema.json +354 -28
  84. package/schema/fixtures/event/invalid/audit-lock-reclaimed-age-without-created.json +19 -0
  85. package/schema/fixtures/event/invalid/audit-lock-reclaimed-agent-actor.json +14 -0
  86. package/schema/fixtures/event/invalid/audit-lock-reclaimed-lockfile-path.json +14 -0
  87. package/schema/fixtures/event/invalid/audit-lock-reclaimed-operator-cleared-system-actor.json +14 -0
  88. package/schema/fixtures/event/invalid/audit-lock-reclaimed-unknown-reason.json +14 -0
  89. package/schema/fixtures/event/invalid/audit-lock-reclaimed-without-age.json +13 -0
  90. package/schema/fixtures/event/invalid/audit-reviewed-agent-actor.json +6 -1
  91. package/schema/fixtures/event/invalid/audit-reviewed-bad-payload-hash.json +21 -0
  92. package/schema/fixtures/event/invalid/audit-reviewed-loved-no-note.json +5 -0
  93. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-hash.json +19 -0
  94. package/schema/fixtures/event/invalid/audit-reviewed-no-subject-seq.json +19 -0
  95. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict-source.json +19 -0
  96. package/schema/fixtures/event/invalid/audit-reviewed-no-verdict.json +19 -0
  97. package/schema/fixtures/event/invalid/audit-reviewed-system-actor.json +6 -1
  98. package/schema/fixtures/event/invalid/audit-sampled-no-policy-hash.json +14 -0
  99. package/schema/fixtures/event/valid/audit-lock-reclaimed-implausible-created.json +18 -0
  100. package/schema/fixtures/event/valid/audit-lock-reclaimed-legacy.json +14 -0
  101. package/schema/fixtures/event/valid/audit-lock-reclaimed-unlock.json +19 -0
  102. package/schema/fixtures/event/valid/audit-lock-reclaimed.json +19 -0
  103. package/schema/fixtures/event/valid/audit-reviewed-reaction.json +5 -0
  104. package/schema/fixtures/event/valid/audit-reviewed.json +6 -1
  105. package/schema/fixtures/event/valid/audit-sampled.json +2 -1
  106. package/schema/fixtures/event/valid/daemon-identity.json +2 -1
  107. package/schema/policy.schema.json +52 -0
@@ -311,6 +311,64 @@ that is due to a refusal of anything: it is a warning on `log verify`, a
311
311
  doctor`'s `checkpoint` row. A gate that held up an action for want of a tap is a
312
312
  gate whose operator turns the check off.
313
313
 
314
+ ## log unlock
315
+
316
+ Takes over `events.jsonl.lock` from a writer that died holding it, when no
317
+ writer could do that itself, and records it.
318
+
319
+ ```
320
+ approval log unlock --pid <n|none> --as human:<id> [--log <path>] [--json]
321
+ ```
322
+
323
+ **Why a person.** Every writer that finds the lock held waits out its whole
324
+ lock timeout (two seconds) and then judges the holder once. It takes the lock
325
+ back by itself only when it can prove the holder gone: the holder ran in this
326
+ writer's own pid namespace and boot, and `kill(pid, 0)` answers ESRCH (on Linux
327
+ also with `/proc/<pid>` absent or a zombie, or the pid now names a process that
328
+ started later), or the lockfile names no holder, is ten minutes old, and sat
329
+ unchanged through the writer's whole wait (a try-once caller never takes it). Any
330
+ other holder (another container, another boot, a pid answering EPERM, a `/proc`
331
+ it cannot read) is never taken automatically, because a wrong reclaim forks the
332
+ chain. The writer's `lock-timeout` refusal then names the pid and this command.
333
+
334
+ **What it does.** It reads the lockfile strictly, refuses a `--pid` that is not
335
+ the one the record names (`none` for a lockfile with no holder record), refuses
336
+ a holder it sees running (Linux: the same start time in this namespace;
337
+ elsewhere: the pid on this host in this boot), and refuses beside a `log sync`
338
+ snapshot. It then takes the lock exactly as a writer's reclaim does (an
339
+ exclusive `link(2)` to `<lock>.stale.<pid>.<created ms>`, then a `rename(2)` of
340
+ its own lockfile over the lock's path) and appends `audit.lock_reclaimed` as the
341
+ first record under it, with your `human:` actor, reason `operator-cleared` (your
342
+ word, which the log never presents as a writer's proof), the strictly parsed
343
+ holder and the lock's age. A holder `created` the log cannot vouch for (more
344
+ than a day before the log's last record, or more than five minutes ahead of
345
+ this clock) is recorded as `null`, with no age. A stale name a dead reclaimer or
346
+ anyone else left is cleared first, and a dead reclaimer's own
347
+ `<lock>.take.<pid>.*` file with it. Anything at the lock's path that is not a
348
+ lockfile (a FIFO, a link, a malformed file) is not taken: the refusal says to
349
+ `rm -v` it.
350
+
351
+ It also refuses, touching nothing:
352
+
353
+ - while a reclaim is in flight: a writer writes its own
354
+ `<lock>.take.<pid>.<nonce>` before it claims, and a running one is named, so
355
+ a reclaimer that is stopped (SIGSTOP, a debugger) is never unlocked around;
356
+ - when the record could not be appended: a daemon whose id the attested
357
+ policy refuses, or a torn tail (repair the tail first);
358
+ - when the claim is refused EPERM or EACCES (Linux `fs.protected_hardlinks`:
359
+ the lockfile is another user's): the refusal names the owner's uid; run it as
360
+ that user or root, or `rm -v` the lockfile once no writer runs;
361
+ - when a directory sits at the stale name: the refusal names `rm -r` for it.
362
+
363
+ **Run it only when no writer is running.** The verb asserts, on your word, that
364
+ the holder is gone. Human-only: the actor comes from `--as` or `APPROVAL_HUMAN`,
365
+ the schema refuses an `agent:` actor on the record, and `core/command-class.ts`
366
+ classifies `approval log unlock` (and `node <cli> log unlock`) `policy.core`, so the harness hook denies an agent
367
+ that tries it that way. Other spellings (`npx approval log unlock`, `npm exec`,
368
+ a wrapper script) classify as workspace writes, and `--as` is the caller's to
369
+ set, so the classification is a speed bump rather than the boundary (APRV-491).
370
+ Exit 0 when it unlocked or there was no lock; 4 when it refused.
371
+
314
372
  ## log follow
315
373
 
316
374
  `approval log follow --from <seq> --json` is the channel-independent decision
@@ -3538,20 +3596,113 @@ carries in the open.
3538
3596
 
3539
3597
  ## audit review
3540
3598
 
3599
+ **The reviewer must be on the class's roster (APRV-483).** Where the policy rule
3600
+ that resolves the sampled action's class names `approvers`, the reviewer must be
3601
+ one of them, compared exactly as a grant's approver is (`namesApprover` in
3602
+ `core/gate.ts`): a bare id on the roster matches `human:<id>`. Anyone else is
3603
+ refused `actor-not-approver` (exit 1) and nothing is appended. This binds every
3604
+ surface, because the check is in `reviewSample` itself: `--as human:<id>` at a
3605
+ terminal and a sender-mapped tap on a Telegram card are held to the same list.
3606
+ A rule that names no `approvers` restricts nobody, exactly as for grants. The
3607
+ class comes from the action's registration (a sample naming no class is refused
3608
+ `actor-not-approver`, never read as "no roster"), and the roster is read from
3609
+ the policy file a grant would read, only when its bytes are the latest
3610
+ attestation's: an unattested, edited, unreadable or `--policy`-substituted file
3611
+ is refused `policy-not-attested` (exit 1), because a roster read from a file
3612
+ nobody attested is one the reviewer could have chosen. Attested bytes that do not
3613
+ load (a schema-invalid rule, a YAML error) are refused `policy-invalid` (exit 1):
3614
+ `policy attest` hashes bytes without parsing them, and the fail-closed reading
3615
+ of a broken policy names no roster, which for a review would mean anyone. Nothing
3616
+ resolves from a broken file, so an on-roster reviewer is refused too; the repair
3617
+ is a corrected policy, attested.
3618
+
3619
+ **The roster is the one in force when the action ran (PR #614 refutation F2,
3620
+ recheck NF-3).** Every `audit.sampled` the runtime writes pins the attested
3621
+ policy the sampled execution ran under, as `payload.policy_sha256`: the
3622
+ `sha256` named by the latest `policy.updated` attestation before the
3623
+ `execution.started` record, which is the policy the gate checked when it
3624
+ recorded the start (a harness start carries the same hash as its own
3625
+ `policy_sha256`). A policy attested between the run and the sweep therefore
3626
+ changes neither who may review the sample nor whether anyone can. Only an
3627
+ execution older than every attestation in the log pins the latest attestation
3628
+ at sampling instead. A review of that sample reads the
3629
+ roster only from policy bytes that hash to the pin, and only when an
3630
+ attestation before the sample names that hash. The bytes come from the payload
3631
+ store beside the log, where every attestation since APRV-356 keeps the text it
3632
+ attested (PR #614 fix round 3), so editing and re-attesting the policy, a
3633
+ settings save from the app included, leaves older open samples reviewable. The
3634
+ policy file on disk is the fallback, used only when its bytes hash to the pin
3635
+ (a chain last attested before APRV-356 stored none). Bytes in neither place are
3636
+ refused `policy-not-attested`, and the message names the hash it needs. A later
3637
+ re-attestation that renames or drops the class's rule therefore cannot leave
3638
+ the sample with no roster, and one that names a different roster cannot open
3639
+ it to a reviewer the action's policy did not name. A class that matches no rule in the pinned policy
3640
+ (it reached supervised through `defaults.autonomy`) is refused
3641
+ `actor-not-approver` with that reason, because the defaults carry no roster
3642
+ and for a review "no roster" would mean anyone. A sample written before samples
3643
+ pinned a policy has no `policy_sha256` and keeps the latest-attestation reading
3644
+ above.
3645
+
3646
+ Exactly one of `--ok` and `--deny` is required (APRV-482). A bare review, or one
3647
+ carrying only `--reaction`, is refused `verdict-required` (exit 1) and writes
3648
+ nothing: under supervised-retro the review counts as the approval, and an
3649
+ approval is something the reviewer says, never a default. Both flags together
3650
+ is a usage error (exit 2). The verdict is judged after the roster (PR #614
3651
+ refutation N6): a reviewer off the roster is told `actor-not-approver` whether
3652
+ or not they gave a verdict, and a grade with no verdict is not judged for its
3653
+ note until the verdict comes.
3654
+
3541
3655
  `--note` is optional — unlike `execution resolve`, this event records only that a
3542
3656
  person looked, and the runtime is not relying on the note for a fact it does not
3543
3657
  otherwise have. Human-only: a runtime that could mark its own samples reviewed
3544
3658
  would be a supervision backlog that empties itself.
3545
3659
 
3546
- No attestation is required, for the reason `execution resolve` states: review
3547
- records an observation, exercises no policy authority, authorizes nothing, and
3548
- spends no budget. A review blocked because a policy file was edited afterwards
3549
- would be a supervision backlog held open by an unrelated fact.
3550
-
3551
- What it appends is `audit.reviewed`, naming the sample's action key and task, with
3552
- payload `{"subject_seq":<seq of the audit.sampled>,"reviewed":true,"note"?:"...",
3553
- "reaction"?:"disliked"|"indifferent"|"liked"|"loved"}`.
3554
- An action key with several open samples refuses `ambiguous-subject`.
3660
+ Since APRV-483 a review needs the attested policy, because its roster check
3661
+ reads the class's `approvers` from it (below). Before that no attestation was
3662
+ required, for the reason `execution resolve` states: review recorded an
3663
+ observation and exercised no policy authority. Under supervised-retro the
3664
+ review is the approval, and the trade is stated plainly: a policy edited and not
3665
+ yet re-attested now holds the review backlog open until a human re-attests.
3666
+
3667
+ What it appends is `audit.reviewed`, naming the sample's action key and task; its
3668
+ payload is listed field by field under [the review record](#the-review-record),
3669
+ which is the one place those names are defined. An action key with several open
3670
+ samples refuses `ambiguous-subject`.
3671
+
3672
+ ### The review record
3673
+
3674
+ The field names a follower reads (the data pipeline's follower and the dbt
3675
+ models built on it read reviews as approvals under supervised-retro). This
3676
+ table is the single definition; other docs link here rather than restating it.
3677
+ Every `audit.reviewed` written since APRV-481 carries the three required fields,
3678
+ and since PR #614's F5 fix a fourth, `verdict_source`; the write boundary refuses
3679
+ one that does not. The verdict alone cannot say whether the reviewer gave it:
3680
+ before APRV-482 the runtime wrote `ok` for a grade-only tap or a bare
3681
+ `audit review`, and such a record may carry all three APRV-481 fields, so
3682
+ `verdict_source` is the field that separates the two. A review written before then
3683
+ may lack them and still verifies (the read boundary is the schema's
3684
+ `audit_reviewed_record_historical`), so a follower treats their absence on an
3685
+ old record as "not recorded", never as a default.
3686
+
3687
+ | Field | Where | Required | Meaning |
3688
+ | --- | --- | --- | --- |
3689
+ | `event` | record | yes | `audit.reviewed`. |
3690
+ | `seq`, `ts`, `hash` | record | yes | The review's own chain position and runtime-assigned time. |
3691
+ | `actor` | record | yes | `human:<id>`, the reviewer. Since APRV-483 a person on the class's `approvers` roster wherever the rule names one. |
3692
+ | `action_key`, `task` | record | when the sample named them | The reviewed action and its task. |
3693
+ | `channel` | record | no | The surface, when the writer recorded one. |
3694
+ | `payload.subject_seq` | payload | yes (APRV-481) | The `seq` of the `audit.sampled` record this review answers. Not the execution's seq. |
3695
+ | `payload.sampled_subject_hash` | payload | yes (APRV-481) | The `hash` of the `execution.started` record the sample named: the join key from a review to the execution it judged. |
3696
+ | `payload.verdict` | payload | yes (APRV-481) | `ok` or `denied`. The enforcement field. Explicit only on records that carry `verdict_source`. |
3697
+ | `payload.verdict_source` | payload | yes (PR #614 F5) | Always `explicit`: the reviewer said the verdict (`--ok`/`--deny`, a card's OK or second Deny). The discriminator for a follower: a review WITHOUT it predates APRV-482 or was written by an older build, and its `ok` may have been defaulted for a grade-only tap or a bare terminal review. Count only reviews carrying it as explicit approvals. |
3698
+ | `payload.payload_hash` | payload | no | The SHA-256/JCS binding of the bytes the execution ran, present ONLY when the surface showed those bytes whole before the verdict (a Telegram card's `bytes` view). Absent when the reviewer saw a hash, nothing, or a terminal. Checked against the execution's binding before it is written. |
3699
+ | `payload.reaction` | payload | no | `disliked`, `indifferent`, `liked` or `loved`. Guidance, never enforcement; absent is absent. |
3700
+ | `payload.note` | payload | with `loved`/`disliked` | The reviewer's words, verbatim. |
3701
+ | `payload.sender`, `payload.sender_source` | payload | no | The authenticated account the review arrived from, when a transport authenticated one. |
3702
+ | `payload.subject_event`, `payload.reviewed` | payload | no | Constants (`audit.sampled`, `true`) kept for older readers. |
3703
+
3704
+ A denial is followed by a runtime-authored `reconciliation.required` whose
3705
+ `payload.review_seq` is the review's `seq`.
3555
3706
 
3556
3707
  **`--json`** (one object on stdout):
3557
3708
 
@@ -3920,6 +4071,224 @@ turn on was already on the `ChannelRequest`, and `--json`, `approval queue` and
3920
4071
  the web page carried it all along; rendering stays a pure function of
3921
4072
  (request, layout).
3922
4073
 
4074
+ ### The minimal card: `channels.telegram.prompt.style` (APRV-489)
4075
+
4076
+ The technical card is three messages written for an engineer. A resident
4077
+ approving on a phone needs to know what their agent wants to do, the exact
4078
+ words it will act on, how long they have, and where the buttons are.
4079
+ `style: minimal` sends that as ONE message, with the whole technical card
4080
+ collapsed inside it:
4081
+
4082
+ ```yaml
4083
+ channels:
4084
+ telegram:
4085
+ prompt:
4086
+ always: [ttl_remaining_ms]
4087
+ style: minimal # technical (the default) | minimal
4088
+ say:
4089
+ digest.share:
4090
+ does: "share a note about you with other people"
4091
+ quote: { scope: "Shared with", expires_at: "Until", text: "Note", digest_id: "Reference" }
4092
+ note: none
4093
+ ```
4094
+
4095
+ **For the approver, in plain words.** The bold first line says what your agent
4096
+ wants to do. The box under it holds the agent's own words, copied exactly from
4097
+ what will be posted, shared or run; approval.md writes nothing inside the box
4098
+ except the labels. If a line under the box starts with ⚠ or says "Not shown
4099
+ here", the box is not the whole story: open "Full details" before deciding. A
4100
+ line marked "not checked" is a description (by an AI model or by your agent)
4101
+ that nobody verified. The next line says how long you have; if you do nothing,
4102
+ nothing happens. Tap "Full details" to see everything the runtime checked,
4103
+ including the exact text this approval is bound to. Approve says yes; Deny says
4104
+ no. For anything that deletes, sends files or money, or talks to outside
4105
+ services, open "Full details" every time.
4106
+
4107
+ **The exact layout** (HTML; `[ ]` are the inline buttons):
4108
+
4109
+ ```
4110
+ <b>Your agent wants to <phrase></b>
4111
+ <blockquote><b>Label:</b> quoted value one payload value per line, verbatim
4112
+ …</blockquote>
4113
+ ⚠ This runs at least 3 commands. Only the beginning is shown above: … a command, only when cut
4114
+ ⚠ There is more than fits here: open Full details before deciding. when any other value was cut
4115
+ Not shown here: <key>, <key>. Open Full details before deciding. when the say entry leaves a field off (~)
4116
+ <i>AI summary (not checked):</i> … only when a model sentence is attached
4117
+ <i>Your agent says (not checked):</i> … only when the class's say entry has note: summary
4118
+ <i>Your agent estimates the cost (not checked):</i> $0.00 only when the estimate is above zero
4119
+ Open for about 3 days. If you don't answer, your agent will not do this.
4120
+ <blockquote expandable><b>Full details (tap to open)</b>
4121
+ … the technical card's three messages, in order, character for character …</blockquote>
4122
+ [✅ Approve] [✋ Deny]
4123
+ ```
4124
+
4125
+ A tool call the agent is blocked in says "Your agent is waiting: about 4
4126
+ minutes left. If you don't answer, it will not do this." instead. The time is
4127
+ the gate's own window for the request (the policy's TTL, narrowed by a
4128
+ harness hook's cap), from the verified log, as it stood when the card was
4129
+ drawn; the line is not refreshed afterwards. A request with no time left says
4130
+ "Time is up: this request has closed, and an answer now will not count."
4131
+
4132
+ **What is computed, what is quoted, what is described.**
4133
+
4134
+ - *Computed by the runtime:* the first line, chosen by the exact class the LOG
4135
+ records: core's own phrase for a class core emits (`network.call`,
4136
+ `read.web`, `browser.exec`, `cron.manage`, `process.write`, `skill.manage`,
4137
+ `agent.delegate`, `message.send`, `files.delete.scratch`); else, for a
4138
+ payload the runtime reads itself, its structural kind ("run a command",
4139
+ "change a file", "send an email", followed by `(type: <class>)`), whatever
4140
+ the class's `say` entry says; else, for an opaque payload only, the attested
4141
+ `say.<class>.does`. The notices under the box; the deadline line; everything
4142
+ in "Full details"; the buttons.
4143
+ - *Quoted from the bound payload:* the box, and only payload values. Each line
4144
+ is one payload value, verbatim (marked as below, and visibly cut when long),
4145
+ under a label (the operator's attested label, or the runtime's fixed label
4146
+ for a field of a shape it knows), or alone when it is the only quotation.
4147
+ Nothing computed, abbreviated, joined or paraphrased is ever inside the box;
4148
+ a list of addresses is one line per address. Each value sits inside the
4149
+ runtime's own Unicode isolate (U+2068 … U+2069), so right-to-left letters in
4150
+ it cannot reorder its label or the lines around it. The cut mark is drawn
4151
+ inside the value's isolate, so a cut right-to-left value shows it at its left
4152
+ edge, still visible; the notice under the box says the same thing outside
4153
+ any isolate. The operator's `does` phrase in the headline is not isolated.
4154
+ The bytes were hash-checked against the request's
4155
+ `payload_hash` before the channel saw them, so they are what will be acted
4156
+ on; their content was written by the agent. A command is shown up to 160
4157
+ characters (under `Command:` when its `cwd` is quoted as `In folder:`); a
4158
+ longer one is shown from its start and cut, and only then does a computed
4159
+ line under the box say so ("⚠ This runs at least 3 commands. Only the
4160
+ beginning is shown above", the classifier's count as a lower bound, since it
4161
+ does not see commands started inside other commands such as a
4162
+ here-document piped into `sh`, `$( … )`, `bash -c` or `eval`; "⚠ Only the
4163
+ beginning of this command is shown above" when it counts one; "⚠ More than
4164
+ one command may be here, and only the beginning is shown above" when it
4165
+ cannot count). A command shown whole gets no line: the
4166
+ card makes no claim about how many commands it is. The line never names or
4167
+ paraphrases a command the box does not show; the
4168
+ classifier's outline is in "Full details" only, because it leaves out flags
4169
+ and their values, which is where a deletion target or an uploaded file
4170
+ lives. A file change shows the file, the change and every other field under
4171
+ its own key name (`tool:`, `replace_all: true`). An email shows From, To, Cc,
4172
+ Bcc, Subject, Format and Message. An opaque payload shows the fields its
4173
+ `say.<class>.quote` map labels.
4174
+ - *Never a clean-looking partial:* when a command is cut, its computed count
4175
+ line says so; when any other value is cut, "⚠ There is more than fits here:
4176
+ open Full details before deciding." follows the box; when the declaration leaves fields off (`~`),
4177
+ "Not shown here: <their key names>. Open Full details before deciding." does.
4178
+ Both are outside the box, so quoted text can neither produce nor suppress
4179
+ them.
4180
+ - *Described, and labelled "not checked":* the AI summary and the agent's own
4181
+ summary and estimate. They never stand alone and never come first: they sit
4182
+ below the box.
4183
+
4184
+ **Quoted text is hostile input.** Every quoted value is one line (a line break
4185
+ is drawn ` ⏎ `). Control, format, bidirectional and every default-ignorable
4186
+ character (variation selectors, joiners, tag characters, U+034F, the Khmer and
4187
+ Mongolian invisibles), the Hangul fillers and the braille blank, and combining
4188
+ marks beyond two on one character are drawn as `«U+202E»`; so are `«` and `⏎`
4189
+ in the value, which makes the marking injective. One exception keeps emoji
4190
+ readable: a grapheme cluster that is WHOLLY a recognised emoji sequence
4191
+ (Unicode's RGI set, `\p{RGI_Emoji}` with the regular-expression `v` flag: ❤️,
4192
+ 👨‍👩‍👧, 1️⃣, 👍🏽) is drawn as itself. Every other presentation selector or joiner
4193
+ is marked, so a U+FE0F after a face that is already an emoji, or a U+200D
4194
+ between two pictographs that form no emoji, cannot carry hidden bits. On a
4195
+ runtime without that property nothing is exempt. Node 20, this package's
4196
+ floor, ships V8 11.3, which has it. A value longer than 280 characters is
4197
+ cut between grapheme clusters with `…(cut; see full details)`, and everything is
4198
+ HTML-escaped. A value cannot start a line of its own, close the box, open or
4199
+ close the collapsed block, or be bold.
4200
+
4201
+ Injective is not "never looks the same". Look-alike letters from other scripts
4202
+ (a Cyrillic а for a Latin a), a lone emoji with and without its presentation
4203
+ selector, and runs of ordinary spaces are drawn as themselves, so two different
4204
+ values can still look alike. Telegram clients also turn URLs, `@names`,
4205
+ `#tags` and `/commands` in the box and in "Full details" into links; a link's
4206
+ text is always its own target (markup in a value is escaped, so a value cannot
4207
+ make link text say something else), but a look-alike domain stays look-alike.
4208
+
4209
+ **When the technical card is sent instead.** The minimal card is never guessed
4210
+ at or drawn partially. These requests get today's technical card, the decision
4211
+ record says why (`payload.rendering.fallback`, below), and the listener says
4212
+ so on stderr:
4213
+
4214
+ - an attestation prompt, a `policy.*` or `log.*` class, or a protected path;
4215
+ - a truncated payload, or none;
4216
+ - any abnormal health fact (a budget over its ceiling, a policy not attested,
4217
+ an autonomy that is not `manual`), even when the row layout hides its row;
4218
+ - an opaque payload whose class has no `say` entry, carrying a key the entry's
4219
+ `quote` map does not name, or that is not an object;
4220
+ - a request delivered as part of a batch: a digest, its members, the collapsed
4221
+ stale-request summary, and a batch whose digest did not fit or has one member;
4222
+ - a card over 3800 characters (Telegram's limit is 4096, and a relay's
4223
+ `Agent: <name>` line needs room): the canonical rendering is never cut to fit;
4224
+ - a policy file that does not match its attestation (`policy-unattested`);
4225
+ - a minimal card the Bot API or a relay refuses (any 4xx but 429): the
4226
+ technical card is sent once instead (`send-refused`); a 429, a 5xx or a
4227
+ network failure is retried by the next cycle as before.
4228
+
4229
+ When the edit that settles a minimal card is refused (it replaces the only
4230
+ message holding the request), a short settle text with the outcome, the
4231
+ request's key and who decided when is sent instead, so a decided card never
4232
+ looks undecided.
4233
+
4234
+ Review cards ("REVIEW — THIS ALREADY RAN"), their note prompt and checkpoint
4235
+ prompts are the same under both styles in this release.
4236
+
4237
+ **`say`.** Keyed by EXACT class name: a pattern such as `files.*` is refused at
4238
+ load (`prompt-say-wildcard`), because one friendly phrase must never stand for
4239
+ several classes. For a class core phrases itself (the list above) core's phrase
4240
+ always wins: a `say` entry may set `quote` and `note` for it but not `does`
4241
+ (`prompt-say-builtin`). The same holds for a class the command classifier
4242
+ emits (`files.delete.out_of_scope`, `vcs.push.main`, `harness.launch.*`, …),
4243
+ whose payload is a command or a file change the runtime phrases itself
4244
+ (`prompt-say-kind`). For any other class `does` is a phrase for OPAQUE
4245
+ payloads only: when a request of that class carries a command, a file change
4246
+ or an email, the runtime's kind phrase is drawn instead and the record notes
4247
+ `say_does_ignored` (below). For those classes `does` is required and is a verb
4248
+ phrase of at most 120 characters that completes "Your agent wants to …": it
4249
+ starts with a letter and has no line break, `.` `!` `?` `…` `:` `;`, markup or
4250
+ `⚠` (`prompt-say-does`), so operator text cannot read as a second sentence or
4251
+ as one of the runtime's notices. `quote` maps every top-level key an opaque
4252
+ payload may carry to the label it is quoted under (letters, digits and spaces,
4253
+ at most 24; `""` only when it is the only key shown) or to `~` for a key
4254
+ deliberately left off, and must show at least one key (`prompt-say-label`,
4255
+ `prompt-say-shape`); `note` is `summary` (the default) or `none`. A `say` entry is read only under `style: minimal`. A class
4256
+ core phrases itself still needs a `say` entry with a `quote` map when its
4257
+ payload is opaque (a Hermes tool payload, `{tool, input}`, is). A `say` entry
4258
+ for a class no rule names loads: it applies if that class is ever requested.
4259
+
4260
+ **Other channels.** `style` and `say` are validated wherever they appear and an
4261
+ unknown value fails the policy closed (`prompt-style-unknown`,
4262
+ `prompt-say-shape`, `prompt-say-wildcard` on an untyped channel; the schema's
4263
+ own `enum`, `pattern` and shape errors on `telegram`, `web` and `cli`). Only
4264
+ Telegram draws a minimal card; the CLI and web channels IGNORE the keys and keep
4265
+ their full prompt, which shows more than was asked for rather than less.
4266
+
4267
+ **What the record says.** A decision taken on a card drawn under a minimal
4268
+ policy carries `payload.rendering` on its `approval.granted` or
4269
+ `approval.rejected`: `{"style": "minimal"}`, or `{"style": "technical",
4270
+ "fallback": "<reason>"}` when the technical card was sent instead. A minimal
4271
+ card whose class's `say` entry set a `does` phrase the runtime did not use
4272
+ (the payload was a command, a file change or an email, so the runtime drew its
4273
+ own phrase) records `{"style": "minimal", "say_does_ignored": true}`. Under a
4274
+ technical policy the key is absent and the record is unchanged. Nothing reads
4275
+ it back; it is a statement about the screen the approver answered on. It is an
4276
+ OPEN field: `schema/event.schema.json` does not constrain it in this release,
4277
+ so a reader with a closed schema for `approval.granted` must admit it.
4278
+
4279
+ **Changing it.** It is a policy key, so changing it is an attestation, and a
4280
+ decision on a request routed under the old policy is refused `policy-drift`, as
4281
+ for any policy change. The Telegram listener resolves `style` and `say` for EACH
4282
+ card it draws, and only from attested bytes: it reads the policy file once,
4283
+ requires those bytes to hash to the latest attestation in the verified log
4284
+ (the digest the gate decides under), and takes the settings from the same
4285
+ bytes. A re-attested setting therefore reaches the next card without a
4286
+ restart, and an edit to the file that nobody attested changes nothing: the card
4287
+ is technical, and when the attested policy asks for minimal the record says
4288
+ `fallback: "policy-unattested"`. An older core refuses the keys at the schema, and the
4289
+ whole policy then fails closed to all-`manual`: roll the core out before the
4290
+ policy that uses them.
4291
+
3923
4292
  ## channel cli
3924
4293
 
3925
4294
  **The rendering convention (SPEC.md §9).** Every displayed field carries a
@@ -4236,6 +4605,34 @@ place in the walkthrough and a duplicate message, never a pending request nobody
4236
4605
  is shown; an approval that depended on a channel's memory would not be an
4237
4606
  acceptable trade.
4238
4607
 
4608
+ **Earlier copies, and `--no-stale-copy`** (APRV-196, APRV-456). Every single-request decision
4609
+ button carries `<verb>:<nonce>:<action ref>` (a digest's "all" buttons carry no reference and never had a fallback), where the nonce is issued by this
4610
+ process and the reference is the first 16 hex of the action key's SHA-256. On
4611
+ the direct-bot shape (no `--api-base`, or one naming `https://api.telegram.org`)
4612
+ a tap whose nonce this process is not holding, such as a button on a copy sent
4613
+ before a restart, falls back to its reference: when this process holds that
4614
+ action open on a newer card, the tap decides it there, and the gate judges it
4615
+ exactly as it judges any other tap. That fallback is safe because only the bot
4616
+ token can put a button in front of the approver.
4617
+
4618
+ The fallback is **off** when `--api-base` names anything other than the Bot API
4619
+ (a relay, or a self-hosted Bot API server, which this rule treats as a relay)
4620
+ and when `--no-stale-copy` is passed. Then a decision tap whose nonce this
4621
+ process is not holding is refused `nonce-not-issued` (a member of the channel
4622
+ decision refusal union, beside `sender-unmapped`): it never reaches the gate,
4623
+ the approver's toast says this listener did not send that button, and the live
4624
+ card stays armed. When the tap's reference names a request this process holds
4625
+ open, one `audit.decision_refused` records the attempt under that request's key,
4626
+ naming the sender the policy resolves; a reference that names nothing open here,
4627
+ or a replay of the same button within this process, records nothing (the memory of refused buttons is per process and bounded, so a redelivery after a restart can record once more). The restart banner says that
4628
+ earlier copies no longer decide. The reason is the relay: anyone holding its
4629
+ token can send the approver a card with forged text and a real pending action's
4630
+ reference, and on the fallback the approver's genuine tap would pass the chat
4631
+ check and the sender mapping. The flag is launch configuration only: no policy
4632
+ key and nothing in an update can set it or clear it. `approval up` and
4633
+ [`channel telegram webhook`](#channel-telegram-webhook) take the same flag and
4634
+ apply the same rule.
4635
+
4239
4636
  **The execution token is printed on this terminal's stdout and is never sent to
4240
4637
  Telegram.** A chat transcript is stored on someone else's servers, backed up to
4241
4638
  phones, and readable by anyone later added to the chat — it is not a credential
@@ -4279,16 +4676,61 @@ Each `audit.sampled` with no later `audit.reviewed` now arrives as a **review
4279
4676
  card**.
4280
4677
 
4281
4678
  A card is not a prompt and says so. Its headline is `REVIEW — THIS ALREADY RAN`,
4282
- it carries no payload region and no approve button, and it accepts no token: the
4283
- action has happened, and a card offering an approve would present a settled fact
4284
- as a live authorization. What it carries is the same rows a prompt would show
4285
- for the same action — class, the command breakdown, task, the agent's claimed
4286
- summary, the model gloss where one is attached — plus two the card adds: `ran
4287
- at` (the `execution.started` the sample named) and `verdict` (that the runtime
4288
- allowed this without asking, which autonomy said so, the rate it was drawn at,
4289
- and how the log says it ended). Everything computed is derived from the verified
4290
- log, the payload store and the classifier; the claimed rows sit under the same
4291
- "NOT verified by the runtime" heading a prompt gives them.
4679
+ it carries no approve button, and it accepts no token: the action has happened,
4680
+ and a card offering an approve would present a settled fact as a live
4681
+ authorization. What it carries is the same rows a prompt would show for the same
4682
+ action — class, the command breakdown, task, the agent's claimed summary, the
4683
+ model gloss where one is attached — plus two the card adds: `ran at` (the
4684
+ `execution.started` the sample named) and `verdict` (that the runtime allowed
4685
+ this without asking, which autonomy said so, the rate it was drawn at, and how
4686
+ the log says it ended). Everything computed is derived from the verified log,
4687
+ the payload store and the classifier; the claimed rows sit under the same "NOT
4688
+ verified by the runtime" heading a prompt gives them.
4689
+
4690
+ **The card shows the payload that ran (APRV-480).** Under supervised-retro the
4691
+ review is the individual approval nobody gave before the action executed, so the
4692
+ reviewer reads the published bytes rather than a key and the agent's own summary
4693
+ of what it did. Between the computed and the claimed rows the card carries one
4694
+ of three payload regions, and its heading says which:
4695
+
4696
+ | Heading | What the reviewer sees | When |
4697
+ | --- | --- | --- |
4698
+ | `PAYLOAD — the bytes that ran, shown whole; this review covers them` | the canonical rendering of the bytes, whole, in a `<pre>` block | the runtime holds bytes that hash to the execution's binding and they fit one card |
4699
+ | `PAYLOAD — NOT SHOWN, hash only; this review does not cover the bytes` | the binding's `sha256`, and why the bytes are absent | nobody holds the bytes, the bytes held do not hash to the binding, or they are longer than this card's payload budget (at most 2000 escaped characters, less when the card's rows leave less room) |
4700
+ | `PAYLOAD — none recorded; the execution bound to no payload hash` | nothing to show and nothing to name | the execution and its registration recorded no `payload_hash` |
4701
+
4702
+ The bytes are shown whole or not at all: a card is one message edited in place,
4703
+ so it cannot spill a payload over several messages the way a request prompt
4704
+ does, and a review over half the bytes would claim more than the reviewer read.
4705
+ Only the first case lets the review record carry a `payload_hash` (see
4706
+ [the review record](#the-review-record)). A terminal review (`approval audit
4707
+ review`) shows no payload and so never records one.
4708
+
4709
+ **The whole card fits one message (PR #614 refutation F4).** The agent's summary
4710
+ is cut at 400 characters and every other row at 300, each with a marker saying
4711
+ how many characters are not shown (the registration record keeps the text
4712
+ whole); a card whose rows are pathologically long cuts them further, in steps,
4713
+ until it fits. A notice shows at most 600 characters, the rest on the
4714
+ listener's stderr. The payload budget is what the card's own rows leave under
4715
+ Telegram's 4096-character limit after the longest heading and the largest
4716
+ notice a tap can add, capped at 2000, and it is computed from the card alone,
4717
+ so the card a reviewer saw and the view a tap records cannot disagree.
4718
+
4719
+ **A card that fails to send (PR #614 recheck NF-2).** When Telegram refuses the
4720
+ card itself (HTTP 400 `message is too long`, `can't parse entities`,
4721
+ `message text is empty` or `text must be non-empty`, `reply markup is too long`,
4722
+ `BUTTON_DATA_INVALID`, `ENTITIES_TOO_LONG`), resending it can only fail again:
4723
+ the listener prints a coded `approval: telegram review-offer-failed:` line
4724
+ naming `approval audit review <seq> --ok` (or `--deny`), leaves that sample for
4725
+ a terminal review, and offers the next one. Every other failure (a 429, a
4726
+ timeout, a 5xx, a network error) prints `approval: telegram review-offer-retry:`
4727
+ and pauses review cards for Telegram's `retry_after` or the backoff, whichever
4728
+ is longer (one minute, doubling per attempt; never more than an hour, whatever
4729
+ `retry_after` says), then offers the same sample again.
4730
+ After five failed attempts the sample is left for a terminal review with the
4731
+ `review-offer-failed` line. Either way the sample stays open in
4732
+ `approval audit list` and QUEUE.md, the queue behind it no longer waits, and a
4733
+ restarted listener offers it afresh.
4292
4734
 
4293
4735
  Six buttons, bare emoji and no words (APRV-302), in two rows: the verdict on the
4294
4736
  first (✅ OK, 🛑 Deny) and the grade on the second, worst to best (👎 disliked,
@@ -4296,12 +4738,39 @@ first (✅ OK, 🛑 Deny) and the grade on the second, worst to best (👎 disli
4296
4738
 
4297
4739
  | Tap | What is recorded |
4298
4740
  | --- | --- |
4299
- | ✅ | `audit.reviewed` with verdict `ok` and no reaction. |
4300
- | a reaction | verdict `ok` and that grade — a reaction alone implies OK. |
4301
- | 🛑 once | **Nothing.** It arms the card, which says `DENY ARMED` on itself. |
4302
- | 🛑 twice | verdict `denied`, and the reconciliation obligation it opens is named on the reply. |
4741
+ | ✅ | `audit.reviewed` with verdict `ok`, and the held grade if there is one. |
4742
+ | a reaction, nothing armed | **Nothing.** Refused `verdict-required` (APRV-482): a grade is not a verdict. The card holds the grade and says `GRADE … HELD` in its heading. |
4743
+ | ✅ after a reaction | verdict `ok` with the held grade (a `loved` or `disliked` asks for words first). |
4744
+ | 🛑 once | **Nothing.** The tap reaches the runtime with no verdict, and the card arms (it says `DENY ARMED` on itself) only when the answer is `verdict-required`, which the runtime gives only to a mapped sender on the class's roster. Any other refusal is shown on the card and arms nothing. |
4745
+ | 🛑 twice | verdict `denied` with the held grade if any, and the reconciliation obligation it opens is named on the reply. |
4303
4746
  | a reaction with deny armed | verdict `denied` with that grade. |
4304
4747
 
4748
+ **A held grade and an armed Deny belong to the account that tapped them (PR #614
4749
+ refutation F3).** The card remembers the transport's sender id with each. A tap
4750
+ that would finish another account's half-finished review records nothing and is
4751
+ refused on the card with one line: any ✅ or 🛑 while another account holds the
4752
+ grade or armed Deny, and any reaction while another account armed Deny. A
4753
+ reaction tapped with nothing armed only replaces the held grade, and its tapper
4754
+ then holds it. A record therefore never carries a grade or a denial its reviewer
4755
+ did not tap. Only an account that may review can leave either behind (PR #614
4756
+ recheck NF-1): a grade is held, and a Deny arms, only after the runtime has
4757
+ passed the sender and the roster and answered `verdict-required`, so an
4758
+ unmapped or off-roster account in the approver chat cannot block the
4759
+ approvers' taps.
4760
+
4761
+ **A verdict is an explicit act (APRV-482).** Under supervised-retro a review
4762
+ counts as the individual approval nobody gave before the action ran, so nothing
4763
+ records `ok` on a reviewer's behalf. The explicit forms per surface:
4764
+
4765
+ | Surface | `ok` | `denied` | Refused `verdict-required` |
4766
+ | --- | --- | --- | --- |
4767
+ | Telegram card | ✅ (alone, or after a grade) | 🛑 twice, or 🛑 then a grade | a grade tapped with nothing armed |
4768
+ | `approval audit review` | `--ok` | `--deny` | neither flag, with or without `--reaction`; both flags is a usage error (exit 2) |
4769
+
4770
+ The refusal on the card is the runtime's own: the tap reaches `reviewSample`
4771
+ with no verdict, and the code and its message come back exactly as a terminal
4772
+ sees them.
4773
+
4305
4774
  The card does not print this table under itself. It used to, and the paragraph
4306
4775
  of rules pushed the rows a review is actually about off the first screen for a
4307
4776
  reader who had read them on the card before. The two things a tap could get
@@ -4334,7 +4803,10 @@ reviewer has to be able to say which half they meant.
4334
4803
 
4335
4804
  Every append goes through the same `reviewSample` that `approval audit review`
4336
4805
  calls, recorded against the human identity this listener was configured with
4337
- (`--as` / `APPROVAL_HUMAN`), never anything the callback carried. So `approval
4806
+ (`--as` / `APPROVAL_HUMAN`), never anything the callback carried. That identity,
4807
+ or the person a sender mapping resolves a tap to, must be on the class's
4808
+ `approvers` roster where the rule names one, or the card shows
4809
+ `actor-not-approver` and keeps its buttons (APRV-483). So `approval
4338
4810
  feedback` shows a reaction given on a card exactly as one given at a terminal:
4339
4811
  same record, same `human:<id>`, same everything.
4340
4812
 
@@ -4459,7 +4931,10 @@ a gate.
4459
4931
 
4460
4932
  **Only the arrival changes, and that is the whole claim.** The pending queue is
4461
4933
  re-derived from the verified log every cycle by the same `dispatchPending`. The
4462
- handlers are the same four function objects `listen` registers. A tap becomes a
4934
+ handlers are the same function objects `listen` registers (since APRV-456 that
4935
+ includes the recorder for a `nonce-not-issued` refusal, and `--no-stale-copy`
4936
+ or a relayed `--api-base` turns the earlier-copy fallback off here exactly as it
4937
+ does there). A tap becomes a
4463
4938
  decision through the same `handleUpdate`, the same `callback_query.from.id`
4464
4939
  reading, the same `approvers.<id>.senders` resolution, the same
4465
4940
  `recordChannelDecision` and the same annotate-after-decision edit. There is no
@@ -6163,6 +6638,13 @@ A 409 that still happens at runtime — another machine, or a poller started
6163
6638
  outside this runtime — is reported once, with the instances the registry knows
6164
6639
  about, and its repeats are counted rather than reprinted.
6165
6640
 
6641
+ **A relayed Telegram channel refuses buttons it is not holding** (APRV-456). An
6642
+ `--api-base` that is not `https://api.telegram.org`, or `--no-stale-copy`, turns
6643
+ off the earlier-copy fallback, and a decision tap whose nonce this process is
6644
+ not holding is refused `nonce-not-issued` instead of reaching the gate. The rule
6645
+ and its reason are under [`channel telegram listen`](#channel-telegram-listen)
6646
+ ("Earlier copies, and `--no-stale-copy`").
6647
+
6166
6648
  **The ambient runtime: the daemon loop and every configured channel in one
6167
6649
  supervised foreground process.** `approval daemon run --with-channels` is the
6168
6650
  same verb spelled from the other side, and it reaches the same function before
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "approval-md",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Human approval for agent actions. CLI runtime for the approval.md convention (pre-release).",
5
5
  "type": "module",
6
6
  "bin": {
@@ -59,5 +59,5 @@
59
59
  "better-sqlite3": "12.11.1",
60
60
  "yaml": "2.9.0"
61
61
  },
62
- "gitHead": "fb0cf987985c2b9884ebe46b7b3b4419147d057e"
62
+ "gitHead": "5f9b9c3da2b0b220b344590d6d5319d27edf3b61"
63
63
  }