toolroll 0.9.15 → 0.9.16
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.
- package/CHANGELOG.md +19 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +49 -26
- package/dist/assignment.d.ts +8 -7
- package/dist/assignment.js +13 -8
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +22 -22
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +5 -0
- package/dist/chat-ask.d.ts +28 -0
- package/dist/chat-ask.js +77 -0
- package/dist/chat-channel.js +6 -1
- package/dist/chat-controls.d.ts +8 -0
- package/dist/chat-controls.js +3 -0
- package/dist/chat-delivery-state.d.ts +6 -1
- package/dist/chat-delivery-state.js +15 -2
- package/dist/chat-delivery.js +20 -3
- package/dist/chat-quiet.js +4 -2
- package/dist/chat-voice.d.ts +3 -6
- package/dist/chat-voice.js +8 -2
- package/dist/discord-chat.js +10 -0
- package/dist/lead-commitments.d.ts +84 -0
- package/dist/lead-commitments.js +208 -0
- package/dist/lead-context.d.ts +32 -3
- package/dist/lead-context.js +133 -19
- package/dist/lead-follow.js +3 -0
- package/dist/lead-identity.d.ts +23 -0
- package/dist/lead-identity.js +33 -0
- package/dist/lead-voice.d.ts +12 -9
- package/dist/lead-voice.js +25 -16
- package/dist/ledger-view.js +3 -2
- package/dist/mate-cli.d.ts +1 -1
- package/dist/mate-cli.js +8 -2
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +11 -4
- package/dist/mate-progress.js +5 -0
- package/dist/mate-tools.d.ts +18 -1
- package/dist/mate-tools.js +250 -6
- package/dist/mate.d.ts +9 -0
- package/dist/mate.js +34 -38
- package/dist/operate.js +2 -1
- package/dist/project-delete.js +3 -1
- package/dist/result-acts.d.ts +14 -0
- package/dist/result-acts.js +19 -0
- package/dist/retention.js +6 -0
- package/dist/serve.js +111 -29
- package/dist/slack-chat.js +10 -0
- package/dist/store.d.ts +51 -0
- package/dist/store.js +115 -1
- package/dist/surface.js +2 -2
- package/dist/task-status.d.ts +5 -15
- package/dist/task-status.js +18 -5
- package/dist/team-runtime.js +2 -1
- package/dist/teams-chat.js +4 -1
- package/dist/telegram-mate.d.ts +4 -1
- package/dist/telegram-mate.js +17 -1
- package/dist/telegram-progress.js +3 -2
- package/dist/telegram.js +43 -3
- package/dist/test-temp.js +1 -1
- package/dist/when-html.d.ts +8 -3
- package/dist/when-html.js +23 -3
- package/dist/work-index.d.ts +2 -0
- package/dist/work-index.js +9 -6
- package/dist/work-summary.d.ts +1 -1
- package/dist/workspace-ui.d.ts +2 -14
- package/dist/workspace-ui.js +4 -22
- package/package.json +1 -1
package/dist/browser-crew.js
CHANGED
|
@@ -47,6 +47,7 @@ export function browserCrewFromIndex(page, conversationId) {
|
|
|
47
47
|
id: summary.rootId, title: summary.title, project: summary.repo,
|
|
48
48
|
state: summary.assignmentState, label: status.label, tone: status.tone, href: link(href), resultHref: resultHref === null ? null : link(resultHref),
|
|
49
49
|
action: action === null || actionHref === null ? null : { label: action.label, href: link(actionHref) },
|
|
50
|
+
...(summary.lead === undefined ? {} : { lead: summary.lead }),
|
|
50
51
|
};
|
|
51
52
|
return { item, rank: status.rank };
|
|
52
53
|
});
|
|
@@ -46,6 +46,8 @@ export type BrowserCrewItem = {
|
|
|
46
46
|
label: string;
|
|
47
47
|
href: string;
|
|
48
48
|
} | null;
|
|
49
|
+
/** "<name> is on it.": the person's own lead took it on, by the name they gave it. */
|
|
50
|
+
lead?: string;
|
|
49
51
|
};
|
|
50
52
|
export type BrowserMessage = {
|
|
51
53
|
id: number;
|
|
@@ -1024,6 +1026,8 @@ export type BrowserWorkspace = {
|
|
|
1024
1026
|
user: string;
|
|
1025
1027
|
csrf: string;
|
|
1026
1028
|
sensitive: boolean;
|
|
1029
|
+
/** What this person named their lead (Settings → Lead): the chat header and every lead message say it. */
|
|
1030
|
+
leadName?: string;
|
|
1027
1031
|
refreshUrl: string;
|
|
1028
1032
|
receipt: {
|
|
1029
1033
|
request: string;
|
|
@@ -1118,6 +1122,7 @@ export type BrowserHome = {
|
|
|
1118
1122
|
allHref: string;
|
|
1119
1123
|
/** What this person's lead is doing now and when it last acted (lead-voice.ts); its task when it named one. */
|
|
1120
1124
|
lead?: {
|
|
1125
|
+
name?: string;
|
|
1121
1126
|
doing: string;
|
|
1122
1127
|
at: string;
|
|
1123
1128
|
href: string | null;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The lead's question to its owner (ask_owner) in Slack, Discord and Teams.
|
|
3
|
+
*
|
|
4
|
+
* The reply's last part carries the question with one button per option and
|
|
5
|
+
* one for "Something else", minted when the part is planned (chat_ask_action).
|
|
6
|
+
* A tapped option is sent as the owner's next message in their own chat with
|
|
7
|
+
* Toolroll, so the lead reads it like anything they typed; "Something else"
|
|
8
|
+
* asks them to type it. Buttons are drawn only in that own chat (any thread of
|
|
9
|
+
* it): in a room the question goes out as text, since a tap there is not the
|
|
10
|
+
* owner's own message. A button works once, only for the person the lead
|
|
11
|
+
* asked, and only while nobody has written in that thread since.
|
|
12
|
+
*/
|
|
13
|
+
import type { ChatBinding, ChatEvent, ChatState } from "./chat-delivery-state.js";
|
|
14
|
+
import { type MateAsk, type Store } from "./store.js";
|
|
15
|
+
/** The question as text with its options, where no buttons are drawn. */
|
|
16
|
+
export declare function chatAskText(ask: Pick<MateAsk, "question" | "options">): string;
|
|
17
|
+
/** The live buttons on one part, in the order they were minted: the options, then "Something else". None when the part
|
|
18
|
+
* is not in its owner's own chat with Toolroll (a room), where a tap could not be their message. */
|
|
19
|
+
export declare function chatAskButtons(state: ChatState, part: number, now: Date): Array<{
|
|
20
|
+
token: string;
|
|
21
|
+
label: string;
|
|
22
|
+
words: boolean;
|
|
23
|
+
}>;
|
|
24
|
+
/** A tapped ask button, inside the action's transaction; false when the token isn't one. */
|
|
25
|
+
export declare function applyChatAskTap(options: {
|
|
26
|
+
store: Store;
|
|
27
|
+
state: ChatState;
|
|
28
|
+
}, event: ChatEvent, binding: ChatBinding, token: string, now: Date): boolean;
|
package/dist/chat-ask.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { chatHash } from "./chat-delivery-state.js";
|
|
2
|
+
import { MATE_ASK_OTHER } from "./store.js";
|
|
3
|
+
/** The question as text with its options, where no buttons are drawn. */
|
|
4
|
+
export function chatAskText(ask) {
|
|
5
|
+
return [ask.question, ...ask.options.map(one => `• ${one}`), `• ${MATE_ASK_OTHER}: say it in your own words`].join("\n");
|
|
6
|
+
}
|
|
7
|
+
/** The live buttons on one part, in the order they were minted: the options, then "Something else". None when the part
|
|
8
|
+
* is not in its owner's own chat with Toolroll (a room), where a tap could not be their message. */
|
|
9
|
+
export function chatAskButtons(state, part, now) {
|
|
10
|
+
const row = state.prepare("SELECT p.payload,e.channel,b.channel AS own FROM chat_part p JOIN chat_event e ON e.id=p.event JOIN chat_binding b ON b.id=e.binding WHERE p.id=?").get(part);
|
|
11
|
+
if (row === undefined)
|
|
12
|
+
return [];
|
|
13
|
+
const content = JSON.parse(String(row["payload"]));
|
|
14
|
+
if ((content.channel ?? row["channel"]) !== row["own"])
|
|
15
|
+
return [];
|
|
16
|
+
const options = content.ask?.options ?? [];
|
|
17
|
+
return state.prepare("SELECT token,choice FROM chat_ask_action WHERE part=? AND consumed IS NULL AND expires>? ORDER BY rowid").all(part, now.toISOString())
|
|
18
|
+
.map(one => {
|
|
19
|
+
const label = one.choice === null ? MATE_ASK_OTHER : options[Number(one.choice)] ?? null;
|
|
20
|
+
return label === null ? null : { token: String(one.token), label: label.slice(0, 75), words: one.choice === null };
|
|
21
|
+
})
|
|
22
|
+
.filter((one) => one !== null);
|
|
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"]));
|
|
28
|
+
const content = { text: `${before.text}\n\n${line}`.slice(0, 3400), edit: event.ts, ...(before.channel === undefined ? {} : { channel: before.channel }) };
|
|
29
|
+
state.prepare("UPDATE chat_part SET payload=?,state='pending',next_at=NULL WHERE id=?").run(JSON.stringify(content), part);
|
|
30
|
+
}
|
|
31
|
+
/** A tapped ask button, inside the action's transaction; false when the token isn't one. */
|
|
32
|
+
export function applyChatAskTap(options, event, binding, token, now) {
|
|
33
|
+
const { store, state } = options;
|
|
34
|
+
const action = state.prepare("SELECT a.*,p.message,e.binding AS owner,e.channel FROM chat_ask_action a JOIN chat_part p ON p.id=a.part JOIN chat_event e ON e.id=p.event WHERE a.token=?").get(token);
|
|
35
|
+
if (action === undefined)
|
|
36
|
+
return false;
|
|
37
|
+
const say = (text) => state.plan(event.id, [{ text }], now);
|
|
38
|
+
// Bound to the person, their own chat and the exact message the button rode.
|
|
39
|
+
if (Number(action["owner"]) !== binding.id || action["channel"] !== event.channel || event.channel !== binding.channel || action["message"] !== event.ts) {
|
|
40
|
+
say("That button expired or was already used.");
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
const turn = Number(action["turn"]), part = Number(action["part"]);
|
|
44
|
+
const found = store.getMateTurn(turn)?.approver === binding.approver ? store.mateAskState(turn, now) : { state: "expired" };
|
|
45
|
+
const expired = found.state === "expired" || String(action["expires"]) <= now.toISOString();
|
|
46
|
+
if (action["consumed"] !== null || found.state !== "open" || expired) {
|
|
47
|
+
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.");
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
const ask = found.ask;
|
|
58
|
+
if (action["choice"] === null) {
|
|
59
|
+
say("Type your answer here as your next message.");
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const option = ask.options[Number(action["choice"])];
|
|
63
|
+
if (option === undefined) {
|
|
64
|
+
say("That option is no longer there.");
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
state.prepare("UPDATE chat_ask_action SET consumed=? WHERE part=? AND consumed IS NULL").run(now.toISOString(), part);
|
|
68
|
+
// The option is the owner's next message, answered like one they typed.
|
|
69
|
+
state.enqueue({
|
|
70
|
+
id: chatHash(`${state.channel}:ask:${event.id}`).slice(0, 32), installation: event.installation, binding: binding.id, kind: "message",
|
|
71
|
+
channel: event.channel, member: event.member, ts: event.ts, thread: event.thread,
|
|
72
|
+
payload: JSON.stringify({ text: option, originalLength: option.length }), created: now.toISOString(),
|
|
73
|
+
});
|
|
74
|
+
repaint(state, part, event, `You chose: ${option}`);
|
|
75
|
+
state.finish(event.id);
|
|
76
|
+
return true;
|
|
77
|
+
}
|
package/dist/chat-channel.js
CHANGED
|
@@ -117,7 +117,7 @@ export function mirrorToTaskChat(store, who, taskId, surface, message, reply, no
|
|
|
117
117
|
}
|
|
118
118
|
/** The one short message a person gets when their question could not be answered. */
|
|
119
119
|
export function couldNotAnswerText(reason) {
|
|
120
|
-
return `I couldn't answer that just now: ${reason}. Ask again, or open the console.`;
|
|
120
|
+
return `I couldn't answer that just now: ${reason.replace(/\.$/, "")}. Ask again, or open the console.`;
|
|
121
121
|
}
|
|
122
122
|
export function tooLongText(length) {
|
|
123
123
|
return `That message is ${length.toLocaleString("en-US")} characters; chat takes up to ${MATE_MESSAGE_MAX_CHARS.toLocaleString("en-US")}. Nothing was sent to the assistant. Send it in shorter parts, or say which part matters most.`;
|
|
@@ -511,6 +511,11 @@ export const CHAT_ACTION_PARITY = {
|
|
|
511
511
|
propose_teammate: { support: "direct", how: "Adds a teammate from a template or a soul file, changes one section of its soul file (or the whole short file), pauses, resumes or removes it, passes it a note, or answers its question for the person asked, through the shared confirm door.", gap: "Soul files longer than one message are edited on the console's teammate page; the phone changes one section at a time." },
|
|
512
512
|
get_flow_insights: { support: "direct", how: "Read during a turn: where each flow's cards pass, fail or are sent back, how long they wait, how its scripts did, and a run's log.", gap: null },
|
|
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
|
+
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
|
+
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 },
|
|
517
|
+
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 },
|
|
518
|
+
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 },
|
|
514
519
|
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 },
|
|
515
520
|
list_tasks: { support: "direct", how: "Read during a turn.", gap: null },
|
|
516
521
|
get_task: {
|
package/dist/chat-controls.d.ts
CHANGED
|
@@ -66,6 +66,14 @@ export declare const CHAT_CONTROLS: {
|
|
|
66
66
|
readonly label: "Manage Discord";
|
|
67
67
|
readonly href: "/settings/discord";
|
|
68
68
|
};
|
|
69
|
+
readonly lead: {
|
|
70
|
+
readonly label: "Name your lead";
|
|
71
|
+
readonly href: "/settings/lead";
|
|
72
|
+
};
|
|
73
|
+
readonly integrations: {
|
|
74
|
+
readonly label: "Set up integrations";
|
|
75
|
+
readonly href: "/settings/integrations";
|
|
76
|
+
};
|
|
69
77
|
readonly settings: {
|
|
70
78
|
readonly label: "Open settings";
|
|
71
79
|
readonly href: "/settings";
|
package/dist/chat-controls.js
CHANGED
|
@@ -18,6 +18,9 @@ export const CHAT_CONTROLS = {
|
|
|
18
18
|
workers: { label: "Manage workers", href: "/fleet" },
|
|
19
19
|
slack: { label: "Manage Slack", href: "/settings/slack" },
|
|
20
20
|
discord: { label: "Manage Discord", href: "/settings/discord" },
|
|
21
|
+
// Settings → Lead: the lead's name and persona, changed there by its person.
|
|
22
|
+
lead: { label: "Name your lead", href: "/settings/lead" },
|
|
23
|
+
integrations: { label: "Set up integrations", href: "/settings/integrations" },
|
|
21
24
|
settings: { label: "Open settings", href: "/settings" },
|
|
22
25
|
permissions: { label: "Review permissions", href: "/settings" },
|
|
23
26
|
providers: { label: "Connect an agent", href: "/settings#providers" },
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Store } from "./store.js";
|
|
2
2
|
export type ChatIdentity = {
|
|
3
3
|
installation: string;
|
|
4
4
|
team?: string;
|
|
@@ -95,6 +95,11 @@ export type ChatContent = {
|
|
|
95
95
|
label: string;
|
|
96
96
|
}>;
|
|
97
97
|
};
|
|
98
|
+
/** The lead's question to its owner rides this part: its options, then "Something else". */
|
|
99
|
+
ask?: {
|
|
100
|
+
turn: number;
|
|
101
|
+
options: string[];
|
|
102
|
+
};
|
|
98
103
|
};
|
|
99
104
|
export declare const chatHash: (text: string) => string;
|
|
100
105
|
export declare class ChatState {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Durable private-chat receipts. Each transport has its own tables and lease. */
|
|
2
2
|
import { createHash, randomBytes } from "node:crypto";
|
|
3
|
+
import { MATE_ASK_TTL_MS } from "./store.js";
|
|
3
4
|
const CHAT_SCHEMA = `
|
|
4
5
|
CREATE TABLE IF NOT EXISTS chat_binding (
|
|
5
6
|
id INTEGER PRIMARY KEY AUTOINCREMENT, installation TEXT NOT NULL,
|
|
@@ -93,6 +94,15 @@ CREATE TABLE IF NOT EXISTS chat_question_prompt (
|
|
|
93
94
|
);
|
|
94
95
|
CREATE INDEX IF NOT EXISTS chat_question_prompt_open ON chat_question_prompt(binding, consumed);
|
|
95
96
|
`;
|
|
97
|
+
/** The lead's question to its owner (ask_owner) in the chat app: one button per option (choice is its index) and one for
|
|
98
|
+
* "Something else" (choice NULL). A tap sends the option as the owner's next message. Never in CHAT_TABLES. */
|
|
99
|
+
const CHAT_ASK_SCHEMA = `
|
|
100
|
+
CREATE TABLE IF NOT EXISTS chat_ask_action (
|
|
101
|
+
token TEXT PRIMARY KEY, part INTEGER NOT NULL REFERENCES chat_part(id) ON DELETE CASCADE,
|
|
102
|
+
turn INTEGER NOT NULL, choice INTEGER, expires TEXT NOT NULL, consumed TEXT
|
|
103
|
+
);
|
|
104
|
+
CREATE INDEX IF NOT EXISTS chat_ask_action_turn ON chat_ask_action(turn);
|
|
105
|
+
`;
|
|
96
106
|
const CHAT_TABLES = [
|
|
97
107
|
"chat_binding",
|
|
98
108
|
"chat_pair",
|
|
@@ -111,7 +121,7 @@ export class ChatState {
|
|
|
111
121
|
this.channel = channel;
|
|
112
122
|
}
|
|
113
123
|
prepare(sql) {
|
|
114
|
-
return this.db.prepare(sql.replace(/\bchat_(binding|pair|event|part|action|progress|runtime|room|meta|flow_action|flow_prompt|question_action|question_prompt)\b/g, `${this.channel}_$1`));
|
|
124
|
+
return this.db.prepare(sql.replace(/\bchat_(binding|pair|event|part|action|progress|runtime|room|meta|flow_action|flow_prompt|question_action|question_prompt|ask_action)\b/g, `${this.channel}_$1`));
|
|
115
125
|
}
|
|
116
126
|
/** Small transport facts a channel must remember per chat (a Teams service URL). Never credentials. */
|
|
117
127
|
meta(installation, key) {
|
|
@@ -273,6 +283,9 @@ export class ChatState {
|
|
|
273
283
|
if (Number(inserted.changes) && part.question)
|
|
274
284
|
for (const one of part.question.choices)
|
|
275
285
|
this.prepare("INSERT INTO chat_question_action(token,part,question,choice,expires) VALUES(?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.question.id, one.choice, new Date(now.getTime() + 7 * 86_400_000).toISOString());
|
|
286
|
+
if (Number(inserted.changes) && part.ask)
|
|
287
|
+
for (const choice of [...part.ask.options.map((_, index) => index), null])
|
|
288
|
+
this.prepare("INSERT INTO chat_ask_action(token,part,turn,choice,expires) VALUES(?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.ask.turn, choice, new Date(now.getTime() + MATE_ASK_TTL_MS).toISOString());
|
|
276
289
|
if (Number(inserted.changes) && part.flow)
|
|
277
290
|
for (const action of part.flow.actions)
|
|
278
291
|
this.prepare("INSERT INTO chat_flow_action(token,part,card,entry,action,expires) VALUES(?,?,?,?,?,?)").run(randomBytes(16).toString("hex"), Number(inserted.lastInsertRowid), part.flow.card, part.flow.entry, action, new Date(now.getTime() + 7 * 86_400_000).toISOString());
|
|
@@ -294,7 +307,7 @@ export class ChatState {
|
|
|
294
307
|
return !!this.prepare("SELECT 1 FROM chat_runtime WHERE installation=? AND owner=? AND lease_until>?").get(installation, owner, now.toISOString());
|
|
295
308
|
}
|
|
296
309
|
}
|
|
297
|
-
export const chatSchema = (channel) => (CHAT_SCHEMA + CHAT_ROOM_SCHEMA + CHAT_FLOW_SCHEMA + CHAT_QUESTION_SCHEMA).replaceAll("chat_", `${channel}_`);
|
|
310
|
+
export const chatSchema = (channel) => (CHAT_SCHEMA + CHAT_ROOM_SCHEMA + CHAT_FLOW_SCHEMA + CHAT_QUESTION_SCHEMA + CHAT_ASK_SCHEMA).replaceAll("chat_", `${channel}_`);
|
|
298
311
|
export const chatTables = (channel) => CHAT_TABLES.map((name) => name.replace("chat_", `${channel}_`));
|
|
299
312
|
export class ChatDeliveryError extends Error {
|
|
300
313
|
code;
|
package/dist/chat-delivery.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Shared saved replies, explicit confirmations and progress for private chat transports. */
|
|
2
2
|
import { answerChatQuestionPrompt, applyChatQuestionTap, questionParts } from "./teammate-question.js";
|
|
3
|
+
import { applyChatAskTap, chatAskText } from "./chat-ask.js";
|
|
3
4
|
import { channelRepos, resolveChannelMate, proposalPreview, proposalOutcomeText, confirmedLink, confirmedCardText, replyContextFor, mirrorToTaskChat, focusContextFor, taskInCeiling, tooLongText, } from "./chat-channel.js";
|
|
4
5
|
import { MATE_MESSAGE_MAX_CHARS, runMateTurn } from "./mate.js";
|
|
5
6
|
import { confirmMateProposal, dismissMateProposal, } from "./mate-doors.js";
|
|
@@ -11,7 +12,9 @@ import { triggerConfigOf } from "./flow-triggers.js";
|
|
|
11
12
|
import { telegramProgressCard } from "./telegram-progress.js";
|
|
12
13
|
import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerson, quietCardView } from "./chat-quiet.js";
|
|
13
14
|
import { BATCH_MS, chatText, chatTitle } from "./chat-voice.js";
|
|
14
|
-
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText } from "./lead-voice.js";
|
|
15
|
+
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
16
|
+
import { leadChannelOf } from "./lead-context.js";
|
|
17
|
+
import { promiseChannelOf } from "./lead-commitments.js";
|
|
15
18
|
import { phoneText, PHONE_HELP, phoneCommand, phoneStatus, phoneTaskView, phoneTaskChoices, phoneTaskListText, resolvePhoneTask, phoneFocusText, PHONE_NO_MATCH, PHONE_BACK_TO_LEAD } from "./telegram-status.js";
|
|
16
19
|
import { applyRoomInbound, conversationRow, roomCardApprover, roomCommand, roomGrantAllowed, roomMessagesAfter, roomMessageText, teamDomain } from "./chat-rooms.js";
|
|
17
20
|
import { isTelegramProgressNotification, proposalTaskOf } from "./store.js";
|
|
@@ -258,6 +261,8 @@ export async function processChatEvent(options) {
|
|
|
258
261
|
.prepare("UPDATE chat_event SET payload=? WHERE id=? AND state='queued'")
|
|
259
262
|
.run(JSON.stringify({ ...input, about }), event.id);
|
|
260
263
|
}
|
|
264
|
+
// An unknown surface names no channel rather than a guess.
|
|
265
|
+
const channel = leadChannelOf(options.label);
|
|
261
266
|
const outcome = await runMateTurn({
|
|
262
267
|
store,
|
|
263
268
|
who: resolved.who,
|
|
@@ -276,6 +281,7 @@ export async function processChatEvent(options) {
|
|
|
276
281
|
clock: () => nowOf(options),
|
|
277
282
|
evidenceRoot: options.evidenceRoot,
|
|
278
283
|
mediaDelivery: "documents",
|
|
284
|
+
...(channel === undefined ? {} : { channel }),
|
|
279
285
|
revalidate: async () => {
|
|
280
286
|
try {
|
|
281
287
|
await channelAccess(options, binding, resolved.who.ceilingDigest);
|
|
@@ -337,6 +343,12 @@ export async function processChatEvent(options) {
|
|
|
337
343
|
.find((message) => message.turn === turn.id && message.role === "assistant")?.text ?? "The reply is no longer in the saved thread.";
|
|
338
344
|
// The lead's answer about one task names it: a reply to it stays on that task.
|
|
339
345
|
const parts = splitChatText(reply, options.partSize).map((text) => (about === null ? { text } : { text, task: about.task, ...(about.run === null ? {} : { run: about.run }) }));
|
|
346
|
+
// The lead's question to its owner: its options as buttons, then "Something else", in their own chat (any thread);
|
|
347
|
+
// in a room a tap is not their message, so the question and its options go out as text.
|
|
348
|
+
const ask = store.mateAsk(turn.id);
|
|
349
|
+
if (ask !== null)
|
|
350
|
+
parts.push({ ...(event.channel === binding.channel ? { text: phoneText(ask.question, 1000), ask: { turn: ask.turn, options: ask.options } } : { text: phoneText(chatAskText(ask), 1000) }),
|
|
351
|
+
...(about === null ? {} : { task: about.task, ...(about.run === null ? {} : { run: about.run }) }) });
|
|
340
352
|
for (const image of store.listMateTurnEvidence(turn.id))
|
|
341
353
|
parts.push({
|
|
342
354
|
text: image.caption,
|
|
@@ -432,6 +444,9 @@ export function applyChatAction(options, event, binding, repos) {
|
|
|
432
444
|
// A teammate's question's button (v93) is answered by the question's own door.
|
|
433
445
|
if (applyChatQuestionTap({ store, state, label: options.label }, event, binding, token, now))
|
|
434
446
|
return;
|
|
447
|
+
// The lead's question to its owner: the tapped option becomes their next message.
|
|
448
|
+
if (applyChatAskTap({ store, state }, event, binding, token, now))
|
|
449
|
+
return;
|
|
435
450
|
const action = state
|
|
436
451
|
.prepare("SELECT a.*,p.message,e.binding,e.channel,e.thread FROM chat_action a JOIN chat_part p ON p.id=a.part JOIN chat_event e ON e.id=p.event WHERE token=?")
|
|
437
452
|
.get(token);
|
|
@@ -665,6 +680,8 @@ export async function planChatNotifications(options) {
|
|
|
665
680
|
const personal = notification.recipient !== null;
|
|
666
681
|
if (notification.createdAt >= binding.created &&
|
|
667
682
|
notification.resolvedAt === null &&
|
|
683
|
+
// A promise the lead made on another chat is reported there (lead-commitments.ts).
|
|
684
|
+
(promiseChannelOf(notification) ?? state.channel) === state.channel &&
|
|
668
685
|
// Pings follow responsibility: the lead's work, this person's own act and a muted project stay in the console.
|
|
669
686
|
store.pingAllowed(notification, binding.approver) &&
|
|
670
687
|
// A flow decision for "anyone who approves" reaches every approver who can see the project.
|
|
@@ -680,7 +697,7 @@ export async function planChatNotifications(options) {
|
|
|
680
697
|
const quiet = !personal && isTaskFact(notification) && store.notificationPreference(binding.approver).mode === "quiet";
|
|
681
698
|
if (notification.kind === LEAD_SAY_KIND) {
|
|
682
699
|
// The lead's words (lead-voice.ts): one message, repainted in place when a later say joins it.
|
|
683
|
-
const content = { text: leadSayText(notification),
|
|
700
|
+
const content = { text: leadSayText(store, notification),
|
|
684
701
|
...(notification.link ? { link: { label: "Open", path: notification.link } } : {}) };
|
|
685
702
|
const earlier = leadSayEarlier(store, notification).map(one => state.prepare("SELECT id FROM chat_part WHERE event=?")
|
|
686
703
|
.get(chatHash(`${options.state.channel}:notice:${binding.id}:${one}`))).find(one => one !== undefined);
|
|
@@ -784,7 +801,7 @@ export async function planChatNotifications(options) {
|
|
|
784
801
|
state.plan(id, [
|
|
785
802
|
{
|
|
786
803
|
// Never the task's id or a "— revision" suffix (chat-voice.ts).
|
|
787
|
-
text: chatText(phoneText(notification.body === "" ? notification.
|
|
804
|
+
text: chatText(phoneText(notification.body === "" ? leadSubjectOf(store, notification, binding.approver) : `${leadSubjectOf(store, notification, binding.approver)}\n\n${notification.body}`, 2500), notification.taskId === null ? [] : [{ id: notification.taskId, title: chatTitle(store, notification.taskId) }]),
|
|
788
805
|
...(notification.taskId ? { task: notification.taskId } : {}),
|
|
789
806
|
...(run ? { run: run.id } : {}),
|
|
790
807
|
...(notification.link
|
package/dist/chat-quiet.js
CHANGED
|
@@ -11,6 +11,7 @@ import { assignmentStatusFacts, headlineEmoji, taskStatusOf } from "./task-statu
|
|
|
11
11
|
import { asksToFinish, batchLine, chatText, chatTitle, factLinkLabel, mentions, READY_ACTIONS } from "./chat-voice.js";
|
|
12
12
|
import { phoneText, projectLabel } from "./telegram-status.js";
|
|
13
13
|
import { isLifecycleNotification } from "./store.js";
|
|
14
|
+
import { leadSubjectOf } from "./lead-voice.js";
|
|
14
15
|
/** What always makes a new message, even in quiet mode: a question or approval waiting, a failure or other
|
|
15
16
|
* attention fact, a result Ready for review, a security alert or release, and anything addressed to one person
|
|
16
17
|
* (a sign-in or plan-limit pause, an evening digest). */
|
|
@@ -128,7 +129,8 @@ function lineOf(store, item, now, root, viewer) {
|
|
|
128
129
|
const row = item.notification === null ? null : store.notificationById(item.notification);
|
|
129
130
|
if (row !== null && row.kind !== "run-finished") {
|
|
130
131
|
// Never the task's id or a "— revision" suffix; the title once, in front unless the words already name it.
|
|
131
|
-
const
|
|
132
|
+
const subject = leadSubjectOf(store, row, viewer);
|
|
133
|
+
const words = chatText(phoneText(row.body === "" ? subject : `${subject}\n\n${row.body}`, 2500), [{ id: ref.externalId, title: summary }]);
|
|
132
134
|
const { phrase, headline } = updatePhrase(row);
|
|
133
135
|
return { also: [], link: row.link === null ? { label: "Open task", path: chatControlHref("task", ref.externalId) } : { label: factLinkLabel(row.link), path: row.link },
|
|
134
136
|
fact: { summary, headline, checks: null, report: false, completedBy: null, update: { words: mentions(words, summary) ? words : `${summary} · ${words}`, phrase } } };
|
|
@@ -149,7 +151,7 @@ function lineOf(store, item, now, root, viewer) {
|
|
|
149
151
|
pullRequest,
|
|
150
152
|
// The lead speaks as "I" for what it did: the work it filed or approved, or the completion it recorded.
|
|
151
153
|
lead: card.status.headline === "Complete" ? card.completedByLead : store.leadWorkOf(item.taskRef) !== null,
|
|
152
|
-
...(card.facts.lead === "on-it" ? { leadOnIt: true } : {}),
|
|
154
|
+
...(card.facts.lead === "on-it" ? { leadOnIt: true, ...(card.facts.leadName === undefined ? {} : { leadName: card.facts.leadName }) } : {}),
|
|
153
155
|
};
|
|
154
156
|
// A result ready for a person offers its real next step, then a look first.
|
|
155
157
|
if (asksToFinish(fact))
|
package/dist/chat-voice.d.ts
CHANGED
|
@@ -1,8 +1,3 @@
|
|
|
1
|
-
/** How chat sounds: a teammate talking. One or two short sentences, outcome
|
|
2
|
-
* first, plain words, one action at most. Every chat (Telegram, Slack,
|
|
3
|
-
* Discord, Teams) takes its titles and its finished-work lines from here, so a
|
|
4
|
-
* task id, a "— revision" suffix or a person's own act never reaches a chat.
|
|
5
|
-
* The console keeps every exact name and fact. */
|
|
6
1
|
import type { TelegramTransport } from "./telegram.js";
|
|
7
2
|
import type { Store } from "./store.js";
|
|
8
3
|
/** The bot's display name in every chat app. */
|
|
@@ -34,8 +29,10 @@ export type FinishedFact = {
|
|
|
34
29
|
words: string;
|
|
35
30
|
phrase: string;
|
|
36
31
|
} | null;
|
|
37
|
-
/** The person's lead took it on (lead-voice.ts): "
|
|
32
|
+
/** The person's lead took it on (lead-voice.ts): "<name> is on it", never "It waits for you". */
|
|
38
33
|
leadOnIt?: boolean;
|
|
34
|
+
/** What the person calls their lead (Settings → Lead); "Lead" when unnamed. */
|
|
35
|
+
leadName?: string;
|
|
39
36
|
};
|
|
40
37
|
/** The two buttons under a result ready for a person: the real next step, then a look first. */
|
|
41
38
|
export declare const READY_ACTIONS: {
|
package/dist/chat-voice.js
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/** How chat sounds: a teammate talking. One or two short sentences, outcome
|
|
2
|
+
* first, plain words, one action at most. Every chat (Telegram, Slack,
|
|
3
|
+
* Discord, Teams) takes its titles and its finished-work lines from here, so a
|
|
4
|
+
* task id, a "— revision" suffix or a person's own act never reaches a chat.
|
|
5
|
+
* The console keeps every exact name and fact. */
|
|
6
|
+
import { leadOnIt } from "./task-status.js";
|
|
1
7
|
import { phoneText } from "./telegram-status.js";
|
|
2
8
|
/** The bot's display name in every chat app. */
|
|
3
9
|
export const BOT_NAME = "Toolroll";
|
|
@@ -55,7 +61,7 @@ export function finishedLine(fact) {
|
|
|
55
61
|
const ready = fact.lead === true ? `I finished ${lower}` : `${name} is ready`;
|
|
56
62
|
// What waited on the person and the lead has taken on (Needs you reads Waiting then).
|
|
57
63
|
if (fact.leadOnIt === true && (fact.headline === "Needs you" || fact.headline === "Waiting" || fact.headline === "Stopped"))
|
|
58
|
-
return `${name}:
|
|
64
|
+
return `${name}: ${leadOnIt(fact.leadName)}`;
|
|
59
65
|
switch (fact.headline) {
|
|
60
66
|
case "Ready for review":
|
|
61
67
|
if (fact.report)
|
|
@@ -67,7 +73,7 @@ export function finishedLine(fact) {
|
|
|
67
73
|
return `${ready}, but no tests ran. Look it over first.`;
|
|
68
74
|
case "Failed":
|
|
69
75
|
if (fact.leadOnIt === true)
|
|
70
|
-
return fact.checks === "failed" ? `${name} is built, but its tests failed.
|
|
76
|
+
return fact.checks === "failed" ? `${name} is built, but its tests failed. ${leadOnIt(fact.leadName)}` : `${name} stopped before it finished. ${leadOnIt(fact.leadName)}`;
|
|
71
77
|
if (fact.checks === "failed")
|
|
72
78
|
return fact.lead === true
|
|
73
79
|
? `I built ${lower}, but its tests failed. It waits for you: retry or ask for changes.`
|
package/dist/discord-chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** Discord messages and buttons transport the shared assistant's saved actions. */
|
|
2
2
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
3
|
+
import { chatAskButtons } from "./chat-ask.js";
|
|
3
4
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
4
5
|
import { channelInbox } from "./chat-inbox.js";
|
|
5
6
|
import { roomCommand } from "./chat-rooms.js";
|
|
@@ -272,6 +273,15 @@ export async function deliverDiscordPart(options) {
|
|
|
272
273
|
custom_id: `so_${one.token}`,
|
|
273
274
|
}))
|
|
274
275
|
: []),
|
|
276
|
+
// The lead's question to its owner: its options, then "Something else".
|
|
277
|
+
...(content.ask
|
|
278
|
+
? chatAskButtons(state, row.id, now).map((one) => ({
|
|
279
|
+
type: 2,
|
|
280
|
+
style: one.words ? 2 : 1,
|
|
281
|
+
label: one.label.slice(0, 80),
|
|
282
|
+
custom_id: `so_${one.token}`,
|
|
283
|
+
}))
|
|
284
|
+
: []),
|
|
275
285
|
...link(options.origin(), content.link),
|
|
276
286
|
...(content.also ?? []).flatMap(one => link(options.origin(), one)),
|
|
277
287
|
];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/** What the lead promised to follow up on. A commitment records the promise in the lead's words, the condition
|
|
2
|
+
* that completes it, when to look next, where to report and when it lapses. The follow pass reads open ones
|
|
3
|
+
* without a model and says one short line when the condition is met, on the channel the promise was made on;
|
|
4
|
+
* nothing otherwise.
|
|
5
|
+
* The table is additive (no schema version bump), so a build that predates it still opens the store. */
|
|
6
|
+
import type { Notification, Store } from './store.js';
|
|
7
|
+
import { type AssignmentSnapshot } from './assignment.js';
|
|
8
|
+
/** The table: `LEAD_COMMITMENT_SCHEMA` in store.ts (additive, no version bump). */
|
|
9
|
+
export declare const COMMITMENT_DAYS = 7;
|
|
10
|
+
export type TaskState = AssignmentSnapshot['state'];
|
|
11
|
+
export declare const TASK_STATES: readonly TaskState[];
|
|
12
|
+
export declare const RUN_OUTCOMES: readonly ['finished', 'built', 'failed'];
|
|
13
|
+
export declare const CHECK_RESULTS: readonly ['passed', 'failed', 'either'];
|
|
14
|
+
/** Where a promise was made, and so where it is reported: `chat` is the console or terminal conversation; a phone
|
|
15
|
+
* or team chat also gets the line as the lead's own message, on that chat only. */
|
|
16
|
+
export declare const COMMITMENT_CHANNELS: readonly ['chat', 'telegram', 'slack', 'discord', 'teams'];
|
|
17
|
+
export type CommitmentChannel = typeof COMMITMENT_CHANNELS[number];
|
|
18
|
+
/** `afterRun`: only a result newer than the one there was when the promise was made counts. */
|
|
19
|
+
export type CommitmentCondition = {
|
|
20
|
+
kind: 'task';
|
|
21
|
+
task: string;
|
|
22
|
+
states: TaskState[];
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'run';
|
|
25
|
+
run: number;
|
|
26
|
+
outcome: typeof RUN_OUTCOMES[number];
|
|
27
|
+
} | {
|
|
28
|
+
kind: 'check';
|
|
29
|
+
task: string;
|
|
30
|
+
result: typeof CHECK_RESULTS[number];
|
|
31
|
+
afterRun: number;
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'check';
|
|
34
|
+
run: number;
|
|
35
|
+
result: typeof CHECK_RESULTS[number];
|
|
36
|
+
} | {
|
|
37
|
+
kind: 'time';
|
|
38
|
+
at: string;
|
|
39
|
+
};
|
|
40
|
+
export type Commitment = {
|
|
41
|
+
id: number;
|
|
42
|
+
owner: string;
|
|
43
|
+
repo: string | null;
|
|
44
|
+
thread: number;
|
|
45
|
+
turn: number | null;
|
|
46
|
+
channel: string;
|
|
47
|
+
what: string;
|
|
48
|
+
condition: CommitmentCondition;
|
|
49
|
+
checkAt: string;
|
|
50
|
+
expiresAt: string;
|
|
51
|
+
state: 'open' | 'done' | 'cancelled' | 'expired';
|
|
52
|
+
createdAt: string;
|
|
53
|
+
checkedAt: string | null;
|
|
54
|
+
closedAt: string | null;
|
|
55
|
+
closedBy: string | null;
|
|
56
|
+
outcome: string | null;
|
|
57
|
+
};
|
|
58
|
+
/** The condition in a few words, for Settings and for the lead's own catch-up. */
|
|
59
|
+
export declare function conditionWords(store: Store, condition: CommitmentCondition): string;
|
|
60
|
+
export type CommitInput = {
|
|
61
|
+
owner: string;
|
|
62
|
+
repo: string | null;
|
|
63
|
+
thread: number;
|
|
64
|
+
turn: number | null;
|
|
65
|
+
channel?: string;
|
|
66
|
+
what: string;
|
|
67
|
+
condition: CommitmentCondition;
|
|
68
|
+
checkAt?: string | null;
|
|
69
|
+
};
|
|
70
|
+
/** Record a promise. The caller has already proved the task, attempt or project is within the owner's access. */
|
|
71
|
+
export declare function recordCommitment(store: Store, input: CommitInput, now: Date): Commitment;
|
|
72
|
+
export declare function getCommitment(store: Store, id: number): Commitment | null;
|
|
73
|
+
/** The owner's open promises that they heard, oldest first. */
|
|
74
|
+
export declare function openCommitments(store: Store, owner: string, limit?: number): Commitment[];
|
|
75
|
+
/** The phone or team chat a lead message is for, when it is a met promise (`lead-promise:<channel>:<id>`); null for
|
|
76
|
+
* every other notification, which goes wherever it always went. */
|
|
77
|
+
export declare function promiseChannelOf(row: Pick<Notification, 'dedupeKey'>): string | null;
|
|
78
|
+
/** Stop following up. Only the owner (from Settings or through their lead) cancels; a closed one stays as it was. */
|
|
79
|
+
export declare function cancelCommitment(store: Store, owner: string, id: number, by: string, reason: string, now: Date): boolean;
|
|
80
|
+
/** The newest result run for a task, for a check condition's baseline. */
|
|
81
|
+
export declare function latestResultRun(store: Store, task: string, repos: readonly string[], now: Date): number;
|
|
82
|
+
/** One deterministic pass over due promises (no model): expire, drop what can no longer be delivered or seen,
|
|
83
|
+
* and report a met condition once — the state change and the message share one transaction. Returns how many reported. */
|
|
84
|
+
export declare function checkLeadCommitments(store: Store, now: Date, root?: string): number;
|