atris 3.68.2 → 3.68.3

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.
@@ -93,7 +93,7 @@ Smoke: `reply with the word OK`, run from `$HOME`, 120s cap, one at a time on a
93
93
  | Claude | `claude -p "<prompt>" --model claude-opus-5-5 --output-format json --no-session-persistence` | `claude-opus-5-5` | 2026-09-23 | 20.3 | `--model opus` also resolves to `claude-opus-5-5` today; pin the id for reproducibility. |
94
94
  | Codex | `codex exec --dangerously-bypass-approvals-and-sandbox --ephemeral -m gpt-6-sol -c model_reasoning_effort=high -o <result-file> "<prompt>" </dev/null` (run from the target repo/worktree; read-only: swap the bypass flag for `--sandbox read-only`) | `gpt-6-sol`, high | 2026-09-23 | 13.8 | Keep the `</dev/null`: with stdin open codex prints "Reading additional input from stdin..." and waits forever (observed 2026-09-23). Outside a git repo add `--skip-git-repo-check` or it exits 1 (re-checked 2026-09-23). Final answer lands in the `-o` file. Run as a tracked background Bash task; in one-shot `claude -p` workers run it in the FOREGROUND, a one-shot session never wakes again (2026-08-11). `codex-companion.mjs task --background` is deprecated: its job store never notifies the session (2026-08-10). |
95
95
  | Cursor | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` (run from the target repo) | `composer-2.5` | 2026-09-23 | 37.5 | `--trust` required for non-interactive. |
96
- | Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5` | 2026-09-23 | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
96
+ | Fable | `atris engine fable --timeout 120 --json "<question>"` | `claude-fable-5-1` | not re-run on 5.1 (2026-09-23 on `claude-fable-5`) | 21.4 | Canonical read-only FABLE ask with receipt; scale `--timeout` to the work. |
97
97
  | Composer | `cursor-agent --trust --model composer-2.5 -p "<prompt>"` | `composer-2.5` via Cursor | 2026-09-23 | 37.5 | Same binary as Cursor. |
98
98
  | Haiku | `claude -p "<prompt>" --model claude-haiku-5-5 --output-format json --no-session-persistence` | `claude-haiku-5-5` | 2026-10-07 | 9.5 | Fast validation and bounded read-only checks. |
99
99
  | Devin (build) | `devin -p --permission-mode dangerous --model swe-2-max --prompt-file <brief>` (run from an isolated worktree) | `swe-2-max` (Free tag in `devin models list`, 2026-09-23) | 2026-09-23 | 14.0 | In `-p` mode the default permission mode rejects shell commands too, so even a read-only audit dies after one step (2026-09-23). Keep the brief outside the worktree. Long runs (5+ min) can print nothing even on success: judge by the worktree diff. `devin cloud` outlives this machine. |
@@ -2,7 +2,7 @@
2
2
  name: customer-lead
3
3
  role: Customer Lead
4
4
  description: Keeps customer commitments visible, spots retention risk, and turns evidence into the next useful follow-up.
5
- version: 1.0.0
5
+ version: 2.0.0
6
6
 
7
7
  skills:
8
8
  - customer-commitments
@@ -2,9 +2,10 @@
2
2
  name: executor
3
3
  role: Builder
4
4
  description: Execute from build specs, one step at a time
5
- version: 1.0.0
5
+ version: 2.0.0
6
6
 
7
7
  skills:
8
+ - isolated-slice-builder
8
9
  - code-writer
9
10
  - test-runner
10
11
 
@@ -40,16 +41,16 @@ tools: []
40
41
 
41
42
  ## Your Job
42
43
 
43
- When navigator hands you build.md:
44
+ When navigator or the orchestrator hands you a build spec or idea:
44
45
 
45
- 1. **Read build.md** — Exact files, steps, error cases
46
- 2. **Execute one step at a time** — Never batch multiple steps
47
- 3. **Show ASCII progress** — After each step, show what happened
48
- 4. **Wait for confirmation** — Human approves before next step
49
- 5. **Final summary** — When done, show ASCII completion status
50
- 6. **Keep wiki memory honest** — If the task changes durable project knowledge, update the relevant `atris/wiki/` page or note the source for immediate ingest
46
+ 1. **Read the spec and MAP.md**: Exact files, steps, and edge cases
47
+ 2. **Execute one bounded slice per loop**: Stay in the declared file scope
48
+ 3. **Prove with bare checks**: Run the real test command (`node --test ...`) and `git diff --check` before finishing
49
+ 4. **Pass cross-model review**: Fix any referee feedback before marking work ready for human review
50
+ 5. **Final summary**: Report exact files changed, check command run, and plain-language outcome
51
+ 6. **Keep wiki and member memory honest**: Append the lesson to `logs/<today>.md` and update `atris/MAP.md` when routes change
51
52
 
52
- **DO NOT skip steps. DO NOT batch. One shot at a time.**
53
+ **Never claim progress without a passing check. Stop for human approval only before destructive actions or out-of-scope refactors.**
53
54
 
54
55
  ---
55
56
 
@@ -114,15 +115,15 @@ If new evidence changes the task, stop and add a task note instead of forcing co
114
115
 
115
116
  ## Rules
116
117
 
117
- 1. **Read build.md first** — Never guess, always follow the spec
118
- 2. **One step at a time** — Show ASCII after each, wait for confirmation
119
- 3. **Check MAP.md** — Verify file paths exist, update if structure changed
120
- 4. **Run tests after changes** — Catch issues immediately
121
- 5. **Check wiki context** — Read `atris/wiki/STATUS.md` if present before executing in a known area
122
- 6. **No shortcuts** — Follow the build.md steps exactly
123
- 7. **Anti-slop aware** — Read `atris/policies/ANTISLOP.md` before writing. No sparkles, no filler, no purple prose.
124
- 8. **Stay in scope** — Only touch files listed in the task. If you need to change something outside scope, stop and flag it. That's a new task.
125
- 9. **If no exit condition, stop** — A task without a clear "done" definition is not ready for execution. Send it back to navigator.
118
+ 1. **Read build.md first**: Never guess, always follow the spec
119
+ 2. **One step at a time**: Show ASCII after each, wait for confirmation
120
+ 3. **Check MAP.md**: Verify file paths exist, update if structure changed
121
+ 4. **Run tests after changes**: Catch issues immediately
122
+ 5. **Check wiki context**: Read `atris/wiki/STATUS.md` if present before executing in a known area
123
+ 6. **No shortcuts**: Follow the build.md steps exactly
124
+ 7. **Anti-slop aware**: Read `atris/policies/ANTISLOP.md` before writing. No sparkles, no filler, no purple prose.
125
+ 8. **Stay in scope**: Only touch files listed in the task. If you need to change something outside scope, stop and flag it. That's a new task.
126
+ 9. **If no exit condition, stop**: A task without a clear "done" definition is not ready for execution. Send it back to navigator.
126
127
 
127
128
  ---
128
129
 
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: isolated-slice-builder
3
+ description: Build one bounded code slice in an isolated checkout, prove it with a bare test command and git diff --check, and update MAP.md before review.
4
+ version: 1.0.0
5
+ tags:
6
+ - build
7
+ - verification
8
+ - worktree
9
+ ---
10
+
11
+ # Isolated Slice Builder
12
+
13
+ Use this skill when building a feature slice or dispatched task in an isolated checkout.
14
+
15
+ ## Workflow
16
+
17
+ 1. Read `atris/MAP.md` to jump directly to the target `file:line` locations.
18
+ 2. Edit only the files required for the current slice, using Node built-ins only.
19
+ 3. Add or update a regression test that exercises the real runtime behavior.
20
+ 4. Run the bare test command (`node --test <file>`) and `git diff --check`.
21
+ 5. Update `atris/MAP.md` if any exported function or command route changed.
@@ -2,8 +2,10 @@
2
2
  name: navigator
3
3
  role: System Navigator
4
4
  description: Transform messy human intent into precise execution plans
5
- version: 1.0.0
5
+ version: 2.0.0
6
6
  agent-id: navigator-3
7
+ skills:
8
+ - loophole-proof-planner
7
9
  permissions:
8
10
  can-read: true
9
11
  can-plan: true
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: loophole-proof-planner
3
+ description: Turn messy operator intent into single-slice tasks with exact file:line pointers, falsifiable bare test commands, and explicit rollback plans.
4
+ version: 1.0.0
5
+ tags:
6
+ - planning
7
+ - navigation
8
+ - confidence-gate
9
+ ---
10
+
11
+ # Loophole Proof Planner
12
+
13
+ Use this skill when turning a broad idea or goal into an executable roadmap.
14
+
15
+ ## Workflow
16
+
17
+ 1. Read `atris/MAP.md` first to locate exact `file:line` references without blind searching.
18
+ 2. Split work touching 4 or more files into ordered single-slice tasks.
19
+ 3. Attach a bare runnable check command (`node --test <file>`) and a rollback step to every task.
20
+ 4. Close every loophole (missing owner, vague exit condition, unrunnable check) before handing off to `executor`.
@@ -2,9 +2,10 @@
2
2
  name: researcher
3
3
  role: Deep Researcher
4
4
  description: Find ground truth on any topic — competitors, standards, technologies, markets
5
- version: 1.0.0
5
+ version: 2.0.0
6
6
 
7
7
  skills:
8
+ - grounded-evidence-brief
8
9
  - deep-search
9
10
  - source-verification
10
11
 
@@ -0,0 +1,19 @@
1
+ ---
2
+ name: grounded-evidence-brief
3
+ description: Answer open technical or product questions using primary sources, exact file:line citations, and one actionable recommendation.
4
+ version: 1.0.0
5
+ tags:
6
+ - research
7
+ - evidence
8
+ - decision
9
+ ---
10
+
11
+ # Grounded Evidence Brief
12
+
13
+ Use this skill when investigating an unknown, comparing approaches, or benchmarking options before building.
14
+
15
+ ## Workflow
16
+
17
+ 1. Check local repo code and receipts first before external sources.
18
+ 2. Cite exact `file:line` paths or primary URLs for every factual claim.
19
+ 3. End with one concrete recommendation and the member that should execute it.
@@ -2,9 +2,10 @@
2
2
  name: validator
3
3
  role: Reviewer
4
4
  description: Validate execution, run tests, ensure quality before shipping
5
- version: 1.0.0
5
+ version: 2.0.0
6
6
 
7
7
  skills:
8
+ - cross-model-referee
8
9
  - test-runner
9
10
  - doc-updater
10
11
 
@@ -30,12 +31,6 @@ tools: []
30
31
 
31
32
  **Validation:** Run `npm test` to verify changes work correctly.
32
33
 
33
- ## Project Context
34
-
35
- **Project Type:** nodejs (nodejs)
36
-
37
- **Validation:** Run `npm test` to verify changes work correctly.
38
-
39
34
  ---
40
35
 
41
36
  ## MAPFIRST (Before ANY Validation)
@@ -0,0 +1,20 @@
1
+ ---
2
+ name: cross-model-referee
3
+ description: Audit worker diffs from an independent model perspective, run bare test commands, block slop or unverified claims, and emit actionable fix instructions.
4
+ version: 1.0.0
5
+ tags:
6
+ - review
7
+ - verification
8
+ - referee
9
+ ---
10
+
11
+ # Cross Model Referee
12
+
13
+ Use this skill when validating a plan before execution or auditing a worker diff before merge.
14
+
15
+ ## Workflow
16
+
17
+ 1. Inspect `git status` and `git diff` directly rather than trusting the worker's summary.
18
+ 2. Run the declared test check bare (`node --test <file>`) and verify zero new dependencies in `package.json`.
19
+ 3. Check that `atris/MAP.md` was updated if any file or command route changed.
20
+ 4. Emit a concrete pass verdict or bounce with the exact line and failure that must be fixed.
@@ -379,6 +379,7 @@ function sweepLanding(root, { ttlDays, staleHours, now = Date.now() } = {}) {
379
379
  remote: false,
380
380
  includeDetached: false,
381
381
  now,
382
+ by: 'atris autoland',
382
383
  });
383
384
  const afterBoard = collectBoard(root, { ttlDays, staleHours, now, light: true });
384
385
  const human = [];
@@ -926,7 +927,7 @@ async function runTickBody(root, { json, policy, receipt, engineValidationDeps =
926
927
  }
927
928
  try {
928
929
  const { cleanupWorktrees } = require('./worktree');
929
- const cleaned = cleanupWorktrees({ root, apply: true });
930
+ const cleaned = cleanupWorktrees({ root, apply: true, by: 'atris autoland' });
930
931
  receipt.worktrees_reaped = cleaned.removed.length;
931
932
  if (cleaned.removed.length > 0) receipt.worktrees_reaped_paths = cleaned.removed.map((w) => w.path);
932
933
  } catch (err) {
@@ -38,7 +38,7 @@ const { teamRosterView } = require('../lib/member-engine');
38
38
  const { engineModelText, engineRunsView, availableModels, modelLabel } = require('../lib/roster-models');
39
39
  const { renderTable } = require('../lib/text-table');
40
40
  const { TEAM_FOOTER } = require('../lib/team-lineup');
41
- const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
41
+ const { readRosterRuns, workerRuns, summarizeRuns, recentRuns, renderRunLine, runView, DEFAULT_DAYS: RUN_DAYS } = require('../lib/roster-runs');
42
42
  const { attachSuggestions } = require('../lib/roster-suggest');
43
43
  const { PIN_MAX_AGE_DAYS: PIN_STALE_DAYS, attachStaleModels, modelCatalog } = require('../lib/roster-stale');
44
44
  const {
@@ -1507,7 +1507,7 @@ const RUNS_LIST_LIMIT = 20;
1507
1507
  function runRosterRunsCommand(job, json, root, now) {
1508
1508
  const runs = recentRuns(readRosterRuns(root, { now, days: RUNS_LIST_DAYS }), { job, limit: RUNS_LIST_LIMIT });
1509
1509
  if (json) {
1510
- console.log(JSON.stringify({ job: job || null, days: RUNS_LIST_DAYS, runs }, null, 2));
1510
+ console.log(JSON.stringify({ job: job || null, days: RUNS_LIST_DAYS, runs: runs.map(runView) }, null, 2));
1511
1511
  return 0;
1512
1512
  }
1513
1513
  if (!runs.length) {
package/commands/goal.js CHANGED
@@ -168,9 +168,65 @@ function renderGoalView(view) {
168
168
  return lines.join('\n');
169
169
  }
170
170
 
171
+ // A goal that never becomes a mission run is a wish. `goal set` therefore also
172
+ // files the sentence through the normal wish capture path (capture only: no
173
+ // mission, no driver), so the goal -> wish -> mission -> run chain starts here.
174
+ // Wish capture is best-effort: it must never fail or undo the goal itself.
175
+ function wishNextStep(wish, goal) {
176
+ return wish.status === 'needs_input'
177
+ ? 'atris wish answer "your words"'
178
+ : `atris wish again ${wish.id} "${goal}"`;
179
+ }
180
+
181
+ function captureGoalWish(sentence, root) {
182
+ const goal = String(sentence || '').replace(/\s+/g, ' ').trim();
183
+ const { isFreshWorkspace } = require('../lib/first-minute');
184
+ if (isFreshWorkspace(root)) {
185
+ console.error('note: wish not filed, no atris/ workspace here. run: atris init');
186
+ return null;
187
+ }
188
+ const previous = process.cwd();
189
+ let wish = null;
190
+ try {
191
+ if (previous !== root) process.chdir(root);
192
+ // Re-setting the same goal must not stack duplicate wishes.
193
+ const { readWishes } = require('../lib/wish-store');
194
+ const open = readWishes(root).find((w) => (
195
+ normalizeText(w.text) === normalizeText(goal)
196
+ && ['captured', 'captured_no_mission', 'needs_input'].includes(w.status)
197
+ ));
198
+ if (open) {
199
+ console.log(`wish already filed: ${open.id}`);
200
+ console.log(`next ${wishNextStep(open, goal)}`);
201
+ return open;
202
+ }
203
+ const { runCapturedWish } = require('../lib/wish-delegate');
204
+ runCapturedWish(goal, root, {
205
+ noMission: true,
206
+ afterCapture: (captured) => { wish = captured; },
207
+ });
208
+ } catch (error) {
209
+ console.error(`warning: goal set, but wish capture failed: ${normalizeText(error?.message || error)}`);
210
+ return null;
211
+ } finally {
212
+ if (process.cwd() !== previous) process.chdir(previous);
213
+ }
214
+ if (!wish) return null;
215
+ let latest = wish;
216
+ try {
217
+ const { readWishes } = require('../lib/wish-store');
218
+ latest = readWishes(root).find((w) => w.id === wish.id) || wish;
219
+ } catch {
220
+ latest = wish;
221
+ }
222
+ console.log(`wish id ${wish.id}`);
223
+ console.log(`next ${wishNextStep(latest, goal)}`);
224
+ return latest;
225
+ }
226
+
171
227
  function printHelp() {
172
228
  console.log('usage: atris goal [--json]');
173
- console.log(' atris goal set "<sentence>"');
229
+ console.log(' atris goal set "<sentence>" [--no-wish]');
174
230
  console.log(' atris goal metric <name> <value>');
175
231
  console.log(' atris goal metric <name> --rm');
176
232
  }
@@ -187,7 +243,9 @@ function run(args = [], context = {}) {
187
243
  }
188
244
 
189
245
  if (command === 'set') {
190
- const goal = normalizeText(args.slice(1).join(' '));
246
+ const noWish = args.includes('--no-wish');
247
+ const sentence = args.slice(1).filter((arg) => arg !== '--no-wish').join(' ');
248
+ const goal = normalizeText(sentence);
191
249
  if (!goal) {
192
250
  console.error('error: goal sentence is required');
193
251
  return 2;
@@ -195,6 +253,7 @@ function run(args = [], context = {}) {
195
253
  const scoreboard = readScoreboard(root);
196
254
  writeScoreboard({ ...scoreboard, goal }, root);
197
255
  console.log('goal set.');
256
+ if (!noWish) captureGoalWish(sentence, root);
198
257
  return 0;
199
258
  }
200
259
 
package/commands/land.js CHANGED
@@ -2,7 +2,7 @@ const fs = require('fs');
2
2
  const path = require('path');
3
3
  const gitSpawn = require('../lib/git-spawn');
4
4
  const { compactErrorPayload, printCliJson } = require('../lib/cli-json');
5
- const { listWorktrees, statusCounts } = require('./worktree');
5
+ const { listWorktrees, statusCounts, defaultMainlineBase, listActiveProcessCwds, worktreeHasActiveProcess, recordWorktreeRemoval } = require('./worktree');
6
6
 
7
7
  const DEFAULT_TTL_DAYS = 7;
8
8
  const DEFAULT_STALE_HOURS = 48;
@@ -38,10 +38,11 @@ function refExists(root, ref) {
38
38
 
39
39
  function baseBranch(root, override = '') {
40
40
  if (override && refExists(root, override)) return override;
41
- if (refExists(root, 'origin/master')) return 'origin/master';
42
- if (refExists(root, 'origin/main')) return 'origin/main';
43
- if (refExists(root, 'master')) return 'master';
44
- if (refExists(root, 'main')) return 'main';
41
+ const mainline = defaultMainlineBase(root);
42
+ if (mainline !== 'HEAD') return mainline;
43
+ for (const candidate of ['main', 'master']) {
44
+ if (refExists(root, candidate)) return candidate;
45
+ }
45
46
  return '';
46
47
  }
47
48
 
@@ -309,7 +310,7 @@ function collectBoard(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_S
309
310
  const cherryCache = loadCherryCache(root);
310
311
  const baseSha = resolveSha(root, base);
311
312
  for (const branch of listBranches(root, base)) {
312
- if (PROTECTED_BRANCHES.has(branch.name)) continue;
313
+ if (PROTECTED_BRANCHES.has(branch.name) || branch.name === base.replace(/^origin\//, '')) continue;
313
314
  const lastCommitMs = Number(branch.lastCommitUnix) * 1000;
314
315
  if (Number.isFinite(lastCommitMs)) lastCommitMsByBranch.set(branch.name, lastCommitMs);
315
316
  const ahead = branch.ahead !== undefined ? branch.ahead : aheadCount(root, base, branch.name);
@@ -527,6 +528,15 @@ function salvageWorktree(w, dir, receipt) {
527
528
  }
528
529
  }
529
530
 
531
+ function reapReason(w, board, ttlDays, force) {
532
+ const state = (board.branches.find((b) => b.name === w.branch) || {}).state;
533
+ if (state === 'landed') return 'branch_landed';
534
+ if (state === 'due') return 'branch_past_ttl';
535
+ if (w.state === 'detached') return 'detached_head';
536
+ if (typeof w.ageDays === 'number' && w.ageDays > ttlDays) return 'worktree_past_ttl';
537
+ return force ? 'forced' : 'unlanded_commits';
538
+ }
539
+
530
540
  // Reap: salvage then delete everything landed (residue) or past TTL.
531
541
  // Salvage first, always: unlanded commits go into a git bundle, dirty
532
542
  // worktrees into patch files + untracked-file copies, so a reap is never
@@ -534,7 +544,9 @@ function salvageWorktree(w, dir, receipt) {
534
544
  // includeDetached: detached-HEAD worktrees have no branch, so the salvage
535
545
  // bundle cannot cover their commits, unattended reaps (autoland) pass false
536
546
  // and leave them for a human reap.
537
- function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now() } = {}) {
547
+ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOURS, base: baseOverride = '', dryRun = false, remote = true, includeDetached = true, protectCurrent = true, force = false, now = Date.now(), activeCwds: activeCwdsOverride, by = 'atris land --reap' } = {}) {
548
+ const readActiveCwds = () => (Array.isArray(activeCwdsOverride) ? activeCwdsOverride : listActiveProcessCwds());
549
+ let activeCwds = null;
538
550
  const board = collectBoard(root, { ttlDays, staleHours, base: baseOverride, now });
539
551
  const targets = board.branches.filter((b) => b.state === 'landed' || b.state === 'due');
540
552
  const targetNames = new Set(targets.map((b) => b.name));
@@ -554,6 +566,13 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
554
566
  protectedWorktrees.push({ ...w, reason: 'current_checkout' });
555
567
  continue;
556
568
  }
569
+ // A run working inside the folder outranks --force: removing it pulls
570
+ // the floor out from under a live engine.
571
+ if (!activeCwds) activeCwds = readActiveCwds();
572
+ if (worktreeHasActiveProcess(w.path, activeCwds)) {
573
+ protectedWorktrees.push({ ...w, reason: 'active_process' });
574
+ continue;
575
+ }
557
576
  if (worktreeWithinReapGrace(w.path, now)) {
558
577
  protectedWorktrees.push({ ...w, reason: 'fresh_worktree_grace' });
559
578
  continue;
@@ -642,7 +661,14 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
642
661
  const survivingWorktreeTargets = worktreeTargets.filter(
643
662
  (w) => targetNames.has(w.branch) || (includeDetached && w.state === 'detached')
644
663
  );
664
+ const activeAtRemoval = survivingWorktreeTargets.length ? readActiveCwds() : [];
645
665
  for (const w of survivingWorktreeTargets) {
666
+ // A run can start in the folder while the bundle is built; look again.
667
+ if (worktreeHasActiveProcess(w.path, activeAtRemoval)) {
668
+ receipt.keptWorktrees.push(`${w.path} (active_process)`);
669
+ if (w.branch) targetNames.delete(w.branch);
670
+ continue;
671
+ }
646
672
  // The grace check ran at scan time, and a bundle build can sit between it
647
673
  // and removal — an engine that booted mid-sweep refreshes the directory
648
674
  // mtime in that gap. Stat once more at removal time, the cheapest place
@@ -665,6 +691,7 @@ function reap(root, { ttlDays = DEFAULT_TTL_DAYS, staleHours = DEFAULT_STALE_HOU
665
691
  const removed = runGit(['worktree', 'remove', '--force', w.path], { cwd: root, check: false });
666
692
  if (removed.status === 0) {
667
693
  receipt.removedWorktrees.push(w.path);
694
+ recordWorktreeRemoval(root, { path: w.path, branch: w.branch, by, reason: reapReason(w, board, ttlDays, force) });
668
695
  } else {
669
696
  // A locked worktree fails here and, unreported, reap re-salvages the
670
697
  // same patches every pass forever. Say what blocked it so a human can