toolroll 0.9.30 → 0.9.32
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 +35 -0
- package/dist/assignment.d.ts +3 -0
- package/dist/assignment.js +8 -2
- package/dist/batch-checks.d.ts +15 -0
- package/dist/batch-checks.js +244 -0
- package/dist/batch-policy.d.ts +23 -0
- package/dist/batch-policy.js +25 -0
- package/dist/browser/workspace.js +13 -13
- package/dist/builder.js +14 -3
- package/dist/chat-actions.js +2 -2
- package/dist/check-levels-ui.d.ts +4 -0
- package/dist/check-levels-ui.js +21 -2
- package/dist/checkout-cleanup.d.ts +2 -1
- package/dist/checkout-cleanup.js +24 -8
- package/dist/desktop-update.d.ts +1 -1
- package/dist/exec.d.ts +2 -0
- package/dist/exec.js +11 -0
- package/dist/flow-gallery-ui.d.ts +14 -9
- package/dist/flow-gallery-ui.js +71 -29
- package/dist/flow-gallery.d.ts +26 -2
- package/dist/flow-gallery.js +182 -7
- package/dist/lead-capabilities.d.ts +0 -7
- package/dist/lead-capabilities.js +11 -1
- package/dist/mate-doors.js +2 -2
- package/dist/mate.d.ts +6 -2
- package/dist/mate.js +63 -38
- package/dist/mcp-connect.d.ts +3 -1
- package/dist/mcp-connect.js +11 -6
- package/dist/never-index.d.ts +3 -0
- package/dist/never-index.js +21 -0
- package/dist/operate.js +86 -2
- package/dist/orphan-sweep.d.ts +63 -0
- package/dist/orphan-sweep.js +235 -0
- package/dist/result-follow-ups.d.ts +22 -2
- package/dist/result-follow-ups.js +44 -10
- package/dist/serve.js +52 -19
- package/dist/shared-deps.d.ts +18 -2
- package/dist/shared-deps.js +129 -15
- package/dist/staged-runtimes.d.ts +42 -0
- package/dist/staged-runtimes.js +191 -0
- package/dist/storage-sweep.d.ts +50 -0
- package/dist/storage-sweep.js +203 -0
- package/dist/storage-ui.d.ts +5 -0
- package/dist/storage-ui.js +30 -4
- package/dist/storage.d.ts +2 -0
- package/dist/storage.js +15 -3
- package/dist/store.d.ts +17 -2
- package/dist/store.js +37 -5
- package/dist/subscription-chat.d.ts +3 -1
- package/dist/subscription-chat.js +1 -0
- package/dist/surface.js +1 -0
- package/dist/task-status.d.ts +13 -0
- package/dist/task-status.js +37 -3
- package/dist/team-leads.js +6 -2
- package/dist/team-runtime.js +2 -1
- package/dist/test-temp.d.ts +20 -2
- package/dist/test-temp.js +53 -13
- package/dist/toolroll-update.js +2 -0
- package/dist/workspace-ui.js +3 -1
- package/dist/worktree.js +9 -0
- package/package.json +1 -1
package/dist/flow-gallery.js
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
* shared flow file later. Every one is built only from existing zone and trigger kinds, and none merges:
|
|
10
10
|
* a pull request it opens waits for a person.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
12
|
+
* A template that puts a connected tool to work names it (`tools`): a one-click service (ONE_CLICK) or the Figma
|
|
13
|
+
* desktop app. Its research steps read that tool and only read; a build gets the project's tools as any build does.
|
|
14
|
+
*
|
|
15
|
+
* Every template without its own send zone can end with "Send me the result" (a "Send to me" zone before its end),
|
|
16
|
+
* off unless asked for (`send-result: yes`): what the flow did reaches the person in their chat apps.
|
|
14
17
|
*/
|
|
15
18
|
import { createHash } from "node:crypto";
|
|
16
19
|
import { scanForSecrets } from "./evidence.js";
|
|
@@ -19,6 +22,7 @@ import { saveScript, validateScript } from "./flow-scripts.js";
|
|
|
19
22
|
import { isToolrollRepo, planeReviewSteps } from "./flow-starters.js";
|
|
20
23
|
import { addFlowTriggerTo, describeTrigger, githubRepoOf, scheduleFromWords, validateTriggerConfig } from "./flow-triggers.js";
|
|
21
24
|
import { choiceTargets, clockTime, flowFromSteps, flowTerms, FLOW_TEMPLATES, ISSUE_LABEL } from "./flows.js";
|
|
25
|
+
import { connectionsOf, localConnectOf, oneClickOf } from "./mcp-connect.js";
|
|
22
26
|
import { publishingOf } from "./pull-request-flow.js";
|
|
23
27
|
export const GALLERY_GROUPS = [
|
|
24
28
|
{ id: "ship", label: "Ship code" },
|
|
@@ -32,6 +36,9 @@ export const OUTDATED_COMMANDS = {
|
|
|
32
36
|
pip: { label: "pip (pip list --outdated)", command: "out=\"$(python3 -m pip list --outdated 2>/dev/null | tail -n +3)\"\nif [ -n \"$out\" ]; then echo \"$out\"; exit 1; fi" },
|
|
33
37
|
cargo: { label: "cargo (cargo outdated)", command: "cargo outdated --root-deps-only --exit-code 1" },
|
|
34
38
|
};
|
|
39
|
+
/** The card's Linear issue key, read from its source as the Linear trigger stored it ("Linear ENG-123"); "none" skips the comment. */
|
|
40
|
+
export const LINEAR_KEY = `key="$(sed -n 's/.*"source":{"kind":"linear","label":"Linear \\([A-Z][A-Z0-9]*-[0-9][0-9]*\\)".*/\\1/p' | head -n 1)"
|
|
41
|
+
if [ -n "$key" ]; then echo "$key"; echo "goto: Found"; else echo none; echo "goto: None"; fi`;
|
|
35
42
|
const NEVER_MERGES = "Never merges. Each pull request waits for you.";
|
|
36
43
|
const work = (text) => `${text}\n\nChanges asked for (if any): {{note}}`;
|
|
37
44
|
const research = (text) => `${text}\n\nFeedback to address (if any): {{note}}`;
|
|
@@ -40,6 +47,11 @@ const pr = { id: "pull-request", title: "Pull request", kind: "pull-request" };
|
|
|
40
47
|
const drawn = (id) => structuredClone(FLOW_TEMPLATES.find(one => one.id === id).definition);
|
|
41
48
|
const about = (id) => FLOW_TEMPLATES.find(one => one.id === id).about;
|
|
42
49
|
const SORTS = "OpenRouter (Jev sorts)";
|
|
50
|
+
const WEEKLY = { key: "schedule", default: "monday 09:00", label: "When", hint: "Like “monday 09:00”." };
|
|
51
|
+
const weekly = (title) => ({ kind: "schedule", schedule: "{{ask.schedule}}", title });
|
|
52
|
+
const send = (title) => ({ id: "send", title, kind: "send" });
|
|
53
|
+
/** A person's choice: build it, or end there. A reply goes to the build as its note. */
|
|
54
|
+
const choose = (title, yes, build) => ({ id: "choose", title, kind: "choose", options: [{ label: yes, goesTo: build }, { label: "Ignore", goesTo: "end" }], ifReplied: build });
|
|
43
55
|
export const GALLERY = [
|
|
44
56
|
// ---------------------------------------------------------------- Ship code
|
|
45
57
|
{
|
|
@@ -137,6 +149,50 @@ export const GALLERY = [
|
|
|
137
149
|
id: "effort-routing", name: "Effort routing", group: "ship", promise: "Small changes go straight to a build; big ones get a plan and a person first.", needs: [SORTS], asks: [], definition: drawn("effort-routing"), triggers: [],
|
|
138
150
|
does: [about("effort-routing")], never: "Never builds a big change before you approve its plan.",
|
|
139
151
|
},
|
|
152
|
+
{
|
|
153
|
+
id: "ui-inspiration", name: "UI inspiration", group: "ship", tools: ["mobbin"],
|
|
154
|
+
promise: "Each week, three real app examples for one part of your UI, ready to build.",
|
|
155
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
156
|
+
steps: [
|
|
157
|
+
{ id: "find", title: "Find examples", kind: "report", instructions: research("Pick one surface of this project (a screen or a flow) that no recent commit or earlier inspiration covered.\n\nWith Mobbin, read only: find 3 real app examples for it. For each: the pattern, why it fits here, how it plugs into this code (files and components), and a Mobbin screenshot.") },
|
|
158
|
+
choose("Implement, remix or ignore?", "Implement", "build"),
|
|
159
|
+
{ id: "build", title: "Build it", kind: "task", instructions: work("Build the chosen example from the report into this project with its own components. A note below is a remix: follow it.\n\nThe report:\n{{stage.find}}") },
|
|
160
|
+
pr,
|
|
161
|
+
send("Send the result"),
|
|
162
|
+
],
|
|
163
|
+
triggers: [weekly("UI inspiration")],
|
|
164
|
+
does: ["Picks one part of your UI {{ask.schedule-words}} and finds 3 Mobbin examples for it, each with a screenshot.", "You choose Implement or Ignore, or reply with a remix note.", "What you choose is built as a task, under your usual approvals, and opens a pull request.", "The result is sent to you."],
|
|
165
|
+
never: NEVER_MERGES,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: "figma-to-pr", name: "Figma frame to pull request", group: "ship", tools: ["figma-desktop"],
|
|
169
|
+
promise: "Paste a Figma frame link; get it built in your own components.",
|
|
170
|
+
needs: ["GitHub"], asks: [],
|
|
171
|
+
steps: [
|
|
172
|
+
{ id: "build", title: "Build the frame", kind: "task", instructions: work("Build this Figma frame in the project's own components and styles: {{card.title}}\n\n{{card.description}}\n\nRead the frame with the Figma desktop app's tools (its code, screenshot and variables). Reuse existing components; add one only where none fits. Take screenshots before and after, on desktop and phone.") },
|
|
173
|
+
pr,
|
|
174
|
+
send("Send before and after"),
|
|
175
|
+
],
|
|
176
|
+
triggers: [{ kind: "button", label: "Build a Figma frame", questions: ["Figma frame link", "Notes (optional)"] }],
|
|
177
|
+
does: ["Adds a “Build a Figma frame” button that asks for the frame link.", "Builds the frame in your components as a task, under your usual approvals, with desktop and phone screenshots.", "Opens a pull request and sends you before and after."],
|
|
178
|
+
never: NEVER_MERGES,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "linear-to-prs", name: "Linear issues to PRs", group: "ship", tools: ["linear"],
|
|
182
|
+
promise: "Labelled Linear issues are built, and the issue gets the pull request link.",
|
|
183
|
+
needs: ["GitHub", "A Linear API key"], asks: [{ key: "team", default: "ENG", label: "Linear team", hint: "Its short key, like ENG." }, { key: "label", default: ISSUE_LABEL, label: "Issue label" }],
|
|
184
|
+
// The issue's key comes from the card's source ("Linear ENG-123"), as the trigger stored it, never from a model.
|
|
185
|
+
scripts: [{ name: "linear-issue-key", about: "Prints the Linear issue the card came from, or none.", body: LINEAR_KEY, timeoutMinutes: 1 }],
|
|
186
|
+
steps: [
|
|
187
|
+
{ id: "build", title: "Build it", kind: "task", instructions: work("Build this issue: {{card.title}}\n\n{{card.description}}") },
|
|
188
|
+
pr,
|
|
189
|
+
{ id: "issue-key", title: "Find the issue key", kind: "check", script: "{{script.linear-issue-key}}", runIn: "folder", routes: [{ answer: "Found", goesTo: "comment" }, { answer: "None", goesTo: "Done" }] },
|
|
190
|
+
{ id: "comment", title: "Comment on the issue", kind: "tool", server: "linear", tool: "create_comment", args: { issueId: "{{stage.issue-key}}", body: "A pull request is ready: {{stage.pull-request}}" } },
|
|
191
|
+
],
|
|
192
|
+
triggers: [{ kind: "linear", team: "{{ask.team}}", label: "{{ask.label}}" }],
|
|
193
|
+
does: ["Watches {{ask.team}} issues labelled “{{ask.label}}” in Linear.", "Builds each one as a task, under your usual approvals, and opens a pull request.", "Comments the pull request link on the issue, through Linear."],
|
|
194
|
+
never: NEVER_MERGES,
|
|
195
|
+
},
|
|
140
196
|
// ---------------------------------------------------------- Keep it healthy
|
|
141
197
|
{
|
|
142
198
|
id: "fix-ci", name: "Fix failing CI", group: "health",
|
|
@@ -211,7 +267,7 @@ export const GALLERY = [
|
|
|
211
267
|
never: NEVER_MERGES,
|
|
212
268
|
},
|
|
213
269
|
{
|
|
214
|
-
id: "error-to-fix", name: "Error to fix", group: "health",
|
|
270
|
+
id: "error-to-fix", name: "Error to fix", group: "health", tools: ["sentry"],
|
|
215
271
|
promise: "New errors from Sentry or any error tracker are sorted, researched and fixed.",
|
|
216
272
|
needs: ["A webhook from Sentry or another tracker", SORTS], asks: [],
|
|
217
273
|
steps: [
|
|
@@ -220,7 +276,7 @@ export const GALLERY = [
|
|
|
220
276
|
{ answer: "Fix it", means: "It comes from our code and is new, frequent, or hurts people", goesTo: "find-cause" },
|
|
221
277
|
{ answer: "Ignore", means: "Noise: bots, browser extensions, a third party's outage, or something known and expected", goesTo: "ignored" },
|
|
222
278
|
], ifNotSure: "by-hand" },
|
|
223
|
-
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("An error came in: {{card.title}}\n\n{{card.description}}\n\
|
|
279
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("An error came in: {{card.title}}\n\n{{card.description}}\n\nWith Sentry, read the issue's stack trace and latest events. Read only. Find the cause in this code: cite the files and lines, say why it happens and what change fixes it.") },
|
|
224
280
|
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this error, with a regression check where one fits: {{card.title}}\n\nThe report:\n{{stage.find-cause}}") },
|
|
225
281
|
owner("review", "Review the fix"),
|
|
226
282
|
{ ...pr, next: "Done" },
|
|
@@ -233,6 +289,56 @@ export const GALLERY = [
|
|
|
233
289
|
"Each kept error is researched, then fixed as a task, under your usual approvals.", "What you approve opens a pull request."],
|
|
234
290
|
never: NEVER_MERGES,
|
|
235
291
|
},
|
|
292
|
+
{
|
|
293
|
+
id: "outage-to-cause", name: "Outage to cause", group: "health", tools: ["betterstack"],
|
|
294
|
+
promise: "A Better Stack incident gets its likely cause, and a fix if you want one.",
|
|
295
|
+
needs: ["A webhook from Better Stack", "GitHub"], asks: [],
|
|
296
|
+
steps: [
|
|
297
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("A Better Stack incident: {{card.title}}\n\n{{card.description}}\n\nWith Better Stack, read the incident and its monitor's recent history. Read this project's commits and deploys from before it started. Read only. Say the likely cause, with times, and what change fixes it.") },
|
|
298
|
+
choose("Fix or ignore?", "Fix", "fix"),
|
|
299
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix the cause of this outage, with a regression check where one fits.\n\nThe report:\n{{stage.find-cause}}") },
|
|
300
|
+
pr,
|
|
301
|
+
],
|
|
302
|
+
triggers: [{ kind: "webhook", title: "Incident", titleField: "data.attributes.name" }],
|
|
303
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Better Stack.", "Reads the incident, its monitor's history and your recent commits and deploys, and sends you the likely cause.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
304
|
+
never: NEVER_MERGES,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
id: "worker-errors", name: "Worker errors to fix", group: "health", tools: ["cloudflare"],
|
|
308
|
+
promise: "Each week, Cloudflare Workers and Pages errors worth fixing get a fix.",
|
|
309
|
+
needs: ["GitHub", SORTS], asks: [WEEKLY],
|
|
310
|
+
steps: [
|
|
311
|
+
{ id: "read", title: "Read the errors", kind: "report", instructions: research("With Cloudflare, read this week's Workers and Pages errors and analytics. Read only. List each error with its count, its Worker or Pages project, and when it started. Say “No errors” if there are none.") },
|
|
312
|
+
{ id: "worth-fixing", title: "Worth fixing?", kind: "sort", question: "Do these errors come from this project's code, often enough to fix now?",
|
|
313
|
+
answers: [
|
|
314
|
+
{ answer: "Fix it", means: "Errors from our own code that are new, frequent or hurt people", goesTo: "find-cause" },
|
|
315
|
+
{ answer: "Ignore", means: "No errors, or only noise: bots, a provider's outage, something known", goesTo: "ignored" },
|
|
316
|
+
], ifNotSure: "find-cause" },
|
|
317
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("Errors worth fixing:\n{{stage.read}}\n\nWith Cloudflare, read the worst one's logs. Read only. Find the cause in this code, citing files and lines, and say what change fixes it.") },
|
|
318
|
+
choose("Fix or ignore?", "Fix", "fix"),
|
|
319
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this error, with a regression check where one fits.\n\nThe report:\n{{stage.find-cause}}") },
|
|
320
|
+
{ ...pr, next: "Done" },
|
|
321
|
+
{ id: "done", title: "Done", kind: "done" },
|
|
322
|
+
{ id: "ignored", title: "Ignored", kind: "done" },
|
|
323
|
+
],
|
|
324
|
+
triggers: [weekly("Worker errors")],
|
|
325
|
+
does: ["Reads your Cloudflare Workers and Pages errors and analytics {{ask.schedule-words}}.", "Jev decides whether they're worth fixing; if so, finds the cause and sends you a report.", "You choose Fix or Ignore; a fix is built as a task, under your usual approvals, and opens a pull request."],
|
|
326
|
+
never: NEVER_MERGES,
|
|
327
|
+
},
|
|
328
|
+
{
|
|
329
|
+
id: "failed-deploy", name: "Failed deploy to fix", group: "health", tools: ["vercel"],
|
|
330
|
+
promise: "A failed Vercel deployment gets its cause found and a fix opened.",
|
|
331
|
+
needs: ["A webhook from Vercel", "GitHub"], asks: [],
|
|
332
|
+
steps: [
|
|
333
|
+
{ id: "find-cause", title: "Find the cause", kind: "report", instructions: research("A Vercel deployment failed: {{card.title}}\n\n{{card.description}}\n\nWith Vercel, read the deployment's build log. Read only. Find the cause in this code, citing files and lines, and say what change fixes it.") },
|
|
334
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix this failed deployment without weakening any check.\n\nThe report:\n{{stage.find-cause}}") },
|
|
335
|
+
pr,
|
|
336
|
+
send("Send the fix"),
|
|
337
|
+
],
|
|
338
|
+
triggers: [{ kind: "webhook", title: "Failed deployment", titleField: "payload.deployment.name" }],
|
|
339
|
+
does: ["Adds a webhook. Once the flow is made, open its Triggers and choose New address, then paste that into Vercel for failed deployments.", "Reads the build log and finds the cause.", "Builds a fix as a task, under your usual approvals, opens a pull request and sends it to you."],
|
|
340
|
+
never: NEVER_MERGES,
|
|
341
|
+
},
|
|
236
342
|
// ---------------------------------------------------------- Hear from users
|
|
237
343
|
{
|
|
238
344
|
id: "feedback-to-feature", name: "Feedback to feature", group: "users",
|
|
@@ -265,6 +371,48 @@ export const GALLERY = [
|
|
|
265
371
|
id: "triage", name: "Issue triage", group: "users", promise: "Jev sorts new issues into bugs, ideas and questions, and says how urgent each is.", needs: [SORTS], asks: [], definition: drawn("triage"), triggers: [],
|
|
266
372
|
does: [about("triage")], never: "Never closes an issue or replies without your approval.",
|
|
267
373
|
},
|
|
374
|
+
{
|
|
375
|
+
id: "metrics-digest", name: "Metrics digest", group: "users", tools: ["posthog"],
|
|
376
|
+
promise: "Each week, this week against last: signups, activation, retention and top events.",
|
|
377
|
+
needs: [], asks: [WEEKLY],
|
|
378
|
+
steps: [
|
|
379
|
+
{ id: "read", title: "Read the numbers", kind: "report", instructions: research("With PostHog, compare this week with last week. Read only. Give signups, activation, retention and the top 10 events, each with its change. Name the biggest change and its likely cause (a release, a campaign, a broken step), citing this week's commits where they fit.") },
|
|
380
|
+
send("Send the digest"),
|
|
381
|
+
],
|
|
382
|
+
triggers: [weekly("Metrics digest")],
|
|
383
|
+
does: ["Compares this week's PostHog numbers with last week's, {{ask.schedule-words}}.", "Names the biggest change and its likely cause.", "Sends you the digest."],
|
|
384
|
+
never: "Never changes PostHog or your code.",
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: "fix-drop-off", name: "Fix the biggest drop-off", group: "users", tools: ["posthog"],
|
|
388
|
+
promise: "Each week, the funnel step losing the most people gets a proposed fix.",
|
|
389
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
390
|
+
steps: [
|
|
391
|
+
{ id: "find", title: "Find the drop-off", kind: "report", instructions: research("With PostHog, find the funnel step that lost the most people this week. Read only. Give the funnel, the step, how many reached it and how many left. Propose one fix in this code, citing files.") },
|
|
392
|
+
choose("Implement or ignore?", "Implement", "build"),
|
|
393
|
+
{ id: "build", title: "Build the fix", kind: "task", instructions: work("Build the proposed fix for this drop-off.\n\nThe report:\n{{stage.find}}") },
|
|
394
|
+
pr,
|
|
395
|
+
send("Send the result"),
|
|
396
|
+
],
|
|
397
|
+
triggers: [weekly("Biggest drop-off")],
|
|
398
|
+
does: ["Finds the PostHog funnel step losing the most people {{ask.schedule-words}}, with the numbers and one proposed fix.", "You choose Implement or Ignore.", "A fix is built as a task, under your usual approvals, opens a pull request and is sent to you."],
|
|
399
|
+
never: NEVER_MERGES,
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
id: "support-to-fix", name: "Support to bug fix", group: "users", tools: ["intercom"],
|
|
403
|
+
promise: "Each week, the bugs your customers hit most, ranked, with a fix for the one you pick.",
|
|
404
|
+
needs: ["GitHub"], asks: [WEEKLY],
|
|
405
|
+
steps: [
|
|
406
|
+
{ id: "group", title: "Rank the bugs", kind: "report", instructions: research("With Intercom, read this week's conversations. Read only. Group the bug reports by problem, rank them by how many people hit each, and number them, with a count and one example conversation each.") },
|
|
407
|
+
{ id: "choose", title: "Which to fix?", kind: "choose", options: [{ label: "Fix the top one", goesTo: "fix" }, { label: "Ignore", goesTo: "end" }], ifReplied: "fix" },
|
|
408
|
+
{ id: "fix", title: "Fix it", kind: "task", instructions: work("Fix the bug chosen from this ranking: the top one, unless the note names another by number. Add a regression check where one fits.\n\nThe ranking:\n{{stage.group}}") },
|
|
409
|
+
pr,
|
|
410
|
+
send("Send the result"),
|
|
411
|
+
],
|
|
412
|
+
triggers: [weekly("Support bugs")],
|
|
413
|
+
does: ["Reads your Intercom conversations {{ask.schedule-words}} and ranks the bug reports by how many people hit each.", "You fix the top one, reply with the number of another, or ignore them.", "The fix is built as a task, under your usual approvals, opens a pull request and is sent to you."],
|
|
414
|
+
never: NEVER_MERGES,
|
|
415
|
+
},
|
|
268
416
|
// ------------------------------------------------------------------ Operations
|
|
269
417
|
...[
|
|
270
418
|
["research", "Research a question, have a person check it, then share it.", [], "Never shares an answer you haven't checked."],
|
|
@@ -281,6 +429,20 @@ export const GALLERY = [
|
|
|
281
429
|
/** The blank flow, for starting from nothing. */
|
|
282
430
|
export const BLANK = { id: "blank", name: "Blank flow", group: "ops", promise: about("blank"), needs: [], asks: [], definition: drawn("blank"), triggers: [], does: [about("blank")], never: "Does nothing until you add zones." };
|
|
283
431
|
export const galleryTemplateOf = (id) => id === BLANK.id ? BLANK : GALLERY.find(one => one.id === id) ?? null;
|
|
432
|
+
/** A tool's name ("Mobbin", "Figma (desktop app)"); null for an id that's neither a one-click service nor an app on this computer. */
|
|
433
|
+
export const galleryToolLabel = (id) => oneClickOf(id)?.label ?? localConnectOf(id)?.label ?? null;
|
|
434
|
+
/** The zones that use a tool: a tool step on it, or a research or build step that names it ("With Mobbin, …"). */
|
|
435
|
+
export function zonesUsing(definition, id) {
|
|
436
|
+
const label = galleryToolLabel(id);
|
|
437
|
+
if (label === null)
|
|
438
|
+
return [];
|
|
439
|
+
const word = new RegExp(`\\b${label.replace(/\s*\(.*\)$/, "").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`);
|
|
440
|
+
return definition.stages.filter(one => one.tool?.server === id || ((one.kind === "report" || one.kind === "task") && word.test(one.instructions ?? ""))).map(one => one.title);
|
|
441
|
+
}
|
|
442
|
+
/** A template's tools, each with its state in a project's connections (connectionsOf), or none without a project. */
|
|
443
|
+
export function galleryToolsOf(template, definition, connections) {
|
|
444
|
+
return (template.tools ?? []).map(id => ({ id, label: galleryToolLabel(id) ?? id, state: connections === null ? null : connections.find(one => one.id === id)?.state ?? "open", zones: zonesUsing(definition, id) }));
|
|
445
|
+
}
|
|
284
446
|
/** Each question's default for a project: the branch pull requests go to, when that's set up. */
|
|
285
447
|
export function galleryDefaults(store, template, repo) {
|
|
286
448
|
const publishing = repo === null ? { on: false } : publishingOf(store, repo);
|
|
@@ -296,12 +458,17 @@ const oneLine = (value, cap, what) => {
|
|
|
296
458
|
};
|
|
297
459
|
/** The optional last step every template offers, and the answer that turns it on. */
|
|
298
460
|
export const SEND_RESULT = { key: "send-result", title: "Send me the result", does: "When a card finishes, sends you what was done (its summary, links and screenshots) in your chat apps." };
|
|
299
|
-
/** A
|
|
461
|
+
/** A template that already sends its result to the person: it isn't offered "Send me the result" again. */
|
|
462
|
+
export const sendsAlready = (template) => galleryDiagram(template).stages.some(one => one.kind === "send");
|
|
463
|
+
/** A flow with "Send me the result" before its end: every path into its main Done zone passes through it first.
|
|
464
|
+
* A flow that already has a send zone is left as it is, so nobody gets the result twice. */
|
|
300
465
|
export function withSendResult(definition) {
|
|
466
|
+
if (definition.stages.some(one => one.kind === "send"))
|
|
467
|
+
return definition;
|
|
301
468
|
// Its main end: Done, else an end that isn't a filtered-out one (drawn in rose), else any end.
|
|
302
469
|
const ends = definition.stages.filter(one => one.kind === "done");
|
|
303
470
|
const end = ends.find(one => one.id === "done") ?? ends.find(one => one.zone.color !== "rose") ?? ends[0];
|
|
304
|
-
if (end === undefined
|
|
471
|
+
if (end === undefined)
|
|
305
472
|
return definition;
|
|
306
473
|
let id = "send-result";
|
|
307
474
|
for (let n = 2; definition.stages.some(one => one.id === id); n++)
|
|
@@ -352,6 +519,11 @@ export function checkAnswers(template, given) {
|
|
|
352
519
|
}
|
|
353
520
|
else if (ask.key === "command")
|
|
354
521
|
answers["command"] = oneLine(value, 500, "test command");
|
|
522
|
+
else if (ask.key === "team") {
|
|
523
|
+
if (!/^[A-Za-z0-9]{1,12}$/.test(value))
|
|
524
|
+
throw new Error("Say the Linear team as its short key, like ENG.");
|
|
525
|
+
answers["team"] = value.toUpperCase();
|
|
526
|
+
}
|
|
355
527
|
else if (ask.key === "outdated") {
|
|
356
528
|
const manager = OUTDATED_COMMANDS[value];
|
|
357
529
|
if (manager === undefined)
|
|
@@ -420,6 +592,9 @@ export function buildFromGallery(store, template, repo, given) {
|
|
|
420
592
|
const github = githubRepoOf(repo);
|
|
421
593
|
const drawn = template.definition !== undefined ? structuredClone(template.definition)
|
|
422
594
|
: flowFromSteps(fill(template.ownSteps !== undefined && isToolrollRepo(repo) ? template.ownSteps : template.steps, answers, names, github), null);
|
|
595
|
+
// One that already sends doesn't take the answer, so it reads as it was asked.
|
|
596
|
+
if (drawn.stages.some(one => one.kind === "send"))
|
|
597
|
+
delete answers[SEND_RESULT.key];
|
|
423
598
|
const sends = answers[SEND_RESULT.key] === "yes";
|
|
424
599
|
const definition = sends ? withSendResult(drawn) : drawn;
|
|
425
600
|
return { template, repo, answers, definition, triggers: fill(template.triggers, answers, names, github), scripts,
|
|
@@ -440,7 +615,7 @@ export function previewGallery(store, template, repo, given, actor, now) {
|
|
|
440
615
|
const steps = flowTerms(built.definition, null);
|
|
441
616
|
if (built.scripts.some(one => !one.existing))
|
|
442
617
|
steps.push(...built.scripts.filter(one => !one.existing).map(one => `Adds the ${one.name} script to this project: ${one.body.split("\n")[0]}${one.body.includes("\n") ? " …" : ""}`));
|
|
443
|
-
return { built, startsFrom, steps, digest: galleryDigest(template.id, repo, built.answers) };
|
|
618
|
+
return { built, startsFrom, steps, tools: galleryToolsOf(template, built.definition, connectionsOf(store, repo)), digest: galleryDigest(template.id, repo, built.answers) };
|
|
444
619
|
}
|
|
445
620
|
/** What a preview showed: the template, the project and the answers. Creating checks the person saw these. */
|
|
446
621
|
export function galleryDigest(id, repo, answers) {
|
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What the lead can rely on right now, in one read (get_capabilities): the agents a project's route would run, the
|
|
3
|
-
* workers that would run them, the project's tools and skills, the integrations, and the project's checks. Every entry
|
|
4
|
-
* says what it lets the lead do and, when it can't, the one next step and the settings control (show_control) that
|
|
5
|
-
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
|
-
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
|
-
*/
|
|
8
1
|
import type { Store } from "./store.js";
|
|
9
2
|
import type { ChatControl } from "./chat-controls.js";
|
|
10
3
|
import type { ProviderId } from "./provider.js";
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the lead can rely on right now, in one read (get_capabilities): the agents a project's route would run, the
|
|
3
|
+
* workers that would run them, the project's tools and skills, the integrations, and the project's checks. Every entry
|
|
4
|
+
* says what it lets the lead do and, when it can't, the one next step and the settings control (show_control) that
|
|
5
|
+
* takes the owner there. Everything here reads what is already recorded — runner readiness reports, sign-in pauses,
|
|
6
|
+
* plan-limit readings, tool tests, skill tests, integration checks — and never probes anything that spends.
|
|
7
|
+
*/
|
|
8
|
+
import { projectBatchChecks } from "./batch-policy.js";
|
|
1
9
|
import { resolveRouteCandidates } from "./agentconfig.js";
|
|
2
10
|
import { authPauseOf, messageCommand, providerName } from "./provider-auth.js";
|
|
3
11
|
import { readAuthMode, SUBSCRIPTION_CAPABLE } from "./keys.js";
|
|
@@ -136,7 +144,9 @@ function checksOf(store, repo) {
|
|
|
136
144
|
return { ...base, state: "Off", ok: false, next: "Builds are not checked. Turn checks on for the project if results need them.", link: "projects" };
|
|
137
145
|
if (!releaseCheck)
|
|
138
146
|
return { ...base, state: `${CHECK_LEVEL_WORDS[level]} · no release check`, ok: false, next: "No release check is set up, so no result can show a passing check. Set one for the project.", link: "projects" };
|
|
139
|
-
|
|
147
|
+
// Batch checks are changed in Settings → Projects → Checks (or `project checks --batch on|off`), never from chat.
|
|
148
|
+
const batched = level === "full" && projectBatchChecks(store, repo).on ? " · batched with results that finish together" : "";
|
|
149
|
+
return { ...base, state: level === "quick" && !quickCheck ? "Quick · runs the release check" : `${CHECK_LEVEL_WORDS[level]} · release check set${batched}`, ok: true, next: null, link: null };
|
|
140
150
|
}
|
|
141
151
|
function workersOf(store, repos, now) {
|
|
142
152
|
const live = store.handle.prepare(`SELECT ref.external_id AS task, ref.repo AS repo FROM claim JOIN task_ref AS ref ON ref.id = claim.task_ref
|
package/dist/mate-doors.js
CHANGED
|
@@ -15,7 +15,7 @@ import { executeSharedAction, sharedActionPayload, sharedActionAllowsChallenge,
|
|
|
15
15
|
* silent re-read.
|
|
16
16
|
*/
|
|
17
17
|
import { isCheckLevel, setTaskCheckLevel } from "./check-levels.js";
|
|
18
|
-
import { verifiedAuthor } from "./store.js";
|
|
18
|
+
import { mateTurnKeepsProposals, verifiedAuthor } from "./store.js";
|
|
19
19
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
20
20
|
import { TeamLeads } from './team-leads.js';
|
|
21
21
|
import { fileTaskProposal } from "./proposal.js";
|
|
@@ -97,7 +97,7 @@ export function confirmMateProposal(store, who, proposalId, now, options) {
|
|
|
97
97
|
return { ok: false, kind: proposal.kind, reason: "ceiling-changed", said: "the admitted projects changed since this was proposed — it cannot be confirmed" };
|
|
98
98
|
}
|
|
99
99
|
const turn = store.getMateTurn(proposal.turn);
|
|
100
|
-
if (turn
|
|
100
|
+
if (!mateTurnKeepsProposals(turn) || proposal.state === "drafting") {
|
|
101
101
|
return { ok: false, kind: proposal.kind, reason: "turn-not-answered", said: "this proposal's turn did not finish — it cannot be confirmed" };
|
|
102
102
|
}
|
|
103
103
|
if (proposal.kind === "cancel") {
|
package/dist/mate.d.ts
CHANGED
|
@@ -93,6 +93,7 @@ export type MateTurnOutcome = {
|
|
|
93
93
|
failed: MateFailure;
|
|
94
94
|
message: string;
|
|
95
95
|
unknownSpend: boolean;
|
|
96
|
+
saved?: boolean;
|
|
96
97
|
};
|
|
97
98
|
/** What happened, what it means, and one next step: plain words a person can act on, never internal terms. */
|
|
98
99
|
export declare const MATE_REFUSAL_COPY: Record<MateRefusal, string>;
|
|
@@ -102,7 +103,7 @@ export declare const MATE_CHANNEL_COPY: Record<MateChannelProblem, string>;
|
|
|
102
103
|
export declare const MATE_FAILURE_COPY: {
|
|
103
104
|
readonly stopped: "This reply was stopped before it finished, from another window or by a newer message. Nothing it proposed was kept. Send your message again if you still need an answer.";
|
|
104
105
|
readonly ended: "Your sign-in or this conversation ended while the lead was answering. Nothing it proposed was kept. Sign in again or start a new chat, then send your message again.";
|
|
105
|
-
readonly tooLong:
|
|
106
|
+
readonly tooLong: string;
|
|
106
107
|
readonly secretRead: "Something the lead read looked like a password or key, so it stopped before sending it anywhere. Nothing was kept. Remove the password or key from that task or note, then ask again.";
|
|
107
108
|
readonly secretReply: "The lead's reply contained something that looked like a password or key, so it was thrown away. Nothing was kept. Ask again, without asking for a password or key.";
|
|
108
109
|
readonly paused: "An earlier reply stopped before its cost was known, so chat is paused. This message wasn't answered. Confirm that earlier reply on the Chat page, then send again.";
|
|
@@ -111,7 +112,7 @@ export declare const MATE_FAILURE_COPY: {
|
|
|
111
112
|
readonly unreadable: "The chat provider sent back an answer that couldn't be read, so it was thrown away. Nothing was kept. Send your message again.";
|
|
112
113
|
readonly signIn: "The chat provider couldn't answer; its sign-in may have expired. Nothing was kept. Sign in to it again on this computer, then send your message again.";
|
|
113
114
|
readonly refused: "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute; if it keeps happening, check the provider in Settings → Lead.";
|
|
114
|
-
readonly tooLongUnknownCost:
|
|
115
|
+
readonly tooLongUnknownCost: string;
|
|
115
116
|
readonly lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.";
|
|
116
117
|
readonly unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.";
|
|
117
118
|
readonly empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.";
|
|
@@ -119,6 +120,9 @@ export declare const MATE_FAILURE_COPY: {
|
|
|
119
120
|
};
|
|
120
121
|
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
|
|
121
122
|
export declare function mateFailureText(reason: string | null): string;
|
|
123
|
+
/** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
|
|
124
|
+
* Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
|
|
125
|
+
export declare const MATE_ABORT_GRACE_MS = 5000;
|
|
122
126
|
/** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
|
|
123
127
|
export declare function historyFor(store: Store, thread: number, queuedMessageId?: number): MateHistoryMessage[];
|
|
124
128
|
/** "read 3 · proposed 1 · 2 steps" — counts only, never results (ruling 11). */
|
package/dist/mate.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { mateToolLabel } from "./mate-progress.js";
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
|
+
import { mateTimeoutNotice } from "./store.js";
|
|
4
5
|
import { RESULT_IMAGES_PER_TURN_CAP } from "./chat-evidence.js";
|
|
5
6
|
import { isVerifiedApprover, reproveApprover } from "./principal.js";
|
|
6
7
|
import { MATE_MAX_CALLS_PER_STEP, MATE_MAX_STEPS, MATE_TOOL_RESULT_CAP_BYTES, TURN_WALL_CLOCK_MS, composeMateRequest, credentialKeyOf, isDirectChatProvider, mateWorstCaseForPrice, performMateRequest, priceForConfig, settleForPrice, subscriptionCredentialKey, } from "./converse.js";
|
|
@@ -56,7 +57,7 @@ export const MATE_CHANNEL_COPY = {
|
|
|
56
57
|
export const MATE_FAILURE_COPY = {
|
|
57
58
|
stopped: "This reply was stopped before it finished, from another window or by a newer message. Nothing it proposed was kept. Send your message again if you still need an answer.",
|
|
58
59
|
ended: "Your sign-in or this conversation ended while the lead was answering. Nothing it proposed was kept. Sign in again or start a new chat, then send your message again.",
|
|
59
|
-
tooLong:
|
|
60
|
+
tooLong: mateTimeoutNotice(false),
|
|
60
61
|
secretRead: "Something the lead read looked like a password or key, so it stopped before sending it anywhere. Nothing was kept. Remove the password or key from that task or note, then ask again.",
|
|
61
62
|
secretReply: "The lead's reply contained something that looked like a password or key, so it was thrown away. Nothing was kept. Ask again, without asking for a password or key.",
|
|
62
63
|
paused: "An earlier reply stopped before its cost was known, so chat is paused. This message wasn't answered. Confirm that earlier reply on the Chat page, then send again.",
|
|
@@ -65,7 +66,7 @@ export const MATE_FAILURE_COPY = {
|
|
|
65
66
|
unreadable: "The chat provider sent back an answer that couldn't be read, so it was thrown away. Nothing was kept. Send your message again.",
|
|
66
67
|
signIn: "The chat provider couldn't answer; its sign-in may have expired. Nothing was kept. Sign in to it again on this computer, then send your message again.",
|
|
67
68
|
refused: "The chat provider turned the request down. Nothing was kept or charged. Try again in a minute; if it keeps happening, check the provider in Settings → Lead.",
|
|
68
|
-
tooLongUnknownCost:
|
|
69
|
+
tooLongUnknownCost: mateTimeoutNotice(false, true),
|
|
69
70
|
lostUnknownCost: "The chat provider stopped responding partway through, and the cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
70
71
|
unreadableUnknownCost: "The chat provider sent back an answer that couldn't be read, and its cost isn't known, so chat is paused. Nothing it proposed was kept. Confirm the cost on the Chat page to turn chat back on.",
|
|
71
72
|
empty: "The lead sent back an empty answer. Nothing was kept. Send your message again.",
|
|
@@ -74,7 +75,8 @@ export const MATE_FAILURE_COPY = {
|
|
|
74
75
|
/** A failed turn's saved reason as plain words, for a surface that reads the turn back later (a restart, a delivery retry). */
|
|
75
76
|
export function mateFailureText(reason) {
|
|
76
77
|
switch (reason) {
|
|
77
|
-
case "timeout":
|
|
78
|
+
case "timeout":
|
|
79
|
+
case "crashed": return MATE_FAILURE_COPY.tooLong;
|
|
78
80
|
case "malformed-reply": return MATE_FAILURE_COPY.unreadable;
|
|
79
81
|
case "secret-refused": return MATE_FAILURE_COPY.secretReply;
|
|
80
82
|
case "latched": return MATE_FAILURE_COPY.paused;
|
|
@@ -84,6 +86,16 @@ export function mateFailureText(reason) {
|
|
|
84
86
|
default: return "The lead's reply didn't finish. Nothing it proposed was kept. Send your message again.";
|
|
85
87
|
}
|
|
86
88
|
}
|
|
89
|
+
/** How long a provider that ignored its abort at the turn's deadline is waited for before the turn ends without it.
|
|
90
|
+
* Its process group was already killed at the deadline; this only bounds the wait for the transport to say so. */
|
|
91
|
+
export const MATE_ABORT_GRACE_MS = 5_000;
|
|
92
|
+
const LATE = Symbol("late");
|
|
93
|
+
/** The work's own result, or LATE once `ms` passes: the turn's clock, not the work, decides when the person hears back. */
|
|
94
|
+
function settleBy(work, ms) {
|
|
95
|
+
let timer;
|
|
96
|
+
const late = new Promise(resolve => { timer = setTimeout(() => resolve(LATE), Math.max(0, ms)); });
|
|
97
|
+
return Promise.race([work, late]).finally(() => clearTimeout(timer));
|
|
98
|
+
}
|
|
87
99
|
const READ_TOOLS = new Set(["get_brief", "get_project_context", "get_actions", "get_action_status", "get_skills", "get_acceptance_evidence", "recap", "list_repos", "list_tasks", "get_task", "get_result", "get_result_images", "get_controls", "get_agents", "get_project_knowledge", "get_task_conversation", "get_diff", "get_check_log", "get_project_tools", "get_flows", "get_flow_insights", "list_decisions", "get_decision", "queue"]);
|
|
88
100
|
/** The last messages of the thread as provider-neutral history, newest kept first until the byte cap. */
|
|
89
101
|
export function historyFor(store, thread, queuedMessageId) {
|
|
@@ -291,6 +303,20 @@ export async function runMateTurn(input) {
|
|
|
291
303
|
store.dropMateAsk(turnId);
|
|
292
304
|
return { ok: false, turn: turnId, failed, message, unknownSpend };
|
|
293
305
|
};
|
|
306
|
+
/** The turn stopped at its deadline: the notice is saved in the thread with whatever its completed tool calls proposed, in
|
|
307
|
+
* the same write that ends the turn, so a refresh, another device or a restart shows the same outcome. */
|
|
308
|
+
const timedOut = (unknownSpend) => {
|
|
309
|
+
now = clock();
|
|
310
|
+
const message = mateTimeoutNotice(proposals > 0, unknownSpend);
|
|
311
|
+
const saved = store.finalizeMateTurn(turnId, started.generation, { state: "failed", settledMicrousd: settled, unknownSpend, tokensIn, tokensOut, failureReason: "timeout", keepProposals: true, message: { text: message, activity: activitySummary(reads, proposals, steps) } }, now);
|
|
312
|
+
store.dropMateAsk(turnId);
|
|
313
|
+
if (!saved)
|
|
314
|
+
return { ok: false, turn: turnId, failed: "superseded", message: MATE_FAILURE_COPY.stopped, unknownSpend: false };
|
|
315
|
+
return { ok: false, turn: turnId, failed: "timeout", message, unknownSpend, saved: true };
|
|
316
|
+
};
|
|
317
|
+
const deadlineLeft = () => TURN_WALL_CLOCK_MS - (clock().getTime() - turnStartedAt);
|
|
318
|
+
/** The channel's standing, bounded by the turn's deadline: a lookup that never answers cannot hold the turn open. */
|
|
319
|
+
const revalidated = async () => input.revalidate === undefined ? { ok: true } : settleBy(input.revalidate(), deadlineLeft() + MATE_ABORT_GRACE_MS);
|
|
294
320
|
/** The turn's own row, re-read: still running under our generation, or someone ended it under us. */
|
|
295
321
|
const stillOurs = () => {
|
|
296
322
|
const row = store.getMateTurn(turnId);
|
|
@@ -301,6 +327,8 @@ export async function runMateTurn(input) {
|
|
|
301
327
|
const guard = (channel) => {
|
|
302
328
|
if (!stillOurs())
|
|
303
329
|
return { ok: false, turn: turnId, failed: "superseded", message: MATE_FAILURE_COPY.stopped, unknownSpend: false };
|
|
330
|
+
if (channel === LATE)
|
|
331
|
+
return timedOut(false);
|
|
304
332
|
if (!channel.ok)
|
|
305
333
|
return fail("revoked", MATE_CHANNEL_COPY[channel.reason] ?? MATE_CHANNEL_COPY["access-changed"], false);
|
|
306
334
|
const standing = reproveApprover(store, who);
|
|
@@ -328,13 +356,13 @@ export async function runMateTurn(input) {
|
|
|
328
356
|
return claim;
|
|
329
357
|
};
|
|
330
358
|
while (steps < maxSteps) {
|
|
331
|
-
const blocked = guard(
|
|
359
|
+
const blocked = guard(await revalidated());
|
|
332
360
|
if (blocked !== null)
|
|
333
361
|
return blocked;
|
|
334
362
|
now = clock();
|
|
335
363
|
const remainingMs = TURN_WALL_CLOCK_MS - (now.getTime() - turnStartedAt);
|
|
336
364
|
if (remainingMs <= 0)
|
|
337
|
-
return
|
|
365
|
+
return timedOut(false);
|
|
338
366
|
const request = direct ? composeDirect(input.key) : composeSubscription();
|
|
339
367
|
// Tool results join the outbound body: scanned again before every dispatch.
|
|
340
368
|
const outbound = typeof request === "string" ? request : request.body;
|
|
@@ -354,39 +382,34 @@ export async function runMateTurn(input) {
|
|
|
354
382
|
progress({ kind: "step", turn: turnId, step: steps });
|
|
355
383
|
const requestBytes = Buffer.byteLength(outbound, "utf8");
|
|
356
384
|
let result;
|
|
385
|
+
// The turn owns its deadline (lead-stall, release check 2438): at the deadline the request is aborted, which ends a
|
|
386
|
+
// harness's whole process group, and a provider that still has not settled MATE_ABORT_GRACE_MS later is left behind.
|
|
387
|
+
// Whatever it says after that is never read; the turn has already ended with its notice.
|
|
388
|
+
const controller = new AbortController();
|
|
389
|
+
const abortAt = setTimeout(() => controller.abort(), remainingMs);
|
|
390
|
+
const settle = (work) => settleBy(work, remainingMs + MATE_ABORT_GRACE_MS)
|
|
391
|
+
.then(value => value === LATE || controller.signal.aborted ? { ok: false, problem: "timeout" } : value)
|
|
392
|
+
.finally(() => clearTimeout(abortAt));
|
|
357
393
|
if (direct) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
try {
|
|
361
|
-
result = await performMateRequest(request, directProvider, controller.signal, fetcher);
|
|
362
|
-
}
|
|
363
|
-
catch {
|
|
364
|
-
result = { ok: false, problem: "network" };
|
|
365
|
-
}
|
|
366
|
-
finally {
|
|
367
|
-
clearTimeout(timer);
|
|
368
|
-
}
|
|
394
|
+
result = await settle(performMateRequest(request, directProvider, controller.signal, fetcher)
|
|
395
|
+
.catch(() => ({ ok: false, problem: "network" })));
|
|
369
396
|
}
|
|
370
397
|
else {
|
|
371
398
|
const runner = input.subscriptionRunner ?? performSubscriptionMateRequest;
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
result = { ok: false, problem: "provider-error" };
|
|
389
|
-
}
|
|
399
|
+
result = await settle((async () => runner({
|
|
400
|
+
provider: subscriptionProvider,
|
|
401
|
+
model: config.model,
|
|
402
|
+
system: MATE_CONTRACT,
|
|
403
|
+
dataDocument: document,
|
|
404
|
+
history,
|
|
405
|
+
tools: MATE_TOOL_SCHEMAS,
|
|
406
|
+
timeoutMs: remainingMs,
|
|
407
|
+
signal: controller.signal,
|
|
408
|
+
// The reply as it is written; text that looks like a secret is
|
|
409
|
+
// never shown (the finished reply is scanned again before saving).
|
|
410
|
+
...(input.onProgress === undefined ? {} : { onText: (text) => { if (!controller.signal.aborted && scanForSecrets(text).length === 0)
|
|
411
|
+
progress({ kind: "text", turn: turnId, step: steps, text }); } }),
|
|
412
|
+
}))().catch(() => ({ ok: false, problem: "provider-error" })));
|
|
390
413
|
}
|
|
391
414
|
now = clock();
|
|
392
415
|
const finishStep = (outcome) => store.finalizeChatTurn(step.id, stepStarted.generation, outcome, now);
|
|
@@ -400,7 +423,9 @@ export async function runMateTurn(input) {
|
|
|
400
423
|
: result.problem === "malformed-reply"
|
|
401
424
|
? MATE_FAILURE_COPY.unreadable
|
|
402
425
|
: MATE_FAILURE_COPY.signIn;
|
|
403
|
-
|
|
426
|
+
if (result.problem === "timeout")
|
|
427
|
+
return timedOut(false);
|
|
428
|
+
return fail(result.problem === "malformed-reply" ? "malformed-reply" : "provider-error", message, false);
|
|
404
429
|
}
|
|
405
430
|
if (result.problem.startsWith("status-")) {
|
|
406
431
|
// The provider ANSWERED with an error: nothing billed for this step.
|
|
@@ -409,7 +434,7 @@ export async function runMateTurn(input) {
|
|
|
409
434
|
}
|
|
410
435
|
if (result.problem === "timeout") {
|
|
411
436
|
finishStep({ state: "failed", failureReason: "timeout", settledMicrousd: null, unknownSpend: true });
|
|
412
|
-
return
|
|
437
|
+
return timedOut(true);
|
|
413
438
|
}
|
|
414
439
|
if (result.problem === "network") {
|
|
415
440
|
finishStep({ state: "failed", failureReason: "provider-error", settledMicrousd: null, unknownSpend: true });
|
|
@@ -437,7 +462,7 @@ export async function runMateTurn(input) {
|
|
|
437
462
|
// After the wait (finding 5): the row may have been failed under us by
|
|
438
463
|
// a revocation or the sweep — then nothing the model said runs; and the
|
|
439
464
|
// approver must still stand before any tool runs as them.
|
|
440
|
-
const changed = guard(
|
|
465
|
+
const changed = guard(await revalidated());
|
|
441
466
|
if (changed !== null)
|
|
442
467
|
return changed;
|
|
443
468
|
if (answer.calls.length === 0) {
|
|
@@ -470,7 +495,7 @@ export async function runMateTurn(input) {
|
|
|
470
495
|
history.push({ role: "assistant", text: answer.text, calls: answer.calls });
|
|
471
496
|
for (const [index, call] of answer.calls.entries()) {
|
|
472
497
|
if (index > 0) {
|
|
473
|
-
const changed = guard(
|
|
498
|
+
const changed = guard(await revalidated());
|
|
474
499
|
if (changed !== null)
|
|
475
500
|
return changed;
|
|
476
501
|
}
|
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -80,12 +80,13 @@ type Server = {
|
|
|
80
80
|
scopes: string[];
|
|
81
81
|
resource: string | null;
|
|
82
82
|
};
|
|
83
|
-
/** A sign-in on its way: what the callback needs to finish it (kept in the console's memory, 15 minutes). */
|
|
83
|
+
/** A sign-in on its way: what the callback needs to finish it (kept in the console's memory, 15 minutes). `kit` or `template` is the page it returns to. */
|
|
84
84
|
export type ConnectVisit = {
|
|
85
85
|
service: string;
|
|
86
86
|
repo: string;
|
|
87
87
|
by: string;
|
|
88
88
|
kit: string | null;
|
|
89
|
+
template: string | null;
|
|
89
90
|
verifier: string;
|
|
90
91
|
clientId: string;
|
|
91
92
|
clientSecret: string | null;
|
|
@@ -107,6 +108,7 @@ export declare function startConnect(input: {
|
|
|
107
108
|
by: string;
|
|
108
109
|
origin: string;
|
|
109
110
|
kit?: string | null;
|
|
111
|
+
template?: string | null;
|
|
110
112
|
}, fetcher?: Fetch, now?: number): Promise<{
|
|
111
113
|
ok: true;
|
|
112
114
|
go: string;
|