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.
Files changed (52) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +7 -0
  3. package/dist/assignment.d.ts +1 -0
  4. package/dist/assignment.js +2 -2
  5. package/dist/browser-workspace.d.ts +3 -3
  6. package/dist/builder.js +4 -7
  7. package/dist/chat-acceptance.d.ts +1 -1
  8. package/dist/contracts/contract.d.ts +33 -5
  9. package/dist/contracts/contract.js +143 -16
  10. package/dist/contracts/flow-propose.d.ts +325 -0
  11. package/dist/contracts/flow-propose.js +41 -0
  12. package/dist/contracts/flow.d.ts +19400 -0
  13. package/dist/contracts/flow.js +359 -0
  14. package/dist/contracts/handoff.d.ts +91 -0
  15. package/dist/contracts/handoff.js +82 -0
  16. package/dist/contracts/proof.d.ts +136 -0
  17. package/dist/contracts/proof.js +110 -0
  18. package/dist/contracts/verification-receipt.d.ts +148 -0
  19. package/dist/contracts/verification-receipt.js +87 -0
  20. package/dist/demo.js +28 -11
  21. package/dist/desktop-update.d.ts +2 -2
  22. package/dist/evidence.d.ts +3 -40
  23. package/dist/evidence.js +5 -1
  24. package/dist/flow-gallery.d.ts +3 -2
  25. package/dist/flow-share.d.ts +10 -32
  26. package/dist/flow-share.js +65 -83
  27. package/dist/flow-starters.d.ts +2 -1
  28. package/dist/flow-triggers.d.ts +15 -72
  29. package/dist/flow-triggers.js +94 -83
  30. package/dist/flows.d.ts +37 -218
  31. package/dist/flows.js +350 -385
  32. package/dist/mate-tools.js +14 -44
  33. package/dist/mcp-connect.d.ts +3 -0
  34. package/dist/mcp-connect.js +16 -4
  35. package/dist/operate.js +31 -2
  36. package/dist/plan.d.ts +5 -5
  37. package/dist/plan.js +5 -25
  38. package/dist/process-recovery-eligibility.d.ts +2 -2
  39. package/dist/process-recovery-eligibility.js +13 -8
  40. package/dist/proof.d.ts +15 -91
  41. package/dist/proof.js +90 -249
  42. package/dist/result-evidence-readers.d.ts +42 -0
  43. package/dist/result-evidence-readers.js +91 -0
  44. package/dist/serve.js +8 -99
  45. package/dist/setup-guide.d.ts +2 -2
  46. package/dist/surface.js +8 -1
  47. package/dist/task-review-brief.d.ts +75 -0
  48. package/dist/task-review-brief.js +179 -0
  49. package/dist/text-limits.d.ts +70 -0
  50. package/dist/text-limits.js +70 -0
  51. package/dist/verification-evidence.js +24 -12
  52. package/package.json +1 -1
@@ -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, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps, stepsFor } from "./flows.js";
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: schema({
894
- operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "choose", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit", "starter"] },
895
- repo: REPO_ARG, flow: { type: "integer", minimum: 1 }, card: { type: "integer", minimum: 1 }, kit: { type: "string", enum: KITS.map(one => one.id) },
896
- starter: { type: "string", enum: [...STARTER_IDS] },
897
- name: { type: "string", maxLength: 80 }, template: { type: "string", enum: FLOW_TEMPLATES.map(one => one.id) },
898
- steps: { type: "array", minItems: 1, maxItems: 24, items: { type: "object", additionalProperties: false, properties: {
899
- id: { type: "string", maxLength: 32 }, title: { type: "string", maxLength: 60 }, kind: { type: "string", enum: [...FLOW_STAGE_KINDS] },
900
- instructions: { type: "string", maxLength: TEXT_LIMITS.flowInstructions }, planning: { type: "string", enum: ["auto", "required", "skip"] },
901
- decider: { type: "string", maxLength: 64 }, message: { type: "string", maxLength: 1000 },
902
- script: { type: "string", maxLength: 40 }, close: { type: "boolean" },
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.
@@ -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
@@ -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
- const scopes = Array.isArray(resource?.["scopes_supported"]) ? resource["scopes_supported"].filter((one) => typeof one === "string") : [];
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: scopes.slice(0, 20), resource: https(named) ? named : null };
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
- /** The retired command is a typed refusal, never a new provider request. */
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 { ContractIssue } from "./contracts/contract.js";
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 path-named line is the message, and the reason keeps the codes the rest of
75
- * the code relies on — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` —
76
- * so repair eligibility (document-only problems) and durable outcomes read the same as before the contract.
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 function planProblemOf(issue: ContractIssue): PlanProblem;
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 path-named line is the message, and the reason keeps the codes the rest of
167
- * the code relies on — `missing-goal`, `bad-touches`, `plan-too-long`, `amendment-too-long`, `acceptance-too-many` —
168
- * so repair eligibility (document-only problems) and durable outcomes read the same as before the contract.
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 function planProblemOf(issue) {
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 | null;
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: import("./scope.js").EvidenceKind;
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
- const receipt = JSON.parse(gate.bytes);
101
- if (receipt.version !== 1 || receipt.source !== undefined || receipt.reusedFrom !== undefined || receipt.executedHere !== undefined ||
102
- receipt.result?.configured !== true || receipt.result.ran !== true || receipt.result.exitCode !== 0 ||
103
- Object.keys(receipt.result).some(key => !["configured", "ran", "exitCode", "setupReplayed"].includes(key)) ||
104
- (receipt.result.setupReplayed !== undefined && receipt.result.setupReplayed !== true) ||
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 handoff = JSON.parse(saved.content.toString("utf8"));
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.schema !== 1 || handoff.runId !== runId || handoff.taskId !== ref.externalId || handoff.sessionId !== null ||
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?.currentAsOf !== run.headRevision ||
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 { type EvidenceKind } from "./scope.js";
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 ParsedCheck = {
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
- /** Caps are BYTES of UTF-8, matching the scout report's rule: a payload is
90
- * the same size whatever script it is written in. */
91
- export declare const PROOF_LIMITS: {
92
- readonly payload: number;
93
- readonly criteria: 12;
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
- /** Re-serialize the validated shape, never the agent's raw bytes (the
112
- * scout report's rule, `scout.ts`): what is stored and later hash-verified
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
- /** The plane's own re-run of the repository's approved verification
259
- * command, when one is configured. `ran: false` covers both "none
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. */