toolroll 0.9.61 → 0.9.63

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 (117) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/README.md +51 -3
  3. package/dist/agent-fence.d.ts +155 -10
  4. package/dist/agent-fence.js +473 -38
  5. package/dist/browser/workspace.css +1 -1
  6. package/dist/browser/workspace.js +21 -21
  7. package/dist/browser-workspace.d.ts +28 -0
  8. package/dist/chat-actions.js +2 -2
  9. package/dist/check-levels.js +3 -0
  10. package/dist/claim.js +1 -1
  11. package/dist/cli-help.d.ts +1 -1
  12. package/dist/cli-help.js +1 -0
  13. package/dist/cli.js +31 -11
  14. package/dist/contracts/cli.d.ts +8 -0
  15. package/dist/contracts/cli.js +3 -1
  16. package/dist/contracts/console-api.d.ts +19 -1
  17. package/dist/contracts/console-api.js +2 -1
  18. package/dist/contracts/integration-metadata.d.ts +2 -1
  19. package/dist/contracts/integration-metadata.js +2 -1
  20. package/dist/desktop-update.d.ts +1 -1
  21. package/dist/dispatch.js +7 -4
  22. package/dist/doctor.d.ts +53 -0
  23. package/dist/doctor.js +427 -0
  24. package/dist/evidence-pack.d.ts +1 -1
  25. package/dist/evidence-pack.js +2 -2
  26. package/dist/exec.d.ts +3 -0
  27. package/dist/exec.js +5 -4
  28. package/dist/flow-code.d.ts +2 -2
  29. package/dist/flow-code.js +4 -15
  30. package/dist/flow-engine.js +11 -6
  31. package/dist/flow-triggers.d.ts +8 -4
  32. package/dist/flow-triggers.js +45 -25
  33. package/dist/flow-unattended-words.d.ts +28 -0
  34. package/dist/flow-unattended-words.js +50 -0
  35. package/dist/flow-unattended.d.ts +227 -0
  36. package/dist/flow-unattended.js +776 -0
  37. package/dist/flows-cli.d.ts +28 -1
  38. package/dist/flows-cli.js +55 -10
  39. package/dist/flows-ui.d.ts +2 -1
  40. package/dist/flows-ui.js +4 -1
  41. package/dist/google-mail.d.ts +1 -0
  42. package/dist/google-mail.js +61 -18
  43. package/dist/integrations-ui.d.ts +2 -1
  44. package/dist/integrations-ui.js +6 -3
  45. package/dist/integrations.d.ts +17 -3
  46. package/dist/integrations.js +38 -18
  47. package/dist/invoke.js +300 -168
  48. package/dist/landed-cleanup.d.ts +26 -12
  49. package/dist/landed-cleanup.js +199 -85
  50. package/dist/lead-contract.d.ts +1 -1
  51. package/dist/lead-contract.js +3 -3
  52. package/dist/lead-tools.js +11 -3
  53. package/dist/mcp-connect.d.ts +8 -7
  54. package/dist/mcp-connect.js +49 -51
  55. package/dist/oauth-run-token.d.ts +51 -0
  56. package/dist/oauth-run-token.js +127 -0
  57. package/dist/operate.js +74 -17
  58. package/dist/plan-auto.d.ts +11 -0
  59. package/dist/plan-auto.js +62 -11
  60. package/dist/process-recovery-eligibility.d.ts +1 -1
  61. package/dist/project-access.d.ts +3 -0
  62. package/dist/project-access.js +3 -0
  63. package/dist/project-admission.d.ts +10 -6
  64. package/dist/project-admission.js +33 -11
  65. package/dist/project-delete-ui.d.ts +1 -0
  66. package/dist/project-delete-ui.js +4 -1
  67. package/dist/project-delete.d.ts +3 -1
  68. package/dist/project-delete.js +10 -4
  69. package/dist/project-file.d.ts +3 -0
  70. package/dist/project-file.js +16 -0
  71. package/dist/project-tools.d.ts +99 -25
  72. package/dist/project-tools.js +394 -98
  73. package/dist/provider-home.d.ts +22 -0
  74. package/dist/provider-home.js +99 -0
  75. package/dist/provider.d.ts +17 -3
  76. package/dist/provider.js +34 -10
  77. package/dist/remote-main.d.ts +61 -0
  78. package/dist/remote-main.js +179 -0
  79. package/dist/scope.d.ts +1 -1
  80. package/dist/serve.js +11 -3
  81. package/dist/server/flows.js +27 -5
  82. package/dist/server/render-tasks.d.ts +3 -0
  83. package/dist/server/render-tasks.js +3 -0
  84. package/dist/server/route-table.js +3 -0
  85. package/dist/server/runtime.d.ts +1 -1
  86. package/dist/server/settings.js +14 -5
  87. package/dist/server/task-views.js +2 -0
  88. package/dist/storage-sweep-child.js +3 -1
  89. package/dist/storage-sweep-process.d.ts +14 -0
  90. package/dist/storage-sweep-process.js +37 -3
  91. package/dist/storage-sweep-schedule.d.ts +30 -0
  92. package/dist/storage-sweep-schedule.js +164 -0
  93. package/dist/storage-sweep.d.ts +6 -5
  94. package/dist/storage-sweep.js +10 -70
  95. package/dist/storage-ui.d.ts +2 -0
  96. package/dist/storage-ui.js +7 -4
  97. package/dist/store.d.ts +75 -7
  98. package/dist/store.js +241 -17
  99. package/dist/subagent-tools.d.ts +1 -1
  100. package/dist/surface.js +10 -6
  101. package/dist/task-base.d.ts +35 -0
  102. package/dist/task-base.js +51 -0
  103. package/dist/teardown-git.d.ts +14 -0
  104. package/dist/teardown-git.js +119 -0
  105. package/dist/tool-broker.d.ts +37 -0
  106. package/dist/tool-broker.js +369 -0
  107. package/dist/tool-launcher.js +5 -4
  108. package/dist/tool-secret-store.d.ts +170 -0
  109. package/dist/tool-secret-store.js +834 -0
  110. package/dist/toolroll-update.js +4 -2
  111. package/dist/tools-ui.d.ts +7 -1
  112. package/dist/tools-ui.js +17 -8
  113. package/dist/work-index.js +5 -3
  114. package/dist/worktree.d.ts +64 -6
  115. package/dist/worktree.js +171 -35
  116. package/package.json +3 -1
  117. package/scripts/provider-canary.mjs +210 -31
@@ -1115,6 +1115,34 @@ export type BrowserFlowView = {
1115
1115
  /** Where the flow stands (v88): the live stream's nudge names it, and a page that already shows it doesn't read again. */
1116
1116
  live: string | null;
1117
1117
  canEdit: boolean;
1118
+ /** "Runs on its own" (flow-unattended.ts): the live grant, and whether this person can sign one (why not, when not). */
1119
+ unattended: {
1120
+ active: {
1121
+ until: string;
1122
+ untilWords: string;
1123
+ signedBy: string;
1124
+ budgetMicrousd: number | null;
1125
+ dailyRunCap: number | null;
1126
+ runsToday: number;
1127
+ words: string[];
1128
+ covers: string[];
1129
+ } | null;
1130
+ canSign: boolean;
1131
+ problem: string | null;
1132
+ maxDays: number;
1133
+ scheduled: boolean;
1134
+ terms: {
1135
+ head: string[];
1136
+ rules: string[];
1137
+ };
1138
+ /** The schedules a signature could cover: plain ones always, `optional` ones (outside content, said why) only when picked. */
1139
+ schedules: {
1140
+ id: number;
1141
+ words: string;
1142
+ optional: string | null;
1143
+ }[];
1144
+ schedulesDigest: string;
1145
+ };
1118
1146
  approvers: string[];
1119
1147
  /** The projects a Build zone may work in: the flow's own first, then others the viewer can reach. */
1120
1148
  projects?: {
@@ -1331,9 +1331,9 @@ function runFlowAction(store, payload, actor, repos, root, now) {
1331
1331
  if (payload.operation.startsWith("flow_trigger_")) {
1332
1332
  const trigger = store.getFlowTrigger(Number(req["trigger"]));
1333
1333
  if (payload.operation === "flow_trigger_remove")
1334
- removeFlowTrigger(store, trigger, now, configDirOf(store));
1334
+ removeFlowTrigger(store, trigger, now, configDirOf(store), actor);
1335
1335
  else
1336
- store.updateFlowTrigger(trigger.id, { state: payload.operation === "flow_trigger_pause" ? "paused" : "active" }, now);
1336
+ store.updateFlowTrigger(trigger.id, { state: payload.operation === "flow_trigger_pause" ? "paused" : "active" }, now, actor);
1337
1337
  return { said: payload.operation === "flow_trigger_remove" ? "Trigger removed." : payload.operation === "flow_trigger_pause" ? "Trigger paused." : "Trigger on again.", href: `/flows/${trigger.flow}` };
1338
1338
  }
1339
1339
  const card = store.getFlowCard(Number(req["card"]));
@@ -153,6 +153,9 @@ export function suggestQuickCommand(repo) {
153
153
  const has = (name) => typeof scripts[name] === "string";
154
154
  const runner = existsSync(join(repo, "pnpm-lock.yaml")) ? "pnpm" : existsSync(join(repo, "yarn.lock")) ? "yarn" : "npm";
155
155
  const run = (name) => runner === "npm" ? `npm run ${name}` : `${runner} ${name}`;
156
+ // A project's own quick script knows its tests best (Toolroll's runs the attack suite when a guard changes).
157
+ if (has("check:quick"))
158
+ return run("check:quick");
156
159
  const exec = runner === "npm" ? "npx" : `${runner} exec`;
157
160
  const typecheck = ["typecheck", "type-check", "check-types", "tsc"].find(has) ?? null;
158
161
  const changed = "$(git diff --name-only HEAD~1)";
package/dist/claim.js CHANGED
@@ -1116,7 +1116,7 @@ function sealPlan(store, leaseId, args) {
1116
1116
  ? ""
1117
1117
  : "The filed goal, exclusions, touches, and acceptance criteria are reproduced exactly.\n"
1118
1118
  : `It AMENDS the filed contract (${oneLine(describeContractChanges(contractChanges).join("; "), 300)})${amendment === null ? "" : ` — because: ${oneLine(amendment, 200)}`}\n`) +
1119
- (autoApproved ? "Approved under the signed operating mode. The next build may proceed with the original terms and required quality checks." : "Review, edit, and approve the scope — nothing builds until you do."),
1119
+ (autoApproved ? `${store.getScope(taskId)?.approvalBasis === "grant" ? "Approved because its flow runs on its own." : "Approved under the signed operating mode."} The next build may proceed with the original terms and required quality checks.` : "Review, edit, and approve the scope — nothing builds until you do."),
1120
1120
  }, now);
1121
1121
  return { ok: true, changes: contractChanges.length, amendment };
1122
1122
  }
@@ -2,4 +2,4 @@
2
2
  * The top-level usage text, on its own so `toolroll --help` can print it
3
3
  * without loading every command family (see cli-fast.ts).
4
4
  */
5
- 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> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\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 release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\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 health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's 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> / worker 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 your lead: 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.";
5
+ 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> add a project; it is in the console at once (no path: the repo you are in)\n toolroll repos remove <path>\n take it out of the console; its tasks and results stay\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 release <branch> release Toolroll itself: gate, your approval, deploy, merge, tag, publish\n (resumable: rerun to continue; --help)\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 health recent server latency, write waits and live streams\n toolroll integrations which integrations work, and what to do about the ones that don't (--json)\n toolroll lead token mint your lead agent's credential: its own work pings nobody\n toolroll lead say \"<text>\" (the lead) one short message in its person's 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> / worker 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 your lead: 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 doctor check that Toolroll's service holds no key a build could read\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.";
package/dist/cli-help.js CHANGED
@@ -63,6 +63,7 @@ and any queue command + --help prints it too
63
63
  (--editor vscode with --runner: file links
64
64
  open in VS Code on the device you browse from)
65
65
  toolroll watch / daemon install advanced: run only the builder
66
+ toolroll doctor check that Toolroll's service holds no key a build could read
66
67
  toolroll reconcile recover what the last stretch left behind
67
68
 
68
69
  With nothing connected it reports everything it can find below the working
package/dist/cli.js CHANGED
@@ -313,6 +313,9 @@ async function dispatch(argv, write, mainOptions) {
313
313
  // Command families below load when their command runs, so one command never pays for the others.
314
314
  if (argv[0] === 'health')
315
315
  return (await import("./health-cli.js")).runHealthCommand(argv.slice(1), write, mainOptions.team ?? mainOptions.remote);
316
+ // This computer's own service, read here and never on a central server.
317
+ if (argv[0] === 'doctor')
318
+ return (await import("./doctor.js")).runDoctorCommand(argv.slice(1), write);
316
319
  const central = await maybeRunRemoteCommand(argv, mainOptions.remote ?? mainOptions.team);
317
320
  if (typeof central === "number")
318
321
  return central;
@@ -506,7 +509,14 @@ async function runReposCommand(argv, write, onboard) {
506
509
  const tokenAt = argv.indexOf("--token");
507
510
  const tokenFlag = tokenAt === -1 ? undefined : argv[tokenAt + 1];
508
511
  const bare = argv.filter((argument, index) => argument !== "--json" && (tokenAt === -1 || (index !== tokenAt && index !== tokenAt + 1)));
509
- const [action, ...paths] = bare;
512
+ const remoteAt = bare.indexOf("--remote");
513
+ const remoteUrl = remoteAt === -1 ? undefined : bare[remoteAt + 1];
514
+ const [action, ...paths] = bare.filter((_, i) => remoteAt === -1 || (i !== remoteAt && i !== remoteAt + 1));
515
+ if (remoteAt !== -1 && (action !== "add" || bare.filter(v => v === "--remote").length !== 1 || remoteUrl === undefined || remoteUrl.startsWith("-") || paths.length > 1)) {
516
+ const message = "Use repos add <project> --remote <url> for one project.";
517
+ write(json ? envelopeJson({ ok: false, command: "repos", reason: "usage", message }) : message);
518
+ return USAGE_EXIT;
519
+ }
510
520
  const databaseFile = databasePath(process.env, homedir());
511
521
  const file = registryBeside(databaseFile);
512
522
  if (tokenAt !== -1 && (tokenFlag === undefined || tokenFlag.startsWith("-"))) {
@@ -529,11 +539,18 @@ async function runReposCommand(argv, write, onboard) {
529
539
  return 1;
530
540
  }
531
541
  if (action === undefined) {
532
- if (json) {
533
- write(envelopeJson({ ok: true, command: "repos", count: loaded.repos.length, repos: loaded.repos, file }));
534
- return 0;
542
+ const store = openStore(databaseFile);
543
+ try {
544
+ const projects = loaded.repos.map(repo => ({ repo, remoteUrl: store.projectRemote(repo) }));
545
+ if (json) {
546
+ write(envelopeJson({ ok: true, command: "repos", count: loaded.repos.length, repos: loaded.repos, projects, file }));
547
+ return 0;
548
+ }
549
+ return listRepos(loaded.repos, file, write, new Map(projects.map(p => [p.repo, p.remoteUrl])));
550
+ }
551
+ finally {
552
+ store.close();
535
553
  }
536
- return listRepos(loaded.repos, file, write);
537
554
  }
538
555
  if (action !== "add" && action !== "remove") {
539
556
  const message = `unknown command \`repos ${action}\` — try \`repos\`, \`repos add\`, \`repos remove\`, or \`repos add-from-github <owner/name>\``;
@@ -545,7 +562,7 @@ async function runReposCommand(argv, write, onboard) {
545
562
  const lines = [];
546
563
  const said = {};
547
564
  const say = json ? (line) => lines.push(line) : write;
548
- const code = await withAdmission({ databaseFile, registryFile: file, ...(leadToken === undefined ? {} : { leadToken }) }, say, said, admission => action === "add" ? addToRepos(admission, targets, say, said) : removeFromRepos(admission, targets, say, said));
565
+ const code = await withAdmission({ databaseFile, registryFile: file, ...(leadToken === undefined ? {} : { leadToken }) }, say, said, admission => action === "add" ? addToRepos(admission, targets, say, said, remoteUrl) : removeFromRepos(admission, targets, say, said));
549
566
  if (json) {
550
567
  write(envelopeJson({
551
568
  ok: code === 0,
@@ -582,7 +599,7 @@ async function withAdmission(where, write, said, body) {
582
599
  store.close();
583
600
  }
584
601
  }
585
- function listRepos(repos, file, write) {
602
+ function listRepos(repos, file, write, remotes) {
586
603
  if (repos.length === 0) {
587
604
  write("No repositories connected. Toolroll reports on everything it can find.");
588
605
  write("");
@@ -592,8 +609,10 @@ function listRepos(repos, file, write) {
592
609
  return 0;
593
610
  }
594
611
  write(`${repos.length === 1 ? "1 repository" : `${repos.length} repositories`} connected:`);
595
- for (const repo of repos)
612
+ for (const repo of repos) {
596
613
  write(` ${repo}${existsSync(repo) ? "" : " (missing)"}`);
614
+ write(` Recorded remote: ${remotes.get(repo) ?? "None"}`);
615
+ }
597
616
  write("");
598
617
  write(" toolroll repos add <path> add another; it is in the console at once");
599
618
  write(" toolroll repos remove <path> take one out of the console; its tasks stay");
@@ -606,7 +625,7 @@ function listRepos(repos, file, write) {
606
625
  function admissionExit(reason) {
607
626
  return reason === "not-git" || reason === "unavailable" ? USAGE_EXIT : 1;
608
627
  }
609
- async function addToRepos(admission, targets, write, said) {
628
+ async function addToRepos(admission, targets, write, said, remoteUrl) {
610
629
  // Every folder is proved before anything is written: one that is not a
611
630
  // repository fails here, not later and further away.
612
631
  let refused = false;
@@ -624,7 +643,7 @@ async function addToRepos(admission, targets, write, said) {
624
643
  const running = consoleRunning(admission.store, now);
625
644
  const projects = [];
626
645
  for (const path of targets) {
627
- const admitted = await admitProject(admission.store, { registryFile: admission.registryFile, path, actor: admission.actor, now });
646
+ const admitted = await admitProject(admission.store, { registryFile: admission.registryFile, path, actor: admission.actor, now, captureOrigin: true, ...(remoteUrl === undefined ? {} : { remoteUrl }) });
628
647
  if (!admitted.ok) {
629
648
  // The updater's own taxonomy survives (verification finding 4): a held
630
649
  // lock or a malformed registry is an operational fact, never "usage".
@@ -633,8 +652,9 @@ async function addToRepos(admission, targets, write, said) {
633
652
  write(admitted.message);
634
653
  return admissionExit(admitted.reason);
635
654
  }
636
- projects.push({ repo: admitted.repo, name: admitted.name, added: admitted.added });
655
+ projects.push({ repo: admitted.repo, name: admitted.name, added: admitted.added, remoteUrl: admitted.remote.url });
637
656
  write(addedWords(admitted.name, admitted.added, running));
657
+ write(`Recorded remote: ${admitted.remote.url ?? "None. Use repos add <project> --remote <url> to record one."}`);
638
658
  }
639
659
  said.projects = projects;
640
660
  return 0;
@@ -647,6 +647,14 @@ export declare const COMMAND_OUTPUTS: {
647
647
  readable: z.ZodBoolean;
648
648
  zones: z.ZodArray<z.ZodUnknown>;
649
649
  triggers: z.ZodArray<z.ZodUnknown>;
650
+ unattended: z.ZodNullable<z.ZodObject<{
651
+ until: z.ZodString;
652
+ signedBy: z.ZodString;
653
+ budgetMicrousd: z.ZodNullable<z.ZodNumber>;
654
+ dailyRunCap: z.ZodNullable<z.ZodNumber>;
655
+ runsToday: z.ZodNumber;
656
+ covers: z.ZodArray<z.ZodString>;
657
+ }, z.core.$loose>>;
650
658
  }, z.core.$loose>;
651
659
  }, z.core.$loose>, z.ZodObject<{
652
660
  envelopeVersion: z.ZodLiteral<1>;
@@ -184,7 +184,9 @@ export const COMMAND_OUTPUTS = {
184
184
  "task unhold": answers("task unhold", { id: str }),
185
185
  "task wait": answers("task wait", { task: str, outcome: str }),
186
186
  "flows list": answers("flows list", { flows: z.array(flowListed) }),
187
- "flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list }) }),
187
+ "flows show": answers("flows show", { flow: z.looseObject({ id: int, name: str, repo: str, project: str, readable: z.boolean(), zones: list, triggers: list,
188
+ // Whether the flow runs on its own, and until when (null: it doesn't).
189
+ unattended: nullable(z.looseObject({ until: str, signedBy: str, budgetMicrousd: nullable(int), dailyRunCap: nullable(int), runsToday: int, covers: z.array(str) })) }) }),
188
190
  grants: answers("grants", { count: int, grants: list }),
189
191
  "runner list": answers("runner list", { count: int, runners: list }),
190
192
  "incident list": answers("incident list", { incidents: list }),
@@ -73,7 +73,7 @@ export declare const CONSOLE_FORMS: {
73
73
  readonly kit: FormContract<"repo">;
74
74
  readonly subagents: FormContract<"choice" | "dailyTurns" | "id" | "manager" | "model" | "name" | "note" | "op" | "repo" | "schedule" | "soul" | "state" | "template" | "text" | "tool" | `field.${string}` | `over.${string}` | `undo.${string}` | `use.${string}`>;
75
75
  readonly galleryUse: FormContract<string>;
76
- readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "choice" | "confirm" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "remove" | "repo" | "revision" | "secret" | "stage" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
76
+ readonly flows: FormContract<"about" | "address" | "answers" | "approve" | "body" | "budget" | "choice" | "confirm" | "days" | "decision" | "definition" | "description" | "document" | "draft" | "entry" | "file" | "include" | "key" | "label" | "language" | "name" | "note" | "owner" | "password" | "previewed" | "remove" | "repo" | "revision" | "runs" | "schedules" | "secret" | "stage" | "template" | "timeoutMinutes" | "title" | "trigger" | "url" | "value" | "watching" | `param.${string}`>;
77
77
  readonly spendBudget: FormContract<"action" | "password" | "stop" | "target" | "usd">;
78
78
  readonly projectConcurrency: FormContract<"concurrency" | "repo">;
79
79
  readonly projectDelete: FormContract<"name" | "password" | "repo" | "step">;
@@ -318,6 +318,15 @@ export declare const flowViewSchema: z.ZodObject<{
318
318
  selectedCard: z.ZodNullable<z.ZodNumber>;
319
319
  live: z.ZodNullable<z.ZodString>;
320
320
  canEdit: z.ZodBoolean;
321
+ unattended: z.ZodObject<{
322
+ active: z.ZodNullable<z.ZodObject<{
323
+ until: z.ZodString;
324
+ signedBy: z.ZodString;
325
+ }, z.core.$loose>>;
326
+ canSign: z.ZodBoolean;
327
+ problem: z.ZodNullable<z.ZodString>;
328
+ scheduled: z.ZodBoolean;
329
+ }, z.core.$loose>;
321
330
  approvers: z.ZodArray<z.ZodString>;
322
331
  kinds: z.ZodArray<z.ZodObject<{
323
332
  kind: z.ZodString;
@@ -467,6 +476,15 @@ export declare const CONSOLE_RESPONSES: {
467
476
  selectedCard: z.ZodNullable<z.ZodNumber>;
468
477
  live: z.ZodNullable<z.ZodString>;
469
478
  canEdit: z.ZodBoolean;
479
+ unattended: z.ZodObject<{
480
+ active: z.ZodNullable<z.ZodObject<{
481
+ until: z.ZodString;
482
+ signedBy: z.ZodString;
483
+ }, z.core.$loose>>;
484
+ canSign: z.ZodBoolean;
485
+ problem: z.ZodNullable<z.ZodString>;
486
+ scheduled: z.ZodBoolean;
487
+ }, z.core.$loose>;
470
488
  approvers: z.ZodArray<z.ZodString>;
471
489
  kinds: z.ZodArray<z.ZodObject<{
472
490
  kind: z.ZodString;
@@ -82,7 +82,7 @@ export const CONSOLE_FORMS = {
82
82
  kit: formContract("POST /kits/<kit>/<act>", ["repo"]),
83
83
  subagents: formContract("POST /settings/lead/subagents/new, /settings/lead/subagents/<id>/<part>, /settings/lead/subagents/questions/<id>/answer", ["choice", "text", "repo", "template", "name", "soul", "op", "id", "schedule", "tool", "state", "note", "model", "dailyTurns", "manager"], { prefixes: ["use.", "undo.", "field.", "over."] }),
84
84
  galleryUse: formContract("POST /flows/new/<template>", ["repo", "send-result", "name", "intent", "previewed"], { dynamic: true }),
85
- flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching"], { prefixes: ["param."] }),
85
+ flows: formContract("POST /flows/new, /flows/example, /flows/import, /flows/<id>/<act>, /flows/<id>/cards/<card>/<act>, /flows/<id>/triggers/<trigger>/<act>", ["repo", "document", "url", "confirm", "previewed", "template", "name", "answers", "password", "secret", "trigger", "approve", "remove", "about", "body", "timeoutMinutes", "language", "file", "address", "key", "definition", "owner", "revision", "value", "title", "description", "stage", "decision", "note", "draft", "choice", "entry", "label", "watching", "days", "budget", "runs", "include", "schedules"], { prefixes: ["param."] }),
86
86
  spendBudget: formContract("POST /spend/budget", ["password", "target", "action", "usd", "stop"]),
87
87
  projectConcurrency: formContract("POST /settings/project/concurrency", ["repo", "concurrency"]),
88
88
  projectDelete: formContract("POST /settings/project/delete", ["repo", "name", "step", "password"]),
@@ -255,6 +255,7 @@ export const flowViewSchema = z.looseObject({
255
255
  selectedCard: z.number().nullable(),
256
256
  live: z.string().nullable(),
257
257
  canEdit: z.boolean(),
258
+ unattended: z.looseObject({ active: z.looseObject({ until: z.string(), signedBy: z.string() }).nullable(), canSign: z.boolean(), problem: z.string().nullable(), scheduled: z.boolean() }),
258
259
  approvers: z.array(z.string()),
259
260
  kinds: z.array(z.looseObject({ kind: z.string(), label: z.string() })),
260
261
  colors: z.array(z.string()),
@@ -22,11 +22,12 @@ export declare const protectedResourceSchema: z.ZodObject<{
22
22
  authorization_servers: z.ZodOptional<z.ZodArray<z.ZodString>>;
23
23
  scopes_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
24
24
  }, z.core.$strip>;
25
- /** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, and register. */
25
+ /** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, register, and revoke. */
26
26
  export declare const authorizationServerSchema: z.ZodObject<{
27
27
  authorization_endpoint: z.ZodOptional<z.ZodString>;
28
28
  token_endpoint: z.ZodOptional<z.ZodString>;
29
29
  registration_endpoint: z.ZodOptional<z.ZodString>;
30
+ revocation_endpoint: z.ZodOptional<z.ZodString>;
30
31
  code_challenge_methods_supported: z.ZodOptional<z.ZodArray<z.ZodString>>;
31
32
  }, z.core.$strip>;
32
33
  /** RFC 7591 registration answer: the client Toolroll is, and its secret when the server gives one. */
@@ -44,11 +44,12 @@ export const protectedResourceSchema = z.object({
44
44
  authorization_servers: z.array(z.string()).optional(),
45
45
  scopes_supported: z.array(z.string().regex(SCOPE_TOKEN)).optional(),
46
46
  });
47
- /** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, and register. */
47
+ /** RFC 8414 (or OpenID) authorization-server metadata: where to sign in, trade a code, register, and revoke. */
48
48
  export const authorizationServerSchema = z.object({
49
49
  authorization_endpoint: z.string().optional(),
50
50
  token_endpoint: z.string().optional(),
51
51
  registration_endpoint: z.string().optional(),
52
+ revocation_endpoint: z.string().optional(),
52
53
  code_challenge_methods_supported: z.array(z.string()).optional(),
53
54
  });
54
55
  /** RFC 7591 registration answer: the client Toolroll is, and its secret when the server gives one. */
@@ -94,6 +94,7 @@ export declare function prepareDesktopUpdate(stateDir: string, installed: string
94
94
  export declare function sqliteLock(file: string, busyTimeoutMs?: number): DatabaseSync | null;
95
95
  export declare const installationLock: (j: UpdateJournal) => DatabaseSync | null;
96
96
  export declare function desktopUpdateStatus(stateDir: string): {
97
+ version?: never;
97
98
  active: boolean;
98
99
  phase: string;
99
100
  detail: string;
@@ -107,7 +108,6 @@ export declare function desktopUpdateStatus(stateDir: string): {
107
108
  wasRunning?: never;
108
109
  backupPath?: never;
109
110
  updateId?: never;
110
- version?: never;
111
111
  buildId?: never;
112
112
  workDir?: never;
113
113
  } | {
package/dist/dispatch.js CHANGED
@@ -15,6 +15,8 @@ import { REVIEW_TOKENS, resultStatusOf, reviewFactsOf } from "./workspace-ui.js"
15
15
  import { replacedWords } from "./task-status.js";
16
16
  import { failedAttemptSentence, latestFinishedAttempt } from "./needs-you.js";
17
17
  import { buildReviewOf } from "./review-switch.js";
18
+ import { grantStandsSql } from "./flow-unattended.js";
19
+ import { accountReachesSql } from "./project-access.js";
18
20
  import { BUILT_IN, parseCapabilityKey } from "./store.js";
19
21
  export const DEFAULT_MAX_OPEN_DECISIONS = 5;
20
22
  /**
@@ -75,13 +77,14 @@ export function scopeApprovedForDispatch(store, taskRef, now) {
75
77
  .prepare(`SELECT 1 AS hit FROM task_scope
76
78
  JOIN task_ref ON task_ref.id = ? AND task_scope.task_id = task_ref.external_id
77
79
  WHERE task_scope.approved_digest = task_scope.digest AND task_scope.approved_at IS NOT NULL
78
- AND (COALESCE(task_scope.approval_basis, 'password') <> 'mode'
79
- OR EXISTS (SELECT 1 FROM operating_mode om
80
+ AND (COALESCE(task_scope.approval_basis, 'password') NOT IN ('mode', 'grant')
81
+ OR (task_scope.approval_basis = 'mode' AND EXISTS (SELECT 1 FROM operating_mode om
80
82
  JOIN approver signer ON signer.name = om.signed_by
81
83
  WHERE om.repo = task_ref.repo AND om.revoked_at IS NULL
82
84
  AND om.absolute_expiry > ? AND om.digest = task_scope.mode_digest
83
- AND signer.revoked_at IS NULL AND signer.role = 'approver'))`)
84
- .get(taskRef, now.toISOString());
85
+ AND signer.revoked_at IS NULL AND signer.role = 'approver' AND ${accountReachesSql("signer", "task_ref.repo")}))
86
+ OR (task_scope.approval_basis = 'grant' AND ${grantStandsSql("task_scope.mode_digest", "?", "task_ref.repo")}))`)
87
+ .get(taskRef, now.toISOString(), now.toISOString());
85
88
  return row !== undefined;
86
89
  }
87
90
  /** The phase the ordinary dispatch loop would choose before it resolves an
@@ -0,0 +1,53 @@
1
+ import { type SupervisorRunner } from "./daemon.js";
2
+ /** Something that holds a credential a build could read, by name only, and what the owner does about it. */
3
+ export type DoctorFinding = {
4
+ where: string;
5
+ names: string[];
6
+ steps: string[];
7
+ };
8
+ /** Something the doctor could not read: never taken for clean. */
9
+ export type DoctorUnreadable = {
10
+ where: string;
11
+ problem: string;
12
+ steps: string[];
13
+ };
14
+ export type DoctorReport = {
15
+ ok: boolean;
16
+ checked: string[];
17
+ findings: DoctorFinding[];
18
+ unreadable: DoctorUnreadable[];
19
+ notes: string[];
20
+ };
21
+ export type DoctorDeps = {
22
+ platform?: NodeJS.Platform;
23
+ home?: string;
24
+ uid?: number;
25
+ /** Runs the read-only supervisor and process queries (launchctl, systemctl, ps). */
26
+ run?: SupervisorRunner;
27
+ /** Where Linux keeps its process files (a test's stand-in). */
28
+ procRoot?: string;
29
+ /** The doctor's own environment: the terminal a `toolroll up` would start from. */
30
+ env?: NodeJS.ProcessEnv;
31
+ };
32
+ /** The names among `pairs` that hold a credential: a provider key, a credential-looking name, or a key-shaped value.
33
+ * Values are only tested here and never leave. */
34
+ export declare function credentialNames(pairs: Iterable<readonly [string, string]>): string[];
35
+ /** A running process's environment names (never values) that hold a credential: macOS through `ps -E`, which reads
36
+ * KERN_PROCARGS2; Linux from /proc/<pid>/environ. */
37
+ export declare function processCredentialNames(pid: number, deps: {
38
+ platform: NodeJS.Platform;
39
+ run: SupervisorRunner;
40
+ procRoot?: string;
41
+ }): Promise<{
42
+ ok: true;
43
+ names: string[];
44
+ } | {
45
+ ok: false;
46
+ problem: string;
47
+ }>;
48
+ /** Audit what Toolroll's service holds. Reads only; the report names variables, never their values. */
49
+ export declare function auditServiceEnvironment(deps?: DoctorDeps): Promise<DoctorReport>;
50
+ /** The report as the owner reads it: one state, then each finding with its steps; detail after. */
51
+ export declare function doctorLines(report: DoctorReport): string[];
52
+ export declare const DOCTOR_HELP = "toolroll doctor [--json]\nChecks that Toolroll's service holds no API key, token or password a build could read: its service definitions, what `daemon install` writes, the environment its manager hands it, and its running process. Reads only: it names what it finds and the steps to fix it, and never changes a file or restarts anything.";
53
+ export declare function runDoctorCommand(argv: readonly string[], write: (line: string) => void, deps?: DoctorDeps): Promise<number>;