toolroll 0.9.14 → 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 +34 -0
- package/dist/assignment-brief.d.ts +10 -0
- package/dist/assignment-brief.js +49 -26
- package/dist/assignment-ui.d.ts +1 -0
- package/dist/assignment-ui.js +6 -4
- package/dist/assignment.d.ts +34 -1
- package/dist/assignment.js +86 -7
- package/dist/browser/workspace.css +2 -2
- package/dist/browser/workspace.js +38 -38
- package/dist/browser-crew.d.ts +2 -1
- package/dist/browser-crew.js +3 -1
- package/dist/browser-workspace.d.ts +93 -18
- package/dist/chat-actions.d.ts +1 -1
- package/dist/chat-actions.js +6 -4
- 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 +7 -2
- 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 +6 -9
- package/dist/chat-voice.js +11 -5
- package/dist/demo.js +82 -2
- package/dist/disclosure.d.ts +6 -0
- package/dist/disclosure.js +13 -0
- 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-status.js +2 -2
- 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/needs-you.d.ts +48 -0
- package/dist/needs-you.js +93 -1
- package/dist/operate.js +2 -1
- package/dist/project-delete.js +3 -1
- package/dist/result-acts.d.ts +63 -5
- package/dist/result-acts.js +53 -3
- package/dist/result-review.d.ts +4 -3
- package/dist/result-review.js +7 -3
- package/dist/retention.js +6 -0
- package/dist/serve.d.ts +3 -0
- package/dist/serve.js +1220 -694
- package/dist/slack-chat.js +11 -1
- 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 +26 -19
- package/dist/task-status.js +59 -21
- 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/transitions-recipes.d.ts +1 -1
- package/dist/transitions-recipes.js +5 -9
- package/dist/when-html.d.ts +24 -2
- package/dist/when-html.js +98 -11
- package/dist/work-index.d.ts +5 -1
- package/dist/work-index.js +22 -9
- package/dist/work-summary.d.ts +1 -1
- package/dist/workspace-motion.js +3 -3
- package/dist/workspace-ui.d.ts +44 -20
- package/dist/workspace-ui.js +63 -37
- package/package.json +1 -1
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,8 +360,17 @@ 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
|
-
// [Look first] beside [Merge] or [
|
|
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}` }))),
|
|
365
375
|
];
|
|
366
376
|
const target = content.edit ?? row.message;
|
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
|
/**
|
package/dist/store.js
CHANGED
|
@@ -183,6 +183,16 @@ CREATE TABLE IF NOT EXISTS chat_batch_item (
|
|
|
183
183
|
PRIMARY KEY (batch, task_ref)
|
|
184
184
|
);
|
|
185
185
|
`;
|
|
186
|
+
/** Settings → Lead (no version bump: additive only): what one person calls their lead and the short persona it
|
|
187
|
+
* speaks with. No row reads as the defaults (lead-identity.ts). */
|
|
188
|
+
const LEAD_CONFIG_SCHEMA = `
|
|
189
|
+
CREATE TABLE IF NOT EXISTS lead_config (
|
|
190
|
+
account TEXT PRIMARY KEY,
|
|
191
|
+
name TEXT NOT NULL,
|
|
192
|
+
persona TEXT NOT NULL,
|
|
193
|
+
updated_at TEXT NOT NULL
|
|
194
|
+
);
|
|
195
|
+
`;
|
|
186
196
|
/** Pings follow responsibility (no version bump: additive only). `lead_credential` is a lead token: an agent acting
|
|
187
197
|
* for `owner`, only its hash kept. `task_act` is who filed, approved, cancelled, completed or handed on a task
|
|
188
198
|
* (`lead` 1: the owner's lead; `person`/`why`: who the lead asked, and why). `notification_actor` is whose act made
|
|
@@ -227,6 +237,31 @@ CREATE TABLE IF NOT EXISTS project_mute (
|
|
|
227
237
|
PRIMARY KEY (account, repo)
|
|
228
238
|
);
|
|
229
239
|
`;
|
|
240
|
+
/** What the lead promised to follow up on (lead-commitments.ts; no version bump: additive only, so a build that
|
|
241
|
+
* predates it still opens the store). `condition_json` is a task, attempt, check or time; `check_at` is when to look
|
|
242
|
+
* next; `channel` is where it reports (`chat`: the conversation it was promised in); a promise lapses at `expires_at`. */
|
|
243
|
+
export const LEAD_COMMITMENT_SCHEMA = `
|
|
244
|
+
CREATE TABLE IF NOT EXISTS lead_commitment (
|
|
245
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
246
|
+
owner TEXT NOT NULL,
|
|
247
|
+
repo TEXT,
|
|
248
|
+
thread INTEGER NOT NULL,
|
|
249
|
+
turn INTEGER,
|
|
250
|
+
channel TEXT NOT NULL DEFAULT 'chat',
|
|
251
|
+
what TEXT NOT NULL,
|
|
252
|
+
condition_json TEXT NOT NULL,
|
|
253
|
+
check_at TEXT NOT NULL,
|
|
254
|
+
expires_at TEXT NOT NULL,
|
|
255
|
+
state TEXT NOT NULL DEFAULT 'open' CHECK (state IN ('open', 'done', 'cancelled', 'expired')),
|
|
256
|
+
created_at TEXT NOT NULL,
|
|
257
|
+
checked_at TEXT,
|
|
258
|
+
closed_at TEXT,
|
|
259
|
+
closed_by TEXT,
|
|
260
|
+
outcome TEXT
|
|
261
|
+
);
|
|
262
|
+
CREATE INDEX IF NOT EXISTS lead_commitment_due ON lead_commitment (state, check_at);
|
|
263
|
+
CREATE INDEX IF NOT EXISTS lead_commitment_owner ON lead_commitment (owner, state, id);
|
|
264
|
+
`;
|
|
230
265
|
/** What still pings on the lead's own work: a security alert. */
|
|
231
266
|
const SECURITY_KINDS = new Set(["secret-detected", "security-release"]);
|
|
232
267
|
/** A failure that leaves nothing for the lead to try: the attempts are spent. */
|
|
@@ -245,6 +280,22 @@ export function stopFactOf(store, runId) {
|
|
|
245
280
|
: /cannot be proven|could not be established|incomplete native/.test(problem) ? "unknown" : "unprovable";
|
|
246
281
|
return { run: Number(/run #(\d+)/.exec(problem)?.[1] ?? runId), kind, problem };
|
|
247
282
|
}
|
|
283
|
+
/** The lead's one question to its owner (no version bump): at most one per answered turn, two to four short options;
|
|
284
|
+
* "Something else" is implied. The tapped option comes back as the owner's next message, so nothing here is an answer:
|
|
285
|
+
* a question is open while no later message from the owner sits in its thread. */
|
|
286
|
+
export const MATE_ASK_SCHEMA = `
|
|
287
|
+
CREATE TABLE IF NOT EXISTS mate_ask (
|
|
288
|
+
turn INTEGER PRIMARY KEY REFERENCES mate_turn(id) ON DELETE CASCADE,
|
|
289
|
+
thread INTEGER NOT NULL REFERENCES mate_thread(id) ON DELETE CASCADE,
|
|
290
|
+
question TEXT NOT NULL,
|
|
291
|
+
options_json TEXT NOT NULL,
|
|
292
|
+
created_at TEXT NOT NULL
|
|
293
|
+
);
|
|
294
|
+
CREATE INDEX IF NOT EXISTS mate_ask_thread ON mate_ask (thread, turn);
|
|
295
|
+
`;
|
|
296
|
+
export const MATE_ASK_OTHER = "Something else";
|
|
297
|
+
/** How long the question's buttons work, on every channel. */
|
|
298
|
+
export const MATE_ASK_TTL_MS = 7 * 86_400_000;
|
|
248
299
|
/** Settings → Integrations (no version bump): each integration's last check. `outcome` is ok, failed, or absent (the check found
|
|
249
300
|
* nothing set up, such as no `gh` sign-in). `ok_at` and `error`/`error_at` keep the last success and the last
|
|
250
301
|
* failure across checks, so a row can say both. `account` is a display name only, never a credential. */
|
|
@@ -3830,6 +3881,7 @@ function initializeStore(db, file) {
|
|
|
3830
3881
|
db.exec(LEDGER_CHAIN_SCHEMA);
|
|
3831
3882
|
db.exec(MONITORING_SCHEMA);
|
|
3832
3883
|
db.exec(INTEGRATIONS_SCHEMA);
|
|
3884
|
+
db.exec(MATE_ASK_SCHEMA);
|
|
3833
3885
|
db.exec(SPEND_SCHEMA);
|
|
3834
3886
|
// Sign-in pauses: one row per incident of a provider's sign-in no longer working.
|
|
3835
3887
|
db.exec(PROVIDER_AUTH_SCHEMA);
|
|
@@ -3848,6 +3900,8 @@ function initializeStore(db, file) {
|
|
|
3848
3900
|
// The update a batch line speaks for: its newest fact about that task (null: a finished result).
|
|
3849
3901
|
addColumn(db, "chat_batch_item", "notification", "INTEGER");
|
|
3850
3902
|
db.exec(LEAD_QUIET_SCHEMA);
|
|
3903
|
+
db.exec(LEAD_CONFIG_SCHEMA);
|
|
3904
|
+
db.exec(LEAD_COMMITMENT_SCHEMA);
|
|
3851
3905
|
mergeCheckTables(db);
|
|
3852
3906
|
addColumn(db, "monitoring_status", "target", "TEXT");
|
|
3853
3907
|
// v105: how a teammate's turn was billed (this computer's Claude sign-in, as last seen).
|
|
@@ -19636,6 +19690,46 @@ export class Store {
|
|
|
19636
19690
|
.run(proposal.thread, proposal.turn, proposal.kind, JSON.stringify(proposal.payload), proposal.ceilingDigest, now.toISOString());
|
|
19637
19691
|
return Number(inserted.lastInsertRowid);
|
|
19638
19692
|
}
|
|
19693
|
+
// ---- the lead's question to its owner (ask_owner) -----------------------------
|
|
19694
|
+
/** Record this turn's one question; false when the turn already asked one. */
|
|
19695
|
+
recordMateAsk(ask, now) {
|
|
19696
|
+
const inserted = this.db.prepare("INSERT OR IGNORE INTO mate_ask (turn, thread, question, options_json, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
19697
|
+
.run(ask.turn, ask.thread, ask.question, JSON.stringify(ask.options), now.toISOString());
|
|
19698
|
+
return Number(inserted.changes) === 1;
|
|
19699
|
+
}
|
|
19700
|
+
/** A failed turn keeps no question, as it keeps no drafts. */
|
|
19701
|
+
dropMateAsk(turn) {
|
|
19702
|
+
this.db.prepare("DELETE FROM mate_ask WHERE turn = ?").run(turn);
|
|
19703
|
+
}
|
|
19704
|
+
/** The question an ANSWERED turn asked; null when it asked none or did not answer. */
|
|
19705
|
+
mateAsk(turn) {
|
|
19706
|
+
const row = this.db.prepare("SELECT a.* FROM mate_ask a JOIN mate_turn t ON t.id = a.turn WHERE a.turn = ? AND t.state = 'answered'").get(turn);
|
|
19707
|
+
if (row === undefined)
|
|
19708
|
+
return null;
|
|
19709
|
+
const options = JSON.parse(String(row["options_json"]));
|
|
19710
|
+
return { turn: Number(row["turn"]), thread: Number(row["thread"]), question: String(row["question"]), options: Array.isArray(options) ? options.map(String) : [], createdAt: String(row["created_at"]) };
|
|
19711
|
+
}
|
|
19712
|
+
/** What a tap on the question finds: open; answered (the owner has written in its thread since the reply that asked it);
|
|
19713
|
+
* or expired (older than MATE_ASK_TTL_MS, or the question or the reply that asked it is gone, as after a retention purge). */
|
|
19714
|
+
mateAskState(turn, now) {
|
|
19715
|
+
const ask = this.mateAsk(turn);
|
|
19716
|
+
if (ask === null)
|
|
19717
|
+
return { state: "expired" };
|
|
19718
|
+
const asking = this.db.prepare("SELECT MAX(id) AS id FROM mate_message WHERE thread = ? AND turn = ? AND role = 'assistant'").get(ask.thread, turn);
|
|
19719
|
+
if (asking === undefined || asking["id"] === null)
|
|
19720
|
+
return { state: "expired" };
|
|
19721
|
+
const later = this.db.prepare("SELECT 1 AS hit FROM mate_message WHERE thread = ? AND role = 'operator' AND id > ? LIMIT 1").get(ask.thread, Number(asking["id"]));
|
|
19722
|
+
if (later !== undefined)
|
|
19723
|
+
return { state: "answered" };
|
|
19724
|
+
if (new Date(ask.createdAt).getTime() + MATE_ASK_TTL_MS <= now.getTime())
|
|
19725
|
+
return { state: "expired" };
|
|
19726
|
+
return { state: "open", ask };
|
|
19727
|
+
}
|
|
19728
|
+
/** The question while it is open (see mateAskState); null otherwise. */
|
|
19729
|
+
mateAskOpen(turn, now) {
|
|
19730
|
+
const found = this.mateAskState(turn, now);
|
|
19731
|
+
return found.state === "open" ? found.ask : null;
|
|
19732
|
+
}
|
|
19639
19733
|
// ---- the screenshots a turn selected (v64) ------------------------------------
|
|
19640
19734
|
/**
|
|
19641
19735
|
* Record the screenshots one turn selected for an exact result, under the
|
|
@@ -21612,6 +21706,10 @@ export class Store {
|
|
|
21612
21706
|
.run(row.binding.id, row.binding.botId, row.binding.chatId, row.binding.userId, row.binding.approver, row.binding.approverGeneration, row.updateId, row.messageId, row.replyTo, row.request, row.text, row.context, row.taskId, row.sourceRun, now.toISOString());
|
|
21613
21707
|
return Number(inserted.lastInsertRowid);
|
|
21614
21708
|
}
|
|
21709
|
+
/** Whether a message from this chat is already waiting on, or being answered from, this bot message (a tapped question). */
|
|
21710
|
+
telegramConversationWaitingOn(binding, messageId) {
|
|
21711
|
+
return this.db.prepare("SELECT 1 AS hit FROM telegram_conversation WHERE binding = ? AND message_id = ? AND state IN ('queued', 'running') LIMIT 1").get(binding, messageId) !== undefined;
|
|
21712
|
+
}
|
|
21615
21713
|
getTelegramConversation(id) {
|
|
21616
21714
|
const row = this.db.prepare("SELECT * FROM telegram_conversation WHERE id = ?").get(id);
|
|
21617
21715
|
return row === undefined ? null : readTelegramConversation(row);
|
|
@@ -21742,7 +21840,7 @@ export class Store {
|
|
|
21742
21840
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?)`);
|
|
21743
21841
|
parts.forEach((part, ordinal) => {
|
|
21744
21842
|
const image = part.kind === "image" ? part : null;
|
|
21745
|
-
insert.run(id, ordinal, part.kind, part.text, part.kind === "reply" ? part.replyTo ?? null : null, part.kind === "card" ? part.proposal : null, part.kind === "card" && part.keyboard != null ? JSON.stringify(part.keyboard) : null, stamp, image?.taskId ?? null, image?.run ?? null, image?.artifact ?? null, image?.sha256 ?? null);
|
|
21843
|
+
insert.run(id, ordinal, part.kind, part.text, part.kind === "reply" ? part.replyTo ?? null : null, part.kind === "card" ? part.proposal : null, (part.kind === "card" || part.kind === "reply") && part.keyboard != null ? JSON.stringify(part.keyboard) : null, stamp, image?.taskId ?? null, image?.run ?? null, image?.artifact ?? null, image?.sha256 ?? null);
|
|
21746
21844
|
});
|
|
21747
21845
|
return true;
|
|
21748
21846
|
});
|
|
@@ -21921,6 +22019,10 @@ export class Store {
|
|
|
21921
22019
|
this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_ELSEWHERE}'
|
|
21922
22020
|
WHERE destination = ? AND delivered_at IS NULL AND receipt IS NULL
|
|
21923
22021
|
AND notification IN (SELECT id FROM notification WHERE recipient IS NOT NULL AND recipient <> ?)`).run(destination, binding.approver);
|
|
22022
|
+
// A promise the lead made on another chat is reported there (lead-commitments.ts), not here.
|
|
22023
|
+
this.db.prepare(`UPDATE notification_delivery SET receipt = '${TELEGRAM_SKIPPED_OTHER_CHAT}'
|
|
22024
|
+
WHERE destination = ? AND delivered_at IS NULL AND receipt IS NULL
|
|
22025
|
+
AND notification IN (SELECT id FROM notification WHERE dedupe_key LIKE 'lead-promise:%' AND dedupe_key NOT LIKE 'lead-promise:telegram:%')`).run(destination);
|
|
21924
22026
|
// Pings follow responsibility (the lead's work, this person's own act, a muted project): settled here, unsent.
|
|
21925
22027
|
for (const raw of this.db.prepare(`SELECT n.* FROM notification n JOIN notification_delivery d ON d.notification = n.id AND d.destination = ?
|
|
21926
22028
|
WHERE n.resolved_at IS NULL AND d.delivered_at IS NULL AND d.receipt IS NULL AND (d.claim_owner IS NULL OR d.claim_expires_at <= ?)`).all(destination, now.toISOString())) {
|
|
@@ -22143,6 +22245,16 @@ export class Store {
|
|
|
22143
22245
|
.run(day, account, day).changes) === 1;
|
|
22144
22246
|
}
|
|
22145
22247
|
// ---- pings follow responsibility: the lead, who acted, replacements and muted projects ----
|
|
22248
|
+
/** What this person named their lead and its persona, or null when they never saved one. */
|
|
22249
|
+
leadConfig(account) {
|
|
22250
|
+
const row = this.db.prepare("SELECT name, persona FROM lead_config WHERE account = ?").get(account);
|
|
22251
|
+
return row === undefined ? null : { name: String(row["name"]), persona: String(row["persona"]) };
|
|
22252
|
+
}
|
|
22253
|
+
/** Save this person's lead name and persona (checked by the caller: lead-identity.ts). */
|
|
22254
|
+
setLeadConfig(account, name, persona, now) {
|
|
22255
|
+
this.db.prepare(`INSERT INTO lead_config (account, name, persona, updated_at) VALUES (?, ?, ?, ?)
|
|
22256
|
+
ON CONFLICT(account) DO UPDATE SET name = excluded.name, persona = excluded.persona, updated_at = excluded.updated_at`).run(account, name, persona, now.toISOString());
|
|
22257
|
+
}
|
|
22146
22258
|
/** A lead token for one person, shown once. A new one ends their earlier ones; the ledger names it "lead for <owner>". */
|
|
22147
22259
|
mintLeadCredential(owner, by, now) {
|
|
22148
22260
|
return this.transact(() => {
|
|
@@ -22879,6 +22991,8 @@ function readHold(row) {
|
|
|
22879
22991
|
export const TELEGRAM_SKIPPED_RECEIPT = "skipped:before-pairing";
|
|
22880
22992
|
/** v83: a notification for another person, settled for this destination without sending. */
|
|
22881
22993
|
export const TELEGRAM_SKIPPED_ELSEWHERE = "skipped:for-another-person";
|
|
22994
|
+
/** A met promise the lead made on another chat: reported there, settled here without sending. */
|
|
22995
|
+
export const TELEGRAM_SKIPPED_OTHER_CHAT = "skipped:for-another-chat";
|
|
22882
22996
|
/** A fact this person is not messaged about: the lead's work, their own act, or a project they muted. */
|
|
22883
22997
|
export const TELEGRAM_SKIPPED_QUIET = "skipped:quiet";
|
|
22884
22998
|
/** A destination receipt (`d` = notification_delivery) that still owes a
|
package/dist/surface.js
CHANGED
|
@@ -194,7 +194,7 @@ export const COMMAND_GUIDE = [
|
|
|
194
194
|
invocation: `assignment ${action}`,
|
|
195
195
|
synopsis: action === "show" ? "read the root, current work, owner and exact receipt"
|
|
196
196
|
: action === "updates" ? "read durable updates after a cursor; save nextCursor after processing"
|
|
197
|
-
: action === "claim" ? "record lead ownership; with the lead token the task reads \"
|
|
197
|
+
: action === "claim" ? "record lead ownership; with the lead token the task reads \"<the lead's name> is on it\" and leaves Needs you until done, handed over, or 2 hours idle; grants no approval or execution authority"
|
|
198
198
|
: action === "brief" ? "catch up from current assignments and project knowledge in the local database"
|
|
199
199
|
: action === "inbox" ? "receive or replay a saved batch of lead status updates"
|
|
200
200
|
: action === "ack" ? "acknowledge delivery of an inbox batch; leaves task completion unchanged"
|
|
@@ -215,7 +215,7 @@ export const COMMAND_GUIDE = [
|
|
|
215
215
|
})),
|
|
216
216
|
operator("task state", "set a task's state by hand — an operator correction, not a workflow step (--replaced-by <id> with cancelled)"),
|
|
217
217
|
operator("lead token", "mint the lead's credential behind your password (--revoke ends it): its own work pings nobody"),
|
|
218
|
-
{ invocation: "lead say", synopsis: "as the lead: one short message in its person's chat
|
|
218
|
+
{ invocation: "lead say", synopsis: "as the lead: one short message in its person's chat under the lead's name (Settings → Lead); several within two minutes are one message",
|
|
219
219
|
audience: "agent", agentMayInvoke: true, mutation: "unkeyed",
|
|
220
220
|
positionals: [{ name: "text", required: true, meaning: "one short line in plain words" }],
|
|
221
221
|
flags: [jsonFlag, dbFlag,
|
package/dist/task-status.d.ts
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
/** One answer per task, then the details (docs/design/task-status.md). Every
|
|
2
|
-
* surface — the Tasks list, the task page, the build page, Crew, the Chat
|
|
3
|
-
* result, the Telegram/Slack/Discord/Teams cards and `toolroll status` — reads
|
|
4
|
-
* its headline, sentence, tone and detail rows from `taskStatusOf`, so the
|
|
5
|
-
* words cannot drift apart again. Pure: it reads facts the existing
|
|
6
|
-
* projections already recorded and never changes a check, approval or
|
|
7
|
-
* completion.
|
|
8
|
-
*
|
|
9
|
-
* Severity: red belongs to the Failed headline alone. A detail problem that
|
|
10
|
-
* doesn't undo the outcome (a pull request that couldn't open, shortened
|
|
11
|
-
* output) is an amber note on its own row with one action; the exact
|
|
12
|
-
* technical reason stays one tap away in `why`. */
|
|
13
1
|
import type { AssignmentSnapshot } from "./assignment.js";
|
|
14
2
|
import { type NeedAction, type NeedContext, type NeedKey, type WaitKey } from "./needs-you.js";
|
|
15
3
|
export type { NeedKey, WaitKey } from "./needs-you.js";
|
|
@@ -20,6 +8,11 @@ export declare const HEADLINES: readonly ["Queued", "Planning", "Needs you", "Wa
|
|
|
20
8
|
export type Headline = (typeof HEADLINES)[number];
|
|
21
9
|
export type HeadlineTone = "neutral" | "live" | "attention" | "ready" | "success" | "danger";
|
|
22
10
|
export declare const HEADLINE_TONE: Readonly<Record<Headline, HeadlineTone>>;
|
|
11
|
+
/** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
|
|
12
|
+
export declare const CHECKS_LABEL = "Project checks";
|
|
13
|
+
export declare const PR_CI_LABEL = "PR CI";
|
|
14
|
+
/** The one verb for opening a saved result, on every surface. */
|
|
15
|
+
export declare const OPEN_RESULT = "Open result";
|
|
23
16
|
export type DetailKey = "checks" | "pull-request" | "requirements" | "evidence";
|
|
24
17
|
/** The row's small icon: the only coloured part of a detail. `failed` is red
|
|
25
18
|
* and only ever appears under the Failed headline; `note` is amber. */
|
|
@@ -86,11 +79,13 @@ export type TaskStatusFacts = {
|
|
|
86
79
|
report?: boolean;
|
|
87
80
|
checks?: ChecksFact | null;
|
|
88
81
|
pullRequest?: PullRequestFact | null;
|
|
89
|
-
/** `unverified`: the report is refuted, so no requirement counts as met however it was marked.
|
|
82
|
+
/** `unverified`: the report is refuted, so no requirement counts as met however it was marked. `missed`: the ones it
|
|
83
|
+
* failed or left unanswered. */
|
|
90
84
|
requirements?: {
|
|
91
85
|
met: number;
|
|
92
86
|
total: number;
|
|
93
87
|
yours: number;
|
|
88
|
+
missed?: number;
|
|
94
89
|
unverified?: boolean;
|
|
95
90
|
} | null;
|
|
96
91
|
evidence?: {
|
|
@@ -109,14 +104,16 @@ export type TaskStatusFacts = {
|
|
|
109
104
|
};
|
|
110
105
|
/** Exact technical reasons, shown only on request. */
|
|
111
106
|
why?: readonly string[];
|
|
112
|
-
/** The person's lead took this on (lead-voice.ts): "on-it" reads "
|
|
107
|
+
/** The person's lead took this on (lead-voice.ts): "on-it" reads "<name> is on it" and needs nobody; "lapsed"
|
|
113
108
|
* (two hours without a lead act) is back with the person, and says so. */
|
|
114
109
|
lead?: "on-it" | "lapsed" | null;
|
|
110
|
+
/** What the person calls their lead (Settings → Lead); "Lead" when unnamed. */
|
|
111
|
+
leadName?: string;
|
|
115
112
|
};
|
|
116
113
|
export declare function headlineOf(facts: Pick<TaskStatusFacts, "stage" | "checks" | "report">): Headline;
|
|
117
114
|
/** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
|
|
118
|
-
export declare const
|
|
119
|
-
export declare const
|
|
115
|
+
export declare const leadOnIt: (name?: string) => string;
|
|
116
|
+
export declare const leadLapsed: (name?: string) => string;
|
|
120
117
|
export declare function taskStatusOf(facts: TaskStatusFacts): TaskStatus;
|
|
121
118
|
export declare function plainReasonOf(stage: TaskStage, code: string, detail: string | null | undefined): string | null;
|
|
122
119
|
export declare function stageOfCode(code: string, options?: {
|
|
@@ -153,14 +150,19 @@ export declare function assignmentStageOf(assignment: Pick<AssignmentSnapshot, "
|
|
|
153
150
|
need?: TaskStatusFacts["need"];
|
|
154
151
|
wait?: WaitKey;
|
|
155
152
|
};
|
|
156
|
-
|
|
157
|
-
export declare function requirementsOf(matrix: readonly {
|
|
153
|
+
type RequirementRow = {
|
|
158
154
|
state: string;
|
|
159
155
|
assessment?: {
|
|
160
156
|
evidenceState?: string;
|
|
161
157
|
} | undefined;
|
|
162
158
|
review?: unknown;
|
|
163
|
-
}
|
|
159
|
+
};
|
|
160
|
+
/** One requirement's state in words: the card's Requirements row and the Checks tab read this one source, so they
|
|
161
|
+
* can't disagree. A refuted report verifies none of its requirements, so each reads Unverified, as the card does. */
|
|
162
|
+
export type RequirementWord = "Met" | "You check" | "Not shown yet" | "Not met" | "Unverified";
|
|
163
|
+
export declare function requirementWordOf(row: RequirementRow, verdict?: string | null): RequirementWord;
|
|
164
|
+
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
165
|
+
export declare function requirementsOf(matrix: readonly RequirementRow[] | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
164
166
|
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
165
167
|
export declare function unverifiedWhenRefuted(requirements: NonNullable<TaskStatusFacts["requirements"]> | null, verdict: string | null | undefined): NonNullable<TaskStatusFacts["requirements"]> | null;
|
|
166
168
|
/** Saved evidence health from the receipt's artifact list and its caveats. */
|
|
@@ -188,6 +190,8 @@ export declare function headlineEmoji(headline: Headline): string;
|
|
|
188
190
|
export declare const statusIconSvg: (mark: DetailMark) => string;
|
|
189
191
|
/** The detail rows as server HTML: quiet rows, colour only on the icon. */
|
|
190
192
|
export declare function statusDetailsHtml(status: TaskStatus): string;
|
|
193
|
+
/** The status card's fold of exact reasons: "More", never a third thing called Details. */
|
|
194
|
+
export declare const STATUS_MORE = "More";
|
|
191
195
|
/** The technical reasons, one tap away. */
|
|
192
196
|
export declare function statusWhyHtml(status: TaskStatus, extra?: readonly string[], diagnostics?: readonly {
|
|
193
197
|
token: string;
|
|
@@ -211,3 +215,6 @@ export declare function stageOfDispatch(d: {
|
|
|
211
215
|
stage: TaskStage;
|
|
212
216
|
need?: TaskStatusFacts["need"];
|
|
213
217
|
};
|
|
218
|
+
/** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
|
|
219
|
+
export declare const DEMO_CHECKS = "Can't run in the demo";
|
|
220
|
+
export declare function demoChecksOf(status: TaskStatus): TaskStatus;
|
package/dist/task-status.js
CHANGED
|
@@ -1,3 +1,16 @@
|
|
|
1
|
+
/** One answer per task, then the details (docs/design/task-status.md). Every
|
|
2
|
+
* surface — the Tasks list, the task page, the build page, Crew, the Chat
|
|
3
|
+
* result, the Telegram/Slack/Discord/Teams cards and `toolroll status` — reads
|
|
4
|
+
* its headline, sentence, tone and detail rows from `taskStatusOf`, so the
|
|
5
|
+
* words cannot drift apart again. Pure: it reads facts the existing
|
|
6
|
+
* projections already recorded and never changes a check, approval or
|
|
7
|
+
* completion.
|
|
8
|
+
*
|
|
9
|
+
* Severity: red belongs to the Failed headline alone. A detail problem that
|
|
10
|
+
* doesn't undo the outcome (a pull request that couldn't open, shortened
|
|
11
|
+
* output) is an amber note on its own row with one action; the exact
|
|
12
|
+
* technical reason stays one tap away in `why`. */
|
|
13
|
+
import { DEFAULT_LEAD_NAME } from "./lead-identity.js";
|
|
1
14
|
import { CODE_NEED, NEEDS, needSentence, waitSentence } from "./needs-you.js";
|
|
2
15
|
/** A cancelled task with a successor reads "Replaced by <id>" everywhere, never "Cancelled". */
|
|
3
16
|
export const replacedWords = (successor) => `Replaced by ${successor}`;
|
|
@@ -8,6 +21,11 @@ export const HEADLINE_TONE = {
|
|
|
8
21
|
Queued: "neutral", Planning: "live", "Needs you": "attention", Waiting: "neutral", Building: "live",
|
|
9
22
|
"Ready for review": "ready", Complete: "success", Failed: "danger", Stopped: "neutral",
|
|
10
23
|
};
|
|
24
|
+
/** The project's own checks and the pull request's CI are two different things, never one word (Checks beside CI). */
|
|
25
|
+
export const CHECKS_LABEL = "Project checks";
|
|
26
|
+
export const PR_CI_LABEL = "PR CI";
|
|
27
|
+
/** The one verb for opening a saved result, on every surface. */
|
|
28
|
+
export const OPEN_RESULT = "Open result";
|
|
11
29
|
const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
|
|
12
30
|
export function headlineOf(facts) {
|
|
13
31
|
switch (facts.stage) {
|
|
@@ -76,19 +94,20 @@ function detailsOf(headline, facts) {
|
|
|
76
94
|
const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
|
|
77
95
|
const quick = checks.level === "quick";
|
|
78
96
|
if (checks.running != null)
|
|
79
|
-
row("checks",
|
|
97
|
+
row("checks", CHECKS_LABEL, `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
|
|
80
98
|
else if (checks.status === "passed")
|
|
81
|
-
row("checks",
|
|
99
|
+
row("checks", CHECKS_LABEL, `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
|
|
82
100
|
else if (checks.status === "failed")
|
|
83
|
-
row("checks",
|
|
101
|
+
row("checks", CHECKS_LABEL, `${quick ? "Quick checks failed" : "Failed"}${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref } : { action: { label: "See what failed", href: checksHref } });
|
|
84
102
|
else if (checks.status === "running")
|
|
85
|
-
row("checks",
|
|
103
|
+
row("checks", CHECKS_LABEL, "Running", "running");
|
|
86
104
|
else if (checks.level === "off")
|
|
87
|
-
row("checks",
|
|
105
|
+
row("checks", CHECKS_LABEL, "Off for this project", "none", runChecks === null ? {} : { action: runChecks });
|
|
88
106
|
else if (checks.status === "not-run")
|
|
89
|
-
row("checks",
|
|
107
|
+
row("checks", CHECKS_LABEL, "Didn't run", "none", runChecks === null ? {} : { action: runChecks });
|
|
108
|
+
// A saved check that can't be read is run again where that is possible.
|
|
90
109
|
else
|
|
91
|
-
row("checks",
|
|
110
|
+
row("checks", CHECKS_LABEL, "Couldn't be read", "note", { action: runChecks ?? { label: OPEN_RESULT, href: facts.links?.result ?? null } });
|
|
92
111
|
}
|
|
93
112
|
const pr = facts.pullRequest;
|
|
94
113
|
if (pr != null) {
|
|
@@ -105,16 +124,19 @@ function detailsOf(headline, facts) {
|
|
|
105
124
|
else if (pr.state === "failed")
|
|
106
125
|
row("pull-request", "Pull request", "Couldn't open", "note", { action: pr.compareUrl ? { label: "Open it on GitHub", href: pr.compareUrl } : { label: "See why", href: facts.links?.pullRequest ?? null }, why: pr.error === null ? "Publishing gave up. The commit is safe locally." : `${pr.error} The commit is safe locally.` });
|
|
107
126
|
else if (pr.ci === "running")
|
|
108
|
-
row("pull-request", "Pull request", `${name} open
|
|
127
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} running`, "running", { href: github });
|
|
109
128
|
else if (pr.ci === "passing")
|
|
110
|
-
row("pull-request", "Pull request", `${name} open
|
|
129
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} passed`, "ok", { href: github });
|
|
111
130
|
else if (pr.ci === "failing")
|
|
112
|
-
row("pull-request", "Pull request", `${name} open
|
|
131
|
+
row("pull-request", "Pull request", `${name} open · ${PR_CI_LABEL} failing`, "note", { action: { label: "Open it on GitHub", href: github } });
|
|
113
132
|
else
|
|
114
133
|
row("pull-request", "Pull request", `${name} open`, "none", { href: github });
|
|
115
134
|
}
|
|
116
135
|
const req = facts.requirements;
|
|
117
|
-
|
|
136
|
+
// A failed result names how many it missed, refuted or not: that is what failed.
|
|
137
|
+
if (req != null && req.total > 0 && headline === "Failed" && (req.missed ?? 0) > 0)
|
|
138
|
+
row("requirements", "Requirements", `${req.missed} missed`, "failed");
|
|
139
|
+
else if (req != null && req.total > 0 && req.unverified === true)
|
|
118
140
|
row("requirements", "Requirements", "Unverified", "none");
|
|
119
141
|
else if (req != null && req.total > 0) {
|
|
120
142
|
const unmet = req.total - req.met - req.yours;
|
|
@@ -136,16 +158,16 @@ function detailsOf(headline, facts) {
|
|
|
136
158
|
return rows;
|
|
137
159
|
}
|
|
138
160
|
/** What the lead's claim says, in place of "waits for you" (lead-voice.ts). */
|
|
139
|
-
export const
|
|
140
|
-
export const
|
|
161
|
+
export const leadOnIt = (name = DEFAULT_LEAD_NAME) => `${name} is on it.`;
|
|
162
|
+
export const leadLapsed = (name = DEFAULT_LEAD_NAME) => `${name} hasn't acted on this for 2 hours, so it's back with you.`;
|
|
141
163
|
/** The headlines a lead's claim speaks for: the ones that would otherwise wait on a person. */
|
|
142
164
|
const LEAD_HEADLINES = new Set(["Needs you", "Failed", "Ready for review", "Stopped"]);
|
|
143
165
|
function leadSentence(headline, facts) {
|
|
144
166
|
const sha = short(facts.checks?.head ?? null);
|
|
145
167
|
// A failed check stays said: the claim changes who acts next, never the result.
|
|
146
168
|
if (headline === "Failed" && facts.checks?.status === "failed")
|
|
147
|
-
return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${
|
|
148
|
-
return
|
|
169
|
+
return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} failed${sha === null ? "" : ` on ${sha}`}. ${leadOnIt(facts.leadName)}`;
|
|
170
|
+
return leadOnIt(facts.leadName);
|
|
149
171
|
}
|
|
150
172
|
export function taskStatusOf(facts) {
|
|
151
173
|
const read = headlineOf(facts);
|
|
@@ -156,7 +178,7 @@ export function taskStatusOf(facts) {
|
|
|
156
178
|
const need = headline === "Needs you" ? { key: facts.need ?? "other", action: NEEDS[facts.need ?? "other"].action } : null;
|
|
157
179
|
const ownWords = need !== null && (need.key === "other" || need.key === "review-result") && facts.action != null;
|
|
158
180
|
const primaryAction = need === null || ownWords ? facts.action ?? null : { label: need.action.label, href: facts.action?.href ?? null };
|
|
159
|
-
const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${
|
|
181
|
+
const sentence = lead === "on-it" ? leadSentence(headline, facts) : lead === "lapsed" ? `${leadLapsed(facts.leadName)} ${sentenceOf(headline, facts)}` : sentenceOf(headline, facts);
|
|
160
182
|
return { headline, tone: HEADLINE_TONE[headline], sentence, details: detailsOf(headline, facts),
|
|
161
183
|
primaryAction, why: [...new Set(facts.why ?? [])].filter(one => one.trim() !== ""), need };
|
|
162
184
|
}
|
|
@@ -258,13 +280,21 @@ export function assignmentStageOf(assignment, work, planning = false) {
|
|
|
258
280
|
}
|
|
259
281
|
}
|
|
260
282
|
}
|
|
283
|
+
/** Evidence that passed and only awaits the retired assessment step is met. */
|
|
284
|
+
const requirementMet = (row) => row.state === "pass" || (row.assessment?.evidenceState === "pass" && (row.review ?? null) === null);
|
|
285
|
+
export function requirementWordOf(row, verdict) {
|
|
286
|
+
if (verdict === "refuted")
|
|
287
|
+
return "Unverified";
|
|
288
|
+
if (requirementMet(row))
|
|
289
|
+
return "Met";
|
|
290
|
+
return row.state === "manual-review" ? "You check" : row.state === "missing" ? "Not shown yet" : "Not met";
|
|
291
|
+
}
|
|
261
292
|
/** Requirements from the stored matrix: met, the ones only a person confirms, and the total. */
|
|
262
293
|
export function requirementsOf(matrix) {
|
|
263
294
|
if (!matrix || matrix.length === 0)
|
|
264
295
|
return null;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return { met: matrix.filter(met).length, total: matrix.length, yours: matrix.filter(row => !met(row) && row.state === "manual-review").length };
|
|
296
|
+
return { met: matrix.filter(requirementMet).length, total: matrix.length, yours: matrix.filter(row => requirementWordOf(row) === "You check").length,
|
|
297
|
+
missed: matrix.filter(row => !requirementMet(row) && (row.state === "failed" || row.state === "missing")).length };
|
|
268
298
|
}
|
|
269
299
|
/** A refuted report verifies none of its requirements, whatever it marked met (a person's acceptance doesn't change that). */
|
|
270
300
|
export function unverifiedWhenRefuted(requirements, verdict) {
|
|
@@ -347,18 +377,20 @@ export const statusIconSvg = (mark) => `<svg class="status-icon status-icon--${m
|
|
|
347
377
|
export function statusDetailsHtml(status) {
|
|
348
378
|
if (status.details.length === 0)
|
|
349
379
|
return "";
|
|
350
|
-
return `<ul class="status-details" aria-label="
|
|
380
|
+
return `<ul class="status-details" aria-label="Status">` + status.details.map(one => {
|
|
351
381
|
const text = one.href === null ? escapeHtml(one.text) : `<a href="${escapeHtml(one.href)}">${escapeHtml(one.text)}</a>`;
|
|
352
382
|
const action = one.action === null ? "" : one.action.href === null ? `<span class="status-detail-act">${escapeHtml(one.action.label)}</span>` : `<a class="status-detail-act" href="${escapeHtml(one.action.href)}">${escapeHtml(one.action.label)}</a>`;
|
|
353
383
|
return `<li class="status-detail status-detail--${one.mark}" data-status-detail="${one.key}" data-mark="${one.mark}">${statusIconSvg(one.mark)}<span class="status-detail-label">${escapeHtml(one.label)}</span><span class="status-detail-text"><span>${text}</span>${action}</span></li>`;
|
|
354
384
|
}).join("") + `</ul>`;
|
|
355
385
|
}
|
|
386
|
+
/** The status card's fold of exact reasons: "More", never a third thing called Details. */
|
|
387
|
+
export const STATUS_MORE = "More";
|
|
356
388
|
/** The technical reasons, one tap away. */
|
|
357
389
|
export function statusWhyHtml(status, extra = [], diagnostics = []) {
|
|
358
390
|
const lines = [...new Set([...status.details.flatMap(one => one.why === null ? [] : [`${one.label}: ${one.why}`]), ...status.why, ...extra])];
|
|
359
391
|
if (lines.length === 0 && diagnostics.length === 0)
|
|
360
392
|
return "";
|
|
361
|
-
return `<details class="status-why"><summary
|
|
393
|
+
return `<details class="status-why"><summary>${STATUS_MORE}</summary>${lines.map(one => `<p class="meta">${escapeHtml(one)}</p>`).join("")}` +
|
|
362
394
|
diagnostics.map(one => `<p class="meta" data-work-diagnostic="${escapeHtml(one.token)}">${escapeHtml(one.label)} · ${escapeHtml(one.detail)}</p>`).join("") + `</details>`;
|
|
363
395
|
}
|
|
364
396
|
/** Shared CSS for the server-rendered status: neutral rows, coloured icons. */
|
|
@@ -377,3 +409,9 @@ export function stageOfDispatch(d, options = {}) {
|
|
|
377
409
|
return { stage: "needs-you", need: "review-result" };
|
|
378
410
|
return stageOfCode(d.code, { needsPerson: d.condition === "waiting" && d.action !== null, planning: d.condition === "running" && d.role === "planner", operatorHold: d.action === "unhold" });
|
|
379
411
|
}
|
|
412
|
+
/** The demo runs no checks: a Checks row that would offer to run one (or open one that never ran) says so instead. */
|
|
413
|
+
export const DEMO_CHECKS = "Can't run in the demo";
|
|
414
|
+
export function demoChecksOf(status) {
|
|
415
|
+
return { ...status, details: status.details.map(one => one.key !== "checks" || one.mark === "ok" || one.mark === "failed" || one.mark === "running" ? one
|
|
416
|
+
: { ...one, text: DEMO_CHECKS, mark: "none", href: null, action: null, why: null }) };
|
|
417
|
+
}
|
package/dist/team-runtime.js
CHANGED
|
@@ -148,7 +148,8 @@ export function createTeamRuntime(options) {
|
|
|
148
148
|
turnId: receipt.turn, ...(turn?.state === 'answered' ? {} : { error: 'Inspect the saved response; this message was not sent again.' }) }, clock());
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
151
|
-
const result = await runMateTurn({ store, who: proof.who, session, thread, ...provider, message: claim.text, requestId,
|
|
151
|
+
const result = await runMateTurn({ store, who: proof.who, session, thread, ...provider, message: claim.text, requestId, channel: "console",
|
|
152
|
+
...(() => { const name = view.leads.find(lead => lead.id === claim.leadId)?.name; return name === undefined ? {} : { leadName: name }; })(),
|
|
152
153
|
queuedMessageId: claim.messageId,
|
|
153
154
|
onAdmitted: turn => { if (!domain.current(claim) || !domain.bindTurn(claim, turn))
|
|
154
155
|
throw new Error('The queued message changed before admission.'); },
|