toolroll 0.9.1 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/agent-fence.d.ts +11 -2
- package/dist/agent-fence.js +29 -3
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +3 -3
- package/dist/builder.js +21 -2
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +3 -0
- package/dist/desktop-update.d.ts +1 -1
- package/dist/flow-engine.d.ts +7 -0
- package/dist/flow-engine.js +47 -15
- package/dist/flow-insights.d.ts +9 -0
- package/dist/flow-insights.js +16 -0
- package/dist/flow-replies.js +1 -1
- package/dist/flow-starters.d.ts +6 -1
- package/dist/flow-starters.js +46 -3
- package/dist/flow-triggers.d.ts +20 -1
- package/dist/flow-triggers.js +105 -6
- package/dist/flows-cli.d.ts +228 -0
- package/dist/flows-cli.js +403 -0
- package/dist/flows-ui.js +1 -1
- package/dist/flows.d.ts +25 -4
- package/dist/flows.js +75 -2
- package/dist/guides.js +23 -0
- package/dist/keys.js +4 -1
- package/dist/mate-contract.js +1 -1
- package/dist/mate-tools.js +4 -3
- package/dist/operate.d.ts +1 -1
- package/dist/operate.js +39 -4
- package/dist/plane-review.d.ts +39 -0
- package/dist/plane-review.js +233 -0
- package/dist/store.d.ts +2 -0
- package/dist/store.js +5 -0
- package/dist/surface.js +4 -0
- package/package.json +1 -1
package/dist/mate-contract.js
CHANGED
|
@@ -19,7 +19,7 @@ export const MATE_CONTRACT = [
|
|
|
19
19
|
"For intake, a plain-language outcome is enough to draft a task. Infer title, narrow goal, safe non-goals and testable criteria; leave touches empty for discovery. Do not ask the operator for a title, paths, implementation details, acceptance wording, model, budget or safely inferable fields. Ask at most three questions with defaults for material ambiguity, conflicting goals or unresolved irreversible/public/security/data-loss/migration choices. With 'use your judgment', use reversible defaults; real blockers may still arise.",
|
|
20
20
|
"Set propose_task planning to 'required' for broad, risky or plan-first work, 'skip' for explicitly small direct builds, otherwise 'auto'. report:true investigates without a branch; follow-ups require confirmed proposals.",
|
|
21
21
|
"Tools are MCP servers a project's builds get, and builds get nothing else: read get_project_tools. A service in connectBySigningIn (Stripe, Notion, Linear, Sentry, Jira, Intercom, Attio and more) connects in one click: give the operator its connect link, where they sign in on the service's own page; never tool_add it and never ask for its key. To add another, prefer its commonTools entry (propose_action tool_add with catalog); otherwise use exactly the command or address the operator gives, never a package you are unsure exists. A tool reaches work approved after it was added. Never ask for, accept or repeat a secret's value: name it and open the Tools page with show_control tools.",
|
|
22
|
-
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_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, propose_flow 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. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. 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 can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it 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. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). A 'pull-request' step opens a pull request for a Build's result and follows its CI (green moves on, red goes back to the build naming the failing check); it merges only when set to, and only after a person approved the card. 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), offer the matching starter flow with propose_flow starter; each is one yes, and none merges without a person. For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
22
|
+
"Flows are a project's process drawn as zones that cards move through: Holding, Build and Research (each files an ordinary task), Person decides, Message, Done. Read get_flows. To make one, propose_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, propose_flow 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. Triggers start cards on their own: a button with questions, a schedule, GitHub (new issues, a label being added, new pull requests, failed checks), Linear, another flow's cards reaching a zone, or email arriving in the operator's mailbox (optionally only from some senders or with words in the subject; reading mail is set up in Settings → Email), or a plane review (every morning, one card per problem from Toolroll's last 24 hours; a problem that comes back joins its card); propose_flow add_trigger. A Slack, Discord or Teams channel feeds a flow when a paired approver sends 'flow <the flow's number>' in that channel (never from here); each message there becomes a card, and an 'update' step answers in its thread. Webhook addresses and the Linear key are set on the flow's Triggers panel, never in chat. Cards have an owner, followers and a discussion: read a card's discussion with get_flows flow and card before acting on what teammates said; comment (with @name to ping someone), assign and follow through propose_flow. Two steps run without a model: 'check' runs one of the project's scripts (named scripts in Python, Node or shell that belong to the project, not a flow — save one with propose_flow save_script and the repo even before any flow exists; keep them short, or point them at a file already in the repository) with the card as its input; what it prints is passed to later steps, a last line 'goto: <answer>' picks where the card goes, it can run in an empty folder or a copy of the card's work, and it takes the failure path if it fails; a schedule trigger can run a script and make a card of each item it prints; 'update' comments on the GitHub or Linear issue the card came from and can close it, or answers in the chat thread it came from. A Build whose project checks fail takes its failure path too. A 'sort' step has Jev (a fast decision model on the operator's OpenRouter account) read the card and send it where the answer it picks leads, or to its not-sure step, noting scores or yes/no answers too; make the sort the first step, because cards wait in a holding step until a person moves them; for triage, spam, lead, effort or exception routing, start from the matching template. A 'draft' step has Claude write a reply, summary or note from the card in seconds and sends nothing itself: put a decision after it (decider 'owner' asks the flow's owner in their chat app to approve, edit or send it back), then the step that posts it. Steps can reach outside too: 'request' calls a web address, 'email' sends mail from the operator's email account (answering the sender of a card that came from email keeps the reply in that thread), 'tool' uses one of the project's tools (get_project_tools); secrets for them are saved on the canvas or Tools page, never in chat, and a decision should come before any that sends what a model wrote or an outsider sent. A 'wait' step after an 'email' step waits for the person to reply (the reply moves the card on and is kept for later steps; with none by the time it waits, the card takes its no-reply step, like a follow-up email), or just waits a set time. Any step can remind whoever it waits on after a while (remindAfter), and holding and decision steps can then move the card on (thenMoveTo): use these for follow-ups and decisions that stall. AI teammates are agents on the operator's team with a soul file (who they are, how they write, what they know, what they decide on their own, what they ask first, what they never do); read get_teammates. They work flow cards within those rules: a teammate named on an approval step decides it (and hands hard ones to that step's person), and a 'teammate' step has one pick where the card goes and write what the next steps send, asking the flow's owner when its rules say to. They never approve code tasks or merges. With propose_teammate you add one (from a template: support, sales, ops, triage), change one rule section (edit_section), pause, resume or remove it, pass it a note, or answer its question for the operator; put it to work with propose_flow. 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 can use the project's tools under a rule per action: do it, ask first (the operator approves each exact call), never, or do it 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. Each teammate has a memory (get_teammates): what people told it (a note adds one) and facts it kept from cards; forget or edit_memory when the operator says it has something wrong. When it suggests a rule change, that is a question for the operator; answer it only as they say. Each teammate has a desk (a flow): the operator can message it by name in their chat app ('@maya, …'), and routines (propose_teammate add_routine) put a card there on a schedule; its answer goes back to whoever asked. Its desk can send a card to a Build zone that files an ordinary task under the usual approvals. When someone new wants a support desk, bug triage, sales follow-up or requests handled, a starter kit (propose_flow kit) sets up the teammate and its flow in one card. get_teammates with a teammate gives its week (what it did, cost, what the operator overrode); a tool call can be undone (propose_teammate undo) only when its action has an undo set (tool_rule undoWith). A 'pull-request' step opens a pull request for a Build's result and follows its CI (green moves on, red goes back to the build naming the failing check); it merges only when set to, and only after a person approved the card. 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 flow with propose_flow starter; each is one yes, and none merges without a person. For where flows break, read get_flow_insights, and read a failed run's log before explaining it.",
|
|
23
23
|
"For skills read get_skills and exact instructions. Enabled means supplied, not proven used or connected. Manage/test through get_actions/propose_action with project/version; require a receipt before claiming deployment/test start.",
|
|
24
24
|
"To see what a result changed, read get_diff: the file list, then one file's changes. To see why checks failed, read get_check_log: its end, or search for the error. When the operator asks for a change, read the relevant file's diff first, then propose_review revise with the exact path and line and one precise instruction in their words.",
|
|
25
25
|
"Each task also has its own chat with the operator (its Ask panel and phone replies). When they refer to what was said or asked about a task, read get_task_conversation. Cards you draft about a task are recorded in that task's chat once confirmed.",
|
package/dist/mate-tools.js
CHANGED
|
@@ -39,7 +39,7 @@ import { agentChoicesFor, routeOfTask, INSTALLATION_SCOPE } from "./agentconfig.
|
|
|
39
39
|
import { isNewModel, modelWords, priceWords, runtimeStates, seenModels } from "./model-catalog.js";
|
|
40
40
|
import { agentsSummary, chosenWords, isRiskLevel, PHASES, postureWords, RISK_CHOICES, riskConsequence, riskTitle, routeProblems, sameSpec, specWords } from "./phase-routing.js";
|
|
41
41
|
import { TOOL_CATALOG, discoverTools, projectToolsOf, secretsSetFor, toolCommandLine, toolStanding } from "./project-tools.js";
|
|
42
|
-
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps } from "./flows.js";
|
|
42
|
+
import { deciderOf, durationWords, FLOW_KIND_WORDS, FLOW_STAGE_KINDS, FLOW_TEMPLATES, flowFromSteps, stepsFor } from "./flows.js";
|
|
43
43
|
import { flowDefinitionOf } from "./flow-engine.js";
|
|
44
44
|
import { flowInsights } from "./flow-insights.js";
|
|
45
45
|
import { describeTrigger, FLOW_TRIGGER_KINDS, triggerConfigOf } from "./flow-triggers.js";
|
|
@@ -871,7 +871,7 @@ export const MATE_TOOLS = [
|
|
|
871
871
|
},
|
|
872
872
|
{
|
|
873
873
|
name: "propose_flow",
|
|
874
|
-
description: "Draft a flow change as a card the operator confirms. 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) — 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)); pause_trigger, resume_trigger, remove_trigger with trigger. Read get_flows first except to create.",
|
|
874
|
+
description: "Draft a flow change as a card the operator confirms. 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. Read get_flows first except to create.",
|
|
875
875
|
inputSchema: schema({
|
|
876
876
|
operation: { type: "string", enum: ["create", "edit", "add_card", "move_card", "approve", "send_back", "cancel_card", "comment", "assign", "follow", "unfollow", "save_script", "add_trigger", "pause_trigger", "resume_trigger", "remove_trigger", "kit", "starter"] },
|
|
877
877
|
repo: REPO_ARG, flow: { type: "integer", minimum: 1 }, card: { type: "integer", minimum: 1 }, kit: { type: "string", enum: KITS.map(one => one.id) },
|
|
@@ -908,12 +908,13 @@ export const MATE_TOOLS = [
|
|
|
908
908
|
team: { type: "string", maxLength: 12 }, state: { type: "string", maxLength: 40 }, follow: { type: "integer", minimum: 1 }, when: { type: "string", maxLength: 60 },
|
|
909
909
|
folder: { type: "string", maxLength: 100 }, sender: { type: "string", maxLength: 300 }, subject: { type: "string", maxLength: 100 },
|
|
910
910
|
script: { type: "string", maxLength: 40 }, secrets: { type: "array", maxItems: 10, items: { type: "string", maxLength: 40 } },
|
|
911
|
+
at: { type: "string", maxLength: 5 }, timeZone: { type: "string", maxLength: 60 },
|
|
911
912
|
} },
|
|
912
913
|
}, ["operation"]),
|
|
913
914
|
handle: (ctx, args) => {
|
|
914
915
|
const pick = (keys) => Object.fromEntries(keys.filter(key => args[key] !== undefined).map(key => [key, args[key]]));
|
|
915
916
|
// "me" in a step means the operator; the drawing stores their name.
|
|
916
|
-
const steps = () => (Array.isArray(args["steps"]) ? args["steps"] : []
|
|
917
|
+
const steps = () => stepsFor(Array.isArray(args["steps"]) ? args["steps"] : [], ctx.who.name);
|
|
917
918
|
const flowOf = () => {
|
|
918
919
|
const flow = Number.isSafeInteger(args["flow"]) ? ctx.store.getFlow(Number(args["flow"])) : null;
|
|
919
920
|
return flow !== null && ctx.who.repos.includes(flow.repo) ? flow : null;
|
package/dist/operate.d.ts
CHANGED
|
@@ -106,7 +106,7 @@ export declare const KEYS_ACTIONS: readonly ["status", "set", "clear", "verify",
|
|
|
106
106
|
*/
|
|
107
107
|
export declare const OPERATE_VALUE_FLAGS: ReadonlySet<string>;
|
|
108
108
|
export declare const OPERATE_BOOLEAN_FLAGS: ReadonlySet<string>;
|
|
109
|
-
export declare function parseOperateArgs(argv: readonly string[]): Args | {
|
|
109
|
+
export declare function parseOperateArgs(argv: readonly string[], ownValues?: ReadonlySet<string>): Args | {
|
|
110
110
|
error: string;
|
|
111
111
|
};
|
|
112
112
|
/** Route an `operate` command. Returns the process exit code. */
|
package/dist/operate.js
CHANGED
|
@@ -37,6 +37,7 @@ import { followSlack } from "./slack.js";
|
|
|
37
37
|
import { validateScopeText } from "./task-text.js";
|
|
38
38
|
import { runMemoryCommand } from "./memory-cli.js";
|
|
39
39
|
import { runKnowledgeCommand } from "./knowledge-cli.js";
|
|
40
|
+
import { FLOWS_VALUE_FLAGS, runFlowsCommand } from "./flows-cli.js";
|
|
40
41
|
import { runAssignmentCommand } from "./assignment-adapters.js";
|
|
41
42
|
import { applyProjectProfile, runProjectCommand } from "./project-cli.js";
|
|
42
43
|
import { runTaskOutcomeCommand } from "./task-outcome-cli.js";
|
|
@@ -414,6 +415,19 @@ Routines — tasks that fire on a schedule, each instance isolated
|
|
|
414
415
|
toolroll routine pause|resume <name>
|
|
415
416
|
toolroll routine run-now <name> --as <you> --token <t>
|
|
416
417
|
|
|
418
|
+
Flows — processes cards move through; the console's rules
|
|
419
|
+
toolroll flows list [--repo <path>] | show <id>
|
|
420
|
+
toolroll flows create --repo <path> --name <name> (--template <id> | --steps <file|->)
|
|
421
|
+
toolroll flows edit <id> --steps <file|-> [--name <name>]
|
|
422
|
+
toolroll flows trigger add <id> <json|file|->
|
|
423
|
+
toolroll flows trigger pause|resume|remove|check <id> <trigger>
|
|
424
|
+
toolroll flows script save --repo <path> --name <name> (--file <path in project> | --body <file>)
|
|
425
|
+
--about "<one line>" [--language shell|python|node] [--timeout-minutes <n>]
|
|
426
|
+
toolroll flows card add <id> --title <t> [--description <d>] [--zone <zone>]
|
|
427
|
+
toolroll flows archive <id>
|
|
428
|
+
writes take --as <you> --token <t> (or the remembered login); create,
|
|
429
|
+
edit, archive and trigger add preview until --yes
|
|
430
|
+
|
|
417
431
|
Agents — which provider and model each phase runs on
|
|
418
432
|
toolroll providers what is installed, logged in, and
|
|
419
433
|
configured on this machine — without
|
|
@@ -579,6 +593,8 @@ export const OPERATE_VALUE_FLAGS = new Set([
|
|
|
579
593
|
"run", "containment", "agent",
|
|
580
594
|
// onboard: the starter flows to switch on.
|
|
581
595
|
"starter",
|
|
596
|
+
// flows: templates, steps, scripts and cards.
|
|
597
|
+
"template", "steps", "about", "language", "timeout-minutes", "body", "description", "zone",
|
|
582
598
|
"token-env", "after", "repair-max-attempts", "consumer", "batch", "feedback", "source", "view", "cursor", "why", "supersedes", "decision", "sessions", "timeout",
|
|
583
599
|
]);
|
|
584
600
|
export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
@@ -592,16 +608,17 @@ export const OPERATE_BOOLEAN_FLAGS = new Set([
|
|
|
592
608
|
// Settings → Integrations: the last checks, without checking again.
|
|
593
609
|
"saved",
|
|
594
610
|
]);
|
|
595
|
-
export function parseOperateArgs(argv) {
|
|
611
|
+
export function parseOperateArgs(argv, ownValues = new Set()) {
|
|
596
612
|
const positional = [];
|
|
597
613
|
const flags = new Map();
|
|
598
614
|
const repoList = [];
|
|
599
|
-
const wantsValue = OPERATE_VALUE_FLAGS;
|
|
615
|
+
const wantsValue = new Set([...OPERATE_VALUE_FLAGS, ...ownValues]);
|
|
600
616
|
// Every boolean flag any verb reads. A --flag in neither set is a typo,
|
|
601
617
|
// and a typo silently becoming `true` (with its intended value demoted to
|
|
602
618
|
// a positional) surfaces later as a different, wronger error — refuse it
|
|
603
619
|
// here by name instead (Codex round-4 findings 3/8).
|
|
604
|
-
|
|
620
|
+
// A verb may read a global switch's name as a value of its own (`flows script save --file <path>`).
|
|
621
|
+
const booleans = new Set([...OPERATE_BOOLEAN_FLAGS].filter(name => !ownValues.has(name)));
|
|
605
622
|
for (let index = 0; index < argv.length; index++) {
|
|
606
623
|
const argument = argv[index];
|
|
607
624
|
if (!argument.startsWith("--")) {
|
|
@@ -644,7 +661,7 @@ export function parseOperateArgs(argv) {
|
|
|
644
661
|
}
|
|
645
662
|
/** Route an `operate` command. Returns the process exit code. */
|
|
646
663
|
export async function runOperate(command, argv, write, options = {}) {
|
|
647
|
-
const parsed = parseOperateArgs(argv);
|
|
664
|
+
const parsed = parseOperateArgs(argv, command === "flows" ? FLOWS_VALUE_FLAGS : undefined);
|
|
648
665
|
// A parse error precedes the flags map, so JSON mode is read from the raw
|
|
649
666
|
// argv — the envelope contract holds even for the earliest refusal.
|
|
650
667
|
if ("error" in parsed)
|
|
@@ -920,6 +937,8 @@ async function dispatch(command, positional, flags, context) {
|
|
|
920
937
|
return incidentCommand(positional, flags, context);
|
|
921
938
|
case "routine":
|
|
922
939
|
return routineCommand(positional, flags, context);
|
|
940
|
+
case "flows":
|
|
941
|
+
return flowsCommand(positional, flags, context);
|
|
923
942
|
case "config":
|
|
924
943
|
return configCommand(positional, flags, context);
|
|
925
944
|
case "chat":
|
|
@@ -10946,6 +10965,22 @@ async function askCredentials(flags, context) {
|
|
|
10946
10965
|
return null;
|
|
10947
10966
|
return { name, token };
|
|
10948
10967
|
}
|
|
10968
|
+
/** `flows …`: the console's flow rules from a terminal. Writes are an approver's (--as/--token or the remembered login). */
|
|
10969
|
+
async function flowsCommand(positional, flags, context) {
|
|
10970
|
+
const { store } = context;
|
|
10971
|
+
const registered = await loadRepos(registryPathOf(context)).catch(() => ({ error: "unreadable" }));
|
|
10972
|
+
const projects = [...new Set([...store.knownRepos(), ...store.listProjects().map(one => one.path), ...("error" in registered ? [] : registered.repos)])];
|
|
10973
|
+
const writes = positional[0] !== undefined && positional[0] !== "list" && positional[0] !== "show";
|
|
10974
|
+
const acting = writes && !flags.has("help") ? await askCredentials(flags, context) : null;
|
|
10975
|
+
const dir = dirname(context.databaseFile);
|
|
10976
|
+
return runFlowsCommand(positional, flags, {
|
|
10977
|
+
store, write: context.write, json: context.json, clock: context.clock, credentials: acting, projects, configDir: dir, evidenceRoot: context.evidenceRoot,
|
|
10978
|
+
// "Check now": the same io the worker's pass checks triggers with.
|
|
10979
|
+
triggerIo: { gh: context.flowTriggerIo?.gh ?? run, fetch: context.flowTriggerIo?.fetch ?? fetch, dir: context.flowTriggerIo?.dir ?? dir,
|
|
10980
|
+
shell: context.flowTriggerIo?.shell ?? context.flowStepIo?.shell ?? run, scratch: context.flowTriggerIo?.scratch ?? join(dir, "flow-scratch"),
|
|
10981
|
+
...(context.flowTriggerIo?.mail === undefined ? {} : { mail: context.flowTriggerIo.mail }) },
|
|
10982
|
+
});
|
|
10983
|
+
}
|
|
10949
10984
|
async function approverCommand(positional, flags, context) {
|
|
10950
10985
|
const { store, write, json, now } = context;
|
|
10951
10986
|
const [action, name] = positional;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Store } from "./store.js";
|
|
2
|
+
export type PlaneProblem = {
|
|
3
|
+
/** Stable across days: the same problem tomorrow has the same key. Only [a-z0-9/._-]. */
|
|
4
|
+
key: string;
|
|
5
|
+
title: string;
|
|
6
|
+
/** One line: how many, in the last 24 hours. */
|
|
7
|
+
summary: string;
|
|
8
|
+
count: number;
|
|
9
|
+
runs: number[];
|
|
10
|
+
/** Short excerpts, secrets hidden, at most EVIDENCE_LINES. */
|
|
11
|
+
evidence: string[];
|
|
12
|
+
};
|
|
13
|
+
export declare const REVIEW_HOURS = 24;
|
|
14
|
+
export declare const RUN_CAUSES: readonly ["provider", "sign-in", "check", "timeout", "no-handoff", "stuck-lease", "orphaned", "no-change", "other"];
|
|
15
|
+
export type RunCause = (typeof RUN_CAUSES)[number];
|
|
16
|
+
/** Anything shaped like a key is hidden; one short line. */
|
|
17
|
+
export declare function excerpt(text: string | null | undefined, cap?: number): string;
|
|
18
|
+
/** Why one finished run is a problem, or null when it isn't. */
|
|
19
|
+
export declare function runCause(run: {
|
|
20
|
+
outcome: string | null;
|
|
21
|
+
reason: string | null;
|
|
22
|
+
terminalClass: string | null;
|
|
23
|
+
handoff: string | null;
|
|
24
|
+
check: string | null;
|
|
25
|
+
stopped: boolean;
|
|
26
|
+
}): RunCause | "plan-limit" | null;
|
|
27
|
+
/**
|
|
28
|
+
* Every problem worth fixing in the `REVIEW_HOURS` before `now`, worst first.
|
|
29
|
+
* `canSee(repo)` says which projects' work counts (a run or task with no
|
|
30
|
+
* project counts only when every project may be seen).
|
|
31
|
+
*/
|
|
32
|
+
export declare function reviewPlane(store: Store, now: Date, canSee?: (repo: string | null) => boolean): PlaneProblem[];
|
|
33
|
+
/** A problem as a card's details (or a day's note on a card it joins). */
|
|
34
|
+
export declare function problemText(problem: PlaneProblem, day: string): string;
|
|
35
|
+
/** The card's opening details: the day's evidence, and what it is. */
|
|
36
|
+
export declare function problemCard(problem: PlaneProblem, day: string): {
|
|
37
|
+
title: string;
|
|
38
|
+
description: string;
|
|
39
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The morning plane review: what went wrong on this plane in the last 24
|
|
3
|
+
* hours, read straight from the store — never a model, never a fenced
|
|
4
|
+
* script — as one problem per distinct thing worth fixing:
|
|
5
|
+
*
|
|
6
|
+
* - failed or no-change runs, grouped by cause (provider error, sign-in
|
|
7
|
+
* expiry, check failure, timeout, quit without handoff, stuck lease,
|
|
8
|
+
* orphaned process, no change, other);
|
|
9
|
+
* - tasks waiting on a person for over a day, grouped by what they wait for;
|
|
10
|
+
* - sign-in and plan-limit pauses, per provider;
|
|
11
|
+
* - chat replies that couldn't be delivered, per app;
|
|
12
|
+
* - integrations that are Broken (integration_check);
|
|
13
|
+
* - release checks that failed;
|
|
14
|
+
* - worker passes that logged work as broke.
|
|
15
|
+
*
|
|
16
|
+
* Each problem has a stable key, so the same problem tomorrow is the same
|
|
17
|
+
* problem (flow-triggers.ts joins it to its card), and carries counts, run
|
|
18
|
+
* ids and short evidence excerpts with anything shaped like a secret hidden.
|
|
19
|
+
* A clean day is no problems at all. Only projects the reader may see count.
|
|
20
|
+
*/
|
|
21
|
+
import { scanForSecrets } from "./evidence.js";
|
|
22
|
+
import { scrubIntegrationText } from "./integrations.js";
|
|
23
|
+
import { taskWaitSnapshot } from "./lead-status.js";
|
|
24
|
+
export const REVIEW_HOURS = 24;
|
|
25
|
+
const EVIDENCE_LINES = 5;
|
|
26
|
+
const WAITING_DAYS = 1;
|
|
27
|
+
/** A Ready result older than this is history, not today's problem. */
|
|
28
|
+
const READY_WINDOW_DAYS = 7;
|
|
29
|
+
export const RUN_CAUSES = ["provider", "sign-in", "check", "timeout", "no-handoff", "stuck-lease", "orphaned", "no-change", "other"];
|
|
30
|
+
const CAUSE_TITLES = {
|
|
31
|
+
provider: "Runs failed on a provider error",
|
|
32
|
+
"sign-in": "Runs stopped: sign-in expired",
|
|
33
|
+
check: "Runs failed their checks",
|
|
34
|
+
timeout: "Runs timed out",
|
|
35
|
+
"no-handoff": "Agents quit without a handoff",
|
|
36
|
+
"stuck-lease": "Leases got stuck",
|
|
37
|
+
orphaned: "Processes were left behind",
|
|
38
|
+
"no-change": "Runs ended with no change",
|
|
39
|
+
other: "Runs failed for another reason",
|
|
40
|
+
};
|
|
41
|
+
/** Anything shaped like a key is hidden; one short line. */
|
|
42
|
+
export function excerpt(text, cap = 160) {
|
|
43
|
+
if (text === null || text === undefined)
|
|
44
|
+
return "";
|
|
45
|
+
const clean = scrubIntegrationText(text);
|
|
46
|
+
if (scanForSecrets(clean).length > 0)
|
|
47
|
+
return "[hidden: it looked like it held a key or password]";
|
|
48
|
+
return clean.length > cap ? `${clean.slice(0, cap - 1).trimEnd()}…` : clean;
|
|
49
|
+
}
|
|
50
|
+
const PROVIDER_REASONS = new Set(["retryable-infra", "provider-init", "provider-protocol", "provider-unattested", "setup"]);
|
|
51
|
+
const TIMEOUT_WORDS = /made no observable progress|ran past \d+ minutes|timed out/i;
|
|
52
|
+
/** Why one finished run is a problem, or null when it isn't. */
|
|
53
|
+
export function runCause(run) {
|
|
54
|
+
const reason = run.reason ?? "";
|
|
55
|
+
if (reason === "auth-expired" || run.terminalClass === "auth-expired")
|
|
56
|
+
return "sign-in";
|
|
57
|
+
if (run.terminalClass === "usage-exhausted" || run.terminalClass === "credits-depleted")
|
|
58
|
+
return "plan-limit";
|
|
59
|
+
if (run.check === "failed")
|
|
60
|
+
return "check";
|
|
61
|
+
if (reason === "timeout" || (run.outcome === "failed" && TIMEOUT_WORDS.test(run.handoff ?? "")))
|
|
62
|
+
return "timeout";
|
|
63
|
+
if (reason === "no-handoff")
|
|
64
|
+
return "no-handoff";
|
|
65
|
+
if (reason === "interrupted" || run.outcome === "interrupted")
|
|
66
|
+
return run.stopped ? null : "orphaned";
|
|
67
|
+
if (PROVIDER_REASONS.has(reason) || run.terminalClass === "transient-throttle")
|
|
68
|
+
return "provider";
|
|
69
|
+
if (run.outcome === "no-change")
|
|
70
|
+
return "no-change";
|
|
71
|
+
if (run.outcome === "failed")
|
|
72
|
+
return "other";
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const plural = (n, word, many = `${word}s`) => `${n} ${n === 1 ? word : many}`;
|
|
76
|
+
const keyPart = (value) => value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "unknown";
|
|
77
|
+
const str = (value) => value === null || value === undefined ? null : String(value);
|
|
78
|
+
const tableExists = (store, table) => store.handle.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?").get(table) !== undefined;
|
|
79
|
+
const columnExists = (store, table, column) => store.handle.prepare(`SELECT 1 FROM pragma_table_info('${table}') WHERE name = ?`).get(column) !== undefined;
|
|
80
|
+
/**
|
|
81
|
+
* Every problem worth fixing in the `REVIEW_HOURS` before `now`, worst first.
|
|
82
|
+
* `canSee(repo)` says which projects' work counts (a run or task with no
|
|
83
|
+
* project counts only when every project may be seen).
|
|
84
|
+
*/
|
|
85
|
+
export function reviewPlane(store, now, canSee = () => true) {
|
|
86
|
+
const since = new Date(now.getTime() - REVIEW_HOURS * 3_600_000).toISOString();
|
|
87
|
+
const buckets = new Map();
|
|
88
|
+
const add = (key, title, noun, item) => {
|
|
89
|
+
const bucket = buckets.get(key) ?? { title, count: 0, runs: new Set(), evidence: [], noun };
|
|
90
|
+
bucket.count += item.count ?? 1;
|
|
91
|
+
if (item.run !== undefined && item.run !== null)
|
|
92
|
+
bucket.runs.add(item.run);
|
|
93
|
+
if (item.evidence && bucket.evidence.length < EVIDENCE_LINES && !bucket.evidence.includes(item.evidence))
|
|
94
|
+
bucket.evidence.push(item.evidence);
|
|
95
|
+
buckets.set(key, bucket);
|
|
96
|
+
};
|
|
97
|
+
// Runs that finished badly, by cause.
|
|
98
|
+
const runs = store.handle.prepare(`SELECT run.id, run.outcome, run.reason, run.terminal_class, run.handoff, run.provider, ref.repo, ref.external_id AS task,
|
|
99
|
+
(SELECT status FROM run_check WHERE run_check.run = run.id AND run_check.release = 0) AS check_status,
|
|
100
|
+
(SELECT line FROM run_check WHERE run_check.run = run.id AND run_check.release = 0) AS check_line,
|
|
101
|
+
EXISTS (SELECT 1 FROM run_stop WHERE run_stop.run = run.id) AS stopped
|
|
102
|
+
FROM run JOIN task_ref ref ON ref.id = run.task_ref
|
|
103
|
+
WHERE run.finished_at >= ? AND run.finished_at <= ? ORDER BY run.id`).all(since, now.toISOString());
|
|
104
|
+
for (const row of runs) {
|
|
105
|
+
if (!canSee(str(row["repo"])))
|
|
106
|
+
continue;
|
|
107
|
+
const cause = runCause({ outcome: str(row["outcome"]), reason: str(row["reason"]), terminalClass: str(row["terminal_class"]), handoff: str(row["handoff"]), check: str(row["check_status"]), stopped: Number(row["stopped"]) === 1 });
|
|
108
|
+
if (cause === null)
|
|
109
|
+
continue;
|
|
110
|
+
const id = Number(row["id"]);
|
|
111
|
+
const said = excerpt(cause === "check" ? str(row["check_line"]) ?? str(row["handoff"]) : str(row["handoff"]) ?? str(row["reason"]));
|
|
112
|
+
const evidence = `Run ${id} (task ${str(row["task"]) ?? "?"})${said === "" ? "" : `: ${said}`}`;
|
|
113
|
+
if (cause === "plan-limit") {
|
|
114
|
+
const provider = str(row["provider"]) ?? "provider";
|
|
115
|
+
add(`pause/limit/${keyPart(provider)}`, `${provider} hit its plan limit`, ["time", "times"], { run: id, evidence });
|
|
116
|
+
}
|
|
117
|
+
else
|
|
118
|
+
add(`run/${cause}`, CAUSE_TITLES[cause], ["run", "runs"], { run: id, evidence });
|
|
119
|
+
}
|
|
120
|
+
// Leases that expired without being given back: the worker holding them went quiet.
|
|
121
|
+
const leases = store.handle.prepare(`SELECT claim.lease_id, claim.runner, claim.expires_at, claim.released_by, ref.repo, ref.external_id AS task,
|
|
122
|
+
(SELECT MAX(run.id) FROM run WHERE run.lease_id = claim.lease_id) AS run
|
|
123
|
+
FROM claim JOIN task_ref ref ON ref.id = claim.task_ref
|
|
124
|
+
WHERE (claim.released_by = 'reaped' AND claim.released_at >= ?) OR (claim.released_at IS NULL AND claim.expires_at <= ? AND claim.expires_at >= ?)
|
|
125
|
+
ORDER BY claim.expires_at`).all(since, now.toISOString(), since);
|
|
126
|
+
for (const row of leases) {
|
|
127
|
+
if (!canSee(str(row["repo"])))
|
|
128
|
+
continue;
|
|
129
|
+
const run = row["run"] === null ? null : Number(row["run"]);
|
|
130
|
+
add("run/stuck-lease", CAUSE_TITLES["stuck-lease"], ["lease", "leases"], { run,
|
|
131
|
+
evidence: `Task ${str(row["task"]) ?? "?"}${run === null ? "" : `, run ${run}`}: the lease held by ${excerpt(str(row["runner"]), 40)} ${row["released_by"] === "reaped" ? "expired and was taken back" : "expired and is still held"}` });
|
|
132
|
+
}
|
|
133
|
+
// Processes still alive for a run that has finished.
|
|
134
|
+
const orphans = store.handle.prepare(`SELECT rp.run, rp.pid, rp.host, ref.repo, ref.external_id AS task FROM run_process rp
|
|
135
|
+
JOIN run ON run.id = rp.run JOIN task_ref ref ON ref.id = run.task_ref
|
|
136
|
+
WHERE rp.exited_at IS NULL AND run.outcome IS NOT NULL AND (run.finished_at >= ? OR rp.observed_at >= ?) ORDER BY rp.run`).all(since, since);
|
|
137
|
+
for (const row of orphans) {
|
|
138
|
+
if (!canSee(str(row["repo"])))
|
|
139
|
+
continue;
|
|
140
|
+
const run = Number(row["run"]);
|
|
141
|
+
add("run/orphaned", CAUSE_TITLES.orphaned, ["run", "runs"], { run, evidence: `Run ${run} (task ${str(row["task"]) ?? "?"}): process ${str(row["pid"]) ?? "?"} on ${excerpt(str(row["host"]), 40)} was still running after it finished` });
|
|
142
|
+
}
|
|
143
|
+
// Tasks waiting on a person for over a day, by what they wait for.
|
|
144
|
+
const waited = new Date(now.getTime() - WAITING_DAYS * 86_400_000).toISOString();
|
|
145
|
+
const readyFrom = new Date(now.getTime() - READY_WINDOW_DAYS * 86_400_000).toISOString();
|
|
146
|
+
const tasks = store.handle.prepare(`SELECT task.id, task.title, task.updated_at, ref.repo FROM task JOIN task_ref ref ON ref.backend = 'built-in' AND ref.external_id = task.id
|
|
147
|
+
WHERE task.updated_at <= ? AND (task.state IN ('queued', 'running') OR (task.state = 'done' AND task.updated_at >= ?)) ORDER BY task.updated_at LIMIT 500`).all(waited, readyFrom);
|
|
148
|
+
for (const row of tasks) {
|
|
149
|
+
if (!canSee(str(row["repo"])))
|
|
150
|
+
continue;
|
|
151
|
+
const snapshot = taskWaitSnapshot(store, String(row["id"]), now);
|
|
152
|
+
if (snapshot === null || (snapshot.reason !== "needs-person" && snapshot.reason !== "ready"))
|
|
153
|
+
continue;
|
|
154
|
+
const days = Math.floor((now.getTime() - Date.parse(String(row["updated_at"]))) / 86_400_000);
|
|
155
|
+
add(`waiting/${keyPart(snapshot.next)}`, `Tasks waited over a day: ${snapshot.next}`, ["task", "tasks"], { run: snapshot.run,
|
|
156
|
+
evidence: `Task ${String(row["id"])} “${excerpt(str(row["title"]), 80)}”: ${plural(days, "day")} waiting` });
|
|
157
|
+
}
|
|
158
|
+
// Sign-in and plan-limit pauses.
|
|
159
|
+
if (tableExists(store, "provider_auth_pause")) {
|
|
160
|
+
for (const row of store.handle.prepare("SELECT provider, opened_at, lifted_at, runs, first_run FROM provider_auth_pause WHERE opened_at >= ? OR lifted_at IS NULL OR lifted_at >= ? ORDER BY id").all(since, since)) {
|
|
161
|
+
const provider = String(row["provider"]);
|
|
162
|
+
add(`pause/sign-in/${keyPart(provider)}`, `${provider} needed signing in again`, ["pause", "pauses"], { run: row["first_run"] === null ? null : Number(row["first_run"]),
|
|
163
|
+
evidence: `Paused ${String(row["opened_at"]).slice(0, 16).replace("T", " ")} UTC, ${plural(Number(row["runs"] ?? 1), "run")} held; ${row["lifted_at"] === null ? "still paused" : "lifted"}` });
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (tableExists(store, "provider_limit")) {
|
|
167
|
+
for (const row of store.handle.prepare("SELECT provider, window, used_percent, resets_at FROM provider_limit WHERE reached = 1 AND observed_at >= ? ORDER BY provider, window").all(since)) {
|
|
168
|
+
const provider = String(row["provider"]);
|
|
169
|
+
add(`pause/limit/${keyPart(provider)}`, `${provider} hit its plan limit`, ["time", "times"], {
|
|
170
|
+
evidence: `The ${excerpt(str(row["window"]), 20)} limit was reached (${Math.round(Number(row["used_percent"]))}% used)${row["resets_at"] === null ? "" : `; resets ${String(row["resets_at"]).slice(0, 16).replace("T", " ")} UTC`}`
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// Chat replies that couldn't be delivered.
|
|
175
|
+
for (const app of ["slack", "discord", "teams"]) {
|
|
176
|
+
if (!tableExists(store, `${app}_part`))
|
|
177
|
+
continue;
|
|
178
|
+
const name = { slack: "Slack", discord: "Discord", teams: "Teams" }[app];
|
|
179
|
+
for (const row of store.handle.prepare(`SELECT id, problem, attempts FROM ${app}_part WHERE state = 'dropped' AND created >= ? ORDER BY id`).all(since)) {
|
|
180
|
+
add(`chat/${app}`, `${name} replies weren't delivered`, ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["problem"])) || "dropped"}` });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (tableExists(store, "telegram_conversation_part")) {
|
|
184
|
+
for (const row of store.handle.prepare("SELECT last_error, attempts FROM telegram_conversation_part WHERE state = 'dropped' AND created_at >= ?").all(since)) {
|
|
185
|
+
add("chat/telegram", "Telegram replies weren't delivered", ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["last_error"])) || "dropped"}` });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (tableExists(store, "notification_delivery")) {
|
|
189
|
+
for (const row of store.handle.prepare("SELECT last_error, attempts FROM notification_delivery WHERE destination LIKE 'telegram:%' AND last_error IS NOT NULL AND delivered_at IS NULL AND last_attempt_at >= ?").all(since)) {
|
|
190
|
+
add("chat/telegram", "Telegram replies weren't delivered", ["reply", "replies"], { evidence: `After ${plural(Number(row["attempts"] ?? 0), "try", "tries")}: ${excerpt(str(row["last_error"]))}` });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Integrations that are Broken now and were checked in the window.
|
|
194
|
+
if (tableExists(store, "integration_check")) {
|
|
195
|
+
for (const row of store.handle.prepare("SELECT key, problem, checked_at FROM integration_check WHERE outcome = 'failed' AND checked_at >= ? ORDER BY key").all(since)) {
|
|
196
|
+
const key = String(row["key"]);
|
|
197
|
+
add(`integration/${keyPart(key)}`, `Integration broken: ${excerpt(key, 40)}`, ["check", "checks"], { evidence: excerpt(str(row["problem"])) || "The last check failed." });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
// Release checks that failed.
|
|
201
|
+
const releases = store.handle.prepare(`SELECT rc.run, rc.line, ref.repo, ref.external_id AS task FROM run_check rc JOIN run ON run.id = rc.run JOIN task_ref ref ON ref.id = run.task_ref
|
|
202
|
+
WHERE rc.release = 1 AND rc.status = 'failed' AND rc.recorded_at >= ? ORDER BY rc.run`).all(since);
|
|
203
|
+
for (const row of releases) {
|
|
204
|
+
if (!canSee(str(row["repo"])))
|
|
205
|
+
continue;
|
|
206
|
+
const run = Number(row["run"]);
|
|
207
|
+
const said = excerpt(str(row["line"]));
|
|
208
|
+
add("release-check", "Release checks failed", ["check", "checks"], { run, evidence: `Run ${run} (task ${str(row["task"]) ?? "?"})${said === "" ? "" : `: ${said}`}` });
|
|
209
|
+
}
|
|
210
|
+
// What the worker counted as broke in its passes.
|
|
211
|
+
if (tableExists(store, "watch_episode") && columnExists(store, "watch_episode", "broke")) {
|
|
212
|
+
for (const row of store.handle.prepare("SELECT id, repo, runner, broke, started_at, ended_at FROM watch_episode WHERE broke > 0 AND COALESCE(ended_at, started_at) >= ? ORDER BY id").all(since)) {
|
|
213
|
+
if (!canSee(str(row["repo"])))
|
|
214
|
+
continue;
|
|
215
|
+
const broke = Number(row["broke"]);
|
|
216
|
+
add("worker/broke", "The worker logged work as broke", ["time", "times"], { count: broke,
|
|
217
|
+
evidence: `Worker ${excerpt(str(row["runner"]), 40)} (pass ${Number(row["id"])}): ${plural(broke, "piece")} of work broke${row["ended_at"] === null ? ", still running" : ""}` });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return [...buckets].map(([key, bucket]) => {
|
|
221
|
+
const runsList = [...bucket.runs].sort((a, b) => a - b);
|
|
222
|
+
return { key, title: bucket.title, count: bucket.count, runs: runsList, evidence: bucket.evidence,
|
|
223
|
+
summary: `${plural(bucket.count, bucket.noun[0], bucket.noun[1])} in the last ${REVIEW_HOURS} hours${runsList.length === 0 ? "" : ` (${runsList.length === 1 ? "run" : "runs"} ${runsList.slice(0, 12).join(", ")}${runsList.length > 12 ? ", …" : ""})`}.` };
|
|
224
|
+
}).sort((a, b) => b.count - a.count || a.key.localeCompare(b.key));
|
|
225
|
+
}
|
|
226
|
+
/** A problem as a card's details (or a day's note on a card it joins). */
|
|
227
|
+
export function problemText(problem, day) {
|
|
228
|
+
return [`${day}: ${problem.summary}`, ...problem.evidence.map(line => `- ${line}`)].join("\n");
|
|
229
|
+
}
|
|
230
|
+
/** The card's opening details: the day's evidence, and what it is. */
|
|
231
|
+
export function problemCard(problem, day) {
|
|
232
|
+
return { title: problem.title, description: `${problemText(problem, day)}\n\nFound by the morning plane review (problem ${problem.key}). If it comes back, the new day joins this card.` };
|
|
233
|
+
}
|
package/dist/store.d.ts
CHANGED
|
@@ -6173,6 +6173,8 @@ export declare class Store {
|
|
|
6173
6173
|
getFlowCard(id: number): FlowCardRow | null;
|
|
6174
6174
|
flowCards(flow: number, includeFinished: boolean): FlowCardRow[];
|
|
6175
6175
|
/** Every active card of every active flow in one project: what a worker's pass advances. */
|
|
6176
|
+
/** The card whose zone filed this task (the newest, should one task have been filed for two). */
|
|
6177
|
+
flowCardByTask(task: string): FlowCardRow | null;
|
|
6176
6178
|
activeFlowCards(repo: string): FlowCardRow[];
|
|
6177
6179
|
/** Move a card to a zone (a fresh entry: its step runs again), with the history line. `expectEntry` makes a stale move a no-op. */
|
|
6178
6180
|
moveFlowCard(id: number, move: {
|
package/dist/store.js
CHANGED
|
@@ -18773,6 +18773,11 @@ export class Store {
|
|
|
18773
18773
|
return this.db.prepare(`SELECT * FROM flow_card WHERE flow = ? ${includeFinished ? "" : "AND state = 'active'"} ORDER BY updated_at DESC, id DESC LIMIT 500`).all(flow).map(readFlowCardRow);
|
|
18774
18774
|
}
|
|
18775
18775
|
/** Every active card of every active flow in one project: what a worker's pass advances. */
|
|
18776
|
+
/** The card whose zone filed this task (the newest, should one task have been filed for two). */
|
|
18777
|
+
flowCardByTask(task) {
|
|
18778
|
+
const row = this.db.prepare("SELECT * FROM flow_card WHERE task = ? ORDER BY id DESC LIMIT 1").get(task);
|
|
18779
|
+
return row === undefined ? null : readFlowCardRow(row);
|
|
18780
|
+
}
|
|
18776
18781
|
activeFlowCards(repo) {
|
|
18777
18782
|
return this.db.prepare("SELECT c.* FROM flow_card c JOIN flow f ON f.id = c.flow WHERE f.repo = ? AND f.state = 'active' AND c.state = 'active' ORDER BY c.id").all(repo).map(readFlowCardRow);
|
|
18778
18783
|
}
|
package/dist/surface.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { KNOWLEDGE_DESCRIPTORS } from "./knowledge-cli.js";
|
|
2
2
|
import { MEMORY_DESCRIPTORS } from "./memory-cli.js";
|
|
3
3
|
import { MODELS_DESCRIPTORS } from "./models-cli.js";
|
|
4
|
+
import { FLOWS_DESCRIPTORS } from "./flows-cli.js";
|
|
4
5
|
/**
|
|
5
6
|
* The declared command guide (arc 5): the agent-facing surface as data,
|
|
6
7
|
* dumped by `contract --commands`. This is DOCUMENTATION with a stable
|
|
@@ -115,6 +116,9 @@ export const COMMAND_GUIDE = [
|
|
|
115
116
|
})),
|
|
116
117
|
...KNOWLEDGE_DESCRIPTORS.map(spec => ({ invocation: `knowledge ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text or source file for impact" }] } : {}) })),
|
|
117
118
|
...MEMORY_DESCRIPTORS.map(spec => ({ invocation: `memory ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "query", required: true, meaning: "search text, a decision id, or the decision sentence" }] } : {}) })),
|
|
119
|
+
// Flow writes carry an approver's credential: an agent runs them only as the person asked, after showing the preview.
|
|
120
|
+
...FLOWS_DESCRIPTORS.map(spec => ({ invocation: `flows ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags,
|
|
121
|
+
...("positionals" in spec ? { positionals: spec.positionals.map(name => ({ name, required: true, meaning: name === "flow" ? "a flow id from flows list" : "a trigger id from flows show, or for trigger add its settings as JSON, a JSON file, or -" })) } : {}) })),
|
|
118
122
|
...MODELS_DESCRIPTORS.map(spec => ({ invocation: `models ${spec.action}`, synopsis: spec.synopsis, audience: "agent", agentMayInvoke: true, mutation: spec.mutation, flags: spec.flags, ...(spec.takesQuery ? { positionals: [{ name: "target", required: true, meaning: "the CLI to update, or on/off" }] } : {}) })),
|
|
119
123
|
// ---- the queue (agent surface) ----
|
|
120
124
|
{ invocation: "status", synopsis: "running work, queued reasons, results to review, the latest release check and plan windows", audience: "agent", agentMayInvoke: true, mutation: "none", flags: [jsonFlag, dbFlag] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolroll",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
|
|
5
5
|
"directories": {
|
|
6
6
|
"doc": "docs"
|