crewly 1.20.296 → 1.20.298

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 (95) hide show
  1. package/config/roles/developer/prompt.md +1 -1
  2. package/config/skills/agent/core/app-data/SKILL.md +13 -1
  3. package/config/skills/agent/core/app-data/execute.sh +60 -6
  4. package/config/skills/agent/core/app-data/execute.test.sh +42 -0
  5. package/config/skills/agent/core/docs-comment/SKILL.md +49 -4
  6. package/config/skills/agent/core/docs-comment/execute.sh +38 -2
  7. package/config/skills/agent/core/docs-comment/execute.test.sh +28 -0
  8. package/dist/backend/backend/src/constants.d.ts +48 -0
  9. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  10. package/dist/backend/backend/src/constants.js +48 -0
  11. package/dist/backend/backend/src/constants.js.map +1 -1
  12. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +2 -0
  13. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  14. package/dist/backend/backend/src/controllers/apps/apps.controller.js +2 -0
  15. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  16. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +1 -0
  17. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  18. package/dist/backend/backend/src/controllers/apps/apps.routes.js +3 -1
  19. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  20. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts +20 -0
  21. package/dist/backend/backend/src/controllers/browser/browser.controller.d.ts.map +1 -1
  22. package/dist/backend/backend/src/controllers/browser/browser.controller.js +52 -9
  23. package/dist/backend/backend/src/controllers/browser/browser.controller.js.map +1 -1
  24. package/dist/backend/backend/src/controllers/google/google.controller.d.ts +31 -0
  25. package/dist/backend/backend/src/controllers/google/google.controller.d.ts.map +1 -1
  26. package/dist/backend/backend/src/controllers/google/google.controller.js +198 -5
  27. package/dist/backend/backend/src/controllers/google/google.controller.js.map +1 -1
  28. package/dist/backend/backend/src/controllers/google/google.routes.d.ts +4 -0
  29. package/dist/backend/backend/src/controllers/google/google.routes.d.ts.map +1 -1
  30. package/dist/backend/backend/src/controllers/google/google.routes.js +9 -1
  31. package/dist/backend/backend/src/controllers/google/google.routes.js.map +1 -1
  32. package/dist/backend/backend/src/index.d.ts.map +1 -1
  33. package/dist/backend/backend/src/index.js +11 -0
  34. package/dist/backend/backend/src/index.js.map +1 -1
  35. package/dist/backend/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  36. package/dist/backend/backend/src/services/agent/agent-registration.service.js +4 -2
  37. package/dist/backend/backend/src/services/agent/agent-registration.service.js.map +1 -1
  38. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  39. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  40. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts +33 -0
  41. package/dist/backend/backend/src/services/apps/app-wake-message.d.ts.map +1 -1
  42. package/dist/backend/backend/src/services/apps/app-wake-message.js +48 -0
  43. package/dist/backend/backend/src/services/apps/app-wake-message.js.map +1 -1
  44. package/dist/backend/backend/src/services/apps/apps.service.d.ts +23 -0
  45. package/dist/backend/backend/src/services/apps/apps.service.d.ts.map +1 -1
  46. package/dist/backend/backend/src/services/apps/apps.service.js +57 -6
  47. package/dist/backend/backend/src/services/apps/apps.service.js.map +1 -1
  48. package/dist/backend/backend/src/services/google/docs-comment-gate.d.ts +166 -0
  49. package/dist/backend/backend/src/services/google/docs-comment-gate.d.ts.map +1 -0
  50. package/dist/backend/backend/src/services/google/docs-comment-gate.js +176 -0
  51. package/dist/backend/backend/src/services/google/docs-comment-gate.js.map +1 -0
  52. package/dist/backend/backend/src/services/google/docs-comments.service.d.ts.map +1 -1
  53. package/dist/backend/backend/src/services/google/docs-comments.service.js +2 -2
  54. package/dist/backend/backend/src/services/google/docs-comments.service.js.map +1 -1
  55. package/dist/backend/backend/src/services/google/google-api.client.d.ts +15 -0
  56. package/dist/backend/backend/src/services/google/google-api.client.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/google/google-api.client.js +16 -2
  58. package/dist/backend/backend/src/services/google/google-api.client.js.map +1 -1
  59. package/dist/backend/backend/src/services/google/google-reauth.wiring.js +1 -1
  60. package/dist/backend/backend/src/services/google/google-reauth.wiring.js.map +1 -1
  61. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts +12 -0
  62. package/dist/backend/backend/src/services/google/google-workspace-token.service.d.ts.map +1 -1
  63. package/dist/backend/backend/src/services/google/google-workspace-token.service.js +18 -0
  64. package/dist/backend/backend/src/services/google/google-workspace-token.service.js.map +1 -1
  65. package/dist/backend/backend/src/services/worktree/runaway-file-watcher.d.ts +131 -0
  66. package/dist/backend/backend/src/services/worktree/runaway-file-watcher.d.ts.map +1 -0
  67. package/dist/backend/backend/src/services/worktree/runaway-file-watcher.js +251 -0
  68. package/dist/backend/backend/src/services/worktree/runaway-file-watcher.js.map +1 -0
  69. package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts +61 -0
  70. package/dist/backend/backend/src/services/worktree/worktree-dirty.d.ts.map +1 -0
  71. package/dist/backend/backend/src/services/worktree/worktree-dirty.js +121 -0
  72. package/dist/backend/backend/src/services/worktree/worktree-dirty.js.map +1 -0
  73. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts +2 -0
  74. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -1
  75. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js +6 -1
  76. package/dist/backend/backend/src/services/worktree/worktree-janitor.git.js.map +1 -1
  77. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts +79 -5
  78. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js +198 -15
  80. package/dist/backend/backend/src/services/worktree/worktree-janitor.service.js.map +1 -1
  81. package/dist/backend/backend/src/services/worktree/worktree-landed.d.ts +76 -0
  82. package/dist/backend/backend/src/services/worktree/worktree-landed.d.ts.map +1 -0
  83. package/dist/backend/backend/src/services/worktree/worktree-landed.js +132 -0
  84. package/dist/backend/backend/src/services/worktree/worktree-landed.js.map +1 -0
  85. package/dist/backend/build-info.json +2 -2
  86. package/dist/cli/backend/src/constants.d.ts +48 -0
  87. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  88. package/dist/cli/backend/src/constants.js +48 -0
  89. package/dist/cli/backend/src/constants.js.map +1 -1
  90. package/dist/cli/backend/src/services/agent/agent-registration.service.d.ts.map +1 -1
  91. package/dist/cli/backend/src/services/agent/agent-registration.service.js +4 -2
  92. package/dist/cli/backend/src/services/agent/agent-registration.service.js.map +1 -1
  93. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -1
  94. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  95. package/package.json +1 -1
@@ -33,15 +33,23 @@
33
33
  * dir's index / HEAD / logs/HEAD) more than 2 hours ago in a known agent
34
34
  * location (`<main>/.claude/worktrees/`, `<tmp>/crewly-worktrees/`, or a
35
35
  * `worktree-agent-*` branch), more than 24 hours ago anywhere else.
36
- * 6. `git status --porcelain --untracked-files=all` is empty: no tracked
37
- * change and no untracked file. Ignored files (node_modules, dist) do not
38
- * count. A failing status keeps it.
36
+ * 6. Nothing but regenerable output is uncommitted: `git status --porcelain`
37
+ * shows no tracked change and no untracked file, ignoring untracked
38
+ * `node_modules` / `.next` / `dist` / `build` / `.turbo` / `coverage` /
39
+ * `.cache` / `logs` directories and `*.log` files (nested packages rarely
40
+ * ignore their own). A failing status keeps it.
39
41
  * 7. Its work has landed: HEAD is an ancestor of `origin/<default branch>`
40
42
  * after a quiet `git fetch` (fetch errors ignored), OR — when `gh` is
41
43
  * installed and authenticated — a MERGED PR has this branch as head AND
42
44
  * its head commit equals the worktree HEAD (squash merges; commits made
43
45
  * after the merge keep the worktree). A repo without an origin (not on
44
- * GitHub) only has the ancestor check, so its worktrees are kept.
46
+ * GitHub) only has the ancestor check, so its worktrees are kept — OR, for
47
+ * any git host (Bitbucket, GitLab, squash merges), its CONTENT is already in
48
+ * `origin/<default>`: merging HEAD into it changes nothing
49
+ * (`git merge-tree --write-tree`, falling back to `git cherry`), or the
50
+ * branch's combined diff is patch-equivalent to one commit the default
51
+ * branch gained since (a squash commit). See worktree-landed.ts. Work in
52
+ * none of these is never removed. `gh` is not asked about non-GitHub remotes.
45
53
  *
46
54
  * ## Removal
47
55
  * `git worktree remove <path>` without `--force`. If git refuses although
@@ -49,6 +57,17 @@
49
57
  * no submodules, it is retried once with `--force`. Then the local branch is
50
58
  * deleted (never a protected name), and `git worktree prune` runs per repo.
51
59
  *
60
+ * ## Low disk: kept worktrees
61
+ * Under low disk, a worktree kept as `dirty` or `not-merged` and idle for
62
+ * 24 h+ still loses its `node_modules` / `.next` directories (reversible:
63
+ * reinstall / rebuild); the verdict carries `reclaimed` and the summary
64
+ * `reclaimedBytes`.
65
+ *
66
+ * ## Work completion
67
+ * A WorkItem reaching done / verified calls `requestTidy()`: one debounced pass
68
+ * (2 min) with a 10 min idle threshold, so a finished worktree is removed as
69
+ * soon as its change has landed, not at the next 30-minute tick.
70
+ *
52
71
  * ## Scratch sweep
53
72
  * After the worktrees, stale Claude Code session dirs
54
73
  * (`<tmp>/claude-<uid>/<slug>/<uuid>/`) are deleted when idle for 3 days,
@@ -64,6 +83,15 @@
64
83
  * the janitor left and why; below 5 GB the notice is urgent and repeats at
65
84
  * most every 6 h.
66
85
  *
86
+ * ## Runaway files
87
+ * Once a minute a watcher scans agent scratch dirs and worktrees for a single
88
+ * file that grows without bound (a 40 GB `all.md` filled a disk, crewly#1078):
89
+ * a file over 5 GB that is still growing, or any file growing over 1 GB/min,
90
+ * has its writer stopped, is cut to its first 1 MB, the agent working there is
91
+ * told and the owner is alerted once through the low-disk notifier. A big file
92
+ * that is not growing is only reported. See
93
+ * {@link module:services/worktree/runaway-file-watcher}.
94
+ *
67
95
  * Runs every 30 minutes (first run 10 minutes after boot). Kill switch:
68
96
  * `CREWLY_WORKTREE_JANITOR=0` (also stops the low-disk guard). Never throws;
69
97
  * logs one summary line per sweep.
@@ -73,12 +101,13 @@
73
101
  import { type StatFsFn } from './low-disk-guard.js';
74
102
  import { type ScratchSweepSummary } from './scratch-janitor.js';
75
103
  import { type DockerReclaimResult } from './docker-reclaim.js';
104
+ import { type RunawayOutcome } from './runaway-file-watcher.js';
76
105
  import { type TraceDiskResult } from '../trace/trace-store.js';
77
106
  /**
78
107
  * Why a worktree is kept, or why it may go. (`not-agent-worktree` is retired:
79
108
  * location no longer decides eligibility, only the idle threshold.)
80
109
  */
81
- export type WorktreeReason = 'main-worktree' | 'bare' | 'prunable' | 'locked' | 'managed-by-workitem-worktrees' | 'agent-session-inside' | 'process-inside' | 'cwd-probe-failed' | 'recent' | 'dirty' | 'status-failed' | 'not-merged' | 'merged' | 'pr-merged';
110
+ export type WorktreeReason = 'main-worktree' | 'bare' | 'prunable' | 'locked' | 'managed-by-workitem-worktrees' | 'agent-session-inside' | 'process-inside' | 'cwd-probe-failed' | 'recent' | 'dirty' | 'status-failed' | 'not-merged' | 'merged' | 'pr-merged' | 'content-landed';
82
111
  /** Verdict for one worktree (the dry-run output). */
83
112
  export interface WorktreeVerdict {
84
113
  /** Main worktree of the repo this worktree belongs to */
@@ -97,6 +126,11 @@ export interface WorktreeVerdict {
97
126
  detail?: string;
98
127
  /** Ms since last touched, when computed */
99
128
  idleMs?: number;
129
+ /** Low disk only: regenerable dirs (node_modules, .next) of a kept worktree that were (or, in a dry run, would be) deleted */
130
+ reclaimed?: {
131
+ dirs: string[];
132
+ freedBytes: number;
133
+ };
100
134
  }
101
135
  /** Outcome of acting on one verdict. */
102
136
  export interface WorktreeOutcome extends WorktreeVerdict {
@@ -127,6 +161,8 @@ export interface JanitorRunSummary {
127
161
  wouldRemove: number;
128
162
  /** Worktrees kept */
129
163
  kept: number;
164
+ /** Bytes freed by deleting regenerable dirs (node_modules, .next) from kept worktrees under low disk */
165
+ reclaimedBytes: number;
130
166
  /** Kept worktrees by reason (removal failures under `remove-failed`) */
131
167
  keptReasons: Record<string, number>;
132
168
  /** Per-worktree results */
@@ -203,6 +239,8 @@ export interface WorktreeJanitorOptions {
203
239
  statePath?: string | null;
204
240
  /** Docker reclaim on low disk (default: the real one, except under jest); null = never */
205
241
  reclaimDocker?: (() => Promise<DockerReclaimResult>) | null;
242
+ /** Debounce before a completion-triggered pass (default COMPLETION_TIDY_DELAY_MS) */
243
+ tidyDelayMs?: number;
206
244
  /** Environment for the kill switch (default process.env) */
207
245
  env?: NodeJS.ProcessEnv;
208
246
  /** Logger (default: component logger `WorktreeJanitor`) */
@@ -234,6 +272,11 @@ export declare class WorktreeJanitorService {
234
272
  private lastPassAt;
235
273
  private memoryNoticeState;
236
274
  private notifier;
275
+ private agentNotifier;
276
+ private runawayTimer;
277
+ private tidyTimer;
278
+ private runawayWatcher;
279
+ private runawayInFlight;
237
280
  /**
238
281
  * @param options - Injectable dependencies; defaults use the live backend
239
282
  */
@@ -245,6 +288,22 @@ export declare class WorktreeJanitorService {
245
288
  * @param notifier - Sender, or null to disable notices
246
289
  */
247
290
  setLowDiskNotifier(notifier: LowDiskNotifier | null): void;
291
+ /**
292
+ * Set how an agent is told its working dir had a runaway file (the server
293
+ * wires the normal agent message delivery here).
294
+ *
295
+ * @param notifier - Sender (true when delivered), or null to skip agent notices
296
+ */
297
+ setAgentNotifier(notifier: ((sessionName: string, message: string) => Promise<boolean>) | null): void;
298
+ /**
299
+ * One runaway-file scan now (also what the minute timer runs). Never
300
+ * throws; overlapping scans are skipped. Honours the kill switch.
301
+ *
302
+ * @returns What was flagged and done (empty when nothing is wrong)
303
+ */
304
+ checkRunawayFiles(): Promise<RunawayOutcome[]>;
305
+ /** Sessions whose cwd holds `file`, or sits in the same scratch session dir; told via the agent notifier. */
306
+ private notifyAgentsNear;
248
307
  /**
249
308
  * Process-wide instance used by the server and the HTTP routes.
250
309
  *
@@ -301,6 +360,15 @@ export declare class WorktreeJanitorService {
301
360
  * @returns Summary with per-worktree verdicts
302
361
  */
303
362
  plan(): Promise<JanitorRunSummary>;
363
+ /**
364
+ * Work just finished (a WorkItem / ticket reached done or verified): run one
365
+ * pass soon, so a finished worktree goes away when its PR is merged instead
366
+ * of waiting for the next 30-minute tick. Bursts are debounced into one pass
367
+ * after COMPLETION_TIDY_DELAY_MS; the idle threshold is COMPLETION_MIN_IDLE_MS.
368
+ * Only worktrees that pass every safety rule are removed (landed, clean, nobody
369
+ * inside) — unmerged work stays for the next pass. No-op when disabled. Never throws.
370
+ */
371
+ requestTidy(): void;
304
372
  /**
305
373
  * One real pass. Concurrent calls share the pass in flight. Never throws.
306
374
  * Honours the kill switch.
@@ -334,6 +402,12 @@ export declare class WorktreeJanitorService {
334
402
  * Apply the removal rules to one worktree (read-only apart from fetch).
335
403
  */
336
404
  private evaluate;
405
+ /**
406
+ * Low disk: delete the regenerable dirs (node_modules, .next) of a kept
407
+ * worktree that has been idle for a day. Reversible; never touches
408
+ * tracked or source files. Never throws.
409
+ */
410
+ private reclaimKept;
337
411
  /** `origin/<default>` after one quiet fetch per repo per pass; null when there is no origin. */
338
412
  private defaultRef;
339
413
  /** Number of a MERGED PR whose head is `branch` at exactly `head`, or null. */
@@ -1 +1 @@
1
- {"version":3,"file":"worktree-janitor.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/worktree/worktree-janitor.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"worktree-janitor.service.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/worktree/worktree-janitor.service.ts"],"names":[],"mappings":"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"}
@@ -33,15 +33,23 @@
33
33
  * dir's index / HEAD / logs/HEAD) more than 2 hours ago in a known agent
34
34
  * location (`<main>/.claude/worktrees/`, `<tmp>/crewly-worktrees/`, or a
35
35
  * `worktree-agent-*` branch), more than 24 hours ago anywhere else.
36
- * 6. `git status --porcelain --untracked-files=all` is empty: no tracked
37
- * change and no untracked file. Ignored files (node_modules, dist) do not
38
- * count. A failing status keeps it.
36
+ * 6. Nothing but regenerable output is uncommitted: `git status --porcelain`
37
+ * shows no tracked change and no untracked file, ignoring untracked
38
+ * `node_modules` / `.next` / `dist` / `build` / `.turbo` / `coverage` /
39
+ * `.cache` / `logs` directories and `*.log` files (nested packages rarely
40
+ * ignore their own). A failing status keeps it.
39
41
  * 7. Its work has landed: HEAD is an ancestor of `origin/<default branch>`
40
42
  * after a quiet `git fetch` (fetch errors ignored), OR — when `gh` is
41
43
  * installed and authenticated — a MERGED PR has this branch as head AND
42
44
  * its head commit equals the worktree HEAD (squash merges; commits made
43
45
  * after the merge keep the worktree). A repo without an origin (not on
44
- * GitHub) only has the ancestor check, so its worktrees are kept.
46
+ * GitHub) only has the ancestor check, so its worktrees are kept — OR, for
47
+ * any git host (Bitbucket, GitLab, squash merges), its CONTENT is already in
48
+ * `origin/<default>`: merging HEAD into it changes nothing
49
+ * (`git merge-tree --write-tree`, falling back to `git cherry`), or the
50
+ * branch's combined diff is patch-equivalent to one commit the default
51
+ * branch gained since (a squash commit). See worktree-landed.ts. Work in
52
+ * none of these is never removed. `gh` is not asked about non-GitHub remotes.
45
53
  *
46
54
  * ## Removal
47
55
  * `git worktree remove <path>` without `--force`. If git refuses although
@@ -49,6 +57,17 @@
49
57
  * no submodules, it is retried once with `--force`. Then the local branch is
50
58
  * deleted (never a protected name), and `git worktree prune` runs per repo.
51
59
  *
60
+ * ## Low disk: kept worktrees
61
+ * Under low disk, a worktree kept as `dirty` or `not-merged` and idle for
62
+ * 24 h+ still loses its `node_modules` / `.next` directories (reversible:
63
+ * reinstall / rebuild); the verdict carries `reclaimed` and the summary
64
+ * `reclaimedBytes`.
65
+ *
66
+ * ## Work completion
67
+ * A WorkItem reaching done / verified calls `requestTidy()`: one debounced pass
68
+ * (2 min) with a 10 min idle threshold, so a finished worktree is removed as
69
+ * soon as its change has landed, not at the next 30-minute tick.
70
+ *
52
71
  * ## Scratch sweep
53
72
  * After the worktrees, stale Claude Code session dirs
54
73
  * (`<tmp>/claude-<uid>/<slug>/<uuid>/`) are deleted when idle for 3 days,
@@ -64,6 +83,15 @@
64
83
  * the janitor left and why; below 5 GB the notice is urgent and repeats at
65
84
  * most every 6 h.
66
85
  *
86
+ * ## Runaway files
87
+ * Once a minute a watcher scans agent scratch dirs and worktrees for a single
88
+ * file that grows without bound (a 40 GB `all.md` filled a disk, crewly#1078):
89
+ * a file over 5 GB that is still growing, or any file growing over 1 GB/min,
90
+ * has its writer stopped, is cut to its first 1 MB, the agent working there is
91
+ * told and the owner is alerted once through the low-disk notifier. A big file
92
+ * that is not growing is only reported. See
93
+ * {@link module:services/worktree/runaway-file-watcher}.
94
+ *
67
95
  * Runs every 30 minutes (first run 10 minutes after boot). Kill switch:
68
96
  * `CREWLY_WORKTREE_JANITOR=0` (also stops the low-disk guard). Never throws;
69
97
  * logs one summary line per sweep.
@@ -80,8 +108,11 @@ import { safeReadJson, modifyJsonFile } from '../../utils/file-io.utils.js';
80
108
  import { buildLowDiskNotice, diskLevel, idleThreshold, noticeToSend, readFreeBytes, recordNotice, } from './low-disk-guard.js';
81
109
  import { defaultScratchRoots, sweepScratch } from './scratch-janitor.js';
82
110
  import { reclaimDocker } from './docker-reclaim.js';
111
+ import { RunawayFileWatcher, runawayWatchRoots } from './runaway-file-watcher.js';
83
112
  import { getTraceStore } from '../trace/trace-store.js';
84
113
  import { canonicalPath, diskUsageBytes, discoverLinkedWorktreeRepos, formatBytes, isJanitorDisabled, isPathInside, latestWorktreeMtimeMs, listProcessCwds, parsePorcelainWorktrees, runCommand, } from './worktree-janitor.git.js';
114
+ import { contentLandedSignal, isNonGithubRemote } from './worktree-landed.js';
115
+ import { dirtyStatusLines, reclaimRegenerableDirs } from './worktree-dirty.js';
85
116
  /** Milliseconds per minute (for the `recent` detail text). */
86
117
  const MS_PER_MINUTE = 60 * 1000;
87
118
  /**
@@ -110,6 +141,11 @@ export class WorktreeJanitorService {
110
141
  lastPassAt = null;
111
142
  memoryNoticeState = {};
112
143
  notifier;
144
+ agentNotifier = null;
145
+ runawayTimer = null;
146
+ tidyTimer = null;
147
+ runawayWatcher = null;
148
+ runawayInFlight = false;
113
149
  /**
114
150
  * @param options - Injectable dependencies; defaults use the live backend
115
151
  */
@@ -126,6 +162,69 @@ export class WorktreeJanitorService {
126
162
  setLowDiskNotifier(notifier) {
127
163
  this.notifier = notifier;
128
164
  }
165
+ /**
166
+ * Set how an agent is told its working dir had a runaway file (the server
167
+ * wires the normal agent message delivery here).
168
+ *
169
+ * @param notifier - Sender (true when delivered), or null to skip agent notices
170
+ */
171
+ setAgentNotifier(notifier) {
172
+ this.agentNotifier = notifier;
173
+ }
174
+ /**
175
+ * One runaway-file scan now (also what the minute timer runs). Never
176
+ * throws; overlapping scans are skipped. Honours the kill switch.
177
+ *
178
+ * @returns What was flagged and done (empty when nothing is wrong)
179
+ */
180
+ async checkRunawayFiles() {
181
+ if (this.isDisabled() || this.runawayInFlight)
182
+ return [];
183
+ this.runawayInFlight = true;
184
+ try {
185
+ if (!this.runawayWatcher) {
186
+ this.runawayWatcher = new RunawayFileWatcher({
187
+ listRoots: async () => {
188
+ const repos = await (this.opts.listRepoPaths ?? defaultListRepoPaths)().catch(() => []);
189
+ return runawayWatchRoots(this.scratchRoots(), this.tmpRoots(), repos);
190
+ },
191
+ notifyOwner: async (notice) => (this.notifier ? this.notifier(notice) : false),
192
+ notifyAgents: (file, message) => this.notifyAgentsNear(file, message),
193
+ now: () => this.now(),
194
+ logger: this.logger,
195
+ });
196
+ }
197
+ return await this.runawayWatcher.scan();
198
+ }
199
+ finally {
200
+ this.runawayInFlight = false;
201
+ }
202
+ }
203
+ /** Sessions whose cwd holds `file`, or sits in the same scratch session dir; told via the agent notifier. */
204
+ async notifyAgentsNear(file, message) {
205
+ const notifier = this.agentNotifier;
206
+ if (!notifier)
207
+ return [];
208
+ const told = [];
209
+ try {
210
+ const { getSessionBackendSync } = await import('../session/session-backend.factory.js');
211
+ const backend = getSessionBackendSync();
212
+ const unit = /^(.*[\\/]claude-\d+[\\/][^\\/]+[\\/][0-9a-f-]{36})(?:[\\/]|$)/i.exec(file)?.[1];
213
+ for (const name of backend?.listSessions() ?? []) {
214
+ const cwd = backend?.getSession(name)?.cwd;
215
+ if (!cwd)
216
+ continue;
217
+ if (isPathInside(file, cwd) || (unit && isPathInside(cwd, unit))) {
218
+ if (await notifier(name, message).catch(() => false))
219
+ told.push(name);
220
+ }
221
+ }
222
+ }
223
+ catch {
224
+ // no session backend — the owner alert still goes out
225
+ }
226
+ return told;
227
+ }
129
228
  /**
130
229
  * Process-wide instance used by the server and the HTTP routes.
131
230
  *
@@ -168,6 +267,8 @@ export class WorktreeJanitorService {
168
267
  this.firstRunTimer.unref?.();
169
268
  this.diskTimer = setInterval(() => void this.checkDisk(), WORKTREE_JANITOR_CONSTANTS.LOW_DISK_CHECK_INTERVAL_MS);
170
269
  this.diskTimer.unref?.();
270
+ this.runawayTimer = setInterval(() => void this.checkRunawayFiles(), WORKTREE_JANITOR_CONSTANTS.RUNAWAY_CHECK_INTERVAL_MS);
271
+ this.runawayTimer.unref?.();
171
272
  return true;
172
273
  }
173
274
  /** Cancel scheduled passes. */
@@ -178,6 +279,12 @@ export class WorktreeJanitorService {
178
279
  clearInterval(this.intervalTimer);
179
280
  if (this.diskTimer)
180
281
  clearInterval(this.diskTimer);
282
+ if (this.runawayTimer)
283
+ clearInterval(this.runawayTimer);
284
+ if (this.tidyTimer)
285
+ clearTimeout(this.tidyTimer);
286
+ this.tidyTimer = null;
287
+ this.runawayTimer = null;
181
288
  this.firstRunTimer = null;
182
289
  this.intervalTimer = null;
183
290
  this.diskTimer = null;
@@ -225,6 +332,41 @@ export class WorktreeJanitorService {
225
332
  async plan() {
226
333
  return this.pass(true);
227
334
  }
335
+ /**
336
+ * Work just finished (a WorkItem / ticket reached done or verified): run one
337
+ * pass soon, so a finished worktree goes away when its PR is merged instead
338
+ * of waiting for the next 30-minute tick. Bursts are debounced into one pass
339
+ * after COMPLETION_TIDY_DELAY_MS; the idle threshold is COMPLETION_MIN_IDLE_MS.
340
+ * Only worktrees that pass every safety rule are removed (landed, clean, nobody
341
+ * inside) — unmerged work stays for the next pass. No-op when disabled. Never throws.
342
+ */
343
+ requestTidy() {
344
+ if (this.isDisabled() || this.tidyTimer)
345
+ return;
346
+ this.tidyTimer = setTimeout(() => {
347
+ this.tidyTimer = null;
348
+ void (async () => {
349
+ try {
350
+ if (this.inFlight)
351
+ await this.inFlight.catch(() => undefined);
352
+ if (this.isDisabled())
353
+ return;
354
+ this.inFlight = this.pass(false, WORKTREE_JANITOR_CONSTANTS.COMPLETION_MIN_IDLE_MS).finally(() => {
355
+ this.inFlight = null;
356
+ });
357
+ const summary = await this.inFlight;
358
+ if (!summary.disabled) {
359
+ this.lastSummary = summary;
360
+ this.lastPassAt = this.now();
361
+ }
362
+ }
363
+ catch (err) {
364
+ this.logger.warn('Completion tidy failed (non-fatal)', { error: err instanceof Error ? err.message : String(err) });
365
+ }
366
+ })();
367
+ }, this.opts.tidyDelayMs ?? WORKTREE_JANITOR_CONSTANTS.COMPLETION_TIDY_DELAY_MS);
368
+ this.tidyTimer.unref?.();
369
+ }
228
370
  /**
229
371
  * One real pass. Concurrent calls share the pass in flight. Never throws.
230
372
  * Honours the kill switch.
@@ -379,7 +521,7 @@ export class WorktreeJanitorService {
379
521
  git(args, options = {}) {
380
522
  return runCommand(this.opts.gitBin ?? 'git', args, options);
381
523
  }
382
- async pass(dryRun) {
524
+ async pass(dryRun, idleOverrideMs) {
383
525
  const started = Date.now();
384
526
  const summary = {
385
527
  startedAt: new Date(started).toISOString(),
@@ -390,6 +532,7 @@ export class WorktreeJanitorService {
390
532
  removed: 0,
391
533
  wouldRemove: 0,
392
534
  kept: 0,
535
+ reclaimedBytes: 0,
393
536
  keptReasons: {},
394
537
  worktrees: [],
395
538
  lowDisk: false,
@@ -411,7 +554,14 @@ export class WorktreeJanitorService {
411
554
  for (const repo of repos) {
412
555
  let removedHere = 0;
413
556
  for (const entry of repo.entries) {
414
- const verdict = await this.evaluate(repo, entry, busy, summary.lowDisk);
557
+ const verdict = await this.evaluate(repo, entry, busy, summary.lowDisk, idleOverrideMs);
558
+ if (summary.lowDisk && verdict.decision === 'keep' && (verdict.reason === 'dirty' || verdict.reason === 'not-merged')) {
559
+ const rec = await this.reclaimKept(entry.path, verdict.idleMs, dryRun);
560
+ if (rec) {
561
+ verdict.reclaimed = rec;
562
+ summary.reclaimedBytes += rec.freedBytes;
563
+ }
564
+ }
415
565
  const outcome = { ...verdict, removed: false, forced: false, branchDeleted: false };
416
566
  if (verdict.decision === 'remove' && !dryRun) {
417
567
  await this.remove(repo, outcome);
@@ -456,7 +606,7 @@ export class WorktreeJanitorService {
456
606
  .sort((a, b) => b[1] - a[1])
457
607
  .map(([k, v]) => `${k}: ${v}`)
458
608
  .join(', ');
459
- this.logger.info(`Worktree janitor: removed ${summary.removed}, kept ${summary.kept}${reasons ? ` (${reasons})` : ''} across ${summary.repos.length} repo(s) in ${summary.durationMs}ms`, { removed: summary.worktrees.filter((w) => w.removed).map((w) => w.path), lowDisk: summary.lowDisk });
609
+ this.logger.info(`Worktree janitor: removed ${summary.removed}, kept ${summary.kept}${reasons ? ` (${reasons})` : ''}${summary.reclaimedBytes > 0 ? `, reclaimed ${formatBytes(summary.reclaimedBytes)} of node_modules/.next from kept worktrees` : ''} across ${summary.repos.length} repo(s) in ${summary.durationMs}ms`, { removed: summary.worktrees.filter((w) => w.removed).map((w) => w.path), lowDisk: summary.lowDisk });
460
610
  const sc = summary.scratch;
461
611
  if (sc) {
462
612
  const scReasons = Object.entries(sc.keptReasons)
@@ -496,7 +646,7 @@ export class WorktreeJanitorService {
496
646
  const key = canonicalPath(main.path);
497
647
  if (seen.has(key))
498
648
  continue;
499
- seen.set(key, { main: main.path, entries, fetched: false, defaultRef: undefined });
649
+ seen.set(key, { main: main.path, entries, fetched: false, defaultRef: undefined, landed: {} });
500
650
  }
501
651
  return [...seen.values()];
502
652
  }
@@ -522,7 +672,7 @@ export class WorktreeJanitorService {
522
672
  /**
523
673
  * Apply the removal rules to one worktree (read-only apart from fetch).
524
674
  */
525
- async evaluate(repo, entry, busy, lowDisk) {
675
+ async evaluate(repo, entry, busy, lowDisk, idleOverrideMs) {
526
676
  const base = { repo: repo.main, path: entry.path, branch: entry.branch, head: entry.head };
527
677
  const keep = (reason, detail, idleMs) => ({
528
678
  ...base,
@@ -555,14 +705,16 @@ export class WorktreeJanitorService {
555
705
  const normalIdle = agentLocation
556
706
  ? (this.opts.minIdleMs ?? WORKTREE_JANITOR_CONSTANTS.MIN_IDLE_MS)
557
707
  : (this.opts.minIdleOtherMs ?? WORKTREE_JANITOR_CONSTANTS.MIN_IDLE_OTHER_MS);
558
- const minIdle = idleThreshold(normalIdle, lowDisk);
708
+ const minIdle = idleOverrideMs !== undefined ? Math.min(idleOverrideMs, idleThreshold(normalIdle, lowDisk)) : idleThreshold(normalIdle, lowDisk);
559
709
  if (idleMs === undefined || idleMs < minIdle) {
560
710
  return keep('recent', `needs ${Math.round(minIdle / MS_PER_MINUTE)} min idle (${agentLocation ? 'agent location' : 'other location'})`, idleMs);
561
711
  }
562
- const status = await this.git(['-C', entry.path, 'status', '--porcelain', '--untracked-files=all']);
712
+ // `normal` keeps untracked directories collapsed (one line per
713
+ // `node_modules/`), so a huge dependency tree cannot flood the output.
714
+ const status = await this.git(['-C', entry.path, 'status', '--porcelain', '--untracked-files=normal']);
563
715
  if (status.code !== 0)
564
716
  return keep('status-failed', status.stderr.trim().slice(0, 200), idleMs);
565
- const dirtyLines = status.stdout.split('\n').filter((l) => l.trim() !== '');
717
+ const dirtyLines = dirtyStatusLines(status.stdout);
566
718
  if (dirtyLines.length > 0)
567
719
  return keep('dirty', `${dirtyLines.length} changed/untracked path(s)`, idleMs);
568
720
  if (!entry.head)
@@ -578,7 +730,31 @@ export class WorktreeJanitorService {
578
730
  if (pr !== null)
579
731
  return { ...base, decision: 'remove', reason: 'pr-merged', detail: `PR #${pr} merged`, idleMs };
580
732
  }
581
- return keep('not-merged', defaultRef ? `not in ${defaultRef}, no merged PR` : 'no origin default branch, no merged PR', idleMs);
733
+ if (defaultRef) {
734
+ // Host-agnostic: squash merges on Bitbucket / GitLab / anywhere leave no
735
+ // ancestry and no GitHub PR, but the CONTENT is in the default branch.
736
+ const signal = await contentLandedSignal((a, o) => this.git(a, o), repo.main, defaultRef, entry.head, repo.landed);
737
+ if (signal)
738
+ return { ...base, decision: 'remove', reason: 'content-landed', detail: `${signal} in ${defaultRef}`, idleMs };
739
+ }
740
+ return keep('not-merged', defaultRef ? `not in ${defaultRef} (history or content), no merged PR` : 'no origin default branch, no merged PR', idleMs);
741
+ }
742
+ /**
743
+ * Low disk: delete the regenerable dirs (node_modules, .next) of a kept
744
+ * worktree that has been idle for a day. Reversible; never touches
745
+ * tracked or source files. Never throws.
746
+ */
747
+ async reclaimKept(worktreePath, idleMs, dryRun) {
748
+ if (idleMs === undefined || idleMs < WORKTREE_JANITOR_CONSTANTS.RECLAIM_MIN_IDLE_MS)
749
+ return null;
750
+ try {
751
+ const r = await reclaimRegenerableDirs(worktreePath, this.opts.sizeOf ?? diskUsageBytes, dryRun);
752
+ return r.dirs.length > 0 ? r : null;
753
+ }
754
+ catch (err) {
755
+ this.logger.warn('Reclaiming regenerable dirs failed (non-fatal)', { path: worktreePath, error: err instanceof Error ? err.message : String(err) });
756
+ return null;
757
+ }
582
758
  }
583
759
  /** `origin/<default>` after one quiet fetch per repo per pass; null when there is no origin. */
584
760
  async defaultRef(repo) {
@@ -628,6 +804,13 @@ export class WorktreeJanitorService {
628
804
  const gh = this.opts.ghBin === undefined ? 'gh' : this.opts.ghBin;
629
805
  if (!gh)
630
806
  return null;
807
+ if (repo.nonGithub === undefined) {
808
+ const url = await this.git(['-C', repo.main, 'config', '--get', 'remote.origin.url']);
809
+ repo.nonGithub = url.code === 0 && isNonGithubRemote(url.stdout);
810
+ }
811
+ // Bitbucket / GitLab remotes: gh cannot see their PRs, and asking costs a call per worktree.
812
+ if (repo.nonGithub)
813
+ return null;
631
814
  if (this.ghUsable === undefined) {
632
815
  const auth = await runCommand(gh, ['auth', 'status'], { cwd: repo.main, timeoutMs: WORKTREE_JANITOR_CONSTANTS.GH_TIMEOUT_MS });
633
816
  this.ghUsable = auth.code === 0;
@@ -654,8 +837,8 @@ export class WorktreeJanitorService {
654
837
  if (r.code !== 0) {
655
838
  // Force only when git's refusal can only be ignored output: status is
656
839
  // still clean (untracked included) and there are no submodules.
657
- const status = await this.git(['-C', outcome.path, 'status', '--porcelain', '--untracked-files=all', '--ignore-submodules=none']);
658
- const clean = status.code === 0 && status.stdout.trim() === '';
840
+ const status = await this.git(['-C', outcome.path, 'status', '--porcelain', '--untracked-files=normal', '--ignore-submodules=none']);
841
+ const clean = status.code === 0 && dirtyStatusLines(status.stdout).length === 0;
659
842
  const hasSubmodules = fs.existsSync(path.join(outcome.path, '.gitmodules'));
660
843
  if (clean && !hasSubmodules) {
661
844
  r = await this.git(['-C', repo.main, 'worktree', 'remove', '--force', outcome.path]);