toolroll 0.9.33 → 0.9.36
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 +57 -0
- package/README.md +7 -0
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +17 -17
- package/dist/browser-crew.js +1 -0
- package/dist/browser-workspace.d.ts +5 -3
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +124 -63
- package/dist/contracts/contract-test.d.ts +45 -0
- package/dist/contracts/contract-test.js +69 -0
- package/dist/contracts/contract.d.ts +87 -0
- package/dist/contracts/contract.js +308 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow.d.ts +19400 -0
- package/dist/contracts/flow.js +359 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/plan.d.ts +58 -0
- package/dist/contracts/plan.js +67 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/demo.js +28 -11
- package/dist/desktop-update.d.ts +1 -1
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +7 -3
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +65 -83
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -218
- package/dist/flows.js +350 -385
- package/dist/mate-tools.js +14 -44
- package/dist/mcp-connect.d.ts +3 -0
- package/dist/mcp-connect.js +16 -4
- package/dist/operate.js +83 -12
- package/dist/plan.d.ts +12 -12
- package/dist/plan.js +109 -72
- package/dist/planner-source.js +2 -1
- package/dist/planner.js +55 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-admission.d.ts +93 -0
- package/dist/project-admission.js +134 -0
- package/dist/project-delete-ui.js +5 -2
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/runner.d.ts +10 -0
- package/dist/runner.js +16 -0
- package/dist/scope.js +4 -3
- package/dist/serve.d.ts +4 -0
- package/dist/serve.js +112 -160
- package/dist/setup-guide.d.ts +2 -2
- package/dist/store.d.ts +2 -0
- package/dist/store.js +4 -0
- package/dist/surface.js +16 -3
- package/dist/task-review-brief.d.ts +75 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/text-limits.d.ts +87 -0
- package/dist/text-limits.js +86 -0
- package/dist/verification-evidence.js +24 -12
- package/dist/when-html.d.ts +2 -0
- package/dist/when-html.js +13 -0
- package/package.json +3 -2
package/dist/mate-tools.js
CHANGED
|
@@ -45,7 +45,10 @@ import { agentChoicesFor, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.
|
|
|
45
45
|
import { isNewModel, modelWords, priceWords, runtimeStates, seenModels } from "./model-catalog.js";
|
|
46
46
|
import { agentsSummary, chosenWords, isRiskLevel, isTaskSize, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, sizeSourceWords, sizeWords, specWords } from "./phase-routing.js";
|
|
47
47
|
import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommandLine, toolStanding } from "./project-tools.js";
|
|
48
|
-
import { deciderOf, durationWords, FLOW_KIND_WORDS,
|
|
48
|
+
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_TEMPLATES, flowFromSteps, stepsFor, withKeptSteps } from "./flows.js";
|
|
49
|
+
import { parseContract } from "./contracts/contract.js";
|
|
50
|
+
import { FLOW_ALIASES } from "./contracts/flow.js";
|
|
51
|
+
import { PROPOSE_FLOW_MODEL_SCHEMA, proposeFlowInputSchema } from "./contracts/flow-propose.js";
|
|
49
52
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
50
53
|
import { flowPersonOf } from "./flow-send.js";
|
|
51
54
|
import { flowInsights } from "./flow-insights.js";
|
|
@@ -890,49 +893,16 @@ export const MATE_TOOLS = [
|
|
|
890
893
|
{
|
|
891
894
|
name: "propose_flow",
|
|
892
895
|
description: "Draft a flow change as a card the operator confirms. To make a flow, create with a template or the steps in plain names; leave instructions out unless the operator gave them, and use decider 'me' when they decide. To change one, edit with the whole step list, keeping existing steps by id. For cards use add_card, move_card, approve, send_back with the operator's note, or cancel_card; find the card by what it is about; comment (with @name to ping someone), assign and follow here too. For triage, spam, lead, effort or exception routing, start from the matching template, with the sort as the first step. Use remindAfter and thenMoveTo for follow-ups and decisions that stall. The Issues to PRs template runs GitHub issues labelled toolroll through build, approval, pull request and a comment on the issue. When the operator asks for something to happen every time (fix CI when it fails, turn labelled issues into tasks, queue work overnight, review what went wrong every morning), offer the matching starter; each is one yes, and none merges without a person. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a kit sets up the teammate and its flow in one card. Teammates: propose_teammate adds one (from a template: support, sales, ops, triage), changes one rule section (edit_section), pauses, resumes or removes it, passes it a note, or answers its question for the operator; put it to work with a flow here. When the operator says something like 'Maya can approve refunds up to $100 now', change that section of her soul file; when it's for this week or a one-off, pass it as a note. Teammates use the project's tools under a rule per action (do it, ask first: the operator approves each exact call, never, or up to a limit on a number); propose_teammate use_tool, stop_tool and tool_rule change them, and when a rule has a limit also change the soul file so its words agree. forget or edit_memory when the operator says a teammate has something wrong; a rule change it suggests is a question for the operator, answered only as they say. propose_teammate add_routine puts a card on its desk on a schedule; propose_teammate undo undoes a tool call only when its action has an undo set (tool_rule undoWith). Actions: starter: switch on a starter flow in a project (repo, starter: ci-fix files a fix task when CI fails on the main branch, issue-task makes a task of each GitHub issue labelled toolroll, overnight holds cards added in the day until 22:00 and leaves results for the morning, plane-review reviews the plane's last 24 hours every morning and turns each problem worth fixing into a researched fix and a pull request) — its zones and trigger in one card; offer the matching one when the operator says to do something every time. kit: set up a starter kit in a project (repo, kit: support-desk, bug-triage, sales-follow-up or ops-requests) — a teammate, the flow it works and its buttons, in one card; prefer it when the operator wants a support desk, bug triage, sales follow-up or requests handled. create: a template, or the steps in order (each leads to the next; Done is added; instructions may be left out). 'request' calls a web address (method, url with its host written out, headers — {{secret.NAME}} uses a secret the operator saved on the step, never in chat — and body); 'email' sends mail (to, subject, body; {{card.email}} is the card's email address); 'tool' calls one of the project's tools (server: the tool's name from get_project_tools, tool: its function, args: an object). Put a decision before any of these when they send what a model wrote or what an outsider sent. A 'draft' step has Claude write something from the card (instructions: what to write); follow it with an approval step (decider 'owner' asks the flow's owner in their chat app, where they can approve, edit or send it back), then an 'update' or 'notify' step whose message is '{{stage.<draft id>}}'. A 'sort' step has Jev pick one of its answers; make it the first step (never a holding step before it, or new cards wait unsorted): question, answers (answer, means: a few words Jev reads, goesTo: a step), sureAt (percent, default 80), ifNotSure (a step; otherwise the card waits for a person), and up to 3 alsoNote (score with levels lowest first, or yes-no); a sort has no next, so give each branch's last step its own next. A 'pull-request' step opens a pull request for the card's built result and waits for CI: next when it passes, ifFails when it fails (only this step defaults it: to the build before it, as a revision carrying the failing check); merge ('squash', 'merge' or 'rebase') merges once checks pass, and needs an approval step before it on every path. waitFor 'hours' with from and until (like '22:00' and '06:00') holds cards until the clock is inside those hours. A 'wait' step, after an 'email' step, waits for a reply to that email from someone it went to (waitFor 'reply', the default): next is where a reply goes (the reply is {{stage.<wait id>}}), ifNoReply where the card goes when none comes within wait (like '3 days', up to 30 days); waitFor 'time' just waits, then next. Any step but wait and done can have remindAfter (like '2 days': whoever it waits on is reminded once; 'none' removes it) and, on holding and approval steps, thenMoveTo (a step the card moves to then). AI teammates (get_teammates): an approval step with teammate (its short name) is decided by that teammate within its rules, and it hands hard ones to the step's decider; a 'teammate' step (teammate, instructions, routes of answer and goesTo) has it read the card, pick where it goes and write what the next steps send (the email body is then {{stage.<id>}}), asking the flow's owner when its rules say to. edit: the full step list, keeping existing steps by id — what a kept step leaves out carries over. add_card (starts in the first zone unless zone is named), move_card, approve, send_back (needs a note), cancel_card, comment (note; @name pings that person), assign (owner: a name, 'me', or 'nobody'), follow, unfollow, save_script (repo, and script: name, about, language python|node|shell, and either body (short) or file (a path in the project, like scripts/enrich.py), and timeoutMinutes; scripts belong to the project, so no flow is needed; a 'check' step in any of its flows names it). A 'check' step runs its script with the card as JSON on stdin (and in $FLOW_INPUT); what it prints is its result for later steps ({{stage.<id>}}); runIn 'folder' (an empty folder: for scripts that work on data) or 'copy' (a copy of the card's work, after setup: for tests on code; the default); routes (answer, goesTo) that a last printed line 'goto: <answer>' picks; ifFails is where a failing script sends the card, such as back to the build (it has no default: without it the card waits there); secrets (names of saved secrets it gets as variables). add_trigger with settings (kind button: label, questions; schedule: schedule like 'daily 09:00 Europe/London', and title, or script (a saved script whose printed items — one per line, a title or JSON with title, description, key — each become a card, once) with secrets; github: repo owner/name, watch issues|pulls|checks, label, branch, from team|anyone; linear: team, state, label; flow: follow (another flow's id), when (its zone); email: folder (default INBOX), sender (addresses or domains), subject (words it must contain); plane-review: at (HH:MM, default 07:30), timeZone — every day it reads the plane's last 24 hours and makes one card per problem worth fixing, a returning problem joining its card); pause_trigger, resume_trigger, remove_trigger with trigger. A 'send' step ('Send to me') sends the card's owner what the step before produced (summary, links, screenshots) in their chat apps, then moves on; a 'choose' step sends the same with 2 to 4 options (label, goesTo: a step, or 'end' to ignore the card) as buttons, ifReplied (where a reply goes as {{note}}; the build before by default), and remindAfter with ifNoReply. choose (card, and choice by number or a note to reply) answers one for the operator. A 'task' step may build in another project: repo (from list_repos). Read get_flows first except to create.",
|
|
893
|
-
inputSchema:
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
question: { type: "string", maxLength: 300 }, sureAt: { type: "integer", minimum: 50, maximum: 99 },
|
|
904
|
-
answers: { type: "array", minItems: 2, maxItems: 12, items: { type: "object", additionalProperties: false, properties: { answer: { type: "string", maxLength: 40 }, means: { type: "string", maxLength: 200 }, goesTo: { type: "string", maxLength: 60 } } } },
|
|
905
|
-
alsoNote: { type: "array", maxItems: 3, items: { type: "object", additionalProperties: false, properties: { question: { type: "string", maxLength: 300 }, kind: { type: "string", enum: ["score", "yes-no"] }, levels: { type: "array", minItems: 2, maxItems: 10, items: { type: "string", maxLength: 120 } } } } },
|
|
906
|
-
method: { type: "string", enum: ["GET", "POST", "PUT", "PATCH", "DELETE"] }, url: { type: "string", maxLength: 2000 },
|
|
907
|
-
headers: { type: "object", additionalProperties: { type: "string", maxLength: 500 } }, body: { type: "string", maxLength: 8000 },
|
|
908
|
-
to: { type: "string", maxLength: 500 }, subject: { type: "string", maxLength: 200 },
|
|
909
|
-
server: { type: "string", maxLength: 64 }, tool: { type: "string", maxLength: 100 }, args: { type: "object" },
|
|
910
|
-
runIn: { type: "string", enum: ["folder", "copy"] }, routes: { type: "array", maxItems: 12, items: { type: "object", additionalProperties: false, properties: { answer: { type: "string", maxLength: 40 }, goesTo: { type: "string", maxLength: 60 } } } },
|
|
911
|
-
secrets: { type: "array", maxItems: 10, items: { type: "string", maxLength: 40 } },
|
|
912
|
-
waitFor: { type: "string", enum: ["reply", "time", "hours"] }, wait: { type: "string", maxLength: 40 }, ifNoReply: { type: "string", maxLength: 60 },
|
|
913
|
-
from: { type: "string", maxLength: 5 }, until: { type: "string", maxLength: 5 }, merge: { type: "string", enum: ["squash", "merge", "rebase"] },
|
|
914
|
-
remindAfter: { type: "string", maxLength: 40 }, thenMoveTo: { type: "string", maxLength: 60 }, teammate: { type: "string", maxLength: 40 },
|
|
915
|
-
options: { type: "array", minItems: 2, maxItems: 4, items: { type: "object", additionalProperties: false, properties: { label: { type: "string", maxLength: 40 }, goesTo: { type: "string", maxLength: 60 } } } },
|
|
916
|
-
ifReplied: { type: "string", maxLength: 60 }, repo: REPO_ARG,
|
|
917
|
-
next: { type: "string", maxLength: 60 }, ifFails: { type: "string", maxLength: 60 }, ifNotSure: { type: "string", maxLength: 60 },
|
|
918
|
-
} } },
|
|
919
|
-
title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 4000 }, zone: { type: "string", maxLength: 60 }, note: { type: "string", maxLength: LIMITS.note, description: `At most ${LIMITS.note} characters; longer is refused with its length, not cut.` },
|
|
920
|
-
choice: { type: "integer", minimum: 1, maximum: 4 },
|
|
921
|
-
trigger: { type: "integer", minimum: 1 }, owner: { type: "string", maxLength: 64 },
|
|
922
|
-
script: { type: "object", additionalProperties: false, properties: { name: { type: "string", maxLength: 40 }, about: { type: "string", maxLength: 160 }, body: { type: "string", maxLength: 1600 }, timeoutMinutes: { type: "integer", minimum: 1, maximum: 60 },
|
|
923
|
-
language: { type: "string", enum: ["python", "node", "shell"] }, file: { type: "string", maxLength: 200 } } },
|
|
924
|
-
settings: { type: "object", additionalProperties: false, properties: {
|
|
925
|
-
kind: { type: "string", enum: FLOW_TRIGGER_KINDS.filter(one => one !== "webhook" && one !== "chat") }, zone: { type: "string", maxLength: 60 },
|
|
926
|
-
label: { type: "string", maxLength: 50 }, questions: { type: "array", maxItems: 6, items: { type: "string", maxLength: 80 } },
|
|
927
|
-
schedule: { type: "string", maxLength: 80 }, title: { type: "string", maxLength: 200 }, description: { type: "string", maxLength: 2000 },
|
|
928
|
-
repo: { type: "string", maxLength: 140 }, watch: { type: "string", enum: ["issues", "pulls", "checks"] }, branch: { type: "string", maxLength: 100 }, from: { type: "string", enum: ["team", "anyone"] },
|
|
929
|
-
team: { type: "string", maxLength: 12 }, state: { type: "string", maxLength: 40 }, follow: { type: "integer", minimum: 1 }, when: { type: "string", maxLength: 60 },
|
|
930
|
-
folder: { type: "string", maxLength: 100 }, sender: { type: "string", maxLength: 300 }, subject: { type: "string", maxLength: 100 },
|
|
931
|
-
script: { type: "string", maxLength: 40 }, secrets: { type: "array", maxItems: 10, items: { type: "string", maxLength: 40 } },
|
|
932
|
-
at: { type: "string", maxLength: 5 }, timeZone: { type: "string", maxLength: 60 },
|
|
933
|
-
} },
|
|
934
|
-
}, ["operation"]),
|
|
935
|
-
handle: (ctx, args) => {
|
|
896
|
+
inputSchema: { ...PROPOSE_FLOW_MODEL_SCHEMA },
|
|
897
|
+
handle: (ctx, given) => {
|
|
898
|
+
// The call is read by the schema the lead was given: a refusal names each path (`steps[0].routes[0].goesTo: required`).
|
|
899
|
+
// An edit's kept steps may leave their kind and name to the zone they keep.
|
|
900
|
+
const editing = given["operation"] === "edit" && Number.isSafeInteger(given["flow"]) ? ctx.store.getFlow(Number(given["flow"])) : null;
|
|
901
|
+
const kept = editing !== null && ctx.who.repos.includes(editing.repo) ? flowDefinitionOf(editing) : null;
|
|
902
|
+
const read = parseContract(proposeFlowInputSchema, given["steps"] === undefined ? given : { ...given, steps: withKeptSteps(given["steps"], kept) }, FLOW_ALIASES);
|
|
903
|
+
if (!read.ok)
|
|
904
|
+
return { ok: false, message: read.issues.map(one => one.line).join("\n") };
|
|
905
|
+
const args = read.value;
|
|
936
906
|
const pick = (keys) => Object.fromEntries(keys.filter(key => args[key] !== undefined).map(key => [key, args[key]]));
|
|
937
907
|
// "me" in a step means the operator; the drawing stores their name.
|
|
938
908
|
// A build step in another project names it as list_repos does (r2); the drawing keeps its path.
|
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -11,6 +11,9 @@ export type OneClick = {
|
|
|
11
11
|
url: string;
|
|
12
12
|
about: string;
|
|
13
13
|
reads?: readonly string[];
|
|
14
|
+
/** Send no scope: the server applies its own preset for this resource (PostHog's MCP preset). Naming scopes there
|
|
15
|
+
* shows a 75-group permission grid instead, and the person can stop on it without ever coming back. */
|
|
16
|
+
serverScopes?: true;
|
|
14
17
|
};
|
|
15
18
|
/**
|
|
16
19
|
* Services that connect this way: their servers speak streamable HTTP and
|
package/dist/mcp-connect.js
CHANGED
|
@@ -52,7 +52,7 @@ export const ONE_CLICK = [
|
|
|
52
52
|
// Mobbin's sign-in is its Supabase auth server, on another origin: its protected-resource metadata names it.
|
|
53
53
|
{ id: "mobbin", label: "Mobbin", url: "https://api.mobbin.com/mcp", about: "Real app screens and flows to learn from.",
|
|
54
54
|
reads: ["screen", "flow", "app", "site", "element", "pattern", "ui", "ios", "android", "web"] },
|
|
55
|
-
{ id: "posthog", label: "PostHog", url: "https://mcp.posthog.com/mcp", about: "Product analytics, funnels and events.",
|
|
55
|
+
{ id: "posthog", label: "PostHog", url: "https://mcp.posthog.com/mcp", about: "Product analytics, funnels and events.", serverScopes: true,
|
|
56
56
|
reads: ["insight", "run", "event", "definition", "property", "properties"] },
|
|
57
57
|
{ id: "betterstack", label: "Better Stack", url: "https://mcp.betterstack.com", about: "Uptime checks and incidents.",
|
|
58
58
|
reads: ["uptime", "monitor", "incident", "availability", "response", "time"] },
|
|
@@ -147,6 +147,9 @@ const ACCESS = "OAUTH_ACCESS_TOKEN", REFRESH = "OAUTH_REFRESH_TOKEN", CLIENT = "
|
|
|
147
147
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
148
148
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
149
149
|
const renewTried = new Map();
|
|
150
|
+
/** RFC 6749 scope tokens; and the most scope text a sign-in address carries (addresses stay well under 8 KB). */
|
|
151
|
+
const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
152
|
+
const SCOPE_PARAM_LIMIT = 6_000;
|
|
150
153
|
const json = async (fetcher, url) => {
|
|
151
154
|
try {
|
|
152
155
|
const answer = await fetcher(url, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(10_000) });
|
|
@@ -195,10 +198,19 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
195
198
|
const methods = Array.isArray(meta["code_challenge_methods_supported"]) ? meta["code_challenge_methods_supported"] : ["S256"];
|
|
196
199
|
if (!methods.includes("S256"))
|
|
197
200
|
return null;
|
|
198
|
-
|
|
201
|
+
// Ask for every scope the server lists for this resource: a token missing one the server itself requires is
|
|
202
|
+
// refused after a successful sign-in (PostHog lists 155 and needs user:read, the 140th). Only well-formed scope
|
|
203
|
+
// tokens, and at most what fits a sign-in address.
|
|
204
|
+
const listed = Array.isArray(resource?.["scopes_supported"]) ? resource["scopes_supported"].filter((one) => typeof one === "string" && SCOPE_TOKEN.test(one)) : [];
|
|
205
|
+
const scopes = [];
|
|
206
|
+
for (const one of listed) {
|
|
207
|
+
if (scopes.join(" ").length + one.length + 1 > SCOPE_PARAM_LIMIT)
|
|
208
|
+
break;
|
|
209
|
+
scopes.push(one);
|
|
210
|
+
}
|
|
199
211
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
200
212
|
const named = resource?.["resource"];
|
|
201
|
-
return { authorize: meta["authorization_endpoint"], token: meta["token_endpoint"], register: meta["registration_endpoint"], scopes
|
|
213
|
+
return { authorize: meta["authorization_endpoint"], token: meta["token_endpoint"], register: meta["registration_endpoint"], scopes, resource: https(named) ? named : null };
|
|
202
214
|
}
|
|
203
215
|
/**
|
|
204
216
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -240,7 +252,7 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
240
252
|
go.searchParams.set("code_challenge_method", "S256");
|
|
241
253
|
go.searchParams.set("state", state);
|
|
242
254
|
go.searchParams.set("resource", resource);
|
|
243
|
-
if (server.scopes.length > 0)
|
|
255
|
+
if (server.scopes.length > 0 && service.serverScopes !== true)
|
|
244
256
|
go.searchParams.set("scope", server.scopes.join(" "));
|
|
245
257
|
return { ok: true, go: go.toString(), state, visit: { service: service.id, repo: input.repo, by: input.by, kit: input.kit ?? null, template: input.template ?? null, verifier, clientId, clientSecret, token: server.token, resource, redirect, expires: now + VISIT_MS } };
|
|
246
258
|
}
|
package/dist/operate.js
CHANGED
|
@@ -53,6 +53,7 @@ import { FLOWS_VALUE_FLAGS, runFlowsCommand } from "./flows-cli.js";
|
|
|
53
53
|
import { runAssignmentCommand } from "./assignment-adapters.js";
|
|
54
54
|
import { applyProjectProfile, runProjectCommand } from "./project-cli.js";
|
|
55
55
|
import { pullRequestLines, runTaskMergeCommand, runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
56
|
+
import { taskReviewBrief, renderReviewBrief } from "./task-review-brief.js";
|
|
56
57
|
/**
|
|
57
58
|
* The commands that actually move work: authoring tasks, and the claim loop.
|
|
58
59
|
*
|
|
@@ -93,6 +94,7 @@ import { pushPass } from "./push.js";
|
|
|
93
94
|
import { chmodSync, closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, openSync, readFileSync, readSync, realpathSync, renameSync, unlinkSync, writeSync, writeFileSync, mkdirSync } from "node:fs";
|
|
94
95
|
import { BRANCH_PREFIX, envTwins, envValue, existingOrFirst, namedPath, taskBranches } from "./names.js";
|
|
95
96
|
import { claimActor, currentActor, parseLeadToken, withActor } from "./actor.js";
|
|
97
|
+
import { admissionRecorded, admitProject, commandLineActor } from "./project-admission.js";
|
|
96
98
|
import { leadClaim, leadSay, noteLeadWork } from "./lead-voice.js";
|
|
97
99
|
import { createServer as createNetServer } from "node:net";
|
|
98
100
|
import { spawn as spawnChild } from "node:child_process";
|
|
@@ -162,7 +164,7 @@ import { attendedLivenessState } from "./liveness.js";
|
|
|
162
164
|
import { HeldSessionCoordinator, sweepHeldOrphans } from "./held.js";
|
|
163
165
|
import { proposeGrant, describeGrant, describeWithheld, permits, MUTATION_CLASSES, DEFAULT_MUTATIONS, } from "./grant.js";
|
|
164
166
|
import { builtIn, guarded } from "./backend.js";
|
|
165
|
-
import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverDead, recoveredAnything, acquireWatchLeaseAuthed, heartbeatWatchLeaseAuthed, addRunnerReposAuthed, registerRunnerIfIdle, retireRunnerIfCurrent, normalizeRunnerName, validRunnerName, canonicalRepos, RUNNER_NAME_MAX, RUNNER_CAPACITY_MAX, parseRunnerCapacity, } from "./runner.js";
|
|
167
|
+
import { register, authenticate, heartbeat as heartbeatRunner, isAlive, recoverDead, recoveredAnything, acquireWatchLeaseAuthed, heartbeatWatchLeaseAuthed, addRunnerReposAuthed, removeRunnerRepoAuthed, registerRunnerIfIdle, retireRunnerIfCurrent, normalizeRunnerName, validRunnerName, canonicalRepos, RUNNER_NAME_MAX, RUNNER_CAPACITY_MAX, parseRunnerCapacity, } from "./runner.js";
|
|
166
168
|
import { mintCoordinator, revokeCoordinator, listCoordinators } from "./coordinator.js";
|
|
167
169
|
import { serveMcp } from "./mcp.js";
|
|
168
170
|
import { createInterface } from "node:readline";
|
|
@@ -260,6 +262,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
260
262
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
261
263
|
toolroll restore <file> [--dry-run] put a backup back (Toolroll stopped; the current database is kept)
|
|
262
264
|
toolroll review show|on|off --repo <path> one automatic review of each finished build; only HIGH findings send it back
|
|
265
|
+
toolroll task review <id> --brief compact saved result, checks and findings; --run <id>, --all, --json
|
|
263
266
|
toolroll policy show|set the organisation policy; an instance operator sets it with --providers claude,codex|any,
|
|
264
267
|
--models <m,…>|any, --tools <t,…>|any, --ceiling safe|standard|escalated
|
|
265
268
|
toolroll ledger verify check the action ledger's hash chain (--checkpoint <n:hash> to compare a copied head)
|
|
@@ -664,7 +667,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
664
667
|
"task",
|
|
665
668
|
]);
|
|
666
669
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
667
|
-
"json", "yes", "all", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
670
|
+
"json", "yes", "all", "brief", "local", "history", "latest-watch", "dry-run", "file", "allow-paid-fallback",
|
|
668
671
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
669
672
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
670
673
|
"self-heal", "plan-auto", "chat-approve", "repair-auto", "review-retry-auto", "no-local",
|
|
@@ -4934,6 +4937,7 @@ async function startConsole(options) {
|
|
|
4934
4937
|
...(options.repos === undefined ? {} : { repos: options.repos }),
|
|
4935
4938
|
...(options.projectRoots === undefined ? {} : { projectRoots: options.projectRoots }),
|
|
4936
4939
|
...(options.currentRepos === undefined ? {} : { currentRepos: options.currentRepos }),
|
|
4940
|
+
...(options.admittedRepos === undefined ? {} : { admittedRepos: options.admittedRepos }),
|
|
4937
4941
|
...(options.publicUrl === undefined ? {} : { publicUrl: options.publicUrl }),
|
|
4938
4942
|
...(options.registryPath === undefined ? {} : { registryPath: options.registryPath }),
|
|
4939
4943
|
...(options.upConsole === undefined ? {} : { upConsole: options.upConsole }),
|
|
@@ -5169,9 +5173,36 @@ async function planTaskCommand(positional, flags, context) {
|
|
|
5169
5173
|
autoPlan ? "An unchanged, verified plan can auto-approve under your signed mode. Amendments and questions still wait for you." : "Its questions reach you like any decision; its plan lands as a scope for you to edit and approve.",
|
|
5170
5174
|
]);
|
|
5171
5175
|
}
|
|
5172
|
-
/**
|
|
5176
|
+
/** Read saved work; neither form requests a new provider review. */
|
|
5173
5177
|
async function reviewTaskCommand(positional, flags, context) {
|
|
5174
5178
|
const { store, write, json } = context;
|
|
5179
|
+
if (flags.has("brief")) {
|
|
5180
|
+
const allowed = new Set(["brief", "all", "run", "json", "db", "as", "token"]);
|
|
5181
|
+
for (const name of flags.keys())
|
|
5182
|
+
if (!allowed.has(name))
|
|
5183
|
+
return fail(write, json, "task review", "usage", `--${name} is not a task review --brief option.`, EXIT.usage);
|
|
5184
|
+
const task = positional[0], selected = flags.get("run");
|
|
5185
|
+
if (positional.length !== 1 || !task || task.length > 64 || /[\x00-\x1f\x7f]/.test(task) ||
|
|
5186
|
+
selected !== undefined && (typeof selected !== "string" || !/^[1-9]\d*$/.test(selected) || !Number.isSafeInteger(Number(selected)))) {
|
|
5187
|
+
return fail(write, json, "task review", "usage", "Use toolroll task review <task> --brief [--run <id>] [--all] [--json].", EXIT.usage);
|
|
5188
|
+
}
|
|
5189
|
+
// The normal CLI entry has already authenticated lead tokens. Local reads
|
|
5190
|
+
// stay available; a signed-in lead sees only its owner's admitted projects.
|
|
5191
|
+
let viewer = currentActor()?.account ?? null;
|
|
5192
|
+
if (viewer === null && (flags.has("as") || flags.has("token"))) {
|
|
5193
|
+
const acting = await askCredentials(flags, context);
|
|
5194
|
+
if (acting === null || !authenticateApprover(store, acting.name, acting.token).ok)
|
|
5195
|
+
return fail(write, json, "task review", "unauthenticated", "That sign-in is not valid.", EXIT.refused);
|
|
5196
|
+
viewer = acting.name;
|
|
5197
|
+
}
|
|
5198
|
+
const repos = viewer === null ? null : store.knownRepos().filter(repo => store.accountCanAccess(viewer, repo));
|
|
5199
|
+
const result = taskReviewBrief(store, task, context.evidenceRoot, context.now, { principal: "operator", repos, includeUnplaced: viewer === null }, { ...(selected === undefined ? {} : { run: Number(selected) }), all: flags.has("all"), secrets: [context.leadToken ?? "", text(flags, "token") ?? ""] });
|
|
5200
|
+
if (!result.ok)
|
|
5201
|
+
return fail(write, json, "task review", result.reason, result.message, EXIT.refused);
|
|
5202
|
+
return succeed(write, json, "task review", { brief: result.brief }, () => renderReviewBrief(result.brief));
|
|
5203
|
+
}
|
|
5204
|
+
if (flags.has("run") || flags.has("all"))
|
|
5205
|
+
return fail(write, json, "task review", "usage", "--run and --all require --brief.", EXIT.usage);
|
|
5175
5206
|
const [runText] = positional;
|
|
5176
5207
|
const runId = Number(runText ?? "");
|
|
5177
5208
|
if (runText === undefined || !Number.isInteger(runId) || runId < 1) {
|
|
@@ -8360,12 +8391,15 @@ async function onboardCommand(flags, context) {
|
|
|
8360
8391
|
return !("error" in registry) && registry.repos.includes(repo);
|
|
8361
8392
|
},
|
|
8362
8393
|
enroll: async (repo) => {
|
|
8394
|
+
// The same admission as `repos add` and the console's Add a project.
|
|
8395
|
+
const actor = commandLineActor(store, { databaseFile: context.databaseFile });
|
|
8396
|
+
if ("ok" in actor)
|
|
8397
|
+
return { ok: false, message: actor.message };
|
|
8363
8398
|
const before = await loadProjectRegistry(registryPath);
|
|
8364
|
-
const
|
|
8365
|
-
if (!
|
|
8366
|
-
return { ok: false, message:
|
|
8367
|
-
|
|
8368
|
-
return { ok: true, added: "error" in before || !before.repos.includes(repo) };
|
|
8399
|
+
const admitted = await admitProject(store, { registryFile: registryPath, path: repo, actor, now: clock() });
|
|
8400
|
+
if (!admitted.ok)
|
|
8401
|
+
return { ok: false, message: admitted.message };
|
|
8402
|
+
return { ok: true, added: "error" in before || !before.repos.includes(admitted.repo) };
|
|
8369
8403
|
},
|
|
8370
8404
|
checkConnection: seams.checkConnection ?? (agent => checker(agent)),
|
|
8371
8405
|
pullRequests: seams.pullRequests ?? (async (repo) => {
|
|
@@ -8495,6 +8529,9 @@ async function upCommand(flags, context) {
|
|
|
8495
8529
|
}
|
|
8496
8530
|
}
|
|
8497
8531
|
const explicitRepos = context.repoList ?? [];
|
|
8532
|
+
// Repositories this start was told to serve stay served while it runs: removing one from the saved list takes
|
|
8533
|
+
// effect at the next start, never under the process that was pinned to it.
|
|
8534
|
+
const pinnedRepos = new Set();
|
|
8498
8535
|
if (explicitRepos.length > 0) {
|
|
8499
8536
|
for (const input of explicitRepos) {
|
|
8500
8537
|
const root = await proveRepo(input);
|
|
@@ -8503,12 +8540,15 @@ async function upCommand(flags, context) {
|
|
|
8503
8540
|
}
|
|
8504
8541
|
if (!repos.includes(root))
|
|
8505
8542
|
repos.push(root);
|
|
8543
|
+
pinnedRepos.add(root);
|
|
8506
8544
|
}
|
|
8507
8545
|
}
|
|
8508
8546
|
else if (context.inferProjectFromCwd !== false) {
|
|
8509
8547
|
const cwdRepo = await proveRepo(process.cwd());
|
|
8510
8548
|
if (cwdRepo !== null && !repos.includes(cwdRepo))
|
|
8511
8549
|
repos.push(cwdRepo);
|
|
8550
|
+
if (cwdRepo !== null)
|
|
8551
|
+
pinnedRepos.add(cwdRepo);
|
|
8512
8552
|
}
|
|
8513
8553
|
if (repos.length === 0 && projectRoots.length === 0) {
|
|
8514
8554
|
return fail(write, json, "up", "no-projects", "choose where your projects live once: `toolroll up --project-root <dir>`", EXIT.refused);
|
|
@@ -8660,6 +8700,7 @@ async function upCommand(flags, context) {
|
|
|
8660
8700
|
poolRoot: pool,
|
|
8661
8701
|
repos,
|
|
8662
8702
|
currentRepos: () => [...activeRepos],
|
|
8703
|
+
admittedRepos: () => [...activeRepos],
|
|
8663
8704
|
upConsole: true,
|
|
8664
8705
|
attended: {
|
|
8665
8706
|
runner: runnerName,
|
|
@@ -8752,6 +8793,8 @@ async function upCommand(flags, context) {
|
|
|
8752
8793
|
// One record of project passes for the whole service: fair scheduling
|
|
8753
8794
|
// across its projects (project-concurrency.ts).
|
|
8754
8795
|
const projectPasses = new ProjectPasses();
|
|
8796
|
+
// Projects removed while running: each one's watch stops admitting work, finishes what it holds, then lets go.
|
|
8797
|
+
const releasedRepos = new Set();
|
|
8755
8798
|
const launchRepo = async (repo, bind) => {
|
|
8756
8799
|
if (loopResults.has(repo) || stopping)
|
|
8757
8800
|
return;
|
|
@@ -8776,7 +8819,7 @@ async function upCommand(flags, context) {
|
|
|
8776
8819
|
token: runnerToken,
|
|
8777
8820
|
repo,
|
|
8778
8821
|
progress: line => progress(`${prefix(repo)}${line}`),
|
|
8779
|
-
isStopping: () => stopping,
|
|
8822
|
+
isStopping: () => stopping || releasedRepos.has(repo),
|
|
8780
8823
|
onFollowController: controller => followControllers.push(controller),
|
|
8781
8824
|
containmentLines,
|
|
8782
8825
|
passes: projectPasses,
|
|
@@ -8789,6 +8832,19 @@ async function upCommand(flags, context) {
|
|
|
8789
8832
|
}).then(result => ({ repo, result }), error => ({ repo, result: { ok: false, reason: "loop-failed", detail: describe(error), ticks: 0, built: 0, broke: 0 } }));
|
|
8790
8833
|
loopResults.set(repo, loop);
|
|
8791
8834
|
void loop.then(({ repo: endedRepo, result }) => {
|
|
8835
|
+
if (releasedRepos.has(endedRepo) && !stopping) {
|
|
8836
|
+
// A removed project's watch has stopped: only now does the builder let go of it.
|
|
8837
|
+
const unbound = removeRunnerRepoAuthed(store, { name: runnerName, token: runnerToken, repo: endedRepo }, clock());
|
|
8838
|
+
if (!unbound.ok) {
|
|
8839
|
+
fatal = `the builder identity changed while removing ${endedRepo} (${unbound.reason})`;
|
|
8840
|
+
stop();
|
|
8841
|
+
}
|
|
8842
|
+
loopResults.delete(endedRepo);
|
|
8843
|
+
releasedRepos.delete(endedRepo);
|
|
8844
|
+
progress(`project removed — ${projectName(endedRepo)} is no longer built here`);
|
|
8845
|
+
markReady();
|
|
8846
|
+
return;
|
|
8847
|
+
}
|
|
8792
8848
|
// A loop that dies while its siblings live must not leave a partial
|
|
8793
8849
|
// cockpit standing silently (finding 5).
|
|
8794
8850
|
if (!result.ok && !stopping) {
|
|
@@ -8842,19 +8898,34 @@ async function upCommand(flags, context) {
|
|
|
8842
8898
|
stop();
|
|
8843
8899
|
break;
|
|
8844
8900
|
}
|
|
8901
|
+
// Removed from the saved list: stop building it and take it out of the console. A pinned start keeps its own.
|
|
8902
|
+
const saved = new Set(loaded.repos.map(one => canonicalProject(one) ?? one));
|
|
8903
|
+
for (const repo of [...activeRepos]) {
|
|
8904
|
+
if (stopping || pinnedRepos.has(repo) || saved.has(repo))
|
|
8905
|
+
continue;
|
|
8906
|
+
activeRepos.delete(repo);
|
|
8907
|
+
releasedRepos.add(repo);
|
|
8908
|
+
if (!loopResults.has(repo))
|
|
8909
|
+
releasedRepos.delete(repo);
|
|
8910
|
+
}
|
|
8845
8911
|
for (const candidate of loaded.repos) {
|
|
8846
8912
|
if (stopping)
|
|
8847
8913
|
break;
|
|
8848
8914
|
const canonical = canonicalProject(candidate);
|
|
8849
|
-
|
|
8915
|
+
// Still letting go of an earlier removal: it rejoins on the next pass after its watch has stopped.
|
|
8916
|
+
if (canonical !== null && (activeRepos.has(canonical) || releasedRepos.has(canonical)))
|
|
8850
8917
|
continue;
|
|
8851
8918
|
const approvedAdditions = context.additionalProjectRepos?.() ?? [];
|
|
8852
8919
|
const admittedCeiling = resolveCeiling([...dynamicCeiling.repos, ...approvedAdditions], dynamicCeiling.roots).ceiling;
|
|
8853
|
-
|
|
8920
|
+
// A project added after this start (`repos add`, the lead, the console) carries its recorded admission; a
|
|
8921
|
+
// registry line alone is not authority. Either way the folder is proved to be that Git checkout again.
|
|
8922
|
+
const allowed = canonical !== null &&
|
|
8923
|
+
((await authorizedProject(admittedCeiling, canonical)) || admissionRecorded(store, canonical)) &&
|
|
8924
|
+
(await proveRepo(canonical)) === canonical;
|
|
8854
8925
|
if (!allowed) {
|
|
8855
8926
|
if (!rejected.has(candidate)) {
|
|
8856
8927
|
rejected.add(candidate);
|
|
8857
|
-
progress(`project not connected — ${candidate}
|
|
8928
|
+
progress(`project not connected — ${candidate} isn't a Git repository added with \`toolroll repos add\` or the console`);
|
|
8858
8929
|
}
|
|
8859
8930
|
continue;
|
|
8860
8931
|
}
|
package/dist/plan.d.ts
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* the scope proposal is authority-bearing, so it gets the park discipline).
|
|
7
7
|
*/
|
|
8
8
|
import { type AcceptanceCriterion } from "./scope.js";
|
|
9
|
+
import { type ContractIssue } from "./contracts/contract.js";
|
|
9
10
|
export type PlanProblem = {
|
|
10
11
|
reason: string;
|
|
11
12
|
message: string;
|
|
@@ -69,18 +70,17 @@ export type PlanParseResult = {
|
|
|
69
70
|
ok: false;
|
|
70
71
|
problems: PlanProblem[];
|
|
71
72
|
};
|
|
72
|
-
/**
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
};
|
|
73
|
+
/**
|
|
74
|
+
* A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
|
|
75
|
+
* — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
|
|
76
|
+
* (document-only problems) and durable outcomes read the same as before the contract.
|
|
77
|
+
*/
|
|
78
|
+
export declare const planProblemOf: (issue: ContractIssue) => PlanProblem;
|
|
79
|
+
/**
|
|
80
|
+
* The planner's handoff, read against the plan contract (src/contracts/plan.ts): fail closed, every problem at once,
|
|
81
|
+
* each one naming its path (`acceptance[0].evidence[0]: must be one of ...`). A plan saved before plans carried
|
|
82
|
+
* `version` reads as it always did.
|
|
83
|
+
*/
|
|
84
84
|
export declare function parsePlan(raw: string): PlanParseResult;
|
|
85
85
|
export type MilestoneState = "pending" | "current" | "completed" | "blocked";
|
|
86
86
|
export type Milestone = {
|