toolroll 0.9.16 → 0.9.17

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 (57) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/assignment-brief.js +11 -4
  3. package/dist/chat-actions.d.ts +7 -0
  4. package/dist/chat-actions.js +40 -4
  5. package/dist/chat-ask.js +29 -15
  6. package/dist/chat-channel.js +3 -1
  7. package/dist/chat-delivery-state.d.ts +2 -0
  8. package/dist/chat-delivery.d.ts +3 -0
  9. package/dist/chat-delivery.js +31 -16
  10. package/dist/chat-warmth.d.ts +21 -0
  11. package/dist/chat-warmth.js +36 -0
  12. package/dist/discord-api.d.ts +1 -1
  13. package/dist/discord-api.js +1 -1
  14. package/dist/discord-chat.d.ts +3 -2
  15. package/dist/discord-chat.js +18 -3
  16. package/dist/lead-about.d.ts +35 -0
  17. package/dist/lead-about.js +62 -0
  18. package/dist/lead-capabilities.d.ts +64 -0
  19. package/dist/lead-capabilities.js +199 -0
  20. package/dist/lead-commitments.js +13 -7
  21. package/dist/lead-context.d.ts +5 -3
  22. package/dist/lead-context.js +29 -7
  23. package/dist/lead-follow.d.ts +1 -1
  24. package/dist/lead-follow.js +3 -2
  25. package/dist/lead-identity.js +1 -1
  26. package/dist/lead-people.d.ts +55 -0
  27. package/dist/lead-people.js +128 -0
  28. package/dist/lead-voice.d.ts +3 -2
  29. package/dist/lead-voice.js +4 -3
  30. package/dist/mate-cli.d.ts +3 -0
  31. package/dist/mate-cli.js +15 -14
  32. package/dist/mate-contract.d.ts +1 -1
  33. package/dist/mate-contract.js +6 -4
  34. package/dist/mate-progress.js +2 -0
  35. package/dist/mate-tools.d.ts +8 -0
  36. package/dist/mate-tools.js +112 -6
  37. package/dist/mate.d.ts +26 -1
  38. package/dist/mate.js +89 -25
  39. package/dist/operate.js +2 -0
  40. package/dist/phase-routing.d.ts +1 -1
  41. package/dist/provider-auth.d.ts +4 -0
  42. package/dist/provider-auth.js +1 -1
  43. package/dist/reply-shape.d.ts +96 -0
  44. package/dist/reply-shape.js +452 -0
  45. package/dist/serve.js +53 -28
  46. package/dist/slack-chat.d.ts +2 -0
  47. package/dist/slack-chat.js +20 -3
  48. package/dist/store.d.ts +12 -3
  49. package/dist/store.js +44 -9
  50. package/dist/team-runtime.js +1 -1
  51. package/dist/teams-chat.js +12 -1
  52. package/dist/telegram-mate.d.ts +2 -0
  53. package/dist/telegram-mate.js +27 -11
  54. package/dist/telegram-team.js +6 -3
  55. package/dist/telegram.js +1 -1
  56. package/dist/test-temp.js +1 -1
  57. package/package.json +1 -1
package/dist/mate.js CHANGED
@@ -11,6 +11,7 @@ import { MATE_MAX_PROPOSALS_PER_TURN, MATE_TOOL_SCHEMAS, executeMateTool, isMate
11
11
  import { composeSubscriptionMatePrompt, performSubscriptionMateRequest } from "./subscription-chat.js";
12
12
  import { leadContext } from './lead-context.js';
13
13
  import { envValue } from "./names.js";
14
+ import { deliverableClaim, deliverableRepair, dropDeliverableClaims, replyCarriesDeliverable } from "./reply-shape.js";
14
15
  export const MATE_MESSAGE_MAX_CHARS = 2_000;
15
16
  /** Where a promise made this turn is reported: the chat app it arrived on, or this conversation for the console and terminal. */
16
17
  function promiseChannelOfTurn(channel) {
@@ -41,6 +42,47 @@ export const MATE_REFUSAL_COPY = {
41
42
  "request-changed": "That send was already received with different text or task context. Reload the conversation before sending a new message.",
42
43
  channel: "This conversation's connection changed, so the message wasn't sent. Reconnect it, then send again.",
43
44
  };
45
+ /** A turn stopped because its chat's standing changed: what happened, what it means, one next step. */
46
+ export const MATE_CHANNEL_COPY = {
47
+ unpaired: "This chat is no longer connected to your account, so the reply was stopped. Nothing it proposed was kept. Connect the chat again in Settings, then send your message again.",
48
+ "not-approver": "Your account can no longer approve work here, so the reply was stopped. Nothing it proposed was kept. Ask an owner to restore your access, then send your message again.",
49
+ "projects-changed": "The projects this chat can see changed while the lead was answering, so the reply was stopped. Nothing it proposed was kept. Send your message again.",
50
+ "projects-unreadable": "Your project list couldn't be read just now, so the reply was stopped. Nothing it proposed was kept. Send your message again in a minute.",
51
+ "member-changed": "Your account in this chat app no longer has access, so the reply was stopped. Nothing it proposed was kept. Ask the app's admin to add you back, then send your message again.",
52
+ "access-changed": "This chat's access changed while the lead was answering, so the reply was stopped. Nothing it proposed was kept. Reconnect the chat, then send your message again.",
53
+ };
54
+ /** Why a reply did not finish, in the same three parts: what happened, what it means, one next step. */
55
+ export const MATE_FAILURE_COPY = {
56
+ stopped: "This reply was stopped before it finished, from another window or by a newer message. Nothing it proposed was kept. Send your message again if you still need an answer.",
57
+ ended: "Your sign-in or this conversation ended while the lead was answering. Nothing it proposed was kept. Sign in again or start a new chat, then send your message again.",
58
+ tooLong: "The reply took too long and was stopped. Nothing it proposed was kept. Send your message again, or ask for less at once.",
59
+ secretRead: "Something the lead read looked like a password or key, so it stopped before sending it anywhere. Nothing was kept. Remove the password or key from that task or note, then ask again.",
60
+ secretReply: "The lead's reply contained something that looked like a password or key, so it was thrown away. Nothing was kept. Ask again, without asking for a password or key.",
61
+ paused: "An earlier reply stopped before its cost was known, so chat is paused. This message wasn't answered. Confirm that earlier reply on the Chat page, then send again.",
62
+ notStarted: "The reply couldn't be started. Nothing was kept or charged. Send your message again.",
63
+ notInstalled: "The chat app the lead uses isn't installed on this computer, so it couldn't answer. Nothing was kept. Install it, or choose another provider in Settings → Lead.",
64
+ unreadable: "The chat provider sent back an answer that couldn't be read, so it was thrown away. Nothing was kept. Send your message again.",
65
+ signIn: "The chat provider couldn't answer; its sign-in may have expired. Nothing was kept. Sign in to it again on this computer, then send your message again.",
66
+ refused: "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute; if it keeps happening, check the provider in Settings → Lead.",
67
+ tooLongUnknownCost: "The reply took too long and was stopped, and its cost isn't known yet, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
68
+ lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
69
+ unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
70
+ empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
71
+ unusable: "The lead's answer couldn't be used, so it was thrown away. Nothing was kept. Send your message again.",
72
+ };
73
+ /** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
74
+ export function mateFailureText(reason) {
75
+ switch (reason) {
76
+ case "timeout": return MATE_FAILURE_COPY.tooLong;
77
+ case "malformed-reply": return MATE_FAILURE_COPY.unreadable;
78
+ case "secret-refused": return MATE_FAILURE_COPY.secretReply;
79
+ case "latched": return MATE_FAILURE_COPY.paused;
80
+ case "revoked": return MATE_FAILURE_COPY.ended;
81
+ case "superseded": return MATE_FAILURE_COPY.stopped;
82
+ case "provider-error": return MATE_FAILURE_COPY.refused;
83
+ default: return "The lead's reply didn't finish. Nothing it proposed was kept. Send your message again.";
84
+ }
85
+ }
44
86
  const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
45
87
  /** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
46
88
  export function historyFor(store, thread, queuedMessageId) {
@@ -222,6 +264,7 @@ export async function runMateTurn(input) {
222
264
  const readDecisions = new Map();
223
265
  const readResults = new Map();
224
266
  const searchedMemory = new Map();
267
+ const checkedCapabilities = new Map();
225
268
  /** The turn's one question to its owner: shown with the reply only once the turn answers; a failed turn drops it. */
226
269
  const ask = (question, options) => store.recordMateAsk({ turn: turnId, thread: thread.id, question, options }, clock());
227
270
  let tokensIn = 0;
@@ -256,20 +299,30 @@ export async function runMateTurn(input) {
256
299
  // AFTER it resolves, immediately before sending context or using a tool.
257
300
  const guard = (channel) => {
258
301
  if (!stillOurs())
259
- return { ok: false, turn: turnId, failed: "superseded", message: "this turn was ended before its next action", unknownSpend: false };
302
+ return { ok: false, turn: turnId, failed: "superseded", message: MATE_FAILURE_COPY.stopped, unknownSpend: false };
260
303
  if (!channel.ok)
261
- return fail("revoked", `this conversation's connection changed (${channel.reason}) — nothing it proposed was kept`, false);
304
+ return fail("revoked", MATE_CHANNEL_COPY[channel.reason] ?? MATE_CHANNEL_COPY["access-changed"], false);
262
305
  const standing = reproveApprover(store, who);
263
306
  const liveSession = store.getMateSession(session.id);
264
307
  const liveThread = store.getMateThread(thread.id);
265
308
  if (!standing.ok || liveSession === null || liveSession.endedAt !== null || liveThread === null || liveThread.closedAt !== null) {
266
- return fail("revoked", "your standing or this conversation ended — nothing it proposed was kept", false);
309
+ return fail("revoked", MATE_FAILURE_COPY.ended, false);
267
310
  }
268
311
  return null;
269
312
  };
270
313
  let reply = null;
271
314
  let stoppedAtCap = false;
272
315
  let lastText = "";
316
+ /** A page shown this turn (show_control): a reply may say it links one. */
317
+ let shownControl = false;
318
+ /** The turn's one repair step for a reply that claims an attachment it does not carry. */
319
+ let repaired = false;
320
+ const unbacked = (text) => {
321
+ const claim = deliverableClaim(text);
322
+ if (claim === null || shownControl || replyCarriesDeliverable(text) || store.listMateTurnEvidence(turnId).length > 0)
323
+ return null;
324
+ return claim;
325
+ };
273
326
  while (steps < maxSteps) {
274
327
  const blocked = guard(input.revalidate === undefined ? { ok: true } : await input.revalidate());
275
328
  if (blocked !== null)
@@ -277,22 +330,22 @@ export async function runMateTurn(input) {
277
330
  now = clock();
278
331
  const remainingMs = TURN_WALL_CLOCK_MS - (now.getTime() - turnStartedAt);
279
332
  if (remainingMs <= 0)
280
- return fail("timeout", "the turn ran out of time before the model finished", false);
333
+ return fail("timeout", MATE_FAILURE_COPY.tooLong, false);
281
334
  const request = direct ? composeDirect(input.key) : composeSubscription();
282
335
  // Tool results join the outbound body: scanned again before every dispatch.
283
336
  const outbound = typeof request === "string" ? request : request.body;
284
337
  if (scanForSecrets(outbound).length > 0) {
285
- return fail("secret-refused", "a tool result contained something credential-shaped — the turn stopped before sending it", false);
338
+ return fail("secret-refused", MATE_FAILURE_COPY.secretRead, false);
286
339
  }
287
340
  const step = store.openMateStep({ mateTurn: turnId, generation: started.generation, approver: who.name, credentialKey, provider: config.provider, model: config.model, deadlineMs: remainingMs + 10_000 }, now);
288
341
  if (!step.ok) {
289
342
  if (step.reason === "latched")
290
- return fail("latched", "an earlier reply stopped before its cost was known, so chat is paused — acknowledge that reply on the Chat page, then send again", false);
291
- return { ok: false, turn: turnId, failed: "superseded", message: "this turn was ended before its next step", unknownSpend: false };
343
+ return fail("latched", MATE_FAILURE_COPY.paused, false);
344
+ return { ok: false, turn: turnId, failed: "superseded", message: MATE_FAILURE_COPY.stopped, unknownSpend: false };
292
345
  }
293
346
  const stepStarted = store.startChatTurn(step.id, now);
294
347
  if (!stepStarted.ok)
295
- return fail("provider-error", "the step could not be dispatched", false);
348
+ return fail("provider-error", MATE_FAILURE_COPY.notStarted, false);
296
349
  steps++;
297
350
  progress({ kind: "step", turn: turnId, step: steps });
298
351
  const requestBytes = Buffer.byteLength(outbound, "utf8");
@@ -337,36 +390,36 @@ export async function runMateTurn(input) {
337
390
  if (!direct) {
338
391
  finishStep({ state: "failed", failureReason: result.problem === "timeout" ? "timeout" : result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", settledMicrousd: 0 });
339
392
  const message = result.problem === "not-found"
340
- ? "the selected subscription CLI is not installed on this machine"
393
+ ? MATE_FAILURE_COPY.notInstalled
341
394
  : result.problem === "timeout"
342
- ? "the subscription turn ran out of time"
395
+ ? MATE_FAILURE_COPY.tooLong
343
396
  : result.problem === "malformed-reply"
344
- ? "the subscription provider returned a malformed answer and it was discarded"
345
- : "the subscription provider refused or could not complete the turn — check its login in the terminal";
397
+ ? MATE_FAILURE_COPY.unreadable
398
+ : MATE_FAILURE_COPY.signIn;
346
399
  return fail(result.problem === "timeout" ? "timeout" : result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false);
347
400
  }
348
401
  if (result.problem.startsWith("status-")) {
349
402
  // The provider ANSWERED with an error: nothing billed for this step.
350
403
  finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: 0 });
351
- return fail("provider-error", "the provider refused the request — nothing was billed for that step", false);
404
+ return fail("provider-error", MATE_FAILURE_COPY.refused, false);
352
405
  }
353
406
  if (result.problem === "timeout") {
354
407
  finishStep({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
355
- return fail("timeout", "the turn timed out; that step's cost is unknown — the whole reservation is charged and the credential is blocked until you acknowledge it", true);
408
+ return fail("timeout", MATE_FAILURE_COPY.tooLongUnknownCost, true);
356
409
  }
357
410
  if (result.problem === "network") {
358
411
  finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
359
- return fail("provider-error", "the provider could not be reached after dispatch; cost unknown — the whole reservation is charged; acknowledge to re-enable chat", true);
412
+ return fail("provider-error", MATE_FAILURE_COPY.lostUnknownCost, true);
360
413
  }
361
414
  finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
362
- return fail("malformed-reply", "the provider's response was malformed and was discarded; cost unknown — the whole reservation is charged; acknowledge to re-enable chat", true);
415
+ return fail("malformed-reply", MATE_FAILURE_COPY.unreadableUnknownCost, true);
363
416
  }
364
417
  const answer = result.answer;
365
418
  // Usage that cannot be true — more input tokens than bytes sent — is a
366
419
  // malformed reply with unknown cost, never a number to settle by.
367
420
  if (direct && answer.tokensIn > requestBytes) {
368
421
  finishStep({ state: "failed", failureReason: "malformed-reply", settledMicrousd: null, unknownSpend: true });
369
- return fail("malformed-reply", "the provider reported usage that cannot be true; cost unknown — the whole reservation is charged; acknowledge to re-enable chat", true);
422
+ return fail("malformed-reply", MATE_FAILURE_COPY.unreadableUnknownCost, true);
370
423
  }
371
424
  // The pinned math, or the provider's own reported charge when HIGHER.
372
425
  const stepSettled = direct
@@ -385,15 +438,23 @@ export async function runMateTurn(input) {
385
438
  return changed;
386
439
  if (answer.calls.length === 0) {
387
440
  if (answer.text.trim() === "")
388
- return fail("malformed-reply", "the model answered with nothing", false);
389
- reply = answer.text;
441
+ return fail("malformed-reply", MATE_FAILURE_COPY.empty, false);
442
+ // "Here's the screenshot" with nothing attached or linked: one repair step to attach it or drop the claim; never the claim alone.
443
+ const claim = unbacked(answer.text);
444
+ if (claim !== null && !repaired && steps < maxSteps) {
445
+ repaired = true;
446
+ history.push({ role: "assistant", text: answer.text, calls: [] });
447
+ history.push({ role: "operator", text: deliverableRepair(claim) });
448
+ continue;
449
+ }
450
+ reply = claim === null ? answer.text : dropDeliverableClaims(answer.text);
390
451
  break;
391
452
  }
392
453
  if (answer.calls.length > MATE_MAX_CALLS_PER_STEP)
393
- return fail("malformed-reply", "the model asked for more tool calls than one step allows", false);
454
+ return fail("malformed-reply", MATE_FAILURE_COPY.unusable, false);
394
455
  for (const call of answer.calls) {
395
456
  if (!isMateTool(call.name))
396
- return fail("malformed-reply", "the model called a tool that does not exist", false);
457
+ return fail("malformed-reply", MATE_FAILURE_COPY.unusable, false);
397
458
  }
398
459
  history.push({ role: "assistant", text: answer.text, calls: answer.calls });
399
460
  for (const [index, call] of answer.calls.entries()) {
@@ -403,9 +464,11 @@ export async function runMateTurn(input) {
403
464
  return changed;
404
465
  }
405
466
  progress({ kind: "tool", turn: turnId, step: steps, label: mateToolLabel(call.name) });
406
- const outcome = executeMateTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, ask, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
467
+ const outcome = executeMateTool({ store, who, now: clock(), draft, selectEvidence, step: steps, readDecisions, readResults, searchedMemory, checkedCapabilities, ask, ...(input.integrations === undefined ? {} : { integrations: input.integrations }), thread: thread.id, turn: turnId, ...(promiseChannelOfTurn(input.channel) === undefined ? {} : { channel: promiseChannelOfTurn(input.channel) }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), ...(input.mediaDelivery === undefined ? {} : { mediaDelivery: input.mediaDelivery }) }, call.name, call.args, view);
407
468
  if (READ_TOOLS.has(call.name))
408
469
  reads++;
470
+ if (call.name === "show_control" && outcome.ok)
471
+ shownControl = true;
409
472
  // Opt-in, local diagnostics for end-to-end runs: what the lead asked of each tool and what came back (its start), keys
410
473
  // blanked. Each line stays whole JSON so a run can assert on what a tool returned rather than on the model's words.
411
474
  if (envValue(process.env, "MATE_TRACE") === "1") {
@@ -419,16 +482,17 @@ export async function runMateTurn(input) {
419
482
  }
420
483
  if (reply === null) {
421
484
  stoppedAtCap = true;
422
- reply = `${lastText.trim() === "" ? "" : `${lastText.trim()}\n\n`}(stopped after ${maxSteps} steps${maxSteps < MATE_MAX_STEPS ? " within this conversation’s remaining allowance" : ""})`;
485
+ const last = lastText.trim() === "" || unbacked(lastText) === null ? lastText.trim() : dropDeliverableClaims(lastText);
486
+ reply = `${last === "" ? "" : `${last}\n\n`}(I stopped here: this answer needed more steps than ${maxSteps < MATE_MAX_STEPS ? "this conversation's remaining spending allows" : "one reply allows"}. Ask me to carry on.)`;
423
487
  }
424
488
  // Ruling 11: model text is scanned before it becomes durable.
425
489
  if (scanForSecrets(reply).length > 0)
426
- return fail("secret-refused", "the model's reply contained something credential-shaped and was discarded", false);
490
+ return fail("secret-refused", MATE_FAILURE_COPY.secretReply, false);
427
491
  now = clock();
428
492
  const activity = activitySummary(reads, proposals, steps);
429
493
  // One write (finding 12): settle, debit, promote the drafts, append the assistant text.
430
494
  const finalized = store.finalizeMateTurn(turnId, started.generation, { state: "answered", settledMicrousd: settled, tokensIn, tokensOut, message: { text: reply, activity } }, now);
431
495
  if (!finalized)
432
- return { ok: false, turn: turnId, failed: "superseded", message: "this turn was ended before it could be kept", unknownSpend: false };
496
+ return { ok: false, turn: turnId, failed: "superseded", message: MATE_FAILURE_COPY.stopped, unknownSpend: false };
433
497
  return { ok: true, turn: turnId, reply, activity, proposals, steps, stoppedAtCap, settledMicrousd: settled };
434
498
  }
package/dist/operate.js CHANGED
@@ -11643,6 +11643,8 @@ async function chatCommand(flags, context) {
11643
11643
  ceilingUsd: ceilingGiven === undefined ? undefined : Number(ceilingGiven),
11644
11644
  ...(context.mateSeams === undefined ? {} : { seams: { ...context.mateSeams, clock: context.mateSeams.clock ?? context.clock } }),
11645
11645
  ...(context.evidenceRoot === undefined ? {} : { evidenceRoot: context.evidenceRoot }),
11646
+ // The console's own address (and its public one): links there read as "the task", "the result", "Settings → Lead".
11647
+ appOrigin: [loadConsoleUrl(process.env, dirname(context.databaseFile)), phoneOrigin(process.env, dirname(context.databaseFile))],
11646
11648
  });
11647
11649
  return result.code;
11648
11650
  }
@@ -234,7 +234,7 @@ export declare function parseOverrides(raw: unknown): RouteOverride[] | null;
234
234
  * nobody had overridden anything. */
235
235
  export declare function overridesFromJson(json: string | null): RouteOverride[] | null;
236
236
  export declare function canonicalOverridesJson(overrides: readonly RouteOverride[]): string | null;
237
- export type ReadinessLookup = (provider: ProviderId) => Pick<ReadinessObservation, "state" | "reason" | "runner" | "observedAt"> | null;
237
+ export type ReadinessLookup = (provider: ProviderId) => (Pick<ReadinessObservation, "state" | "reason" | "runner" | "observedAt"> & Partial<Pick<ReadinessObservation, "probe">>) | null;
238
238
  export type RouteLegProjection = RouteLeg & {
239
239
  readiness: ReadinessState;
240
240
  readinessReason: string | null;
@@ -34,6 +34,10 @@ export declare function providerName(provider: string): string;
34
34
  export declare function signInReason(pause: Pick<AuthPause, "provider" | "authMode">): string;
35
35
  /** What to run, as a command. */
36
36
  export declare function signInCommand(pause: Pick<AuthPause, "provider" | "authMode">): string;
37
+ /** The same command for a chat message: the phone scrub hides anything shaped
38
+ * like a path (`/login` included), so a message names Claude's equivalent
39
+ * `claude auth login`. */
40
+ export declare function messageCommand(pause: Pick<AuthPause, "provider" | "authMode">): string;
37
41
  /** One line for status, ready and the console. */
38
42
  export declare function signInWords(pause: AuthPause): string;
39
43
  /** The open pause for a provider, or null. */
@@ -44,7 +44,7 @@ export function signInCommand(pause) {
44
44
  /** The same command for a chat message: the phone scrub hides anything shaped
45
45
  * like a path (`/login` included), so a message names Claude's equivalent
46
46
  * `claude auth login`. */
47
- function messageCommand(pause) {
47
+ export function messageCommand(pause) {
48
48
  const command = signInCommand(pause);
49
49
  return command === "claude /login" ? "claude auth login" : command;
50
50
  }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * The lead's voice, enforced: every chat surface (console, terminal, Telegram, Slack, Discord, Teams) runs a reply
3
+ * through here before it is sent. The contract asks for plain text; this makes it so, without changing what the reply
4
+ * says. Shaping is channel-neutral and idempotent — it yields a small canonical form (`**anchor**`, `[label](url)`,
5
+ * `code`) — and each channel renders that form in its own format.
6
+ *
7
+ * - Markdown headers become plain lines.
8
+ * - Bold stays on at most three short anchors; the rest is plain.
9
+ * - Bare URLs become labelled links, named from their target: "the task", "the result", "Settings → Lead", else the host.
10
+ * - Internal ids (run #12, digests, r1-style project ids) are removed unless the owner asked for them. When a reply names
11
+ * several of one kind (two runs, two projects), they stay: two items must never read the same.
12
+ * - Runs of blank lines collapse to one.
13
+ * Code (inline or fenced) is left exactly as written.
14
+ */
15
+ export type ReplyChannel = "console" | "terminal" | "telegram" | "slack" | "discord" | "teams";
16
+ export type ShapeOptions = {
17
+ /** The owner's message this reply answers: when it asks for ids, the ids stay. */
18
+ asked?: string;
19
+ /** This app's own origin (or origins: the console's address and its public one). Only a link there is named as the
20
+ * app's page ("the task"); every other link is named by its real host, whatever label the model wrote, so a reply can
21
+ * never dress a foreign link up as ours. */
22
+ appOrigin?: string | readonly (string | null)[] | null;
23
+ };
24
+ export declare const BOLD_ANCHORS = 3;
25
+ /** A bold anchor is a few words, not a sentence. */
26
+ export declare const BOLD_ANCHOR_CHARS = 40;
27
+ /** Did the owner ask for ids (an id, a digest, a hash, a run number or a project's r-number)? */
28
+ export declare function askedForIds(asked: string | undefined): boolean;
29
+ /** What a link is called, from where it goes: the app's own task, result and settings pages by name (only on `appOrigin`); anything else by host. */
30
+ export declare function linkLabel(url: string, appOrigin?: ShapeOptions["appOrigin"]): string;
31
+ /**
32
+ * The canonical shaped reply: plain lines, at most three bold anchors, labelled links, no internal ids unless asked
33
+ * (or unless several of a kind would otherwise read the same), single blank lines. Idempotent: shaping a shaped reply
34
+ * changes nothing.
35
+ */
36
+ export declare function shapeReply(text: string, options?: ShapeOptions): string;
37
+ /**
38
+ * A long reply split into parts of at most `size` characters, then each part shaped: the cut is made on the reply as
39
+ * written, at a paragraph or line where possible and never inside code, a link or a bold anchor, so no part carries
40
+ * half of one. The parts share one bold budget and one rule for ids, as if the reply were shaped whole. `measure` is
41
+ * how long a shaped part is once the channel renders it (its own link and escape format): a part that renders past
42
+ * `size` is cut again, smaller. Only a single link longer than a part can exceed it, and it is never cut.
43
+ */
44
+ export declare function shapeReplyParts(text: string, size: number, options?: ShapeOptions, measure?: (shaped: string) => number): string[];
45
+ export type ReplyPiece = {
46
+ kind: "text";
47
+ text: string;
48
+ } | {
49
+ kind: "bold";
50
+ text: string;
51
+ } | {
52
+ kind: "code";
53
+ text: string;
54
+ } | {
55
+ kind: "link";
56
+ label: string;
57
+ url: string;
58
+ };
59
+ /** A shaped reply as pieces: plain text, bold anchors, inline code and labelled links. Fenced code stays text. */
60
+ export declare function replyPieces(shaped: string): ReplyPiece[];
61
+ /** Discord's presentation syntax, escaped; mentions are broken so nothing pings. */
62
+ export declare const discordPlain: (text: string) => string;
63
+ /** One line of a shaped reply as console HTML (the console lays out paragraphs and lists itself). */
64
+ export declare function replyHtmlInline(shapedLine: string): string;
65
+ /** A shaped reply in a channel's own text format (Telegram's formatting rides entities: see `telegramReply`). */
66
+ export declare function renderReply(shaped: string, channel: ReplyChannel): string;
67
+ export type TelegramEntity = {
68
+ type: "bold" | "code";
69
+ offset: number;
70
+ length: number;
71
+ } | {
72
+ type: "text_link";
73
+ offset: number;
74
+ length: number;
75
+ url: string;
76
+ };
77
+ /** A shaped reply for Telegram: plain text plus entities (bold anchors, code, labelled links). Offsets are UTF-16, as JS strings are. */
78
+ export declare function telegramReply(shaped: string): {
79
+ text: string;
80
+ entities: TelegramEntity[];
81
+ };
82
+ /** Shape and render in one step, for a surface that sends the reply as text. */
83
+ export declare function voiceReply(text: string, channel: ReplyChannel, options?: ShapeOptions): string;
84
+ /**
85
+ * The words in a reply that say something is being sent or attached ("I've attached the log", "here's the
86
+ * screenshot"), or null. A claim needs an attachment noun and a send verb; one whose content the reply lists itself
87
+ * ("I've included the files I changed: a.ts") is not a claim.
88
+ */
89
+ export declare function deliverableClaim(text: string): string | null;
90
+ /** Does the reply carry the thing itself: a link, or content quoted in a code block? */
91
+ export declare function replyCarriesDeliverable(text: string): boolean;
92
+ export declare const NOTHING_ATTACHED = "I couldn't attach that to this reply.";
93
+ /** The reply without its claims of an attachment, said plainly once; whatever a claim listed stays; never the claim alone. */
94
+ export declare function dropDeliverableClaims(text: string): string;
95
+ /** The one repair step's words to the lead: attach it, or drop the claim. */
96
+ export declare function deliverableRepair(claim: string): string;