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
@@ -219,10 +219,10 @@
219
219
  * process memory and nothing else: no event is appended, no message is edited,
220
220
  * and the log is not opened.
221
221
  */
222
- import type { ChannelBatch, ChannelDecision, ChannelHealth, ChannelRequest, DecisionOutcome, DeliveryId, RenderedRequest, TaggedField, TestableChannel } from "./contract.js";
222
+ import type { ChannelBatch, ChannelDecision, ChannelDecisionRefusal, ChannelHealth, ChannelRequest, DecisionOutcome, DeliveryId, RenderedRequest, TaggedField, TestableChannel } from "./contract.js";
223
223
  import type { ChannelSender } from "../core/sender-identity.js";
224
224
  import { type Reaction, type ReviewVerdict } from "../core/audit.js";
225
- import { type PromptLayout } from "../core/prompt-layout.js";
225
+ import { type PromptLayout, type PromptSay, type PromptStyle } from "../core/prompt-layout.js";
226
226
  export { TELEGRAM_CHAT_ENV, TELEGRAM_TOKEN_ENV, telegramChatEnvFor, telegramTokenEnvFor, } from "../core/telegram-config.js";
227
227
  /** The real Bot API. Overridden only by tests, against a local mock. */
228
228
  export declare const TELEGRAM_DEFAULT_API_BASE = "https://api.telegram.org";
@@ -486,6 +486,66 @@ export interface TelegramConfig {
486
486
  * what it rendered before the key existed.
487
487
  */
488
488
  layout?: PromptLayout;
489
+ /**
490
+ * Whether APRV-196's stale-copy fallback is on (APRV-456). Defaults to `true`,
491
+ * the direct-bot shape.
492
+ *
493
+ * On, a decision tap whose nonce this process is not holding falls back to
494
+ * its action reference, and is carried to the gate when this process holds
495
+ * that action open. The bound that makes that safe is that only the bot token
496
+ * can put a button in front of the approver, so any button carrying a live
497
+ * reference was put there by some process of this gate.
498
+ *
499
+ * Off, that tap is refused `nonce-not-issued` before the gate is called and
500
+ * never reaches the decision handler; at most one `audit.decision_refused` is
501
+ * written for it, through {@link TelegramChannel.onRefusedDecision}. The verb
502
+ * turns it off when the channel runs through a relay (an `--api-base` other
503
+ * than the Bot API), where anyone holding the relay's token can send the
504
+ * approver a card with forged text and a real action's reference, and when
505
+ * the operator passes `--no-stale-copy`. It is launch configuration and
506
+ * nothing else: no policy key and nothing in an update can set it.
507
+ */
508
+ staleCopy?: boolean;
509
+ /**
510
+ * Which layout a prompt is drawn in (APRV-489), from
511
+ * `channels.telegram.prompt.style`. Passed in by the verb, which has already
512
+ * loaded the policy, for {@link TelegramConfig.layout}'s reason. Defaults to
513
+ * `technical`, so a channel constructed without one sends exactly what it
514
+ * sent before the key existed.
515
+ */
516
+ promptStyle?: PromptStyle;
517
+ /**
518
+ * The operator's plain-words declarations per class (APRV-489), from
519
+ * `channels.telegram.prompt.say`. Read only under `promptStyle: minimal`.
520
+ */
521
+ say?: PromptSay;
522
+ /**
523
+ * Where the style and the `say` map come from FOR EACH DELIVERY (fix round
524
+ * 2, S4). The verb passes a function that reads the policy file the tagger
525
+ * reads, so a re-attested style or `say` takes effect on the next card
526
+ * without a restart, exactly as a re-attested class rule does. When absent,
527
+ * {@link TelegramConfig.promptStyle} and {@link TelegramConfig.say} are used.
528
+ * A resolver that throws means `technical`.
529
+ */
530
+ promptFor?: () => {
531
+ style: PromptStyle;
532
+ say: PromptSay;
533
+ fallback?: string;
534
+ };
535
+ }
536
+ /**
537
+ * A decision tap the channel refused on its own, before any handler ran
538
+ * (APRV-456), handed to {@link TelegramChannel.onRefusedDecision} so the
539
+ * runtime can record that a human's attention was spent.
540
+ *
541
+ * `decision.action_key` and `decision.deliveryId` come from the delivery THIS
542
+ * process holds open for the reference the bytes carried, never from the bytes
543
+ * themselves; a tap whose reference names nothing open here produces no
544
+ * {@link TelegramRefusedDecision} at all.
545
+ */
546
+ export interface TelegramRefusedDecision {
547
+ decision: ChannelDecision;
548
+ refusal: ChannelDecisionRefusal;
489
549
  }
490
550
  /**
491
551
  * Why a callback was ignored.
@@ -542,6 +602,14 @@ export interface TelegramStats {
542
602
  * restart is costing the approver a wrong tap.
543
603
  */
544
604
  staleCopyDecisions: number;
605
+ /**
606
+ * Taps refused `nonce-not-issued` because the stale-copy fallback is off
607
+ * (APRV-456): a decision button whose nonce this process is not holding, on
608
+ * a relayed channel or one started with `--no-stale-copy`. Counted whether or
609
+ * not a record was written, because a rising number here on a relayed gate
610
+ * is either restarts or somebody putting cards in front of the approver.
611
+ */
612
+ staleCopyRefusals: number;
545
613
  /**
546
614
  * Bot commands handed to the runtime's command handler (APRV-216). Never a
547
615
  * decision and never a log event: a command reorders what this process shows
@@ -694,10 +762,21 @@ export interface TelegramRendering {
694
762
  * attached one. Naming the subset as a type is what lets {@link reviewRow} and
695
763
  * {@link telegramRow} share one implementation of those five without either
696
764
  * side casting: a card supplies exactly these fields, and the compiler refuses
697
- * a card that reaches for `budgets`, `fullPayload`, or anything else that only
698
- * a pending question has.
765
+ * a card that reaches for `budgets`, `ttl_remaining_ms`, or anything else that
766
+ * only a pending question has.
767
+ *
768
+ * Since APRV-480 a card also carries `fullPayload`, the bytes the sampled
769
+ * execution bound to, and `payload_hash`, the binding the log recorded for them.
770
+ * Under supervised-retro a review counts as the individual approval nobody gave
771
+ * before the action ran, so the reviewer has to be able to read what was
772
+ * published; a uuid beside the agent's own summary of what it did is the agent
773
+ * describing its own homework. `fullPayload` is computed the way a prompt's is
774
+ * (hash-checked against the binding before anything renders it) and is `null`
775
+ * when nobody holds the bytes; `payload_hash` is absent only when the execution
776
+ * recorded no binding at all. Which of the two a card SHOWS is
777
+ * {@link reviewPayloadView}'s decision, and the card says which.
699
778
  */
700
- export type ReviewCardFields = Pick<ChannelRequest, "action_key" | "class" | "task" | "summary"> & Partial<Pick<ChannelRequest, "command_breakdown" | "gloss">>;
779
+ export type ReviewCardFields = Pick<ChannelRequest, "action_key" | "class" | "task" | "summary" | "fullPayload"> & Partial<Pick<ChannelRequest, "command_breakdown" | "gloss" | "payload_hash">>;
701
780
  /** The rows a review card renders, in the order it renders them (APRV-299). */
702
781
  export declare const REVIEW_CARD_ROWS: readonly ["class", "command_breakdown", "task", "summary", "gloss"];
703
782
  export type ReviewCardRow = (typeof REVIEW_CARD_ROWS)[number];
@@ -726,6 +805,40 @@ export type ReviewCardRow = (typeof REVIEW_CARD_ROWS)[number];
726
805
  * computed heading.
727
806
  */
728
807
  export declare function renderTelegram(request: ChannelRequest, heading?: string, layout?: PromptLayout): TelegramRendering;
808
+ /**
809
+ * The settle edit of a minimal card (APRV-489): the outcome, the card's
810
+ * headline, the detail lines, and the collapsed block WHOLE.
811
+ *
812
+ * The edit replaces the only message that holds the canonical rendering, so
813
+ * the rendering is never what gives way to fit: the runtime's own detail lines
814
+ * are shortened instead, with a marker, and the outcome headline always stays.
815
+ * The card was at most 3800 characters with its quote box and
816
+ * deadline line, which the edit drops, so headline plus collapsed block always
817
+ * fit under {@link TELEGRAM_SETTLE_BUDGET} (the card budget is `MINIMAL_MESSAGE_BUDGET` in `telegram-minimal.ts`).
818
+ */
819
+ export declare function minimalSettleText(outcome: string, detail: string[], card: {
820
+ headline: string;
821
+ details: string;
822
+ }): string;
823
+ /**
824
+ * The settle edit of last resort (fix round 2, S3): sent when Telegram refused
825
+ * the full settle edit of a minimal card, so a decided card never keeps looking
826
+ * undecided. The outcome, the request's key, the first detail line (who, when,
827
+ * which record) cut to fit, and where the rest is. Always under 1000 units.
828
+ */
829
+ export declare function shortSettleText(outcome: string, actionKey: string, detail: string[]): string;
830
+ /**
831
+ * Whether a Bot API failure is a refusal of THIS message rather than a passing
832
+ * failure (fix round 2, S2): any 4xx but 429. A 429, a 5xx and a transport
833
+ * failure are retried by the dispatch cycle as before; a refusal never will
834
+ * succeed with the same text, so the minimal card gives way to the technical one.
835
+ */
836
+ export declare function isMessageRefusal(cause: unknown): boolean;
837
+ /**
838
+ * The longest settle edit of a minimal card, in characters of HTML: under
839
+ * Telegram's 4096 with room for a relay's `Agent: <name>` line.
840
+ */
841
+ export declare const TELEGRAM_SETTLE_BUDGET = 3990;
729
842
  /**
730
843
  * Split `text` so every chunk survives HTML escaping inside the message limit.
731
844
  *
@@ -1017,6 +1130,13 @@ export declare const TELEGRAM_REVIEW_DENIED = "\u2717 REVIEWED \u2014 DENIED";
1017
1130
  * been recorded.
1018
1131
  */
1019
1132
  export declare const TELEGRAM_REVIEW_ARMED = "DENY ARMED \u2014 nothing is recorded yet";
1133
+ /**
1134
+ * The headline a card wears while a grade is held and no verdict has been
1135
+ * given (APRV-482). A grade is not a verdict: tapping one with nothing armed
1136
+ * records nothing, and the card keeps the grade so the OK or the Deny that
1137
+ * follows records it.
1138
+ */
1139
+ export declare function telegramReviewGradeHeld(reaction: Reaction): string;
1020
1140
  /**
1021
1141
  * What a review tap's single answer says (APRV-302).
1022
1142
  *
@@ -1032,8 +1152,25 @@ export declare const TELEGRAM_REVIEW_ARMED = "DENY ARMED \u2014 nothing is recor
1032
1152
  export declare const TELEGRAM_REVIEW_ACK = "Heard \u2014 recording your review. The card will say what the log recorded.";
1033
1153
  /** The toast a first Deny tap gets: it says plainly that nothing was written. */
1034
1154
  export declare const TELEGRAM_REVIEW_ARM_TOAST = "Deny armed \u2014 nothing recorded. Tap Deny again to record it, or a reaction to record it with a grade.";
1155
+ /**
1156
+ * The card line and toast for a tap that would finish another account's
1157
+ * half-finished review (PR #614 refutation F3): an armed Deny or a held grade
1158
+ * belongs to the account that tapped it, and nothing is recorded. A held grade
1159
+ * is replaced by the tapper's own grade (a grade finishes nothing), which the
1160
+ * line says (PR #614 recheck NF-1, minor). Under 200 characters, the Bot API's
1161
+ * limit for a callback answer.
1162
+ */
1163
+ export declare const TELEGRAM_REVIEW_OTHER_SENDER = "Another account's unfinished review (armed Deny or held grade) is on this card. Nothing was recorded. Tap your own grade to replace a held one; an armed Deny is theirs to finish.";
1035
1164
  /** The toast a reaction that needs the human's own words gets. */
1036
1165
  export declare const TELEGRAM_REVIEW_NOTE_TOAST = "Heard \u2014 reply to the prompt with why. Nothing is recorded until it arrives.";
1166
+ /**
1167
+ * The card line for a note prompt that could not be sent (APRV-492). The toast
1168
+ * went out before the send and told the human to reply to a prompt, so the
1169
+ * card says the prompt is not coming and how to be asked again: a grade tap is
1170
+ * what the relay licenses a prompt for. With Deny armed the grade alone asks;
1171
+ * otherwise the grade is held and OK asks.
1172
+ */
1173
+ export declare function telegramReviewNoteUnasked(verdict: ReviewVerdict): string;
1037
1174
  /**
1038
1175
  * What the ForceReply prompt asks for.
1039
1176
  *
@@ -1057,7 +1194,14 @@ export declare function reviewNotePromptLines(reaction: Reaction, verdict: Revie
1057
1194
  export interface ReviewTap {
1058
1195
  /** `seq` of the `audit.sampled` record this card was drawn for. */
1059
1196
  sampleSeq: number;
1060
- verdict: ReviewVerdict;
1197
+ /**
1198
+ * The verdict the taps added up to, or absent when the human gave none
1199
+ * (APRV-482): a grade tapped with nothing armed. The channel still hands that
1200
+ * tap to the runtime, which refuses it `verdict-required`, so the refusal a
1201
+ * reviewer reads is the core's own and no channel decides which gestures are
1202
+ * approvals.
1203
+ */
1204
+ verdict?: ReviewVerdict;
1061
1205
  /** The grade, when the human gave one. Absent means absent. */
1062
1206
  reaction?: Reaction;
1063
1207
  /** The human's words, when a note prompt collected any. */
@@ -1074,6 +1218,14 @@ export interface ReviewTap {
1074
1218
  * policy, refuses an account it does not name, and records the one it does.
1075
1219
  */
1076
1220
  sender?: ChannelSender;
1221
+ /**
1222
+ * The payload binding this card showed WHOLE (APRV-481), present only when
1223
+ * {@link reviewPayloadView} put the bytes on the screen. Set by the channel
1224
+ * from the card it is holding, never from anything the network sent; the
1225
+ * runtime checks it against the log before recording it as the review's
1226
+ * `payload_hash`.
1227
+ */
1228
+ payloadHash?: string;
1077
1229
  }
1078
1230
  /** What the runtime did with a review tap, as it reports it back. */
1079
1231
  export interface ReviewTapResponse {
@@ -1085,6 +1237,15 @@ export interface ReviewTapResponse {
1085
1237
  detail: string[];
1086
1238
  /** The toast, which Telegram caps at a short sentence. */
1087
1239
  toast: string;
1240
+ /**
1241
+ * The refusal code, when the runtime refused (PR #614 refutation N6/F3).
1242
+ * The card holds a lone grade only when this is `verdict-required`: the
1243
+ * runtime judges the verdict after the sender and the roster, so that code
1244
+ * says the tapper may review and lacks only the verdict. A grade refused for
1245
+ * anything else (an unmapped sender, a reviewer off the roster) is not held,
1246
+ * so it cannot block the next account's tap.
1247
+ */
1248
+ code?: string;
1088
1249
  }
1089
1250
  export type ReviewTapHandler = (tap: ReviewTap) => ReviewTapResponse | Promise<ReviewTapResponse>;
1090
1251
  /**
@@ -1130,6 +1291,22 @@ export interface ReviewCardState {
1130
1291
  * whose next Deny tap arms again.
1131
1292
  */
1132
1293
  denyArmed: boolean;
1294
+ /**
1295
+ * A grade tapped before any verdict (APRV-482), held so the OK or the second
1296
+ * Deny that follows records it. **Process memory**, like the arming: it
1297
+ * appends nothing, the card states it, and losing it to a restart costs a tap.
1298
+ */
1299
+ heldReaction: Reaction | null;
1300
+ /**
1301
+ * Who armed Deny and who holds the grade, as the transport attributed the tap
1302
+ * (PR #614 refutation F3): the sender's id, or `null` for a tap that carried
1303
+ * none. A held grade or an armed Deny is one person's half-finished review,
1304
+ * so it rides only on that person's next verdict tap; another account's tap
1305
+ * is refused on the card rather than recording someone else's grade or
1306
+ * denial under its own name. Process memory, cleared with the state it owns.
1307
+ */
1308
+ armedBy: string | null;
1309
+ heldBy: string | null;
1133
1310
  /**
1134
1311
  * The outcome, once the runtime has recorded one. Written only from the
1135
1312
  * handler's answer, never inferred here.
@@ -1198,6 +1375,105 @@ export declare function parseReviewCallback(data: unknown): {
1198
1375
  nonce: string;
1199
1376
  choice: ReviewChoice;
1200
1377
  } | null;
1378
+ /**
1379
+ * How much payload, in ESCAPED characters, a review card carries whole
1380
+ * (APRV-480).
1381
+ *
1382
+ * A card is one message edited in place, so it cannot spill the payload over
1383
+ * several messages the way a prompt does: the edit that settles it replaces one
1384
+ * message, and a second message holding the rest of the bytes would outlive the
1385
+ * card it belonged to. So the bytes are shown whole or not at all. Never cut:
1386
+ * a review that recorded a payload hash over bytes the reviewer saw half of
1387
+ * would claim more than they read. This is the CEILING; the budget a given
1388
+ * card gets is {@link reviewPayloadBudget}, which is this or the headroom the
1389
+ * card's own rows leave under {@link TELEGRAM_MAX_MESSAGE_CHARS}, whichever is
1390
+ * smaller (PR #614 refutation F4).
1391
+ */
1392
+ export declare const REVIEW_PAYLOAD_BUDGET = 2000;
1393
+ /**
1394
+ * The longest agent-written summary a review card shows, in characters (PR
1395
+ * #614 refutation F4). The summary is the requesting party's own claim and its
1396
+ * schema sets no length, so before this cap an agent could write a summary
1397
+ * that pushed the card past Telegram's limit, which failed the send and held
1398
+ * the review queue. Longer text is cut with a marker saying how much is not
1399
+ * shown; the registration record keeps it whole.
1400
+ */
1401
+ export declare const REVIEW_SUMMARY_MAX = 400;
1402
+ /**
1403
+ * The most characters a review card's notice region (a refusal, or the arming
1404
+ * line) shows, all its lines together (PR #614 refutation F4). Each line was
1405
+ * already cut at {@link REVIEW_NOTICE_MAX}; the region had no total, so a
1406
+ * refusal with several long lines could take the card over the limit and make
1407
+ * the redraw that carries it fail. The whole refusal is on the listener's
1408
+ * stderr.
1409
+ */
1410
+ export declare const REVIEW_NOTICE_REGION_MAX = 600;
1411
+ /**
1412
+ * The visible length of HTML this channel wrote: tags removed and each entity
1413
+ * {@link escapeHtml} produces counted as the one character it stands for.
1414
+ * Telegram measures its limit after entity parsing, so this is the number the
1415
+ * limit applies to.
1416
+ */
1417
+ export declare function telegramVisibleLength(html: string): number;
1418
+ /**
1419
+ * The longest payload text, in characters, a review card will even try to
1420
+ * render (APRV-480 refutation: review-delivery resource cap).
1421
+ *
1422
+ * Supervised payloads carry no size cap of their own (`PROPOSE_PAYLOAD_MAX_BYTES`
1423
+ * binds proposals only), and the party under review writes them. Without this
1424
+ * bound every render of a card, every redraw after a tap, and every tap's
1425
+ * {@link reviewPayloadView} would run the canonical renderer and HTML escaping
1426
+ * over the whole payload only to conclude "too long", so an agent could make
1427
+ * each review gesture cost work proportional to bytes it chose. Text past this
1428
+ * length is `too-long` without being rendered at all; the card builder
1429
+ * (`cli/audit-card.ts`) keeps no more than this much text in the card it holds.
1430
+ * Eight times the card budget, because a canonical rendering is never that much
1431
+ * shorter than the JSON it renders, and erring here only ever shows the hash.
1432
+ */
1433
+ export declare const REVIEW_RENDER_INPUT_MAX: number;
1434
+ /** The heading over a payload the card shows whole (APRV-480). */
1435
+ export declare const TELEGRAM_REVIEW_PAYLOAD_BYTES = "PAYLOAD \u2014 the bytes that ran, shown whole; this review covers them";
1436
+ /** The heading over a payload the card names by hash only (APRV-480). */
1437
+ export declare const TELEGRAM_REVIEW_PAYLOAD_HASH_ONLY = "PAYLOAD \u2014 NOT SHOWN, hash only; this review does not cover the bytes";
1438
+ /** The heading on a card whose execution recorded no payload binding (APRV-480). */
1439
+ export declare const TELEGRAM_REVIEW_PAYLOAD_NONE = "PAYLOAD \u2014 none recorded; the execution bound to no payload hash";
1440
+ /**
1441
+ * What a review card shows of the payload, decided once from the card alone
1442
+ * (APRV-480).
1443
+ *
1444
+ * - `bytes`: the canonical rendering, whole. Only this case lets a review
1445
+ * record a payload hash (APRV-481), because only here did the reviewer read
1446
+ * the bytes the hash names.
1447
+ * - `hash`: the binding, and the reason the bytes are not on the card: nobody
1448
+ * holds them (`unavailable`), or they are longer than one card carries
1449
+ * (`too-long`). The card says so in its heading.
1450
+ * - `none`: the execution recorded no binding, so there is nothing to show and
1451
+ * nothing to name.
1452
+ *
1453
+ * Pure and deterministic over the card, so the rendering and the tap that
1454
+ * follows it can never disagree about which case was on the screen.
1455
+ */
1456
+ export type ReviewPayloadView = {
1457
+ kind: "bytes";
1458
+ hash: string;
1459
+ text: string;
1460
+ } | {
1461
+ kind: "hash";
1462
+ hash: string;
1463
+ reason: "unavailable" | "too-long";
1464
+ } | {
1465
+ kind: "none";
1466
+ };
1467
+ export declare function reviewPayloadView(card: ReviewCard): ReviewPayloadView;
1468
+ /**
1469
+ * How many escaped characters of payload this card can show whole (PR #614
1470
+ * refutation F4): {@link REVIEW_PAYLOAD_BUDGET}, or the room the card's own
1471
+ * rows leave under {@link TELEGRAM_MAX_MESSAGE_CHARS} after the longest heading,
1472
+ * the payload heading and the largest notice a tap can add, whichever is
1473
+ * smaller. Pure over the card, so the view a card is drawn with and the view a
1474
+ * tap records are the same view.
1475
+ */
1476
+ export declare function reviewPayloadBudget(card: ReviewCard): number;
1201
1477
  /**
1202
1478
  * The card's message: the rows, whatever notice the last tap produced, and the
1203
1479
  * keyboard.
@@ -1210,13 +1486,15 @@ export declare function parseReviewCallback(data: unknown): {
1210
1486
  * every card said the same thing to a reader who had already read them once, and
1211
1487
  * pushed the rows a review is actually about off the first screen.
1212
1488
  *
1213
- * Pure. Two things it deliberately does NOT carry, and both are the same rule
1214
- * read twice: no payload region, and no approve button. SPEC.md §10.3 requires
1215
- * the canonical rendering in front of an approver before a DECISION is
1216
- * collected, and this collects none — the action ran, the review says only what
1217
- * a person thought of it, and a card that offered an approve would be
1218
- * presenting a settled fact as a live authorization. A sample is never
1219
- * delivered as an approval request and never accepts a token.
1489
+ * Pure. It carries no approve button: the action ran, and a card that offered
1490
+ * an approve would be presenting a settled fact as a live authorization. A
1491
+ * sample is never delivered as an approval request and never accepts a token.
1492
+ *
1493
+ * It DOES carry a payload region since APRV-480, between the computed rows and
1494
+ * the claimed ones, exactly where a prompt puts its own. Under supervised-retro
1495
+ * the review is the individual approval, so the reviewer reads the bytes that
1496
+ * ran, or is told plainly that the card shows only their hash and why
1497
+ * ({@link reviewPayloadView}). The heading says which, every time.
1220
1498
  */
1221
1499
  export declare function renderReviewCard(state: ReviewCardState): {
1222
1500
  text: string;
@@ -1239,6 +1517,13 @@ export declare class TelegramApiError extends Error {
1239
1517
  * JSON, or carried no description.
1240
1518
  */
1241
1519
  readonly description: string | null;
1520
+ /**
1521
+ * The Bot API's `parameters.retry_after`, in seconds, when the failure
1522
+ * carried one (PR #614 recheck NF-2). Telegram sends it with 429 "Too Many
1523
+ * Requests" and means it: a send before it elapses is refused again.
1524
+ * `null` when the body named none.
1525
+ */
1526
+ readonly retryAfterSeconds: number | null;
1242
1527
  constructor(message: string, method: string,
1243
1528
  /**
1244
1529
  * The HTTP status, when the failure was an HTTP one. `null` for a transport
@@ -1251,8 +1536,32 @@ export declare class TelegramApiError extends Error {
1251
1536
  * error body carried one. `null` when the body was absent, unreadable, not
1252
1537
  * JSON, or carried no description.
1253
1538
  */
1254
- description?: string | null);
1539
+ description?: string | null,
1540
+ /**
1541
+ * The Bot API's `parameters.retry_after`, in seconds, when the failure
1542
+ * carried one (PR #614 recheck NF-2). Telegram sends it with 429 "Too Many
1543
+ * Requests" and means it: a send before it elapses is refused again.
1544
+ * `null` when the body named none.
1545
+ */
1546
+ retryAfterSeconds?: number | null);
1255
1547
  }
1548
+ /**
1549
+ * Whether a failed send is the Bot API refusing the message itself, so that
1550
+ * sending the same message again can only fail again (PR #614 recheck NF-2).
1551
+ *
1552
+ * Only an HTTP 400 whose description matches
1553
+ * {@link TELEGRAM_DETERMINISTIC_SEND_REFUSALS} qualifies. Anything else, an
1554
+ * error that is not a {@link TelegramApiError} included, is treated as
1555
+ * transient, which is the direction that keeps a card in front of the
1556
+ * approver: a transient failure misread as deterministic hides a sample from
1557
+ * the phone, and the converse costs a bounded number of retries.
1558
+ */
1559
+ export declare function isDeterministicSendRefusal(cause: unknown): boolean;
1560
+ /**
1561
+ * The wait, in milliseconds, a failed call's `retry_after` asks for, or `null`
1562
+ * when it named none (PR #614 recheck NF-2).
1563
+ */
1564
+ export declare function retryAfterMsOf(cause: unknown): number | null;
1256
1565
  /**
1257
1566
  * Whether a failed call is the Bot API saying an edit changed nothing
1258
1567
  * (APRV-277).
@@ -1302,6 +1611,17 @@ export interface TelegramPollResult {
1302
1611
  tap: ReviewTap;
1303
1612
  ok: boolean;
1304
1613
  }[];
1614
+ /**
1615
+ * Decision taps the channel itself refused before any handler ran (APRV-456),
1616
+ * in order. `action_key` is the action this process holds open for the
1617
+ * reference the tap carried, or `null` when it holds nothing for it; only a
1618
+ * non-null one can have produced an `audit.decision_refused`.
1619
+ */
1620
+ refused: {
1621
+ code: ChannelDecisionRefusal["code"];
1622
+ action_key: string | null;
1623
+ detail: string;
1624
+ }[];
1305
1625
  }
1306
1626
  /**
1307
1627
  * How updates reach this channel (APRV-424).
@@ -1414,6 +1734,12 @@ export declare class TelegramChannel implements TestableChannel {
1414
1734
  private readonly describeAction;
1415
1735
  /** The policy's row layout for this channel (APRV-218). Read-only, and pure input to the renderer. */
1416
1736
  private readonly layout;
1737
+ /** The policy's prompt style for this channel (APRV-489). */
1738
+ private readonly promptStyle;
1739
+ /** The policy's `say` declarations (APRV-489). Pure input to the minimal renderer. */
1740
+ private readonly say;
1741
+ /** The per-delivery resolver (fix round 2, S4), or null for the fixed values above. */
1742
+ private readonly promptFor;
1417
1743
  /** When {@link sweep} last ran, so the poll loop can call it every cycle. */
1418
1744
  private lastSweepMs;
1419
1745
  /**
@@ -1423,6 +1749,25 @@ export declare class TelegramChannel implements TestableChannel {
1423
1749
  */
1424
1750
  private ack;
1425
1751
  private handler;
1752
+ /**
1753
+ * APRV-196's stale-copy fallback, on or off (APRV-456). Fixed at
1754
+ * construction from {@link TelegramConfig.staleCopy}; nothing an update
1755
+ * carries can change it.
1756
+ */
1757
+ private readonly staleCopy;
1758
+ /**
1759
+ * Where a `nonce-not-issued` refusal is recorded (APRV-456). The runtime's,
1760
+ * like every other handler here; absent, the refusal still happens and only
1761
+ * the record is missing.
1762
+ */
1763
+ private refusedDecisionHandler;
1764
+ /**
1765
+ * Nonces already refused `nonce-not-issued` (APRV-456), oldest first, capped
1766
+ * at {@link TELEGRAM_REFUSED_NONCE_CAP}. A redelivered or replayed tap on the
1767
+ * same button is refused again with the same toast and records nothing, so
1768
+ * one button costs the log at most one record.
1769
+ */
1770
+ private readonly unissuedRefused;
1426
1771
  /**
1427
1772
  * What to do with a bot command (APRV-216). Absent unless the runtime asked
1428
1773
  * for commands, and its absence is what keeps `message` out of
@@ -1527,6 +1872,21 @@ export declare class TelegramChannel implements TestableChannel {
1527
1872
  private readonly counters;
1528
1873
  constructor(config: TelegramConfig);
1529
1874
  onDecision(handler: (decision: ChannelDecision) => DecisionOutcome): void;
1875
+ /**
1876
+ * Register where a tap this channel refused on its own is recorded
1877
+ * (APRV-456).
1878
+ *
1879
+ * Separate from {@link onDecision} on purpose: that handler's job is to call
1880
+ * the gate, and a `nonce-not-issued` tap must never reach the gate. The
1881
+ * runtime wires this to an audit-tier append that grants nothing.
1882
+ */
1883
+ onRefusedDecision(handler: (refused: TelegramRefusedDecision) => void): void;
1884
+ /**
1885
+ * Whether APRV-196's stale-copy fallback is on (APRV-456). Read by the
1886
+ * listener for the restart banner, which must not promise that earlier
1887
+ * copies still decide when they do not.
1888
+ */
1889
+ staleCopyEnabled(): boolean;
1530
1890
  /**
1531
1891
  * Register what to do with a checkpoint tap (APRV-257).
1532
1892
  *
@@ -1569,8 +1929,9 @@ export declare class TelegramChannel implements TestableChannel {
1569
1929
  * A unit like a checkpoint prompt: one thing to read, never grouped into a
1570
1930
  * digest, and never delivered through {@link notify} — a digest is a set of
1571
1931
  * similar pending REQUESTS decided together, and a sample is neither pending
1572
- * nor a request. It sends ONE message: no payload region, and a keyboard
1573
- * whose six buttons collect a verdict and a grade and mint nothing.
1932
+ * nor a request. It sends ONE message: the rows, the payload whole or by hash
1933
+ * (APRV-480, never split across messages), and a keyboard whose six buttons
1934
+ * collect a verdict and a grade and mint nothing.
1574
1935
  *
1575
1936
  * Refuses when no handler is registered, rather than sending a dead button.
1576
1937
  */
@@ -1666,6 +2027,28 @@ export declare class TelegramChannel implements TestableChannel {
1666
2027
  */
1667
2028
  private sendPrompt;
1668
2029
  private deliverOne;
2030
+ /**
2031
+ * Send `request` as ONE minimal card (APRV-489), or report why it was not.
2032
+ *
2033
+ * The card is drawn from the same `renderTelegram` output the technical card
2034
+ * would send, under the same layout, so the collapsed block IS the technical
2035
+ * card. The buttons carry the same callback data as the technical card's; only
2036
+ * the reject label reads "Deny" (R7). Nothing is sent when the card is refused,
2037
+ * so the caller's technical send is the only send.
2038
+ */
2039
+ private sendMinimal;
2040
+ /**
2041
+ * The style and `say` map in force for the delivery about to be drawn (fix
2042
+ * round 2, S4): the resolver's answer when the verb gave one, else the fixed
2043
+ * configuration. A resolver that throws is `technical`, the side that shows more.
2044
+ */
2045
+ private currentPrompt;
2046
+ /**
2047
+ * What a decision on a digest records about its layout (APRV-489): nothing
2048
+ * under a technical policy, and the technical card with its reason under a
2049
+ * minimal one, since a digest is always drawn technical.
2050
+ */
2051
+ private digestRendering;
1669
2052
  /**
1670
2053
  * Forget every nonce issued for `deliveryId`, and report the action key it
1671
2054
  * was issued for.
@@ -2113,9 +2496,32 @@ export declare class TelegramChannel implements TestableChannel {
2113
2496
  * stays open, and a fresh card is offered — and can never cost a record
2114
2497
  * nobody asked for.
2115
2498
  *
2116
- * A second prompt replaces the first: only one grade can be outstanding on
2117
- * one card, and the older prompt stops resolving so a late reply to it lands
2118
- * nowhere rather than recording a grade the human moved on from.
2499
+ * Only one grade can be outstanding on one card, so a second prompt that is
2500
+ * SENT replaces the first, and the older prompt stops resolving: a late reply
2501
+ * to it lands nowhere rather than recording a grade the human moved on from.
2502
+ *
2503
+ * APRV-492, and the relay licenses one prompt per GRADE tap, so which taps
2504
+ * get a prompt sent is not this method's choice alone. Three rules:
2505
+ *
2506
+ * - A tap that would ask the SAME question again (same verdict, same grade,
2507
+ * same account: a doubled OK or a doubled second Deny) sends nothing. The
2508
+ * prompt on screen already collects those words, and a second send would
2509
+ * be refused.
2510
+ * - A prompt for a DIFFERENT VERDICT retires the old one BEFORE the send. A
2511
+ * verdict change is not a grade tap, so the relay refuses that send, and an
2512
+ * old prompt left live would record the verdict the human just changed. A
2513
+ * note may be lost this way; a wrong verdict is never recorded.
2514
+ * - A prompt for the same verdict and a different grade follows a grade tap,
2515
+ * which the relay licenses, and retires the old one only once the new send
2516
+ * has succeeded. A send that fails anyway (the network, not the relay)
2517
+ * leaves the old prompt live, and a reply to it records the grade the old
2518
+ * prompt names in its own text, not the one the card now holds: an
2519
+ * accepted residual (PR #619 refutation S2).
2520
+ *
2521
+ * A send that fails says so on the card, because the toast already told the
2522
+ * human to reply to a prompt: the note was not asked for, nothing was
2523
+ * recorded, and a grade tap asks again. Then it throws, so the operator hears
2524
+ * of it. A later prompt that is sent clears that line.
2119
2525
  */
2120
2526
  private askForNote;
2121
2527
  /**
@@ -2145,6 +2551,35 @@ export declare class TelegramChannel implements TestableChannel {
2145
2551
  private recordReview;
2146
2552
  /** One `editMessageText` that replaces a review card's text and its keyboard. */
2147
2553
  private redrawReview;
2554
+ /**
2555
+ * Refuse a decision tap whose nonce this process is not holding, with the
2556
+ * stale-copy fallback off (APRV-456).
2557
+ *
2558
+ * What it does, in order, and what it never does:
2559
+ *
2560
+ * - It never calls the decision handler, so the gate is never asked. That is
2561
+ * the property: a button this process did not put on the screen cannot
2562
+ * carry a human's tap to `decide()`, whatever reference it carries.
2563
+ * - It looks the reference up among the deliveries THIS process holds open,
2564
+ * and only to name the action on the audit record. A reference that names
2565
+ * nothing open here produces no record: there is no action key this
2566
+ * process could truthfully write, and inventing one from bytes that came
2567
+ * off the network is the self-report §11.1 invariant 4 forbids.
2568
+ * - It hands at most one record per nonce to {@link onRefusedDecision}: a
2569
+ * redelivered or replayed tap on the same button is refused with the same
2570
+ * toast and records nothing.
2571
+ * - It leaves the live card alone. The request stays pending, and the card
2572
+ * this process did send still decides it.
2573
+ *
2574
+ * "Not holding" covers a nonce this process never issued and one it issued
2575
+ * and has since forgotten (an annotated card, a swept delivery, a restart).
2576
+ * It does not try to tell them apart: a set of every nonce ever issued would
2577
+ * reopen the hole for anyone who has seen one genuine button's bytes, because
2578
+ * an old nonce with a new reference would pass it. The cost is that a tap on
2579
+ * an earlier copy after a restart is refused on this shape and the approver
2580
+ * taps the newest card, which the restart banner says.
2581
+ */
2582
+ private refuseUnissued;
2148
2583
  private ignore;
2149
2584
  private answer;
2150
2585
  /**