toolroll 0.9.31 → 0.9.33

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 (68) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/assignment.d.ts +3 -0
  3. package/dist/assignment.js +8 -2
  4. package/dist/batch-checks.d.ts +15 -0
  5. package/dist/batch-checks.js +244 -0
  6. package/dist/batch-policy.d.ts +23 -0
  7. package/dist/batch-policy.js +25 -0
  8. package/dist/browser/workspace.js +13 -13
  9. package/dist/builder.js +14 -3
  10. package/dist/chat-actions.js +2 -2
  11. package/dist/check-levels-ui.d.ts +4 -0
  12. package/dist/check-levels-ui.js +21 -2
  13. package/dist/checkout-cleanup.d.ts +2 -1
  14. package/dist/checkout-cleanup.js +24 -8
  15. package/dist/coding-context.js +2 -2
  16. package/dist/coding-handoff.js +2 -2
  17. package/dist/desktop-update.d.ts +1 -1
  18. package/dist/exec.d.ts +2 -0
  19. package/dist/exec.js +11 -0
  20. package/dist/guides.js +3 -0
  21. package/dist/knowledge-cli.d.ts +31 -0
  22. package/dist/knowledge-cli.js +35 -2
  23. package/dist/knowledge-ui.d.ts +1 -1
  24. package/dist/knowledge-ui.js +7 -0
  25. package/dist/lead-capabilities.d.ts +0 -7
  26. package/dist/lead-capabilities.js +11 -1
  27. package/dist/mate-doors.js +2 -2
  28. package/dist/mate.d.ts +6 -2
  29. package/dist/mate.js +63 -38
  30. package/dist/never-index.d.ts +3 -0
  31. package/dist/never-index.js +21 -0
  32. package/dist/operate.js +99 -5
  33. package/dist/orphan-sweep.d.ts +63 -0
  34. package/dist/orphan-sweep.js +235 -0
  35. package/dist/project-delete.js +1 -1
  36. package/dist/project-knowledge.d.ts +28 -3
  37. package/dist/project-knowledge.js +64 -25
  38. package/dist/project-learning.d.ts +33 -0
  39. package/dist/project-learning.js +105 -15
  40. package/dist/project-skills.js +27 -21
  41. package/dist/result-follow-ups.d.ts +22 -2
  42. package/dist/result-follow-ups.js +44 -10
  43. package/dist/serve.js +33 -11
  44. package/dist/shared-deps.d.ts +18 -2
  45. package/dist/shared-deps.js +129 -15
  46. package/dist/staged-runtimes.d.ts +42 -0
  47. package/dist/staged-runtimes.js +191 -0
  48. package/dist/storage-sweep.d.ts +50 -0
  49. package/dist/storage-sweep.js +203 -0
  50. package/dist/storage-ui.d.ts +5 -0
  51. package/dist/storage-ui.js +30 -4
  52. package/dist/storage.d.ts +2 -0
  53. package/dist/storage.js +15 -3
  54. package/dist/store.d.ts +17 -2
  55. package/dist/store.js +37 -5
  56. package/dist/subscription-chat.d.ts +3 -1
  57. package/dist/subscription-chat.js +1 -0
  58. package/dist/surface.js +2 -1
  59. package/dist/task-status.d.ts +13 -0
  60. package/dist/task-status.js +37 -3
  61. package/dist/team-leads.js +6 -2
  62. package/dist/team-runtime.js +2 -1
  63. package/dist/test-temp.d.ts +20 -2
  64. package/dist/test-temp.js +53 -13
  65. package/dist/toolroll-update.js +2 -0
  66. package/dist/workspace-ui.js +3 -1
  67. package/dist/worktree.js +9 -0
  68. package/package.json +1 -1
@@ -26,6 +26,21 @@ export const CHECKS_LABEL = "Project checks";
26
26
  export const PR_CI_LABEL = "PR CI";
27
27
  /** The one verb for opening a saved result, on every surface. */
28
28
  export const OPEN_RESULT = "Open result";
29
+ /** "T2", "T2 and T3", "T2, T3 and 2 more": short enough for one line. The full list is on the result. */
30
+ export function peerWords(peers, shown = 2) {
31
+ if (peers.length <= shown)
32
+ return peers.length <= 1 ? peers.join("") : `${peers.slice(0, -1).join(", ")} and ${peers.at(-1)}`;
33
+ return `${peers.slice(0, shown).join(", ")} and ${peers.length - shown} more`;
34
+ }
35
+ /** "Checked together with T2 on 1a2b3c4": every surface says a shared check the same way, naming the commit it tested. */
36
+ export function checkedTogetherWords(batch) {
37
+ const sha = short(batch.tested);
38
+ return batch.state === "together" && batch.peers.length > 0 && sha !== null ? `Checked together with ${peerWords(batch.peers)} on ${sha}` : null;
39
+ }
40
+ const BATCH_WHY = {
41
+ split: "Its batch check failed, so it was checked on its own.",
42
+ conflict: "It conflicted with another result when merged for a batch check, so it was checked on its own.",
43
+ };
29
44
  const short = (sha) => sha !== null && /^[a-f0-9]{7,40}$/.test(sha) ? sha.slice(0, 7) : null;
30
45
  export function headlineOf(facts) {
31
46
  switch (facts.stage) {
@@ -64,8 +79,15 @@ function sentenceOf(headline, facts) {
64
79
  return "The report is ready to read. Read it, then mark it complete.";
65
80
  if (facts.checks?.level === "off" && facts.checks.status !== "passed")
66
81
  return "Checks are off for this project. Review the change, then mark it complete.";
67
- if (facts.checks?.status === "passed")
82
+ if (facts.checks?.status === "passed") {
83
+ // The Checks row names the others and the batch commit; the sentence doesn't repeat them.
84
+ const peers = facts.checks.batch?.state === "together" ? facts.checks.batch.peers.length : 0;
85
+ if (peers > 0)
86
+ return `Checks passed together with ${peers === 1 ? "1 other result" : `${peers} other results`}. Review the change, then mark it complete.`;
68
87
  return `${facts.checks.level === "quick" ? "Quick checks" : "Checks"} passed${sha === null ? "" : ` on ${sha}`}. Review the change, then mark it complete.`;
88
+ }
89
+ if (facts.checks?.batch?.state === "waiting")
90
+ return "Its full check runs with other results within 10 minutes. Review the change, then mark it complete.";
69
91
  // Never claim a check that isn't known to have passed.
70
92
  if (facts.checks == null)
71
93
  return "Review the change, then mark it complete.";
@@ -93,8 +115,19 @@ function detailsOf(headline, facts) {
93
115
  const checksHref = facts.links?.checks ?? facts.links?.result ?? null;
94
116
  const runChecks = facts.links?.runChecks === undefined ? null : { label: "Run checks", href: facts.links.runChecks };
95
117
  const quick = checks.level === "quick";
96
- if (checks.running != null)
118
+ const batch = checks.batch ?? null;
119
+ const together = batch === null ? null : checkedTogetherWords(batch);
120
+ const batchWhy = batch === null ? null : BATCH_WHY[batch.state] ?? null;
121
+ if (batch?.state === "waiting")
122
+ row("checks", CHECKS_LABEL, "Waiting to check with other results", "running", { href: checksHref });
123
+ else if (checks.running != null)
97
124
  row("checks", CHECKS_LABEL, `${checks.running === "quick" ? "Quick" : "Full"} checks running`, "running", { href: checksHref });
125
+ else if (checks.status === "passed" && together !== null)
126
+ row("checks", CHECKS_LABEL, together, "ok", { href: checksHref });
127
+ else if (checks.status === "passed" && batchWhy !== null)
128
+ row("checks", CHECKS_LABEL, `Passed on its own${sha === null ? "" : ` on ${sha}`}`, "ok", { href: checksHref, why: batchWhy });
129
+ else if (checks.status === "failed" && batchWhy !== null)
130
+ row("checks", CHECKS_LABEL, `Failed on its own${checks.exitCode === null ? "" : ` (exit ${checks.exitCode})`}`, problem(headline), headline === "Failed" ? { href: checksHref, why: batchWhy } : { action: { label: "See what failed", href: checksHref }, why: batchWhy });
98
131
  else if (checks.status === "passed")
99
132
  row("checks", CHECKS_LABEL, `${quick ? "Quick checks passed" : "Passed"}${sha === null ? "" : ` on ${sha}`}`, "ok", quick && runChecks !== null && headline !== "Complete" ? { href: checksHref, action: { label: "Run full checks", href: runChecks.href } } : { href: checksHref });
100
133
  else if (checks.status === "failed")
@@ -320,7 +353,8 @@ export function assignmentStatusFacts(assignment, options = {}) {
320
353
  const report = receipt?.completionKind === "research-report";
321
354
  const checks = receipt === null || !withResult ? null
322
355
  : { status: receipt.checks.status, exitCode: receipt.checks.exitCode, head: receipt.head,
323
- ...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }) };
356
+ ...(receipt.checks.level == null ? {} : { level: receipt.checks.level }), ...(receipt.checks.running == null ? {} : { running: receipt.checks.running }),
357
+ ...(receipt.checks.batch == null ? {} : { batch: receipt.checks.batch }) };
324
358
  const publication = assignment.publication;
325
359
  const pullRequest = options.pullRequest !== undefined ? options.pullRequest
326
360
  : publication === null ? (withResult && !report ? { state: "none", number: null, url: null, ci: null, error: null } : null)
@@ -32,6 +32,10 @@ function role(value) {
32
32
  refuse('invalid', 'Choose Viewer, Contributor or Manager.');
33
33
  return value;
34
34
  }
35
+ /** A failed turn's words for its message: none when the turn said why in the thread itself (it stopped at its deadline). */
36
+ function savedFailure(reason) {
37
+ return reason === 'timeout' || reason === 'crashed' ? null : String(reason ?? 'The saved turn failed.');
38
+ }
35
39
  function hash(value) { return createHash('sha256').update(JSON.stringify(value)).digest('hex'); }
36
40
  function stable(value) {
37
41
  if (Array.isArray(value))
@@ -461,7 +465,7 @@ export class TeamLeads {
461
465
  const uncertain = this.deliveryUncertain(Number(row['turn_id']));
462
466
  const cancelled = Number(row['stop_requested']) === 1 || !this.authorStillAllowed(row);
463
467
  const status = uncertain ? 'uncertain' : cancelled ? 'cancelled' : row['turn_state'] === 'answered' ? 'answered' : 'failed';
464
- const error = uncertain ? 'Delivery could not be confirmed. Inspect saved activity before continuing.' : cancelled ? 'Stopped or conversation access changed.' : status === 'failed' ? String(row['failure_reason'] ?? 'The saved turn failed.') : null;
468
+ const error = uncertain ? 'Delivery could not be confirmed. Inspect saved activity before continuing.' : cancelled ? 'Stopped or conversation access changed.' : status === 'failed' ? savedFailure(row['failure_reason']) : null;
465
469
  const result = this.db.prepare("UPDATE team_message SET status=?,error=?,generation=generation+1,revision=revision+1 WHERE message=? AND status='running' AND generation=?").run(status, error, Number(row['message']), Number(row['generation']));
466
470
  if (Number(result.changes) === 1) {
467
471
  changed++;
@@ -480,7 +484,7 @@ export class TeamLeads {
480
484
  for (const row of rows) {
481
485
  const turn = row['turn_id'] === null ? null : this.store.getMateTurn(Number(row['turn_id']));
482
486
  const status = turn?.state === 'answered' ? 'answered' : turn?.state === 'failed' && !this.deliveryUncertain(turn.id) ? 'failed' : 'uncertain';
483
- const error = status === 'uncertain' ? 'Delivery unconfirmed after the previous worker stopped. Inspect activity before continuing.' : status === 'failed' ? (turn?.failureReason ?? 'The saved turn failed.') : null;
487
+ const error = status === 'uncertain' ? 'Delivery unconfirmed after the previous worker stopped. Inspect activity before continuing.' : status === 'failed' ? savedFailure(turn?.failureReason) : null;
484
488
  this.db.prepare('UPDATE team_message SET status=?,generation=generation+1,revision=revision+1,error=? WHERE message=?').run(status, error, Number(row['message']));
485
489
  this.event(String(row['lead']), String(row['conversation']), status === 'uncertain' ? 'delivery-unconfirmed' : 'message-reconciled', String(row['author']), now);
486
490
  }
@@ -166,9 +166,10 @@ export function createTeamRuntime(options) {
166
166
  const state = admission(claim.actor, claim.conversationId);
167
167
  if (domain.defer(claim, result.message, clock()))
168
168
  waiting.set(claim.messageId, state.stamp);
169
+ // A turn that saved its outcome in the thread (stopped at its deadline) is said there, not again on the message.
169
170
  }
170
171
  else
171
- domain.finish(claim, { status: 'unknownSpend' in result && result.unknownSpend ? 'uncertain' : 'failed', error: result.message, ...('turn' in result ? { turnId: result.turn } : {}) }, clock());
172
+ domain.finish(claim, { status: 'unknownSpend' in result && result.unknownSpend ? 'uncertain' : 'failed', ...('saved' in result && result.saved ? {} : { error: result.message }), ...('turn' in result ? { turnId: result.turn } : {}) }, clock());
172
173
  }
173
174
  catch {
174
175
  // An exception may have followed native admission. Preserve an explicit
@@ -1,5 +1,13 @@
1
+ /** In a suite's temp root: who made it (scripts/suite-lifecycle.mjs). */
2
+ export declare const OWNER_FILE = ".toolroll-temp-owner";
3
+ export type TempOwner = {
4
+ pid: number;
5
+ startedAt: number;
6
+ };
1
7
  /** Name prefixes of what tests, journeys and scripts leave in the temp folder. */
2
8
  export declare const TEST_TEMP_PREFIXES: readonly string[];
9
+ /** Who made a temp root, from its owner file; null when it has none (older suites, scripts, Playwright). */
10
+ export declare function tempOwner(path: string): TempOwner | null;
3
11
  export declare function isTestTemp(name: string): boolean;
4
12
  /** Where they collect: the temp folder (and /tmp, where some scripts write). */
5
13
  export declare function tempRoots(): string[];
@@ -12,8 +20,18 @@ export declare function testTempFolders(roots: readonly string[], now: Date): {
12
20
  all: TempLeftover[];
13
21
  stale: TempLeftover[];
14
22
  };
15
- /** Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes. */
16
- export declare function removeStaleTestTemp(roots: readonly string[], now: Date): {
23
+ export type RemoveOptions = {
24
+ /** Whether a temp root's owner (by its owner file) is still running: its root stays, however old it looks. */
25
+ ownerAlive?: (owner: TempOwner) => boolean;
26
+ /** At most this many go in one pass (a sweep after weeks can find 100,000); `more` says some were left. */
27
+ max?: number;
28
+ };
29
+ /**
30
+ * Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
31
+ * folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone).
32
+ */
33
+ export declare function removeStaleTestTemp(roots: readonly string[], now: Date, options?: RemoveOptions): {
17
34
  removed: string[];
18
35
  failed: string[];
36
+ more: boolean;
19
37
  };
package/dist/test-temp.js CHANGED
@@ -1,16 +1,20 @@
1
1
  /**
2
- * Test temp folders (`toolroll storage`, `storage clean`): what the unit tests, journeys and scripts make in the
3
- * system temp folder. The tests delete their own (each run gets one temp root that its global teardown removes:
4
- * test/temp-root.ts); a run that was killed, and older versions, leave folders behind. `toolroll storage` counts them
5
- * and `storage clean` removes the ones nothing has touched for a day.
2
+ * Test temp folders (`toolroll storage`, `storage clean`, the plane's daily storage sweep): what the unit tests,
3
+ * journeys and scripts make in the system temp folder. The suites delete their own on pass, failure, timeout and
4
+ * interrupt (scripts/suite-lifecycle.mjs, test/temp-root.ts); a run killed outright, a proof that keeps its output and
5
+ * older versions leave folders behind. `toolroll storage` counts them; the daily sweep (storage-sweep.ts) and
6
+ * `storage clean` remove the ones nothing has touched for a day, never one whose owner is still running.
6
7
  *
7
8
  * Every prefix the tests and scripts give tmpdir() is covered here (test-temp.test.ts reads the sources to keep it
8
9
  * so), and Playwright's own browser profiles.
9
10
  */
10
- import { lstatSync, readdirSync, rmSync } from "node:fs";
11
+ import { spawnSync } from "node:child_process";
12
+ import { lstatSync, readFileSync, readdirSync, rmSync } from "node:fs";
11
13
  import { tmpdir } from "node:os";
12
- import { join } from "node:path";
14
+ import { dirname, join, resolve } from "node:path";
13
15
  const DAY_MS = 86_400_000;
16
+ /** In a suite's temp root: who made it (scripts/suite-lifecycle.mjs). */
17
+ export const OWNER_FILE = ".toolroll-temp-owner";
14
18
  /** Name prefixes of what tests, journeys and scripts leave in the temp folder. */
15
19
  export const TEST_TEMP_PREFIXES = [
16
20
  "so-", "standing-orders-", "toolroll-", "no-wt", "epoch", "cancel-floor",
@@ -18,10 +22,21 @@ export const TEST_TEMP_PREFIXES = [
18
22
  "flow-goal-", "flow-scratch", "flows-chat-", "invoke-attest-", "knowledge-test-", "learning-", "peek-", "phone-origin-",
19
23
  "proposal-repo-", "race-", "refresh-migration-", "refresh-refusal-", "release-check-", "shared-chat-", "skills-test-",
20
24
  "task-status-surfaces-", "template-repo-", "template-test-", "v14-migrate-", "v17-migrate-", "watch-anchor-",
21
- "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-", "sizing-repo-",
25
+ "needs-you-", "deps-lock-", "deps-state-", "upgrade-fresh-", "upgrade-path-", "deploy-restore-", "result-review-", "one-result-page-", "deploy-before-swap-", "one-vocabulary-", "accept-finishes-", "lead-promises-", "lead-people-", "sizing-repo-", "batch-checks-", "deps-identity-",
22
26
  // Playwright's browser profiles and artifacts.
23
27
  "playwright_chromiumdev_profile-", "playwright_firefoxdev_profile-", "playwright-artifacts-",
24
28
  ];
29
+ /** Who made a temp root, from its owner file; null when it has none (older suites, scripts, Playwright). */
30
+ export function tempOwner(path) {
31
+ try {
32
+ const parsed = JSON.parse(readFileSync(join(path, OWNER_FILE), "utf8"));
33
+ const startedAt = typeof parsed.startedAt === "string" ? Date.parse(parsed.startedAt) : NaN;
34
+ return Number.isSafeInteger(parsed.pid) && Number(parsed.pid) > 1 && Number.isFinite(startedAt) ? { pid: Number(parsed.pid), startedAt } : null;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
25
40
  export function isTestTemp(name) {
26
41
  return TEST_TEMP_PREFIXES.some(prefix => name.startsWith(prefix));
27
42
  }
@@ -79,20 +94,45 @@ export function testTempFolders(roots, now) {
79
94
  }
80
95
  return { all, stale: all.filter(one => now.getTime() - one.touchedAt > DAY_MS) };
81
96
  }
82
- /** Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes. */
83
- export function removeStaleTestTemp(roots, now) {
97
+ /**
98
+ * Remove the test temp folders nothing has touched for a day; each is looked at again right before it goes (still a
99
+ * folder of its own, not a link, still directly in its temp folder, still a day untouched, its owner gone).
100
+ */
101
+ export function removeStaleTestTemp(roots, now, options = {}) {
84
102
  const removed = [];
85
103
  const failed = [];
86
- for (const one of testTempFolders(roots, now).stale) {
104
+ const stale = testTempFolders(roots, now).stale;
105
+ const max = options.max ?? Infinity;
106
+ const inside = new Set(roots.map(root => resolve(root)));
107
+ for (const one of stale) {
108
+ if (removed.length + failed.length >= max)
109
+ return { removed, failed, more: true };
87
110
  try {
88
- if (now.getTime() - (lstatSync(one.path).isDirectory() ? touchedAt(one.path) : lstatSync(one.path).mtimeMs) <= DAY_MS)
111
+ const stat = lstatSync(one.path);
112
+ if (stat.isSymbolicLink() || !inside.has(resolve(dirname(one.path))))
113
+ continue;
114
+ if (now.getTime() - (stat.isDirectory() ? touchedAt(one.path) : stat.mtimeMs) <= DAY_MS)
115
+ continue;
116
+ const owner = stat.isDirectory() ? tempOwner(one.path) : null;
117
+ if (owner !== null && (options.ownerAlive ?? (() => false))(owner))
89
118
  continue;
90
- rmSync(one.path, { recursive: true, force: true, maxRetries: 3 });
119
+ removeTree(one.path);
91
120
  removed.push(one.path);
92
121
  }
93
122
  catch {
94
123
  failed.push(one.path);
95
124
  }
96
125
  }
97
- return { removed, failed };
126
+ return { removed, failed, more: false };
127
+ }
128
+ /** rm -r; a tree a test made read-only (a shared dependency copy, a locked fixture) is made writable first. */
129
+ function removeTree(path) {
130
+ try {
131
+ rmSync(path, { recursive: true, force: true, maxRetries: 3 });
132
+ return;
133
+ }
134
+ catch { /* read-only inside */ }
135
+ if (process.platform !== "win32")
136
+ spawnSync("chmod", ["-R", "u+w", path], { timeout: 120_000 });
137
+ rmSync(path, { recursive: true, force: true, maxRetries: 3 });
98
138
  }
@@ -36,6 +36,7 @@ import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, re
36
36
  import { installLaunchdService, launchdPlist, stopLaunchdService, writeFileDurably } from "./daemon.js";
37
37
  import { NAME } from "./names.js";
38
38
  import { isNewer, REGISTRY } from "./releases.js";
39
+ import { markNeverIndex } from "./never-index.js";
39
40
  /** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
40
41
  * and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
41
42
  function sqlite() {
@@ -699,6 +700,7 @@ function newJournal(kind, o, now) {
699
700
  const id = randomUUID(), stageDir = join(resolve(o.stateDir), "staged-upgrades", `${kind === "update" ? "release" : "rollback"}-${o.to.version}-${id.slice(0, 8)}`);
700
701
  mkdirSync(stageDir, { recursive: true, mode: 0o700 });
701
702
  chmodSync(stageDir, 0o700);
703
+ markNeverIndex(dirname(stageDir));
702
704
  return { version: 1, id, kind, stateDir: resolve(o.stateDir), databaseFile: o.databaseFile, stageDir, from: o.from, to: o.to, when: o.when, at: o.at, actor: o.actor, phase: "scheduled", detail: "", steps: [], startedAt: now.toISOString(), updatedAt: now.toISOString() };
703
705
  }
704
706
  function assertNoneActive(stateDir) {
@@ -371,7 +371,9 @@ function sentenceOf(text) {
371
371
  * the saved log. Null when the log printed nothing. */
372
372
  export function lastErrorLineOf(log) {
373
373
  const lines = log.split("\n");
374
- const printed = (text) => text.trim() !== "" && !/^\$ /.test(text) && !/^\(exit -?[0-9]+\)$/.test(text.trim()) && !/^--- (stdout|stderr) ---$/.test(text.trim());
374
+ const printed = (text) => text.trim() !== "" && !/^\$ /.test(text) && !/^\(exit -?[0-9]+\)$/.test(text.trim()) && !/^--- (stdout|stderr) ---$/.test(text.trim())
375
+ // A section's own header ("=== Project check · attempt 1 ===") is the log's, never the check's output.
376
+ && !/^=== .* ===$/.test(text.trim());
375
377
  const stderr = lines.findIndex(text => text.trim() === "--- stderr ---");
376
378
  const pick = (from, to) => {
377
379
  let last = null;
package/dist/worktree.js CHANGED
@@ -31,6 +31,7 @@ import { currentBootId, provenDeadByBootChange } from "./boot-identity.js";
31
31
  import { HANDOFF_PREFIX, MAILBOX_SUFFIX, looksLikeProtocolFile, readMailbox } from "./evidence.js";
32
32
  import { parseHandoff } from "./decision.js";
33
33
  import { diskBytes } from "./storage.js";
34
+ import { markNeverIndex } from "./never-index.js";
34
35
  /**
35
36
  * The note a lease leaves in the checkout, naming the process holding it.
36
37
  *
@@ -337,6 +338,14 @@ export class WorktreePool {
337
338
  const branchExists = request.base !== undefined && request.reuseBranch === true &&
338
339
  (await this.git(request.repo, ["rev-parse", "--verify", "--quiet", `refs/heads/${request.branch}`], READ_ONLY)).code === 0;
339
340
  const fromBase = request.base !== undefined && !branchExists;
341
+ // Spotlight leaves the checkouts alone: the marker sits in the pool and the project's folder, never inside a
342
+ // checkout, where a build's `git add -A` would commit it (never-index.ts).
343
+ try {
344
+ mkdirSync(dirname(path), { recursive: true });
345
+ }
346
+ catch { /* git reports it */ }
347
+ markNeverIndex(this.options.root);
348
+ markNeverIndex(dirname(path));
340
349
  const add = await this.git(request.repo, [
341
350
  "worktree",
342
351
  "add",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolroll",
3
- "version": "0.9.31",
3
+ "version": "0.9.33",
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"