toolroll 0.9.59 → 0.9.61

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 (152) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +8 -6
  3. package/dist/agent-board.d.ts +208 -0
  4. package/dist/agent-board.js +454 -0
  5. package/dist/agent-fence.d.ts +19 -0
  6. package/dist/agent-fence.js +100 -8
  7. package/dist/agent-pulse.d.ts +43 -0
  8. package/dist/agent-pulse.js +214 -0
  9. package/dist/agent-turns.d.ts +52 -0
  10. package/dist/agent-turns.js +137 -0
  11. package/dist/agents-board.d.ts +101 -0
  12. package/dist/agents-board.js +225 -0
  13. package/dist/assignment-adapters.d.ts +2 -1
  14. package/dist/assignment-adapters.js +1 -1
  15. package/dist/assignment-delivery.d.ts +16 -0
  16. package/dist/assignment-delivery.js +54 -28
  17. package/dist/assignment-status.d.ts +14 -5
  18. package/dist/assignment-status.js +70 -39
  19. package/dist/assignment.d.ts +10 -2
  20. package/dist/assignment.js +21 -14
  21. package/dist/bin.js +11 -2
  22. package/dist/brief-fence.d.ts +9 -0
  23. package/dist/brief-fence.js +22 -0
  24. package/dist/browser/workspace.css +2 -2
  25. package/dist/browser/workspace.js +29 -25
  26. package/dist/browser-crew.js +2 -0
  27. package/dist/browser-workspace.d.ts +79 -1
  28. package/dist/browser-workspace.js +1 -0
  29. package/dist/builder.d.ts +8 -0
  30. package/dist/builder.js +178 -29
  31. package/dist/chat-channel.js +10 -1
  32. package/dist/chat-core.d.ts +6 -0
  33. package/dist/chat-core.js +8 -0
  34. package/dist/chat-delivery.js +22 -0
  35. package/dist/chat-messages.js +8 -1
  36. package/dist/chat-quiet.js +1 -0
  37. package/dist/chat-voice.d.ts +2 -0
  38. package/dist/chat-voice.js +2 -0
  39. package/dist/checkout-cleanup.d.ts +3 -0
  40. package/dist/checkout-cleanup.js +11 -2
  41. package/dist/cli-fast.d.ts +8 -0
  42. package/dist/cli-fast.js +18 -0
  43. package/dist/cli-help.d.ts +5 -0
  44. package/dist/cli-help.js +91 -0
  45. package/dist/cli.d.ts +6 -5
  46. package/dist/cli.js +17 -96
  47. package/dist/contracts/console-api.d.ts +3 -1
  48. package/dist/contracts/console-api.js +5 -1
  49. package/dist/contracts/lead-tools.d.ts +16 -2
  50. package/dist/contracts/lead-tools.js +5 -1
  51. package/dist/design/tokens.css +234 -0
  52. package/dist/desktop-update.d.ts +1 -1
  53. package/dist/discord.js +2 -1
  54. package/dist/evidence-pack.js +1 -1
  55. package/dist/evidence.d.ts +28 -0
  56. package/dist/evidence.js +87 -17
  57. package/dist/exec.d.ts +32 -0
  58. package/dist/exec.js +229 -10
  59. package/dist/flow-actions.js +2 -1
  60. package/dist/guides.js +14 -11
  61. package/dist/invoke.js +110 -18
  62. package/dist/landed-cleanup.d.ts +157 -0
  63. package/dist/landed-cleanup.js +707 -0
  64. package/dist/lead-cli.js +22 -19
  65. package/dist/lead-contract.js +1 -1
  66. package/dist/lead-doors.js +49 -2
  67. package/dist/lead-follow.d.ts +2 -0
  68. package/dist/lead-follow.js +138 -88
  69. package/dist/lead-tools.js +48 -8
  70. package/dist/live-input.d.ts +68 -0
  71. package/dist/live-input.js +254 -0
  72. package/dist/live.d.ts +1 -1
  73. package/dist/live.js +4 -2
  74. package/dist/mailbox.js +3 -2
  75. package/dist/maintenance.d.ts +2 -0
  76. package/dist/maintenance.js +5 -2
  77. package/dist/mcp-core.js +1 -1
  78. package/dist/operate-remote-arguments.js +5 -1
  79. package/dist/operate-remote.js +1 -1
  80. package/dist/operate.d.ts +1 -1
  81. package/dist/operate.js +201 -19
  82. package/dist/orphan-sweep.d.ts +2 -0
  83. package/dist/orphan-sweep.js +5 -3
  84. package/dist/project-delete-ui.d.ts +11 -0
  85. package/dist/project-delete-ui.js +29 -2
  86. package/dist/project-tools.d.ts +7 -11
  87. package/dist/project-tools.js +13 -37
  88. package/dist/provider.d.ts +73 -8
  89. package/dist/provider.js +87 -17
  90. package/dist/result-completion.d.ts +7 -0
  91. package/dist/result-completion.js +9 -0
  92. package/dist/runner.d.ts +4 -2
  93. package/dist/runner.js +3 -3
  94. package/dist/serve.js +22 -3
  95. package/dist/server/chrome.d.ts +6 -3
  96. package/dist/server/chrome.js +8 -4
  97. package/dist/server/console.css +9 -64
  98. package/dist/server/live.d.ts +3 -0
  99. package/dist/server/live.js +35 -2
  100. package/dist/server/pages.js +59 -35
  101. package/dist/server/render-chat.js +14 -2
  102. package/dist/server/render-tasks.d.ts +14 -2
  103. package/dist/server/render-tasks.js +38 -20
  104. package/dist/server/route-table.js +4 -1
  105. package/dist/server/settings.js +2 -1
  106. package/dist/server/task-views.js +10 -0
  107. package/dist/server/tasks.js +63 -0
  108. package/dist/shared-deps.d.ts +9 -4
  109. package/dist/shared-deps.js +89 -13
  110. package/dist/slack-socket.d.ts +12 -0
  111. package/dist/slack-socket.js +29 -0
  112. package/dist/slack.d.ts +0 -7
  113. package/dist/slack.js +2 -24
  114. package/dist/staged-runtimes.d.ts +16 -4
  115. package/dist/staged-runtimes.js +160 -38
  116. package/dist/steer-live.d.ts +19 -0
  117. package/dist/steer-live.js +93 -0
  118. package/dist/steer-mailbox.d.ts +39 -0
  119. package/dist/steer-mailbox.js +207 -0
  120. package/dist/steer-status.d.ts +25 -0
  121. package/dist/steer-status.js +44 -0
  122. package/dist/storage-sweep-child.d.ts +1 -0
  123. package/dist/storage-sweep-child.js +76 -0
  124. package/dist/storage-sweep-process.d.ts +41 -0
  125. package/dist/storage-sweep-process.js +187 -0
  126. package/dist/storage-sweep.d.ts +30 -6
  127. package/dist/storage-sweep.js +214 -28
  128. package/dist/storage-ui.d.ts +2 -0
  129. package/dist/storage-ui.js +16 -4
  130. package/dist/storage.d.ts +6 -0
  131. package/dist/storage.js +35 -0
  132. package/dist/store.d.ts +202 -2
  133. package/dist/store.js +440 -13
  134. package/dist/surface.js +5 -3
  135. package/dist/sweep-gate.d.ts +10 -0
  136. package/dist/sweep-gate.js +18 -0
  137. package/dist/sweep-quarantine.d.ts +28 -0
  138. package/dist/sweep-quarantine.js +153 -0
  139. package/dist/task-status.d.ts +2 -0
  140. package/dist/task-status.js +5 -1
  141. package/dist/team-updates.js +3 -3
  142. package/dist/test-temp.d.ts +6 -3
  143. package/dist/test-temp.js +15 -9
  144. package/dist/toolroll-update.js +15 -2
  145. package/dist/work-index.d.ts +1 -0
  146. package/dist/work-index.js +32 -12
  147. package/dist/work-summary.d.ts +2 -2
  148. package/dist/workspace-revision.js +3 -1
  149. package/dist/worktree.d.ts +3 -1
  150. package/dist/worktree.js +93 -10
  151. package/package.json +21 -21
  152. package/scripts/provider-canary.mjs +235 -147
package/dist/worktree.js CHANGED
@@ -23,7 +23,7 @@
23
23
  * over `.git` internals corrupts both.
24
24
  */
25
25
  import { createHash, randomBytes } from "node:crypto";
26
- import { existsSync, readFileSync, writeFileSync, rmSync, realpathSync, mkdirSync, readdirSync } from "node:fs";
26
+ import { existsSync, readFileSync, writeFileSync, rmSync, realpathSync, mkdirSync, readdirSync, renameSync } from "node:fs";
27
27
  import { hostname } from "node:os";
28
28
  import { dirname, join } from "node:path";
29
29
  import { run } from "./exec.js";
@@ -42,6 +42,61 @@ import { markNeverIndex } from "./never-index.js";
42
42
  export const MARKER = ".standing-orders-lease";
43
43
  /** Checkouts this process is leasing or removing right now: a lease and storage retention never overlap on one. */
44
44
  const busy = new Map();
45
+ // Outside the checkout so removal cannot erase the reservation before git and the row update finish.
46
+ const reservationPath = (path) => `${path}.occupancy`;
47
+ function reservation(path) {
48
+ try {
49
+ const note = JSON.parse(readFileSync(reservationPath(path), "utf8"));
50
+ if (Number.isSafeInteger(note.pid) && note.pid > 0 && typeof note.host === "string" && typeof note.token === "string")
51
+ return note;
52
+ }
53
+ catch (error) {
54
+ if (error.code === "ENOENT")
55
+ return null;
56
+ }
57
+ // An unreadable reservation is occupied, never a reason to delete someone's checkout.
58
+ return { pid: 0, host: "unknown", bootId: null, token: "unknown" };
59
+ }
60
+ function reservationAlive(note) {
61
+ if (note.pid === 0 || note.host !== hostname())
62
+ return true;
63
+ if (note.bootId !== null && provenDeadByBootChange(note))
64
+ return false;
65
+ try {
66
+ process.kill(note.pid, 0);
67
+ return true;
68
+ }
69
+ catch (error) {
70
+ return error.code !== "ESRCH";
71
+ }
72
+ }
73
+ /** A short store transaction arbitrates marker creation/recovery, never git, a scan or deletion.
74
+ * Atomic rename publishes a complete owner; a crash cannot leave a half-written reservation. */
75
+ function reserveCheckout(store, path) {
76
+ const note = { pid: process.pid, host: hostname(), bootId: currentBootId(), token: randomBytes(12).toString("hex") };
77
+ const file = reservationPath(path), temp = `${file}.${note.token}`;
78
+ const held = store.transact(() => {
79
+ const prior = reservation(path);
80
+ if (prior !== null && reservationAlive(prior))
81
+ return false;
82
+ try {
83
+ mkdirSync(dirname(path), { recursive: true });
84
+ writeFileSync(temp, JSON.stringify(note), { flag: "wx", mode: 0o600 });
85
+ renameSync(temp, file);
86
+ return true;
87
+ }
88
+ catch {
89
+ return false;
90
+ }
91
+ finally {
92
+ rmSync(temp, { force: true });
93
+ }
94
+ });
95
+ if (!held)
96
+ return null;
97
+ return () => store.transact(() => { if (reservation(path)?.token === note.token)
98
+ rmSync(file, { force: true }); });
99
+ }
45
100
  /** What a kept checkout drops once its run ends (only where .gitignore hides it): dependencies and build output. */
46
101
  export const SLIM_NAMES = ["node_modules", "dist", "build", "out", ".next", ".nuxt", ".svelte-kit", ".turbo", ".parcel-cache", "coverage", "target"];
47
102
  /** Creating a worktree copies a tree; it is local work but not instant. */
@@ -94,6 +149,9 @@ export function recordWorktreeProcess(store, path, runner, pid, leaseEpoch) {
94
149
  }
95
150
  /** Read-only occupancy witness shared with stop settlement. */
96
151
  export function worktreeProcessOccupancy(path) {
152
+ const reserved = reservation(path);
153
+ if (reserved !== null && (reserved.pid !== process.pid || reserved.host !== hostname()) && reservationAlive(reserved))
154
+ return { held: true, by: reserved.pid };
97
155
  const note = join(path, MARKER);
98
156
  if (!existsSync(note))
99
157
  return { held: false };
@@ -176,6 +234,8 @@ export class WorktreePool {
176
234
  store;
177
235
  options;
178
236
  runner;
237
+ /** Carried to the independent storage sweep process. */
238
+ get root() { return this.options.root; }
179
239
  constructor(store, options) {
180
240
  this.store = store;
181
241
  this.options = options;
@@ -237,17 +297,18 @@ export class WorktreePool {
237
297
  }
238
298
  async lease(request) {
239
299
  const target = worktreePath(this.options.root, request.repo, request.branch);
240
- if (busy.get(target) === "removing")
241
- return { ok: false, reason: "in-use", message: `${target} is being removed; try again in a moment` };
242
- const mine = !busy.has(target);
243
- if (mine)
244
- busy.set(target, "leasing");
300
+ if (busy.has(target))
301
+ return { ok: false, reason: "in-use", message: `${target} is busy; try again in a moment` };
302
+ const release = reserveCheckout(this.store, target);
303
+ if (release === null)
304
+ return { ok: false, reason: "in-use", message: `${target} is busy; try again in a moment` };
305
+ busy.set(target, "leasing");
245
306
  try {
246
307
  return await this.leaseNow(request);
247
308
  }
248
309
  finally {
249
- if (mine)
250
- busy.delete(target);
310
+ release();
311
+ busy.delete(target);
251
312
  }
252
313
  }
253
314
  async leaseNow(request) {
@@ -688,7 +749,10 @@ export class WorktreePool {
688
749
  for (const row of rows) {
689
750
  if (removed.length >= max)
690
751
  break;
691
- if (row.releasedAt === null || row.runner !== null || busy.has(row.path) || !wanted(row, false))
752
+ if (row.releasedAt === null || row.runner !== null || busy.has(row.path))
753
+ continue;
754
+ const selected = wanted(row, false);
755
+ if (!(typeof selected === "boolean" ? selected : await selected))
692
756
  continue;
693
757
  if (!existsSync(row.path) || !idle(row, row.releasedAt))
694
758
  continue;
@@ -699,11 +763,18 @@ export class WorktreePool {
699
763
  }
700
764
  if (busy.has(row.path))
701
765
  continue;
766
+ const release = reserveCheckout(this.store, row.path);
767
+ if (release === null)
768
+ continue;
702
769
  busy.set(row.path, "removing");
703
770
  try {
704
771
  // A lease, or a task coming back, since the listing wins: look again right before removing.
705
772
  const now = this.store.getWorktree(row.path);
706
- if (!idle(now, row.releasedAt) || !wanted(now, true))
773
+ if (!idle(now, row.releasedAt))
774
+ continue;
775
+ // Landed cleanup rechecks Git here; existing synchronous guards keep their synchronous path.
776
+ const allowed = wanted(now, true);
777
+ if (!(typeof allowed === "boolean" ? allowed : await allowed))
707
778
  continue;
708
779
  const gone = await this.removeWorkingCopy(row);
709
780
  if (!gone) {
@@ -713,6 +784,7 @@ export class WorktreePool {
713
784
  removed.push(row);
714
785
  }
715
786
  finally {
787
+ release();
716
788
  busy.delete(row.path);
717
789
  }
718
790
  }
@@ -735,6 +807,9 @@ export class WorktreePool {
735
807
  .filter(one => { const name = one.split("/").at(-1); return SLIM_NAMES.includes(name) && (deps || name !== "node_modules"); });
736
808
  if (drop.length === 0)
737
809
  return { ok: true, dropped: [], bytes: 0 };
810
+ const release = reserveCheckout(this.store, row.path);
811
+ if (release === null)
812
+ return { ok: false, message: `${row.path} is in use` };
738
813
  busy.set(row.path, "removing");
739
814
  const note = join(row.path, MARKER);
740
815
  let held = false;
@@ -759,6 +834,7 @@ export class WorktreePool {
759
834
  finally {
760
835
  if (held)
761
836
  rmSync(note, { force: true });
837
+ release();
762
838
  busy.delete(row.path);
763
839
  }
764
840
  }
@@ -775,8 +851,14 @@ export class WorktreePool {
775
851
  const occupied = this.inUse(path);
776
852
  if (occupied.held)
777
853
  return { ok: false, message: `${path} is being used by process ${occupied.by} — it can't be discarded now` };
854
+ const release = reserveCheckout(this.store, path);
855
+ if (release === null)
856
+ return { ok: false, message: `${path} is in use` };
778
857
  busy.set(path, "removing");
779
858
  try {
859
+ const fresh = this.store.getWorktree(path);
860
+ if (fresh === null || fresh.runner !== null || fresh.releasedAt !== row.releasedAt || this.inUse(path).held)
861
+ return { ok: false, message: `${path} is in use` };
780
862
  if (!existsSync(path)) {
781
863
  this.store.transact(() => { const still = this.store.getWorktree(path); if (still !== null && still.runner === null && still.releasedAt === row.releasedAt)
782
864
  this.store.forgetWorktree(path); });
@@ -785,6 +867,7 @@ export class WorktreePool {
785
867
  return (await this.removeWorkingCopy(row)) ? { ok: true, row } : { ok: false, message: `git would not remove ${path}` };
786
868
  }
787
869
  finally {
870
+ release();
788
871
  busy.delete(path);
789
872
  }
790
873
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.59",
3
+ "version": "0.9.61",
4
4
  "description": "A control plane for unattended coding agents — queue tasks, walk away, come back to pull requests. Agents build in leased worktrees, never touch your default branch, and interrupt you only for decisions that need a human.",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -62,16 +62,36 @@
62
62
  "node": ">=22.13.0"
63
63
  },
64
64
  "devDependencies": {
65
+ "@radix-ui/react-collapsible": "1.1.20",
66
+ "@radix-ui/react-dialog": "1.1.23",
67
+ "@radix-ui/react-dropdown-menu": "2.1.24",
68
+ "@radix-ui/react-label": "2.1.15",
69
+ "@radix-ui/react-radio-group": "1.4.7",
70
+ "@radix-ui/react-select": "2.3.7",
71
+ "@radix-ui/react-separator": "1.1.15",
72
+ "@radix-ui/react-slot": "1.3.3",
73
+ "@radix-ui/react-switch": "1.3.7",
74
+ "@radix-ui/react-tabs": "1.1.21",
75
+ "@radix-ui/react-tooltip": "1.2.16",
65
76
  "@tailwindcss/cli": "4.3.3",
66
77
  "@types/mailparser": "3.9.0",
67
78
  "@types/node": "26.6.4",
68
79
  "@types/react": "19.3.0",
69
80
  "@types/react-dom": "19.3.0",
81
+ "@xyflow/react": "12.12.0",
82
+ "class-variance-authority": "0.7.1",
83
+ "clsx": "2.1.1",
70
84
  "esbuild": "0.28.2",
71
85
  "happy-dom": "20.14.5",
86
+ "lucide-react": "1.52.0",
87
+ "react": "19.3.0",
88
+ "react-dom": "19.3.0",
89
+ "sonner": "2.0.8",
90
+ "tailwind-merge": "3.7.0",
72
91
  "tailwindcss": "4.3.3",
73
92
  "tsx": "4.23.15",
74
93
  "typescript": "7.0.2",
94
+ "use-stick-to-bottom": "1.1.6",
75
95
  "vitest": "4.1.11"
76
96
  },
77
97
  "files": [
@@ -96,32 +116,12 @@
96
116
  }
97
117
  },
98
118
  "dependencies": {
99
- "@radix-ui/react-collapsible": "1.1.20",
100
- "@radix-ui/react-dialog": "1.1.23",
101
- "@radix-ui/react-dropdown-menu": "2.1.24",
102
- "@radix-ui/react-label": "2.1.15",
103
- "@radix-ui/react-radio-group": "1.4.7",
104
- "@radix-ui/react-select": "2.3.7",
105
- "@radix-ui/react-separator": "1.1.15",
106
- "@radix-ui/react-slot": "1.3.3",
107
- "@radix-ui/react-switch": "1.3.7",
108
- "@radix-ui/react-tabs": "1.1.21",
109
- "@radix-ui/react-tooltip": "1.2.16",
110
119
  "@slack/socket-mode": "3.1.0",
111
- "@xyflow/react": "12.12.0",
112
- "class-variance-authority": "0.7.1",
113
- "clsx": "2.1.1",
114
120
  "discord.js": "14.27.0",
115
121
  "imapflow": "2.2.5",
116
- "lucide-react": "1.52.0",
117
122
  "mailparser": "3.9.36",
118
123
  "nodemailer": "10.0.15",
119
- "react": "19.3.0",
120
- "react-dom": "19.3.0",
121
- "sonner": "2.0.8",
122
- "tailwind-merge": "3.7.0",
123
124
  "typescript-parser": "npm:typescript@6.0.3",
124
- "use-stick-to-bottom": "1.1.6",
125
125
  "yaml": "2.9.1",
126
126
  "zod": "^4.6.5"
127
127
  }