toolroll 0.9.72 → 0.9.73
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 +76 -0
- package/dist/agent-fence.d.ts +5 -0
- package/dist/agent-fence.js +42 -7
- package/dist/browser/workspace.js +28 -28
- package/dist/chat-actions.js +13 -8
- package/dist/chat-decide.d.ts +47 -1
- package/dist/chat-decide.js +170 -4
- package/dist/check-report.js +2 -1
- package/dist/coding-provider.js +4 -2
- package/dist/commit-extract.d.ts +17 -0
- package/dist/commit-extract.js +82 -4
- package/dist/contracts/console-api.d.ts +3 -3
- package/dist/contracts/console-api.js +4 -4
- package/dist/contracts/release-terms.d.ts +7 -1
- package/dist/contracts/release-terms.js +6 -2
- package/dist/exec.d.ts +1 -1
- package/dist/exec.js +7 -3
- package/dist/flow-gallery-ui.js +4 -2
- package/dist/flow-gallery.d.ts +3 -0
- package/dist/flow-gallery.js +6 -2
- package/dist/flow-triggers.d.ts +8 -0
- package/dist/flow-triggers.js +15 -0
- package/dist/flows-cli.js +13 -3
- package/dist/guides.js +10 -1
- package/dist/kits-ui.js +4 -2
- package/dist/kits.d.ts +6 -4
- package/dist/kits.js +9 -11
- package/dist/mcp-connect.d.ts +10 -0
- package/dist/mcp-connect.js +2 -0
- package/dist/project-tools.js +3 -1
- package/dist/protected-storage.d.ts +67 -0
- package/dist/protected-storage.js +398 -0
- package/dist/release-adapters.d.ts +8 -1
- package/dist/release-adapters.js +80 -14
- package/dist/release-authority.d.ts +6 -0
- package/dist/release-authority.js +9 -0
- package/dist/release-card.d.ts +76 -0
- package/dist/release-card.js +252 -0
- package/dist/release-carry.d.ts +3 -0
- package/dist/release-carry.js +23 -4
- package/dist/release-cli.js +36 -3
- package/dist/release-git.d.ts +43 -0
- package/dist/release-git.js +106 -0
- package/dist/release.d.ts +8 -2
- package/dist/release.js +79 -6
- package/dist/scope.d.ts +7 -0
- package/dist/scope.js +22 -5
- package/dist/server/flows.js +40 -10
- package/dist/server/render-settings.js +1 -1
- package/dist/server/settings.js +26 -5
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -0
- package/dist/subagent-memory.d.ts +1 -0
- package/dist/subagent-memory.js +1 -1
- package/dist/subagent-tools.d.ts +22 -6
- package/dist/subagent-tools.js +44 -8
- package/dist/subagent-work.d.ts +2 -0
- package/dist/subagent-work.js +5 -2
- package/dist/subagents-ui.js +6 -4
- package/dist/telegram.d.ts +6 -1
- package/dist/telegram.js +58 -6
- package/package.json +1 -1
package/dist/subagent-tools.d.ts
CHANGED
|
@@ -56,10 +56,20 @@ export type OfferedTool = {
|
|
|
56
56
|
export declare function offeredTools(store: Store, mate: SubagentRow): OfferedTool[];
|
|
57
57
|
/** Before a turn: re-list tools whose listing is old (or never happened), so the turn sees what they offer today. A tool that can't be reached keeps its last listing. */
|
|
58
58
|
export declare function refreshGrants(store: Store, mate: SubagentRow, now: Date, io: ToolIo): Promise<void>;
|
|
59
|
-
/** Let a subagent use one of the project's tools. Its actions start at their defaults: reading is free, everything else asks first.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
export declare function
|
|
59
|
+
/** Let a subagent use one of the project's tools. Its actions start at their defaults: reading is free, everything else asks first.
|
|
60
|
+
* A grant widens what it does unasked, so it takes the person's proof (a password, or a session under Passwordless):
|
|
61
|
+
* `widening: "proved"` (chat-management.md §0a.3). */
|
|
62
|
+
export declare function grantTool(store: Store, mate: SubagentRow, tool: string, by: string, now: Date, io?: ToolIo,
|
|
63
|
+
/** `stillProved`: the proof re-checked after the listing's wait, in the same synchronous step as the save. */
|
|
64
|
+
options?: {
|
|
65
|
+
widening?: "proved";
|
|
66
|
+
stillProved?: () => boolean;
|
|
67
|
+
}): Promise<Done>;
|
|
68
|
+
/** A grant from what the tool offers (or, not reachable now, the names its last test found). Every road to a grant
|
|
69
|
+
* comes here: it takes the person's proof, and a tool it already uses keeps its rules (a grant never resets them). */
|
|
70
|
+
export declare function grantListed(store: Store, mate: SubagentRow, tool: ProjectTool, actions: ToolActionInfo[] | null, by: string, now: Date, options?: {
|
|
71
|
+
widening?: "proved";
|
|
72
|
+
}): Done;
|
|
63
73
|
export declare function revokeTool(store: Store, mate: SubagentRow, tool: string, by: string, now?: Date): Done;
|
|
64
74
|
/** Check one rule: an action the tool offers, a known use, and a limit on one of its number fields. */
|
|
65
75
|
export declare function checkRule(grant: Pick<SubagentGrantRow, "tool" | "actions">, action: string, rule: {
|
|
@@ -76,7 +86,11 @@ export declare function checkRule(grant: Pick<SubagentGrantRow, "tool" | "action
|
|
|
76
86
|
ok: false;
|
|
77
87
|
said: string;
|
|
78
88
|
};
|
|
79
|
-
/**
|
|
89
|
+
/** Whether `next` lets the subagent do more unasked than `before` (chat-management.md §0a.3): a higher rank, or a limit
|
|
90
|
+
* on another number or a higher one. The undo action is never authority. */
|
|
91
|
+
export declare function ruleLoosens(before: ToolRule, next: ToolRule): boolean;
|
|
92
|
+
/** Set the rules for some of a granted tool's actions; the others keep theirs. Loosening any of them takes the
|
|
93
|
+
* person's proof (a password, or a session under Passwordless): `widening: "proved"`; tightening never does. */
|
|
80
94
|
export declare function setToolRules(store: Store, mate: SubagentRow, tool: string, changes: Record<string, {
|
|
81
95
|
use: unknown;
|
|
82
96
|
limit?: {
|
|
@@ -84,7 +98,9 @@ export declare function setToolRules(store: Store, mate: SubagentRow, tool: stri
|
|
|
84
98
|
over: unknown;
|
|
85
99
|
} | null;
|
|
86
100
|
undo?: unknown;
|
|
87
|
-
}>, by: string, now: Date
|
|
101
|
+
}>, by: string, now: Date, options?: {
|
|
102
|
+
widening?: "proved";
|
|
103
|
+
}): Done;
|
|
88
104
|
/**
|
|
89
105
|
* Make one call now: a free one, or one a person approved (exactly as they
|
|
90
106
|
* saw it). What the tool said is kept on the receipt, secrets scrubbed; a
|
package/dist/subagent-tools.js
CHANGED
|
@@ -116,7 +116,14 @@ export async function refreshGrants(store, mate, now, io) {
|
|
|
116
116
|
const listed = await listed_(store, tool, mate.repo, io);
|
|
117
117
|
if (!listed.ok)
|
|
118
118
|
continue;
|
|
119
|
-
|
|
119
|
+
// The grant as it is now, after the listing's wait, in one transaction with the save: one taken away meanwhile stays
|
|
120
|
+
// gone, and only the tool's list of actions is new (rules changed meanwhile, tightened ones included, stand).
|
|
121
|
+
store.transact(() => {
|
|
122
|
+
const current = store.subagentGrant(mate.id, grant.tool);
|
|
123
|
+
if (current === null)
|
|
124
|
+
return;
|
|
125
|
+
store.saveSubagentGrant({ subagent: mate.id, tool: grant.tool, ...withActions(current.rules, listed.actions), listedAt: now.toISOString() }, current.updatedBy, now);
|
|
126
|
+
});
|
|
120
127
|
}
|
|
121
128
|
}
|
|
122
129
|
const listed_ = (store, tool, repo, io) => (io.listTools ?? ((spec, values) => listProjectToolActions(spec, values, { timeoutMs: 60_000, omitEnv: ALL_CREDENTIAL_ENV, server: serverOf(store, repo, io) })))(tool.spec, readToolSecrets(repo, tool.name, io.toolHome));
|
|
@@ -124,18 +131,32 @@ const listed_ = (store, tool, repo, io) => (io.listTools ?? ((spec, values) => l
|
|
|
124
131
|
const serverOf = (store, repo, io) => toolServerFor(store, repo, { ...(io.toolHome === undefined ? {} : { home: io.toolHome }), ...(io.fenceAvailable === undefined ? {} : { available: io.fenceAvailable }) });
|
|
125
132
|
/** Names only, from the tool's last test: what a grant starts with when it can't be listed right now. */
|
|
126
133
|
const namesOnly = (tool) => (tool.lastTest?.tools ?? []).map(name => ({ name, about: "", input: null, readOnly: false }));
|
|
127
|
-
/** Let a subagent use one of the project's tools. Its actions start at their defaults: reading is free, everything else asks first.
|
|
128
|
-
|
|
134
|
+
/** Let a subagent use one of the project's tools. Its actions start at their defaults: reading is free, everything else asks first.
|
|
135
|
+
* A grant widens what it does unasked, so it takes the person's proof (a password, or a session under Passwordless):
|
|
136
|
+
* `widening: "proved"` (chat-management.md §0a.3). */
|
|
137
|
+
export async function grantTool(store, mate, tool, by, now, io = {},
|
|
138
|
+
/** `stillProved`: the proof re-checked after the listing's wait, in the same synchronous step as the save. */
|
|
139
|
+
options = {}) {
|
|
140
|
+
if (options.widening !== "proved")
|
|
141
|
+
return grantRefused(tool);
|
|
129
142
|
const found = projectToolsOf(store, mate.repo).find(one => one.name === tool);
|
|
130
143
|
if (found === undefined)
|
|
131
144
|
return { ok: false, said: `This project has no tool called ${tool}. Add it on the Tools page first.` };
|
|
132
145
|
if (store.subagentGrant(mate.id, tool) !== null)
|
|
133
146
|
return { ok: true, said: `It can already use ${tool}.` };
|
|
134
147
|
const listed = await listed_(store, found, mate.repo, io);
|
|
135
|
-
|
|
148
|
+
if (options.stillProved !== undefined && !options.stillProved())
|
|
149
|
+
return grantRefused(tool);
|
|
150
|
+
return grantListed(store, mate, found, listed.ok ? listed.actions : null, by, now, { widening: "proved" });
|
|
136
151
|
}
|
|
137
|
-
|
|
138
|
-
|
|
152
|
+
const grantRefused = (tool) => ({ ok: false, said: `Letting it use ${tool} takes your password. Do it on its page in Toolroll.` });
|
|
153
|
+
/** A grant from what the tool offers (or, not reachable now, the names its last test found). Every road to a grant
|
|
154
|
+
* comes here: it takes the person's proof, and a tool it already uses keeps its rules (a grant never resets them). */
|
|
155
|
+
export function grantListed(store, mate, tool, actions, by, now, options = {}) {
|
|
156
|
+
if (options.widening !== "proved")
|
|
157
|
+
return grantRefused(tool.name);
|
|
158
|
+
if (store.subagentGrant(mate.id, tool.name) !== null)
|
|
159
|
+
return { ok: true, said: `It can already use ${tool.name}.` };
|
|
139
160
|
const offered = actions ?? namesOnly(tool);
|
|
140
161
|
if (offered.length === 0)
|
|
141
162
|
return { ok: false, said: `${tool.name} didn't say what it can do. Test it on the Tools page, then try again.` };
|
|
@@ -176,8 +197,19 @@ export function checkRule(grant, action, rule) {
|
|
|
176
197
|
return { ok: false, said: "A limit is a number, 0 or more." };
|
|
177
198
|
return withUndo({ use: "free", limit: { field, over } });
|
|
178
199
|
}
|
|
179
|
-
/**
|
|
180
|
-
|
|
200
|
+
/** How much a rule lets a subagent do unasked: never, ask first, on its own up to a limit, on its own. */
|
|
201
|
+
const ruleRank = (rule) => rule.use === "never" ? 0 : rule.use === "ask" ? 1 : rule.limit !== undefined ? 2 : 3;
|
|
202
|
+
/** Whether `next` lets the subagent do more unasked than `before` (chat-management.md §0a.3): a higher rank, or a limit
|
|
203
|
+
* on another number or a higher one. The undo action is never authority. */
|
|
204
|
+
export function ruleLoosens(before, next) {
|
|
205
|
+
const was = ruleRank(before), now = ruleRank(next);
|
|
206
|
+
if (now !== was)
|
|
207
|
+
return now > was;
|
|
208
|
+
return now === 2 && (next.limit.field !== before.limit.field || next.limit.over > before.limit.over);
|
|
209
|
+
}
|
|
210
|
+
/** Set the rules for some of a granted tool's actions; the others keep theirs. Loosening any of them takes the
|
|
211
|
+
* person's proof (a password, or a session under Passwordless): `widening: "proved"`; tightening never does. */
|
|
212
|
+
export function setToolRules(store, mate, tool, changes, by, now, options = {}) {
|
|
181
213
|
const grant = store.subagentGrant(mate.id, tool);
|
|
182
214
|
if (grant === null)
|
|
183
215
|
return { ok: false, said: `It doesn't use ${tool}. Let it use ${tool} first.` };
|
|
@@ -186,6 +218,10 @@ export function setToolRules(store, mate, tool, changes, by, now) {
|
|
|
186
218
|
const checked = checkRule(grant, action, change);
|
|
187
219
|
if (!checked.ok)
|
|
188
220
|
return { ok: false, said: checked.said };
|
|
221
|
+
const info = grant.actions.find(one => one.name === action);
|
|
222
|
+
if (options.widening !== "proved" && ruleLoosens(grant.rules[action] ?? defaultRule(info), checked.rule)) {
|
|
223
|
+
return { ok: false, said: `Letting it do more with ${action} on its own takes your password. Change the rule on its page in Toolroll.` };
|
|
224
|
+
}
|
|
189
225
|
rules[action] = checked.rule;
|
|
190
226
|
}
|
|
191
227
|
store.saveSubagentGrant({ subagent: mate.id, tool, actions: grant.actions, rules }, by, now);
|
package/dist/subagent-work.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ export declare function answerSubagentQuestion(store: Store, id: number, answer:
|
|
|
65
65
|
text: string | null;
|
|
66
66
|
by: string;
|
|
67
67
|
via: string;
|
|
68
|
+
/** The person proved themselves for this answer: only then may accepting a suggested rule loosen it. */
|
|
69
|
+
widening?: "proved";
|
|
68
70
|
}, now: Date): {
|
|
69
71
|
ok: true;
|
|
70
72
|
said: string;
|
package/dist/subagent-work.js
CHANGED
|
@@ -5,7 +5,7 @@ import { claudeTurnRunner, parseSoul, readTurn, subagentActor, subagentLabel, TU
|
|
|
5
5
|
import { passOn, shortenAsk, TEXT_LIMITS } from "./text-limits.js";
|
|
6
6
|
import { LIMITS } from "./decision.js";
|
|
7
7
|
import { callName, callOutcome, callWords, inputProblem, makeCall, offeredTools, refreshGrants, ruleFor } from "./subagent-tools.js";
|
|
8
|
-
import { answerSuggestion, considerSuggestion, memoriesFor, remember } from "./subagent-memory.js";
|
|
8
|
+
import { ACCEPT, answerSuggestion, considerSuggestion, memoriesFor, remember } from "./subagent-memory.js";
|
|
9
9
|
import { replyToAsker } from "./subagent-desk.js";
|
|
10
10
|
import { claudeMachineBilling } from "./spend.js";
|
|
11
11
|
/** A question waits for its answer this long before the step is due again on its own: never, in practice. */
|
|
@@ -275,8 +275,11 @@ export function answerSubagentQuestion(store, id, answer, now) {
|
|
|
275
275
|
const card = store.getFlowCard(question.card);
|
|
276
276
|
// v95: a rule change it suggested to its manager. Nothing about the card waits on it.
|
|
277
277
|
if (question.suggestion !== null) {
|
|
278
|
+
// A suggestion loosens a rule: accepted only with the person's proof, on its page (chat-management.md §0a.3).
|
|
279
|
+
if (choice?.id === ACCEPT && answer.widening !== "proved")
|
|
280
|
+
return { ok: false, said: "Changing this rule takes your password. Answer it on the subagent's page in Toolroll." };
|
|
278
281
|
const mate = store.getSubagent(question.subagent);
|
|
279
|
-
const decided = mate === null ? { ok: false, said: "That subagent is off the team." } : answerSuggestion(store, mate, question.suggestion, { choice: choice?.id ?? null, text, by: answer.by }, now);
|
|
282
|
+
const decided = mate === null ? { ok: false, said: "That subagent is off the team." } : answerSuggestion(store, mate, question.suggestion, { choice: choice?.id ?? null, text, by: answer.by, ...(answer.widening === undefined ? {} : { widening: answer.widening }) }, now);
|
|
280
283
|
store.answerSubagentQuestion(id, { choice: choice?.id ?? null, text, by: answer.by, via: answer.via }, now);
|
|
281
284
|
store.addSubagentEvent({ subagent: question.subagent, card: question.card, entry: question.entry, kind: "answered", said: `${answer.by}: ${[choice?.label, text].filter(Boolean).join(" — ")}`, by: answer.by }, now);
|
|
282
285
|
return decided;
|
package/dist/subagents-ui.js
CHANGED
|
@@ -6,7 +6,7 @@ import { MEMORY_CHARS, searchMemories } from "./subagent-memory.js";
|
|
|
6
6
|
import { deskOf, localZone, routinesOf } from "./subagent-desk.js";
|
|
7
7
|
import { weekOf, weekWords } from "./subagent-week.js";
|
|
8
8
|
import { describeSchedule, parseSchedule } from "./flow-schedule.js";
|
|
9
|
-
import { html, joinHtml, postForm } from "./html.js";
|
|
9
|
+
import { html, joinHtml, postForm, reauth } from "./html.js";
|
|
10
10
|
/** v94: the Tools section's rule rows; a limit's fields show only while "Up to a limit" is chosen. */
|
|
11
11
|
export const SUBAGENT_CSS = `.subagent-tools .tool-grant{border-top:1px solid var(--so-line);padding-top:12px;margin-top:12px}.subagent-tools .tool-grant:first-of-type{border-top:0;margin-top:0;padding-top:0}.subagent-tools h3{font-size:1rem;margin:0 0 4px}.subagent-tools .tool-rule{display:grid;gap:6px;padding:10px 0;border-bottom:1px solid var(--so-line)}.subagent-tools .tool-rule:last-of-type{border-bottom:0}.subagent-tools .tool-rule .meta{display:block}.subagent-tools select,.subagent-tools input{max-width:100%;box-sizing:border-box}.subagent-tools .tool-limit{display:none;flex-wrap:wrap;align-items:center;gap:6px}.subagent-tools .tool-rule:has(option[value="limit"]:checked) .tool-limit{display:flex}.subagent-tools .tool-rule>select{width:auto;min-width:16em}.subagent-tools .tool-limit select{width:auto}.subagent-tools .tool-limit input{width:7em}.subagent-tools .tool-buttons{display:flex;flex-wrap:wrap;gap:8px;margin-top:8px}.subagent-tools button{min-height:44px}.subagent-activity li{overflow-wrap:anywhere}.subagent-memory .memory-tell,.subagent-memory .memory-search{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:0 0 12px}.subagent-memory .memory-tell textarea{flex:1 1 280px}.subagent-memory .memory-search input{flex:1 1 220px}.subagent-memory button{min-height:44px}.subagent-memories{list-style:none;padding:0;margin:0}.subagent-memories li{padding:10px 0;border-top:1px solid var(--so-line);overflow-wrap:anywhere}.subagent-memories li:first-child{border-top:0}.subagent-memories .meta{display:block;font-size:.85rem}.subagent-memories details.memory-edit{border:0;padding:0;margin:2px 0 0;background:transparent;box-shadow:none}.memory-edit summary{cursor:pointer;min-height:32px;display:inline-flex;align-items:center;font-size:.85rem;font-weight:400;color:var(--so-muted)}.tool-undo{border:0;padding:0;margin:8px 0;background:transparent;box-shadow:none}.tool-undo summary{cursor:pointer;min-height:36px;display:inline-flex;align-items:center;font-size:.9rem}.tool-undo-row{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin:6px 0}.tool-undo-row select{width:auto}.subagent-overrides,.subagent-undo{padding-left:18px}.subagent-undo li{margin:6px 0}.subagent-undo form{display:inline}.subagent-week h3{font-size:1rem;margin:14px 0 4px}.subagent-week button{min-height:44px}.subagent-routines{list-style:none;padding:0;margin:8px 0}.subagent-routines li{display:grid;gap:4px;padding:10px 0;border-top:1px solid var(--so-line)}.subagent-routines li:first-child{border-top:0}.routine-buttons{display:flex;flex-wrap:wrap;gap:8px}.routine-buttons button,.routine-add button{min-height:44px}.routine-add{display:flex;flex-wrap:wrap;gap:8px;align-items:flex-end;margin:8px 0}.routine-add label{display:grid;gap:4px;flex:1 1 200px}.routine-add label:first-of-type{flex:0 1 180px}.subagent-actions,.question-options{display:flex;flex-wrap:wrap;gap:8px;margin:8px 0}.subagent-actions form,.question-options form{margin:0}.memory-edit form{display:grid;gap:8px;margin-top:6px}.memory-buttons{display:flex;flex-wrap:wrap;gap:8px}@media(max-width:600px){.subagent-tools select,.subagent-tools input{font-size:16px}}`;
|
|
12
12
|
const USES = [["free", "Do it"], ["limit", "Do it, up to a limit"], ["ask", "Ask first"], ["never", "Never"]];
|
|
@@ -38,12 +38,12 @@ function toolsSection(store, mate, canManage) {
|
|
|
38
38
|
});
|
|
39
39
|
const undoBox = undoing.length > 0 && html `<details class="tool-undo"${grant.actions.some(one => grant.rules[one.name]?.undo !== undefined) && html ` open`}><summary>Undo</summary><p class="meta">When an action has an opposite, a person can press Undo on its receipts: the opposite is called with the same input.</p>${undoing}</details>`;
|
|
40
40
|
return canManage
|
|
41
|
-
? html `<div class="tool-grant" data-tool-grant="${grant.tool}">${head}${postForm(`/settings/lead/subagents/${mate.id}/tools`, html `${rows}${undoBox}<div class="tool-buttons"><button name="op" value="rules">Save rules</button><button name="op" value="revoke" class="secondary">Stop using ${grant.tool}</button></div>`, { hidden: { tool: grant.tool } })}</div>`
|
|
41
|
+
? html `<div class="tool-grant" data-tool-grant="${grant.tool}">${head}${postForm(`/settings/lead/subagents/${mate.id}/tools`, html `${rows}${undoBox}${reauth(html `<label>Your password <span class="meta">only to let it do more on its own</span><input type="password" name="password" autocomplete="current-password"></label>`)}<div class="tool-buttons"><button name="op" value="rules">Save rules</button><button name="op" value="revoke" class="secondary">Stop using ${grant.tool}</button></div>`, { hidden: { tool: grant.tool } })}</div>`
|
|
42
42
|
: html `<div class="tool-grant" data-tool-grant="${grant.tool}">${head}<ul>${rows}</ul></div>`;
|
|
43
43
|
});
|
|
44
44
|
const open = tools.filter(one => !grants.some(grant => grant.tool === one.name));
|
|
45
45
|
const add = !canManage ? null : tools.length === 0 ? html `<p class="meta">This project has no tools yet. <a href="${toolsPage}">Add one</a>, then let ${name} use it here.</p>`
|
|
46
|
-
: open.length === 0 ? null : postForm(`/settings/lead/subagents/${mate.id}/tools`, html `<label>Let ${name} use<select name="tool">${open.map(one => html `<option value="${one.name}">${one.name}${one.spec.about === "" ? "" : ` — ${one.spec.about.slice(0, 80)}`}</option>`)}</select></label
|
|
46
|
+
: open.length === 0 ? null : postForm(`/settings/lead/subagents/${mate.id}/tools`, html `<label>Let ${name} use<select name="tool">${open.map(one => html `<option value="${one.name}">${one.name}${one.spec.about === "" ? "" : ` — ${one.spec.about.slice(0, 80)}`}</option>`)}</select></label>${reauth(html `<label>Your password<input type="password" name="password" autocomplete="current-password"></label>`)}<button>Add tool</button>`, { attrs: { class: "tool-add" }, hidden: { op: "grant" } });
|
|
47
47
|
if (grants.length === 0 && add === null)
|
|
48
48
|
return "";
|
|
49
49
|
return html `<section class="card subagent-tools" id="tools"><h2>Tools</h2>${grants.length === 0 && html `<p class="meta">${name} doesn't use any tools yet. Reading starts as “Do it”; everything else asks you first.</p>`}${granted}${add}</section>`;
|
|
@@ -110,7 +110,9 @@ export function subagentPageHtml(store, mate, viewer, projectName, canManage, ap
|
|
|
110
110
|
const flow = card === null ? null : store.getFlow(card.flow);
|
|
111
111
|
const mine = question.askedOf === viewer;
|
|
112
112
|
const cardLink = card === null || flow === null ? "a card" : html `<a href="/flows/${flow.id}?card=${card.id}">${card.title}</a>`;
|
|
113
|
-
const answer = mine && html `<div class="question-options">${question.options.map(one => postForm(`/settings/lead/subagents/questions/${question.id}/answer`,
|
|
113
|
+
const answer = mine && html `<div class="question-options">${question.options.map(one => postForm(`/settings/lead/subagents/questions/${question.id}/answer`,
|
|
114
|
+
// Accepting a looser rule it suggested takes the password (chat-management.md §0a.3).
|
|
115
|
+
question.suggestion !== null && one.id === "accept" ? html `${reauth(html `<input type="password" name="password" autocomplete="current-password" aria-label="Your password" placeholder="Your password">`)}<button>${one.label}</button>` : html `<button>${one.label}</button>`, { hidden: { choice: one.id } }))}</div>${postForm(`/settings/lead/subagents/questions/${question.id}/answer`, html `<label>Or answer in your words<textarea name="text" rows="2" maxlength="2000"></textarea></label><button>Answer</button>`, { attrs: { class: "question-reply" } })}`;
|
|
114
116
|
return html `<article class="card subagent-question" data-question="${question.id}"${question.suggestion !== null && html ` data-suggestion`}><p class="meta">${question.suggestion === null ? html `About ${cardLink}` : "Suggests a rule change"} · ${when(question.createdAt)}${!mine && ` · for ${question.askedOf}`}</p><p><strong>${question.question}</strong></p>${answer}</article>`;
|
|
115
117
|
});
|
|
116
118
|
const zones = zonesOf(store, mate);
|
package/dist/telegram.d.ts
CHANGED
|
@@ -94,9 +94,14 @@ more?: readonly TelegramUpload[]) => Promise<{
|
|
|
94
94
|
export type TelegramUpload = {
|
|
95
95
|
field: "document" | "photo" | `shot${number}`;
|
|
96
96
|
fileName: string;
|
|
97
|
-
contentType: "image/png" | "image/jpeg";
|
|
97
|
+
contentType: "image/png" | "image/jpeg" | "text/plain";
|
|
98
98
|
bytes: Buffer;
|
|
99
99
|
};
|
|
100
|
+
/** A release card's full terms (release-card.ts) as the document Telegram sends with it: text this process wrote. */
|
|
101
|
+
export declare const termsUpload: (file: {
|
|
102
|
+
name: string;
|
|
103
|
+
text: string;
|
|
104
|
+
}) => TelegramUpload;
|
|
100
105
|
export declare function createTransport(token: string, timeoutMs?: number): TelegramTransport;
|
|
101
106
|
/**
|
|
102
107
|
* Why this process can't reach Telegram the way the service does, or null.
|
package/dist/telegram.js
CHANGED
|
@@ -172,6 +172,8 @@ export function redactToken(token) {
|
|
|
172
172
|
export function scrub(text, token) {
|
|
173
173
|
return token === "" ? text : text.split(token).join(redactToken(token));
|
|
174
174
|
}
|
|
175
|
+
/** A release card's full terms (release-card.ts) as the document Telegram sends with it: text this process wrote. */
|
|
176
|
+
export const termsUpload = (file) => ({ field: "document", fileName: file.name, contentType: "text/plain", bytes: Buffer.from(file.text, "utf8") });
|
|
175
177
|
export function createTransport(token, timeoutMs = 30_000) {
|
|
176
178
|
return async (method, params, signal, upload, more) => {
|
|
177
179
|
const controller = new AbortController();
|
|
@@ -763,6 +765,15 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
763
765
|
sentAny = true;
|
|
764
766
|
return answer;
|
|
765
767
|
};
|
|
768
|
+
/** A release card's terms file (release-card.ts), sent before the card itself. */
|
|
769
|
+
const fileSender = async (file) => {
|
|
770
|
+
try {
|
|
771
|
+
return (await wire("sendDocument", { chat_id: binding.chatId, caption: "Every term of this release" }, undefined, termsUpload(file))).ok === true;
|
|
772
|
+
}
|
|
773
|
+
catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
};
|
|
766
777
|
// `beforeSend`: the fence before a transport call also asks the watch — until something of this delivery went out;
|
|
767
778
|
// the one after a confirmed send never does, so a message Telegram accepted is recorded as delivered.
|
|
768
779
|
const fence = (beforeSend = !sentAny) => {
|
|
@@ -984,7 +995,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
984
995
|
// A lone update that is not finished work goes out exactly as it always has; a later one edits it in place.
|
|
985
996
|
const lone = batch.items.length === 1 && batch.items[0].notification !== null;
|
|
986
997
|
if (batch.message === null && lone) {
|
|
987
|
-
const sent = await deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects);
|
|
998
|
+
const sent = await deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects, fileSender);
|
|
988
999
|
const placed = store.telegramMessageOf(fact.id, fact.destination);
|
|
989
1000
|
if (placed !== null)
|
|
990
1001
|
store.setChatBatchMessage(batch.id, placed, "");
|
|
@@ -996,7 +1007,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
996
1007
|
if (view === null) {
|
|
997
1008
|
if (readyRun !== null)
|
|
998
1009
|
return { ok: true, receipt: receiptFor(botId, binding.chatId, card.messageId) };
|
|
999
|
-
return deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects);
|
|
1010
|
+
return deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects, fileSender);
|
|
1000
1011
|
}
|
|
1001
1012
|
// A message about one result, plan, failure or pull request acts in place (chat-decide.ts); its buttons are
|
|
1002
1013
|
// minted only when what it offers changed, or its own were spent (Not now), so an unchanged repaint keeps the
|
|
@@ -1008,6 +1019,8 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1008
1019
|
let keyboard = own !== null ? [own] : viewKeyboard(phoneOrigin, view);
|
|
1009
1020
|
const shown = createHash("sha256").update(JSON.stringify(offer === null ? [view.text, keyboard] : [text, offerFingerprint(offer)])).digest("hex");
|
|
1010
1021
|
let tokens = [];
|
|
1022
|
+
// A release card's terms go as a file first, on this road as on a lone delivery: no file accepted, no buttons.
|
|
1023
|
+
const termsFirst = async () => offer?.file === undefined || (fileSender !== undefined && await fileSender(offer.file)) ? null : { ok: false, error: "The release's terms file wasn't accepted" };
|
|
1011
1024
|
// Minted unplaced: they ride a message only once it shows them, and until then the old card's buttons work.
|
|
1012
1025
|
const mintFor = () => {
|
|
1013
1026
|
if (offer === null)
|
|
@@ -1024,6 +1037,9 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1024
1037
|
const problem = (await readAccess()) ?? fence();
|
|
1025
1038
|
if (problem !== null)
|
|
1026
1039
|
return { ok: false, error: problem };
|
|
1040
|
+
const terms = await termsFirst();
|
|
1041
|
+
if (terms !== null)
|
|
1042
|
+
return terms;
|
|
1027
1043
|
mintFor();
|
|
1028
1044
|
const edited = await editProgress(wire, binding.chatId, batch.message, text, keyboard);
|
|
1029
1045
|
if (edited.ok) {
|
|
@@ -1044,6 +1060,11 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1044
1060
|
return edited;
|
|
1045
1061
|
}
|
|
1046
1062
|
// A new message: its buttons are minted for it, never the ones a failed edit was given.
|
|
1063
|
+
if (batch.message === null) {
|
|
1064
|
+
const terms = await termsFirst();
|
|
1065
|
+
if (terms !== null)
|
|
1066
|
+
return terms;
|
|
1067
|
+
}
|
|
1047
1068
|
mintFor();
|
|
1048
1069
|
const sent = await sender(text, keyboard.length === 0 ? undefined : keyboard, [fact]);
|
|
1049
1070
|
const placed = sent.ok ? sent.messageId : store.telegramMessageOf(fact.id, fact.destination);
|
|
@@ -1059,7 +1080,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1059
1080
|
dropDecideTokens(store, tokens, clock());
|
|
1060
1081
|
return sent.ok ? { ok: true, receipt: receiptFor(botId, binding.chatId, sent.messageId) } : sent;
|
|
1061
1082
|
}
|
|
1062
|
-
return fact.kind === "acceptance-evidence" ? imageSender(fact) : deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects);
|
|
1083
|
+
return fact.kind === "acceptance-evidence" ? imageSender(fact) : deliverOne(store, botId, binding, sender, fact, clock, phoneOrigin, evidenceRoot, projects, fileSender);
|
|
1063
1084
|
};
|
|
1064
1085
|
/** The lead's words (lead-voice.ts): one message; a later say within two minutes edits it in place. */
|
|
1065
1086
|
const leadSayOutcome = async (fact) => {
|
|
@@ -1102,7 +1123,7 @@ async function deliverOutboxTo(store, botId, binding, transport, owner, clock, r
|
|
|
1102
1123
|
outcome = updated !== null ? updated.ok ? { ok: true, receipt: receiptFor(botId, binding.chatId, updated.messageId) } : updated
|
|
1103
1124
|
: rows[0].kind === "acceptance-evidence" ? await imageSender(rows[0]) : batched
|
|
1104
1125
|
? await deliverDigest(botId, binding, sender, rows, digest.lastSentAt, clock, taskId => chatTitle(store, taskId))
|
|
1105
|
-
: await deliverOne(store, botId, binding, sender, rows[0], clock, phoneOrigin, evidenceRoot, projects);
|
|
1126
|
+
: await deliverOne(store, botId, binding, sender, rows[0], clock, phoneOrigin, evidenceRoot, projects, fileSender);
|
|
1106
1127
|
}
|
|
1107
1128
|
const finalProblem = outcome.ok ? (await readAccess()) ?? fence(false) : null;
|
|
1108
1129
|
const settled = finalProblem === null ? outcome : { ok: false, error: finalProblem };
|
|
@@ -1180,7 +1201,7 @@ async function deliverDigest(botId, binding, sender, rows, since, clock, titleOf
|
|
|
1180
1201
|
}
|
|
1181
1202
|
return { ok: true, receipt: receiptFor(botId, binding.chatId, last) };
|
|
1182
1203
|
}
|
|
1183
|
-
async function deliverOne(store, botId, binding, sender, notification, clock, phoneOrigin, evidenceRoot, projects = []) {
|
|
1204
|
+
async function deliverOne(store, botId, binding, sender, notification, clock, phoneOrigin, evidenceRoot, projects = [], fileSender) {
|
|
1184
1205
|
const decisionId = /^decision:(\d+)$/.exec(notification.dedupeKey);
|
|
1185
1206
|
const decision = decisionId === null ? null : store.getDecision(Number(decisionId[1]));
|
|
1186
1207
|
if (decision !== null && (notification.taskRef === null || store.getRun(decision.run)?.taskRef !== notification.taskRef || (notification.run !== null && notification.run !== decision.run))) {
|
|
@@ -1272,6 +1293,13 @@ async function deliverOne(store, botId, binding, sender, notification, clock, ph
|
|
|
1272
1293
|
const questionKeys = asked === null ? null : telegramQuestionButtons(store, binding, asked, clock());
|
|
1273
1294
|
const keys = flowKeys ?? choiceKeys ?? questionKeys;
|
|
1274
1295
|
let last = null;
|
|
1296
|
+
// A release card: every term goes as a file first, so Continue is offered only once it has arrived.
|
|
1297
|
+
if (offer?.file !== undefined && decided !== null) {
|
|
1298
|
+
if (fileSender === undefined || !await fileSender(offer.file)) {
|
|
1299
|
+
dropDecideTokens(store, decided.tokens, clock());
|
|
1300
|
+
return { ok: false, error: "The release's terms file wasn't accepted" };
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1275
1303
|
for (const [index, part] of parts.entries()) {
|
|
1276
1304
|
const final = index === parts.length - 1;
|
|
1277
1305
|
const decideKeys = decided === null ? [] : decideKeyboard(phoneOrigin, decided.rows);
|
|
@@ -2309,7 +2337,31 @@ function applyCallback(context, update, effects) {
|
|
|
2309
2337
|
if (tapped.ignored === true)
|
|
2310
2338
|
report.ignored++;
|
|
2311
2339
|
ack(tapped.ack.slice(0, 190));
|
|
2312
|
-
|
|
2340
|
+
// A release card's terms, as a file, before its Approve is shown (chat-management.md §0a.5): Approve appears only
|
|
2341
|
+
// once Telegram has accepted this file; a refused or failed upload shows the card without it, Continue to try again.
|
|
2342
|
+
if (tapped.file !== undefined && tapped.edit !== undefined && tapped.unsent !== undefined) {
|
|
2343
|
+
const file = tapped.file, edit = tapped.edit, unsent = tapped.unsent;
|
|
2344
|
+
effects.push(async () => {
|
|
2345
|
+
let sent = false;
|
|
2346
|
+
try {
|
|
2347
|
+
sent = (await transport("sendDocument", { chat_id: binding.chatId, caption: "Every term of this release, read again just now" }, undefined, termsUpload(file))).ok === true;
|
|
2348
|
+
}
|
|
2349
|
+
catch {
|
|
2350
|
+
sent = false;
|
|
2351
|
+
}
|
|
2352
|
+
const shown = sent ? edit : unsent;
|
|
2353
|
+
dropDecideTokens(store, (sent ? unsent : edit).tokens, clock());
|
|
2354
|
+
await transport("editMessageText", { chat_id: binding.chatId, message_id: message.message_id, text: shown.text.slice(0, 4000), link_preview_options: { is_disabled: true },
|
|
2355
|
+
reply_markup: { inline_keyboard: decideKeyboard(context.conversation?.phoneOrigin, shown.rows) } });
|
|
2356
|
+
});
|
|
2357
|
+
}
|
|
2358
|
+
else if (tapped.file !== undefined) {
|
|
2359
|
+
// A file without its gate never reaches here (chat-decide pairs them); fail closed: nothing that approves is shown.
|
|
2360
|
+
if (tapped.edit !== undefined)
|
|
2361
|
+
dropDecideTokens(store, tapped.edit.tokens, clock());
|
|
2362
|
+
editText(`${message.text ?? ""}\n\nThis release can't be approved here. Open it in Toolroll.`.trim().slice(0, 4000), []);
|
|
2363
|
+
}
|
|
2364
|
+
else if (tapped.edit !== undefined)
|
|
2313
2365
|
editText(tapped.edit.text.slice(0, 4000), decideKeyboard(context.conversation?.phoneOrigin, tapped.edit.rows));
|
|
2314
2366
|
if (tapped.prompt !== undefined) {
|
|
2315
2367
|
const prompt = tapped.prompt;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.73",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|