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
@@ -35,7 +35,7 @@ import { processMayBeAlive } from "./process-liveness.js";
35
35
  import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner, removeCodingUpdateGate } from "./coding-update.js";
36
36
  import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
37
37
  import { NAME } from "./names.js";
38
- import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS } from "./store.js";
38
+ import { orphanKey, updateSafeSchema, V117_RENAMED_COLUMNS, V117_RENAMED_TABLES, V118_MERGED_TABLES, V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS } from "./store.js";
39
39
  import { isNewer, REGISTRY } from "./releases.js";
40
40
  import { markNeverIndex } from "./never-index.js";
41
41
  import { readRuntimeUpdateJournal, RUNTIME_PHASES, RUNTIME_UPDATE_STEPS, stagedStartedAt, updaterStartingOf } from "./contracts/update-journal.js";
@@ -399,7 +399,9 @@ const HISTORY_RULES = {
399
399
  // v120: a steering note's mailing and acknowledgement, and a run's steering folder, are new columns; every row
400
400
  // saved before them reads none.
401
401
  // v121: live steering's columns and a run's attempt key read none, and no sends, the same way.
402
- ...addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS),
402
+ // v122: a project's recorded remote reads none the same way.
403
+ // v123: a plan authorization's basis reads the operating mode, and a trigger firing reads not on time and no script ran.
404
+ ...addedColumns(V119_ADDED_COLUMNS, V120_ADDED_COLUMNS, V121_ADDED_COLUMNS, V122_ADDED_COLUMNS, V123_ADDED_COLUMNS),
403
405
  };
404
406
  /** The tables a migration may remove whole (v115's removed features). */
405
407
  export const RETIRED_TABLES = Object.freeze(Object.keys(HISTORY_RULES).filter(name => HISTORY_RULES[name].retired));
@@ -2,7 +2,7 @@
2
2
  * entered here and nowhere else, and never shown again. */
3
3
  import { type Html } from "./html.js";
4
4
  import { type FoundTool, type ProjectTool, type ToolSpec } from "./project-tools.js";
5
- export declare const TOOLS_CSS = ".tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready=\"false\"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:\"\";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state=\"connected\"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}";
5
+ export declare const TOOLS_CSS = ".tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready=\"false\"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .card>p.tool-credential{margin-top:4px;font-size:.875rem;color:var(--so-muted)}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:\"\";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state=\"connected\"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}@media(max-width:600px){.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}";
6
6
  export type ToolsView = {
7
7
  repo: string;
8
8
  project: string;
@@ -26,6 +26,12 @@ export type ToolsView = {
26
26
  label: string;
27
27
  })[];
28
28
  found: FoundTool[];
29
+ /** Where this computer keeps tool secrets (project-tools.ts toolSecretStorage): said plainly when it's files only. */
30
+ storage?: {
31
+ words: string;
32
+ filesOnly: boolean;
33
+ problem?: string;
34
+ };
29
35
  /** The person's other projects and the one-click services still open there: where a connection here can also go. */
30
36
  others?: {
31
37
  repo: string;
package/dist/tools-ui.js CHANGED
@@ -2,9 +2,10 @@
2
2
  * entered here and nowhere else, and never shown again. */
3
3
  import { html, postForm } from "./html.js";
4
4
  import { brandMarkHtml, TILE_PHONE } from "./brand-mark.js";
5
- import { toolCommandLine, toolStanding } from "./project-tools.js";
5
+ import { signedIn } from "./oauth-run-token.js";
6
+ import { credentialModeOf, toolCommandLine, toolStanding } from "./project-tools.js";
6
7
  const hidden = (data) => Object.entries(data).map(([k, v]) => html `<input type="hidden" name="${k}" value="${v}">`);
7
- export const TOOLS_CSS = `.tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready="false"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:"";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state="connected"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}${TILE_PHONE}{.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}`;
8
+ export const TOOLS_CSS = `.tools{max-width:780px;min-width:0;overflow-wrap:anywhere}.tools .card{padding:16px 18px;margin:12px 0;min-width:0}.tools .card h2{font-size:1.05rem;margin:0;color:var(--so-ink);font-weight:600}.tools .tool-heading{display:flex;align-items:center;gap:10px}.tools .tool-heading h2{flex:1;min-width:0}.tools .tool-state{font-size:.85rem;flex-shrink:0;color:var(--so-muted)}.tools .tool-state[data-ready="false"]{color:var(--so-warning)}.tools .card>p{margin:6px 0 0}.tools .card>p.tool-credential{margin-top:4px;font-size:.875rem;color:var(--so-muted)}.tools .tool-note{color:var(--so-warning)}.tools code{white-space:pre-wrap;overflow-wrap:anywhere;word-break:break-word;font-size:.85rem}.tools form{display:grid;gap:10px;margin:0}.tools label{display:grid;gap:6px}.tools input,.tools select{box-sizing:border-box;width:100%;max-width:100%;min-width:0}.tools button{justify-self:start;white-space:nowrap;min-height:44px;max-width:100%;overflow:hidden;text-overflow:ellipsis}.tools details:not(.card){margin:6px 0 0;padding:0;border:0;border-radius:0;background:none;box-shadow:none}.tools details:not(.card)>summary{min-height:36px;display:flex;align-items:center;font-size:.9rem;color:var(--so-muted);cursor:pointer}.tools details:not(.card)[open]>summary{color:var(--so-ink)}.tools details.card>summary{cursor:pointer;min-height:44px;display:flex;align-items:center;font-weight:600}.tools summary{list-style:none;gap:10px}.tools summary::-webkit-details-marker{display:none}.tools summary::before{content:"";flex-shrink:0;width:6px;height:6px;margin:0 2px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .15s}.tools details[open]>summary::before{transform:rotate(45deg)}.tools details:not(.card) form{padding:4px 0 10px}.tools .tool-facts{margin:0 0 8px;display:grid;gap:4px;font-size:.9rem}.tools .tool-facts p{margin:0}.tools .tool-controls{display:flex;gap:12px;flex-wrap:wrap;margin-top:12px}.tools .tool-found{display:flex;gap:10px;align-items:flex-start}.tools .tool-found input{flex:0 0 auto;width:18px;height:18px;margin-top:3px}.tools .tool-found span{min-width:0}.tools .problem{padding:10px 12px;margin:8px 0 0}.tools .tool-connect{scroll-margin-top:72px}.tools .connect-wanted{background:var(--so-accent);color:var(--so-on-accent);border-color:var(--so-accent)}.tools .connect-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(210px,1fr));gap:8px}.tools .connect-tile{display:grid;grid-template-columns:auto 1fr;column-gap:10px;align-items:center;justify-self:stretch;text-align:left;white-space:normal;padding:10px 12px;border:1px solid var(--so-line);border-radius:10px;background:var(--so-paper);color:var(--so-ink);min-height:60px;font:inherit;cursor:pointer;scroll-margin-top:96px}.tools .connect-tile:hover{border-color:var(--so-input-line);background:var(--so-raised)}.tools .connect-tile:target,.tools .connect-tile:focus-visible{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile .brand-mark{grid-row:span 2}.tools .connect-tile strong{font-weight:600;line-height:1.3}.tools .connect-tile>span:not(.brand-mark){font-size:.8rem;color:var(--so-muted);line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.tools .connect-tile[data-state="connected"]>span:not(.brand-mark){color:var(--so-success)}.tools .connect-local{grid-column:1/-1}.tools .connect-local>span:not(.brand-mark){-webkit-line-clamp:unset}.tools .connect-problem{margin:0}.tools .connect-problem:focus{outline:2px solid var(--so-accent);outline-offset:2px}.tools .connect-tile:disabled{cursor:default;opacity:.6}${TILE_PHONE}{.tools .connect-grid{grid-template-columns:1fr 1fr}.tools .connect-tile:not(.connect-local)>span:not(.brand-mark){display:none}.tools .connect-local>span:not(.brand-mark){grid-column:2}.tools .connect-tile{min-height:52px;padding:8px 10px}.tools .connect-tile .brand-mark{grid-row:auto}.tools input,.tools select{font-size:16px}.tools .card{padding:14px}}`;
8
9
  /** The project chooser: it opens the chosen project at once; without scripts, its button does. */
9
10
  export function toolsProjectPicker(projects, chosen, keep = {}) {
10
11
  if (projects.length < 2)
@@ -18,19 +19,23 @@ const password = html `<label>Your Toolroll password<input type="password" name=
18
19
  function toolCard(one, base, manage, project, research = null) {
19
20
  const { tool, secretsSet } = one;
20
21
  const standing = toolStanding(tool, secretsSet);
21
- // One line per secret: the disclosure is both where it stands and where it is set.
22
- const secrets = manage ? tool.spec.secrets.map(secret => {
22
+ // How its runs get credentials, in one line; what that means is in Details.
23
+ const mode = credentialModeOf(tool.spec, secretsSet);
24
+ const credential = mode === null ? "" : html `<p class="tool-credential" data-credential="${mode.mode}">${mode.words}</p>`;
25
+ // One line per secret: the disclosure is both where it stands and where it is set. A service connected by signing
26
+ // in gets its tokens only by signing in again (its Connect tile): no token of it is ever pasted here.
27
+ const secrets = manage && !signedIn(tool.spec) ? tool.spec.secrets.map(secret => {
23
28
  const set = secretsSet.includes(secret.name);
24
29
  return html `<details><summary>${set ? `Replace ${secret.name}` : `Set ${secret.name}${secret.optional ? " (optional)" : ""}`}</summary>${postForm("/settings/tools/change", html `<label>${secret.name}<input type="password" name="value" autocomplete="off" required></label>${password}<button>Save</button>`, { hidden: { ...base, action: "secret", name: tool.name, secret: secret.name } })}</details>`;
25
30
  }) : "";
26
31
  const tested = tool.lastTest?.ok ? html `<p>Tested ${tool.lastTest.at.slice(0, 16).replace("T", " ")} UTC: ${tool.lastTest.tools.join(", ") || "it lists no tools"}</p>` : "";
27
- const details = html `<details><summary>Details</summary><div class="tool-facts"><p>Starts: <code>${toolCommandLine(tool.spec)}</code></p><p>From ${tool.source} · added by ${tool.createdBy}</p>${tested}</div></details>`;
32
+ const details = html `<details><summary>Details</summary><div class="tool-facts">${mode?.detail == null ? "" : html `<p>${mode.detail}</p>`}<p>Starts: <code>${toolCommandLine(tool.spec)}</code></p><p>From ${tool.source} · added by ${tool.createdBy}</p>${tested}</div></details>`;
28
33
  const failed = tool.lastTest !== null && !tool.lastTest.ok ? html `<p class="problem" role="status">${tool.lastTest.problem ?? "The last test failed."}</p>` : "";
29
34
  // Connected, but research can't use it (its grant writes): said once, with the one way to fix it.
30
35
  const reconnect = research === null || !manage ? "" : html `<details><summary>Reconnect read-only</summary>${postForm("/settings/tools/connect", html `${password}<button>Reconnect read-only</button>`, { hidden: { ...base, service: tool.name, access: "read" } })}</details>`;
31
36
  const limited = research === null ? "" : html `<p class="tool-note" role="status">${research}</p>${reconnect}`;
32
37
  const controls = manage ? html `<div class="tool-controls">${postForm("/settings/tools/change", html `<button aria-label="Test ${tool.name} in ${project}">Test</button>`, { hidden: { ...base, action: "test", name: tool.name } })}${postForm("/settings/tools/change", html `<button class="secondary">Remove from ${project}</button>`, { hidden: { ...base, action: "remove", name: tool.name } })}</div>` : "";
33
- return html `<article class="card" id="tool-${tool.name}"><div class="tool-heading">${brandMarkHtml(tool.source === "custom" ? null : tool.name, tool.name, standing.ready)}<h2>${tool.name}</h2><span class="tool-state" data-ready="${String(standing.ready)}">${standing.words}</span></div><p>${tool.spec.about}</p>${failed}${limited}${secrets}${details}${controls}</article>`;
38
+ return html `<article class="card" id="tool-${tool.name}"><div class="tool-heading">${brandMarkHtml(tool.source === "custom" ? null : tool.name, tool.name, standing.ready)}<h2>${tool.name}</h2><span class="tool-state" data-ready="${String(standing.ready)}">${standing.words}</span></div><p>${tool.spec.about}</p>${credential}${failed}${limited}${secrets}${details}${controls}</article>`;
34
39
  }
35
40
  /** A service connected here, offered to the person's other projects: each button signs in again for that project (tokens are never copied). */
36
41
  function elsewhere(view, base) {
@@ -44,7 +49,11 @@ export function toolsHtml(view, csrf, canManage, message = {}) {
44
49
  // `shown` is the project this page shows; the server refuses a post whose repo differs from it.
45
50
  const base = { repo: view.repo, shown: view.repo };
46
51
  const to = ` to ${view.project}`;
47
- const intro = html `<p>Builds in ${view.project} use exactly these tools. MCP servers set up elsewhere on this computer aren't used.</p><details><summary>How tools work</summary><ul><li>A tool you add reaches work you approve from now on. Approve a task again to give it a new tool.</li><li>Removing a tool takes it away from every build right away.</li><li>Secrets stay on this computer, are never shown again, and go only to the tool that needs them.</li><li>Reviewers and the lead chat never get tools. Gemini builds don't use tools yet.</li></ul></details>`;
52
+ const keptIn = view.storage === undefined || view.storage.filesOnly ? "Secrets stay on this computer" : view.storage.words.replace(/\.$/, "");
53
+ // No keychain here: said once, plainly, where it can't be missed.
54
+ const storage = view.storage?.problem !== undefined ? html `<p class="problem tool-storage" role="alert">${view.storage.problem}</p>`
55
+ : view.storage?.filesOnly === true ? html `<p class="tool-storage" role="note">${view.storage.words}</p>` : "";
56
+ const intro = html `<p>Builds in ${view.project} use exactly these tools. MCP servers set up elsewhere on this computer aren't used.</p><details><summary>How tools work</summary><ul><li>A tool you add reaches work you approve from now on. Approve a task again to give it a new tool.</li><li>Removing a tool takes it away from every build right away.</li><li>${keptIn}, are never shown again, and go only to the tool that needs them.</li><li>Reviewers and the lead chat never get tools. Gemini builds don't use tools yet.</li></ul></details>`;
48
57
  const cards = view.tools.length === 0 ? html `<p class="meta">No tools yet. Builds in this project get none.</p>` : view.tools.map(one => toolCard(one, base, manage, view.project, view.connections.find(c => c.id === one.tool.name && c.state === "connected")?.research ?? null));
49
58
  const fromList = view.catalog.length === 0 ? "" : postForm("/settings/tools/change", html `<label>Common tools<select name="catalog">${view.catalog.map(one => html `<option value="${one.name}">${one.label}: ${one.about}</option>`)}</select></label>${password}<button>Add${to}</button>`, { hidden: { ...base, action: "add-catalog" } });
50
59
  const custom = html `<details><summary>Add your own</summary>${postForm("/settings/tools/change", html `<label>Name<input name="name" pattern="[a-z0-9][a-z0-9_\\-]{0,39}" maxlength="40" required placeholder="for example: postgres"></label><label>It runs as<select name="transport"><option value="stdio">A program on this computer</option><option value="http">A web address</option></select></label><label>Command or address<input name="target" required placeholder="npx -y some-mcp-server@1.2.3, or https://…"></label><label>Secrets it needs (names, comma separated)<input name="secrets" placeholder="for example: DATABASE_URL"></label><p class="meta">For a web address, the first secret is sent as its sign-in token. Set each secret's value after adding.</p>${password}<button>Add${to}</button>`, { hidden: { ...base, action: "add-custom" } })}</details>`;
@@ -62,5 +71,5 @@ export function toolsHtml(view, csrf, canManage, message = {}) {
62
71
  const add = manage ? html `<details class="card"${view.tools.length === 0 ? html ` open` : ""}><summary>Add a tool</summary>${fromList}${custom}</details>` : "";
63
72
  const found = manage && view.found.length > 0 ? html `<details class="card"><summary>Found on this computer (${view.found.length})</summary><p class="meta">MCP servers your other apps use. Adding one copies its settings, and any secrets it has, into this project.</p>${postForm("/settings/tools/change", html `${view.found.map(one => html `<label class="tool-found"><input type="checkbox" name="import" value="${one.spec.name}"><span><strong>${one.spec.name}</strong> · ${one.source}<br><code>${toolCommandLine(one.spec)}</code>${one.spec.secrets.length === 0 ? "" : html `<br><span class="meta">Brings ${one.spec.secrets.map(s => s.name).join(", ")}</span>`}</span></label>`)}${password}<button>Add selected${to}</button>`, { hidden: { ...base, action: "import" } })}</details>` : "";
64
73
  const note = atButton ? "" : message.problem ? html `<p class="problem" role="alert">${message.problem}</p>` : message.said ? html `<p role="status">${message.said}</p>` : "";
65
- return html `<section class="tools">${note}${intro}${cards}${connect}${add}${found}</section>`;
74
+ return html `<section class="tools">${note}${intro}${storage}${cards}${connect}${add}${found}</section>`;
66
75
  }
@@ -5,7 +5,7 @@ import { leadNameOf } from './lead-identity.js';
5
5
  import { createHash } from 'node:crypto';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { join } from 'node:path';
8
- import { readProjectAccess } from './project-access.js';
8
+ import { accountReachesSql, readProjectAccess } from './project-access.js';
9
9
  import { pauseOnTaskProviders, providerName, signInReason } from './provider-auth.js';
10
10
  import { scopeTermsProblem, stopFactOf } from './store.js';
11
11
  import { assignmentChecksForRun } from './assignment.js';
@@ -19,6 +19,7 @@ import { withFollowUps } from './result-follow-ups.js';
19
19
  import { LANDED_ACTOR, landedReasonOf } from './result-completion.js';
20
20
  import { ASKS, NEED_ASK, NEEDS, askChipOf, failedAttemptSentence, processNeedOf, resultHoldUpSentence } from './needs-you.js';
21
21
  import { LEAD_CLAIM_SQL, LEAD_IDLE_MS } from './lead-voice.js';
22
+ import { grantStandsSql } from './flow-unattended.js';
22
23
  export const WORK_INDEX_PAGE_LIMIT = 40;
23
24
  export const WORK_INDEX_MAX_LIMIT = 100;
24
25
  const EMPTY = { all: 0, 'needs-you': 0, running: 0, completed: 0 };
@@ -134,9 +135,10 @@ const PROJECTION = `WITH RECURSIVE admitted AS MATERIALIZED (
134
135
  UNION ALL SELECT json_extract(leg.value,'$.provider') FROM json_each(CASE WHEN json_valid(sc.approved_route_json) THEN sc.approved_route_json ELSE '{}' END,'$.legs') leg
135
136
  WHERE json_extract(leg.value,'$.phase')='build'))) signed_out,
136
137
  (sc.approved_at IS NOT NULL AND sc.approved_by IS NOT NULL AND sc.approved_digest=sc.digest) approved,
137
- (sc.approval_basis IS NOT 'mode' OR EXISTS(SELECT 1 FROM operating_mode om JOIN approver signer ON signer.name=om.signed_by
138
+ ((sc.approval_basis IS NOT 'mode' AND sc.approval_basis IS NOT 'grant') OR (sc.approval_basis='mode' AND EXISTS(SELECT 1 FROM operating_mode om JOIN approver signer ON signer.name=om.signed_by
138
139
  WHERE om.repo=c.repo AND om.digest=sc.mode_digest AND om.revoked_at IS NULL AND om.absolute_expiry>$now
139
- AND signer.role='approver' AND signer.revoked_at IS NULL)) dispatch_approval,
140
+ AND signer.role='approver' AND signer.revoked_at IS NULL AND ${accountReachesSql("signer", "c.repo")}))
141
+ OR (sc.approval_basis='grant' AND ${grantStandsSql("sc.mode_digest", "$now", "c.repo")})) dispatch_approval,
140
142
  CASE WHEN sc.task_id IS NULL THEN 0 ELSE so_work_scope_valid(json_object(
141
143
  'touches',sc.touches,'acceptance_json',sc.acceptance_json,'budget_microusd',sc.budget_microusd,
142
144
  'goal',CASE WHEN typeof(sc.goal)='text' THEN '' ELSE sc.goal END,
@@ -24,6 +24,7 @@
24
24
  */
25
25
  import { type ExecResult, type RunOptions } from "./exec.js";
26
26
  import type { Store, WorktreeRow } from "./store.js";
27
+ import { type Git } from "./teardown-git.js";
27
28
  export type Runner = (file: string, args: readonly string[], options?: RunOptions) => Promise<ExecResult>;
28
29
  export type PoolOptions = {
29
30
  /** Where worktrees are created. Outside the repo, and outside any sync root. */
@@ -52,6 +53,32 @@ export type LeaseRequest = {
52
53
  * stands, as its kept checkout would have been reused. Without it, `base` always starts a new branch (and an
53
54
  * existing one refuses). */
54
55
  reuseBranch?: boolean;
56
+ /**
57
+ * Start from current main (fresh base): a checkout still at its recorded base — no commits of its own — and with no
58
+ * changes moves, with its branch, to `main` before the attempt. Anything else is never moved; the result says how
59
+ * far behind main it is. Needs `taskRef`. `recordedBase` null means no attempt on the branch recorded one yet, and
60
+ * then only a HEAD that main already contains counts as having no commits of its own.
61
+ */
62
+ freshBase?: {
63
+ main: string;
64
+ recordedBase: string | null;
65
+ };
66
+ };
67
+ /** Where a checkout stands against current main once its lease is granted (fresh base). */
68
+ export type BaseStanding = {
69
+ /** HEAD as the attempt starts. */
70
+ head: string;
71
+ /** Current main's commit. */
72
+ main: string;
73
+ /** Commits on main that HEAD does not have. */
74
+ behind: number;
75
+ /** Set when this lease moved the checkout to main: where it was, and how far behind that was. */
76
+ moved: {
77
+ from: string;
78
+ behind: number;
79
+ } | null;
80
+ /** Why a checkout behind main stayed where it is. */
81
+ kept: "has commits" | "has changes" | "not on its branch" | null;
55
82
  };
56
83
  export type LeaseResult = {
57
84
  ok: true;
@@ -60,6 +87,8 @@ export type LeaseResult = {
60
87
  reclaimed?: string;
61
88
  resumedFromRun?: number;
62
89
  recoveryKind?: "completed" | "partial";
90
+ /** With `freshBase`: where the checkout stands against main (null when main could not be read). */
91
+ base?: BaseStanding | null;
63
92
  } | {
64
93
  ok: false;
65
94
  reason: LeaseFailure;
@@ -76,6 +105,13 @@ export type KeptWhy = "has changes" | "has commits" | "unreadable" | "git refuse
76
105
  * something is in there.
77
106
  */
78
107
  export declare const MARKER = ".standing-orders-lease";
108
+ /**
109
+ * The reflog message of a branch Toolroll created, naming it: its creation entry is the proof, later, that the branch
110
+ * is Toolroll's and how old it is. A branch copied or renamed from it names the old branch, so it proves nothing.
111
+ */
112
+ export declare const branchCreatedMessage: (branch: string) => string;
113
+ /** Scouts' disposable branches: never landed work. */
114
+ export declare const SCOUT_PREFIX = "toolroll-scout/";
79
115
  /** What a kept checkout drops once its run ends (only where .gitignore hides it): dependencies and build output. */
80
116
  export declare const SLIM_NAMES: readonly string[];
81
117
  /** Creating a worktree copies a tree; it is local work but not instant. */
@@ -88,6 +124,8 @@ export declare const WORKTREE_TIMEOUT_MS = 60000;
88
124
  * the pool root through its own slashes.
89
125
  */
90
126
  export declare function worktreePath(root: string, repo: string, branch: string): string;
127
+ /** The pool root a checkout at `path` of `repo` was made under (worktreePath's layout), or null for one it didn't make. */
128
+ export declare function poolRootOf(path: string, repo: string): string | null;
91
129
  /** Record a potentially writing subprocess against the original checkout lease. */
92
130
  export declare function recordWorktreeProcess(store: Store, path: string, runner: string, pid: number, leaseEpoch?: string | null): void;
93
131
  /** Read-only occupancy witness shared with stop settlement. */
@@ -157,6 +195,13 @@ export declare class WorktreePool {
157
195
  recordProviderOccupancy(path: string, runner: string, providerPid: number, leaseEpoch?: string | null): void;
158
196
  lease(request: LeaseRequest): Promise<LeaseResult>;
159
197
  private leaseNow;
198
+ /**
199
+ * Move an untouched checkout to current main, or say how far behind it is. Untouched means all three: on its own
200
+ * branch, HEAD still the recorded base (or, with none recorded, a commit main already has), and nothing changed or
201
+ * untracked. The move is `git reset --keep`, which refuses rather than overwrite anything; HEAD and the tree are read
202
+ * again right before and after it, and any surprise fails the lease with the checkout left for a person.
203
+ */
204
+ private freshen;
160
205
  /**
161
206
  * A crash can strand either a finished handoff or a partial source draft.
162
207
  * The expired lease can no longer authorize a commit, so this never ingests
@@ -239,16 +284,18 @@ export declare class WorktreePool {
239
284
  /**
240
285
  * Whether a let-go checkout could go without losing anything: clean (build output that .gitignore hides is not
241
286
  * work, and neither are the files Toolroll itself wrote in there), and everything its HEAD reaches on a branch, tag
242
- * or remote. Reads only.
287
+ * or remote. Reads only, through `git` when given (teardown's hardened git).
243
288
  */
244
- inspect(path: string): Promise<"clean" | KeptWhy>;
289
+ inspect(path: string, git?: Git): Promise<"clean" | KeptWhy>;
245
290
  /**
246
291
  * Keep storage in check: remove the working copies nobody needs any more. Of `rows`, a checkout goes when it is
247
292
  * let go, nothing holds it, `wanted` says it may go (asked again right before it goes, so a task coming back or a
248
- * lease since keeps it) and `inspect` finds it clean. Only the working copy goes: its branch, and so every commit,
249
- * stays, and a later lease of the same branch makes a new one. At most `max` go per pass.
293
+ * lease since keeps it) and `inspect` finds it clean, then clean still right before it goes (git refuses an edit
294
+ * made after that). Only the working copy goes: its branch, and so every commit,
295
+ * stays, and a later lease of the same branch makes a new one. At most `max` go per pass. Every git command goes
296
+ * through `git` when given: landed cleanup passes its hardened teardown git (teardown-git.ts).
250
297
  */
251
- prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number): Promise<{
298
+ prune(rows: readonly WorktreeRow[], wanted: (row: WorktreeRow, fresh: boolean) => boolean | Promise<boolean>, max?: number, git?: Git): Promise<{
252
299
  removed: WorktreeRow[];
253
300
  kept: {
254
301
  path: string;
@@ -280,8 +327,17 @@ export declare class WorktreePool {
280
327
  ok: false;
281
328
  message: string;
282
329
  }>;
283
- /** Remove one idle checkout's working copy (the caller holds it in `busy`) and forget its row. */
330
+ /** Throw away one idle checkout's working copy, changes and all (the caller holds it in `busy`), and forget its row.
331
+ * Only `discardChanges`, where losing them is what a person asked for. */
284
332
  private removeWorkingCopy;
333
+ /**
334
+ * Remove one idle checkout's working copy only while it is still clean (the caller holds it in `busy`, and its
335
+ * reservation outside the checkout holds it for other processes): inspected once more right before it goes, then
336
+ * removed without --force, so git itself refuses an edit made since. Only Toolroll's own files go first, because
337
+ * git's check would count them.
338
+ */
339
+ private removeCleanCopy;
340
+ private forgetRemoved;
285
341
  /**
286
342
  * Whether anybody's work is in there. null means we could not tell, which is
287
343
  * neither clean nor dirty and must not be rounded to either.
@@ -290,5 +346,7 @@ export declare class WorktreePool {
290
346
  * hides is not work, but a file somebody dropped in and never staged is.
291
347
  */
292
348
  private isDirty;
349
+ /** All disposal and inspection paths share the hardened teardown boundary. */
350
+ private readonly teardown;
293
351
  private git;
294
352
  }