toolroll 0.9.16 → 0.9.18

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 (70) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/assignment-brief.js +11 -4
  3. package/dist/browser/workspace.js +14 -14
  4. package/dist/browser-workspace.d.ts +2 -0
  5. package/dist/chat-actions.d.ts +7 -0
  6. package/dist/chat-actions.js +40 -4
  7. package/dist/chat-ask.js +29 -15
  8. package/dist/chat-channel.js +3 -1
  9. package/dist/chat-decide.d.ts +203 -0
  10. package/dist/chat-decide.js +554 -0
  11. package/dist/chat-delivery-state.d.ts +7 -0
  12. package/dist/chat-delivery.d.ts +3 -0
  13. package/dist/chat-delivery.js +63 -17
  14. package/dist/chat-quiet.d.ts +4 -1
  15. package/dist/chat-quiet.js +6 -2
  16. package/dist/chat-warmth.d.ts +21 -0
  17. package/dist/chat-warmth.js +36 -0
  18. package/dist/desktop-update.d.ts +1 -1
  19. package/dist/discord-api.d.ts +3 -1
  20. package/dist/discord-api.js +4 -2
  21. package/dist/discord-chat.d.ts +3 -2
  22. package/dist/discord-chat.js +35 -6
  23. package/dist/lead-about.d.ts +35 -0
  24. package/dist/lead-about.js +62 -0
  25. package/dist/lead-capabilities.d.ts +64 -0
  26. package/dist/lead-capabilities.js +199 -0
  27. package/dist/lead-commitments.js +13 -7
  28. package/dist/lead-context.d.ts +5 -3
  29. package/dist/lead-context.js +29 -7
  30. package/dist/lead-follow.d.ts +1 -1
  31. package/dist/lead-follow.js +3 -2
  32. package/dist/lead-identity.js +1 -1
  33. package/dist/lead-people.d.ts +55 -0
  34. package/dist/lead-people.js +128 -0
  35. package/dist/lead-voice.d.ts +3 -2
  36. package/dist/lead-voice.js +4 -3
  37. package/dist/mate-cli.d.ts +3 -0
  38. package/dist/mate-cli.js +15 -14
  39. package/dist/mate-contract.d.ts +1 -1
  40. package/dist/mate-contract.js +6 -4
  41. package/dist/mate-progress.js +2 -0
  42. package/dist/mate-tools.d.ts +8 -0
  43. package/dist/mate-tools.js +112 -6
  44. package/dist/mate.d.ts +26 -1
  45. package/dist/mate.js +89 -25
  46. package/dist/modes.d.ts +6 -0
  47. package/dist/modes.js +6 -0
  48. package/dist/operate.js +20 -8
  49. package/dist/phase-routing.d.ts +1 -1
  50. package/dist/project-delete.js +3 -0
  51. package/dist/provider-auth.d.ts +4 -0
  52. package/dist/provider-auth.js +1 -1
  53. package/dist/reply-shape.d.ts +96 -0
  54. package/dist/reply-shape.js +452 -0
  55. package/dist/result-shots.d.ts +51 -0
  56. package/dist/result-shots.js +119 -0
  57. package/dist/serve.js +76 -49
  58. package/dist/slack-chat.d.ts +2 -0
  59. package/dist/slack-chat.js +38 -7
  60. package/dist/store.d.ts +36 -3
  61. package/dist/store.js +163 -16
  62. package/dist/team-runtime.js +1 -1
  63. package/dist/teams-chat.js +17 -1
  64. package/dist/telegram-mate.d.ts +12 -0
  65. package/dist/telegram-mate.js +27 -11
  66. package/dist/telegram-team.js +6 -3
  67. package/dist/telegram.d.ts +5 -3
  68. package/dist/telegram.js +227 -21
  69. package/dist/test-temp.js +1 -1
  70. package/package.json +1 -1
@@ -225,6 +225,8 @@ export type BrowserSettingsView = {
225
225
  chat?: {
226
226
  mode: 'quiet' | 'all';
227
227
  digestAt: string | null;
228
+ /** Whether a result's saved screenshots follow its chat message: off, the first one, or up to 4. */
229
+ screenshots?: 'off' | 'first' | 'all';
228
230
  /** Each project this person can see; a muted one sends no pings but stays in Tasks and the digest. */
229
231
  projects?: {
230
232
  repo: string;
@@ -196,6 +196,11 @@ export declare const CHAT_ACTIONS: {
196
196
  readonly protected: false;
197
197
  readonly password: false;
198
198
  };
199
+ readonly lead_about_you: {
200
+ readonly label: "Remember";
201
+ readonly protected: false;
202
+ readonly password: false;
203
+ };
199
204
  readonly scope_approve: {
200
205
  readonly label: "Approve work";
201
206
  readonly protected: true;
@@ -222,6 +227,8 @@ export type ChatAction = keyof typeof CHAT_ACTIONS;
222
227
  * yes/cancel challenge instead of the console's secure screen. Password
223
228
  * actions and long or redacted terms never qualify. */
224
229
  export declare const CHALLENGE_ACTIONS: ReadonlySet<ChatAction>;
230
+ /** Actions about the person themselves, in no project: their card carries no project (repo ""). */
231
+ export declare const OWNER_ACTIONS: ReadonlySet<ChatAction>;
225
232
  export declare function isChatAction(value: unknown): value is ChatAction;
226
233
  export type SharedAction = {
227
234
  operation: ChatAction;
@@ -32,6 +32,7 @@ import { kitInstalled, kitOf, KITS, setUpKitNow } from "./kits.js";
32
32
  import { starterFlowOf, starterOf, startersFor, starterTerms, STARTER_FLOWS, switchOnStarter } from "./flow-starters.js";
33
33
  import { answerTeammateQuestion } from "./teammate-work.js";
34
34
  import { callWords, checkRule, defaultRule, grantListed, revokeTool, ruleWords, setToolRules } from "./teammate-tools.js";
35
+ import { aboutYouOf, checkAboutYouLine, saveAboutYou, withAboutYouLine } from "./lead-about.js";
35
36
  /** A tool from the lead's card: a common tool by its id, or the operator's own program or address. */
36
37
  function toolSpecFromRequest(input) {
37
38
  if (input["catalog"] !== undefined) {
@@ -107,6 +108,8 @@ export const CHAT_ACTIONS = {
107
108
  kit_setup: { label: "Set up kit", protected: false, password: false },
108
109
  decision_record: { label: "Record decision", protected: false, password: false },
109
110
  decision_retire: { label: "Retire decision", protected: false, password: false },
111
+ // What the owner's own lead knows about them: theirs alone, in no project (lead-about.ts).
112
+ lead_about_you: { label: "Remember", protected: false, password: false },
110
113
  scope_approve: { label: "Approve work", protected: true, password: true },
111
114
  result_accept: { label: "Accept and finish", protected: true, password: false },
112
115
  task_cancel: { label: "Cancel task", protected: true, password: false },
@@ -116,6 +119,8 @@ export const CHAT_ACTIONS = {
116
119
  * yes/cancel challenge instead of the console's secure screen. Password
117
120
  * actions and long or redacted terms never qualify. */
118
121
  export const CHALLENGE_ACTIONS = new Set(["result_accept"]);
122
+ /** Actions about the person themselves, in no project: their card carries no project (repo ""). */
123
+ export const OWNER_ACTIONS = new Set(["lead_about_you"]);
119
124
  export function isChatAction(value) {
120
125
  return typeof value === "string" && Object.hasOwn(CHAT_ACTIONS, value);
121
126
  }
@@ -160,19 +165,21 @@ export const CHAT_ACTION_FIELDS = {
160
165
  kit_setup: ["repo", "kit"],
161
166
  decision_record: ["repo", "claim", "why", "supersedes", "source"],
162
167
  decision_retire: ["repo", "decision", "reason"],
168
+ lead_about_you: ["line", "replaces"],
163
169
  scope_approve: ["task"],
164
170
  result_accept: ["task", "run"],
165
171
  task_cancel: ["task"],
166
172
  task_resume: ["task", "run"],
167
173
  };
168
174
  const nonceHash = (value) => createHash("sha256").update(value).digest("hex");
169
- function requireActor(store, who, repo) {
175
+ function requireActor(store, who, repo, operation) {
170
176
  if (!store.schemaCurrent())
171
177
  throw Error("The running build changed. Reload before proposing an action.");
178
+ // An action about the person themselves names no project; every other one names one they can reach.
179
+ const owned = repo === "" && operation !== undefined && OWNER_ACTIONS.has(operation);
172
180
  if (!isVerifiedApprover(who) ||
173
181
  !reproveApprover(store, who).ok ||
174
- !who.repos.includes(repo) ||
175
- !store.accountCanAccess(who.name, repo))
182
+ (!owned && (!who.repos.includes(repo) || !store.accountCanAccess(who.name, repo))))
176
183
  throw Error("This project is outside your current access.");
177
184
  }
178
185
  function text(input, key, cap, optional = false) {
@@ -267,6 +274,28 @@ export function prepareSharedAction(store, who, operation, input, root, now = ne
267
274
  const allowed = CHAT_ACTION_FIELDS[operation];
268
275
  if (Object.keys(input).some((key) => !allowed.includes(key)))
269
276
  throw Error("This action contains an unsupported field.");
277
+ if (operation === "lead_about_you") {
278
+ // The owner's own note: the card shows the line, and the line it replaces beside it.
279
+ const checked = checkAboutYouLine(input["line"]);
280
+ if (!checked.ok)
281
+ throw Error(checked.message);
282
+ const lines = aboutYouOf(store, who.name);
283
+ const replaces = input["replaces"] === undefined || input["replaces"] === 0 ? 0 : integer(input, "replaces");
284
+ const next = withAboutYouLine(lines, checked.line, replaces);
285
+ if (!next.ok)
286
+ throw Error(next.message);
287
+ requireActor(store, who, "", operation);
288
+ const request = { line: checked.line, ...(replaces === 0 ? {} : { replaces }) };
289
+ const terms = replaces === 0
290
+ ? [checked.line, "Your lead keeps this about you in every project. Change it in Settings → Lead."]
291
+ : [`Was: ${lines[replaces - 1]}`, `Now: ${checked.line}`, "Replaces that line in what your lead knows about you. Change it in Settings → Lead."];
292
+ // An added line goes on the end, so it holds whatever else changed; a replacing card holds only while the line
293
+ // it replaces is still that line.
294
+ const state = replaces === 0 ? {} : { was: lines[replaces - 1] };
295
+ const title = replaces === 0 ? "Remember about you" : "Update what your lead knows about you";
296
+ const stamp = hash({ operation, request, repo: "", state, terms, actor: who.name, generation: who.generation, ceiling: who.ceilingDigest });
297
+ return { operation, request, repo: "", title, terms, stamp, state };
298
+ }
270
299
  const task = operation.startsWith("skill_") || operation.startsWith("knowledge_") || operation.startsWith("decision_") || operation.startsWith("tool_") || operation.startsWith("flow_") || operation.startsWith("teammate_") || operation.startsWith("kit_")
271
300
  ? null
272
301
  : text(input, "task", 64);
@@ -954,7 +983,7 @@ function savedActionContext(store, who, id, state) {
954
983
  (!shared && turn?.session !== session.id) ||
955
984
  turn?.state !== "answered")
956
985
  throw Error("This conversation ended or its project access changed. Ask for a fresh proposal.");
957
- requireActor(store, who, payload.repo);
986
+ requireActor(store, who, payload.repo, payload.operation);
958
987
  return payload;
959
988
  }
960
989
  export function mintSharedActionReview(store, who, id, root, now) {
@@ -1013,6 +1042,13 @@ export function executeSharedAction(store, who, id, payload, now, options) {
1013
1042
  return store.savepoint(() => {
1014
1043
  const task = typeof req["task"] === "string" ? req["task"] : null, repo = payload.repo, actor = who.name;
1015
1044
  let taskId = task;
1045
+ if (payload.operation === "lead_about_you") {
1046
+ const next = withAboutYouLine(aboutYouOf(store, actor), String(req["line"]), Number(req["replaces"] ?? 0));
1047
+ if (!next.ok)
1048
+ throw Error(next.message);
1049
+ saveAboutYou(store, actor, next.lines, now);
1050
+ return { ok: true, taskId: null, said: req["replaces"] === undefined ? "Your lead will remember this." : "Updated what your lead knows about you.", href: "/settings/lead" };
1051
+ }
1016
1052
  if (payload.operation === "skill_import")
1017
1053
  importSkill(store, repo, actor, req["files"], "Chat import", now);
1018
1054
  else if (payload.operation === "skill_test")
package/dist/chat-ask.js CHANGED
@@ -21,12 +21,21 @@ export function chatAskButtons(state, part, now) {
21
21
  })
22
22
  .filter((one) => one !== null);
23
23
  }
24
- /** Show the question again with what happened, and without its buttons. */
25
- function repaint(state, part, event, line) {
26
- const row = state.prepare("SELECT payload FROM chat_part WHERE id=?").get(part);
27
- const before = row === undefined ? { text: "" } : JSON.parse(String(row["payload"]));
24
+ /** How old a part may be and still be sent: older ones are dropped undelivered (slack-chat.ts, discord-chat.ts,
25
+ * teams-chat.ts), so an edit to one would never land. */
26
+ const EDIT_WINDOW_MS = 86_400_000;
27
+ /** Show the question again with what happened, and without its buttons: one edit, under the part's own time. A question
28
+ * too old to edit gets the line once as a new message instead. */
29
+ function repaint(state, part, event, line, now) {
30
+ const row = state.prepare("SELECT payload,created FROM chat_part WHERE id=?").get(part);
31
+ if (row === undefined || new Date(String(row["created"])).getTime() + EDIT_WINDOW_MS <= now.getTime()) {
32
+ state.plan(event.id, [{ text: line }], now);
33
+ return;
34
+ }
35
+ const before = JSON.parse(String(row["payload"]));
28
36
  const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.channel === undefined ? {} : { channel: before.channel }) };
29
37
  state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(JSON.stringify(content), part);
38
+ state.finish(event.id);
30
39
  }
31
40
  /** A tapped ask button, inside the action's transaction; false when the token isn't one. */
32
41
  export function applyChatAskTap(options, event, binding, token, now) {
@@ -41,17 +50,23 @@ export function applyChatAskTap(options, event, binding, token, now) {
41
50
  return true;
42
51
  }
43
52
  const turn = Number(action["turn"]), part = Number(action["part"]);
44
- const found = store.getMateTurn(turn)?.approver === binding.approver ? store.mateAskState(turn, now) : { state: "expired" };
53
+ // Only the person the lead asked answers it: anyone else's tap changes nothing and says nothing.
54
+ const asker = store.getMateTurn(turn)?.approver;
55
+ if (asker !== undefined && asker !== binding.approver) {
56
+ state.finish(event.id);
57
+ return true;
58
+ }
59
+ const found = asker === undefined ? { state: "expired" } : store.mateAskState(turn, now);
45
60
  const expired = found.state === "expired" || String(action["expires"]) <= now.toISOString();
46
- if (action["consumed"] !== null || found.state !== "open" || expired) {
61
+ // A question already settled was shown so once (or is being): a later tap says nothing more.
62
+ if (action["consumed"] !== null) {
63
+ state.finish(event.id);
64
+ return true;
65
+ }
66
+ if (found.state !== "open" || expired) {
47
67
  state.prepare("UPDATE chat_ask_action SET consumed=? WHERE part=? AND consumed IS NULL").run(now.toISOString(), part);
48
- if (found.state === "answered" || (action["consumed"] !== null && !expired)) {
49
- repaint(state, part, event, "This question was already answered.");
50
- state.finish(event.id);
51
- }
52
- // A question this old can no longer be edited in place: the answer to the tap is a new message.
53
- else
54
- say("This question expired. If it still matters, send your answer as a message.");
68
+ // The question is shown again without its buttons, so a later tap has nothing to press.
69
+ repaint(state, part, event, found.state === "answered" ? "This question was already answered." : "This question expired. If it still matters, send your answer as a message.", now);
55
70
  return true;
56
71
  }
57
72
  const ask = found.ask;
@@ -71,7 +86,6 @@ export function applyChatAskTap(options, event, binding, token, now) {
71
86
  channel: event.channel, member: event.member, ts: event.ts, thread: event.thread,
72
87
  payload: JSON.stringify({ text: option, originalLength: option.length }), created: now.toISOString(),
73
88
  });
74
- repaint(state, part, event, `You chose: ${option}`);
75
- state.finish(event.id);
89
+ repaint(state, part, event, `You chose: ${option}`, now);
76
90
  return true;
77
91
  }
@@ -513,8 +513,10 @@ export const CHAT_ACTION_PARITY = {
513
513
  propose_flow: { support: "direct", how: "Creates or changes a flow, adds, moves, approves, sends back or cancels its cards, comments on them (@name pings that person), sets their owner, follows them, saves the project's scripts, and adds, pauses or removes its triggers, through the shared confirm door; a long drawing opens the secure review. Work a card files is an ordinary task under the usual approvals.", gap: "The flow canvas itself is on the console; the phone confirms cards but draws nothing. Webhook addresses and the Linear key are set on the console's Triggers panel." },
514
514
  commit_to: { support: "direct", how: "Records what the lead promised to follow up on (a task, attempt, check or time; 7 days at most). The follow pass says one line when it is met, in the shared conversation and on the chat the promise was made on.", gap: null },
515
515
  release_commitment: { support: "direct", how: "Stops following up on one of the person's open promises, with the reason.", gap: null },
516
- remember: { support: "direct", how: "Proposes a correction or lasting preference as a decision or project instruction card, through the shared confirm door.", gap: null },
516
+ remember: { support: "direct", how: "Proposes a correction or lasting preference as a decision, project instruction or about-you card, through the shared confirm door; an about-you card shows the line it replaces.", gap: null },
517
+ get_person: { support: "direct", how: "Read during a turn: one person, AI teammate or team chat in full, with their open tasks.", gap: null },
517
518
  get_integrations: { support: "direct", how: "Read during a turn: which chat apps, email, GitHub, project tools and monitoring are Connected, Not set up or Broken, as Settings → Integrations shows them.", gap: null },
519
+ get_capabilities: { support: "direct", how: "Read during a turn: which agents are signed in, which workers are online, which tools and skills work, which integrations are connected and what checks the project runs, each with its next step and settings link.", gap: null },
518
520
  ask_owner: { support: "direct", how: "One question with 2-4 options and Something else, drawn as buttons under the reply; the tapped option is sent as the person's next message.", gap: null },
519
521
  search_project_memory: { support: "direct", how: "Read during a turn: one search over decisions, references, lessons and the conversations the person may read.", gap: null },
520
522
  list_tasks: { support: "direct", how: "Read during a turn.", gap: null },
@@ -0,0 +1,203 @@
1
+ import { type ModeTerms } from "./modes.js";
2
+ import { type VerifiedApprover } from "./principal.js";
3
+ import type { Scope } from "./scope.js";
4
+ import type { Store } from "./store.js";
5
+ /** The tokens live in chat_decide_action and the open "What should change?" in chat_decide_prompt (store.ts). */
6
+ export type DecideAct = "accept" | "changes" | "retry" | "approve" | "not-now" | "merge";
7
+ export type DecideTarget = {
8
+ kind: "result";
9
+ taskId: string;
10
+ run: number;
11
+ } | {
12
+ kind: "failed";
13
+ taskId: string;
14
+ } | {
15
+ kind: "plan";
16
+ taskId: string;
17
+ } | {
18
+ kind: "merge";
19
+ taskId: string;
20
+ run: number;
21
+ };
22
+ export type DecideChannel = "telegram" | "slack" | "discord" | "teams";
23
+ /** Who is tapping, where: one paired person's binding on one channel, and the chat the card is in. */
24
+ export type DecideSeat = {
25
+ channel: DecideChannel;
26
+ binding: number;
27
+ chat: string;
28
+ approver: string;
29
+ generation: number;
30
+ };
31
+ export type DecideLink = {
32
+ label: string;
33
+ path: string;
34
+ };
35
+ /** A button as a channel draws it: an act (its token) or a link to the console. */
36
+ export type DecideButton = {
37
+ label: string;
38
+ token: string;
39
+ } | {
40
+ label: string;
41
+ link: DecideLink;
42
+ };
43
+ /** The words, said once. */
44
+ export declare const DECIDE_LABELS: {
45
+ readonly accept: "Accept and finish";
46
+ readonly changes: "Request changes";
47
+ readonly retry: "Retry";
48
+ readonly approve: "Approve & start";
49
+ readonly "not-now": "Not now";
50
+ readonly merge: "Merge";
51
+ readonly look: "Look first";
52
+ readonly edit: "Edit";
53
+ readonly yes: "Yes";
54
+ readonly cancel: "Cancel";
55
+ };
56
+ export declare const WHAT_SHOULD_CHANGE = "What should change?";
57
+ type ActSpec = {
58
+ act: DecideAct;
59
+ run: number | null;
60
+ digest: string;
61
+ };
62
+ /** What a card offers: an optional replacement text (the plan card) and rows of acts and links. */
63
+ export type DecideOffer = {
64
+ text: string | null;
65
+ rows: Array<Array<ActSpec | DecideLink>>;
66
+ };
67
+ /** The live mode on this repository, when it lets this person's chat approve: the term signed, by them. */
68
+ export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date): {
69
+ ok: true;
70
+ digest: string;
71
+ terms: ModeTerms;
72
+ } | {
73
+ ok: false;
74
+ why: string;
75
+ };
76
+ /** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
77
+ export declare function permissionPlainWords(profile: Scope["profile"] | null | undefined): string | null;
78
+ /** A plan this person's chat may approve now: the term, the plan's exact bytes, and none of the reasons it must open
79
+ * Toolroll instead. `digest` binds the scope and the plan document the card shows. */
80
+ export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, root?: string): {
81
+ ok: true;
82
+ modeDigest: string;
83
+ digest: string;
84
+ scope: Scope;
85
+ } | {
86
+ ok: false;
87
+ why: string;
88
+ };
89
+ /** A ready pull request this person's chat may merge now, bound to the exact commit. */
90
+ export declare function mergeInChat(store: Store, taskId: string, run: number, approver: string, now: Date): {
91
+ ok: true;
92
+ modeDigest: string;
93
+ head: string;
94
+ } | {
95
+ ok: false;
96
+ why: string;
97
+ };
98
+ /** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. */
99
+ export declare function planCardText(store: Store, taskId: string, scope: Scope, root?: string): string;
100
+ /** The acts a card about this target can take in chat right now, or null when none can (the card keeps its own
101
+ * link). Reads only; nothing is minted. */
102
+ export declare function decideOffer(store: Store, target: DecideTarget, who: VerifiedApprover, now: Date, root?: string): DecideOffer | null;
103
+ /** One fingerprint of what the card offers, without its tokens: a repaint with the same offer keeps its buttons. */
104
+ export declare function offerFingerprint(offer: DecideOffer): string;
105
+ /** The card's buttons, minted now (message unknown until it is sent: `placeDecideTokens` puts them on it). */
106
+ export declare function mintDecideButtons(store: Store, seat: DecideSeat, target: DecideTarget, offer: DecideOffer, now: Date, message?: string | null): {
107
+ rows: DecideButton[][];
108
+ tokens: string[];
109
+ };
110
+ /** Put freshly minted tokens on the message they ride: a tap on any other message proves itself stale. */
111
+ export declare function placeDecideTokens(store: Store, tokens: readonly string[], message: string): void;
112
+ /** Every live button on one message stops working: the card was repainted, or one of its acts was taken. */
113
+ export declare function retireDecideTokens(store: Store, channel: DecideChannel, chat: string, message: string, now: Date): void;
114
+ /** Buttons minted for a send or an edit that didn't land: they never act. */
115
+ export declare function dropDecideTokens(store: Store, tokens: readonly string[], now: Date): void;
116
+ /** Whether a message still carries a button that acts (an offer, or an armed Yes or Cancel): a card whose acts were
117
+ * all spent, as by Not now, needs fresh ones. */
118
+ export declare function hasLiveDecideTokens(store: Store, channel: DecideChannel, chat: string, message: string, now: Date): boolean;
119
+ export declare function isDecideToken(store: Store, token: string): boolean;
120
+ export type DecideTapOutcome = {
121
+ ack: string;
122
+ /** The card repainted: its text and buttons now. */
123
+ edit?: {
124
+ text: string;
125
+ rows: DecideButton[][];
126
+ tokens: string[];
127
+ };
128
+ /** Ask "What should change?": the person's next message is the feedback. `id` names the prompt, so the channel can
129
+ * record the message it sent (a reply to anything else is not the feedback). */
130
+ prompt?: {
131
+ id: number;
132
+ text: string;
133
+ };
134
+ /** Merge after the transaction (GitHub is a network call); the card says "Merging…" until it lands, and
135
+ * `recordChatMerge` ledgers how it went. */
136
+ merge?: ChatMerge;
137
+ ignored?: boolean;
138
+ };
139
+ /** A merge approved in chat, waiting on GitHub: what its ledger line names once the answer is back. */
140
+ export type ChatMerge = {
141
+ runId: number;
142
+ taskId: string;
143
+ by: string;
144
+ repo: string;
145
+ via: string;
146
+ };
147
+ /** The links a card keeps once its acts are spent or stale: where to look, never a token. */
148
+ export declare function linksFor(target: DecideTarget): DecideButton[][];
149
+ /**
150
+ * One tap on a decide button, inside the caller's transaction. Null: not this module's token. The seat must be the
151
+ * token's own (person, chat, message); a spent, expired or foreign button acts on nothing.
152
+ */
153
+ export declare function applyDecideTap(store: Store, seat: DecideSeat, input: {
154
+ token: string;
155
+ message: string;
156
+ shown: string;
157
+ repos: readonly string[];
158
+ root?: string;
159
+ now: Date;
160
+ }): DecideTapOutcome | null;
161
+ /** A chat merge's second ledger line, after the GitHub call: merged, or failed and why (its approval is the first). */
162
+ export declare function recordChatMerge(store: Store, merge: ChatMerge, result: {
163
+ ok: true;
164
+ } | {
165
+ ok: false;
166
+ message: string;
167
+ }, now: Date): void;
168
+ /** What the card says once the merge came back. */
169
+ export declare function mergedText(shown: string, result: {
170
+ ok: true;
171
+ } | {
172
+ ok: false;
173
+ message: string;
174
+ }): string;
175
+ export type DecidePrompt = {
176
+ id: number;
177
+ taskId: string;
178
+ run: number;
179
+ digest: string;
180
+ message: string | null;
181
+ };
182
+ /** One open "What should change?" per person and chat: a newer one replaces it. */
183
+ export declare function openDecidePrompt(store: Store, seat: DecideSeat, taskId: string, run: number, digest: string, now: Date): number;
184
+ /** The message that asked "What should change?", once it was sent. */
185
+ export declare function placeDecidePrompt(store: Store, id: number, message: string): void;
186
+ export declare function openPromptFor(store: Store, seat: Pick<DecideSeat, "channel" | "binding" | "chat">, now: Date): DecidePrompt | null;
187
+ /** Whether this message answers the prompt: the person's next message, unless it replies to some other message. */
188
+ export declare function answersPrompt(prompt: DecidePrompt, replyTo: string | null): boolean;
189
+ /** The person's message after "What should change?": a revision of the exact result, with these words as its
190
+ * feedback, through the result page's own service. Says what was sent, once. */
191
+ export declare function applyDecideFeedback(store: Store, seat: DecideSeat, prompt: DecidePrompt, text: string, repos: readonly string[], root: string | undefined, now: Date): {
192
+ said: string;
193
+ link: DecideLink | null;
194
+ };
195
+ /** The link a card keeps when it can't act in chat: a plan's is Review & start, where the password step lives. */
196
+ export declare function decideFallbackLink(target: DecideTarget | null): DecideLink | null;
197
+ /** Which card a pushed notification is, from its recorded kind and task — never from its words. */
198
+ export declare function decideTargetOf(row: {
199
+ kind: string;
200
+ taskId: string | null;
201
+ run: number | null;
202
+ }): DecideTarget | null;
203
+ export {};