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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
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
|
+
|
|
22
|
+
## 0.9.42 — 2026-10-06
|
|
23
|
+
|
|
24
|
+
- **Zod revamp, waves 3 and 4.** What comes into Toolroll from outside and
|
|
25
|
+
what it reads back from disk now goes through one schema each in
|
|
26
|
+
`src/contracts/`: console forms and JSON, CLI `--json` output, webhook,
|
|
27
|
+
GitHub, Linear, email and form payloads, OAuth replies and tool specs,
|
|
28
|
+
settings, every JSON column in the database, update and recovery journals,
|
|
29
|
+
and evidence files. All of it is read as leniently as before. Replayed
|
|
30
|
+
against real data with 0 differences: 655 runs and 984 evidence folders,
|
|
31
|
+
5,158 database rows, 7 tool connections. Console JSON and CLI `--json` bytes
|
|
32
|
+
are unchanged. Console API and CLI input are partly done; what remains is
|
|
33
|
+
listed in `docs/plans/zod-revamp.md`.
|
|
34
|
+
- **Changed on purpose.** Recipe refusals start with the field's path. An
|
|
35
|
+
unknown provider is refused as `provider: unknown provider`, without
|
|
36
|
+
repeating what was typed. A sign-in that answers with an empty token says
|
|
37
|
+
it couldn't be reached instead of crashing. An update journal missing its
|
|
38
|
+
core fields (no release since 0.8.0 writes one) is refused rather than read.
|
|
39
|
+
|
|
3
40
|
## 0.9.41 — 2026-10-06
|
|
4
41
|
|
|
5
42
|
- **A read-only PostHog connection works for research.** PostHog adds
|
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
|
@@ -17,7 +17,9 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
17
17
|
import { configPath, loadRepos } from "./repos.js";
|
|
18
18
|
import { addedWords, admitProject, commandLineActor, consoleRunning, proveProjectRoot, registryBeside, releaseProject } from "./project-admission.js";
|
|
19
19
|
import { envValue } from "./names.js";
|
|
20
|
-
import { CAPABILITIES, ENVELOPE_VERSION, capturedEnvelope,
|
|
20
|
+
import { CAPABILITIES, ENVELOPE_VERSION, capturedEnvelope, onEnvelopeCaptured, resetCapturedEnvelope } from "./envelope.js";
|
|
21
|
+
// Every envelope is checked against its command's schema (logged, never refused), then serialized unchanged.
|
|
22
|
+
import { checkedEnvelopeJson as envelopeJson } from "./contracts/cli.js";
|
|
21
23
|
import { applyClaudeCodeInstall, applyInstall, contextBlock, defaultClaudeCodeSkillDir, planClaudeCodeInstall, planInstall, } from "./skills.js";
|
|
22
24
|
import { GUIDES, guideNamed } from "./guides.js";
|
|
23
25
|
import { parseGithubRepo, previewGithubRepo, cloneGithubRepo, isLargeRepo } from "./onboard.js";
|
|
@@ -251,7 +253,7 @@ export const OPERATE_COMMANDS = new Set([
|
|
|
251
253
|
"run",
|
|
252
254
|
"routine",
|
|
253
255
|
"flows",
|
|
254
|
-
"config", "chat", "proposals", "mode", "people", "keys",
|
|
256
|
+
"config", "chat", "proposals", "mode", "chat-approval", "people", "keys",
|
|
255
257
|
"setup",
|
|
256
258
|
"verify",
|
|
257
259
|
"intake",
|
package/dist/coding-types.d.ts
CHANGED
|
@@ -1,29 +1,7 @@
|
|
|
1
1
|
export type { CodingSession, CodingStatus } from './contracts/coding-workspace.js';
|
|
2
2
|
import type { CodingSession } from './contracts/coding-workspace.js';
|
|
3
|
-
export type CodingItem
|
|
4
|
-
|
|
5
|
-
type: string;
|
|
6
|
-
text: string;
|
|
7
|
-
status: string | null;
|
|
8
|
-
clientId?: string;
|
|
9
|
-
};
|
|
10
|
-
export type CodingQuestion = {
|
|
11
|
-
id: string;
|
|
12
|
-
header: string;
|
|
13
|
-
question: string;
|
|
14
|
-
options: {
|
|
15
|
-
label: string;
|
|
16
|
-
description: string;
|
|
17
|
-
}[];
|
|
18
|
-
};
|
|
19
|
-
export type CodingRequest = {
|
|
20
|
-
id: string;
|
|
21
|
-
kind: 'command' | 'files' | 'questions';
|
|
22
|
-
method: string;
|
|
23
|
-
title: string;
|
|
24
|
-
detail: string;
|
|
25
|
-
questions: CodingQuestion[];
|
|
26
|
-
};
|
|
3
|
+
export type { CodingItem, CodingQuestion, CodingRequest } from './contracts/coding-activity.js';
|
|
4
|
+
import type { CodingItem, CodingRequest } from './contracts/coding-activity.js';
|
|
27
5
|
export type CodingSnapshot = {
|
|
28
6
|
session: CodingSession;
|
|
29
7
|
items: CodingItem[];
|
package/dist/coding-workspace.js
CHANGED
|
@@ -10,6 +10,7 @@ import { processMayBeAlive } from './process-liveness.js';
|
|
|
10
10
|
import { createCodexCodingProvider } from './coding-provider.js';
|
|
11
11
|
import { verifyCodingContext } from './coding-context.js';
|
|
12
12
|
import { codingSessionDocument, parseCodingSessionDocument } from './contracts/coding-workspace.js';
|
|
13
|
+
import { readCodingCustodyWitness, readCodingItem, readCodingRequest, readCodingRpcId } from './contracts/coding-activity.js';
|
|
13
14
|
export class CodingActionError extends Error {
|
|
14
15
|
delivery;
|
|
15
16
|
sessionId;
|
|
@@ -163,8 +164,8 @@ export class CodingWorkspace {
|
|
|
163
164
|
}
|
|
164
165
|
snapshot(id, actor) {
|
|
165
166
|
const session = this.get(id, actor);
|
|
166
|
-
const items = this.db.prepare('SELECT payload FROM coding_item WHERE session=? ORDER BY position').all(id).map(row =>
|
|
167
|
-
const requests = this.db.prepare("SELECT payload FROM coding_request WHERE session=? AND status='pending' ORDER BY rowid").all(id).map(row =>
|
|
167
|
+
const items = this.db.prepare('SELECT payload FROM coding_item WHERE session=? ORDER BY position').all(id).map(row => readCodingItem(String(row['payload'])));
|
|
168
|
+
const requests = this.db.prepare("SELECT payload FROM coding_request WHERE session=? AND status='pending' ORDER BY rowid").all(id).map(row => readCodingRequest(String(row['payload'])));
|
|
168
169
|
const revision = Number(this.db.prepare('SELECT revision FROM coding_session WHERE id=?').get(id)?.['revision']);
|
|
169
170
|
return { session, items, requests, revision };
|
|
170
171
|
}
|
|
@@ -174,8 +175,8 @@ export class CodingWorkspace {
|
|
|
174
175
|
const session = this.get(id, actor);
|
|
175
176
|
const itemRows = this.db.prepare('SELECT payload FROM coding_item WHERE session=? ORDER BY position DESC LIMIT 101').all(id);
|
|
176
177
|
const requestRows = this.db.prepare("SELECT payload FROM coding_request WHERE session=? AND status='pending' ORDER BY rowid LIMIT 51").all(id);
|
|
177
|
-
const items = itemRows.slice(0, 100).reverse().map(row =>
|
|
178
|
-
const requests = requestRows.slice(0, 50).map(row =>
|
|
178
|
+
const items = itemRows.slice(0, 100).reverse().map(row => readCodingItem(String(row['payload'])));
|
|
179
|
+
const requests = requestRows.slice(0, 50).map(row => readCodingRequest(String(row['payload'])));
|
|
179
180
|
const revision = Number(this.db.prepare('SELECT revision FROM coding_session WHERE id=?').get(id)?.['revision']);
|
|
180
181
|
return { session, items, requests, revision, truncated: itemRows.length > items.length || requestRows.length > requests.length };
|
|
181
182
|
}
|
|
@@ -373,7 +374,7 @@ export class CodingWorkspace {
|
|
|
373
374
|
}
|
|
374
375
|
else if (event.method === 'item/agentMessage/delta') {
|
|
375
376
|
const id = string(p['itemId']), key = `${session.id}:${id}`;
|
|
376
|
-
const prior = this.pendingItems.get(key)?.item ?? (() => { const row = this.db.prepare('SELECT payload FROM coding_item WHERE session=? AND id=?').get(session.id, id); return row ?
|
|
377
|
+
const prior = this.pendingItems.get(key)?.item ?? (() => { const row = this.db.prepare('SELECT payload FROM coding_item WHERE session=? AND id=?').get(session.id, id); return row ? readCodingItem(String(row['payload'])) : { id, type: 'agentMessage', text: '', status: 'inProgress' }; })();
|
|
377
378
|
prior.text += string(p['delta']);
|
|
378
379
|
if (prior.text.length > 100_000)
|
|
379
380
|
prior.text = prior.text.slice(0, 100_000);
|
|
@@ -524,8 +525,8 @@ export class CodingWorkspace {
|
|
|
524
525
|
if (!row)
|
|
525
526
|
return false;
|
|
526
527
|
try {
|
|
527
|
-
const c =
|
|
528
|
-
if (c
|
|
528
|
+
const c = readCodingCustodyWitness(String(row['payload']));
|
|
529
|
+
if (c === null || c.host !== hostname())
|
|
529
530
|
return false;
|
|
530
531
|
if (provenDeadByBootChange(c))
|
|
531
532
|
return true;
|
|
@@ -688,8 +689,8 @@ export class CodingWorkspace {
|
|
|
688
689
|
if (!session.nativeThreadId || !session.turnId || !busy.has(session.status))
|
|
689
690
|
throw new CodingActionError('There is no running turn to stop.', 'rejected', id);
|
|
690
691
|
for (const row of this.db.prepare("SELECT token,rpc_id,payload FROM coding_request WHERE session=? AND status='pending'").all(id)) {
|
|
691
|
-
const request =
|
|
692
|
-
this.native().respond(
|
|
692
|
+
const request = readCodingRequest(String(row['payload']));
|
|
693
|
+
this.native().respond(readCodingRpcId(String(row['rpc_id'])), request.kind === 'questions' ? { answers: {} } : { decision: 'cancel' });
|
|
693
694
|
this.db.prepare("UPDATE coding_request SET status='withdrawn' WHERE token=?").run(String(row['token']));
|
|
694
695
|
}
|
|
695
696
|
this.save({ ...session, status: 'stopping' });
|
|
@@ -700,11 +701,11 @@ export class CodingWorkspace {
|
|
|
700
701
|
const row = this.db.prepare("SELECT * FROM coding_request WHERE session=? AND token=? AND status='pending'").get(id, token);
|
|
701
702
|
if (!row || session.status !== 'needs-input' || !this.loaded.has(id))
|
|
702
703
|
throw Error('This request is no longer waiting for an answer.');
|
|
703
|
-
const request =
|
|
704
|
+
const request = readCodingRequest(String(row['payload']));
|
|
704
705
|
if (!['accept', 'decline', 'cancel'].includes(decision))
|
|
705
706
|
throw Error('Choose an available decision.');
|
|
706
707
|
const result = request.kind === 'questions' ? { answers: decision === 'accept' ? this.validateAnswers(request, answers) : {} } : { decision };
|
|
707
|
-
this.native().respond(
|
|
708
|
+
this.native().respond(readCodingRpcId(String(row['rpc_id'])), result);
|
|
708
709
|
this.db.prepare("UPDATE coding_request SET status='answered' WHERE token=?").run(token);
|
|
709
710
|
const remaining = this.db.prepare("SELECT 1 FROM coding_request WHERE session=? AND status='pending'").get(id);
|
|
710
711
|
this.save({ ...session, status: remaining ? 'needs-input' : 'working' });
|