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/result-acts.js
CHANGED
|
@@ -23,6 +23,25 @@
|
|
|
23
23
|
* which act is ink; the page recomputes it as the person answers each check. */
|
|
24
24
|
/** The words over the reason field an Accept of a refuted result requires. */
|
|
25
25
|
export const ACCEPT_NEEDS_REASON = "Accepting needs a reason";
|
|
26
|
+
export const FAILED_CHECK = /^the repository's approved verification command exited (-?[0-9]+)/;
|
|
27
|
+
export function evidenceProblemOf(verdict, reasons) {
|
|
28
|
+
if (verdict === "verified" || verdict === "attested")
|
|
29
|
+
return "none";
|
|
30
|
+
if (verdict === "refuted")
|
|
31
|
+
return reasons.some(reason => FAILED_CHECK.test(reason)) ? "checks-failed" : "mismatched";
|
|
32
|
+
return "missing";
|
|
33
|
+
}
|
|
34
|
+
/** Why a result can't be accepted as it stands, in one plain line, or null
|
|
35
|
+
* when nothing refutes it (a person's acceptance settles it). The result page
|
|
36
|
+
* shows it before Request changes, its one ink act. */
|
|
37
|
+
export function cantAcceptYetOf(verdict, reasons, accepted) {
|
|
38
|
+
if (accepted || verdict !== "refuted")
|
|
39
|
+
return null;
|
|
40
|
+
// A mismatch is the status card's own headline; here only what accepting takes.
|
|
41
|
+
return evidenceProblemOf(verdict, reasons) === "checks-failed"
|
|
42
|
+
? "Can't accept yet: the project's check failed on these changes."
|
|
43
|
+
: ACCEPT_NEEDS_REASON;
|
|
44
|
+
}
|
|
26
45
|
export function resultActsOf(facts) {
|
|
27
46
|
const request = facts.canRequest ? "request-changes" : null;
|
|
28
47
|
const accept = facts.accept === null ? null : "accept";
|
package/dist/retention.js
CHANGED
|
@@ -263,11 +263,17 @@ export function sweepRetention(store, evidenceRoot, now, actor = "worker") {
|
|
|
263
263
|
if (periods.chat !== null) {
|
|
264
264
|
const team = db.prepare("DELETE FROM team_message WHERE message = ? AND status NOT IN ('queued', 'running', 'uncertain')");
|
|
265
265
|
const message = db.prepare("DELETE FROM mate_message WHERE id = ? AND NOT EXISTS (SELECT 1 FROM team_message WHERE message = ?)");
|
|
266
|
+
const asked = db.prepare("SELECT turn FROM mate_message WHERE id = ? AND role = 'assistant' AND turn IS NOT NULL");
|
|
267
|
+
// The lead's question goes with the reply that asked it; its buttons then say it expired.
|
|
268
|
+
const ask = db.prepare("DELETE FROM mate_ask WHERE turn = ? AND NOT EXISTS (SELECT 1 FROM mate_message WHERE turn = ? AND role = 'assistant')");
|
|
266
269
|
for (const one of plan.items.chat) {
|
|
267
270
|
team.run(one.id);
|
|
271
|
+
const turn = asked.get(one.id)?.["turn"];
|
|
268
272
|
if (Number(message.run(one.id, one.id).changes) === 1) {
|
|
269
273
|
done.chat.count++;
|
|
270
274
|
done.chat.bytes += one.bytes;
|
|
275
|
+
if (turn !== undefined && turn !== null)
|
|
276
|
+
ask.run(Number(turn), Number(turn));
|
|
271
277
|
}
|
|
272
278
|
}
|
|
273
279
|
}
|
package/dist/serve.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { checkLeadIdentity, leadIdentityOf, leadNameOf, LEAD_NAME_MAX, LEAD_PERSONA_MAX } from "./lead-identity.js";
|
|
1
2
|
import { withActor } from "./actor.js";
|
|
2
3
|
import { repositoryContext, repositoryContextRead } from './repository-context.js';
|
|
3
4
|
import { ADD_TESTS_ACTION, followUpChecksOf, requestFollowUpChecks, fileAddTestsTask, resultCheckLevel } from './result-follow-ups.js';
|
|
@@ -6,6 +7,7 @@ import { repositoryContextHtml } from './repository-context-ui.js';
|
|
|
6
7
|
import { browserAssetsAvailable, browserWorkspaceDocument, serveBrowserAsset } from './browser-shell.js';
|
|
7
8
|
import { browserCrewOf, browserCrewFromIndex, browserWorkActionHref, browserProjectsOf, browserNavigationOf } from './browser-workspace.js';
|
|
8
9
|
import { configureLeadFollow, leadFollowStatus, runLeadFollowPass } from './lead-follow.js';
|
|
10
|
+
import { cancelCommitment, conditionWords, openCommitments } from './lead-commitments.js';
|
|
9
11
|
import { startMaintenance } from './maintenance.js';
|
|
10
12
|
import { codingHandoffPreview, createCodingHandoff } from './coding-handoff.js';
|
|
11
13
|
import { codingShippingHtml, CODING_SHIPPING_CSS } from './coding-shipping-ui.js';
|
|
@@ -65,7 +67,7 @@ import { prepareWorkspaceRevision, WorkspaceValidatorCache } from "./workspace-r
|
|
|
65
67
|
import { workIndexPage, workCountsByProject, WorkIndexCursorError, WORK_INDEX_MAX_LIMIT } from "./work-index.js";
|
|
66
68
|
import { leadActivity } from "./lead-voice.js";
|
|
67
69
|
import { openWorkDecisionOf } from "./work-summary.js";
|
|
68
|
-
import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator,
|
|
70
|
+
import { acceptAndCompleteAsOperator, assignmentOf, checkAssignmentAsOperator, owedAcceptance } from './assignment.js';
|
|
69
71
|
import { demoChatHtml, demoThreadHtml, DEMO_CHAT_SCRIPT } from "./demo-chat.js";
|
|
70
72
|
import { pullRequestSettingsHtml, PULL_REQUEST_SETTINGS_CSS } from './pull-request-ui.js';
|
|
71
73
|
import { checkSettingsHtml, CHECK_SETTINGS_CSS } from './check-levels-ui.js';
|
|
@@ -185,7 +187,7 @@ import { dirname } from "node:path";
|
|
|
185
187
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
186
188
|
import { loadOrCreateVapidKeys, validatePushEndpoint } from "./push.js";
|
|
187
189
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, listGithubRepos, isLargeRepo } from "./onboard.js";
|
|
188
|
-
import { verifiedAuthor, LEAD_THREAD, isDigestTime } from "./store.js";
|
|
190
|
+
import { verifiedAuthor, LEAD_THREAD, isDigestTime, MATE_ASK_OTHER } from "./store.js";
|
|
189
191
|
import { digestTimes } from "./digest-times.js";
|
|
190
192
|
import { updateRepos, addRepos, removeRepos } from "./repos.js";
|
|
191
193
|
import { run as execRun } from "./exec.js";
|
|
@@ -2465,6 +2467,7 @@ export function createDecisionServer(options) {
|
|
|
2465
2467
|
csrf,
|
|
2466
2468
|
canRetryReview: who.via === "cookie" && who.role === "approver",
|
|
2467
2469
|
noted: url.searchParams.get("noted") !== null,
|
|
2470
|
+
refusal: Object.hasOwn(RESULT_REFUSALS, url.searchParams.get("refused") ?? "") ? RESULT_REFUSALS[url.searchParams.get("refused")] : null,
|
|
2468
2471
|
tab: parseResultTab(url.searchParams.get("tab")),
|
|
2469
2472
|
user: who.name,
|
|
2470
2473
|
now,
|
|
@@ -2914,8 +2917,9 @@ export function createDecisionServer(options) {
|
|
|
2914
2917
|
const version = url.searchParams.get("version");
|
|
2915
2918
|
if (version !== null && !family.versions.some(one => one.id === version))
|
|
2916
2919
|
return refuse(response, who, 404, "That version is not available for this task.", taskHref(family.root.id));
|
|
2920
|
+
// A version's own address lands on its family's page, still asking for the plan editor when it did (Chat's Edit plan).
|
|
2917
2921
|
if (task.taskId !== family.root.id)
|
|
2918
|
-
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}`);
|
|
2922
|
+
return redirect(response, `${taskHref(family.root.id)}?version=${encodeURIComponent(task.taskId)}${url.searchParams.get("edit") === "plan" ? "&edit=plan" : ""}`);
|
|
2919
2923
|
return taskScreen(response, who, version ?? family.current.id, null, 200, undefined, undefined, url.searchParams.get("edit") === "plan");
|
|
2920
2924
|
}
|
|
2921
2925
|
// v103: a task's evidence pack, as a printable page or JSON (the whole family, sealed ledger entries included).
|
|
@@ -3482,7 +3486,7 @@ export function createDecisionServer(options) {
|
|
|
3482
3486
|
const fragments = known === version
|
|
3483
3487
|
? null
|
|
3484
3488
|
: {
|
|
3485
|
-
thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id) }),
|
|
3489
|
+
thread: mateThreadHtml({ ...rows, focusTask, csrf, now, problem: takeMateNote(csrf, session.id), chatProject }),
|
|
3486
3490
|
after: mateAfterComposerHtml({ messages: rows.messages, pending: rows.pending, focusTask, csrf }),
|
|
3487
3491
|
live: focusTask === null ? null : taskChatLiveRegion(focusTask, csrf, true, rows.pending !== null),
|
|
3488
3492
|
};
|
|
@@ -4132,8 +4136,9 @@ export function createDecisionServer(options) {
|
|
|
4132
4136
|
returnTo: "/settings/lead",
|
|
4133
4137
|
};
|
|
4134
4138
|
const signedIn = localSignIn?.states.claude === "connected" ? "Claude Code" : localSignIn?.states.codex === "connected" ? "Codex" : null;
|
|
4139
|
+
const promises = openCommitments(store, who.name, 50).map(one => ({ id: one.id, what: one.what, when: conditionWords(store, one.condition), until: one.expiresAt }));
|
|
4135
4140
|
return sendScreen(response, 200, screen("Lead", leadSettingsHtml({ config, facts, words: leadWords(), signedIn, command: agentSignInCommand(),
|
|
4136
|
-
said: url.searchParams.get("said") }), { chrome: chromeFor(project, "settings") }));
|
|
4141
|
+
said: url.searchParams.get("said"), saved: url.searchParams.get("saved") === "1", identity: leadIdentityOf(store, who.name), promises }), { chrome: chromeFor(project, "settings") }));
|
|
4137
4142
|
}
|
|
4138
4143
|
if (url.pathname === "/settings/telegram") {
|
|
4139
4144
|
// Any approver pairs their OWN phone here; the bot token stays on /settings.
|
|
@@ -4432,8 +4437,16 @@ export function createDecisionServer(options) {
|
|
|
4432
4437
|
// One reply runs at a time per person; this thread shows it only when
|
|
4433
4438
|
// the reply is its own.
|
|
4434
4439
|
const live = store.liveMateTurnFor(who.name);
|
|
4440
|
+
const messages = store.listMateMessages(opened.thread.id, 40);
|
|
4441
|
+
// The lead's question with buttons, while its reply is the last word in the thread.
|
|
4442
|
+
const last = messages.at(-1);
|
|
4443
|
+
const ask = last?.role === "assistant" && last.turn !== null && store.getMateTurn(last.turn)?.approver === who.name ? store.mateAskOpen(last.turn, now) : null;
|
|
4444
|
+
// Answered questions stay readable above the answer; only their buttons go.
|
|
4445
|
+
const asks = new Map(messages.flatMap(one => { const asked = one.role === "assistant" && one.turn !== null ? store.mateAsk(one.turn) : null; return asked === null ? [] : [[asked.turn, asked]]; }));
|
|
4435
4446
|
return {
|
|
4436
|
-
messages
|
|
4447
|
+
messages,
|
|
4448
|
+
ask,
|
|
4449
|
+
asks,
|
|
4437
4450
|
proposals,
|
|
4438
4451
|
decisions: decisionsFor(store, [...proposals, ...coordinatorProposals]),
|
|
4439
4452
|
coordinatorProposals,
|
|
@@ -4508,7 +4521,7 @@ export function createDecisionServer(options) {
|
|
|
4508
4521
|
const rows = mateConversationRows(who, principal, focusTask, now, chatProject);
|
|
4509
4522
|
return {
|
|
4510
4523
|
sessionId: session.id, user: session.approver, version: mateChatVersion({ ...rows, focusTask }),
|
|
4511
|
-
messages: mateBrowserMessages(rows, who.session.csrf, back),
|
|
4524
|
+
messages: mateBrowserMessages(rows, who.session.csrf, back, { task: focusTask?.id ?? null, project: chatProject }),
|
|
4512
4525
|
pendingTurnId: rows.pending?.id ?? null, requestId: randomBytes(16).toString("hex"), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
4513
4526
|
taskId: focusTask?.id ?? null, resultRunId, project: focusTask === null ? chatProject : null,
|
|
4514
4527
|
};
|
|
@@ -4976,7 +4989,8 @@ export function createDecisionServer(options) {
|
|
|
4976
4989
|
try {
|
|
4977
4990
|
const project = s.chrome.active === 'chat' ? null : s.chrome.project;
|
|
4978
4991
|
crew = extras.team?.tasks ? browserCrewFromIndex(extras.team.tasks, extras.team.selected?.id) : requestFacts.workCrew?.project === project ? browserCrewFromIndex(requestFacts.workCrew.page)
|
|
4979
|
-
|
|
4992
|
+
// The reader's own lead's claims read "<name> is on it" here too.
|
|
4993
|
+
: browserCrewOf(store, clock(), { principal: 'operator', repos: managedRepos(), includeUnplaced: false, viewer: requestFacts.actor ?? null }, { evidenceRoot, project });
|
|
4980
4994
|
}
|
|
4981
4995
|
catch {
|
|
4982
4996
|
notices.push('Crew updates are unavailable. Open Tasks to inspect saved work.');
|
|
@@ -5018,6 +5032,7 @@ export function createDecisionServer(options) {
|
|
|
5018
5032
|
const request = requestFacts.workspaceRequest;
|
|
5019
5033
|
const workspace = {
|
|
5020
5034
|
version: 1, path: currentPath, title: s.title, user: requestFacts.actor ?? '', csrf: requestFacts.csrf, sensitive,
|
|
5035
|
+
...(requestFacts.actor ? { leadName: leadNameOf(store, requestFacts.actor) } : {}),
|
|
5021
5036
|
refreshUrl: path.pathname + path.search,
|
|
5022
5037
|
receipt: conversation !== null && request && REQUEST_TOKEN.test(request)
|
|
5023
5038
|
? { request, received: store.mateRequestReceipt(conversation.sessionId, request) !== null } : null,
|
|
@@ -5123,7 +5138,7 @@ export function createDecisionServer(options) {
|
|
|
5123
5138
|
// One line: what this person's lead is doing now and when it last acted.
|
|
5124
5139
|
const activity = leadActivity(store, who.name);
|
|
5125
5140
|
const leadTask = activity?.taskId == null ? null : store.lookupRef(activity.taskId);
|
|
5126
|
-
const lead = activity === null ? null : { doing: activity.doing, at: activity.at,
|
|
5141
|
+
const lead = activity === null ? null : { name: activity.name, doing: activity.doing, at: activity.at,
|
|
5127
5142
|
href: leadTask == null || !admitted(leadTask.repo) ? null : taskHref(familyOf(activity.taskId)?.root.id ?? activity.taskId) };
|
|
5128
5143
|
return {
|
|
5129
5144
|
agents, counts, catchUp, allHref: "/work", ...(lead === null ? {} : { lead }),
|
|
@@ -5224,7 +5239,7 @@ export function createDecisionServer(options) {
|
|
|
5224
5239
|
};
|
|
5225
5240
|
}
|
|
5226
5241
|
function workAccess() {
|
|
5227
|
-
// The reader's own lead's claims read "
|
|
5242
|
+
// The reader's own lead's claims read "<name> is on it" (lead-voice.ts); nobody else's.
|
|
5228
5243
|
return { principal: 'operator', repos: admissionList(), includeUnplaced: visible(null), viewer: requestContext.getStore()?.actor ?? null };
|
|
5229
5244
|
}
|
|
5230
5245
|
// All project counts are read together. No task artifacts or processes are
|
|
@@ -9206,6 +9221,25 @@ export function createDecisionServer(options) {
|
|
|
9206
9221
|
}
|
|
9207
9222
|
// Turn the lead on with the agent signed in on this computer: its membership spends no dollars, so like starting a
|
|
9208
9223
|
// conversation it needs no second password; the full form (Advanced) still asks for one.
|
|
9224
|
+
// Settings → Lead: the name and persona this person's lead speaks with.
|
|
9225
|
+
if (url.pathname === "/settings/lead/identity") {
|
|
9226
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
9227
|
+
return refuse(response, who, 403, "An approver names their lead.", "/settings/lead");
|
|
9228
|
+
const checked = checkLeadIdentity(body.get("name") ?? "", body.get("persona") ?? "");
|
|
9229
|
+
if (!checked.ok)
|
|
9230
|
+
return redirect(response, chatReturnWithSaid("/settings/lead", checked.message));
|
|
9231
|
+
store.setLeadConfig(who.name, checked.identity.name, checked.identity.persona, now);
|
|
9232
|
+
return redirect(response, "/settings/lead?saved=1");
|
|
9233
|
+
}
|
|
9234
|
+
// Settings → Lead: stop the lead following up on one of your promises.
|
|
9235
|
+
if (url.pathname === "/settings/lead/promise/cancel") {
|
|
9236
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
9237
|
+
return refuse(response, who, 403, "An approver manages the lead's promises.", "/settings/lead");
|
|
9238
|
+
const id = Number(body.get("promise"));
|
|
9239
|
+
const done = Number.isSafeInteger(id) && cancelCommitment(store, who.name, id, who.name, "Cancelled in Settings.", now);
|
|
9240
|
+
// The promise leaving the list says it was cancelled; only a refusal needs words.
|
|
9241
|
+
return redirect(response, done ? "/settings/lead" : chatReturnWithSaid("/settings/lead", "That promise was already closed."));
|
|
9242
|
+
}
|
|
9209
9243
|
if (url.pathname === "/settings/lead/on") {
|
|
9210
9244
|
if (who.via !== "cookie" || who.role !== "approver")
|
|
9211
9245
|
return refuse(response, who, 403, "An approver turns the lead on.", "/settings/lead");
|
|
@@ -9519,7 +9553,7 @@ export function createDecisionServer(options) {
|
|
|
9519
9553
|
}
|
|
9520
9554
|
const opened = store.openMateThread(who.name, principal.ceilingDigest, now, chatScopeOf(focusTask, chatProject));
|
|
9521
9555
|
const onProgress = beginLiveTurn(opened.thread.id);
|
|
9522
|
-
void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
|
|
9556
|
+
void runMateTurn({ store, who: principal, session: mateSession, thread: opened.thread, config: enabled.config, key: enabled.key, message, onProgress, channel: "console", ...(requestId === null ? {} : { requestId }), ...(focusTask === null && chatProject !== null ? { context: `Current project: ${projectName(chatProject)} (${chatProject}). Keep this conversation about that project unless the operator explicitly asks to broaden it; use it as the repo for project tools.` } : {}), ...(focusTask === null ? {} : { context: `Current task: ${focusTask.id}. Read it with get_task before answering or proposing changes. Read its currentExecution next and bind new actions to that exact execution. Never replace the target of a prior proposal with a newer revision. Keep this turn about that task unless the operator explicitly asks to broaden it.${resultContext}${modeContext}` }), fetcher: chatFetcher, ...(options.subscriptionChatRunner === undefined ? {} : { subscriptionRunner: options.subscriptionChatRunner }), clock, evidenceRoot })
|
|
9523
9557
|
.then(outcome => {
|
|
9524
9558
|
if (!outcome.ok)
|
|
9525
9559
|
noteMate(who.session.csrf, "turn" in outcome ? outcome.turn : null, outcome.message);
|
|
@@ -11019,8 +11053,14 @@ export function createDecisionServer(options) {
|
|
|
11019
11053
|
throw error;
|
|
11020
11054
|
saved = { ok: false, status: 409, message: "Not saved: this would change what the agent may do. Change permissions in Details, under edit the scope." };
|
|
11021
11055
|
}
|
|
11022
|
-
if (!saved.ok)
|
|
11023
|
-
|
|
11056
|
+
if (!saved.ok) {
|
|
11057
|
+
// Refused because the plan moved on: the draft comes back with the version now on file, so saving it
|
|
11058
|
+
// again (having read the reason) can succeed instead of being refused the same way.
|
|
11059
|
+
const draft = new URLSearchParams(body);
|
|
11060
|
+
if (saved.status === 409)
|
|
11061
|
+
draft.set("sawDigest", store.getScope(taskId)?.digest ?? "");
|
|
11062
|
+
return taskScreen(response, who, taskId, saved.message, saved.status, draft);
|
|
11063
|
+
}
|
|
11024
11064
|
return redirect(response, taskHref(taskId));
|
|
11025
11065
|
}
|
|
11026
11066
|
case "approve": {
|
|
@@ -11135,8 +11175,13 @@ export function createDecisionServer(options) {
|
|
|
11135
11175
|
note = validated.note;
|
|
11136
11176
|
}
|
|
11137
11177
|
// A failed task's result is accepted only anyway, on purpose: with a reason.
|
|
11138
|
-
if (note === null && store.getTask(taskId)?.state === "failed")
|
|
11139
|
-
|
|
11178
|
+
if (note === null && store.getTask(taskId)?.state === "failed") {
|
|
11179
|
+
// Posted from the result page: back there, where the reason field is, with the refusal said on it.
|
|
11180
|
+
const back = body.get("return") === null ? null : safeReturn(body.get("return"));
|
|
11181
|
+
if (back !== null && back.startsWith("/review?"))
|
|
11182
|
+
return redirect(response, withRefusal(back, "reason"));
|
|
11183
|
+
return taskScreen(response, who, taskId, ACCEPT_ANYWAY_NEEDS_REASON, 400);
|
|
11184
|
+
}
|
|
11140
11185
|
store.acceptProof(latest.id, verifiedAuthor(who.name), note, now);
|
|
11141
11186
|
return redirect(response, body.get("return") === null ? taskHref(taskId) : safeReturn(body.get("return")));
|
|
11142
11187
|
}
|
|
@@ -11166,7 +11211,7 @@ export function createDecisionServer(options) {
|
|
|
11166
11211
|
const publish = body.get("publish") === "1";
|
|
11167
11212
|
const finished = acceptAndCompleteAsOperator(store, taskId, { runId: Number(namedRun), receiptDigest: digest, note }, principal, now, evidenceRoot, publish ? accepted => completeAndOpenPullRequest(store, { taskId, digest: accepted, runId: Number(namedRun), who: principal, root: evidenceRoot }, now) : undefined);
|
|
11168
11213
|
if (!finished.ok)
|
|
11169
|
-
return taskScreen(response, who, taskId, finished.message, 409);
|
|
11214
|
+
return taskScreen(response, who, taskId, finished.message, finished.reason === "needs-reason" ? 400 : 409);
|
|
11170
11215
|
return redirect(response, publish ? `${taskHref(familyOf(taskId)?.root.id ?? taskId)}#merge` : body.get("return") === null ? reviewHref(taskId, Number(namedRun)) : safeReturn(body.get("return")));
|
|
11171
11216
|
}
|
|
11172
11217
|
if (body.get("publish") === "1") {
|
|
@@ -17548,6 +17593,14 @@ function leadSettingsHtml(data) {
|
|
|
17548
17593
|
`<p><a href="/settings">Settings</a></p><h1>Lead</h1>`,
|
|
17549
17594
|
data.said === null ? "" : `<p class="problem" role="status">${escape(data.said)}</p>`,
|
|
17550
17595
|
`<section class="card lead-settings" data-lead-settings>${summary}</section>`,
|
|
17596
|
+
data.identity === undefined ? "" : `<form method="post" action="/settings/lead/identity" class="card lead-identity" data-lead-identity>` +
|
|
17597
|
+
`<input type="hidden" name="csrf" value="${escape(facts.csrf)}">` +
|
|
17598
|
+
`<label>Name your lead<input name="name" value="${escape(data.identity.name)}" maxlength="${LEAD_NAME_MAX}" autocomplete="off" required></label>` +
|
|
17599
|
+
`<label>Persona<textarea name="persona" rows="4" maxlength="${LEAD_PERSONA_MAX}">${escape(data.identity.persona)}</textarea></label>` +
|
|
17600
|
+
`<button type="submit">Save</button>${data.saved === true ? ` <span class="meta" role="status">Saved.</span>` : ""}</form>`,
|
|
17601
|
+
// What the lead promised to follow up on; it reports each once, here in chat, and drops it after 7 days.
|
|
17602
|
+
(data.promises ?? []).length === 0 ? "" : `<section class="card lead-promises" data-lead-promises><h2>Promises</h2><ul class="lead-promise-list">${data.promises.map(one => `<li data-promise="${one.id}"><p>${escape(one.what)}</p><p class="meta">${escape(one.when.charAt(0).toUpperCase() + one.when.slice(1))} · <span class="nowrap">until ${escape(new Date(one.until).toLocaleDateString("en-US", { month: "short", day: "numeric", timeZone: "UTC" }))}</span></p>` +
|
|
17603
|
+
`<form method="post" action="/settings/lead/promise/cancel" class="inline">${hidden}<input type="hidden" name="promise" value="${one.id}"><button type="submit" class="secondary">Cancel</button></form></li>`).join("")}</ul></section>`,
|
|
17551
17604
|
`<details class="lead-advanced" data-lead-advanced><summary>Advanced</summary>`,
|
|
17552
17605
|
leadConfigForm(config, facts),
|
|
17553
17606
|
config === null ? "" : `<form method="post" action="/chat/config" class="inline">${hidden}<input type="hidden" name="off" value="1">` +
|
|
@@ -18041,13 +18094,30 @@ function coordinatorProposalsSection(proposals, decisions, csrf, now, heading =
|
|
|
18041
18094
|
proposals.map(one => coordinatorProposalCard(one, csrf, now, decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, returnTo)).join("") +
|
|
18042
18095
|
`</div>`);
|
|
18043
18096
|
}
|
|
18097
|
+
/** The lead's question as buttons: each option sends itself as the next message (the same POST as a typed one);
|
|
18098
|
+
* "Something else" moves to the composer. Nothing shows while a reply is running. */
|
|
18099
|
+
function mateAskHtml(ask, csrf, target, composer, open = true) {
|
|
18100
|
+
if (!open)
|
|
18101
|
+
return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p></div>`;
|
|
18102
|
+
const hidden = `<input type="hidden" name="csrf" value="${escape(csrf)}">` +
|
|
18103
|
+
(target.task !== null ? `<input type="hidden" name="task" value="${escape(target.task)}">` : target.project !== null ? `<input type="hidden" name="project" value="${escape(target.project)}">` : "");
|
|
18104
|
+
return `<div class="so-owner-ask" data-ask="${ask.turn}"><p class="so-owner-ask-question"><strong>${escape(ask.question)}</strong></p>` +
|
|
18105
|
+
`<div class="so-owner-ask-options" role="group" aria-label="Answer options">` +
|
|
18106
|
+
ask.options.map(option => `<form method="post" action="/chat" class="inline">${hidden}<button type="submit" name="message" value="${escape(option)}" class="so-suggestion quiet">${escape(option)}</button></form>`).join("") +
|
|
18107
|
+
`<label for="${escape(composer)}" class="so-suggestion so-owner-ask-other">${escape(MATE_ASK_OTHER)}</label></div></div>`;
|
|
18108
|
+
}
|
|
18044
18109
|
/** The thread's messages as the React conversation renders them, each
|
|
18045
18110
|
* with the proposal cards its turn produced; `back` is where a card's
|
|
18046
18111
|
* confirm or dismiss returns. */
|
|
18047
|
-
function mateBrowserMessages(rows, csrf, back) {
|
|
18112
|
+
function mateBrowserMessages(rows, csrf, back, target = { task: null, project: null }) {
|
|
18113
|
+
const open = rows.pending === null ? rows.ask?.turn ?? null : null;
|
|
18114
|
+
const asked = (turn) => {
|
|
18115
|
+
const ask = turn === null ? undefined : rows.asks?.get(turn);
|
|
18116
|
+
return ask === undefined ? "" : mateAskHtml(ask, csrf, target, target.task === null ? "lead-message" : "task-message", ask.turn === open);
|
|
18117
|
+
};
|
|
18048
18118
|
return rows.messages.map(message => ({
|
|
18049
18119
|
id: message.id, role: message.role, text: message.text,
|
|
18050
|
-
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text),
|
|
18120
|
+
html: message.role === 'operator' ? `<p>${escape(message.text)}</p>` : renderChatText(message.text) + asked(message.turn),
|
|
18051
18121
|
activity: message.activity, createdAt: message.createdAt,
|
|
18052
18122
|
...(() => {
|
|
18053
18123
|
const parts = message.turn === null ? [] : rows.proposals.filter(one => one.turn === message.turn)
|
|
@@ -18070,6 +18140,7 @@ function mateChatVersion(rows) {
|
|
|
18070
18140
|
rows.coordinatorProposals.map(one => [one.id, one.state, one.outcome === null ? null : JSON.stringify(one.outcome)]),
|
|
18071
18141
|
[...rows.decisions.values()].map(one => [one.id, one.state, one.choice ?? null]),
|
|
18072
18142
|
rows.pending === null ? null : [rows.pending.id, rows.pending.steps],
|
|
18143
|
+
rows.ask?.turn ?? null,
|
|
18073
18144
|
rows.recent[0] === undefined ? null : [rows.recent[0].id, rows.recent[0].state, rows.recent[0].failureReason],
|
|
18074
18145
|
focus === null
|
|
18075
18146
|
? null
|
|
@@ -18125,6 +18196,10 @@ function mateThreadHtml(data) {
|
|
|
18125
18196
|
const cards = message.turn === null ? [] : (byTurn.get(message.turn) ?? []);
|
|
18126
18197
|
parts.push(`<div class="msg mate" data-message-role="assistant" data-key="m${message.id}"${message.id === latestReply ? ' id="latest"' : ""}>` +
|
|
18127
18198
|
renderChatText(message.text) +
|
|
18199
|
+
(() => {
|
|
18200
|
+
const ask = message.turn === null ? undefined : data.asks?.get(message.turn);
|
|
18201
|
+
return ask === undefined ? "" : mateAskHtml(ask, data.csrf, { task: data.focusTask?.id ?? null, project: data.chatProject ?? null }, "chat-message", data.pending === null && data.ask?.turn === ask.turn);
|
|
18202
|
+
})() +
|
|
18128
18203
|
cards.map(one => mateProposalCard(one, data.csrf, inert, data.decisions.get(typeof one.payload["decision"] === "number" ? one.payload["decision"] : -1) ?? null, data.focusTask === null ? null : returnTo)).join("") +
|
|
18129
18204
|
`<div class="chat-message-foot">${chatActivity(message.activity)}<time datetime="${escape(message.createdAt)}">${escape(relativeAge(message.createdAt, data.now))}</time></div>` +
|
|
18130
18205
|
`</div>`);
|
|
@@ -18185,12 +18260,12 @@ function matePage(chrome, data) {
|
|
|
18185
18260
|
}
|
|
18186
18261
|
const lastMessage = data.messages.at(-1);
|
|
18187
18262
|
const latestReply = data.pending === null && lastMessage?.role === "assistant" ? lastMessage.id : null;
|
|
18188
|
-
conversation.push(mateThreadHtml({ ...data, problem: null }));
|
|
18263
|
+
conversation.push(mateThreadHtml({ ...data, problem: null, chatProject }));
|
|
18189
18264
|
conversation.push(
|
|
18190
18265
|
// The New update action (package 2): hidden until a live update lands
|
|
18191
18266
|
// while the reader is above the latest message; a real button, so the
|
|
18192
18267
|
// keyboard reaches it. Only this act moves the reader.
|
|
18193
|
-
`<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>Message<textarea name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
|
|
18268
|
+
`<div class="chat-new-update-holder"><button type="button" class="chat-new-update" id="chat-new-update" hidden>New update ↓</button></div>`, `<form method="post" action="/chat" class="card composer" id="${latestReply === null && data.pending === null ? "latest" : "chat-composer"}" aria-label="message the mate" data-chat-session="${data.session.id}" data-chat-task="${escape(data.focusTask?.id ?? "")}" data-chat-user="${escape(data.session.approver)}" data-chat-busy="${data.pending === null ? "0" : "1"}" data-chat-version="${mateChatVersion(data)}" data-chat-approval="${escape(data.focusTask?.approval?.digest ?? "")}">`, `<input type="hidden" name="csrf" value="${escape(data.csrf)}">`, `<input type="hidden" name="request" value="${randomBytes(16).toString("hex")}"><input type="hidden" name="request-session" value="${data.session.id}">`, data.focusTask === null ? "" : `<input type="hidden" name="task" value="${escape(data.focusTask.id)}">`, chatProject === null ? "" : `<input type="hidden" name="project" value="${escape(chatProject)}">`, data.resultRunId == null ? "" : `<input type="hidden" name="result" value="${data.resultRunId}">`, `<label>Message<textarea id="chat-message" name="message" rows="1" maxlength="${MATE_MESSAGE_MAX_CHARS}" placeholder="${data.focusTask !== null ? "Ask about this task…" : chatProject !== null ? `Ask about ${escape(projectName(chatProject))}…` : "Describe what you want done…"}"></textarea></label>`, `<button type="submit" aria-label="${data.pending === null ? "send message" : "wait for the current reply before sending"}"${data.pending === null ? "" : " disabled"}>Send</button>`, `</form>`,
|
|
18194
18269
|
// Concise pass (2026-09-13): the status line speaks only when there is
|
|
18195
18270
|
// a state to report — a reply in progress here, the connection from
|
|
18196
18271
|
// the continuity script — and the composer carries no second intro.
|
|
@@ -18202,7 +18277,7 @@ function matePage(chrome, data) {
|
|
|
18202
18277
|
workspace: {
|
|
18203
18278
|
conversation: {
|
|
18204
18279
|
sessionId: data.session.id, user: data.session.approver, version: mateChatVersion(data),
|
|
18205
|
-
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo),
|
|
18280
|
+
messages: mateBrowserMessages(data, data.csrf, data.focusTask === null && chatProject === null ? null : returnTo, { task: data.focusTask?.id ?? null, project: chatProject }),
|
|
18206
18281
|
pendingTurnId: data.pending?.id ?? null, requestId: randomBytes(16).toString('hex'), maxChars: MATE_MESSAGE_MAX_CHARS,
|
|
18207
18282
|
taskId: data.focusTask?.id ?? null, resultRunId: data.resultRunId ?? null, project: chatProject,
|
|
18208
18283
|
},
|
|
@@ -19529,6 +19604,15 @@ function accentHead() {
|
|
|
19529
19604
|
}
|
|
19530
19605
|
const requestContext = new AsyncLocalStorage();
|
|
19531
19606
|
/** A same-site path or "/": never a scheme, a host, or a protocol-relative road. */
|
|
19607
|
+
/** The words a result page shows for a refusal its own form led to, by the fixed code a redirect carries. */
|
|
19608
|
+
const ACCEPT_ANYWAY_NEEDS_REASON = "Accepting anyway needs a reason.";
|
|
19609
|
+
const RESULT_REFUSALS = { reason: ACCEPT_ANYWAY_NEEDS_REASON };
|
|
19610
|
+
/** A same-site page address with a refusal code added, before any fragment. */
|
|
19611
|
+
function withRefusal(href, code) {
|
|
19612
|
+
const at = href.indexOf("#");
|
|
19613
|
+
const [path, hash] = at === -1 ? [href, ""] : [href.slice(0, at), href.slice(at)];
|
|
19614
|
+
return `${path}${path.includes("?") ? "&" : "?"}refused=${code}${hash}`;
|
|
19615
|
+
}
|
|
19532
19616
|
function safeReturn(raw) {
|
|
19533
19617
|
if (raw === null || raw === undefined)
|
|
19534
19618
|
return "/";
|
|
@@ -21779,12 +21863,13 @@ function reviewCockpitPage(chrome, data) {
|
|
|
21779
21863
|
capped: queue.length >= data.queueCap ? data.queueCap : null,
|
|
21780
21864
|
missing: data.missing === null ? null : `No completed task ${data.missing} is in view here — it may not be finished, or it is outside this console's projects.`,
|
|
21781
21865
|
beyond: data.beyondQueue && selected !== null,
|
|
21782
|
-
selected: detailParts === null ? null : detailParts.selected,
|
|
21866
|
+
selected: detailParts === null ? null : data.refusal == null ? detailParts.selected : { ...detailParts.selected, problem: data.refusal },
|
|
21783
21867
|
};
|
|
21784
21868
|
return screen("review", [
|
|
21785
21869
|
selected === null ? `<h1>Results</h1>` : "",
|
|
21786
21870
|
missingNote,
|
|
21787
21871
|
beyondNote,
|
|
21872
|
+
data.refusal == null || selected === null ? "" : `<p class="problem" data-result-refusal>${escape(data.refusal)}</p>`,
|
|
21788
21873
|
`<div class="cockpit">${queuePane}${detail}</div>`,
|
|
21789
21874
|
].join("\n"), {
|
|
21790
21875
|
chrome,
|
|
@@ -22011,14 +22096,11 @@ function reviewCockpitDetailParts(view, csrf, noted, canRetryReview, tab, user)
|
|
|
22011
22096
|
},
|
|
22012
22097
|
};
|
|
22013
22098
|
}
|
|
22014
|
-
/** What Accept and finish owes before it completes, as the form asks for it: the
|
|
22015
|
-
*
|
|
22099
|
+
/** What Accept and finish owes before it completes, as the form asks for it: the server's own rule
|
|
22100
|
+
* (owedAcceptance), so the form asks for exactly what the completion requires. */
|
|
22016
22101
|
function owedAcceptanceOf(receipt) {
|
|
22017
|
-
|
|
22018
|
-
|
|
22019
|
-
if (personCheckPending(receipt))
|
|
22020
|
-
return { note: null };
|
|
22021
|
-
return cantAcceptYetOf(receipt.proof?.verdict ?? null, receipt.proof?.reasons ?? [], false) === ACCEPT_NEEDS_REASON ? { note: "Why is this safe to accept?" } : null;
|
|
22102
|
+
const owed = owedAcceptance(receipt);
|
|
22103
|
+
return owed === null ? null : owed === "person-check" ? { note: null } : { note: "Why is this safe to accept?" };
|
|
22022
22104
|
}
|
|
22023
22105
|
function completionForm(taskId, runId, digest, csrf, pullRequestTo = null, accept = null) {
|
|
22024
22106
|
// Accept and finish, in one request: with an acceptance owed (the person's own checks, or an exception and
|
package/dist/slack-chat.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { chatFlowButtons } from "./chat-flow.js";
|
|
2
2
|
import { chatQuestionButtons } from "./teammate-question.js";
|
|
3
|
+
import { chatAskButtons } from "./chat-ask.js";
|
|
3
4
|
import { channelInbox } from "./chat-inbox.js";
|
|
4
5
|
import { roomCommand } from "./chat-rooms.js";
|
|
5
6
|
import { processChatEvent, applyChatAction, planChatNotifications, channelAccess, planRoomMessages, } from "./chat-delivery.js";
|
|
@@ -359,6 +360,15 @@ export async function deliverSlackPart(options) {
|
|
|
359
360
|
value: one.token,
|
|
360
361
|
}))
|
|
361
362
|
: []),
|
|
363
|
+
// The lead's question to its owner: its options, then "Something else".
|
|
364
|
+
...(content.ask
|
|
365
|
+
? chatAskButtons(state, row.id, now).map((one) => ({
|
|
366
|
+
type: "button",
|
|
367
|
+
text: { type: "plain_text", text: one.label },
|
|
368
|
+
action_id: one.words ? "toolroll_question_words" : "toolroll_question_choice",
|
|
369
|
+
value: one.token,
|
|
370
|
+
}))
|
|
371
|
+
: []),
|
|
362
372
|
...linkButton(options.origin(), content.link),
|
|
363
373
|
// [Look first] beside [Merge] or [Accept and finish]: each button's own action id.
|
|
364
374
|
...(content.also ?? []).flatMap((one, index) => linkButton(options.origin(), one).map(button => ({ ...button, action_id: `toolroll_link_${index + 2}` }))),
|
package/dist/store.d.ts
CHANGED
|
@@ -21,6 +21,10 @@ import { type RetentionKind, type RetentionPeriods } from "./retention.js";
|
|
|
21
21
|
import { type CheckoutCleanup } from "./storage.js";
|
|
22
22
|
import { type LedgerChainReport } from "./ledger-chain.js";
|
|
23
23
|
import { type ApprovalGate, type ApprovalRules, type ApproverKind, type Filer } from "./approval-policy.js";
|
|
24
|
+
/** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
|
|
25
|
+
* predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
|
|
26
|
+
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
27
|
+
export declare const LEAD_COMMITMENT_SCHEMA = "\nCREATE TABLE IF NOT EXISTS lead_commitment (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n owner TEXT NOT NULL,\n repo TEXT,\n thread INTEGER NOT NULL,\n turn INTEGER,\n channel TEXT NOT NULL DEFAULT 'chat',\n what TEXT NOT NULL,\n condition_json TEXT NOT NULL,\n check_at TEXT NOT NULL,\n expires_at TEXT NOT NULL,\n state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'done', 'cancelled', 'expired')),\n created_at TEXT NOT NULL,\n checked_at TEXT,\n closed_at TEXT,\n closed_by TEXT,\n outcome TEXT\n);\nCREATE INDEX IF NOT EXISTS lead_commitment_due ON lead_commitment (state, check_at);\nCREATE INDEX IF NOT EXISTS lead_commitment_owner ON lead_commitment (owner, state, id);\n";
|
|
24
28
|
/** What stops a finished run reading stopped (Store.stopQuiescenceFact). */
|
|
25
29
|
export type StopFactKind = "open" | "alive" | "elsewhere" | "unprovable" | "unknown";
|
|
26
30
|
export type TaskAct = "filed" | "approved" | "cancelled" | "completed" | "asked";
|
|
@@ -39,6 +43,20 @@ export type TaskActRow = {
|
|
|
39
43
|
why: string | null;
|
|
40
44
|
at: string;
|
|
41
45
|
};
|
|
46
|
+
/** The lead's one question to its owner (no version bump): at most one per answered turn, two to four short options;
|
|
47
|
+
* "Something else" is implied. The tapped option comes back as the owner's next message, so nothing here is an answer:
|
|
48
|
+
* a question is open while no later message from the owner sits in its thread. */
|
|
49
|
+
export declare const MATE_ASK_SCHEMA = "\nCREATE TABLE IF NOT EXISTS mate_ask (\n turn INTEGER PRIMARY KEY REFERENCES mate_turn(id) ON DELETE CASCADE,\n thread INTEGER NOT NULL REFERENCES mate_thread(id) ON DELETE CASCADE,\n question TEXT NOT NULL,\n options_json TEXT NOT NULL,\n created_at TEXT NOT NULL\n);\nCREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);\n";
|
|
50
|
+
export type MateAsk = {
|
|
51
|
+
turn: number;
|
|
52
|
+
thread: number;
|
|
53
|
+
question: string;
|
|
54
|
+
options: string[];
|
|
55
|
+
createdAt: string;
|
|
56
|
+
};
|
|
57
|
+
export declare const MATE_ASK_OTHER = "Something else";
|
|
58
|
+
/** How long the question's buttons work, on every channel. */
|
|
59
|
+
export declare const MATE_ASK_TTL_MS: number;
|
|
42
60
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
43
61
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
44
62
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -1532,6 +1550,7 @@ export type TelegramConversationPartPlan = {
|
|
|
1532
1550
|
kind: "reply";
|
|
1533
1551
|
text: string;
|
|
1534
1552
|
replyTo?: string | null;
|
|
1553
|
+
keyboard?: TelegramConversationPart["keyboard"];
|
|
1535
1554
|
} | {
|
|
1536
1555
|
kind: "card";
|
|
1537
1556
|
text: string;
|
|
@@ -6672,6 +6691,27 @@ export declare class Store {
|
|
|
6672
6691
|
payload: Record<string, unknown>;
|
|
6673
6692
|
ceilingDigest: string;
|
|
6674
6693
|
}, now: Date): number;
|
|
6694
|
+
/** Record this turn's one question; false when the turn already asked one. */
|
|
6695
|
+
recordMateAsk(ask: {
|
|
6696
|
+
turn: number;
|
|
6697
|
+
thread: number;
|
|
6698
|
+
question: string;
|
|
6699
|
+
options: readonly string[];
|
|
6700
|
+
}, now: Date): boolean;
|
|
6701
|
+
/** A failed turn keeps no question, as it keeps no drafts. */
|
|
6702
|
+
dropMateAsk(turn: number): void;
|
|
6703
|
+
/** The question an ANSWERED turn asked; null when it asked none or did not answer. */
|
|
6704
|
+
mateAsk(turn: number): MateAsk | null;
|
|
6705
|
+
/** What a tap on the question finds: open; answered (the owner has written in its thread since the reply that asked it);
|
|
6706
|
+
* or expired (older than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
|
|
6707
|
+
mateAskState(turn: number, now: Date): {
|
|
6708
|
+
state: "open";
|
|
6709
|
+
ask: MateAsk;
|
|
6710
|
+
} | {
|
|
6711
|
+
state: "answered" | "expired";
|
|
6712
|
+
};
|
|
6713
|
+
/** The question while it is open (see mateAskState); null otherwise. */
|
|
6714
|
+
mateAskOpen(turn: number, now: Date): MateAsk | null;
|
|
6675
6715
|
/**
|
|
6676
6716
|
* Record the screenshots one turn selected for an exact result, under the
|
|
6677
6717
|
* turn, so a channel that delivers files can plan them from the completed
|
|
@@ -7287,6 +7327,8 @@ export declare class Store {
|
|
|
7287
7327
|
taskId: string | null;
|
|
7288
7328
|
sourceRun: number | null;
|
|
7289
7329
|
}, now: Date): number;
|
|
7330
|
+
/** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
|
|
7331
|
+
telegramConversationWaitingOn(binding: number, messageId: string): boolean;
|
|
7290
7332
|
getTelegramConversation(id: number): TelegramConversation | null;
|
|
7291
7333
|
listTelegramConversations(botId: string): TelegramConversation[];
|
|
7292
7334
|
/**
|
|
@@ -7459,6 +7501,13 @@ export declare class Store {
|
|
|
7459
7501
|
}[];
|
|
7460
7502
|
/** Claim one local day's digest for one person: true once, false if it was already considered. */
|
|
7461
7503
|
claimEveningDigestDay(account: string, day: string): boolean;
|
|
7504
|
+
/** What this person named their lead and its persona, or null when they never saved one. */
|
|
7505
|
+
leadConfig(account: string): {
|
|
7506
|
+
name: string;
|
|
7507
|
+
persona: string;
|
|
7508
|
+
} | null;
|
|
7509
|
+
/** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
|
|
7510
|
+
setLeadConfig(account: string, name: string, persona: string, now: Date): void;
|
|
7462
7511
|
/** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
|
|
7463
7512
|
mintLeadCredential(owner: string, by: string, now: Date): {
|
|
7464
7513
|
id: string;
|
|
@@ -7667,6 +7716,8 @@ export declare class Store {
|
|
|
7667
7716
|
export declare const TELEGRAM_SKIPPED_RECEIPT = "skipped:before-pairing";
|
|
7668
7717
|
/** v83: a notification for another person, settled for this destination without sending. */
|
|
7669
7718
|
export declare const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
|
|
7719
|
+
/** A met promise the lead made on another chat: reported there, settled here without sending. */
|
|
7720
|
+
export declare const TELEGRAM_SKIPPED_OTHER_CHAT = "skipped:for-another-chat";
|
|
7670
7721
|
/** A fact this person is not messaged about: the lead's work, their own act, or a project they muted. */
|
|
7671
7722
|
export declare const TELEGRAM_SKIPPED_QUIET = "skipped:quiet";
|
|
7672
7723
|
/**
|