toolroll 0.9.6 → 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/dist/actor.d.ts +21 -0
  3. package/dist/actor.js +48 -0
  4. package/dist/assignment-adapters.d.ts +1 -0
  5. package/dist/assignment-ui.js +3 -1
  6. package/dist/assignment.d.ts +9 -0
  7. package/dist/assignment.js +62 -13
  8. package/dist/browser/workspace.css +2 -2
  9. package/dist/browser/workspace.js +28 -24
  10. package/dist/browser-workspace.d.ts +117 -2
  11. package/dist/builder.d.ts +1 -0
  12. package/dist/builder.js +33 -13
  13. package/dist/chat-actions.js +2 -1
  14. package/dist/chat-delivery.js +2 -0
  15. package/dist/chat-quiet.js +11 -1
  16. package/dist/chat-task-actions.d.ts +13 -0
  17. package/dist/chat-task-actions.js +19 -1
  18. package/dist/check-levels-ui.d.ts +21 -0
  19. package/dist/check-levels-ui.js +70 -0
  20. package/dist/check-levels.d.ts +67 -0
  21. package/dist/check-levels.js +171 -0
  22. package/dist/claim.d.ts +22 -0
  23. package/dist/claim.js +43 -0
  24. package/dist/cli.d.ts +1 -1
  25. package/dist/cli.js +4 -0
  26. package/dist/demo-chat.js +4 -1
  27. package/dist/dispatch.js +7 -2
  28. package/dist/dispose.d.ts +8 -0
  29. package/dist/dispose.js +7 -1
  30. package/dist/export.js +3 -2
  31. package/dist/first-run.d.ts +34 -0
  32. package/dist/first-run.js +37 -1
  33. package/dist/flow-gallery-ui.d.ts +30 -0
  34. package/dist/flow-gallery-ui.js +85 -0
  35. package/dist/flow-gallery.d.ts +111 -0
  36. package/dist/flow-gallery.js +445 -0
  37. package/dist/flow-share.d.ts +102 -0
  38. package/dist/flow-share.js +614 -0
  39. package/dist/flow-starters.d.ts +2 -0
  40. package/dist/flow-starters.js +1 -1
  41. package/dist/flow-steps.js +7 -0
  42. package/dist/flow-triggers.js +2 -0
  43. package/dist/flows-cli.d.ts +56 -0
  44. package/dist/flows-cli.js +109 -3
  45. package/dist/flows-ui.d.ts +15 -1
  46. package/dist/flows-ui.js +31 -6
  47. package/dist/guides.js +27 -2
  48. package/dist/lead-context.js +6 -2
  49. package/dist/mate-doors.d.ts +0 -15
  50. package/dist/mate-doors.js +5 -1
  51. package/dist/mate-tools.js +9 -4
  52. package/dist/models-ui.js +1 -1
  53. package/dist/operate.d.ts +4 -1
  54. package/dist/operate.js +601 -85
  55. package/dist/project-concurrency.d.ts +62 -0
  56. package/dist/project-concurrency.js +116 -0
  57. package/dist/project-delete-ui.d.ts +7 -0
  58. package/dist/project-delete-ui.js +19 -2
  59. package/dist/project-delete.js +4 -0
  60. package/dist/pull-request-flow.d.ts +9 -0
  61. package/dist/pull-request-flow.js +21 -3
  62. package/dist/result-follow-ups.d.ts +101 -0
  63. package/dist/result-follow-ups.js +333 -0
  64. package/dist/scope.js +11 -0
  65. package/dist/serve.d.ts +211 -2
  66. package/dist/serve.js +1009 -141
  67. package/dist/store.d.ts +76 -3
  68. package/dist/store.js +311 -22
  69. package/dist/surface.js +31 -8
  70. package/dist/tailnet.d.ts +11 -0
  71. package/dist/tailnet.js +47 -0
  72. package/dist/task-outcome-cli.js +2 -2
  73. package/dist/task-status.d.ts +8 -1
  74. package/dist/task-status.js +28 -10
  75. package/dist/team-cli.js +3 -0
  76. package/dist/telegram-status.js +2 -2
  77. package/dist/verification-evidence.js +21 -1
  78. package/dist/webhooks.js +5 -0
  79. package/dist/work-index.js +39 -6
  80. package/dist/work-summary.d.ts +0 -4
  81. package/dist/work-summary.js +2 -0
  82. package/dist/workspace-ui.d.ts +3 -0
  83. package/dist/workspace-ui.js +5 -3
  84. package/package.json +1 -1
@@ -0,0 +1,171 @@
1
+ /** Check levels: how much of the project check runs after a build.
2
+ *
3
+ * - Quick: a fast check while work is in progress (typecheck plus the tests
4
+ * near the change). New projects start here.
5
+ * - Full: the project's approved check, as before. Projects that already
6
+ * had a check keep it.
7
+ * - Off: no check. The result reads Ready for review, with "Checks: Off for this project" underneath.
8
+ *
9
+ * Nothing here needs a new table. A project's level and a task's choice are
10
+ * append-only ledger entries (the newest one wins, and the ledger already
11
+ * says who changed what, when). The quick command is a second approved
12
+ * verification grant with its own digest, kept under a `quick:` key beside
13
+ * the full one, so approving it is the same ceremony as `verify set`. */
14
+ import { existsSync, readFileSync } from "node:fs";
15
+ import { join } from "node:path";
16
+ import { approvalOf } from "./scope.js";
17
+ export const CHECK_LEVELS = ["quick", "full", "off"];
18
+ export const isCheckLevel = (value) => typeof value === "string" && CHECK_LEVELS.includes(value);
19
+ export const CHECK_LEVEL_WORDS = { quick: "Quick", full: "Full", off: "Off" };
20
+ export const CHECK_LEVEL_HINTS = {
21
+ quick: "A fast check while building: typecheck and the tests near the change.",
22
+ full: "The project's full check after every build.",
23
+ off: "No check. Results are Ready for review, and say no check ran.",
24
+ };
25
+ /** Ledger actions. The ledger is append-only, so the newest entry is the setting. */
26
+ export const PROJECT_LEVEL_ACTION = "check level changed";
27
+ export const TASK_LEVEL_ACTION = "task checks chosen";
28
+ export const RUN_LEVEL_ACTION = "checks used";
29
+ /** The quick command's grant lives beside the full one under its own key. */
30
+ export const quickVerifyKey = (repo) => `quick:${repo}`;
31
+ export function liveQuickCommand(store, repo) {
32
+ return store.liveVerifyCommand(quickVerifyKey(repo));
33
+ }
34
+ /** Ledger reads go through the repo index: `repo = ?` (or IS NULL for unplaced work). */
35
+ export const repoClause = (repo) => repo === null ? ["repo IS NULL", []] : ["repo = ?", [repo]];
36
+ export function repoOfRun(store, runId) {
37
+ const run = store.getRun(runId);
38
+ return run === null ? null : store.refById(run.taskRef)?.repo ?? null;
39
+ }
40
+ function newest(store, sql, ...params) {
41
+ const row = store.handle.prepare(sql).get(...params);
42
+ return row === undefined ? null : { actor: String(row["actor"]), at: String(row["at"]), outcome: String(row["outcome"]) };
43
+ }
44
+ /** A project's level. With nothing recorded it is Full: a project that existed
45
+ * before levels keeps exactly the check it had. New projects are stamped Quick
46
+ * when they are added (stampNewProjectLevel). */
47
+ export function projectCheckLevel(store, repo) {
48
+ const row = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? ORDER BY id DESC LIMIT 1", repo, PROJECT_LEVEL_ACTION);
49
+ if (row === null || !isCheckLevel(row.outcome))
50
+ return { level: "full", setBy: null, at: null };
51
+ return { level: row.outcome, setBy: row.actor, at: row.at };
52
+ }
53
+ /** An approver's act: set the project's level. The ledger keeps before → after. */
54
+ export function setProjectCheckLevel(store, repo, level, by, now) {
55
+ const before = projectCheckLevel(store, repo);
56
+ if (before.level === level && before.setBy !== null)
57
+ return { changed: false, before: before.level };
58
+ store.recordAction({ at: now.toISOString(), actor: by, repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: level, source: "policy",
59
+ detail: `${CHECK_LEVEL_WORDS[before.level]} → ${CHECK_LEVEL_WORDS[level]}` });
60
+ return { changed: before.level !== level, before: before.level };
61
+ }
62
+ /** A project added from now on starts on Quick. One that already has a level,
63
+ * or an approved check from before levels existed, is left exactly as it is. */
64
+ export function stampNewProjectLevel(store, repo, now) {
65
+ const known = newest(store, "SELECT actor, at, outcome FROM action_ledger WHERE repo = ? AND task_id IS NULL AND action = ? LIMIT 1", repo, PROJECT_LEVEL_ACTION);
66
+ if (known !== null)
67
+ return false;
68
+ const hadCheck = store.handle.prepare("SELECT 1 FROM verify_command WHERE repo = ? LIMIT 1").get(repo) !== undefined;
69
+ if (hadCheck)
70
+ return false;
71
+ store.recordAction({ at: now.toISOString(), actor: "system", repo, taskId: null, runId: null, action: PROJECT_LEVEL_ACTION, outcome: "quick", source: "policy",
72
+ detail: "New project → Quick" });
73
+ return true;
74
+ }
75
+ /** A task's own choice, if it made one; a revision inherits the task it revises. */
76
+ export function taskCheckLevel(store, taskId) {
77
+ let id = taskId;
78
+ for (let depth = 0; id !== null && depth < 64; depth++) {
79
+ const ref = store.lookupRef(id);
80
+ const [where, params] = repoClause(ref?.repo ?? null);
81
+ const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND task_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, id, TASK_LEVEL_ACTION);
82
+ if (row !== null && isCheckLevel(row.outcome))
83
+ return row.outcome;
84
+ id = ref?.revisionOf ?? null;
85
+ }
86
+ return null;
87
+ }
88
+ /** A filing choice: this task's checks, overriding the project. Fixed once its
89
+ * plan is approved, so what the approver saw is what runs. */
90
+ export function setTaskCheckLevel(store, taskId, level, by, now) {
91
+ const ref = store.lookupRef(taskId);
92
+ if (ref === null)
93
+ return { ok: false, message: `No task ${taskId}.` };
94
+ if (approvalOf(store.getScope(taskId)).approved) {
95
+ return { ok: false, message: "This task's plan is already approved, so its checks are fixed. File a new task to change them." };
96
+ }
97
+ if (taskCheckLevel(store, taskId) === level)
98
+ return { ok: true };
99
+ store.recordAction({ at: now.toISOString(), actor: by, repo: ref.repo, taskId, runId: null, action: TASK_LEVEL_ACTION, outcome: level, source: "request",
100
+ detail: `Checks for this task: ${CHECK_LEVEL_WORDS[level]}` });
101
+ return { ok: true };
102
+ }
103
+ export function effectiveCheckLevel(store, repo, taskId) {
104
+ const own = taskId === null ? null : taskCheckLevel(store, taskId);
105
+ if (own !== null)
106
+ return { level: own, from: "task" };
107
+ return { level: repo === null ? "full" : projectCheckLevel(store, repo).level, from: "project" };
108
+ }
109
+ /** The command a level runs. Quick without its own command runs the full one
110
+ * (and says so: the level that ran is Full). Off runs nothing. */
111
+ export function checkCommandFor(store, repo, level) {
112
+ if (level === "off")
113
+ return { level: "off", command: null };
114
+ if (level === "quick") {
115
+ const quick = liveQuickCommand(store, repo);
116
+ if (quick !== null)
117
+ return { level: "quick", command: quick };
118
+ }
119
+ return { level: "full", command: store.liveVerifyCommand(repo) };
120
+ }
121
+ /** The builder records which level a run used, so its result says it honestly. */
122
+ export function recordRunCheckLevel(store, run, level, from, now) {
123
+ store.recordAction({ at: now.toISOString(), actor: "system", repo: run.repo, taskId: run.taskId, runId: run.id, action: RUN_LEVEL_ACTION, outcome: level, source: "work",
124
+ detail: `${CHECK_LEVEL_WORDS[level]} checks (${from === "task" ? "this task's choice" : "project setting"})` });
125
+ }
126
+ /** The level a finished run's checks used; null for a run from before levels. */
127
+ export function runCheckLevel(store, runId) {
128
+ const [where, params] = repoClause(repoOfRun(store, runId));
129
+ const row = newest(store, `SELECT actor, at, outcome FROM action_ledger WHERE ${where} AND run_id = ? AND action = ? ORDER BY id DESC LIMIT 1`, ...params, runId, RUN_LEVEL_ACTION);
130
+ return row !== null && isCheckLevel(row.outcome) ? row.outcome : null;
131
+ }
132
+ /** A starting point for the quick command, from the project's own scripts.
133
+ * It is only a suggestion: a person approves the exact command. */
134
+ export function suggestQuickCommand(repo) {
135
+ const file = join(repo, "package.json");
136
+ if (!existsSync(file))
137
+ return null;
138
+ let scripts = {};
139
+ let deps = {};
140
+ try {
141
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
142
+ scripts = parsed.scripts ?? {};
143
+ deps = { ...(parsed.dependencies ?? {}), ...(parsed.devDependencies ?? {}) };
144
+ }
145
+ catch {
146
+ return null;
147
+ }
148
+ const has = (name) => typeof scripts[name] === "string";
149
+ const runner = existsSync(join(repo, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(repo, "yarn.lock")) ? "yarn" : "npm";
150
+ const run = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
151
+ const exec = runner === "npm" ? "npx" : `${runner} exec`;
152
+ const typecheck = ["typecheck", "type-check", "check-types", "tsc"].find(has) ?? null;
153
+ const changed = "$(git diff --name-only HEAD~1)";
154
+ const tests = "vitest" in deps ? `${exec} vitest related --run ${changed}`
155
+ : "jest" in deps ? `${exec} jest --findRelatedTests --passWithNoTests ${changed}` : null;
156
+ const parts = [typecheck === null ? null : run(typecheck), tests].filter((one) => one !== null);
157
+ if (parts.length > 0)
158
+ return parts.join(" && ");
159
+ return has("lint") ? run("lint") : null;
160
+ }
161
+ /** "skip the tests", "run the full checks": a filing request in chat, as a level. */
162
+ export function checkLevelFromWords(text) {
163
+ const said = text.toLowerCase().replace(/[’']/g, "'");
164
+ if (/\b(skip|without|no|don't run|do not run|turn off|switch off)\b[^.!?\n]{0,20}\b(the )?(tests|checks)\b/.test(said) || /\b(tests|checks) off\b/.test(said))
165
+ return "off";
166
+ if (/\b(full|all the|every|complete)\s+(test|check)s?\b/.test(said))
167
+ return "full";
168
+ if (/\bquick\s+(test|check)s?\b/.test(said))
169
+ return "quick";
170
+ return null;
171
+ }
package/dist/claim.d.ts CHANGED
@@ -411,6 +411,28 @@ export declare function finalizeInterruptedFenced(store: Store, args: {
411
411
  committed?: boolean;
412
412
  now: Date;
413
413
  }): InterruptSeal;
414
+ /**
415
+ * Hand an attempt back because the SERVICE stopped (its builder is
416
+ * stopping or restarting), not because anyone stopped the task: one fenced
417
+ * transaction releases the claim as `interrupted`, ends the run (and every
418
+ * open run it owns) as `failed` / `interrupted` — the words dead-runner
419
+ * recovery writes, so the next attempt inherits the preserved work in the
420
+ * same checkout exactly as after a crash — and returns the task to the
421
+ * queue with no strike, no backoff and no hold, so the next builder pass
422
+ * resumes it. A lease the world moved past still ends the run, touching no
423
+ * task state.
424
+ */
425
+ export declare function finalizeHandBackFenced(store: Store, args: {
426
+ leaseId: string;
427
+ runId: number;
428
+ taskId: string;
429
+ message: string;
430
+ now: Date;
431
+ }): {
432
+ ok: true;
433
+ requeued: boolean;
434
+ fenced: boolean;
435
+ };
414
436
  /**
415
437
  * THE STOP FENCE at settlement (v52): when a stop applies to the run —
416
438
  * its own or an owning ancestor's — seal the attempt as interrupted and
package/dist/claim.js CHANGED
@@ -1003,6 +1003,49 @@ export function finalizeInterruptedFenced(store, args) {
1003
1003
  return { ok: true, stopRun, requeued: Number(requeued.changes) > 0, fenced: false };
1004
1004
  });
1005
1005
  }
1006
+ /**
1007
+ * Hand an attempt back because the SERVICE stopped (its builder is
1008
+ * stopping or restarting), not because anyone stopped the task: one fenced
1009
+ * transaction releases the claim as `interrupted`, ends the run (and every
1010
+ * open run it owns) as `failed` / `interrupted` — the words dead-runner
1011
+ * recovery writes, so the next attempt inherits the preserved work in the
1012
+ * same checkout exactly as after a crash — and returns the task to the
1013
+ * queue with no strike, no backoff and no hold, so the next builder pass
1014
+ * resumes it. A lease the world moved past still ends the run, touching no
1015
+ * task state.
1016
+ */
1017
+ export function finalizeHandBackFenced(store, args) {
1018
+ const { leaseId, runId, taskId, now } = args;
1019
+ const db = store.handle;
1020
+ return inTransaction(store, () => {
1021
+ const run = store.getRun(runId);
1022
+ if (run === null || run.leaseId !== leaseId || run.outcome !== null) {
1023
+ throw new Error(`run ${runId} is not ${leaseId}'s open attempt — a hand-back seals exactly one`);
1024
+ }
1025
+ const { changes } = db
1026
+ .prepare(`UPDATE claim SET released_at = ?, released_by = 'interrupted'
1027
+ WHERE lease_id = ? AND released_at IS NULL AND ${NOT_SUPERSEDED}`)
1028
+ .run(now.toISOString(), leaseId);
1029
+ const fenced = Number(changes) === 0;
1030
+ if (args.message.trim() !== "")
1031
+ store.recordOutcomeFacts(runId, { handoff: args.message });
1032
+ for (const owned of store.ownedRunsOf(runId)) {
1033
+ if (owned === runId)
1034
+ continue;
1035
+ const child = store.getRun(owned);
1036
+ if (child !== null && child.outcome === null)
1037
+ store.finishRun(owned, { outcome: "failed", reason: "interrupted", now });
1038
+ }
1039
+ store.finishRun(runId, { outcome: "failed", reason: "interrupted", now });
1040
+ if (fenced)
1041
+ return { ok: true, requeued: false, fenced: true };
1042
+ const requeued = db
1043
+ .prepare("UPDATE task SET state = 'queued', updated_at = ? WHERE id = ? AND state = 'running'")
1044
+ .run(now.toISOString(), taskId);
1045
+ store.bumpWake();
1046
+ return { ok: true, requeued: Number(requeued.changes) > 0, fenced: false };
1047
+ });
1048
+ }
1006
1049
  /**
1007
1050
  * THE STOP FENCE at settlement (v52): when a stop applies to the run —
1008
1051
  * its own or an owning ancestor's — seal the attempt as interrupted and
package/dist/cli.d.ts CHANGED
@@ -39,7 +39,7 @@ export type ParseResult = {
39
39
  error: string;
40
40
  };
41
41
  type Write = (line: string) => void;
42
- export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
42
+ export declare const HELP = "toolroll \u2014 a control plane for unattended coding agents\n\nUsage\n toolroll [path...] report what is in flight\n toolroll pulls report what is waiting on a person\n toolroll graph report which work graph is already here\n toolroll repos list connected repositories, and how to adjust\n toolroll repos add <path> connect one (no path: the repo you are in)\n toolroll repos remove <path>\n toolroll repos add-from-github <owner/name> --root <dir>\n preview, then clone and connect (--yes)\n toolroll link put `toolroll` on your PATH\n toolroll unlink take it off again\n toolroll update update to the latest release: verified, drained, undoable\n (preview first; --yes, --now, --at HH:MM, --rollback)\n toolroll contract the machine contract: envelope version + capabilities\n (--commands dumps the declared command guide)\n toolroll skills install --claude-code [--dir <path>]\n install this binary's guides for Claude Code (preview first)\n toolroll skills install teach a repo's agents this queue exists (preview first)\n toolroll skills list the guides this exact binary serves\n toolroll skills get <name> print one guide (version-matched, never stale)\n toolroll demo a seeded throwaway sandbox \u2014 see it working in 90 seconds\n toolroll up app + builder for every saved project \u2014 the normal start\n toolroll onboard make your agent Toolroll's lead: add this repo, install its skill (--yes), offer starter flows (--starter)\n toolroll status running, queued, ready results, release check and plan windows\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll session native coding sessions through the running service\n toolroll connect save a private connection to your central service\n toolroll lead named leads on the connected service\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll conversation shared and private conversations on that service\n toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)\n\nOperating the queue \u2014 `toolroll task` prints the whole surface,\nand any queue command + --help prints it too\n toolroll approver add <name>\n mint the credential that lets a person say yes\n toolroll ready what could be dispatched right now\n toolroll check-progress <run> current or final approved check progress\n toolroll task wait <id> [--timeout <seconds>]\n wait for this attempt to finish or need a person\n toolroll task add <title> queue work\n toolroll task scope <id> --goal <text>\n state what success is; approval binds to it\n toolroll task approve <id>\n the yes \u2014 nothing builds without one\n toolroll claim <id> --runner <name>\n toolroll heartbeat <lease> / release <lease> / reap\n toolroll tick --runner <name> --token <t> --repo <path>\n one unattended pass over the ready set\n toolroll flows list | show <id> | create | edit | trigger | script | card | archive\n set up and inspect flows (writes preview until --yes)\n toolroll chat --as <you> --token <t>\n talk to the mate: one conversation across every\n project; it proposes, you confirm (--say \"\u2026\" for one turn)\n toolroll serve --repo <path> advanced: run only the console\n (--editor vscode with --runner: file links\n open in VS Code on the device you browse from)\n toolroll watch / daemon install advanced: run only the builder\n toolroll reconcile recover what the last stretch left behind\n\nWith nothing connected it reports everything it can find below the working\ndirectory. Once you connect repositories it reports those instead.\n\nOptions\n --all report everything discoverable, ignoring connected repos\n --depth <n> directory levels to descend (default: 6)\n --hidden include dot-directories, which are skipped by default\n --dirty also read each working tree for uncommitted files\n --local branches only: no network, no pull requests, no issues\n --json emit a machine-readable envelope instead of a report\n -o <file> also write that envelope to a file (requires --json)\n -h, --help show this\n\nNothing is written, installed, or configured. Every repository is read\nthrough git with --no-optional-locks, so a scan never contends for the\nindex lock with an editor you have open.\n\n--dirty is off by default because reading a working tree is unbounded\nwork: on a large or cloud-synced repo `git status` can take minutes,\nwhere listing branches takes milliseconds.\n\nlink shows what it would do and changes nothing; add --yes to apply it,\nor --to <dir> to choose where. It never uses sudo, never leaves your\nhome directory, and never touches a file it did not create.";
43
43
  export declare function parseArgs(argv: readonly string[]): ParseResult;
44
44
  /** The commands that operate the queue rather than report on the world. */
45
45
  /** Every verb the CLI routes OUTSIDE runOperate. Exported (with
package/dist/cli.js CHANGED
@@ -65,6 +65,7 @@ Usage
65
65
  toolroll session native coding sessions through the running service
66
66
  toolroll connect save a private connection to your central service
67
67
  toolroll lead named leads on the connected service
68
+ toolroll lead token mint your lead agent's credential: its own work pings nobody
68
69
  toolroll conversation shared and private conversations on that service
69
70
  toolroll chat --lead <id> --conversation <id> central chat (use --local for local chat)
70
71
 
@@ -192,6 +193,8 @@ export const TOP_LEVEL_COMMANDS = [
192
193
  ];
193
194
  export const OPERATE_COMMANDS = new Set([
194
195
  "up",
196
+ // Only `lead token` reaches here; the central service's lead verbs answer first.
197
+ "lead",
195
198
  "onboard",
196
199
  "status",
197
200
  "ready",
@@ -1275,6 +1278,7 @@ async function runDemoCommand(argv, write) {
1275
1278
  write(` sandbox ${sandbox}`);
1276
1279
  write("");
1277
1280
  write("Open Chat and ask for a change, like \"fix the flaky refund test\".");
1281
+ write("For your own project, run `npx toolroll up` in its folder.");
1278
1282
  write("");
1279
1283
  write(keep
1280
1284
  ? "Ctrl-C stops the server; the sandbox is KEPT. It stays a demo forever — every spending command refuses it."
package/dist/demo-chat.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * rendered as plain server HTML with native forms. One small script keeps a
4
4
  * running build's progress live; without it the page still works by reload.
5
5
  */
6
+ import { START_COMMAND } from "./first-run.js";
6
7
  import { headlineOf } from "./task-status.js";
7
8
  const SUGGESTIONS = ["Fix the flaky refund test", "Rewrite the empty Payouts page", "Put the new invoice view behind a flag"];
8
9
  const esc = (text) => text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
@@ -100,7 +101,9 @@ export function demoThreadHtml(exchanges, csrf, resultOf) {
100
101
  if (exchange.state === "ready" || exchange.state === "complete" || exchange.state === "sent-back")
101
102
  parts.push(resultCard(exchange, csrf, resultOf(exchange)));
102
103
  if (exchange.state === "complete") {
103
- parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`);
104
+ parts.push(`<div class="demo-said demo-lead"><p class="demo-who">Lead</p><p>Done. That's the whole loop: ask, approve, Ready, Complete. Ask for something else whenever you like.</p></div>`, `<section class="card demo-handoff" data-demo-handoff aria-label="Your own project"><h2>Now try it on your own project</h2>` +
105
+ `<p>In your repository's folder, run:</p><pre class="demo-command"><code>${esc(START_COMMAND)}</code></pre>` +
106
+ `<p class="meta">It opens in your browser, already signed in.</p></section>`);
104
107
  }
105
108
  return `<article class="demo-turn" id="demo-${exchange.id}">${parts.join("")}</article>`;
106
109
  }).join("");
package/dist/dispatch.js CHANGED
@@ -12,6 +12,7 @@ import { approvalOf } from "./scope.js";
12
12
  import { plannerSourceProblemOf } from "./planner-source.js";
13
13
  import { authPauseOf, authWaitOf, providerName, signInReason } from "./provider-auth.js";
14
14
  import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js";
15
+ import { replacedWords } from "./task-status.js";
15
16
  import { BUILT_IN, parseCapabilityKey } from "./store.js";
16
17
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
17
18
  /**
@@ -210,8 +211,12 @@ export function diagnoseTaskDispatch(store, taskId, now) {
210
211
  ? `${status.detail} Its independent review succeeded on attempt ${review.attempt} of ${review.cap}.`
211
212
  : status.detail, { action: "open-result", review });
212
213
  }
213
- if (task.state === "cancelled")
214
- return answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.");
214
+ if (task.state === "cancelled") {
215
+ // Replaced, never "Cancelled": a successor carries the work on.
216
+ const successor = store.replacementOf(taskId);
217
+ return successor === null ? answer("cancelled", "terminal", "Cancelled", "Nothing else will run for this task.")
218
+ : answer("cancelled", "terminal", replacedWords(successor), `${replacedWords(successor)}. Nothing else will run for this task.`);
219
+ }
215
220
  if (task.state === "failed")
216
221
  return answer("failed", "terminal", "Needs a retry", "The last attempt stopped; review its incident, then retry it.", { action: "retry-task" });
217
222
  if (store.hasLiveClaim(ref.id, now))
package/dist/dispose.d.ts CHANGED
@@ -73,6 +73,14 @@ export type Disposition = {
73
73
  kind: "stopped";
74
74
  stopRun: number;
75
75
  requeued: boolean;
76
+ }
77
+ /** The service stopped or restarted while the attempt ran (no one stopped
78
+ * the task): its work is preserved, the run ends as interrupted, and the
79
+ * task is back in the queue — no strike, no backoff — so the next builder
80
+ * pass resumes it from the kept work. */
81
+ | {
82
+ kind: "handed-back";
83
+ requeued: boolean;
76
84
  } | {
77
85
  kind: "invariant";
78
86
  reason: string;
package/dist/dispose.js CHANGED
@@ -14,7 +14,7 @@ import { originalTaskBase, focusedTestCommandSupported } from "./observations.js
14
14
  * inlined before.
15
15
  */
16
16
  import { createHash } from "node:crypto";
17
- import { completeFenced, finalizeFailureFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
17
+ import { completeFenced, finalizeFailureFenced, finalizeHandBackFenced, finalizeMalformedFenced, finalizeParkFenced, interruptIfStopped, release, } from "./claim.js";
18
18
  import { bodyHashOf, publicationBody } from "./publish.js";
19
19
  import { modeTermsFromJson } from "./modes.js";
20
20
  import { readVerifiedArtifact, writeEvidenceFile } from "./evidence.js";
@@ -338,6 +338,12 @@ function disposeBuildOutcomeLocked(context, result) {
338
338
  });
339
339
  return { kind: "malformed", sealed: sealed.ok };
340
340
  }
341
+ // A stop no operator asked for is the SERVICE stopping (the fence above
342
+ // already settled every task stop): hand the attempt back, never strike it.
343
+ if (result.reason === "stopped" && policy === "tick" && leaseId !== undefined && store.getRun(runId)?.role === "builder") {
344
+ const handed = finalizeHandBackFenced(store, { leaseId, runId, taskId, message: result.message, now: clock() });
345
+ return { kind: "handed-back", requeued: handed.requeued };
346
+ }
341
347
  if (TICK_FAILURE_REASONS.has(result.reason)) {
342
348
  // One fenced transaction decides what the failure means. The
343
349
  // classification trusts only what the machine itself observed.
package/dist/export.js CHANGED
@@ -33,7 +33,7 @@ const SECRET_TABLES = new Set(["web_session", "ceremony_nonce", "telegram_pairin
33
33
  const DERIVED_TABLE = /^(sqlite_|memory_search)/;
34
34
  /** Columns that hold a secret (or what stands in for one): never exported. */
35
35
  const SECRET_COLUMNS = new Set([
36
- "api_token.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
36
+ "api_token.secret_hash", "lead_credential.secret_hash", "approver.credential_hash", "coordinator_credential.credential_hash", "runner.credential_hash", "invite.token_hash",
37
37
  "flow_trigger.hook_hash", "held_session.cookie", "push_subscription.endpoint", "push_subscription.p256dh", "push_subscription.auth",
38
38
  "chat_turn.credential_key", "mate_session.credential_key", "mate_turn.credential_key", "bridge_lease.push_url", "workflow_preview.token",
39
39
  "quota.credential_fp",
@@ -56,7 +56,7 @@ export function categoryOf(table) {
56
56
  [/^teammate/, "teammates"],
57
57
  [/^(flow|routine|workflow_)/, "flows"],
58
58
  [/^(chat_|mate_|team_|telegram_|slack_|discord_|teams_|notification|push_|bridge_lease)/, "chats"],
59
- [/^(approver|api_token|invite|sso_identity|coordinator_)/, "people"],
59
+ [/^(approver|api_token|lead_credential|invite|sso_identity|coordinator_)/, "people"],
60
60
  [/^(run|artifact|claim|worktree$|execution_slot|contest|tournament_terms|fallback_cycle|fallback_transition|held_session|session_turn|attended_authorization|criterion_review|diff_comment|proof_|review_request|repair_chain|incident|publication$|merge_|side_spend|knowledge_snapshot|learning_snapshot|skill_snapshot)/, "runs"],
61
61
  [/^(task|hold$|plan_|scope_|tool_seal|decision|external_|skill_test)/, "tasks"],
62
62
  [/^(project|approval_policy|capability|verify_command|worktree_setup|backend_grant|intake_grant|publication_grant|operating_mode|knowledge_|learning_|memory_|skill_)/, "projects"],
@@ -67,6 +67,7 @@ export function categoryOf(table) {
67
67
  /** Extra shapes beside the evidence scanner's: this installation's own tokens and password hashes, and bearer headers. */
68
68
  const EXTRA_SHAPES = [
69
69
  /\bso_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
70
+ /\blt_[a-f0-9]{12}_[A-Za-z0-9_-]{43}/g,
70
71
  /\bscrypt\$[^\s"',;]+/g,
71
72
  /\bBearer\s+[A-Za-z0-9._~+/-]{16,}=*/gi,
72
73
  /\bsk-[A-Za-z0-9_-]{20,}/g,
@@ -1,6 +1,20 @@
1
1
  import type { Runner } from "./backend.js";
2
2
  export declare const SIGN_IN_COMMAND = "claude auth login";
3
3
  export declare const SANDBOX_COMMAND = "npx toolroll demo";
4
+ /** How a person starts on a real project (the demo hands off to this). */
5
+ export declare const START_COMMAND = "npx toolroll up";
6
+ /** How it works, once, above the composer until the first request. */
7
+ export declare const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
8
+ type SignInState = "connected" | "signed-out" | "not-installed" | "unverified" | "key-present" | "key-works" | "key-refused" | "missing-key";
9
+ /**
10
+ * The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
11
+ * first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
12
+ * (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
13
+ */
14
+ export declare function signInCommandFor(states: {
15
+ claude: SignInState;
16
+ codex: SignInState;
17
+ } | null, platform: NodeJS.Platform): string;
4
18
  export type FirstRunAction = {
5
19
  kind: "link";
6
20
  label: string;
@@ -23,6 +37,7 @@ export type FirstRunFacts = {
23
37
  projects: number;
24
38
  hasTask: boolean;
25
39
  firstResultAt: string | null;
40
+ signInCommand?: string;
26
41
  };
27
42
  /** The three steps, each done or with the one action that does it; null once the first result has arrived. */
28
43
  export declare function firstRunSteps(facts: FirstRunFacts): FirstRunStep[] | null;
@@ -58,3 +73,22 @@ export declare const TODO_GREP_ARGS: readonly string[];
58
73
  export declare function findFirstTasks(repo: string, runner?: Runner): Promise<FirstTaskSuggestion[]>;
59
74
  /** "First result in 7 min": how long after the installation began its first Ready result arrived. */
60
75
  export declare function firstResultWords(since: string, at: string): string | null;
76
+ /** The first task's five steps, as a person reads them. */
77
+ export declare const JOURNEY_STEPS: readonly [readonly ["plan", "Plan"], readonly ["approve", "You approve"], readonly ["build", "Build"], readonly ["checks", "Checks"], readonly ["ready", "Ready"]];
78
+ export type JourneyKey = (typeof JOURNEY_STEPS)[number][0];
79
+ /** `stuck`: the step it stopped at (Failed, Stopped, or waiting on something other than the approval). */
80
+ export type JourneyStep = {
81
+ key: JourneyKey;
82
+ label: string;
83
+ state: "done" | "current" | "stuck" | "todo";
84
+ };
85
+ /**
86
+ * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
87
+ * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
88
+ * the build finished with a result.
89
+ */
90
+ export declare function firstTaskJourney(read: {
91
+ stage: "queued" | "planning" | "needs-you" | "building" | "checking" | "finished" | "complete" | "failed" | "stopped";
92
+ need?: string | undefined;
93
+ }, approved: boolean, built?: boolean): JourneyStep[];
94
+ export {};
package/dist/first-run.js CHANGED
@@ -9,6 +9,23 @@ import { run } from "./exec.js";
9
9
  import { githubIssues } from "./issues.js";
10
10
  export const SIGN_IN_COMMAND = "claude auth login";
11
11
  export const SANDBOX_COMMAND = "npx toolroll demo";
12
+ /** How a person starts on a real project (the demo hands off to this). */
13
+ export const START_COMMAND = "npx toolroll up";
14
+ /** How it works, once, above the composer until the first request. */
15
+ export const HOW_IT_WORKS = "Ask for a change. The lead writes a short plan; you approve it; an agent builds it on its own branch; it's Ready when your tests pass.";
16
+ /**
17
+ * The one command that gets an agent signed in on this computer: sign in to the CLI that's installed (Claude Code
18
+ * first, then Codex), or install Claude Code and sign in, written for this operating system's shell. Unknown states
19
+ * (not checked yet) get the install-and-sign-in command, which is right on a fresh machine and harmless otherwise.
20
+ */
21
+ export function signInCommandFor(states, platform) {
22
+ const installed = (state) => state !== undefined && state !== "not-installed";
23
+ if (installed(states?.claude))
24
+ return SIGN_IN_COMMAND;
25
+ if (installed(states?.codex))
26
+ return "codex login";
27
+ return `npm install -g @anthropic-ai/claude-code${platform === "win32" ? ";" : " &&"} ${SIGN_IN_COMMAND}`;
28
+ }
12
29
  /** The three steps, each done or with the one action that does it; null once the first result has arrived. */
13
30
  export function firstRunSteps(facts) {
14
31
  if (facts.firstResultAt !== null)
@@ -16,7 +33,7 @@ export function firstRunSteps(facts) {
16
33
  const project = facts.projects > 0;
17
34
  return [
18
35
  facts.agentSignedIn === null ? { key: "agent", title: "Agent signed in", done: false, action: null, checking: true }
19
- : { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: SIGN_IN_COMMAND } },
36
+ : { key: "agent", title: "Agent signed in", done: facts.agentSignedIn, action: facts.agentSignedIn ? null : { kind: "command", command: facts.signInCommand ?? SIGN_IN_COMMAND } },
20
37
  { key: "project", title: "Project added", done: project, action: project ? null : { kind: "link", label: "Add a project", href: "/projects" } },
21
38
  { key: "task", title: "Your first task", done: facts.hasTask, action: facts.hasTask ? null : { kind: "link", label: "New task", href: "/tasks/new" } },
22
39
  ];
@@ -103,3 +120,22 @@ export function firstResultWords(since, at) {
103
120
  }
104
121
  return `First result in ${Math.round(minutes / (24 * 60))} days`;
105
122
  }
123
+ /** The first task's five steps, as a person reads them. */
124
+ export const JOURNEY_STEPS = [["plan", "Plan"], ["approve", "You approve"], ["build", "Build"], ["checks", "Checks"], ["ready", "Ready"]];
125
+ /**
126
+ * Where the first task stands on Plan → You approve → Build → Checks → Ready, from the one stage every surface reads
127
+ * (task-status.ts). Waiting for a worker before approval is still the plan; after approval it is the build. `built`:
128
+ * the build finished with a result.
129
+ */
130
+ export function firstTaskJourney(read, approved, built = false) {
131
+ const at = read.stage === "finished" || read.stage === "complete" ? { index: 5, stuck: false }
132
+ : read.stage === "checking" ? { index: 3, stuck: false }
133
+ : read.stage === "building" ? { index: 2, stuck: false }
134
+ : read.stage === "needs-you" && read.need === "approval" ? { index: 1, stuck: false }
135
+ : read.stage === "planning" ? { index: 0, stuck: false }
136
+ : read.stage === "queued" ? { index: approved ? 2 : 0, stuck: false }
137
+ // A build that finished but isn't Ready (its checks failed or didn't run, or it needs a decision) stopped at Checks.
138
+ : { index: built ? 3 : approved ? 2 : 0, stuck: true };
139
+ return JOURNEY_STEPS.map(([key, label], index) => ({ key, label,
140
+ state: index < at.index ? "done" : index === at.index ? (at.stuck ? "stuck" : "current") : "todo" }));
141
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * The flow gallery in the console (Flows → New and Settings → Flows): cards grouped by what they're for, each with
3
+ * its promise, a small drawing of its zones, what it needs and "Use this". A template's page asks only what it
4
+ * needs, previews in plain words what it will do and never do, and creates it — the same answers it previewed.
5
+ */
6
+ import { type GalleryAnswers, type GalleryPreview, type GalleryTemplate } from "./flow-gallery.js";
7
+ import type { FlowDefinition } from "./flows.js";
8
+ export declare const GALLERY_CSS: string;
9
+ /** A small drawing of a flow's zones where they sit on its canvas, with its paths; a person's decisions stand out. */
10
+ export declare function zonesDiagram(definition: FlowDefinition, label: string): string;
11
+ /** The gallery: every template, grouped, each with one "Use this". `repo` carries a chosen project to the template's page. */
12
+ export declare function galleryHtml(input: {
13
+ repo: string | null;
14
+ canUse: boolean;
15
+ }): string;
16
+ /** A template's page: its questions, then what it will do and never do, then Create. */
17
+ export declare function galleryUseHtml(input: {
18
+ template: GalleryTemplate;
19
+ projects: readonly {
20
+ path: string;
21
+ name: string;
22
+ }[];
23
+ repo: string;
24
+ answers: GalleryAnswers;
25
+ name: string;
26
+ preview: GalleryPreview | null;
27
+ problem: string | null;
28
+ csrf: string;
29
+ diagram: FlowDefinition | null;
30
+ }): string;