toolroll 0.9.34 → 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 +44 -0
- package/README.md +7 -0
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +1 -1
- package/dist/contracts/contract.d.ts +33 -5
- package/dist/contracts/contract.js +143 -16
- 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/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 +2 -2
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- 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 +31 -2
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- 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/serve.js +8 -99
- package/dist/setup-guide.d.ts +2 -2
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +75 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/text-limits.d.ts +70 -0
- package/dist/text-limits.js +70 -0
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
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
|
*
|
|
@@ -261,6 +262,7 @@ export const OPERATE_HELP = `toolroll — operating the queue
|
|
|
261
262
|
toolroll backup now|list back the database up now; list backups and how the last ones went
|
|
262
263
|
toolroll restore <file> [--dry-run] put a backup back (Toolroll stopped; the current database is kept)
|
|
263
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
|
|
264
266
|
toolroll policy show|set the organisation policy; an instance operator sets it with --providers claude,codex|any,
|
|
265
267
|
--models <m,…>|any, --tools <t,…>|any, --ceiling safe|standard|escalated
|
|
266
268
|
toolroll ledger verify check the action ledger's hash chain (--checkpoint <n:hash> to compare a copied head)
|
|
@@ -665,7 +667,7 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
665
667
|
"task",
|
|
666
668
|
]);
|
|
667
669
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
668
|
-
"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",
|
|
669
671
|
"clear", "follow", "ready", "all-tasks", "inbound-only", "help", "undo", "anyone", "allow-dispatch", "allow-merge", "merge-delete-branch",
|
|
670
672
|
"no-open", "remove", "no-verify", "no-follow", "end", "report", "off", "tmux",
|
|
671
673
|
"self-heal", "plan-auto", "chat-approve", "repair-auto", "review-retry-auto", "no-local",
|
|
@@ -5171,9 +5173,36 @@ async function planTaskCommand(positional, flags, context) {
|
|
|
5171
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.",
|
|
5172
5174
|
]);
|
|
5173
5175
|
}
|
|
5174
|
-
/**
|
|
5176
|
+
/** Read saved work; neither form requests a new provider review. */
|
|
5175
5177
|
async function reviewTaskCommand(positional, flags, context) {
|
|
5176
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);
|
|
5177
5206
|
const [runText] = positional;
|
|
5178
5207
|
const runId = Number(runText ?? "");
|
|
5179
5208
|
if (runText === undefined || !Number.isInteger(runId) || runId < 1) {
|
package/dist/plan.d.ts
CHANGED
|
@@ -6,7 +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
|
|
9
|
+
import { type ContractIssue } from "./contracts/contract.js";
|
|
10
10
|
export type PlanProblem = {
|
|
11
11
|
reason: string;
|
|
12
12
|
message: string;
|
|
@@ -71,11 +71,11 @@ export type PlanParseResult = {
|
|
|
71
71
|
problems: PlanProblem[];
|
|
72
72
|
};
|
|
73
73
|
/**
|
|
74
|
-
* A contract issue as a plan problem: the
|
|
75
|
-
*
|
|
76
|
-
*
|
|
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
77
|
*/
|
|
78
|
-
export declare
|
|
78
|
+
export declare const planProblemOf: (issue: ContractIssue) => PlanProblem;
|
|
79
79
|
/**
|
|
80
80
|
* The planner's handoff, read against the plan contract (src/contracts/plan.ts): fail closed, every problem at once,
|
|
81
81
|
* each one naming its path (`acceptance[0].evidence[0]: must be one of ...`). A plan saved before plans carried
|
package/dist/plan.js
CHANGED
|
@@ -9,6 +9,7 @@ import { createHash } from "node:crypto";
|
|
|
9
9
|
import { hasForbiddenControls } from "./decision.js";
|
|
10
10
|
import { parseAcceptanceCriteria } from "./scope.js";
|
|
11
11
|
import { TEXT_LIMITS } from "./text-limits.js";
|
|
12
|
+
import { contractProblemOf } from "./contracts/contract.js";
|
|
12
13
|
import { planPayloadBody, readPlanPayload } from "./contracts/plan.js";
|
|
13
14
|
const EXECUTION_PLAN_SECTIONS = ["approach", "milestones", "dependencies", "risks", "proof"];
|
|
14
15
|
const EXECUTION_PLAN_LIST_CAP = 12;
|
|
@@ -163,32 +164,11 @@ function prose(value, field, cap, required, problems) {
|
|
|
163
164
|
return value;
|
|
164
165
|
}
|
|
165
166
|
/**
|
|
166
|
-
* A contract issue as a plan problem: the
|
|
167
|
-
*
|
|
168
|
-
*
|
|
167
|
+
* A contract issue as a plan problem (`contractProblemOf`): the reason keeps the codes the rest of the code relies on
|
|
168
|
+
* — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` — so repair eligibility
|
|
169
|
+
* (document-only problems) and durable outcomes read the same as before the contract.
|
|
169
170
|
*/
|
|
170
|
-
export
|
|
171
|
-
const at = issue.path;
|
|
172
|
-
const reason = (() => {
|
|
173
|
-
switch (issue.kind) {
|
|
174
|
-
case "required":
|
|
175
|
-
case "empty":
|
|
176
|
-
case "too-few":
|
|
177
|
-
return `missing-${at}`;
|
|
178
|
-
case "too-long":
|
|
179
|
-
return `${at}-too-long`;
|
|
180
|
-
case "too-many":
|
|
181
|
-
return `${at}-too-many`;
|
|
182
|
-
case "unknown-key":
|
|
183
|
-
return `${at}-unknown-key`;
|
|
184
|
-
case "newer-version":
|
|
185
|
-
return "newer-version";
|
|
186
|
-
default:
|
|
187
|
-
return `bad-${at}`;
|
|
188
|
-
}
|
|
189
|
-
})();
|
|
190
|
-
return { reason, message: issue.line };
|
|
191
|
-
}
|
|
171
|
+
export const planProblemOf = contractProblemOf;
|
|
192
172
|
/** scope.ts's rubric problem as a path-named line: `acceptance[0].id: is over 40 bytes`. */
|
|
193
173
|
function acceptanceLine(problem) {
|
|
194
174
|
const named = /^(acceptance(?:\[\d+\])?(?:\.[A-Za-z]+)?) (.*)$/.exec(problem.message);
|
|
@@ -37,7 +37,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
37
37
|
leaseId: string;
|
|
38
38
|
role: "builder";
|
|
39
39
|
outcome: "built" | "no-change";
|
|
40
|
-
committed: boolean
|
|
40
|
+
committed: boolean;
|
|
41
41
|
startedAt: string;
|
|
42
42
|
finishedAt: string;
|
|
43
43
|
branch: string;
|
|
@@ -81,7 +81,7 @@ export declare function preparedCandidateObserverGapEligibility(store: Store, in
|
|
|
81
81
|
readonly state: import("./proof.js").CriterionMatrixState;
|
|
82
82
|
readonly detail: readonly string[];
|
|
83
83
|
readonly answered: readonly {
|
|
84
|
-
readonly kind:
|
|
84
|
+
readonly kind: "changed-path" | "check" | "manual-review" | "screenshot";
|
|
85
85
|
readonly ref: string;
|
|
86
86
|
}[];
|
|
87
87
|
readonly review: {
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
9
|
import { normalizeBootId } from "./boot-identity.js";
|
|
10
10
|
import { scopeApprovedForDispatch } from "./dispatch.js";
|
|
11
|
+
import { parseHandoffArtifact } from "./contracts/handoff.js";
|
|
12
|
+
import { readVerificationReceipt } from "./contracts/verification-receipt.js";
|
|
11
13
|
import { readVerifiedArtifact } from "./evidence.js";
|
|
12
14
|
import { ownedProcessCount, runOwnerTag } from "./exec.js";
|
|
13
15
|
import { scopeAuthorityOf } from "./scope.js";
|
|
@@ -97,11 +99,11 @@ export function preparedCandidateObserverGapEligibility(store, input) {
|
|
|
97
99
|
const gate = verificationEvidence(store, evidenceRoot, runId);
|
|
98
100
|
if (!gate.ok || gate.bytes === null)
|
|
99
101
|
return refuse("verification-evidence-unavailable");
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
102
|
+
// A legacy log-header view is not a sealed receipt: the strict schema refuses its extra keys.
|
|
103
|
+
const view = JSON.parse(gate.bytes);
|
|
104
|
+
const receipt = readVerificationReceipt(view);
|
|
105
|
+
if (!receipt.ok || view.version !== 1 || receipt.value.reusedFrom !== undefined || receipt.value.executedHere !== undefined ||
|
|
106
|
+
receipt.value.result.configured !== true || receipt.value.result.ran !== true || receipt.value.result.exitCode !== 0 ||
|
|
105
107
|
!instant(gateArtifact.createdAt) || Date.parse(gateArtifact.createdAt) < Date.parse(run.startedAt) ||
|
|
106
108
|
Date.parse(gateArtifact.createdAt) > Date.parse(machine.decidedAt) ||
|
|
107
109
|
Date.parse(gateArtifact.createdAt) > Date.parse(run.finishedAt))
|
|
@@ -115,12 +117,15 @@ export function preparedCandidateObserverGapEligibility(store, input) {
|
|
|
115
117
|
const saved = readVerifiedArtifact(evidenceRoot, artifact);
|
|
116
118
|
if (!saved.ok)
|
|
117
119
|
return refuse("machine-prepared-handoff-unavailable");
|
|
118
|
-
const
|
|
120
|
+
const read = parseHandoffArtifact(saved.content.toString("utf8"));
|
|
121
|
+
if (!read.ok)
|
|
122
|
+
return refuse("machine-prepared-handoff-mismatch");
|
|
123
|
+
const handoff = read.value;
|
|
119
124
|
const conclusion = `Prepared candidate ${scope.candidate} was checked out by the machine; no agent ran. ${run.outcome === "no-change" ? "The branch already matched it." : "The sealed diff spans this task's base to that candidate."}`;
|
|
120
|
-
if (handoff.
|
|
125
|
+
if (handoff.runId !== runId || handoff.taskId !== ref.externalId || handoff.sessionId !== null ||
|
|
121
126
|
handoff.provider !== run.provider || handoff.branch !== run.branch || handoff.worktree !== run.worktree ||
|
|
122
127
|
handoff.base !== run.baseRevision || handoff.head !== run.headRevision || handoff.outcome !== run.outcome ||
|
|
123
|
-
handoff.committed !== run.committed || handoff.freshness
|
|
128
|
+
handoff.committed !== run.committed || handoff.freshness.currentAsOf !== run.headRevision ||
|
|
124
129
|
handoff.conclusion !== conclusion || run.handoff !== conclusion ||
|
|
125
130
|
!instant(artifact.createdAt) || Date.parse(artifact.createdAt) < Date.parse(run.startedAt) ||
|
|
126
131
|
Date.parse(artifact.createdAt) > Date.parse(gateArtifact.createdAt))
|
package/dist/proof.d.ts
CHANGED
|
@@ -18,31 +18,12 @@
|
|
|
18
18
|
* returned — is gathered by the caller (`builder.ts`) and handed in as data,
|
|
19
19
|
* so the review itself stays testable without a filesystem or a database.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
21
|
+
import type { EvidenceKind } from "./scope.js";
|
|
22
|
+
import { type ContractProblem } from "./contracts/contract.js";
|
|
23
|
+
import type { VerifyCommandFacts } from "./contracts/verification-receipt.js";
|
|
24
|
+
import { type CriterionEvidenceRef, type ParsedProof } from "./contracts/proof.js";
|
|
25
|
+
export { PROOF_LIMITS, type CriterionEvidenceRef, type ParsedCheck, type ParsedCriterion, type ParsedProof, type ParsedScreenshot } from "./contracts/proof.js";
|
|
22
26
|
export type ProofVerdict = "verified" | "attested" | "short" | "refuted";
|
|
23
|
-
/** One typed reference the proof cites to answer a criterion's required
|
|
24
|
-
* evidence: `ref` names an existing check's command, an existing
|
|
25
|
-
* screenshot's path, a path inside `changed`, or (kind `manual-review`)
|
|
26
|
-
* free-text pointing at nothing machine-checkable. Additive (v39): a
|
|
27
|
-
* criterion with no `evidence` array parses the same as one that always
|
|
28
|
-
* had none — legacy proofs, and every proof against a rubric-less scope,
|
|
29
|
-
* are untouched. */
|
|
30
|
-
export type CriterionEvidenceRef = {
|
|
31
|
-
kind: EvidenceKind;
|
|
32
|
-
ref: string;
|
|
33
|
-
};
|
|
34
|
-
export type ParsedCriterion = {
|
|
35
|
-
id: string;
|
|
36
|
-
statement: string;
|
|
37
|
-
/** pending-verification asserts all agent-owned work is met; only the
|
|
38
|
-
* machine-owned final check remains. It never upgrades a legacy failure. */
|
|
39
|
-
verdict: "met" | "not-met" | "not-checked" | "pending-verification";
|
|
40
|
-
how: string;
|
|
41
|
-
/** v39: typed references answering a SIGNED criterion by exact id.
|
|
42
|
-
* `[]` for a criterion the agent added beyond the rubric, or for any
|
|
43
|
-
* proof written before this migration. */
|
|
44
|
-
evidence: CriterionEvidenceRef[];
|
|
45
|
-
};
|
|
46
27
|
/** v39: the rubric side of a criterion, as `adjudicate` reads it — the
|
|
47
28
|
* SIGNED id, statement, and required evidence kinds. Never `how` (never
|
|
48
29
|
* signed). Empty = nothing signed: `adjudicate` runs the v1 rules,
|
|
@@ -54,31 +35,7 @@ export type ApprovedCriterion = {
|
|
|
54
35
|
};
|
|
55
36
|
/** Only submission defects. A truthful not-met answer is not malformed. */
|
|
56
37
|
export declare function proofSubmissionProblems(proof: ParsedProof, rubric: readonly ApprovedCriterion[]): string[];
|
|
57
|
-
export type
|
|
58
|
-
command: string;
|
|
59
|
-
exitCode: number;
|
|
60
|
-
summary: string;
|
|
61
|
-
};
|
|
62
|
-
/** A screenshot the agent claims proves UI-facing work. `path` is the
|
|
63
|
-
* repository-relative path the file lived at in the worktree when the
|
|
64
|
-
* agent wrote the proof — validated as a path shape here; validated as an
|
|
65
|
-
* actual bounded PNG/JPEG, and read, by the caller. */
|
|
66
|
-
export type ParsedScreenshot = {
|
|
67
|
-
path: string;
|
|
68
|
-
caption: string;
|
|
69
|
-
};
|
|
70
|
-
export type ParsedProof = {
|
|
71
|
-
version: 1;
|
|
72
|
-
criteria: ParsedCriterion[];
|
|
73
|
-
checks: ParsedCheck[];
|
|
74
|
-
changed: string[];
|
|
75
|
-
caveats: string[];
|
|
76
|
-
screenshots: ParsedScreenshot[];
|
|
77
|
-
};
|
|
78
|
-
export type ProofProblem = {
|
|
79
|
-
reason: string;
|
|
80
|
-
message: string;
|
|
81
|
-
};
|
|
38
|
+
export type ProofProblem = ContractProblem;
|
|
82
39
|
export type ProofParseResult = {
|
|
83
40
|
ok: true;
|
|
84
41
|
proof: ParsedProof;
|
|
@@ -86,31 +43,14 @@ export type ProofParseResult = {
|
|
|
86
43
|
ok: false;
|
|
87
44
|
problems: ProofProblem[];
|
|
88
45
|
};
|
|
89
|
-
/**
|
|
90
|
-
* the
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
readonly criterionId: 40;
|
|
95
|
-
readonly criterionStatement: 300;
|
|
96
|
-
readonly criterionHow: 500;
|
|
97
|
-
readonly evidencePerCriterion: 4;
|
|
98
|
-
readonly evidenceRef: 300;
|
|
99
|
-
readonly checks: 12;
|
|
100
|
-
readonly checkCommand: 300;
|
|
101
|
-
readonly checkSummary: 300;
|
|
102
|
-
readonly changed: 64;
|
|
103
|
-
readonly changedPath: 300;
|
|
104
|
-
readonly caveats: 8;
|
|
105
|
-
readonly caveat: 300;
|
|
106
|
-
readonly screenshots: 8;
|
|
107
|
-
readonly screenshotPath: 300;
|
|
108
|
-
readonly screenshotCaption: 300;
|
|
109
|
-
};
|
|
46
|
+
/**
|
|
47
|
+
* Read a proof: the payload byte cap and JSON first, then the proof schema (version 2 as itself, the version 1 a
|
|
48
|
+
* builder writes upgraded, a newer one refused plainly), then the plain-code rules. Every problem names its path and
|
|
49
|
+
* is reported at once; fail closed.
|
|
50
|
+
*/
|
|
110
51
|
export declare function parseProof(raw: string): ProofParseResult;
|
|
111
|
-
/**
|
|
112
|
-
*
|
|
113
|
-
* is exactly what this parser admitted, key order and all. */
|
|
52
|
+
/** Serialize the validated shape, never the agent's raw bytes (the scout report's rule, `scout.ts`): what is stored
|
|
53
|
+
* and later hash-verified is exactly what the schema admitted, in its key order. */
|
|
114
54
|
export declare function serializeProof(proof: ParsedProof): string;
|
|
115
55
|
/** One caveat that admits an exception to a criterion the same proof marks
|
|
116
56
|
* `met` — the proof's own verdict disagreeing with its own words (atomic
|
|
@@ -255,24 +195,8 @@ export declare function frozenCriterionProblems(submitted: ParsedProof, correcte
|
|
|
255
195
|
* correction and re-reads it after that correction and after the final
|
|
256
196
|
* gate; a reading that differs is never adjudicated from the cached one. */
|
|
257
197
|
export declare function sameDiffStatFacts(a: DiffStatFacts | null, b: DiffStatFacts | null): boolean;
|
|
258
|
-
/**
|
|
259
|
-
|
|
260
|
-
* configured" and "configured but the run could not attempt it" — the
|
|
261
|
-
* latter is distinguished by `attemptFailed`, which downgrades to `short`
|
|
262
|
-
* rather than `refuted`: "we could not check" is not "the claim is false". */
|
|
263
|
-
export type VerifyCommandFacts = {
|
|
264
|
-
configured: false;
|
|
265
|
-
} | {
|
|
266
|
-
configured: true;
|
|
267
|
-
ran: false;
|
|
268
|
-
attemptFailed: true;
|
|
269
|
-
failure?: "spawn-failed" | "timed-out" | "dependency-missing" | "setup-stale" | "setup-failed" | "own-install-failed" | "tracked-files-changed" | "setup-changed-files" | "checkout-moved" | "cleanliness-unavailable" | "dependency-still-missing" | "retry-spawn-failed" | "retry-timed-out" | "custody-lost";
|
|
270
|
-
} | {
|
|
271
|
-
configured: true;
|
|
272
|
-
ran: true;
|
|
273
|
-
exitCode: number;
|
|
274
|
-
setupReplayed?: true;
|
|
275
|
-
};
|
|
198
|
+
/** What the approved verification command did (the receipt's `result`); its schema lives with the receipt. */
|
|
199
|
+
export type { VerifyCommandFacts } from "./contracts/verification-receipt.js";
|
|
276
200
|
export type AdjudicateInput = {
|
|
277
201
|
/** New captures may assess the signed goal directly when the builder wrote
|
|
278
202
|
* no optional proof. Historical calls retain their original contract. */
|