toolroll 0.9.42 → 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 +19 -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 +1 -1
- package/dist/contracts/console-api.d.ts +1 -0
- package/dist/contracts/console-api.js +1 -0
- package/dist/contracts/lead-tools.d.ts +11 -0
- package/dist/contracts/lead-tools.js +2 -0
- package/dist/flow-engine.d.ts +2 -0
- package/dist/flow-engine.js +10 -2
- 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/operate.js +53 -3
- package/dist/process-recovery-eligibility.d.ts +1 -1
- package/dist/scope.d.ts +4 -2
- package/dist/serve.js +51 -2
- package/dist/store.d.ts +8 -1
- package/dist/store.js +42 -2
- package/dist/surface.js +4 -1
- 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/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.43 — 2026-10-06
|
|
4
|
+
|
|
5
|
+
- **Approve from chat and the CLI.** A plan waiting on you and a flow's
|
|
6
|
+
approval or choose step get **Approve** and **Send back** buttons in your
|
|
7
|
+
paired Telegram chat, two taps each, recorded like a console decision. Ask
|
|
8
|
+
the lead to "approve X" and it answers with those buttons; your tap is
|
|
9
|
+
still the approval. `toolroll flows card approve <flow> <card>` and
|
|
10
|
+
`toolroll flows card send-back <flow> <card> --note …` do the same from the
|
|
11
|
+
terminal with your login.
|
|
12
|
+
- **Chat approval can stay on.** `toolroll chat-approval on|off [--repo <p>]`
|
|
13
|
+
turns approving from chat on for one project or all of them, with your
|
|
14
|
+
password to turn it on; off is instant and off for all really is all. It no
|
|
15
|
+
longer lapses with a weekly mode. Plans that widen permissions, go over a
|
|
16
|
+
cap or touch protected paths still go to the console.
|
|
17
|
+
- **Old cards point onward.** A card in a past conversation says where the
|
|
18
|
+
item went and links to it when you can open it.
|
|
19
|
+
- Still to come: Slack, Discord and Teams buttons, a Settings screen for chat
|
|
20
|
+
approval, and sending a plan back from chat.
|
|
21
|
+
|
|
3
22
|
## 0.9.42 — 2026-10-06
|
|
4
23
|
|
|
5
24
|
- **Zod revamp, waves 3 and 4.** What comes into Toolroll from outside and
|
package/dist/chat-actions.js
CHANGED
|
@@ -1347,7 +1347,7 @@ function runFlowAction(store, payload, actor, repos, root, now) {
|
|
|
1347
1347
|
return chosen.ok ? { ok: true, said: chosen.said, card: card.id } : { ok: false, message: chosen.message };
|
|
1348
1348
|
})()
|
|
1349
1349
|
: (() => {
|
|
1350
|
-
const decided = decideFlowCard(store, { card: card.id, decision: payload.operation === "flow_card_approve" ? "approve" : "send-back", note: typeof req["note"] === "string" ? req["note"] : null, actor, repos, ...(root === undefined ? {} : { evidenceRoot: root }) }, now);
|
|
1350
|
+
const decided = decideFlowCard(store, { card: card.id, decision: payload.operation === "flow_card_approve" ? "approve" : "send-back", note: typeof req["note"] === "string" ? req["note"] : null, actor, repos, where: "Chat", ...(root === undefined ? {} : { evidenceRoot: root }) }, now);
|
|
1351
1351
|
return decided.ok ? { ok: true, said: decided.said, card: card.id } : { ok: false, message: decided.message };
|
|
1352
1352
|
})();
|
|
1353
1353
|
if (!acted.ok)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
/** The all-projects row's scope. */
|
|
3
|
+
export declare const ALL_PROJECTS = "*";
|
|
4
|
+
export type ChatApprovalLimits = {
|
|
5
|
+
fullAccess: boolean;
|
|
6
|
+
capMicrousd: number | null;
|
|
7
|
+
};
|
|
8
|
+
export type ChatApprovalSetting = ChatApprovalLimits & {
|
|
9
|
+
approver: string;
|
|
10
|
+
scope: string;
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
generation: number;
|
|
13
|
+
digest: string;
|
|
14
|
+
updatedBy: string;
|
|
15
|
+
updatedAt: string;
|
|
16
|
+
};
|
|
17
|
+
/** This person's rows: all projects first, then each project's. */
|
|
18
|
+
export declare function chatApprovalSettings(store: Store, approver: string): ChatApprovalSetting[];
|
|
19
|
+
/** The limits in plain words, said once: what a yes in chat may start. */
|
|
20
|
+
export declare function chatApprovalWords(limits: ChatApprovalLimits): string;
|
|
21
|
+
/** Whether this person's chat may approve on this project now, under their lasting setting: which row, and its limits. */
|
|
22
|
+
export declare function effectiveChatApproval(store: Store, repo: string, approver: string): {
|
|
23
|
+
ok: true;
|
|
24
|
+
setting: ChatApprovalSetting;
|
|
25
|
+
limits: ChatApprovalLimits;
|
|
26
|
+
} | {
|
|
27
|
+
ok: false;
|
|
28
|
+
why: string;
|
|
29
|
+
};
|
|
30
|
+
export type ChatApprovalChange = {
|
|
31
|
+
approver: string;
|
|
32
|
+
scope: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
limits?: ChatApprovalLimits;
|
|
35
|
+
via: string;
|
|
36
|
+
};
|
|
37
|
+
/** Save and ledger the setting; all-projects off also disables every saved project row for this owner.
|
|
38
|
+
* The caller has proved who is asking, and for "on" the password. */
|
|
39
|
+
export declare function setChatApproval(store: Store, change: ChatApprovalChange, now: Date): {
|
|
40
|
+
ok: true;
|
|
41
|
+
said: string;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
message: string;
|
|
45
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/** Approving from chat as a lasting owner setting. An approver turns it on for all their projects or for one, with the
|
|
2
|
+
* limits they agree to (whether a plan may ask for full access, and the most one attempt may cost); a project's own
|
|
3
|
+
* row outranks the all-projects one, and an explicit "off" on a project keeps it off there. Turning it off for all
|
|
4
|
+
* projects also disables that owner's saved project rows. It is off until turned on.
|
|
5
|
+
*
|
|
6
|
+
* It never lapses by date, but it ends with the account: a reset password, revoked access or a lost project ends it.
|
|
7
|
+
* Turning it on or changing its limits takes the password; turning it off is one step. Every change is a ledger line.
|
|
8
|
+
*
|
|
9
|
+
* What the setting allows is only that the owner's own two taps in their paired chat count as their yes. Every other
|
|
10
|
+
* rule still applies at the offer and again at the tap (chat-decide.ts planInChat): protected paths, separation of
|
|
11
|
+
* duties, organisation policy, written-for-you plans, unresolved routes, and the limits here. */
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
13
|
+
/** The all-projects row's scope. */
|
|
14
|
+
export const ALL_PROJECTS = "*";
|
|
15
|
+
const money = (micro) => `$${(micro / 1_000_000).toFixed(2)}`;
|
|
16
|
+
function digestOf(approver, scope, limits, generation) {
|
|
17
|
+
return createHash("sha256").update(JSON.stringify({ v: 1, approver, scope, fullAccess: limits.fullAccess, capMicrousd: limits.capMicrousd, generation })).digest("hex");
|
|
18
|
+
}
|
|
19
|
+
function readRow(row) {
|
|
20
|
+
return {
|
|
21
|
+
approver: String(row["approver"]), scope: String(row["scope"]), enabled: Number(row["enabled"]) === 1, fullAccess: Number(row["full_access"]) === 1,
|
|
22
|
+
capMicrousd: row["attempt_cap_microusd"] == null ? null : Number(row["attempt_cap_microusd"]), generation: Number(row["generation"]),
|
|
23
|
+
digest: String(row["digest"]), updatedBy: String(row["updated_by"]), updatedAt: String(row["updated_at"]),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** This person's rows: all projects first, then each project's. */
|
|
27
|
+
export function chatApprovalSettings(store, approver) {
|
|
28
|
+
return store.handle.prepare("SELECT * FROM chat_approval_setting WHERE approver = ? ORDER BY scope = '*' DESC, scope").all(approver).map(row => readRow(row));
|
|
29
|
+
}
|
|
30
|
+
/** The limits in plain words, said once: what a yes in chat may start. */
|
|
31
|
+
export function chatApprovalWords(limits) {
|
|
32
|
+
return `${limits.fullAccess ? "plans may ask for full access" : "plans that ask for full access open in Toolroll"} · ${limits.capMicrousd === null ? "any attempt limit" : `attempts up to ${money(limits.capMicrousd)}`}`;
|
|
33
|
+
}
|
|
34
|
+
/** Whether this person's chat may approve on this project now, under their lasting setting: which row, and its limits. */
|
|
35
|
+
export function effectiveChatApproval(store, repo, approver) {
|
|
36
|
+
const project = store.handle.prepare("SELECT * FROM chat_approval_setting WHERE approver = ? AND scope = ?").get(approver, repo);
|
|
37
|
+
const all = store.handle.prepare("SELECT * FROM chat_approval_setting WHERE approver = ? AND scope = ?").get(approver, ALL_PROJECTS);
|
|
38
|
+
const row = project ?? all;
|
|
39
|
+
const setting = row === undefined ? null : readRow(row);
|
|
40
|
+
if (setting === null || !setting.enabled)
|
|
41
|
+
return { ok: false, why: "Approving from chat isn't turned on for this project." };
|
|
42
|
+
const account = store.accountOf(approver);
|
|
43
|
+
if (account === null || account.revokedAt !== null || account.role !== "approver" || account.generation !== setting.generation || !store.accountCanAccess(approver, repo)) {
|
|
44
|
+
return { ok: false, why: "Your access changed since you turned on approving from chat. Turn it on again in Settings." };
|
|
45
|
+
}
|
|
46
|
+
if (setting.digest !== digestOf(setting.approver, setting.scope, setting, setting.generation))
|
|
47
|
+
return { ok: false, why: "Your chat approval setting can't be read. Turn it on again in Settings." };
|
|
48
|
+
return { ok: true, setting, limits: { fullAccess: setting.fullAccess, capMicrousd: setting.capMicrousd } };
|
|
49
|
+
}
|
|
50
|
+
/** Save and ledger the setting; all-projects off also disables every saved project row for this owner.
|
|
51
|
+
* The caller has proved who is asking, and for "on" the password. */
|
|
52
|
+
export function setChatApproval(store, change, now) {
|
|
53
|
+
const account = store.accountOf(change.approver);
|
|
54
|
+
if (account === null || account.revokedAt !== null || account.role !== "approver")
|
|
55
|
+
return { ok: false, message: "Only an approver can approve from chat." };
|
|
56
|
+
if (change.scope !== ALL_PROJECTS && !store.accountCanAccess(change.approver, change.scope))
|
|
57
|
+
return { ok: false, message: "That isn't one of your projects." };
|
|
58
|
+
const limits = change.limits ?? { fullAccess: false, capMicrousd: null };
|
|
59
|
+
if (limits.capMicrousd !== null && (!Number.isSafeInteger(limits.capMicrousd) || limits.capMicrousd < 0))
|
|
60
|
+
return { ok: false, message: "Give the attempt limit in dollars, like 5." };
|
|
61
|
+
const where = change.scope === ALL_PROJECTS ? "all your projects" : change.scope;
|
|
62
|
+
store.transact(() => {
|
|
63
|
+
const settings = [{ scope: change.scope, limits }];
|
|
64
|
+
if (change.scope === ALL_PROJECTS && !change.enabled) {
|
|
65
|
+
for (const setting of chatApprovalSettings(store, change.approver)) {
|
|
66
|
+
if (setting.scope !== ALL_PROJECTS)
|
|
67
|
+
settings.push({ scope: setting.scope, limits: setting });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const save = store.handle.prepare(`INSERT INTO chat_approval_setting (approver, scope, enabled, full_access, attempt_cap_microusd, generation, digest, updated_by, updated_at)
|
|
71
|
+
VALUES (?,?,?,?,?,?,?,?,?) ON CONFLICT (approver, scope) DO UPDATE SET enabled = excluded.enabled, full_access = excluded.full_access,
|
|
72
|
+
attempt_cap_microusd = excluded.attempt_cap_microusd, generation = excluded.generation, digest = excluded.digest, updated_by = excluded.updated_by, updated_at = excluded.updated_at`);
|
|
73
|
+
for (const { scope, limits } of settings) {
|
|
74
|
+
save.run(change.approver, scope, change.enabled ? 1 : 0, limits.fullAccess ? 1 : 0, limits.capMicrousd, account.generation, digestOf(change.approver, scope, limits, account.generation), change.approver, now.toISOString());
|
|
75
|
+
store.recordAction({ at: now.toISOString(), actor: change.approver, repo: scope === ALL_PROJECTS ? null : scope, taskId: null, runId: null,
|
|
76
|
+
action: "chat approval setting", outcome: change.enabled ? "on" : "off", source: "request",
|
|
77
|
+
detail: `${scope === ALL_PROJECTS ? "all projects" : "this project"}${change.enabled ? ` · ${chatApprovalWords(limits)}` : ""} · via ${change.via}` });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
return { ok: true, said: change.enabled ? `Approving from chat is on for ${where}: ${chatApprovalWords(limits)}.` : `Approving from chat is off for ${where}.` };
|
|
81
|
+
}
|
package/dist/chat-channel.js
CHANGED
|
@@ -556,6 +556,11 @@ export const CHAT_ACTION_PARITY = {
|
|
|
556
556
|
how: "Shared read-only acceptance packet: exact result, criterion states, gate, reviewer findings, caveats and recorded human acceptance. Screenshot files use get_result_images; marking complete is a propose_action result_accept confirmed behind the phone's own yes/cancel challenge or on the signed-in console.",
|
|
557
557
|
gap: "A physical-phone completion has not been exercised.",
|
|
558
558
|
},
|
|
559
|
+
offer_approval: {
|
|
560
|
+
support: "direct",
|
|
561
|
+
how: "Pushes the plan's or flow card's own decision card to the owner's paired chat: Approve arms Yes and Cancel, and only the owner's Yes decides, through sealScopeApproval (as a chat-origin approval) or decideFlowCard. Outside the owner's chat-approval setting or a decision's rules it answers why, and the lead opens the console control.",
|
|
562
|
+
gap: "A flow card's Send back, Edit and a Person chooses step act from the pushed card; a plan's Send back stays in the console. Fixture proof only, no physical-phone rendering.",
|
|
563
|
+
},
|
|
559
564
|
show_control: {
|
|
560
565
|
support: "handoff",
|
|
561
566
|
how: "The card names the control and the task, with one url button to that exact console control when a trusted https console-url is configured; the button opens the signed-in console and acts on nothing.",
|
package/dist/chat-decide.d.ts
CHANGED
|
@@ -4,14 +4,15 @@
|
|
|
4
4
|
* and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
|
|
5
5
|
* newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
|
|
6
6
|
*
|
|
7
|
-
* Approving a plan or merging needs the password, which never goes in chat, so they act in chat only
|
|
8
|
-
*
|
|
9
|
-
* that widens permissions, exceeds the
|
|
7
|
+
* Approving a plan or merging needs the password, which never goes in chat, so they act in chat only where the owner
|
|
8
|
+
* turned approving from chat on (their lasting setting, chat-approval.ts, or a signed mode's chatApprove term), for
|
|
9
|
+
* their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
|
|
10
|
+
* card keeps its link.
|
|
10
11
|
*
|
|
11
12
|
* Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
|
|
12
13
|
* tokens (the `channel` column). Every act runs through the door the console uses. */
|
|
13
14
|
import { DECIDE_ACTS } from "./contracts/chat-callback-rows.js";
|
|
14
|
-
import { type
|
|
15
|
+
import { type ChatApprovalLimits } from "./chat-approval.js";
|
|
15
16
|
import { type VerifiedApprover } from "./principal.js";
|
|
16
17
|
import type { Scope } from "./scope.js";
|
|
17
18
|
import type { Store } from "./store.js";
|
|
@@ -77,11 +78,14 @@ export type DecideOffer = {
|
|
|
77
78
|
text: string | null;
|
|
78
79
|
rows: Array<Array<ActSpec | DecideLink>>;
|
|
79
80
|
};
|
|
80
|
-
/**
|
|
81
|
+
/** What lets this person's chat approve on this repository now: their lasting chat-approval setting (chat-approval.ts),
|
|
82
|
+
* or else a live signed mode carrying the chatApprove term, signed by them. `digest` names that authority for the
|
|
83
|
+
* ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
|
|
81
84
|
export declare function chatApproveMode(store: Store, repo: string | null, approver: string, now: Date): {
|
|
82
85
|
ok: true;
|
|
83
86
|
digest: string;
|
|
84
|
-
|
|
87
|
+
limits: ChatApprovalLimits;
|
|
88
|
+
source: "setting" | "mode";
|
|
85
89
|
} | {
|
|
86
90
|
ok: false;
|
|
87
91
|
why: string;
|
|
@@ -93,6 +97,7 @@ export declare function permissionPlainWords(profile: Scope["profile"] | null |
|
|
|
93
97
|
export declare function planInChat(store: Store, taskId: string, approver: string, now: Date, root?: string): {
|
|
94
98
|
ok: true;
|
|
95
99
|
modeDigest: string;
|
|
100
|
+
source: "setting" | "mode";
|
|
96
101
|
digest: string;
|
|
97
102
|
scope: Scope;
|
|
98
103
|
} | {
|
|
@@ -103,6 +108,7 @@ export declare function planInChat(store: Store, taskId: string, approver: strin
|
|
|
103
108
|
export declare function mergeInChat(store: Store, taskId: string, run: number, approver: string, now: Date): {
|
|
104
109
|
ok: true;
|
|
105
110
|
modeDigest: string;
|
|
111
|
+
source: "setting" | "mode";
|
|
106
112
|
head: string;
|
|
107
113
|
} | {
|
|
108
114
|
ok: false;
|
|
@@ -160,6 +166,12 @@ export type ChatMerge = {
|
|
|
160
166
|
};
|
|
161
167
|
/** The links a card keeps once its acts are spent or stale: where to look, never a token. */
|
|
162
168
|
export declare function linksFor(target: DecideTarget): DecideButton[][];
|
|
169
|
+
/** Where a plan card's task is now, for a card whose buttons no longer act: the family's current version, named only
|
|
170
|
+
* inside this person's projects, and linked only while it still waits on them. Never a live button. */
|
|
171
|
+
export declare function planWhereNow(store: Store, taskId: string, who: VerifiedApprover, now: Date, root?: string): {
|
|
172
|
+
said: string;
|
|
173
|
+
rows: DecideButton[][];
|
|
174
|
+
};
|
|
163
175
|
/**
|
|
164
176
|
* One tap on a decide button, inside the caller's transaction. Null: not this module's token. The seat must be the
|
|
165
177
|
* token's own (person, chat, message); a spent, expired or foreign button acts on nothing.
|
package/dist/chat-decide.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* and each button is one opaque token bound to the exact result, receipt, plan or commit the card showed: a card a
|
|
5
5
|
* newer result, a finished task or a changed plan has overtaken says so and acts on nothing.
|
|
6
6
|
*
|
|
7
|
-
* Approving a plan or merging needs the password, which never goes in chat, so they act in chat only
|
|
8
|
-
*
|
|
9
|
-
* that widens permissions, exceeds the
|
|
7
|
+
* Approving a plan or merging needs the password, which never goes in chat, so they act in chat only where the owner
|
|
8
|
+
* turned approving from chat on (their lasting setting, chat-approval.ts, or a signed mode's chatApprove term), for
|
|
9
|
+
* their own chat; otherwise, and for a plan that widens permissions, exceeds the limit or touches protected paths, the
|
|
10
|
+
* card keeps its link.
|
|
10
11
|
*
|
|
11
12
|
* Channel-agnostic: Telegram renders these today, and Slack, Discord and Teams reach the same acts through the same
|
|
12
13
|
* tokens (the `channel` column). Every act runs through the door the console uses. */
|
|
@@ -17,6 +18,7 @@ import { chatControlHref, chatResultHref } from "./chat-controls.js";
|
|
|
17
18
|
import { applyChatTaskAction, chatTaskStamp } from "./chat-task-actions.js";
|
|
18
19
|
import { chatTitle } from "./chat-voice.js";
|
|
19
20
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
21
|
+
import { effectiveChatApproval } from "./chat-approval.js";
|
|
20
22
|
import { modeTermsFromJson } from "./modes.js";
|
|
21
23
|
import { parseExecutionPlanDocument } from "./plan.js";
|
|
22
24
|
import { verifyApproverStanding } from "./principal.js";
|
|
@@ -45,15 +47,22 @@ const sha = (text) => createHash("sha256").update(text).digest("hex");
|
|
|
45
47
|
const isAct = (one) => "act" in one;
|
|
46
48
|
const title = (store, taskId) => phoneText(chatTitle(store, taskId), 60);
|
|
47
49
|
// ---- the chatApprove term ----------------------------------------------------------
|
|
48
|
-
/**
|
|
50
|
+
/** What lets this person's chat approve on this repository now: their lasting chat-approval setting (chat-approval.ts),
|
|
51
|
+
* or else a live signed mode carrying the chatApprove term, signed by them. `digest` names that authority for the
|
|
52
|
+
* ledger; the yes itself is the person's tap and is sealed as theirs, so it doesn't lapse when the authority does. */
|
|
49
53
|
export function chatApproveMode(store, repo, approver, now) {
|
|
50
|
-
|
|
54
|
+
if (repo === null)
|
|
55
|
+
return { ok: false, why: "Approving from chat isn't turned on for this project." };
|
|
56
|
+
const setting = effectiveChatApproval(store, repo, approver);
|
|
57
|
+
if (setting.ok)
|
|
58
|
+
return { ok: true, digest: setting.setting.digest, limits: setting.limits, source: "setting" };
|
|
59
|
+
const mode = store.activeMode(repo, now);
|
|
51
60
|
const terms = mode === null ? null : modeTermsFromJson(mode.termsJson);
|
|
52
61
|
if (mode === null || terms === null || !terms.chatApprove)
|
|
53
|
-
return
|
|
62
|
+
return setting;
|
|
54
63
|
if (mode.signedBy !== approver || !store.accountCanAccess(approver, repo))
|
|
55
64
|
return { ok: false, why: "Only the person who signed this project's mode can approve from chat." };
|
|
56
|
-
return { ok: true, digest: mode.digest, terms };
|
|
65
|
+
return { ok: true, digest: mode.digest, limits: { fullAccess: terms.permissionDefault === "escalated", capMicrousd: terms.perAttemptBudgetMicrousd }, source: "mode" };
|
|
57
66
|
}
|
|
58
67
|
/** What a plan's yes allows an agent to do, in a few plain words (the approval sheet's "You're allowing"). */
|
|
59
68
|
export function permissionPlainWords(profile) {
|
|
@@ -102,11 +111,12 @@ export function planInChat(store, taskId, approver, now, root) {
|
|
|
102
111
|
return { ok: false, why: "Agents compete on this task, so you approve it in Toolroll." };
|
|
103
112
|
if (scope.profileState === "unresolved" || scope.routeEra == null || (scope.termsProblem ?? null) !== null)
|
|
104
113
|
return { ok: false, why: "This plan can't say exactly which agents run, so you approve it in Toolroll." };
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
114
|
+
const yours = mode.source === "setting" ? "your chat approval setting" : "your mode";
|
|
115
|
+
if (fullAccess(scope.profile) && !mode.limits.fullAccess)
|
|
116
|
+
return { ok: false, why: `This plan asks for more access than ${yours} allows.` };
|
|
117
|
+
const cap = mode.limits.capMicrousd;
|
|
108
118
|
if (cap !== null && (scope.budgetMicrousd === null || scope.budgetMicrousd > cap))
|
|
109
|
-
return { ok: false, why: `This plan has ${scope.budgetMicrousd === null ? "no attempt limit" : `a ${money(scope.budgetMicrousd)} attempt limit`}, more than your mode's ${money(cap)}.` };
|
|
119
|
+
return { ok: false, why: `This plan has ${scope.budgetMicrousd === null ? "no attempt limit" : `a ${money(scope.budgetMicrousd)} attempt limit`}, more than ${yours === "your mode" ? "your mode's" : "your chat approval limit of"} ${money(cap)}.` };
|
|
110
120
|
if (store.approvalGate(taskId, approver, "mode").verdict !== "seal")
|
|
111
121
|
return { ok: false, why: "This plan touches protected work, so two people approve it in Toolroll." };
|
|
112
122
|
if (store.scopePolicyRefusal(taskId) !== null)
|
|
@@ -137,7 +147,7 @@ export function planInChat(store, taskId, approver, now, root) {
|
|
|
137
147
|
if (!verified)
|
|
138
148
|
return { ok: false, why: "The saved plan can't be verified here, so you approve it in Toolroll." };
|
|
139
149
|
}
|
|
140
|
-
return { ok: true, modeDigest: mode.digest, scope, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}`) };
|
|
150
|
+
return { ok: true, modeDigest: mode.digest, source: mode.source, scope, digest: sha(`${scope.digest}:${plan?.sha256 ?? ""}`) };
|
|
141
151
|
}
|
|
142
152
|
/** A ready pull request this person's chat may merge now, bound to the exact commit. */
|
|
143
153
|
export function mergeInChat(store, taskId, run, approver, now) {
|
|
@@ -153,7 +163,7 @@ export function mergeInChat(store, taskId, run, approver, now) {
|
|
|
153
163
|
const mode = chatApproveMode(store, ref.repo, approver, now);
|
|
154
164
|
if (!mode.ok)
|
|
155
165
|
return mode;
|
|
156
|
-
return { ok: true, modeDigest: mode.digest, head: publication.headSha };
|
|
166
|
+
return { ok: true, modeDigest: mode.digest, source: mode.source, head: publication.headSha };
|
|
157
167
|
}
|
|
158
168
|
/** The plan card: title, goal in two lines, what changes, done when, and what the yes allows. On Telegram the yes's
|
|
159
169
|
* terms come second, under the title, and the steps are fewer: the full plan is behind Edit. */
|
|
@@ -380,6 +390,27 @@ export function linksFor(target) {
|
|
|
380
390
|
case "failed": return [[{ label: "Open task", link: { label: "Open task", path: chatControlHref("task", target.taskId) } }]];
|
|
381
391
|
}
|
|
382
392
|
}
|
|
393
|
+
/** Where a plan card's task is now, for a card whose buttons no longer act: the family's current version, named only
|
|
394
|
+
* inside this person's projects, and linked only while it still waits on them. Never a live button. */
|
|
395
|
+
export function planWhereNow(store, taskId, who, now, root) {
|
|
396
|
+
const family = store.taskFamilyOf(taskId, who.repos, false);
|
|
397
|
+
if (family === null)
|
|
398
|
+
return { said: "It has moved on.", rows: [] };
|
|
399
|
+
const current = family.current.id;
|
|
400
|
+
const name = title(store, current);
|
|
401
|
+
const state = store.getTask(current)?.state;
|
|
402
|
+
if (state === "done" || state === "cancelled") {
|
|
403
|
+
const assignment = assignmentOf(store, current, now, { principal: "operator", repos: who.repos }, root);
|
|
404
|
+
if (assignment?.state !== "ready-to-check")
|
|
405
|
+
return { said: state === "cancelled" ? `"${name}" was cancelled.` : `"${name}" is finished.`, rows: [] };
|
|
406
|
+
return { said: `"${name}" has a result waiting for you.`, rows: [[{ label: "Open the task", link: { label: "Open the task", path: chatControlHref("task", current) } }]] };
|
|
407
|
+
}
|
|
408
|
+
const scope = store.getScope(current);
|
|
409
|
+
if (scope !== null && !(scope.approvedDigest != null && scope.approvedDigest === scope.digest) && store.lookupRef(current)?.plan !== "requested") {
|
|
410
|
+
return { said: `"${name}" waits for your approval${current === taskId ? "" : " (a newer version)"}.`, rows: [[{ label: "Review & start", link: { label: "Review & start", path: chatControlHref("approval", current) } }]] };
|
|
411
|
+
}
|
|
412
|
+
return { said: `"${name}" is under way.`, rows: [] };
|
|
413
|
+
}
|
|
383
414
|
const QUESTION_STARTS = ["Accept and finish \"", "Retry \"", "Approve and start \"", "Merge \""];
|
|
384
415
|
/** The card's words without a question this module appended. */
|
|
385
416
|
function cardBody(shown) {
|
|
@@ -417,14 +448,16 @@ export function applyDecideTap(store, seat, input) {
|
|
|
417
448
|
store.handle.prepare("UPDATE chat_decide_action SET consumed_at = ? WHERE token = ? AND consumed_at IS NULL").run(now.toISOString(), row.token);
|
|
418
449
|
retireDecideTokens(store, seat.channel, seat.chat, input.message, now);
|
|
419
450
|
};
|
|
451
|
+
// A plan's card names where its task is now (its newest version, or finished), and links it only while it waits.
|
|
452
|
+
const here = target.kind === "plan" ? planWhereNow(store, row.taskId, who, now, input.root) : null;
|
|
420
453
|
const stale = (why) => {
|
|
421
454
|
spend();
|
|
422
|
-
return { ack: why, edit: { text: `${body}\n\n${why} Nothing was done
|
|
455
|
+
return { ack: why, edit: { text: `${body}\n\n${why} Nothing was done.${here === null || here.rows.length > 0 && why.startsWith("The plan changed") ? "" : ` ${here.said}`}`, rows: here?.rows ?? linksFor(target), tokens: [] } };
|
|
423
456
|
};
|
|
424
|
-
// An expired button: the card says so and
|
|
457
|
+
// An expired button: the card says so and where the work is now, so it is never a dead end; the old button never acts.
|
|
425
458
|
if (row.expiresAt <= now.toISOString()) {
|
|
426
459
|
spend();
|
|
427
|
-
return { ack: "That button expired.", edit: { text: `${body}\n\nThat button expired. Nothing was done
|
|
460
|
+
return { ack: "That button expired.", edit: { text: `${body}\n\nThat button expired. Nothing was done.${here === null ? "" : ` ${here.said}`}`, rows: here?.rows ?? linksFor(target), tokens: [] } };
|
|
428
461
|
}
|
|
429
462
|
if (row.phase === "cancel") {
|
|
430
463
|
spend();
|
|
@@ -475,17 +508,18 @@ export function applyDecideTap(store, seat, input) {
|
|
|
475
508
|
const plan = planInChat(store, row.taskId, who.name, now, input.root);
|
|
476
509
|
if (!plan.ok)
|
|
477
510
|
return notDone(plan.why);
|
|
478
|
-
|
|
511
|
+
// The owner's own yes, sealed as theirs ("chat"), never as a mode's: it stands like the console's after the mode or setting ends.
|
|
512
|
+
if (!store.sealScopeApproval(row.taskId, who.name, now, {}, { kind: "chat", modeDigest: plan.modeDigest }))
|
|
479
513
|
return notDone("the plan couldn't be approved from chat. Open it in Toolroll.");
|
|
480
514
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: null, action: "plan approved in chat", outcome: "approved",
|
|
481
|
-
source: "request", detail: `via ${seat.channel} · chat binding #${seat.binding} ·
|
|
482
|
-
return done("✓ Approved
|
|
515
|
+
source: "request", detail: `via ${seat.channel} · chat binding #${seat.binding} · ${plan.source} ${plan.modeDigest}` });
|
|
516
|
+
return done("✓ Approved. Work starts when a worker is free.");
|
|
483
517
|
}
|
|
484
518
|
case "merge": {
|
|
485
519
|
const merge = mergeInChat(store, row.taskId, row.run ?? 0, who.name, now);
|
|
486
520
|
if (!merge.ok)
|
|
487
521
|
return notDone(merge.why);
|
|
488
|
-
const via = `via ${seat.channel} · chat binding #${seat.binding} ·
|
|
522
|
+
const via = `via ${seat.channel} · chat binding #${seat.binding} · ${merge.source} ${merge.modeDigest} · ${merge.head.slice(0, 12)}`;
|
|
489
523
|
// The approval is ledgered with the tap, so a bridge that stops before GitHub answers still leaves it on record;
|
|
490
524
|
// what GitHub then did is its own line (recordChatMerge).
|
|
491
525
|
store.recordAction({ at: now.toISOString(), actor: who.name, repo: ref.repo, taskId: row.taskId, runId: row.run ?? null, action: "merge approved in chat", outcome: "approved",
|
package/dist/chat-flow.js
CHANGED
|
@@ -190,7 +190,7 @@ export function applyChatFlowTap(options, event, binding, token, repos, now) {
|
|
|
190
190
|
}
|
|
191
191
|
const kind = String(action["action"]);
|
|
192
192
|
if (kind === "approve") {
|
|
193
|
-
const decided = decideFlowCard(store, { card, decision: "approve", note: null, actor: binding.approver, repos, entry }, now);
|
|
193
|
+
const decided = decideFlowCard(store, { card, decision: "approve", note: null, actor: binding.approver, repos, entry, where: options.label }, now);
|
|
194
194
|
if (!decided.ok) {
|
|
195
195
|
say(decided.message);
|
|
196
196
|
return true;
|
|
@@ -441,7 +441,7 @@ export function answerChatFlowPrompt(options, event, binding, input, repos, now)
|
|
|
441
441
|
state.plan(event.id, decisionParts(`${waiting.flow.name}: your version of the draft for “${waiting.card.title}”\n\n${kept}\n\nApprove to send it as written.`, { card, entry, actions: actionsFor(fresh) }, { label: "Open", path: flowCardHref(waiting.flow.id, card) }), now);
|
|
442
442
|
return true;
|
|
443
443
|
}
|
|
444
|
-
const decided = decideFlowCard(store, { card, decision: "send-back", note: said, actor: binding.approver, repos, entry }, now);
|
|
444
|
+
const decided = decideFlowCard(store, { card, decision: "send-back", note: said, actor: binding.approver, repos, entry, where: options.label }, now);
|
|
445
445
|
if (!decided.ok) {
|
|
446
446
|
close();
|
|
447
447
|
say(decided.message);
|
package/dist/cli.js
CHANGED
|
@@ -253,7 +253,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
253
253
|
"run",
|
|
254
254
|
"routine",
|
|
255
255
|
"flows",
|
|
256
|
-
"config", "chat", "proposals", "mode", "people", "keys",
|
|
256
|
+
"config", "chat", "proposals", "mode", "chat-approval", "people", "keys",
|
|
257
257
|
"setup",
|
|
258
258
|
"verify",
|
|
259
259
|
"intake",
|
|
@@ -102,6 +102,7 @@ export declare const CONSOLE_FORMS: {
|
|
|
102
102
|
readonly learningChange: FormContract<"action" | "identity" | "lesson" | "repo" | "revision" | "sha" | "version">;
|
|
103
103
|
readonly projectSetup: FormContract<"command" | "fingerprint" | "model" | "nonce" | "provider" | "repo" | "seconds" | "token">;
|
|
104
104
|
readonly slack: FormContract<"app-token" | "bot-token" | "password">;
|
|
105
|
+
readonly chatApproval: FormContract<"cap-usd" | "digest" | "full-access" | "nonce" | "scope" | "token">;
|
|
105
106
|
readonly telegramPair: FormContract<"password">;
|
|
106
107
|
readonly teams: FormContract<"app-id" | "password" | "secret" | "tenant">;
|
|
107
108
|
readonly discord: FormContract<"bot-token" | "password">;
|
|
@@ -111,6 +111,7 @@ export const CONSOLE_FORMS = {
|
|
|
111
111
|
learningChange: formContract("POST /settings/learning/change", ["repo", "action", "identity", "revision", "lesson", "version", "sha"]),
|
|
112
112
|
projectSetup: formContract("POST /control/setup-preview, /control/setup-approve, /control/instructions-preview, /control/instructions-approve", ["repo", "nonce", "fingerprint", "token", "provider", "model", "command", "seconds"]),
|
|
113
113
|
slack: formContract("POST /settings/slack/<connect|pair|unpair|disconnect|alerts>", ["password", "app-token", "bot-token"]),
|
|
114
|
+
chatApproval: formContract("POST /settings/chat-approval/confirm, /settings/chat-approval/save, /settings/chat-approval/off", ["scope", "full-access", "cap-usd", "nonce", "digest", "token"]),
|
|
114
115
|
telegramPair: formContract("POST /settings/telegram/pair, /settings/telegram/unpair", ["password"]),
|
|
115
116
|
teams: formContract("POST /settings/teams/<connect|pair|unpair|disconnect|alerts>", ["password", "app-id", "tenant", "secret"]),
|
|
116
117
|
discord: formContract("POST /settings/discord/<connect|pair|unpair|disconnect|alerts>", ["password", "bot-token"]),
|
|
@@ -88,6 +88,10 @@ export declare const LEAD_TOOL_INPUTS: {
|
|
|
88
88
|
repo: z.ZodOptional<z.ZodString>;
|
|
89
89
|
run: z.ZodOptional<z.ZodInt>;
|
|
90
90
|
}, z.core.$strict>;
|
|
91
|
+
readonly offer_approval: z.ZodObject<{
|
|
92
|
+
task: z.ZodOptional<z.ZodString>;
|
|
93
|
+
card: z.ZodOptional<z.ZodInt>;
|
|
94
|
+
}, z.core.$strict>;
|
|
91
95
|
readonly get_result: z.ZodObject<{
|
|
92
96
|
task: z.ZodString;
|
|
93
97
|
run: z.ZodOptional<z.ZodInt>;
|
|
@@ -777,6 +781,10 @@ export declare const LEAD_TOOL_READ_INPUTS: {
|
|
|
777
781
|
repo: z.ZodOptional<z.ZodString>;
|
|
778
782
|
run: z.ZodOptional<z.ZodInt>;
|
|
779
783
|
}, z.core.$strict>;
|
|
784
|
+
readonly offer_approval: z.ZodObject<{
|
|
785
|
+
task: z.ZodOptional<z.ZodString>;
|
|
786
|
+
card: z.ZodOptional<z.ZodInt>;
|
|
787
|
+
}, z.core.$strict>;
|
|
780
788
|
readonly get_result: z.ZodObject<{
|
|
781
789
|
task: z.ZodString;
|
|
782
790
|
run: z.ZodOptional<z.ZodInt>;
|
|
@@ -1535,6 +1543,9 @@ export declare const LEAD_TOOL_OUTPUTS: {
|
|
|
1535
1543
|
label: z.ZodString;
|
|
1536
1544
|
action: z.ZodString;
|
|
1537
1545
|
}, z.core.$loose>;
|
|
1546
|
+
readonly offer_approval: z.ZodObject<{
|
|
1547
|
+
offered: z.ZodBoolean;
|
|
1548
|
+
}, z.core.$loose>;
|
|
1538
1549
|
readonly get_result: z.ZodObject<{
|
|
1539
1550
|
task: z.ZodString;
|
|
1540
1551
|
root: z.ZodString;
|
|
@@ -65,6 +65,7 @@ export const LEAD_TOOL_INPUTS = {
|
|
|
65
65
|
propose_task_action: z.strictObject({ task: taskRef, operation: enumOf(Object.keys(CHAT_TASK_ACTIONS)), dependency: taskRef.optional(), run: id.optional() }),
|
|
66
66
|
get_controls: z.strictObject({}),
|
|
67
67
|
show_control: z.strictObject({ control: enumOf(Object.keys(CHAT_CONTROLS)), task: taskRef.optional(), repo: repoId.optional(), run: id.optional() }),
|
|
68
|
+
offer_approval: z.strictObject({ task: taskRef.optional(), card: id.optional() }),
|
|
68
69
|
get_result: z.strictObject({ task: taskRef, run: id.optional(), feedback_offset: offset.optional() }),
|
|
69
70
|
get_diff: z.strictObject({ task: taskRef, run: id.optional(), file: text("diffPath").min(1).optional(), offset: offset.optional() }),
|
|
70
71
|
get_check_log: z.strictObject({ task: taskRef, run: id.optional(), search: text("logSearch").min(2).optional(), offset: offset.optional() }),
|
|
@@ -189,6 +190,7 @@ export const LEAD_TOOL_OUTPUTS = {
|
|
|
189
190
|
propose_task_action: z.looseObject({ proposal: int, action: str, awaiting: str }),
|
|
190
191
|
get_controls: z.looseObject({ confirmedInChat: z.array(str), existingControls: rows({ id: str, label: str, needsTask: z.boolean(), needsProject: z.boolean() }), rule: str }),
|
|
191
192
|
show_control: z.looseObject({ card: int, label: str, action: str }),
|
|
193
|
+
offer_approval: z.looseObject({ offered: z.boolean() }),
|
|
192
194
|
get_result: z.looseObject({
|
|
193
195
|
task: str, root: str, currentExecution: str, run: int, title: str, feedback: list, feedbackTotal: int, nextFeedbackOffset: nullable(int),
|
|
194
196
|
changes: str, changesShortened: z.boolean(), verification: str, accepted: z.boolean(), evidenceTool: str, canRevise: z.boolean(),
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -74,4 +74,6 @@ export declare function decideFlowCard(store: Store, input: {
|
|
|
74
74
|
entry?: number;
|
|
75
75
|
/** v92: the AI teammate (by handle) deciding a zone it staffs; `actor` is then how it reads in history. */
|
|
76
76
|
teammate?: string;
|
|
77
|
+
/** Where the decision was made, for its ledger line: "the console", "the command line", "Telegram". */
|
|
78
|
+
where?: string;
|
|
77
79
|
}, now: Date): FlowDecision;
|
package/dist/flow-engine.js
CHANGED
|
@@ -485,6 +485,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
485
485
|
// v102: a protected project's decisions are a person's, never an AI teammate's.
|
|
486
486
|
if (input.teammate !== undefined && store.approvalRules(flow.repo).protectProject)
|
|
487
487
|
return { ok: false, message: "This project is protected: a person decides here." };
|
|
488
|
+
// Every decision is one ledger line, the same wherever it was made; only `via` says where.
|
|
489
|
+
const ledger = (outcome) => store.recordAction({ at: now.toISOString(), actor: input.actor, repo: flow.repo, taskId: card.primaryTask, runId: null,
|
|
490
|
+
action: "flow decision", outcome, source: "request", detail: `${flow.name} · card ${card.id} · ${stage.title}${input.teammate === undefined ? "" : ` · teammate ${input.teammate}`} · via ${input.where ?? "Toolroll"}` });
|
|
488
491
|
if (input.decision === "approve") {
|
|
489
492
|
// An edited draft replaces the one Claude wrote, so the steps after this send what the person approved.
|
|
490
493
|
const draft = draftFor(definition, stage);
|
|
@@ -498,9 +501,12 @@ export function decideFlowCard(store, input, now) {
|
|
|
498
501
|
}
|
|
499
502
|
if (stage.next === null) {
|
|
500
503
|
store.updateFlowCard(card.id, { state: "done", waiting: null }, now);
|
|
504
|
+
ledger("approved");
|
|
501
505
|
return { ok: true, said: "Approved. The card is done." };
|
|
502
506
|
}
|
|
503
|
-
store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now)
|
|
507
|
+
if (!store.moveFlowCard(card.id, { to: stage.next, outcome: "approved", actor: input.actor, note: input.note, expectEntry: card.entry }, now))
|
|
508
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
509
|
+
ledger("approved");
|
|
504
510
|
const title = definition.stages.find(one => one.id === stage.next)?.title ?? stage.next;
|
|
505
511
|
if (card.owner !== null)
|
|
506
512
|
notifyPeople(store, card, [card.owner], input.actor, { key: `approved:${card.entry}`, subject: `${input.actor} approved “${card.title}”`, body: `${stage.title}: approved. It moves to ${title}.${input.note === null || input.note.trim() === "" ? "" : `\n\n${input.note.trim()}`}` }, now);
|
|
@@ -512,7 +518,9 @@ export function decideFlowCard(store, input, now) {
|
|
|
512
518
|
return { ok: false, message: "Say what should change." };
|
|
513
519
|
const target = definition.stages.find(one => one.id === stage.onFail);
|
|
514
520
|
const revision = revisionWithNote(store, card, target, input.note.trim(), input.actor, input.repos, input.evidenceRoot, now);
|
|
515
|
-
store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now)
|
|
521
|
+
if (!store.moveFlowCard(card.id, { to: stage.onFail, outcome: "sent-back", actor: input.actor, note: input.note.trim(), ...(revision === null ? {} : { task: revision }), expectEntry: card.entry }, now))
|
|
522
|
+
return { ok: false, message: "That card has moved on since; nothing was changed." };
|
|
523
|
+
ledger("sent-back");
|
|
516
524
|
if (card.owner !== null)
|
|
517
525
|
notifyPeople(store, card, [card.owner], input.actor, { key: `sent-back:${card.entry}`, subject: `${input.actor} sent “${card.title}” back`, body: `Sent back to ${target?.title ?? stage.onFail}:\n\n${input.note.trim()}`, attention: true }, now);
|
|
518
526
|
return { ok: true, said: revision === null ? `Sent back to ${target?.title ?? stage.onFail} with your note.` : `Sent back to ${target?.title ?? stage.onFail}: a revision was made with your note.` };
|
package/dist/flows-cli.d.ts
CHANGED
|
@@ -188,6 +188,34 @@ export declare const FLOWS_DESCRIPTORS: readonly [{
|
|
|
188
188
|
readonly takesValue: true;
|
|
189
189
|
readonly meaning: 'a zone id or name';
|
|
190
190
|
}];
|
|
191
|
+
}, {
|
|
192
|
+
readonly action: 'card approve';
|
|
193
|
+
readonly synopsis: "approve a card waiting at a Person decides step (or pick --option at a Person chooses step), as the console's Approve does";
|
|
194
|
+
readonly mutation: 'identity-idempotent';
|
|
195
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
196
|
+
readonly flags: readonly [...{
|
|
197
|
+
name: string;
|
|
198
|
+
takesValue: boolean;
|
|
199
|
+
meaning: string;
|
|
200
|
+
}[], {
|
|
201
|
+
readonly name: 'option';
|
|
202
|
+
readonly takesValue: true;
|
|
203
|
+
readonly meaning: "at a Person chooses step: the option's words or its number";
|
|
204
|
+
}];
|
|
205
|
+
}, {
|
|
206
|
+
readonly action: 'card send-back';
|
|
207
|
+
readonly synopsis: "send a waiting card back with a note, as the console's Send back (or a reply at a Person chooses step) does";
|
|
208
|
+
readonly mutation: 'identity-idempotent';
|
|
209
|
+
readonly positionals: readonly ['flow', 'card'];
|
|
210
|
+
readonly flags: readonly [...{
|
|
211
|
+
name: string;
|
|
212
|
+
takesValue: boolean;
|
|
213
|
+
meaning: string;
|
|
214
|
+
}[], {
|
|
215
|
+
readonly name: 'note';
|
|
216
|
+
readonly takesValue: true;
|
|
217
|
+
readonly meaning: 'what should change';
|
|
218
|
+
}];
|
|
191
219
|
}, {
|
|
192
220
|
readonly action: 'export';
|
|
193
221
|
readonly synopsis: 'write a flow as a *.toolroll-flow.json file: zones, paths, trigger settings and scripts; never secrets, webhook addresses, names or cards';
|