approval-md 0.4.5 → 0.4.6

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 (44) hide show
  1. package/SPEC.md +13 -2
  2. package/dist/src/channels/telegram.d.ts +105 -31
  3. package/dist/src/channels/telegram.js +157 -81
  4. package/dist/src/channels/telegram.js.map +1 -1
  5. package/dist/src/cli/channel-telegram.d.ts +100 -35
  6. package/dist/src/cli/channel-telegram.js +216 -119
  7. package/dist/src/cli/channel-telegram.js.map +1 -1
  8. package/dist/src/cli/doctor.js +33 -13
  9. package/dist/src/cli/doctor.js.map +1 -1
  10. package/dist/src/cli/help.js +3 -3
  11. package/dist/src/cli/hook-exec-code.d.ts +150 -0
  12. package/dist/src/cli/hook-exec-code.js +357 -0
  13. package/dist/src/cli/hook-exec-code.js.map +1 -0
  14. package/dist/src/cli/hook.d.ts +125 -1
  15. package/dist/src/cli/hook.js +2177 -107
  16. package/dist/src/cli/hook.js.map +1 -1
  17. package/dist/src/cli/wordmark.d.ts +2 -2
  18. package/dist/src/cli/wordmark.js +2 -2
  19. package/dist/src/core/command-class.d.ts +349 -1
  20. package/dist/src/core/command-class.js +2347 -87
  21. package/dist/src/core/command-class.js.map +1 -1
  22. package/dist/src/core/exec-code.d.ts +77 -0
  23. package/dist/src/core/exec-code.js +60 -0
  24. package/dist/src/core/exec-code.js.map +1 -0
  25. package/dist/src/core/git-push-argv.d.ts +111 -0
  26. package/dist/src/core/git-push-argv.js +375 -0
  27. package/dist/src/core/git-push-argv.js.map +1 -0
  28. package/dist/src/core/harness-version.d.ts +71 -30
  29. package/dist/src/core/harness-version.js +101 -34
  30. package/dist/src/core/harness-version.js.map +1 -1
  31. package/dist/src/core/policy-load.d.ts +8 -0
  32. package/dist/src/core/policy-load.js.map +1 -1
  33. package/dist/src/daemon/git-evidence.d.ts +2 -2
  34. package/dist/src/daemon/git-evidence.js +1 -1
  35. package/docs/README-extended.md +3 -2
  36. package/docs/cli-reference.md +20 -7
  37. package/package.json +2 -2
  38. package/schema/event.schema.json +1 -1
  39. package/schema/fixtures/policy/invalid/exec-code-custody-boolean.json +11 -0
  40. package/schema/fixtures/policy/invalid/exec-code-execute-code-value.json +11 -0
  41. package/schema/fixtures/policy/invalid/exec-code-unknown-key.json +12 -0
  42. package/schema/fixtures/policy/valid/exec-code-both.json +17 -0
  43. package/schema/fixtures/policy/valid/exec-code-custody.json +11 -0
  44. package/schema/policy.schema.json +23 -0
package/SPEC.md CHANGED
@@ -135,6 +135,8 @@ channels:
135
135
 
136
136
  **The `delegation` block is reserved.** A policy MAY carry one top-level `delegation` block with the keys `model` (null, or a judge identity `model:<name>@<major>.<minor>.<patch>`), `classes` (exact keys of `classes`), `max_autonomy` (`manual`, `supervised-live` or `supervised-retro`; absent means `manual`), `daily_cap` (an integer from 0 to 1000; absent means 0), `escalate_on` (a subset of `deny`, `low_confidence`, `irreversible`, `unknown_class`), `advice` (boolean; absent means false) and `reviewers` (`human:<approver id>` or `model:<name>@<version>`). Implementations MUST validate the block's relationships to the rest of the file and fail the policy closed when one does not hold: every `classes` entry is an exact key of `classes` whose declared autonomy is neither `human-only` nor `autonomous`; every listed class declares an autonomy at least as strict as `max_autonomy` (a pin between the block and the class rows, so loosening a delegated row without rewriting the block fails the load); a `daily_cap` above 0 requires `escalate_on` to contain `irreversible` and `unknown_class`; a `model:` reviewer equals `model`; a `human:` reviewer names a key of `approvers`; and `advice: true`, a cap above 0 or a `model:` reviewer requires `model`. An implementation that does not implement delegation MUST load the block only when every key is absent or at its off value (`model: null`, `classes: []`, `max_autonomy: manual`, `daily_cap: 0`, `escalate_on: []`, `advice: false`, `reviewers: []`), where it changes nothing, and MUST refuse any other value with the load code `delegation-not-supported`, so the policy resolves every class `manual`. When a block both breaks one of the relationship rules above and is switched on, the relationship rule wins and the load fails `schema-invalid`: `delegation-not-supported` is answered only for a block that is well-formed and switched on. A setting its author believes is in force is never silently ignored. Delegation never changes a class's autonomy; it changes who may supply a decision or a review. (Amended APRV-500, pending sign-off.)
137
137
 
138
+ **The `exec` block opens opaque code (proposed).** A policy MAY carry one top-level `exec` block whose only member is `code`, with two keys: `custody` (`attested` or `unattested`) and `execute_code` (`hooked` or `unhooked`). An absent key is the closed answer. `exec.code.custody: attested` opens the `exec.code` class of §7; `exec.code.execute_code: hooked`, beside it, opens a harness tool that runs a program in-process. Any other key or value fails the policy closed at the schema, and so does the block on a core that predates it, which makes the rollout order core first, policy second. (Amended APRV-510, pending sign-off.)
139
+
138
140
  ### 5.2 Policy semantics
139
141
 
140
142
  - **Matching.** Classes match most-specific-first; `*` is a single-segment wildcard, a trailing `.*` matches any depth. An action whose class matches no rule takes `defaults.autonomy`. Implementations MUST fail closed: unparseable policy means everything is `manual`. A trailing `.*` consumes one or more segments: `read.*` matches `read.web` and `read.web.page`, and it does not match the bare class `read`. The schema admits `read` and `read.*` as two distinct keys a policy may list separately with different autonomy, so neither is an alias of the other, and a policy that wants the bare class covered writes it as its own rule or leaves it to `defaults.autonomy`. A bare `*` is a single-segment wildcard rather than a trailing one, so it matches any one-segment class and nothing deeper, and `*.*` matches any class of two or more segments. (Amended APRV-137.)
@@ -155,7 +157,7 @@ channels:
155
157
  - **Request-volume limits.** `limits.max_pending` (per class or global): maximum simultaneously pending requests; further requests are refused at intake with reason `queue-full`. `limits.requests_per_hour` (per origin): rolling-window ceiling on request creation; excess is refused with reason `rate-limited` and logged. Defaults are generous; the purpose is a tripwire against queue-flooding and approver fatigue, which attack the human's attention rather than the action budget. Refused requests consume no budget and no attention beyond the log line. (Policy vocabulary in v0.1; enforcement lands with M4/M5.)
156
158
  - **Agents MUST NOT be able to modify `APPROVAL.md`, the log, or channel credentials.** The reference runtime refuses to mint tokens if the policy file's mtime/hash changed without a signed `policy.updated` event from a human session. Attestation makes this mechanical: a human runs `approval policy attest`, which appends a `policy.updated` event carrying the SHA-256 of the policy file's bytes. Gate operations — request intake, grant recording, token minting — MUST refuse, with a distinct machine-readable reason, whenever the live file's hash differs from the latest attestation or no attestation exists. An edited policy is inoperative until a human re-attests it. Attestation also names the rules a decision was made under: gate-written `approval.requested` and `approval.granted` events carry `policy_sha256`, the attested hash in force when the runtime evaluated them. The field is assigned at the write boundary exactly as `ts` is (§8): no caller can supply it, and a value arriving from outside the runtime is refused. When the hash in force at grant time differs from the hash recorded on the matching request, the grant MUST refuse with its own reason, `policy-drift`, distinct from the unattested-file refusal above: the file is attested and is a different policy, so the pending request is void and must be re-requested under the rules now in force. The field is additive per §8: records written before it existed still validate and verify, and a verifier accepts both forms. (Amended APRV-118.)
157
159
  - **An attestation stores the bytes it attests.** An attestation records a digest, and a digest alone cannot produce the file it names, so a reader asking what the policy IN FORCE actually says has nothing to read. That matters wherever a rule has to be decided against the policy in force rather than against the one being proposed, which is exactly the privileged-gesture rule of §10.3: whoever edited a proposed file must not be able to map their own account as the one entitled to attest it. Every attestation — at a terminal and from a channel alike — MUST therefore store the attested text in the payload store of §10.4 and bind its hash on the `policy.updated` record it appends, and an implementation that cannot store those bytes MUST refuse the attestation rather than append a record whose bytes are unrecoverable. A reader recovering the text MUST re-hash it against the attested digest from the verified log and MUST treat a mismatch as unrecoverable, so the store is checked rather than trusted. The bytes of the policy currently in force are not prunable under `payload_retention` at any age, for the reason a live approval's material is not: something in force binds them. A chain attested before an implementation adopted this rule carries no such binding, and a reader MUST keep its fail-closed answer for that case rather than assume. (APRV-356.)
158
- - **Attestation of the gate's organs.** The organs are the harness files that install the enforcement hook (`.claude/settings*`, `.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents/`, `.codex/hooks.json`, `.codex/hooks/`, `.codex/config.toml`): the `policy.core` surface outside the approval home and outside the policy file itself. Three organs are not repository-relative: a Hermes Agent home's `$HERMES_HOME/config.yaml`, `$HERMES_HOME/agent-hooks/` and `$HERMES_HOME/shell-hooks-allowlist.json`, with `$HERMES_HOME/.env` and `$HERMES_HOME/auth.json` beside them, a read of which classifies `account.credential`. Their protection is conditional on the spelling: the classifier recognises them by a `.hermes` path segment, so a home whose last segment is spelled otherwise is not protected, and the set is not closed under every `HERMES_HOME`. (Amended APRV-415.) A human attests one by content, exactly as they attest the policy file, with `approval policy attest --organ <path>`, which appends a `gate.organ.attested` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, one path per call, and the digest is computed by the runtime from the file on disk: a caller who could supply the hash could attest bytes nobody read. The organs need this because they are `policy.core` and a policy may resolve `policy.core` to `human-only`, in which case the gate mints no record of any kind for a change to one (§11.1 invariant 9), so grant-shaped evidence for a hand edit cannot exist however carefully the edit was made. What reads these records is the after-the-fact enforcement of §10.1: a checker requiring evidence that a human saw a protected change accepts an organ whose bytes at the commit under review hash to a digest attested FOR THAT SAME PATH, and a digest attested for another path is not evidence. What does NOT read them is the gate: an organ attestation MUST NOT make an unattested policy operative, MUST NOT alter the `policy_sha256` a request or a grant is decided under, and MUST NOT satisfy any check about the policy file. Implementations SHOULD discharge that structurally, by giving the organ record its own event type, rather than by filtering a shared one in every reader. (APRV-272.)
160
+ - **Attestation of the gate's organs.** The organs are the harness files that install the enforcement hook (`.claude/settings*`, `.cursor/hooks.json`, `.cursor/hooks/`, `.cursor/agents/`, `.codex/hooks.json`, `.codex/hooks/`, `.codex/config.toml`): the `policy.core` surface outside the approval home and outside the policy file itself. Five organs are not repository-relative: a Hermes Agent home's `$HERMES_HOME/config.yaml`, `$HERMES_HOME/agent-hooks/`, `$HERMES_HOME/shell-hooks-allowlist.json`, `$HERMES_HOME/hermes-agent/` (the runtime's source install) and `$HERMES_HOME/plugins/` (where its plugins load from), with `$HERMES_HOME/.env`, `$HERMES_HOME/.op.env`, `$HERMES_HOME/auth.json` and `$HERMES_HOME/hermes-agent/.env` beside them, a read of which classifies `account.credential`. (Amended APRV-511, pending sign-off.) Their protection is conditional on the spelling: the classifier recognises them by a `.hermes` path segment (in any case), by an unexpanded `$HERMES_HOME`, and, for the organ and credential rows only, by any other unexpanded variable or command substitution that may stand for the home, so a home whose last segment is spelled otherwise is not protected, and the set is not closed under every `HERMES_HOME`. (Amended APRV-415.) A human attests one by content, exactly as they attest the policy file, with `approval policy attest --organ <path>`, which appends a `gate.organ.attested` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, one path per call, and the digest is computed by the runtime from the file on disk: a caller who could supply the hash could attest bytes nobody read. The organs need this because they are `policy.core` and a policy may resolve `policy.core` to `human-only`, in which case the gate mints no record of any kind for a change to one (§11.1 invariant 9), so grant-shaped evidence for a hand edit cannot exist however carefully the edit was made. What reads these records is the after-the-fact enforcement of §10.1: a checker requiring evidence that a human saw a protected change accepts an organ whose bytes at the commit under review hash to a digest attested FOR THAT SAME PATH, and a digest attested for another path is not evidence. What does NOT read them is the gate: an organ attestation MUST NOT make an unattested policy operative, MUST NOT alter the `policy_sha256` a request or a grant is decided under, and MUST NOT satisfy any check about the policy file. Implementations SHOULD discharge that structurally, by giving the organ record its own event type, rather than by filtering a shared one in every reader. (APRV-272.)
159
161
  - **Sign-off on a protected path.** A protected path is a file whose edit classifies `policy.edit` or one of its sub-classes: the built-in prose and configuration set, plus everything `protected_paths` widens to. A human signs one off by content with `approval policy attest --path <path>`, which appends a `gate.path.signed_off` event carrying the file's repository-relative path and the SHA-256 of its bytes. The verb is human-only, takes one path per call, and the digest is computed by the runtime from the file on disk, for the reasons the organ attestation above carries: a caller who could supply the hash could ratify bytes nobody read. This record is what RESOLVES the amendment-provenance suffix stated at the head of this document. Text that reached a protected file without a grant carries `(Amended APRV-n, pending sign-off.)` and holds no more authority than a proposal until a human ratifies it; a sign-off over that file's exact bytes is the ratification, and until one exists the suffix stands. It is WHOLE-FILE evidence, and therefore weaker than a grant, which binds the exact change a human was shown. Two rules follow, and both are normative. The after-the-fact enforcement of §10.1 MUST consult a sign-off only after its search for authorization covering the change has failed, and MUST state in its finding that the verdict rests on whole-file evidence; a change some authorization does cover passes on that authorization and names it. And a sign-off is appropriate only for ratifying text a human has READ at that commit: it answers for an edit the gate never saw, and it is never a substitute for taking an edit to the gate while one still can be. The three content records stay distinct, on the reasoning the organ bullet gives: the policy file is `policy.updated`, an organ is `gate.organ.attested`, a protected path is `gate.path.signed_off`, no reader of one may be answered by another, and the verb MUST refuse the policy file, every `policy.core` surface and the log directory, each with its own machine-readable code. (APRV-338.)
160
162
  - **`payload_retention`.** An optional top-level duration bounding how long the payload bytes in `.approval/payloads/` (§9) are kept. A payload is prunable once the action it is bound to has been in a terminal state (`executed`, `rejected`, `expired`, `revoked`) for longer than the duration. A payload whose action is not terminal is never prunable, at any age: a pending or granted approval binds to those exact bytes, and discarding them would leave a live authorization pointing at nothing. When the key is present, orphaned payloads (bytes with no recorded binding) are prunable at any age: the duration governs bound payloads and does not gate residue nothing ever bound. When the key is absent, the pruning subsystem does not run and nothing is deleted, orphaned or not; the store holds the material evidence of what a human approved, so forgetting anything is an operator's explicit choice, and an operator who never made that choice never asked the runtime to delete anything (amended APRV-49 to match the enforcement shipped in APRV-41). Pruning is performed by the daemon and by nothing else, and each removal appends a `payload.pruned` event, so a log states what its store no longer holds. The bytes an attestation binds (§5.2, §10.4) are never prunable while that attestation is the one in force: they are not bound to an action and have no terminal state, and the policy in force binds them exactly as a live approval binds its own material. (Policy vocabulary in v0.1; enforcement lands with the M5 daemon. The attestation sentence is APRV-356.)
161
163
  - **`protected_paths`.** An optional top-level list widening the set of files whose edit is classified `policy.edit`, so a project can put its own governing documents (a specification, a constitution, a design directory) behind the gate that already stands in front of `APPROVAL.md`. Entries are repo-relative and literal: an exact file path (`SPEC.md`, `docs/constitution.md`) or a directory prefix ending in `/` (`design/`). Globs, negation, absolute paths and `..` segments are schema violations, because a pattern language the runtime half-implemented would leave an author believing a file is gated when it is not. Matching is by path segments and never resolves against a checkout, so a linked worktree and the primary answer alike: an exact path matches a candidate whose trailing segments are that path (a single-segment entry therefore matches that filename in any directory, exactly as the built-in filenames do), and a directory prefix matches a candidate containing those segments as a contiguous run. The key is ADDITIVE and can only widen: the runtime's built-in protected set (the policy file, the agent instruction files, the approval home, the harness settings, the release configuration) stays protected whatever this list says or omits, and a policy that fails to load leaves those built-ins in force while every class resolves to `manual`. (Amended APRV-107.) Every path a policy adds is `policy.edit`: a policy widening its own protected surface is naming prose and configuration, and cannot mint authority over the gate's organs, whose classes the runtime fixes. (Amended APRV-198.)
@@ -300,7 +302,7 @@ The developer-workstation namespaces below are reserved alongside them. They nam
300
302
  | `vcs.*` | `.commit.branch`, `.push.branch`, `.push.main`, `.history.rewrite`, `.ref.delete` | autonomous for branch commits and pushes; supervised for the trunk; manual for history rewrites and for remote ref deletion |
301
303
  | `deps.*` | `.add`, `.upgrade`, `.remove` | manual: a dependency change is a supply-chain decision |
302
304
  | `release.*` | `.publish`, `.tag`, `.version` | manual, always |
303
- | `exec.*` | `.local` (tests, lint, build, scripts inside the workspace) | autonomous |
305
+ | `exec.*` | `.local` (tests, lint, build, scripts inside the workspace), `.code` (an opaque program: inline source or an interpreter running a file that is not a skill script; closed unless the attested policy attests credential custody) | autonomous for `.local`; `.code` as the policy says once opened (Amended APRV-510, pending sign-off.) |
304
306
  | `network.*` | `.call` (any request beyond package installs) | manual |
305
307
  | `policy.*` | `.edit` (agent instructions, CI and release configuration, and the paths a policy protects), `.core` (the policy file itself and the gate's own directory, minus its log) | manual, always; a policy MAY declare `.core` human-only |
306
308
  | `log.*` | `.sync`, `.advance` (section 10.1), `.mutate` (any write, redirect, append, truncation or rename aimed at the log directory) | manual, always; a policy MAY declare `.mutate` human-only |
@@ -315,6 +317,8 @@ The `policy.*` split is by consequence rather than by file type. `policy.edit` i
315
317
 
316
318
  `vcs.ref.delete` is the deletion of a remote ref: `git push <remote> --delete <ref>`, the `-d` spelling, the colon refspec (`:refs/heads/x`, `:x`), and the bulk forms that mix several. It is separate from `vcs.push.main` because a push and a deletion are different acts, and a policy that sampled trunk pushes retrospectively was thereby sampling irreversible removals: a push adds commits that remain visible, while a deletion removes the only name an unmerged branch had, and the reflog that could recover it sits on a server the acting party cannot reach. It is separate from `vcs.history.rewrite`, which guards shared history that is being moved rather than removed. An implementation that offers the class MUST bind the ref names it matched to the classified action, MUST keep a force push in `vcs.history.rewrite` even when the same command deletes, and MUST NOT use the class to loosen a deletion that already takes a stricter one: a tag deletion stays `release.publish` wherever a tag push does, and a deletion whose refs the text cannot read stays in the class with nothing bound rather than falling back to a push class (§11.1, fail closed). A policy that declares no line for the class resolves it by `defaults.autonomy` like any other, so adopting it is an explicit act. (Amended APRV-352.)
317
319
 
320
+ `exec.code` is an opaque program an agent runs: an inline program (`python3 -c`, `node -e`, `deno eval` and kin) or an interpreter running a file that is not a skill script. Its effect is unreadable from the text, so an implementation MUST refuse it before policy unless the attested policy declares `exec.code.custody: attested` (§5.2), the operator's statement that credential custody is in place; a policy that never names the class MUST see the refusals it saw before the class existed, unchanged. Once opened, the class resolves like any other. A record of it MUST bind the program's SHA-256 and length and MUST NOT carry the program's text (§11.1 invariant 3), and it records that the program existed, never what it did: it authorizes nothing the program then does through other tools, each of which is its own gated call, and an implementation MUST NOT present it as containment. A harness tool that runs a program in-process (Hermes `execute_code`) MAY be classified `exec.code` only under a second declaration (`exec.code.execute_code: hooked`) that records the operator's acceptance that the harness re-fires its hook for tool calls made inside the program. Shell wrappers (`bash -c`, `sh -c`), `eval`, a program read from stdin, and a program word the shell would still change are outside the class, and an open window does not reach it. (Amended APRV-510, pending sign-off.)
321
+
318
322
  `files.delete.out_of_scope` (destructive deletes outside the task's stated scope, inside the workspace) sits under the existing `files` namespace at manual; `data.delete` remains the class for deletes outside the workspace.
319
323
 
320
324
  | Namespace | Examples | Default gravity |
@@ -863,6 +867,13 @@ Three properties bind the whole registry. A code fires for exactly the condition
863
867
  | `log-corrupt` | The chain does not verify. |
864
868
  | `append-failed` | The write boundary refused the record; the writer's own code rides in the detail. |
865
869
 
870
+ **Harness-hook refusals for `exec.code`** (Amended APRV-510, pending sign-off). Two members of the harness hook's deny vocabulary, registered here because a second implementation of the class has to produce the same split. Evaluated after classification: a policy that does not name `exec.code` (no `exec` block and no exact `exec.code` class key) gets the refusal the command got before the class existed (`hook-opaque`, `hook-unclassified`, or Hermes's `hook-hermes-execute-code-unbound`), byte for byte, and neither code below fires. Both append nothing.
871
+
872
+ | code | fires when |
873
+ |---|---|
874
+ | `hook-exec-code-custody-unattested` | The call is an opaque program the class would cover, and the policy names `exec.code` without declaring `exec.code.custody: attested`. The detail names the key. For Hermes `execute_code` it also fires for `execute_code: hooked` without custody; with custody and without `execute_code: hooked`, `execute_code` keeps `hook-hermes-execute-code-unbound`. |
875
+ | `hook-exec-code-unreadable` | The class is open and a program FILE cannot be read and hashed by the hook: it is absent, unreadable by the hook's process, not a regular file, above the size the implementation hashes, or named relatively after a segment that moves the working directory. The same code refuses a file the hook cannot place as the kernel would: a path with a `..` component, or a `.` or empty component after the first; a directory that is not its own realpath (a symlinked working directory included); a line with any segment besides the program's; or a call for which the harness states no absolute working directory. It also refuses a command or Hermes `execute_code` program that is not well-formed text (a lone UTF-16 surrogate), which has no exact bytes to hash. Checked before any resolution. A file that resolves onto a credential or protected path is refused `hook-unclassified` instead. |
876
+
866
877
  ## 12. Interoperability
867
878
 
868
879
  - **Backlog.md:** native. Tasks live in `backlog/`, the envelope is one preserved frontmatter key, board `status` and approval `state` are independent. approval.md ships no board; use Backlog.md's.
@@ -296,27 +296,72 @@ export declare const TELEGRAM_STALE_UNKNOWN = "This request is not open here \u2
296
296
  */
297
297
  export declare const TELEGRAM_PROMPT_HEADING = "APPROVAL REQUIRED";
298
298
  /**
299
- * What the label over the payload chunks names (APRV-162).
299
+ * What the label over the payload chunks names (APRV-162), in a resident's
300
+ * words (P-9).
300
301
  *
301
302
  * The chunks carry the canonical rendering, which is a deterministic function
302
- * of the bytes and not the bytes themselves; calling it "the exact bytes" told
303
- * the reader that a diff view and a JSON file were the same object. The
304
- * rendering names its own `display_hash`, and the store path inside it is the
305
- * route back to the bytes.
303
+ * of the bytes and not the bytes themselves, and the rendering names its own
304
+ * `display_hash` and store path inside the block (SPEC.md §10.3, bound
305
+ * verbatim by `display_hash`). This label sits OUTSIDE that block, so it is
306
+ * free to say what the block is for the person reading it: what they are
307
+ * approving. Before P-9 it said "the canonical rendering this approval's
308
+ * display_hash names; raw bytes at the store path inside", which is the
309
+ * block's own first lines restated in the system's vocabulary.
306
310
  */
307
- export declare const PAYLOAD_CHUNK_LABEL_TAIL = "the canonical rendering this approval's display_hash names; raw bytes at the store path inside";
308
- export declare const PAYLOAD_CHUNK_LABEL = "PAYLOAD \u2014 the canonical rendering this approval's display_hash names; raw bytes at the store path inside";
311
+ export declare const PAYLOAD_CHUNK_LABEL_TAIL = "what you are approving";
312
+ export declare const PAYLOAD_CHUNK_LABEL = "Exactly what you are approving";
313
+ /**
314
+ * The plain-words label over lines the requesting agent wrote (P-9).
315
+ *
316
+ * The same words the minimal card uses for the agent's own summary: the reader
317
+ * learns that the agent said it and that nobody checked it, which is what the
318
+ * old "CLAIMED ... NOT verified by the runtime" said in the system's
319
+ * vocabulary. The author id stays after it, because which agent said a thing
320
+ * is part of what an approver weighs (APRV-144).
321
+ */
322
+ export declare const TELEGRAM_CLAIMED_LABEL = "What your agent says (not checked)";
309
323
  /**
310
324
  * What the claimed block is headed, and what a second claimed message is headed
311
325
  * when a rationale overflows one (APRV-165).
312
326
  *
313
- * Both say CLAIMED and both say NOT verified, because a continuation is a
314
- * message a reader may see first, and a claimed line that arrives under no
315
- * heading at all reads as the runtime's own.
327
+ * Both say the lines are the agent's and not checked, because a continuation
328
+ * is a message a reader may see first, and a claimed line that arrives under
329
+ * no heading at all reads as the runtime's own.
316
330
  */
317
- export declare const TELEGRAM_CLAIMED_HEADING_PREFIX = "WHAT THIS DOES \u2014 CLAIMED by";
318
- export declare const TELEGRAM_CLAIMED_HEADING_SUFFIX = "NOT verified by the runtime";
319
- export declare const TELEGRAM_CLAIMED_CONTINUED_HEADING = "WHAT THIS DOES (continued) \u2014 CLAIMED, NOT verified by the runtime";
331
+ export declare const TELEGRAM_CLAIMED_HEADING_PREFIX = "What your agent says (not checked) \u2014";
332
+ export declare const TELEGRAM_CLAIMED_CONTINUED_HEADING = "What your agent says (continued, not checked)";
333
+ /**
334
+ * The heading over the rows the runtime derived itself (P-9): the class, the
335
+ * wait, the budget. Before P-9 it read "COMPUTED — derived by the runtime from
336
+ * the log, the policy and the payload bytes". The rows keep their origin tags,
337
+ * which the village relay reads (its quiet hold matches the `ttl` row up to
338
+ * its `<i>(` tag).
339
+ */
340
+ export declare const TELEGRAM_COMPUTED_HEADING = "Checked by approval.md";
341
+ /** The most characters of an author id the claimed label shows (fold round 1, S3). */
342
+ export declare const TELEGRAM_CLAIMED_AUTHOR_MAX = 80;
343
+ /**
344
+ * {@link TELEGRAM_CLAIMED_LABEL} naming who wrote the lines under it.
345
+ *
346
+ * The author is the requester's actor id, which the actor rule
347
+ * (`^(human|agent):.+`) checks only at its start: a line break later in it
348
+ * passes. Escaping stops markup, but not a second line inside the bold heading
349
+ * that reads like the runtime's own "Checked by approval.md" (fold round 1,
350
+ * S3). So every run of whitespace, line breaks included, becomes one space,
351
+ * control and format characters are dropped, and the id is cut at
352
+ * {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis. The label is
353
+ * always one line.
354
+ */
355
+ export declare function claimedLabelFor(author: string): string;
356
+ /**
357
+ * An author id as one short line (fold round 1, S3): whitespace runs, line
358
+ * breaks included, become one space; control and format characters go; the id
359
+ * is cut at {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis.
360
+ * Used by {@link claimedLabelFor} and by the `(author)` tag on each claimed
361
+ * row, which carries the same id. A computed row's origin is a fixed word, so
362
+ * this changes nothing there.
363
+ */
364
+ export declare function oneLineAuthor(author: string): string;
320
365
  /**
321
366
  * The most members one digest may carry (APRV-115).
322
367
  *
@@ -909,6 +954,13 @@ export interface DigestMemberState {
909
954
  summary: string;
910
955
  /** The agent's cost estimate, formatted. Claimed. */
911
956
  cost: string;
957
+ /**
958
+ * The member's class, computed, set only when the digest's members differ in
959
+ * class (APRV-287: the several classes of one tool call). Their summaries are
960
+ * then one command's summary repeated, and the class is what tells the lines
961
+ * apart and what each "Approve" covers (P-9, in place of the action key).
962
+ */
963
+ kind?: string;
912
964
  /**
913
965
  * The terminal outcome, once one has been observed for this member. Written
914
966
  * only from a gate record or the verified log, never inferred here.
@@ -937,8 +989,28 @@ export interface DigestMemberState {
937
989
  * terminal.
938
990
  */
939
991
  export interface StaleSummary {
940
- /** Computed lines: how many, how old the oldest is, which classes. */
992
+ /**
993
+ * The reminder this message opens with on a restart (P-9), the first line in
994
+ * bold: how many requests this cycle re-sends in all, and, when cards follow
995
+ * it, whether the buttons on older messages still work. It replaces the
996
+ * separate re-delivery banner for that cycle. Shown only while every member
997
+ * is still open: once one is decided, the count it states is no longer true.
998
+ */
999
+ reminder?: string[];
1000
+ /** Computed lines: how many are listed, and why nobody is waiting on them. */
941
1001
  lines: string[];
1002
+ /**
1003
+ * How long ago each member was asked, computed, in member order (P-9). Shown
1004
+ * beside each claimed summary so the members can be told apart without an
1005
+ * action key.
1006
+ */
1007
+ ages?: string[];
1008
+ /**
1009
+ * Whether the buttons on earlier copies still decide (APRV-456), which the
1010
+ * footer's advice depends on. Filled from the channel's own setting by
1011
+ * {@link TelegramChannel.notifyStale} when the caller leaves it out.
1012
+ */
1013
+ staleCopy?: boolean;
942
1014
  }
943
1015
  /** One digest message, as the delivering process remembers it. */
944
1016
  export interface DigestState {
@@ -948,12 +1020,11 @@ export interface DigestState {
948
1020
  batchDeliveryId: DeliveryId;
949
1021
  /** The nonce the "all" buttons were issued under. */
950
1022
  allNonce: string;
951
- /** The computed facts every member shares, already rendered as text. */
952
- facts: {
953
- label: string;
954
- text: string;
955
- origin: string;
956
- }[];
1023
+ /**
1024
+ * The one computed line the digest states about its members, already
1025
+ * rendered as text ({@link digestLine}); empty for a stale summary.
1026
+ */
1027
+ line: string;
957
1028
  /**
958
1029
  * The collapsed re-delivery this message is, or `null` for an ordinary digest
959
1030
  * (APRV-287). See {@link StaleSummary}.
@@ -979,19 +1050,22 @@ export interface DigestState {
979
1050
  windowMs: number | null;
980
1051
  }
981
1052
  /**
982
- * The computed facts a digest's members share, as the digest states them.
1053
+ * The one computed line a digest states about its members (P-9).
1054
+ *
1055
+ * Read off the members themselves, which is sound precisely because the
1056
+ * grouping key made them alike: a digest whose members disagreed about their
1057
+ * class or their task is a digest the listener would not have built. What the
1058
+ * line says is the thing an approver needs before the buttons: why these are
1059
+ * one message, and that every request it decides is shown in full above it
1060
+ * (SPEC.md §10.3).
983
1061
  *
984
- * Every one is read off the first member, which is sound precisely because the
985
- * grouping key made them equal across the set: a digest whose members disagreed
986
- * about their class or their task is a digest the listener would not have
987
- * built. The last line is the one an approver needs most — it says how many
988
- * payloads are above and that each request has its own.
1062
+ * Before P-9 this was five facts with origin tags: the class name, the
1063
+ * autonomy, the task id, the grouping key (`keys:body,from,subject,to`) and the
1064
+ * payload binding. Each is still on every member's own card above, where an
1065
+ * operator who wants it finds it; the digest is where a resident answers, and
1066
+ * there it was ids and vocabulary between them and the buttons.
989
1067
  */
990
- export declare function digestFacts(members: ChannelRequest[]): {
991
- label: string;
992
- text: string;
993
- origin: string;
994
- }[];
1068
+ export declare function digestLine(members: ChannelRequest[]): string;
995
1069
  /**
996
1070
  * The digest message: text plus the keyboard for whatever is still open.
997
1071
  *