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
@@ -310,27 +310,84 @@ export const TELEGRAM_STALE_UNKNOWN = "This request is not open here — it was
310
310
  */
311
311
  export const TELEGRAM_PROMPT_HEADING = "APPROVAL REQUIRED";
312
312
  /**
313
- * What the label over the payload chunks names (APRV-162).
313
+ * What the label over the payload chunks names (APRV-162), in a resident's
314
+ * words (P-9).
314
315
  *
315
316
  * The chunks carry the canonical rendering, which is a deterministic function
316
- * of the bytes and not the bytes themselves; calling it "the exact bytes" told
317
- * the reader that a diff view and a JSON file were the same object. The
318
- * rendering names its own `display_hash`, and the store path inside it is the
319
- * route back to the bytes.
317
+ * of the bytes and not the bytes themselves, and the rendering names its own
318
+ * `display_hash` and store path inside the block (SPEC.md §10.3, bound
319
+ * verbatim by `display_hash`). This label sits OUTSIDE that block, so it is
320
+ * free to say what the block is for the person reading it: what they are
321
+ * approving. Before P-9 it said "the canonical rendering this approval's
322
+ * display_hash names; raw bytes at the store path inside", which is the
323
+ * block's own first lines restated in the system's vocabulary.
320
324
  */
321
- export const PAYLOAD_CHUNK_LABEL_TAIL = "the canonical rendering this approval's display_hash names; raw bytes at the store path inside";
322
- export const PAYLOAD_CHUNK_LABEL = `PAYLOAD — ${PAYLOAD_CHUNK_LABEL_TAIL}`;
325
+ export const PAYLOAD_CHUNK_LABEL_TAIL = "what you are approving";
326
+ export const PAYLOAD_CHUNK_LABEL = `Exactly ${PAYLOAD_CHUNK_LABEL_TAIL}`;
327
+ /**
328
+ * The plain-words label over lines the requesting agent wrote (P-9).
329
+ *
330
+ * The same words the minimal card uses for the agent's own summary: the reader
331
+ * learns that the agent said it and that nobody checked it, which is what the
332
+ * old "CLAIMED ... NOT verified by the runtime" said in the system's
333
+ * vocabulary. The author id stays after it, because which agent said a thing
334
+ * is part of what an approver weighs (APRV-144).
335
+ */
336
+ export const TELEGRAM_CLAIMED_LABEL = "What your agent says (not checked)";
323
337
  /**
324
338
  * What the claimed block is headed, and what a second claimed message is headed
325
339
  * when a rationale overflows one (APRV-165).
326
340
  *
327
- * Both say CLAIMED and both say NOT verified, because a continuation is a
328
- * message a reader may see first, and a claimed line that arrives under no
329
- * heading at all reads as the runtime's own.
341
+ * Both say the lines are the agent's and not checked, because a continuation
342
+ * is a message a reader may see first, and a claimed line that arrives under
343
+ * no heading at all reads as the runtime's own.
344
+ */
345
+ export const TELEGRAM_CLAIMED_HEADING_PREFIX = `${TELEGRAM_CLAIMED_LABEL} —`;
346
+ export const TELEGRAM_CLAIMED_CONTINUED_HEADING = "What your agent says (continued, not checked)";
347
+ /**
348
+ * The heading over the rows the runtime derived itself (P-9): the class, the
349
+ * wait, the budget. Before P-9 it read "COMPUTED — derived by the runtime from
350
+ * the log, the policy and the payload bytes". The rows keep their origin tags,
351
+ * which the village relay reads (its quiet hold matches the `ttl` row up to
352
+ * its `<i>(` tag).
353
+ */
354
+ export const TELEGRAM_COMPUTED_HEADING = "Checked by approval.md";
355
+ /** The most characters of an author id the claimed label shows (fold round 1, S3). */
356
+ export const TELEGRAM_CLAIMED_AUTHOR_MAX = 80;
357
+ /**
358
+ * {@link TELEGRAM_CLAIMED_LABEL} naming who wrote the lines under it.
359
+ *
360
+ * The author is the requester's actor id, which the actor rule
361
+ * (`^(human|agent):.+`) checks only at its start: a line break later in it
362
+ * passes. Escaping stops markup, but not a second line inside the bold heading
363
+ * that reads like the runtime's own "Checked by approval.md" (fold round 1,
364
+ * S3). So every run of whitespace, line breaks included, becomes one space,
365
+ * control and format characters are dropped, and the id is cut at
366
+ * {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis. The label is
367
+ * always one line.
368
+ */
369
+ export function claimedLabelFor(author) {
370
+ return `${TELEGRAM_CLAIMED_LABEL} — ${oneLineAuthor(author)}`;
371
+ }
372
+ /**
373
+ * An author id as one short line (fold round 1, S3): whitespace runs, line
374
+ * breaks included, become one space; control and format characters go; the id
375
+ * is cut at {@link TELEGRAM_CLAIMED_AUTHOR_MAX} characters with an ellipsis.
376
+ * Used by {@link claimedLabelFor} and by the `(author)` tag on each claimed
377
+ * row, which carries the same id. A computed row's origin is a fixed word, so
378
+ * this changes nothing there.
330
379
  */
331
- export const TELEGRAM_CLAIMED_HEADING_PREFIX = "WHAT THIS DOES — CLAIMED by";
332
- export const TELEGRAM_CLAIMED_HEADING_SUFFIX = "NOT verified by the runtime";
333
- export const TELEGRAM_CLAIMED_CONTINUED_HEADING = `WHAT THIS DOES (continued) — CLAIMED, ${TELEGRAM_CLAIMED_HEADING_SUFFIX}`;
380
+ export function oneLineAuthor(author) {
381
+ const flat = author
382
+ .replace(/\s+/gu, " ")
383
+ .replace(/[\p{Cc}\p{Cf}]/gu, "")
384
+ .trim();
385
+ const chars = Array.from(flat);
386
+ const shown = chars.length > TELEGRAM_CLAIMED_AUTHOR_MAX
387
+ ? `${chars.slice(0, TELEGRAM_CLAIMED_AUTHOR_MAX - 1).join("")}…`
388
+ : flat;
389
+ return shown.length === 0 ? "unknown" : shown;
390
+ }
334
391
  /**
335
392
  * The most members one digest may carry (APRV-115).
336
393
  *
@@ -736,16 +793,16 @@ export function renderTelegram(request, heading = TELEGRAM_PROMPT_HEADING, layou
736
793
  claimedLines.push(entry);
737
794
  }
738
795
  const author = request.summary.kind === "claimed" ? request.summary.author : "the requesting party";
739
- const render = (entry) => `• <b>${escapeHtml(entry.label)}:</b> ${escapeHtml(entry.text)} <i>(${escapeHtml(entry.origin)})</i>`;
796
+ const render = (entry) => `• <b>${escapeHtml(entry.label)}:</b> ${escapeHtml(entry.text)} <i>(${escapeHtml(oneLineAuthor(entry.origin))})</i>`;
740
797
  const header = [
741
798
  `<b>${escapeHtml(heading)}</b>`,
742
799
  `<code>${escapeHtml(request.action_key.value)}</code>`,
743
800
  "",
744
- "<b>COMPUTED — derived by the runtime from the log, the policy and the payload bytes</b>",
801
+ `<b>${TELEGRAM_COMPUTED_HEADING}</b>`,
745
802
  ...computedLines.map(render),
746
803
  ].join("\n");
747
804
  const claimedText = [
748
- `<b>${TELEGRAM_CLAIMED_HEADING_PREFIX} ${escapeHtml(author)}, ${TELEGRAM_CLAIMED_HEADING_SUFFIX}</b>`,
805
+ `<b>${escapeHtml(claimedLabelFor(author))}</b>`,
749
806
  ...claimedLines.map(render),
750
807
  ].join("\n");
751
808
  return {
@@ -1048,44 +1105,33 @@ export function groupForDigest(requests, max = TELEGRAM_DIGEST_MAX_MEMBERS) {
1048
1105
  return groups;
1049
1106
  }
1050
1107
  /**
1051
- * The computed facts a digest's members share, as the digest states them.
1108
+ * The one computed line a digest states about its members (P-9).
1052
1109
  *
1053
- * Every one is read off the first member, which is sound precisely because the
1054
- * grouping key made them equal across the set: a digest whose members disagreed
1055
- * about their class or their task is a digest the listener would not have
1056
- * built. The last line is the one an approver needs most — it says how many
1057
- * payloads are above and that each request has its own.
1110
+ * Read off the members themselves, which is sound precisely because the
1111
+ * grouping key made them alike: a digest whose members disagreed about their
1112
+ * class or their task is a digest the listener would not have built. What the
1113
+ * line says is the thing an approver needs before the buttons: why these are
1114
+ * one message, and that every request it decides is shown in full above it
1115
+ * (SPEC.md §10.3).
1116
+ *
1117
+ * Before P-9 this was five facts with origin tags: the class name, the
1118
+ * autonomy, the task id, the grouping key (`keys:body,from,subject,to`) and the
1119
+ * payload binding. Each is still on every member's own card above, where an
1120
+ * operator who wants it finds it; the digest is where a resident answers, and
1121
+ * there it was ids and vocabulary between them and the buttons.
1058
1122
  */
1059
- export function digestFacts(members) {
1123
+ export function digestLine(members) {
1060
1124
  const first = members[0];
1061
1125
  if (first === undefined)
1062
- return [];
1063
- // APRV-287. A digest may now carry the several classes of ONE tool call, so
1064
- // the class line states the set rather than the first member's, and the
1065
- // grouping line says which of the two groupings put this set together. A
1066
- // digest whose members share one class reads exactly as it did.
1067
- const classes = [...new Set(members.map((member) => member.class.value))];
1068
- const shared = sharedPayload(members);
1069
- const autonomies = [...new Set(members.map((member) => member.autonomy.value))];
1070
- return [
1071
- { label: "class", text: classes.join(", "), origin: originOf(first.class) },
1072
- { label: "autonomy", text: autonomies.join(", "), origin: originOf(first.autonomy) },
1073
- { label: "task", text: first.task.value ?? "(none)", origin: originOf(first.task) },
1074
- {
1075
- label: "grouped by",
1076
- text: shared === null
1077
- ? `one class, one task, one payload shape (${payloadShapeKey(first.fullPayload.value?.value)})`
1078
- : `one tool call: one task, one payload, ${String(classes.length)} class(es) of the same command`,
1079
- origin: "grouping",
1080
- },
1081
- {
1082
- label: "payloads",
1083
- text: shared === null
1084
- ? `${members.length} full payloads, one per request, in the ${members.length} prompts above this message`
1085
- : `one payload, shared by all ${members.length} requests, in the prompt above this message`,
1086
- origin: originOf(first.fullPayload),
1087
- },
1088
- ];
1126
+ return "";
1127
+ const count = String(members.length);
1128
+ if (sharedPayload(members) !== null) {
1129
+ return `All ${count} are about one action, shown once in full above.`;
1130
+ }
1131
+ const task = first.task.value;
1132
+ return task === null || task.length === 0
1133
+ ? `All ${count} are the same kind of request. Each one is shown in full above.`
1134
+ : `All ${count} come from the same task. Each one is shown in full above.`;
1089
1135
  }
1090
1136
  /**
1091
1137
  * The one payload every member is about, or `null` when they differ
@@ -1113,36 +1159,56 @@ function sharedPayload(members) {
1113
1159
  }
1114
1160
  /**
1115
1161
  * The collapsed re-delivery's own message: what is waiting, and one way to
1116
- * clear it (APRV-287).
1162
+ * clear it (APRV-287), in a resident's words (P-9).
1117
1163
  *
1118
- * Everything above the buttons is computed by the runtime from the verified log
1119
- * — the count, the ages, the classes — and the per-member lines carry the
1120
- * agent's own summaries under a heading that says so, exactly as an ordinary
1121
- * digest does. What it does NOT carry is any payload, and the message says so
1122
- * in the same breath as it explains why the only button rejects: a decision is
1123
- * bound to the bytes the approver was shown, and nothing here shows them.
1164
+ * Everything above the claimed label is computed by the runtime from the
1165
+ * verified log (the reminder's count, why nobody is waiting, each member's
1166
+ * age), and the per-member lines carry the agent's own summaries under a label
1167
+ * that says they are the agent's and were not checked, exactly as an ordinary
1168
+ * digest does (SPEC.md §9: computed and claimed stay visibly separate). What it
1169
+ * does NOT carry is any payload, and the footer says so in the same breath as
1170
+ * it explains why the only button rejects: a decision is bound to the bytes the
1171
+ * approver was shown, and nothing here shows them.
1172
+ *
1173
+ * Before P-9 each member line led with its action key, and the text explained
1174
+ * the hook's wait, log events and `approval grant <action key>`. None of that
1175
+ * helps the person holding the phone, so a member is now its summary and its
1176
+ * age, and the footer says what they can do: use the earlier message if its
1177
+ * buttons still work (`staleCopy`), or let the agent ask again.
1124
1178
  */
1125
1179
  function renderStaleSummary(digest, stale, open, total) {
1180
+ // P-9: the restart's one reminder, while every member is still open. Its
1181
+ // first line is the headline; any further line (what follows this message)
1182
+ // goes after the computed lines, which say what this message lists.
1183
+ const reminder = open === total ? (stale.reminder ?? []) : [];
1184
+ const headline = reminder[0] ??
1185
+ (open === 0
1186
+ ? `All ${String(total)} older requests decided`
1187
+ : open === total
1188
+ ? `${String(open)} older request${open === 1 ? " is" : "s are"} still open`
1189
+ : `${String(open)} of ${String(total)} older requests still open`);
1126
1190
  const lines = [
1127
- `<b>${escapeHtml(open === 0
1128
- ? `ALL ${total} STALE REQUESTS DECIDED`
1129
- : `${open} STALE REQUEST${open === 1 ? "" : "S"} — NOBODY IS WAITING ON ${open === 1 ? "IT" : "THEM"}`)}</b>`,
1130
- "",
1131
- "<b>COMPUTED — derived by the runtime from the log and the policy</b>",
1132
- ...stale.lines.map((line) => `• ${escapeHtml(line)}`),
1133
- ...digest.facts.map((fact) => `• <b>${escapeHtml(fact.label)}:</b> ${escapeHtml(fact.text)} <i>(${escapeHtml(fact.origin)})</i>`),
1191
+ `<b>${escapeHtml(headline)}</b>`,
1192
+ ...stale.lines.map((line) => escapeHtml(line)),
1193
+ ...reminder.slice(1).map((line) => escapeHtml(line)),
1134
1194
  "",
1135
- `<b>CLAIMED — authored by ${escapeHtml(digest.author)}, NOT verified by the runtime</b>`,
1195
+ `<b>${escapeHtml(claimedLabelFor(digest.author))}</b>`,
1136
1196
  ];
1137
1197
  for (const [index, member] of digest.members.entries()) {
1138
- lines.push(`${index + 1}. <code>${escapeHtml(member.actionKey)}</code> — ${escapeHtml(member.summary)}`);
1198
+ const age = stale.ages?.[index];
1199
+ lines.push(`${index + 1}. ${escapeHtml(member.summary)}${age === undefined ? "" : ` · ${escapeHtml(age)}`}`);
1139
1200
  if (member.settled !== null) {
1140
1201
  lines.push(` <b>${escapeHtml(member.settled.headline)}</b>`);
1141
1202
  for (const detail of member.settled.detail)
1142
1203
  lines.push(` ${escapeHtml(detail)}`);
1143
1204
  }
1144
1205
  }
1145
- lines.push("", escapeHtml("These asked while a hook waited, and the wait is long over: no tool call is holding the answer. This message carries NO payload, so it offers no approve button — a decision is bound to the bytes you were shown, and nothing here shows them. Rejecting is one log event per request and authorizes nothing. To approve one instead, decide it on its own card or run `approval grant <action key>`; the requests stay listed by /queue either way."));
1206
+ if (open > 0) {
1207
+ const approve = stale.staleCopy === false
1208
+ ? "To approve one, ask your agent to try again."
1209
+ : "To approve one, use its earlier message if you still have it, or ask your agent to try again.";
1210
+ lines.push("", escapeHtml(`This message does not show what they would send, so it has no approve button. ${approve} Reject all turns down only ${open === 1 ? "this one" : `these ${String(open)}`}.`));
1211
+ }
1146
1212
  const rows = open === 0
1147
1213
  ? []
1148
1214
  : [
@@ -1161,10 +1227,10 @@ function renderStaleSummary(digest, stale, open, total) {
1161
1227
  /** The digest's headline, given how much of it is still open. */
1162
1228
  function digestHeadline(open, total) {
1163
1229
  if (open === 0)
1164
- return `ALL ${total} REQUESTS DECIDED`;
1230
+ return `All ${total} requests decided`;
1165
1231
  if (open === total)
1166
- return `${total} REQUESTS AWAITING APPROVAL`;
1167
- return `${open} OF ${total} REQUESTS STILL AWAITING APPROVAL`;
1232
+ return `${total} requests are waiting for you`;
1233
+ return `${open} of ${total} requests still waiting for you`;
1168
1234
  }
1169
1235
  /**
1170
1236
  * The digest message: text plus the keyboard for whatever is still open.
@@ -1186,23 +1252,28 @@ export function renderDigest(digest) {
1186
1252
  const stale = digest.stale ?? null;
1187
1253
  if (stale !== null)
1188
1254
  return renderStaleSummary(digest, stale, open.length, total);
1255
+ // P-9: a headline, one computed line, then the agent's own words under a
1256
+ // label that says they are the agent's and unchecked. No action key: the
1257
+ // members are told apart by number, and the numbers are on the buttons.
1189
1258
  const lines = [
1190
1259
  `<b>${escapeHtml(digestHeadline(open.length, total))}</b>`,
1260
+ escapeHtml(digest.line),
1191
1261
  "",
1192
- "<b>COMPUTED — derived by the runtime from the log and the policy</b>",
1193
- ...digest.facts.map((fact) => `• <b>${escapeHtml(fact.label)}:</b> ${escapeHtml(fact.text)} <i>(${escapeHtml(fact.origin)})</i>`),
1194
- "",
1195
- `<b>CLAIMED — authored by ${escapeHtml(digest.author)}, NOT verified by the runtime</b>`,
1262
+ `<b>${escapeHtml(claimedLabelFor(digest.author))}</b>`,
1196
1263
  ];
1197
1264
  for (const [index, member] of digest.members.entries()) {
1198
- lines.push(`${index + 1}. <code>${escapeHtml(member.actionKey)}</code> — ${escapeHtml(member.summary)} · ${escapeHtml(member.cost)}`);
1265
+ lines.push(`${index + 1}. ${member.kind === undefined ? "" : `${escapeHtml(member.kind)} — `}${escapeHtml(member.summary)} · agent's estimate ${escapeHtml(member.cost)}`);
1199
1266
  if (member.settled !== null) {
1200
1267
  lines.push(` <b>${escapeHtml(member.settled.headline)}</b>`);
1201
1268
  for (const detail of member.settled.detail)
1202
1269
  lines.push(` ${escapeHtml(detail)}`);
1203
1270
  }
1204
1271
  }
1205
- lines.push("", escapeHtml(`Each button decides ONE request, numbered as above. "all" is ${open.length} separate decisions, one log event each; the full payload of every request is in the messages above this one.`));
1272
+ if (open.length > 0) {
1273
+ lines.push("", escapeHtml(open.length > 1
1274
+ ? `Each button answers one request, by number. "All" answers every request still open.`
1275
+ : "Each button answers one request, by number."));
1276
+ }
1206
1277
  const rows = [];
1207
1278
  for (const [index, member] of digest.members.entries()) {
1208
1279
  if (member.settled !== null)
@@ -2498,7 +2569,10 @@ export class TelegramChannel {
2498
2569
  async notifyStale(members, stale) {
2499
2570
  if (members.length === 0)
2500
2571
  return null;
2501
- return this.deliverDigest(members, `tg-batch-${this.makeNonce()}`, stale);
2572
+ return this.deliverDigest(members, `tg-batch-${this.makeNonce()}`, {
2573
+ ...stale,
2574
+ staleCopy: stale.staleCopy ?? this.staleCopy,
2575
+ });
2502
2576
  }
2503
2577
  /**
2504
2578
  * The digest itself: every member's prompt and payload, then the one message
@@ -2520,6 +2594,7 @@ export class TelegramChannel {
2520
2594
  const windowMs = windows.includes(null)
2521
2595
  ? null
2522
2596
  : Math.max(...windows.filter((window) => window !== null));
2597
+ const mixedClasses = new Set(members.map((member) => member.class.value)).size > 1;
2523
2598
  const state = {
2524
2599
  deliveredAtMs,
2525
2600
  windowMs,
@@ -2528,13 +2603,14 @@ export class TelegramChannel {
2528
2603
  batchDeliveryId,
2529
2604
  allNonce,
2530
2605
  stale,
2531
- facts: stale === null ? digestFacts(members) : [],
2606
+ line: stale === null ? digestLine(members) : "",
2532
2607
  author: originOf(members[0].summary),
2533
2608
  members: members.map((member) => ({
2534
2609
  actionKey: member.action_key.value,
2535
2610
  nonce: this.makeNonce(),
2536
2611
  summary: member.summary.value ?? "(none given)",
2537
2612
  cost: `$${member.est_cost_usd.value.toFixed(2)}`,
2613
+ ...(mixedClasses ? { kind: member.class.value } : {}),
2538
2614
  settled: null,
2539
2615
  })),
2540
2616
  };
@@ -2557,12 +2633,12 @@ export class TelegramChannel {
2557
2633
  }
2558
2634
  else if (shared === null) {
2559
2635
  for (const [index, member] of members.entries()) {
2560
- const one = await this.sendPrompt(member, `REQUEST ${index + 1} OF ${members.length} — decide it on the digest below`, null);
2636
+ const one = await this.sendPrompt(member, `Request ${index + 1} of ${members.length} — answer it on the summary below`, null);
2561
2637
  rendered.push({ ...one.rendered, batchDeliveryId });
2562
2638
  }
2563
2639
  }
2564
2640
  else {
2565
- const one = await this.sendPrompt(shared, `THE COMMAND ALL ${members.length} REQUESTS ARE ABOUT — decide it on the digest below`, null);
2641
+ const one = await this.sendPrompt(shared, `What all ${members.length} requests are about — answer them on the summary below`, null);
2566
2642
  for (const member of members) {
2567
2643
  rendered.push({
2568
2644
  ...one.rendered,
@@ -2631,7 +2707,7 @@ export class TelegramChannel {
2631
2707
  for (const [index, chunk] of chunks.entries()) {
2632
2708
  const label = chunks.length === 1
2633
2709
  ? `<b>${PAYLOAD_CHUNK_LABEL}</b>`
2634
- : `<b>PAYLOAD ${index + 1}/${chunks.length} — ${PAYLOAD_CHUNK_LABEL_TAIL}</b>`;
2710
+ : `<b>${PAYLOAD_CHUNK_LABEL} (part ${index + 1} of ${chunks.length})</b>`;
2635
2711
  segments.push(`${label}\n<pre>${escapeHtml(chunk)}</pre>`);
2636
2712
  }
2637
2713
  }