toolroll 0.9.41 → 0.9.43
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 +37 -0
- package/dist/chat-actions.js +1 -1
- package/dist/chat-approval.d.ts +45 -0
- package/dist/chat-approval.js +81 -0
- package/dist/chat-channel.js +5 -0
- package/dist/chat-decide.d.ts +18 -6
- package/dist/chat-decide.js +54 -20
- package/dist/chat-flow.js +2 -2
- package/dist/cli.js +4 -2
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +484 -0
- package/dist/contracts/console-api.js +274 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- package/dist/flow-triggers.js +61 -60
- package/dist/flows-cli.d.ts +28 -0
- package/dist/flows-cli.js +43 -1
- package/dist/guides.js +2 -0
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +43 -1
- package/dist/mcp-connect.js +37 -22
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +55 -4
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +193 -30
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +11 -16
- package/dist/store.js +122 -160
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +10 -2
- package/dist/teammate-work.js +1 -1
- package/dist/telegram-flow.d.ts +41 -1
- package/dist/telegram-flow.js +96 -12
- package/dist/telegram.js +27 -3
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/surface.d.ts
CHANGED
|
@@ -1,57 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* shape, not authority — legacy verbs do not execute from it,
|
|
5
|
-
* and the runtime envelope remains the truth. Its honest limits ride the
|
|
6
|
-
* envelope itself as SURFACE_NOTES, because a source comment is invisible
|
|
7
|
-
* to consumers (arc-5 review, finding 1).
|
|
8
|
-
*
|
|
9
|
-
* What the tests DO hold to the code: every root here is a routed verb
|
|
10
|
-
* and every routed verb appears here; every declared flag lives in the
|
|
11
|
-
* parser's global vocabulary with the declared arity; the subcommand
|
|
12
|
-
* inventories for task/publish/config/approver/routine/contest match the
|
|
13
|
-
* exported action lists their dispatchers consult; keyed mutations
|
|
14
|
-
* declare --key; notableReasons come from DOCUMENTED_REASONS. What they
|
|
15
|
-
* CANNOT prove — that a given flag is read by a given handler — is why
|
|
16
|
-
* SURFACE_NOTES.flags says "intended".
|
|
17
|
-
*/
|
|
18
|
-
import { type SessionDescriptor } from './session-contract.js';
|
|
19
|
-
export type CommandFlag = {
|
|
20
|
-
readonly name: string;
|
|
21
|
-
readonly takesValue: boolean;
|
|
22
|
-
readonly meaning: string;
|
|
23
|
-
};
|
|
24
|
-
export type CommandRow = {
|
|
25
|
-
/** What you type after `toolroll` (subcommands included). */
|
|
26
|
-
readonly invocation: string;
|
|
27
|
-
/** The `command` field the envelope answers with, where it differs
|
|
28
|
-
* from the invocation (the no-verb report answers as "scan"). */
|
|
29
|
-
readonly envelopeCommand?: string;
|
|
30
|
-
readonly synopsis: string;
|
|
31
|
-
/** Who this act belongs to. "operator" rows are ceremonies or
|
|
32
|
-
* infrastructure: an agent must not invoke them even when credentials
|
|
33
|
-
* are within reach — the credential IS the person. The operator() helper
|
|
34
|
-
* omits flag detail on purpose; an explicit row may document flags when
|
|
35
|
-
* its command contract requires them. A schema is never permission. */
|
|
36
|
-
readonly audience: "agent" | "operator";
|
|
37
|
-
readonly agentMayInvoke: boolean;
|
|
38
|
-
/** Truthful retry semantics, not a boolean:
|
|
39
|
-
* keyed — takes --key; same key returns the first answer.
|
|
40
|
-
* identity-idempotent — repeating it converges (same lease, same
|
|
41
|
-
* path, same managed file); no key needed.
|
|
42
|
-
* unkeyed — a mutation without replay protection: do not blind-retry.
|
|
43
|
-
* none — a read. */
|
|
44
|
-
readonly mutation: "keyed" | "identity-idempotent" | "unkeyed" | "none";
|
|
45
|
-
readonly positionals?: readonly {
|
|
46
|
-
name: string;
|
|
47
|
-
required: boolean;
|
|
48
|
-
meaning: string;
|
|
49
|
-
}[];
|
|
50
|
-
readonly flags?: readonly CommandFlag[];
|
|
51
|
-
readonly notableReasons?: readonly string[];
|
|
52
|
-
/** Session schemas are executable input contracts, not a grant of authority. */
|
|
53
|
-
readonly inputSchema?: SessionDescriptor['inputSchema'];
|
|
54
|
-
};
|
|
1
|
+
import { type CommandFlag, type CommandRow } from './contracts/cli.js';
|
|
2
|
+
/** A guide row and its flags are derived from their schemas in src/contracts/cli.ts. */
|
|
3
|
+
export type { CommandFlag, CommandRow };
|
|
55
4
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
56
5
|
export declare const SURFACE_NOTES: {
|
|
57
6
|
readonly authority: "documentation is not permission; session descriptors also drive parser and request validation; legacy entries remain advisory";
|
|
@@ -60,4 +9,7 @@ export declare const SURFACE_NOTES: {
|
|
|
60
9
|
readonly database: "local database commands wait up to 15 seconds for another writer; an exhausted wait returns database-busy and names the selected file";
|
|
61
10
|
};
|
|
62
11
|
export declare const SURFACE_SCHEMA_VERSION = 1;
|
|
12
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts). */
|
|
13
|
+
export declare const COMMAND_ENTRIES: readonly import("./contracts/cli.js").CommandEntry[];
|
|
14
|
+
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
63
15
|
export declare const COMMAND_GUIDE: readonly CommandRow[];
|
package/dist/surface.js
CHANGED
|
@@ -21,6 +21,7 @@ import { FLOWS_DESCRIPTORS } from "./flows-cli.js";
|
|
|
21
21
|
*/
|
|
22
22
|
import { SESSION_DESCRIPTORS } from './session-contract.js';
|
|
23
23
|
import { sessionCliFlags } from './session-cli.js';
|
|
24
|
+
import { commandEntries } from './contracts/cli.js';
|
|
24
25
|
/** The guide's limits, stated machine-readably in every dump. */
|
|
25
26
|
export const SURFACE_NOTES = {
|
|
26
27
|
authority: "documentation is not permission; session descriptors also drive parser and request validation; legacy entries remain advisory",
|
|
@@ -47,7 +48,7 @@ const operatorRead = (invocation, synopsis) => ({
|
|
|
47
48
|
agentMayInvoke: false,
|
|
48
49
|
mutation: "none",
|
|
49
50
|
});
|
|
50
|
-
|
|
51
|
+
const GUIDE_ROWS = [
|
|
51
52
|
// ---- reports (reads) ----
|
|
52
53
|
{
|
|
53
54
|
invocation: "",
|
|
@@ -124,7 +125,7 @@ export const COMMAND_GUIDE = [
|
|
|
124
125
|
...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
|
|
125
126
|
// Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
|
|
126
127
|
...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags,
|
|
127
|
-
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
128
|
+
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : name === "card" ? "a card id from flows show" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
128
129
|
...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
|
|
129
130
|
// ---- the queue (agent surface) ----
|
|
130
131
|
{ invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
|
@@ -383,6 +384,9 @@ export const COMMAND_GUIDE = [
|
|
|
383
384
|
operatorRead("review show", "whether a project's finished builds get one automatic review"),
|
|
384
385
|
operator("review on", "one automatic review of each finished build whose check passes; only HIGH findings send it back, once"),
|
|
385
386
|
operator("review off", "finished builds go straight to the person, unreviewed"),
|
|
387
|
+
operatorRead("chat-approval show", "whether your own taps in your paired chat approve plans and merges: all projects and each project's own setting"),
|
|
388
|
+
operator("chat-approval on", "let your paired chat approve plans and merges in all your projects or one (--repo); lasting, with limits; previews until --yes"),
|
|
389
|
+
operator("chat-approval off", "stop approving from chat for all your projects or one (--repo); one step"),
|
|
386
390
|
operator("mode revoke", "revoke a repository's mode — one click, every act it covered falls back to its own ceremony"),
|
|
387
391
|
operator("config set", "set phase or spend configuration"),
|
|
388
392
|
operator("config clear", "clear phase or spend configuration"),
|
|
@@ -412,3 +416,7 @@ export const COMMAND_GUIDE = [
|
|
|
412
416
|
operator("onboard", "add this repository, report signed-in agents, install or --remove the operator skill (--yes), offer the starter flows (--starter <ids>), and print the handoff"),
|
|
413
417
|
operator("daemon", "install the loop under the OS service manager"),
|
|
414
418
|
];
|
|
419
|
+
/** Every declared command, in guide order, paired with the schema of its `--json` answer (src/contracts/cli.ts). */
|
|
420
|
+
export const COMMAND_ENTRIES = commandEntries(GUIDE_ROWS);
|
|
421
|
+
/** The guide `contract --commands` dumps: each declared command's row, projected from COMMAND_ENTRIES. */
|
|
422
|
+
export const COMMAND_GUIDE = COMMAND_ENTRIES.map(entry => entry.guide);
|
package/dist/teammate-work.js
CHANGED
|
@@ -206,7 +206,7 @@ function carryOut(store, flow, definition, stage, card, mate, context, answer, n
|
|
|
206
206
|
if (answer.action === "approve" || answer.action === "send_back") {
|
|
207
207
|
const draft = draftFor(definition, stage);
|
|
208
208
|
const decided = decideFlowCard(store, {
|
|
209
|
-
card: card.id, decision: answer.action === "approve" ? "approve" : "send-back", note: answer.action === "approve" ? answer.reason || null : note(answer.note), actor, repos, teammate: mate.handle, entry: card.entry,
|
|
209
|
+
card: card.id, decision: answer.action === "approve" ? "approve" : "send-back", note: answer.action === "approve" ? answer.reason || null : note(answer.note), actor, repos, teammate: mate.handle, entry: card.entry, where: "a teammate",
|
|
210
210
|
...(answer.action === "approve" && draft !== null && answer.text !== "" ? { draft: wholeDraft(answer.text) } : {}),
|
|
211
211
|
...(io.evidenceRoot === undefined ? {} : { evidenceRoot: io.evidenceRoot }),
|
|
212
212
|
}, now);
|
package/dist/telegram-flow.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { type FlowChoiceVisit, type FlowSendContent } from "./flow-send.js";
|
|
|
2
2
|
import { type FlowStage } from "./flows.js";
|
|
3
3
|
import { type InlineButton } from "./telegram-mate.js";
|
|
4
4
|
import type { FlowCardRow, FlowRow, Store, TelegramBinding, TelegramFlowAction, TelegramFlowChoice, TelegramFlowPrompt } from "./store.js";
|
|
5
|
+
/** A flow decision's notice: one per visit, and again (`:ask:<ms>`) when the owner asks the lead to approve it. */
|
|
5
6
|
export declare const FLOW_DECIDE_KEY: RegExp;
|
|
6
7
|
type Waiting = {
|
|
7
8
|
card: FlowCardRow;
|
|
@@ -41,21 +42,60 @@ export declare function applyFlowChoiceTap(store: Store, binding: TelegramBindin
|
|
|
41
42
|
export type FlowTapEffect = {
|
|
42
43
|
kind: "ack";
|
|
43
44
|
text: string;
|
|
44
|
-
}
|
|
45
|
+
}
|
|
46
|
+
/** The message repainted; `keyboard` (absent: none) replaces its buttons, and links are paths under the console. */
|
|
47
|
+
| {
|
|
45
48
|
kind: "edit";
|
|
46
49
|
text: string;
|
|
50
|
+
keyboard?: FlowKeyboard;
|
|
51
|
+
place?: string[];
|
|
47
52
|
} | {
|
|
48
53
|
kind: "prompt";
|
|
49
54
|
text: string;
|
|
50
55
|
placeholder: string;
|
|
51
56
|
prompt: Omit<TelegramFlowPrompt, "messageId">;
|
|
52
57
|
};
|
|
58
|
+
/** A repainted message's buttons: a token, or a link to a console path. `place` names fresh flow buttons to stamp on the message. */
|
|
59
|
+
export type FlowKeyboard = Array<Array<{
|
|
60
|
+
label: string;
|
|
61
|
+
token: string;
|
|
62
|
+
} | {
|
|
63
|
+
label: string;
|
|
64
|
+
link: {
|
|
65
|
+
label: string;
|
|
66
|
+
path: string;
|
|
67
|
+
};
|
|
68
|
+
}>>;
|
|
69
|
+
/** Where a card is now, for a message whose buttons no longer act: named and linked only when it is in one of the
|
|
70
|
+
* viewer's projects, and linked only while it still waits on someone. Never a live button. */
|
|
71
|
+
export declare function flowWhereNow(store: Store, cardId: number, repos: readonly string[] | null): {
|
|
72
|
+
said: string;
|
|
73
|
+
keyboard: FlowKeyboard;
|
|
74
|
+
};
|
|
53
75
|
/** A tapped flow button, applied inside the update's transaction. */
|
|
54
76
|
export declare function applyFlowTap(store: Store, binding: TelegramBinding, action: TelegramFlowAction, message: {
|
|
55
77
|
chatId: string;
|
|
56
78
|
messageId: string;
|
|
57
79
|
text: string;
|
|
58
80
|
}, repos: readonly string[] | null, now: Date): FlowTapEffect[];
|
|
81
|
+
export type FlowConfirm = {
|
|
82
|
+
token: string;
|
|
83
|
+
binding: number;
|
|
84
|
+
chatId: string;
|
|
85
|
+
messageId: string;
|
|
86
|
+
card: number;
|
|
87
|
+
entry: number;
|
|
88
|
+
phase: "yes" | "cancel";
|
|
89
|
+
expiresAt: string;
|
|
90
|
+
consumedAt: string | null;
|
|
91
|
+
};
|
|
92
|
+
/** A Yes or Cancel this module armed, or null when the token isn't one. */
|
|
93
|
+
export declare function flowConfirmOf(store: Store, token: string): FlowConfirm | null;
|
|
94
|
+
/** The second tap, inside the update's transaction: Yes decides the card through the console's door, once; Cancel puts
|
|
95
|
+
* the card's own buttons back. The caller has proved the binding, chat and message are the ones it was armed on. */
|
|
96
|
+
export declare function applyFlowConfirm(store: Store, binding: TelegramBinding, confirm: FlowConfirm, message: {
|
|
97
|
+
text: string;
|
|
98
|
+
}, repos: readonly string[] | null, now: Date): FlowTapEffect[];
|
|
59
99
|
export type FlowReplyEffect = {
|
|
60
100
|
kind: "say";
|
|
61
101
|
text: string;
|
package/dist/telegram-flow.js
CHANGED
|
@@ -22,12 +22,13 @@
|
|
|
22
22
|
import { randomBytes } from "node:crypto";
|
|
23
23
|
import { LIMITS } from "./decision.js";
|
|
24
24
|
import { keptDraft } from "./flow-draft.js";
|
|
25
|
-
import { decideFlowCard, draftFor, flowDefinitionOf } from "./flow-engine.js";
|
|
26
|
-
import { chooseFlowCard, flowChoiceAt, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
25
|
+
import { decideFlowCard, draftFor, flowCardHref, flowDefinitionOf } from "./flow-engine.js";
|
|
26
|
+
import { chooseFlowCard, flowChoiceAt, flowPersonOf, flowSendPaths, readFlowSend } from "./flow-send.js";
|
|
27
27
|
import { deciderOf, FLOW_END, replyTarget } from "./flows.js";
|
|
28
28
|
import { phoneLinkButton } from "./telegram-mate.js";
|
|
29
29
|
import { telegramButton } from "./contracts/telegram-callback.js";
|
|
30
|
-
|
|
30
|
+
/** A flow decision's notice: one per visit, and again (`:ask:<ms>`) when the owner asks the lead to approve it. */
|
|
31
|
+
export const FLOW_DECIDE_KEY = /^flow-decide:([1-9][0-9]{0,14}):([1-9][0-9]{0,9})(?::ask:[0-9]{1,15})?$/;
|
|
31
32
|
const DRAFT_LIMIT = 4000;
|
|
32
33
|
/** The card, if it is still at that visit, waiting at a decision. */
|
|
33
34
|
export function flowDecisionAt(store, cardId, entry) {
|
|
@@ -126,24 +127,66 @@ export function applyFlowChoiceTap(store, binding, choice, message, repos, now)
|
|
|
126
127
|
const said = chosen.said.startsWith(`${choice.label}.`) ? chosen.said : `You chose “${choice.label}”. ${chosen.said}`;
|
|
127
128
|
return [{ kind: "ack", text: choice.label.slice(0, 190) }, { kind: "edit", text: `${message.text}\n\n✅ ${said}`.slice(0, 4000) }];
|
|
128
129
|
}
|
|
130
|
+
/** A card's own fresh buttons, as a repaint carries them. */
|
|
131
|
+
function freshButtons(store, binding, waiting, now) {
|
|
132
|
+
const minted = flowButtons(store, binding, waiting, now);
|
|
133
|
+
return { keyboard: minted.keyboard.map(row => row.flatMap(one => "callback_data" in one && typeof one.callback_data === "string" ? [{ label: one.text, token: one.callback_data }] : [])), place: minted.tokens };
|
|
134
|
+
}
|
|
135
|
+
const CONFIRM_TTL_MS = 10 * 60_000;
|
|
136
|
+
const FLOW_QUESTION = /\n\nApprove “[^\n]*”\? [^\n]*$/;
|
|
137
|
+
/** Where a card is now, for a message whose buttons no longer act: named and linked only when it is in one of the
|
|
138
|
+
* viewer's projects, and linked only while it still waits on someone. Never a live button. */
|
|
139
|
+
export function flowWhereNow(store, cardId, repos) {
|
|
140
|
+
const card = store.getFlowCard(cardId);
|
|
141
|
+
const flow = card === null ? null : store.getFlow(card.flow);
|
|
142
|
+
if (card === null || flow === null || repos === null || !repos.includes(flow.repo))
|
|
143
|
+
return { said: "This card has moved on since.", keyboard: [] };
|
|
144
|
+
if (card.state !== "active")
|
|
145
|
+
return { said: card.state === "done" ? "This card is done." : "This card was closed.", keyboard: [] };
|
|
146
|
+
const definition = flowDefinitionOf(flow);
|
|
147
|
+
const stage = definition?.stages.find(one => one.id === card.stage);
|
|
148
|
+
const where = stage?.title ?? card.stage;
|
|
149
|
+
const open = { label: "Open it in Flows", path: flowCardHref(flow.id, card.id) };
|
|
150
|
+
if (stage?.kind === "approval" || stage?.kind === "choose") {
|
|
151
|
+
const person = stage.kind === "approval" ? deciderOf(stage, flow) ?? "an approver" : flowPersonOf(card, flow);
|
|
152
|
+
return { said: `It's waiting for ${person} in ${where} (${flow.name}).`, keyboard: [[{ label: open.label, link: open }]] };
|
|
153
|
+
}
|
|
154
|
+
if (card.waiting !== null)
|
|
155
|
+
return { said: `It's in ${where} now (${flow.name}): ${card.waiting.replace(/\.$/, "")}.`, keyboard: [[{ label: open.label, link: open }]] };
|
|
156
|
+
return { said: `It moved on to ${where} (${flow.name}).`, keyboard: [] };
|
|
157
|
+
}
|
|
129
158
|
/** A tapped flow button, applied inside the update's transaction. */
|
|
130
159
|
export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
131
|
-
if (action.consumedAt !== null || action.expiresAt <= now.toISOString())
|
|
132
|
-
|
|
160
|
+
if (action.consumedAt !== null || action.expiresAt <= now.toISOString()) {
|
|
161
|
+
// Spent or too old: say where the card is now, with a way to it while it still waits. The old button never acts again.
|
|
162
|
+
const now_ = flowWhereNow(store, action.card, repos);
|
|
163
|
+
return [{ kind: "ack", text: "These buttons were already used, or are too old." }, { kind: "edit", text: `${message.text.replace(FLOW_QUESTION, "")}\n\nThese buttons were already used, or are too old. ${now_.said}`.slice(0, 4000), keyboard: now_.keyboard }];
|
|
164
|
+
}
|
|
133
165
|
const waiting = flowDecisionAt(store, action.card, action.entry);
|
|
134
|
-
if (waiting === null)
|
|
135
|
-
|
|
166
|
+
if (waiting === null) {
|
|
167
|
+
const now_ = flowWhereNow(store, action.card, repos);
|
|
168
|
+
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
169
|
+
return [{ kind: "ack", text: "That card has moved on since; nothing was changed." }, { kind: "edit", text: `${message.text}\n\nNothing was changed. ${now_.said}`.slice(0, 4000), keyboard: now_.keyboard }];
|
|
170
|
+
}
|
|
136
171
|
const decider = deciderOf(waiting.stage, waiting.flow);
|
|
137
172
|
if (decider !== null && decider !== binding.approver)
|
|
138
173
|
return [{ kind: "ack", text: `Only ${decider} decides here.` }];
|
|
139
174
|
if (action.action === "approve") {
|
|
175
|
+
// The first tap asks; only the Yes decides, bound to exactly this visit of this card, on this message.
|
|
140
176
|
if (repos === null)
|
|
141
177
|
return [{ kind: "ack", text: "Couldn't check your projects just now. Try again in a moment." }];
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
return [{ kind: "ack", text: decided.message.slice(0, 190) }];
|
|
178
|
+
if (!repos.includes(waiting.flow.repo))
|
|
179
|
+
return [{ kind: "ack", text: "That card isn't in one of your projects now." }];
|
|
145
180
|
store.retireTelegramFlowVisit(action.card, action.entry, now);
|
|
146
|
-
|
|
181
|
+
const mint = (phase) => {
|
|
182
|
+
const token = randomBytes(16).toString("hex");
|
|
183
|
+
store.handle.prepare("INSERT INTO telegram_flow_confirm (token, binding, chat_id, message_id, card, entry, phase, created_at, expires_at) VALUES (?,?,?,?,?,?,?,?,?)")
|
|
184
|
+
.run(token, binding.id, binding.chatId, message.messageId, action.card, action.entry, phase, now.toISOString(), new Date(now.getTime() + CONFIRM_TTL_MS).toISOString());
|
|
185
|
+
return token;
|
|
186
|
+
};
|
|
187
|
+
const where = waiting.stage.next === null ? "The card is then done." : `It moves to ${titleOf(waiting, waiting.stage.next)}.`;
|
|
188
|
+
return [{ kind: "ack", text: "Confirm below." }, { kind: "edit", text: `${message.text}\n\nApprove “${waiting.card.title}”? ${where}`.slice(0, 4000),
|
|
189
|
+
keyboard: [[{ label: "Yes", token: mint("yes") }, { label: "Cancel", token: mint("cancel") }]] }];
|
|
147
190
|
}
|
|
148
191
|
const prompt = { chatId: binding.chatId, binding: binding.id, card: action.card, entry: action.entry, mode: action.action };
|
|
149
192
|
if (action.action === "edit") {
|
|
@@ -154,6 +197,47 @@ export function applyFlowTap(store, binding, action, message, repos, now) {
|
|
|
154
197
|
return [{ kind: "ack", text: "What should change?" }, { kind: "prompt", prompt, placeholder: "What should change",
|
|
155
198
|
text: `What should change on “${waiting.card.title}”? Reply to this message and it goes back to ${back} with your note.` }];
|
|
156
199
|
}
|
|
200
|
+
/** A Yes or Cancel this module armed, or null when the token isn't one. */
|
|
201
|
+
export function flowConfirmOf(store, token) {
|
|
202
|
+
if (!/^[0-9a-f]{32}$/.test(token))
|
|
203
|
+
return null;
|
|
204
|
+
const row = store.handle.prepare("SELECT * FROM telegram_flow_confirm WHERE token = ?").get(token);
|
|
205
|
+
if (row === undefined)
|
|
206
|
+
return null;
|
|
207
|
+
return { token: String(row["token"]), binding: Number(row["binding"]), chatId: String(row["chat_id"]), messageId: String(row["message_id"]), card: Number(row["card"]), entry: Number(row["entry"]),
|
|
208
|
+
phase: String(row["phase"]) === "cancel" ? "cancel" : "yes", expiresAt: String(row["expires_at"]), consumedAt: row["consumed_at"] == null ? null : String(row["consumed_at"]) };
|
|
209
|
+
}
|
|
210
|
+
/** The second tap, inside the update's transaction: Yes decides the card through the console's door, once; Cancel puts
|
|
211
|
+
* the card's own buttons back. The caller has proved the binding, chat and message are the ones it was armed on. */
|
|
212
|
+
export function applyFlowConfirm(store, binding, confirm, message, repos, now) {
|
|
213
|
+
const body = message.text.replace(FLOW_QUESTION, "");
|
|
214
|
+
const spend = () => store.handle.prepare("UPDATE telegram_flow_confirm SET consumed_at = ? WHERE card = ? AND entry = ? AND message_id = ? AND consumed_at IS NULL").run(now.toISOString(), confirm.card, confirm.entry, confirm.messageId);
|
|
215
|
+
if (confirm.consumedAt !== null || confirm.expiresAt <= now.toISOString()) {
|
|
216
|
+
spend();
|
|
217
|
+
const now_ = flowWhereNow(store, confirm.card, repos);
|
|
218
|
+
const waiting = flowDecisionAt(store, confirm.card, confirm.entry);
|
|
219
|
+
// Too old, but the same visit still waits: fresh buttons, never the old Yes.
|
|
220
|
+
if (waiting !== null && repos !== null && repos.includes(waiting.flow.repo)) {
|
|
221
|
+
return [{ kind: "ack", text: "That Yes expired. Nothing was done." }, { kind: "edit", text: body, ...freshButtons(store, binding, waiting, now) }];
|
|
222
|
+
}
|
|
223
|
+
return [{ kind: "ack", text: "That Yes expired. Nothing was done." }, { kind: "edit", text: `${body}\n\nNothing was done. ${now_.said}`.slice(0, 4000), keyboard: now_.keyboard }];
|
|
224
|
+
}
|
|
225
|
+
spend();
|
|
226
|
+
const waiting = flowDecisionAt(store, confirm.card, confirm.entry);
|
|
227
|
+
if (waiting === null || repos === null) {
|
|
228
|
+
const now_ = flowWhereNow(store, confirm.card, repos);
|
|
229
|
+
return [{ kind: "ack", text: repos === null ? "Couldn't check your projects just now. Try again in a moment." : "That card has moved on since; nothing was changed." },
|
|
230
|
+
{ kind: "edit", text: `${body}\n\nNothing was changed. ${now_.said}`.slice(0, 4000), keyboard: now_.keyboard }];
|
|
231
|
+
}
|
|
232
|
+
if (confirm.phase === "cancel") {
|
|
233
|
+
return [{ kind: "ack", text: "Cancelled." }, { kind: "edit", text: body, ...freshButtons(store, binding, waiting, now) }];
|
|
234
|
+
}
|
|
235
|
+
const decided = decideFlowCard(store, { card: waiting.card.id, decision: "approve", note: null, actor: binding.approver, repos, entry: confirm.entry, where: "Telegram" }, now);
|
|
236
|
+
if (!decided.ok)
|
|
237
|
+
return [{ kind: "ack", text: "Not done." }, { kind: "edit", text: `${body}\n\n✗ Not done: ${decided.message}`.slice(0, 4000), keyboard: flowWhereNow(store, confirm.card, repos).keyboard }];
|
|
238
|
+
store.retireTelegramFlowVisit(confirm.card, confirm.entry, now);
|
|
239
|
+
return [{ kind: "ack", text: "Approved" }, { kind: "edit", text: `${body}\n\n✅ ${decided.said}`.slice(0, 4000) }];
|
|
240
|
+
}
|
|
157
241
|
const titleOf = (waiting, id) => flowDefinitionOf(waiting.flow)?.stages.find(one => one.id === id)?.title ?? id;
|
|
158
242
|
/** A reply to an Edit or Send back prompt, applied inside the update's transaction. */
|
|
159
243
|
export function applyFlowReply(store, binding, prompt, text, repos, now) {
|
|
@@ -200,7 +284,7 @@ export function applyFlowReply(store, binding, prompt, text, repos, now) {
|
|
|
200
284
|
return [{ kind: "say", text: `That's ${said.length.toLocaleString("en-US")} characters. Keep the note to ${LIMITS.note.toLocaleString("en-US")}, or send it back in Toolroll.` }];
|
|
201
285
|
if (repos === null)
|
|
202
286
|
return [{ kind: "say", text: "Couldn't check your projects just now. Reply again in a moment." }];
|
|
203
|
-
const decided = decideFlowCard(store, { card: waiting.card.id, decision: "send-back", note: said, actor: binding.approver, repos, entry: prompt.entry }, now);
|
|
287
|
+
const decided = decideFlowCard(store, { card: waiting.card.id, decision: "send-back", note: said, actor: binding.approver, repos, entry: prompt.entry, where: "Telegram" }, now);
|
|
204
288
|
if (!decided.ok)
|
|
205
289
|
return [{ kind: "say", text: decided.message }];
|
|
206
290
|
store.retireTelegramFlowVisit(prompt.card, prompt.entry, now);
|
package/dist/telegram.js
CHANGED
|
@@ -34,7 +34,7 @@ import { enqueueEveningDigests, finishedView, isTaskFact, joinsBatch, needsPerso
|
|
|
34
34
|
import { BATCH_MS, chatText, chatTitle, factLinkLabel, mentions, nameTelegramBot } from "./chat-voice.js";
|
|
35
35
|
import { LEAD_SAY_KIND, enqueueLeadLapses, leadSayEarlier, leadSayText, leadSubjectOf } from "./lead-voice.js";
|
|
36
36
|
import { applyTeamInbound, deliverTeamChats, teamCommand } from "./telegram-team.js";
|
|
37
|
-
import { applyFlowChoiceTap, applyFlowReply, applyFlowTap, FLOW_DECIDE_KEY, flowButtons, flowChoiceButtons, flowChoiceHead, flowDecisionAt, flowDecisionText, flowSendKeyboardRow, flowSentContent } from "./telegram-flow.js";
|
|
37
|
+
import { applyFlowChoiceTap, applyFlowConfirm, applyFlowReply, applyFlowTap, flowConfirmOf, FLOW_DECIDE_KEY, flowButtons, flowChoiceButtons, flowChoiceHead, flowDecisionAt, flowDecisionText, flowSendKeyboardRow, flowSentContent } from "./telegram-flow.js";
|
|
38
38
|
import { flowChoiceAt, flowSendTail, FLOW_CHOOSE_KEY, FLOW_SEND_KEY } from "./flow-send.js";
|
|
39
39
|
import { cleanFlowMessage, fitFlowMessage } from "./flow-items.js";
|
|
40
40
|
import { telegramReply } from "./reply-shape.js";
|
|
@@ -1523,7 +1523,8 @@ async function enrolledForMessage(context, update) {
|
|
|
1523
1523
|
async function projectsForTap(context, update) {
|
|
1524
1524
|
const callback = update.callback_query;
|
|
1525
1525
|
// Flow decision buttons (v86) work with or without the lead's conversation on this phone.
|
|
1526
|
-
const flowTap = callback !== undefined && (context.store.getTelegramFlowAction(callback.data ?? "") !== null || context.store.getTelegramFlowChoice(callback.data ?? "") !== null
|
|
1526
|
+
const flowTap = callback !== undefined && (context.store.getTelegramFlowAction(callback.data ?? "") !== null || context.store.getTelegramFlowChoice(callback.data ?? "") !== null
|
|
1527
|
+
|| flowConfirmOf(context.store, callback.data ?? "") !== null);
|
|
1527
1528
|
// So do a result's, plan's or pull request's own buttons (chat-decide.ts).
|
|
1528
1529
|
const decideTap = callback !== undefined && isDecideToken(context.store, callback.data ?? "");
|
|
1529
1530
|
if (callback === undefined || context.readProjects === undefined || (context.conversation === undefined && !flowTap && !decideTap))
|
|
@@ -2172,6 +2173,29 @@ function applyCallback(context, update, effects) {
|
|
|
2172
2173
|
}
|
|
2173
2174
|
return;
|
|
2174
2175
|
}
|
|
2176
|
+
// A flow decision's repaint: its buttons (fresh ones stamped on this message) or links under the console.
|
|
2177
|
+
const flowEdit = (effect) => {
|
|
2178
|
+
if (effect.place !== undefined && effect.place.length > 0)
|
|
2179
|
+
store.placeTelegramFlowActions(effect.place, String(message.message_id));
|
|
2180
|
+
editText(effect.text, decideKeyboard(context.conversation?.phoneOrigin, effect.keyboard ?? []));
|
|
2181
|
+
};
|
|
2182
|
+
// The Yes or Cancel a flow Approve armed (telegram-flow.ts): on the message it was armed on, by the person it was armed for.
|
|
2183
|
+
const flowConfirm = flowConfirmOf(store, token);
|
|
2184
|
+
if (flowConfirm !== null) {
|
|
2185
|
+
if (flowConfirm.binding !== binding.id || flowConfirm.chatId !== tapChat || flowConfirm.messageId !== String(message.message_id)) {
|
|
2186
|
+
report.ignored++;
|
|
2187
|
+
ack("That button isn't for this chat. Nothing was done.");
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
const repos = context.projects === null ? null : telegramConversationRepos(store, binding.approver, context.projects);
|
|
2191
|
+
for (const effect of applyFlowConfirm(store, binding, flowConfirm, { text: message.text ?? "" }, repos, clock())) {
|
|
2192
|
+
if (effect.kind === "ack")
|
|
2193
|
+
ack(effect.text);
|
|
2194
|
+
else if (effect.kind === "edit")
|
|
2195
|
+
flowEdit(effect);
|
|
2196
|
+
}
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2175
2199
|
const flowAction = store.getTelegramFlowAction(token);
|
|
2176
2200
|
if (flowAction !== null) {
|
|
2177
2201
|
if (flowAction.binding !== binding.id || flowAction.chatId !== tapChat || (flowAction.messageId !== null && flowAction.messageId !== String(message.message_id))) {
|
|
@@ -2184,7 +2208,7 @@ function applyCallback(context, update, effects) {
|
|
|
2184
2208
|
if (effect.kind === "ack")
|
|
2185
2209
|
ack(effect.text);
|
|
2186
2210
|
else if (effect.kind === "edit")
|
|
2187
|
-
|
|
2211
|
+
flowEdit(effect);
|
|
2188
2212
|
else
|
|
2189
2213
|
effects.push(async () => {
|
|
2190
2214
|
// A reply box: whatever they send back as a reply to this prompt is the new draft, or the note.
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { type
|
|
3
|
-
import { type ReleasedCodingOwner } from "./coding-update.js";
|
|
2
|
+
import { type UpdateWaiting } from "./desktop-update.js";
|
|
4
3
|
import { type SupervisorRunner } from "./daemon.js";
|
|
4
|
+
import { RUNTIME_PHASES, type RuntimeUpdateJournalRecord } from "./contracts/update-journal.js";
|
|
5
5
|
export declare const PROVENANCE_REPOSITORY = "https://github.com/ap9000/toolroll";
|
|
6
6
|
export declare const PROVENANCE_WORKFLOW = ".github/workflows/publish.yml";
|
|
7
7
|
export declare const PROVENANCE_ISSUER = "https://token.actions.githubusercontent.com";
|
|
8
8
|
/** The steps a person sees, in order. */
|
|
9
9
|
export declare const UPDATE_STEPS: readonly ["verifying", "draining", "backing-up", "rehearsing", "switching", "restarting", "health"];
|
|
10
10
|
export type UpdateStep = typeof UPDATE_STEPS[number];
|
|
11
|
-
export type RuntimePhase =
|
|
11
|
+
export type RuntimePhase = typeof RUNTIME_PHASES[number];
|
|
12
12
|
export declare const STEP_WORDS: Record<UpdateStep, string>;
|
|
13
13
|
export type When = "now" | "when-idle" | "at";
|
|
14
14
|
export type RuntimeRef = {
|
|
@@ -19,100 +19,9 @@ export type RuntimeRef = {
|
|
|
19
19
|
export declare const KEEP_RUNTIMES = 2;
|
|
20
20
|
/** The one-off launchd job the console starts the updater as. */
|
|
21
21
|
export declare const UPDATE_JOB_LABEL = "com.toolroll.update";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
kind: "update" | "rollback";
|
|
26
|
-
stateDir: string;
|
|
27
|
-
databaseFile: string;
|
|
28
|
-
stageDir: string;
|
|
29
|
-
from: RuntimeRef;
|
|
30
|
-
to: RuntimeRef;
|
|
31
|
-
when: When;
|
|
32
|
-
at: string | null;
|
|
33
|
-
actor: string;
|
|
34
|
-
phase: RuntimePhase;
|
|
35
|
-
detail: string;
|
|
36
|
-
error?: string;
|
|
37
|
-
steps: {
|
|
38
|
-
phase: RuntimePhase;
|
|
39
|
-
at: string;
|
|
40
|
-
}[];
|
|
41
|
-
startedAt: string;
|
|
42
|
-
updatedAt: string;
|
|
43
|
-
finishedAt?: string;
|
|
44
|
-
package?: {
|
|
45
|
-
sha512: string;
|
|
46
|
-
repository: string;
|
|
47
|
-
workflow: string;
|
|
48
|
-
};
|
|
49
|
-
notes?: string[];
|
|
50
|
-
/** This run's own verified copies of the live database and coding catalog: what a failure restores. */
|
|
51
|
-
backupPath?: string;
|
|
52
|
-
backupHash?: string;
|
|
53
|
-
codingBackupPath?: string;
|
|
54
|
-
codingBackupHash?: string;
|
|
55
|
-
/** A rollback installs the update's earlier backups (checked against their recorded hashes). */
|
|
56
|
-
restoreFrom?: {
|
|
57
|
-
path: string;
|
|
58
|
-
hash: string;
|
|
59
|
-
updateId: string;
|
|
60
|
-
codingPath?: string;
|
|
61
|
-
codingHash?: string;
|
|
62
|
-
};
|
|
63
|
-
rehearsal?: {
|
|
64
|
-
tables: number;
|
|
65
|
-
rows: number;
|
|
66
|
-
};
|
|
67
|
-
/** The background service, recorded before it is stopped: its definition and the processes that must be gone. */
|
|
68
|
-
service?: {
|
|
69
|
-
unit: string;
|
|
70
|
-
pids: number[];
|
|
71
|
-
};
|
|
72
|
-
/** Each repo's watch daemon that runs this version, recorded the same way: switched with the service, and stopped and
|
|
73
|
-
* restarted with it only when launchd had it loaded before the update (`loaded` absent: 0.8.1 stopped every one). */
|
|
74
|
-
watches?: {
|
|
75
|
-
unit: string;
|
|
76
|
-
pids: number[];
|
|
77
|
-
loaded?: boolean;
|
|
78
|
-
}[];
|
|
79
|
-
/** Recorded before each change so a resumed or failed run knows what to put back. */
|
|
80
|
-
switched?: {
|
|
81
|
-
links: {
|
|
82
|
-
path: string;
|
|
83
|
-
previous: string;
|
|
84
|
-
}[];
|
|
85
|
-
unit: {
|
|
86
|
-
path: string;
|
|
87
|
-
saved: string;
|
|
88
|
-
} | null;
|
|
89
|
-
watches?: {
|
|
90
|
-
path: string;
|
|
91
|
-
saved: string;
|
|
92
|
-
}[];
|
|
93
|
-
databaseRestored?: boolean;
|
|
94
|
-
};
|
|
95
|
-
/** What the live database held when a failed run restored its backup: nothing written is lost. The newest of `kept`. */
|
|
96
|
-
keptAside?: string;
|
|
97
|
-
/** The live database could not be read, so it was moved aside whole rather than copied. */
|
|
98
|
-
keptAsideUnreadable?: boolean;
|
|
99
|
-
/** Every copy kept aside, oldest first: a retried restore adds one and never drops the pointer to an earlier one. */
|
|
100
|
-
kept?: {
|
|
101
|
-
path: string;
|
|
102
|
-
unreadable: boolean;
|
|
103
|
-
}[];
|
|
104
|
-
/** The restore put the backup back: a retried restore never puts it back again (what was written since belongs to
|
|
105
|
-
* the restored version and would be lost), and it keeps a fresh copy aside before every attempt until then. */
|
|
106
|
-
restoredDatabase?: boolean;
|
|
107
|
-
/** A stale coding owner record this run released after proving its processes gone. */
|
|
108
|
-
codingOwnerReleased?: ReleasedCodingOwner;
|
|
109
|
-
seen?: boolean;
|
|
110
|
-
/** The finished run the update is waiting on (or stopped waiting on): what is in the way and the command that
|
|
111
|
-
* clears it. Absent while it waits on ordinary running work. */
|
|
112
|
-
waiting?: LingeringRun & {
|
|
113
|
-
since: string;
|
|
114
|
-
};
|
|
115
|
-
};
|
|
22
|
+
/** The update's journal (`toolroll-update.json`, `.last.json` and the stage's `update.json`):
|
|
23
|
+
* src/contracts/update-journal.ts. */
|
|
24
|
+
export type RuntimeUpdateJournal = RuntimeUpdateJournalRecord;
|
|
116
25
|
export type PackageRelease = {
|
|
117
26
|
version: string;
|
|
118
27
|
tarball: string;
|
|
@@ -164,11 +73,11 @@ export type UpdateSystem = {
|
|
|
164
73
|
};
|
|
165
74
|
export type UpdateOutcome = {
|
|
166
75
|
ok: boolean;
|
|
167
|
-
phase:
|
|
76
|
+
phase: RuntimeUpdateJournal["phase"];
|
|
168
77
|
message: string;
|
|
169
78
|
journal: RuntimeUpdateJournal | null;
|
|
170
79
|
};
|
|
171
|
-
export declare const runtimeUpdateTerminal: (phase:
|
|
80
|
+
export declare const runtimeUpdateTerminal: (phase: string) => boolean;
|
|
172
81
|
/** How long a launched or resuming updater is expected to take to start and hold the lock. */
|
|
173
82
|
export declare const UPDATER_START_MS: number;
|
|
174
83
|
export declare function readRuntimeUpdate(stateDir: string, file?: string): RuntimeUpdateJournal | null;
|