toolroll 0.9.15 → 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.
- package/CHANGELOG.md +36 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +56 -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-actions.d.ts +7 -0
- package/dist/chat-actions.js +40 -4
- package/dist/chat-ask.d.ts +28 -0
- package/dist/chat-ask.js +91 -0
- package/dist/chat-channel.js +8 -1
- package/dist/chat-controls.d.ts +8 -0
- package/dist/chat-controls.js +3 -0
- package/dist/chat-delivery-state.d.ts +8 -1
- package/dist/chat-delivery-state.js +15 -2
- package/dist/chat-delivery.d.ts +3 -0
- package/dist/chat-delivery.js +49 -17
- package/dist/chat-quiet.js +4 -2
- package/dist/chat-voice.d.ts +3 -6
- package/dist/chat-voice.js +8 -2
- package/dist/chat-warmth.d.ts +21 -0
- package/dist/chat-warmth.js +36 -0
- package/dist/discord-api.d.ts +1 -1
- package/dist/discord-api.js +1 -1
- package/dist/discord-chat.d.ts +3 -2
- package/dist/discord-chat.js +28 -3
- package/dist/lead-about.d.ts +35 -0
- package/dist/lead-about.js +62 -0
- package/dist/lead-capabilities.d.ts +64 -0
- package/dist/lead-capabilities.js +199 -0
- package/dist/lead-commitments.d.ts +84 -0
- package/dist/lead-commitments.js +214 -0
- package/dist/lead-context.d.ts +34 -3
- package/dist/lead-context.js +155 -19
- package/dist/lead-follow.d.ts +1 -1
- package/dist/lead-follow.js +6 -2
- package/dist/lead-identity.d.ts +23 -0
- package/dist/lead-identity.js +33 -0
- package/dist/lead-people.d.ts +55 -0
- package/dist/lead-people.js +128 -0
- package/dist/lead-voice.d.ts +13 -9
- package/dist/lead-voice.js +26 -16
- package/dist/ledger-view.js +3 -2
- package/dist/mate-cli.d.ts +4 -1
- package/dist/mate-cli.js +23 -16
- package/dist/mate-contract.d.ts +1 -1
- package/dist/mate-contract.js +13 -4
- package/dist/mate-progress.js +7 -0
- package/dist/mate-tools.d.ts +26 -1
- package/dist/mate-tools.js +357 -7
- package/dist/mate.d.ts +35 -1
- package/dist/mate.js +121 -61
- package/dist/operate.js +4 -1
- package/dist/phase-routing.d.ts +1 -1
- package/dist/project-delete.js +3 -1
- package/dist/provider-auth.d.ts +4 -0
- package/dist/provider-auth.js +1 -1
- package/dist/reply-shape.d.ts +96 -0
- package/dist/reply-shape.js +452 -0
- 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 +162 -55
- package/dist/slack-chat.d.ts +2 -0
- package/dist/slack-chat.js +30 -3
- package/dist/store.d.ts +60 -0
- package/dist/store.js +152 -3
- 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 +3 -2
- package/dist/teams-chat.js +16 -2
- package/dist/telegram-mate.d.ts +6 -1
- package/dist/telegram-mate.js +43 -11
- package/dist/telegram-progress.js +3 -2
- package/dist/telegram-team.js +6 -3
- 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
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const DEFAULT_LEAD_NAME = "Lead";
|
|
2
|
+
export const DEFAULT_LEAD_PERSONA = "Be genuinely helpful, not performative. Have opinions and say which you'd pick. Check before you claim. Be resourceful before asking. Keep it short.";
|
|
3
|
+
export const LEAD_NAME_MAX = 40;
|
|
4
|
+
export const LEAD_PERSONA_MAX = 600;
|
|
5
|
+
/** This person's lead: their saved name and persona, else the defaults. An older store reads as the defaults. */
|
|
6
|
+
export function leadIdentityOf(store, account) {
|
|
7
|
+
if (account == null || account === "")
|
|
8
|
+
return { name: DEFAULT_LEAD_NAME, persona: DEFAULT_LEAD_PERSONA };
|
|
9
|
+
try {
|
|
10
|
+
const saved = store.leadConfig(account);
|
|
11
|
+
if (saved !== null)
|
|
12
|
+
return { name: saved.name || DEFAULT_LEAD_NAME, persona: saved.persona || DEFAULT_LEAD_PERSONA };
|
|
13
|
+
}
|
|
14
|
+
catch { /* no lead_config yet: the defaults */ }
|
|
15
|
+
return { name: DEFAULT_LEAD_NAME, persona: DEFAULT_LEAD_PERSONA };
|
|
16
|
+
}
|
|
17
|
+
/** The name alone, for the places the lead speaks. */
|
|
18
|
+
export function leadNameOf(store, account) {
|
|
19
|
+
return leadIdentityOf(store, account).name;
|
|
20
|
+
}
|
|
21
|
+
/** The saved form of a name and persona, or the problem with them. An empty name or persona means the default. */
|
|
22
|
+
export function checkLeadIdentity(name, persona) {
|
|
23
|
+
const named = name.trim().replace(/\s+/g, " "), described = persona.trim().replace(/\r\n?/g, "\n");
|
|
24
|
+
if (named.length > LEAD_NAME_MAX)
|
|
25
|
+
return { ok: false, message: `Keep the name to ${LEAD_NAME_MAX} characters.` };
|
|
26
|
+
if (!/^[\p{L}\p{N}][\p{L}\p{N} .'’_-]*$/u.test(named) && named !== "")
|
|
27
|
+
return { ok: false, message: "Use letters, numbers and spaces in the name." };
|
|
28
|
+
if (described.length > LEAD_PERSONA_MAX)
|
|
29
|
+
return { ok: false, message: `Keep the persona to ${LEAD_PERSONA_MAX} characters.` };
|
|
30
|
+
if (/[\u0000-\u0009\u000b-\u001f\u007f]/.test(described))
|
|
31
|
+
return { ok: false, message: "Use plain text in the persona." };
|
|
32
|
+
return { ok: true, identity: { name: named || DEFAULT_LEAD_NAME, persona: described || DEFAULT_LEAD_PERSONA } };
|
|
33
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Store, TeammateRow } from "./store.js";
|
|
2
|
+
export type PersonEntry = {
|
|
3
|
+
id: string;
|
|
4
|
+
account: string;
|
|
5
|
+
name: string;
|
|
6
|
+
role: string;
|
|
7
|
+
projects: string[];
|
|
8
|
+
profile: string | null;
|
|
9
|
+
};
|
|
10
|
+
export type MateEntry = {
|
|
11
|
+
id: string;
|
|
12
|
+
mate: TeammateRow;
|
|
13
|
+
name: string;
|
|
14
|
+
role: string;
|
|
15
|
+
project: string;
|
|
16
|
+
};
|
|
17
|
+
export type TeamEntry = {
|
|
18
|
+
id: string;
|
|
19
|
+
conversation: string;
|
|
20
|
+
name: string;
|
|
21
|
+
members: string[];
|
|
22
|
+
purpose: string;
|
|
23
|
+
projects: string[];
|
|
24
|
+
};
|
|
25
|
+
export type PeopleIndex = {
|
|
26
|
+
people: PersonEntry[];
|
|
27
|
+
teammates: MateEntry[];
|
|
28
|
+
teams: TeamEntry[];
|
|
29
|
+
};
|
|
30
|
+
/** get_person's id shapes. */
|
|
31
|
+
export declare const PERSON_ID: RegExp;
|
|
32
|
+
/** The team chat this conversation is, or null when it is the person's own chat with their lead. */
|
|
33
|
+
export declare function teamRoomOf(store: Store, thread: number | undefined): string | null;
|
|
34
|
+
/** Everyone the owner's lead works with, in order; in a team chat (`room`), only that room and its members. */
|
|
35
|
+
export declare function peopleIndexOf(store: Store, owner: string, repos: readonly string[], room?: string | null): PeopleIndex;
|
|
36
|
+
/** One line each, as the bundle carries them. Free text goes through `redact`; names and project ids do not. */
|
|
37
|
+
export declare function peopleLines(index: PeopleIndex, redact: (text: string) => string): {
|
|
38
|
+
people: string[];
|
|
39
|
+
teammates: string[];
|
|
40
|
+
teams: string[];
|
|
41
|
+
};
|
|
42
|
+
/** One entry in full by its id (p3fa91c2e, t5, c07b1d9a4) or by name; the candidates when a name fits several, null when
|
|
43
|
+
* none. In a team chat (`room`), only that room and its members. */
|
|
44
|
+
export declare function personEntry(store: Store, owner: string, repos: readonly string[], ask: {
|
|
45
|
+
id?: string;
|
|
46
|
+
name?: string;
|
|
47
|
+
}, room?: string | null): {
|
|
48
|
+
found: Record<string, unknown>;
|
|
49
|
+
} | {
|
|
50
|
+
several: {
|
|
51
|
+
id: string;
|
|
52
|
+
name: string;
|
|
53
|
+
kind: string;
|
|
54
|
+
}[];
|
|
55
|
+
} | null;
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/** Who the lead works with: the people on this installation who share a project with its owner, the AI teammates in
|
|
2
|
+
* those projects, and the team chats the owner is in. The bundle carries one line each (lead-context.ts); get_person
|
|
3
|
+
* reads one in full, with their open work. Ids stay the same from turn to turn however the list changes: p and c plus a
|
|
4
|
+
* short digest of the account or team chat, t plus the teammate's number. In a team chat the lead speaks for the room,
|
|
5
|
+
* so only that room and its members are listed. Only the owner's own projects are listed, by their r1.. ids; first
|
|
6
|
+
* names are shown on purpose, everything else is scrubbed by the caller. */
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
import { firstNameOf } from "./lead-context.js";
|
|
9
|
+
import { labelOf, nameOf, zonesOf } from "./teammate-admin.js";
|
|
10
|
+
import { parseSoul } from "./teammates.js";
|
|
11
|
+
import { deskOf } from "./teammate-desk.js";
|
|
12
|
+
const OPEN_TASK = new Set(["queued", "running", "failed"]);
|
|
13
|
+
/** get_person's id shapes. */
|
|
14
|
+
export const PERSON_ID = /^(p[0-9a-f]{8}|t[0-9]{1,9}|c[0-9a-f]{8})$/;
|
|
15
|
+
const stableId = (prefix, key) => `${prefix}${createHash("sha256").update(key).digest("hex").slice(0, 8)}`;
|
|
16
|
+
/** The team chat this conversation is, or null when it is the person's own chat with their lead. */
|
|
17
|
+
export function teamRoomOf(store, thread) {
|
|
18
|
+
if (thread === undefined)
|
|
19
|
+
return null;
|
|
20
|
+
try {
|
|
21
|
+
const row = store.handle.prepare("SELECT id FROM team_conversation WHERE thread = ?").get(thread);
|
|
22
|
+
return row === undefined ? null : String(row["id"]);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
} // an older store has no team chats
|
|
27
|
+
}
|
|
28
|
+
const line = (text, max) => { const flat = text.replace(/\s+/g, " ").trim(); return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat; };
|
|
29
|
+
/** Everyone the owner's lead works with, in order; in a team chat (`room`), only that room and its members. */
|
|
30
|
+
export function peopleIndexOf(store, owner, repos, room = null) {
|
|
31
|
+
const label = (repo) => { const index = repos.indexOf(repo); return index === -1 ? null : `r${index + 1}`; };
|
|
32
|
+
const named = (list) => list.map(label).filter((one) => one !== null);
|
|
33
|
+
const inRoom = room === null ? null : new Set(store.handle.prepare("SELECT account FROM team_participant WHERE conversation = ? AND active = 1").all(room).map(one => String(one["account"])));
|
|
34
|
+
const people = store.accountFacts()
|
|
35
|
+
.filter(one => one.name !== owner && one.revokedAt === null && (inRoom === null || inRoom.has(one.name)))
|
|
36
|
+
.map(one => ({ one, shared: one.projects === null ? [...repos] : repos.filter(repo => one.projects.includes(repo)) }))
|
|
37
|
+
.filter(({ shared }) => shared.length > 0)
|
|
38
|
+
.map(({ one, shared }) => ({ id: stableId("p", one.name), account: one.name, name: firstNameOf(one.name) || one.name,
|
|
39
|
+
role: one.role === "approver" ? "approves work" : "watches work",
|
|
40
|
+
// No person has a written profile yet; the field is there for when one does.
|
|
41
|
+
projects: named(shared), profile: null }));
|
|
42
|
+
// AI teammates are not members of a team chat.
|
|
43
|
+
const teammates = room !== null ? [] : store.teammates(repos).map(mate => {
|
|
44
|
+
const soul = parseSoul(mate.soul);
|
|
45
|
+
return { id: `t${mate.id}`, mate, name: nameOf(mate), role: soul.ok ? soul.soul.role : labelOf(mate), project: label(mate.repo) ?? "" };
|
|
46
|
+
});
|
|
47
|
+
let teams = [];
|
|
48
|
+
try {
|
|
49
|
+
const rows = store.handle.prepare(`SELECT c.id, c.title, c.projects_json, l.name AS lead, l.instructions FROM team_conversation c JOIN team_lead l ON l.id = c.lead
|
|
50
|
+
WHERE EXISTS (SELECT 1 FROM team_participant p WHERE p.conversation = c.id AND p.account = ? AND p.active = 1)
|
|
51
|
+
OR (c.visibility = 'team' AND EXISTS (SELECT 1 FROM team_lead_member m WHERE m.lead = c.lead AND m.account = ? AND m.active = 1))
|
|
52
|
+
ORDER BY c.created_at, c.id`).all(owner, owner);
|
|
53
|
+
teams = rows.filter(row => room === null || String(row["id"]) === room).map(row => {
|
|
54
|
+
const members = store.handle.prepare("SELECT account FROM team_participant WHERE conversation = ? AND active = 1 ORDER BY account").all(row["id"])
|
|
55
|
+
.map(one => String(one["account"])).map(account => account === owner ? "you" : firstNameOf(account) || account);
|
|
56
|
+
let projects = [];
|
|
57
|
+
try {
|
|
58
|
+
const listed = JSON.parse(String(row["projects_json"]));
|
|
59
|
+
if (Array.isArray(listed))
|
|
60
|
+
projects = named(listed.map(String));
|
|
61
|
+
}
|
|
62
|
+
catch { /* none named */ }
|
|
63
|
+
const purpose = String(row["instructions"] ?? "").split("\n").map(one => one.trim()).find(one => one !== "") ?? "";
|
|
64
|
+
return { id: stableId("c", String(row["id"])), conversation: String(row["id"]), name: String(row["title"]), members, purpose: purpose || String(row["lead"]), projects };
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch { /* an older store has no team chats */ }
|
|
68
|
+
return { people, teammates, teams };
|
|
69
|
+
}
|
|
70
|
+
/** One line each, as the bundle carries them. Free text goes through `redact`; names and project ids do not. */
|
|
71
|
+
export function peopleLines(index, redact) {
|
|
72
|
+
return {
|
|
73
|
+
people: index.people.map(one => `${one.id} ${one.name}: ${one.role}; ${one.projects.join(", ")}${one.profile === null ? "" : `; ${line(redact(one.profile), 120)}`}`),
|
|
74
|
+
teammates: index.teammates.map(one => `${one.id} ${one.name}: ${line(redact(one.role), 60)}${one.project === "" ? "" : ` (${one.project})`}`),
|
|
75
|
+
teams: index.teams.map(one => `${one.id} ${line(redact(one.name), 60)}: ${one.members.join(", ")}; ${line(redact(one.purpose), 120)}`),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
/** The work still open for one person: tasks last filed, approved or handed to them in the owner's projects, and flow cards they own. */
|
|
79
|
+
function openWorkOf(store, account, repos) {
|
|
80
|
+
const repoId = (repo) => `r${repos.indexOf(repo) + 1}`;
|
|
81
|
+
const tasks = repos.length === 0 ? [] : store.handle.prepare(`SELECT r.external_id AS task, r.repo FROM task_ref r JOIN task_act a ON a.id = (SELECT MAX(id) FROM task_act WHERE task_ref = r.id AND act IN ('filed', 'approved', 'asked'))
|
|
82
|
+
WHERE r.repo IN (${repos.map(() => "?").join(", ")}) AND ((a.act IN ('filed', 'approved') AND a.account = ?) OR (a.act = 'asked' AND a.person = ?)) ORDER BY r.id DESC LIMIT 100`).all(...repos, account, account)
|
|
83
|
+
.flatMap(row => { const task = store.getTask(String(row["task"])); return task !== null && OPEN_TASK.has(task.state) ? [{ task: task.id, title: task.title, state: task.state, repo: repoId(String(row["repo"])) }] : []; })
|
|
84
|
+
.slice(0, 10);
|
|
85
|
+
const cards = repos.flatMap(repo => store.activeFlowCards(repo).filter(card => card.owner === account).map(card => ({ card: card.id, flow: card.flow, title: card.title, zone: card.stage, repo: repoId(repo) }))).slice(0, 10);
|
|
86
|
+
return { tasks, cards };
|
|
87
|
+
}
|
|
88
|
+
/** One entry in full by its id (p3fa91c2e, t5, c07b1d9a4) or by name; the candidates when a name fits several, null when
|
|
89
|
+
* none. In a team chat (`room`), only that room and its members. */
|
|
90
|
+
export function personEntry(store, owner, repos, ask, room = null) {
|
|
91
|
+
const index = peopleIndexOf(store, owner, repos, room);
|
|
92
|
+
const wanted = ask.name?.trim().toLowerCase() ?? "";
|
|
93
|
+
const all = [
|
|
94
|
+
...index.people.map(one => ({ id: one.id, name: one.name, kind: "person", also: one.account })),
|
|
95
|
+
...index.teammates.map(one => ({ id: one.id, name: one.name, kind: "AI teammate", also: one.mate.handle })),
|
|
96
|
+
...index.teams.map(one => ({ id: one.id, name: one.name, kind: "team chat", also: "" })),
|
|
97
|
+
];
|
|
98
|
+
const hits = ask.id !== undefined ? all.filter(one => one.id === ask.id)
|
|
99
|
+
: all.filter(one => one.name.toLowerCase() === wanted || one.also.toLowerCase() === wanted);
|
|
100
|
+
const loose = hits.length > 0 || wanted.length < 2 ? hits : all.filter(one => one.name.toLowerCase().includes(wanted));
|
|
101
|
+
if (loose.length === 0)
|
|
102
|
+
return null;
|
|
103
|
+
if (loose.length > 1)
|
|
104
|
+
return { several: loose.map(({ id, name, kind }) => ({ id, name, kind })) };
|
|
105
|
+
const id = loose[0].id;
|
|
106
|
+
const person = index.people.find(one => one.id === id);
|
|
107
|
+
if (person !== undefined) {
|
|
108
|
+
const work = openWorkOf(store, person.account, repos);
|
|
109
|
+
return { found: { id, kind: "person", name: person.name, role: person.role, projects: person.projects, profile: person.profile, openTasks: work.tasks, openCards: work.cards } };
|
|
110
|
+
}
|
|
111
|
+
const mate = index.teammates.find(one => one.id === id);
|
|
112
|
+
if (mate !== undefined) {
|
|
113
|
+
const flows = new Set([...zonesOf(store, mate.mate).map(one => `${one.flow}:${one.zone}`)]);
|
|
114
|
+
const desk = deskOf(store, mate.mate)?.id ?? null;
|
|
115
|
+
const cards = store.activeFlowCards(mate.mate.repo).filter(card => flows.has(`${card.flow}:${card.stage}`) || card.flow === desk)
|
|
116
|
+
.slice(0, 10).map(card => ({ card: card.id, flow: card.flow, title: card.title, zone: card.stage }));
|
|
117
|
+
return { found: { id, kind: "AI teammate", name: mate.name, role: mate.role, project: mate.project, working: mate.mate.state === "active", soul: mate.mate.soul,
|
|
118
|
+
worksOn: zonesOf(store, mate.mate).map(one => ({ flow: one.flowName, zone: one.title, how: one.kind })),
|
|
119
|
+
openTasks: cards, questions: store.openTeammateQuestions([mate.mate.id]).map(one => ({ question: one.id, asks: one.question })),
|
|
120
|
+
more: "get_teammates with this teammate's number reads its memory, routines, tools and recent log." } };
|
|
121
|
+
}
|
|
122
|
+
const team = index.teams.find(one => one.id === id);
|
|
123
|
+
const purpose = String(store.handle.prepare("SELECT l.instructions FROM team_conversation c JOIN team_lead l ON l.id = c.lead WHERE c.id = ?").get(team.conversation)?.["instructions"] ?? "");
|
|
124
|
+
const tasks = store.handle.prepare("SELECT r.external_id AS task, r.repo FROM team_task_owner o JOIN task_ref r ON r.id = o.task_ref WHERE o.conversation = ? ORDER BY r.id DESC LIMIT 100").all(team.conversation)
|
|
125
|
+
.flatMap(row => { const task = store.getTask(String(row["task"])), repo = String(row["repo"]); return task !== null && OPEN_TASK.has(task.state) && repos.includes(repo) ? [{ task: task.id, title: task.title, state: task.state, repo: `r${repos.indexOf(repo) + 1}` }] : []; })
|
|
126
|
+
.slice(0, 10);
|
|
127
|
+
return { found: { id, kind: "team chat", name: team.name, members: team.members, purpose: line(purpose || team.purpose, 600), projects: team.projects, openTasks: tasks } };
|
|
128
|
+
}
|
package/dist/lead-voice.d.ts
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/** The lead's voice: what the person's lead (an agent under their lead token) is doing, in their chat and on the
|
|
2
|
-
* console. `lead say` posts one short message from
|
|
3
|
-
* in place as it grows. `assignment claim` marks a task the lead's: it reads "
|
|
2
|
+
* console. `lead say` posts one short message from the lead, by the name its person gave it (Settings → Lead); several within two minutes are one message, edited
|
|
3
|
+
* in place as it grows. `assignment claim` marks a task the lead's: it reads "<name> is on it" and leaves Needs
|
|
4
4
|
* you until the lead completes it, hands it to a person, or two hours pass with no lead act on it.
|
|
5
5
|
*
|
|
6
6
|
* Every fact is an append-only ledger row under "lead for <owner>"; no table of its own. The chat message is an
|
|
7
7
|
* ordinary notification addressed to the owner, so every chat delivers it with the usual receipts. */
|
|
8
8
|
import { type Actor } from "./actor.js";
|
|
9
9
|
import { type Notification, type Store } from "./store.js";
|
|
10
|
-
/** Who the person's chat says the lead's messages are from. */
|
|
11
|
-
export declare const LEAD_NAME = "Your lead";
|
|
12
10
|
export declare const LEAD_SAY_KIND = "lead-say";
|
|
13
11
|
/** Repaints the owner's task card when the lead takes a task on: it never makes a new message in quiet chat. */
|
|
14
12
|
export declare const LEAD_ON_IT_KIND = "lead-on-it";
|
|
@@ -34,7 +32,8 @@ export type LeadActivity = {
|
|
|
34
32
|
owner: string;
|
|
35
33
|
doing: string;
|
|
36
34
|
at: string;
|
|
37
|
-
taskId: string | null;
|
|
35
|
+
taskId: string | null; /** What the owner calls their lead. */
|
|
36
|
+
name: string;
|
|
38
37
|
};
|
|
39
38
|
type Failure = {
|
|
40
39
|
ok: false;
|
|
@@ -51,8 +50,13 @@ export declare function leadSay(store: Store, actor: Actor, text: string, taskId
|
|
|
51
50
|
joined: boolean;
|
|
52
51
|
notification: number | null;
|
|
53
52
|
} | Failure;
|
|
54
|
-
/**
|
|
55
|
-
|
|
53
|
+
/** A lead row's subject in its owner's current name for their lead, read when it is shown, so a rename reaches
|
|
54
|
+
* earlier cards too; any other row's own subject. `reader`: who the chat belongs to, for a row with no recipient
|
|
55
|
+
* (only its owner ever receives a lead row). */
|
|
56
|
+
export declare function leadSubjectOf(store: Store, row: Pick<Notification, "kind" | "subject" | "recipient">, reader?: string | null): string;
|
|
57
|
+
/** The words a chat shows for a lead-say row: the lead's name, then each line, plain, with no task id. `reader`: whose
|
|
58
|
+
* chat it is, for a row with no recipient (see leadSubjectOf). */
|
|
59
|
+
export declare function leadSayText(store: Store, row: Pick<Notification, "kind" | "subject" | "body" | "recipient">, reader?: string | null): string;
|
|
56
60
|
/** The earlier rows of a lead-say message (newest first): a chat that showed one edits it rather than sending again. */
|
|
57
61
|
export declare function leadSayEarlier(store: Store, row: Pick<Notification, "id" | "kind" | "dedupeKey" | "recipient">): number[];
|
|
58
62
|
/** `assignment claim` under the lead token: the task is the lead's until it completes it, hands it on, or goes
|
|
@@ -72,10 +76,10 @@ export declare function enqueueLeadLapses(store: Store, now: Date): number;
|
|
|
72
76
|
/** The lead's act on a task that a claim counts (a retry, a state change, a steer): one ledger row as the lead. */
|
|
73
77
|
export declare function noteLeadWork(store: Store, actor: Actor, taskId: string, what: string, now: Date): void;
|
|
74
78
|
/** What `owner`'s own lead is doing now and when it last acted, both from its one newest act: its words when that
|
|
75
|
-
* was a say, else the act in words. Nobody (null) has no lead line: another person's lead is never
|
|
79
|
+
* was a say, else the act in words. Nobody (null) has no lead line: another person's lead is never theirs. */
|
|
76
80
|
export declare function leadActivity(store: Store, owner: string | null | undefined): LeadActivity | null;
|
|
77
81
|
/** "just now", "3 min ago", "2 h ago", "yesterday", "4 days ago". */
|
|
78
82
|
export declare function agoWords(at: string, now: Date): string;
|
|
79
|
-
/** The one line `toolroll status` and the console say: "
|
|
83
|
+
/** The one line `toolroll status` and the console say: "Lead: fixing 0.9.12's release check · 3 min ago". */
|
|
80
84
|
export declare function leadActivityLine(activity: LeadActivity, now: Date): string;
|
|
81
85
|
export {};
|
package/dist/lead-voice.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The lead's voice: what the person's lead (an agent under their lead token) is doing, in their chat and on the
|
|
2
|
-
* console. `lead say` posts one short message from
|
|
3
|
-
* in place as it grows. `assignment claim` marks a task the lead's: it reads "
|
|
2
|
+
* console. `lead say` posts one short message from the lead, by the name its person gave it (Settings → Lead); several within two minutes are one message, edited
|
|
3
|
+
* in place as it grows. `assignment claim` marks a task the lead's: it reads "<name> is on it" and leaves Needs
|
|
4
4
|
* you until the lead completes it, hands it to a person, or two hours pass with no lead act on it.
|
|
5
5
|
*
|
|
6
6
|
* Every fact is an append-only ledger row under "lead for <owner>"; no table of its own. The chat message is an
|
|
@@ -9,10 +9,9 @@ import { actorLabel, withActor } from "./actor.js";
|
|
|
9
9
|
import { chatControlHref } from "./chat-controls.js";
|
|
10
10
|
import { BATCH_MS, chatText, chatTitle } from "./chat-voice.js";
|
|
11
11
|
import { BUILT_IN } from "./store.js";
|
|
12
|
-
import {
|
|
12
|
+
import { leadNameOf } from "./lead-identity.js";
|
|
13
|
+
import { leadLapsed } from "./task-status.js";
|
|
13
14
|
import { phoneText as plain } from "./telegram-status.js";
|
|
14
|
-
/** Who the person's chat says the lead's messages are from. */
|
|
15
|
-
export const LEAD_NAME = "Your lead";
|
|
16
15
|
export const LEAD_SAY_KIND = "lead-say";
|
|
17
16
|
/** Repaints the owner's task card when the lead takes a task on: it never makes a new message in quiet chat. */
|
|
18
17
|
export const LEAD_ON_IT_KIND = "lead-on-it";
|
|
@@ -97,15 +96,26 @@ export function leadSay(store, actor, text, taskId, now) {
|
|
|
97
96
|
if (joined)
|
|
98
97
|
store.handle.prepare("UPDATE notification SET resolved_at = ? WHERE id = ? AND resolved_at IS NULL").run(stamp, Number(open["id"]));
|
|
99
98
|
const key = `lead-say:${joined ? opened : stamp}:${ledger}`;
|
|
100
|
-
store.enqueueNotification({ dedupeKey: key, kind: LEAD_SAY_KIND, subject:
|
|
99
|
+
store.enqueueNotification({ dedupeKey: key, kind: LEAD_SAY_KIND, subject: leadNameOf(store, actor.account), body: lines.join("\n"),
|
|
101
100
|
recipient: actor.account, source: { installation: true }, ...(taskId === null ? {} : { link: chatControlHref("task", taskId) }) }, now);
|
|
102
101
|
const row = store.handle.prepare("SELECT id FROM notification WHERE dedupe_key = ?").get(key);
|
|
103
102
|
return { ok: true, joined, notification: row === undefined ? null : Number(row["id"]) };
|
|
104
103
|
});
|
|
105
104
|
}
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
105
|
+
/** A lead row's subject in its owner's current name for their lead, read when it is shown, so a rename reaches
|
|
106
|
+
* earlier cards too; any other row's own subject. `reader`: who the chat belongs to, for a row with no recipient
|
|
107
|
+
* (only its owner ever receives a lead row). */
|
|
108
|
+
export function leadSubjectOf(store, row, reader) {
|
|
109
|
+
const owner = row.recipient ?? reader ?? null;
|
|
110
|
+
if (owner === null || (row.kind !== LEAD_SAY_KIND && row.kind !== LEAD_ON_IT_KIND && row.kind !== LEAD_LAPSED_KIND))
|
|
111
|
+
return row.subject;
|
|
112
|
+
const name = leadNameOf(store, owner);
|
|
113
|
+
return row.kind === LEAD_SAY_KIND ? name : row.kind === LEAD_ON_IT_KIND ? `${name} is on it` : leadLapsed(name);
|
|
114
|
+
}
|
|
115
|
+
/** The words a chat shows for a lead-say row: the lead's name, then each line, plain, with no task id. `reader`: whose
|
|
116
|
+
* chat it is, for a row with no recipient (see leadSubjectOf). */
|
|
117
|
+
export function leadSayText(store, row, reader) {
|
|
118
|
+
return chatText([plain(leadSubjectOf(store, row, reader), 60), "", ...row.body.split("\n").map(line => plain(line, LEAD_SAY_MAX)).filter(line => line !== "")].join("\n"));
|
|
109
119
|
}
|
|
110
120
|
/** The earlier rows of a lead-say message (newest first): a chat that showed one edits it rather than sending again. */
|
|
111
121
|
export function leadSayEarlier(store, row) {
|
|
@@ -129,9 +139,9 @@ export function leadClaim(store, actor, taskId, now) {
|
|
|
129
139
|
store.transact(() => {
|
|
130
140
|
const ledger = store.recordAction({ at: now.toISOString(), actor: actorLabel(actor), repo: task.repo, taskId: root, runId: null,
|
|
131
141
|
action: LEAD_CLAIMED, outcome: "recorded", source: "work" });
|
|
132
|
-
// The owner's card for this task repaints to "
|
|
142
|
+
// The owner's card for this task repaints to "<name> is on it": quiet chat edits its card, and every-update
|
|
133
143
|
// chat edits the attempt's own card (the Failed alert), or follows it up when that message is gone.
|
|
134
|
-
store.enqueueNotification({ dedupeKey: `lead-on-it:${ledger}`, kind: LEAD_ON_IT_KIND, subject:
|
|
144
|
+
store.enqueueNotification({ dedupeKey: `lead-on-it:${ledger}`, kind: LEAD_ON_IT_KIND, subject: `${leadNameOf(store, actor.account)} is on it`, body: "",
|
|
135
145
|
link: chatControlHref("task", taskId), source: cardSource(store, root) ?? { taskRef: task.refId } }, now);
|
|
136
146
|
});
|
|
137
147
|
return { ok: true, root, claim: leadClaimOf(store, taskId, now, actor.account) };
|
|
@@ -210,7 +220,7 @@ export function enqueueLeadLapses(store, now) {
|
|
|
210
220
|
continue;
|
|
211
221
|
// The owner's lead let it go: only its owner's card hears of it (store.pingAllowed).
|
|
212
222
|
if (withActor({ account: owner, lead: true }, () => store.enqueueNotification({ dedupeKey: `lead-lapsed:${root}:${owner}:${claim.lastActedAt}`,
|
|
213
|
-
kind: LEAD_LAPSED_KIND, subject:
|
|
223
|
+
kind: LEAD_LAPSED_KIND, subject: leadLapsed(leadNameOf(store, owner)), body: "", link: chatControlHref("task", root), source }, now)))
|
|
214
224
|
count++;
|
|
215
225
|
}
|
|
216
226
|
}
|
|
@@ -233,7 +243,7 @@ const ACT_WORDS = {
|
|
|
233
243
|
"task handed to a person": "handed on", [LEAD_CLAIMED]: "working on",
|
|
234
244
|
};
|
|
235
245
|
/** What `owner`'s own lead is doing now and when it last acted, both from its one newest act: its words when that
|
|
236
|
-
* was a say, else the act in words. Nobody (null) has no lead line: another person's lead is never
|
|
246
|
+
* was a say, else the act in words. Nobody (null) has no lead line: another person's lead is never theirs. */
|
|
237
247
|
export function leadActivity(store, owner) {
|
|
238
248
|
if (owner == null || owner === "")
|
|
239
249
|
return null;
|
|
@@ -250,7 +260,7 @@ export function leadActivity(store, owner) {
|
|
|
250
260
|
} })();
|
|
251
261
|
const doing = last["action"] === LEAD_SAID && last["detail"] != null ? chatText(String(last["detail"]), task === null ? [] : [{ id: task, title: title.trim() }])
|
|
252
262
|
: `${ACT_WORDS[String(last["action"])] ?? String(last["action"]).replace(/^lead /, "")}${title}`;
|
|
253
|
-
return { owner, doing: doing.replace(/[.\s]+$/u, ""), at: String(last["at"]), taskId: task };
|
|
263
|
+
return { owner, doing: doing.replace(/[.\s]+$/u, ""), at: String(last["at"]), taskId: task, name: leadNameOf(store, owner) };
|
|
254
264
|
}
|
|
255
265
|
catch {
|
|
256
266
|
return null;
|
|
@@ -269,7 +279,7 @@ export function agoWords(at, now) {
|
|
|
269
279
|
const days = Math.floor(hours / 24);
|
|
270
280
|
return days === 1 ? "yesterday" : `${days} days ago`;
|
|
271
281
|
}
|
|
272
|
-
/** The one line `toolroll status` and the console say: "
|
|
282
|
+
/** The one line `toolroll status` and the console say: "Lead: fixing 0.9.12's release check · 3 min ago". */
|
|
273
283
|
export function leadActivityLine(activity, now) {
|
|
274
|
-
return `${
|
|
284
|
+
return `${activity.name}: ${activity.doing} · ${agoWords(activity.at, now)}`;
|
|
275
285
|
}
|
package/dist/ledger-view.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { projectName } from "./project.js";
|
|
2
|
+
import { exactWhenHtml } from "./when-html.js";
|
|
2
3
|
const html = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3
4
|
/** The ledger's kinds of event, as the filter names them. */
|
|
4
5
|
const EVENT_KINDS = [["work", "Work"], ["request", "Requests"], ["access", "Access"], ["sign-in", "Sign-ins"], ["policy", "Policy changes"]];
|
|
@@ -47,11 +48,11 @@ export function ledgerBody(rows, projects, params, chain) {
|
|
|
47
48
|
<label>Outcome<input type="text" name="outcome" value="${html(params.get("outcome") ?? "")}" placeholder="Any outcome"></label>
|
|
48
49
|
<label>Task<input type="text" name="task" value="${html(params.get("task") ?? "")}" placeholder="Any task"></label>
|
|
49
50
|
<button type="submit">Filter</button><a href="/ledger">Clear</a></form></details>
|
|
50
|
-
<div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time
|
|
51
|
+
<div class="ledger-list" aria-label="Action history"><div class="ledger-event ledger-labels" aria-hidden="true"><span>Time</span><span>Actor</span><span>Action</span><span>Project / work</span><span>Outcome / event</span></div>${shown.length === 0 ? `<p class="ledger-empty">No actions match these filters yet.</p>` : shown.map(row => {
|
|
51
52
|
const subject = row.runId !== null ? `<a href="/r/${row.runId}">Run #${row.runId}</a>` : row.taskId !== null ? `<a href="/t/${encodeURIComponent(row.taskId)}">${html(row.taskId)}</a>` : "";
|
|
52
53
|
const byActor = new URLSearchParams(fresh);
|
|
53
54
|
byActor.set("actor", row.actor);
|
|
54
|
-
return `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time"
|
|
55
|
+
return `<article class="ledger-event" data-ledger-id="${row.id}"><div class="ledger-time">${exactWhenHtml(row.at)}</div><div class="ledger-actor"><a class="actor" href="/ledger?${html(byActor.toString())}" title="Filter by this actor">${html(row.actor)}</a></div><div class="ledger-action">${html(row.action)}${row.detail === null ? "" : `<small class="ledger-detail">${html(row.detail)}</small>`}</div><div class="ledger-place">${row.repo === null ? "Instance" : html(projectName(row.repo))} ${subject}</div><div class="ledger-result">${html(row.outcome)}<small>${html(row.source)}</small></div></article>`;
|
|
55
56
|
}).join("")}</div>
|
|
56
57
|
<p class="row"><a href="/ledger?${html(fresh.toString())}">Newest actions</a>${more ? `<a rel="next" href="/ledger?${html(next.toString())}">Older actions</a>` : ""}</p>
|
|
57
58
|
<p class="meta">Accepted requests record acceptance; worker events record results. History starts from this upgrade. Review evidence is on each task or run.</p>`;
|
package/dist/mate-cli.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type MateProposal, type Store } from "./store.js";
|
|
2
|
+
import { type ShapeOptions } from "./reply-shape.js";
|
|
2
3
|
import type { SubscriptionMateRunner } from "./subscription-chat.js";
|
|
3
4
|
export type MateCliSeams = {
|
|
4
5
|
fetcher?: typeof fetch;
|
|
@@ -27,6 +28,8 @@ export type MateCliInput = {
|
|
|
27
28
|
seams?: MateCliSeams;
|
|
28
29
|
/** Where evidence lives — a scout's report reads from here. */
|
|
29
30
|
evidenceRoot?: string;
|
|
31
|
+
/** The console's address(es): a reply's links there are named as its pages ("the task"), not by host. */
|
|
32
|
+
appOrigin?: ShapeOptions["appOrigin"];
|
|
30
33
|
};
|
|
31
34
|
export type MateCliResult = {
|
|
32
35
|
code: number;
|
package/dist/mate-cli.js
CHANGED
|
@@ -10,9 +10,11 @@ import { configureLeadFollow, leadFollowStatus } from './lead-follow.js';
|
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
import { readFileSync } from "node:fs";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
+
import { MATE_ASK_OTHER } from "./store.js";
|
|
13
14
|
import { CHAT_KEY_ENV, credentialKeyOf, isDirectChatProvider, priceForConfig, subscriptionCredentialKey } from "./converse.js";
|
|
14
15
|
import { verifyApproverByPassword } from "./principal.js";
|
|
15
16
|
import { runMateTurn } from "./mate.js";
|
|
17
|
+
import { voiceReply } from "./reply-shape.js";
|
|
16
18
|
import { confirmMateProposal, dismissMateProposal } from "./mate-doors.js";
|
|
17
19
|
import { projectName } from "./project.js";
|
|
18
20
|
import { CHAT_CONTROLS, chatControlHref, isChatControl } from "./chat-controls.js";
|
|
@@ -152,7 +154,7 @@ export async function runMateCli(input) {
|
|
|
152
154
|
return refuse("no-key", `no ${directProvider} key — export ${CHAT_KEY_ENV[directProvider]}, or paste one on the console's chat page`);
|
|
153
155
|
const repos = [...input.repos];
|
|
154
156
|
if (repos.length === 0)
|
|
155
|
-
return refuse("empty-ceiling", "
|
|
157
|
+
return refuse("empty-ceiling", "The lead has no projects to look at. Name some with --repo, or add projects in the console.", MATE_CLI_EXIT.usage);
|
|
156
158
|
const verified = verifyApproverByPassword(store, input.credentials.name, input.credentials.token, repos);
|
|
157
159
|
if (!verified.ok)
|
|
158
160
|
return refuse("unauthenticated", "that is not an approver, or the password does not match");
|
|
@@ -163,7 +165,7 @@ export async function runMateCli(input) {
|
|
|
163
165
|
const sessions = store.endMateSessionsFor(who.name, who.name, now);
|
|
164
166
|
const threads = store.closeMateThreadsFor(who.name, now);
|
|
165
167
|
emit({ ok: true, ended: { sessions, threads, turns } });
|
|
166
|
-
say(sessions === 0 ? "
|
|
168
|
+
say(sessions === 0 ? "No conversation was open, so there was nothing to end." : "The conversation has ended and its history is forgotten.");
|
|
167
169
|
return { code: MATE_CLI_EXIT.ok };
|
|
168
170
|
}
|
|
169
171
|
store.sweepStaleMateTurns(now);
|
|
@@ -172,12 +174,12 @@ export async function runMateCli(input) {
|
|
|
172
174
|
if (session !== null && session.credentialKey !== credentialKey) {
|
|
173
175
|
// A session minted under another provider key cannot be spent by this
|
|
174
176
|
// one (slice-2 review, finding 9): say so, never loop on `not-yours`.
|
|
175
|
-
return refuse("key-mismatch", "
|
|
177
|
+
return refuse("key-mismatch", "The open conversation was started with a different provider key. Use that key, or end it with --end and start a new one.");
|
|
176
178
|
}
|
|
177
179
|
if (session !== null && session.ceilingDigest !== who.ceilingDigest) {
|
|
178
180
|
store.endMateSession(session.id, who.name, now);
|
|
179
181
|
store.closeMateThreadsFor(who.name, now);
|
|
180
|
-
say("
|
|
182
|
+
say("The projects you named differ from the open conversation's, so it ended. Starting a new one.");
|
|
181
183
|
session = null;
|
|
182
184
|
}
|
|
183
185
|
if (session === null) {
|
|
@@ -189,15 +191,15 @@ export async function runMateCli(input) {
|
|
|
189
191
|
const id = store.mintMateSession({ approver: who.name, approverGeneration: who.generation, credentialKey, ceilingMicrousd, ceilingDigest: who.ceilingDigest, termsDigest }, now);
|
|
190
192
|
session = store.getMateSession(id);
|
|
191
193
|
if (session === null)
|
|
192
|
-
return refuse("failed", "
|
|
194
|
+
return refuse("failed", "A new conversation couldn't be started. Try again.", MATE_CLI_EXIT.failed);
|
|
193
195
|
if (!direct && input.ceilingUsd !== undefined)
|
|
194
196
|
say("the supplied --ceiling-usd value is ignored for membership usage");
|
|
195
|
-
say(`mate conversation started: ${direct ? `up to ${money(ceilingMicrousd)}` : "subscription usage (no dollar
|
|
197
|
+
say(`mate conversation started: ${direct ? `up to ${money(ceilingMicrousd)}` : "subscription usage (no dollar limit)"} over ${repos.map(one => projectName(one)).join(", ")} — live until you end it`);
|
|
196
198
|
if (direct)
|
|
197
|
-
say(`(the weekly chat
|
|
199
|
+
say(`(the weekly chat limit, ${money(config.weeklyCeilingMicrousd)}, still applies)`);
|
|
198
200
|
}
|
|
199
201
|
else {
|
|
200
|
-
say(`mate conversation live: ${direct ? `${money(session.spentMicrousd)} of ${money(session.ceilingMicrousd)} spent` : "subscription usage (no dollar
|
|
202
|
+
say(`mate conversation live: ${direct ? `${money(session.spentMicrousd)} of ${money(session.ceilingMicrousd)} spent` : "subscription usage (no dollar limit)"} — live until you end it`);
|
|
201
203
|
}
|
|
202
204
|
const thread = store.openMateThread(who.name, who.ceilingDigest, now).thread;
|
|
203
205
|
if (input.follow !== undefined) {
|
|
@@ -239,21 +241,26 @@ export async function runMateCli(input) {
|
|
|
239
241
|
const turn = async (message) => {
|
|
240
242
|
const live = store.activeMateSession(who.name);
|
|
241
243
|
if (live === null) {
|
|
242
|
-
const outcome = { ok: false, refused: "session-ended", message: "
|
|
244
|
+
const outcome = { ok: false, refused: "session-ended", message: "This conversation has ended. Run chat again to start a new one." };
|
|
243
245
|
return outcome;
|
|
244
246
|
}
|
|
245
|
-
return runMateTurn({ store, who, session: live, thread, config, key, message, ...(seams.fetcher === undefined ? {} : { fetcher: seams.fetcher }), ...(seams.subscriptionRunner === undefined ? {} : { subscriptionRunner: seams.subscriptionRunner }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), clock });
|
|
247
|
+
return runMateTurn({ store, who, session: live, thread, config, key, message, channel: "terminal", ...(seams.fetcher === undefined ? {} : { fetcher: seams.fetcher }), ...(seams.subscriptionRunner === undefined ? {} : { subscriptionRunner: seams.subscriptionRunner }), ...(input.evidenceRoot === undefined ? {} : { evidenceRoot: input.evidenceRoot }), clock });
|
|
246
248
|
};
|
|
247
|
-
const report = (outcome) => {
|
|
249
|
+
const report = (outcome, message) => {
|
|
248
250
|
if (outcome.ok && outcome.replayed) {
|
|
249
251
|
emit(outcome);
|
|
250
252
|
say(`Message already received as turn #${outcome.turn}.`);
|
|
251
253
|
return;
|
|
252
254
|
}
|
|
253
255
|
if (outcome.ok) {
|
|
254
|
-
|
|
256
|
+
const ask = store.mateAsk(outcome.turn);
|
|
257
|
+
emit({ ok: true, turn: outcome.turn, reply: outcome.reply, activity: outcome.activity, steps: outcome.steps, settledMicrousd: outcome.settledMicrousd, proposals: pendingProposals().map(one => ({ id: one.id, ordinal: ordinalOf(one), kind: one.kind, payload: one.payload })),
|
|
258
|
+
...(ask === null ? {} : { ask: { question: ask.question, options: [...ask.options, MATE_ASK_OTHER] } }) });
|
|
255
259
|
say(` ${outcome.activity}`);
|
|
256
|
-
say(outcome.reply);
|
|
260
|
+
say(voiceReply(outcome.reply, "terminal", { appOrigin: input.appOrigin ?? null, ...(message === undefined ? {} : { asked: message }) }));
|
|
261
|
+
// The lead's question: the terminal has no buttons, so the options are listed to type back.
|
|
262
|
+
if (ask !== null)
|
|
263
|
+
say(`${ask.question}\n${ask.options.map(one => ` · ${one}`).join("\n")}\n · ${MATE_ASK_OTHER} (type your answer)`);
|
|
257
264
|
printProposals();
|
|
258
265
|
return;
|
|
259
266
|
}
|
|
@@ -262,7 +269,7 @@ export async function runMateCli(input) {
|
|
|
262
269
|
};
|
|
263
270
|
if (input.say !== undefined) {
|
|
264
271
|
const outcome = await turn(input.say);
|
|
265
|
-
report(outcome);
|
|
272
|
+
report(outcome, input.say);
|
|
266
273
|
return { code: outcome.ok ? MATE_CLI_EXIT.ok : MATE_CLI_EXIT.refused };
|
|
267
274
|
}
|
|
268
275
|
// The REPL. Text is a turn; a few words are acts on the cards.
|
|
@@ -331,7 +338,7 @@ export async function runMateCli(input) {
|
|
|
331
338
|
store.failLiveMateTurnsFor(who.name, "ended", now);
|
|
332
339
|
store.endMateSessionsFor(who.name, who.name, now);
|
|
333
340
|
store.closeMateThreadsFor(who.name, now);
|
|
334
|
-
say("
|
|
341
|
+
say("The conversation has ended and its history is forgotten.");
|
|
335
342
|
emit({ ok: true, ended: true });
|
|
336
343
|
return { code: MATE_CLI_EXIT.ok };
|
|
337
344
|
}
|
|
@@ -339,7 +346,7 @@ export async function runMateCli(input) {
|
|
|
339
346
|
say("say something, or: proposals · confirm N · dismiss N · open N · end · quit");
|
|
340
347
|
continue;
|
|
341
348
|
}
|
|
342
|
-
report(await turn(line));
|
|
349
|
+
report(await turn(line), line);
|
|
343
350
|
}
|
|
344
351
|
return { code: MATE_CLI_EXIT.ok };
|
|
345
352
|
}
|
package/dist/mate-contract.d.ts
CHANGED
|
@@ -6,5 +6,5 @@
|
|
|
6
6
|
* recommendations never reach the model — the tools already hide them —
|
|
7
7
|
* so the contract need not forbid repeating what it cannot see.
|
|
8
8
|
*/
|
|
9
|
-
export declare const MATE_CONTRACT_VERSION =
|
|
9
|
+
export declare const MATE_CONTRACT_VERSION = 47;
|
|
10
10
|
export declare const MATE_CONTRACT: string;
|