crewly 1.20.301 → 1.20.303

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 (203) hide show
  1. package/config/roles/developer/prompt.md +7 -0
  2. package/config/roles/fullstack-dev/prompt.md +7 -0
  3. package/config/roles/product-manager/prompt.md +7 -0
  4. package/config/skills/agent/core/receive-file/SKILL.md +4 -0
  5. package/config/skills/agent/core/request-file/SKILL.md +74 -0
  6. package/config/skills/agent/core/request-file/execute.sh +89 -0
  7. package/config/skills/agent/core/request-file/execute.test.sh +77 -0
  8. package/config/skills/agent/core/slack-file/SKILL.md +1 -1
  9. package/config/skills/orchestrator/usage-categories/SKILL.md +59 -0
  10. package/config/skills/orchestrator/usage-categories/execute.sh +81 -0
  11. package/config/skills/orchestrator/usage-categories/execute.test.sh +45 -0
  12. package/config/skills/orchestrator/usage-report/SKILL.md +71 -0
  13. package/config/skills/orchestrator/usage-report/execute.sh +84 -0
  14. package/config/skills/orchestrator/usage-report/execute.test.sh +39 -0
  15. package/config/skills/team-leader/usage-report/SKILL.md +71 -0
  16. package/config/skills/team-leader/usage-report/execute.sh +84 -0
  17. package/config/skills/team-leader/usage-report/execute.test.sh +39 -0
  18. package/dist/backend/backend/src/constants.d.ts +24 -4
  19. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  20. package/dist/backend/backend/src/constants.js +29 -6
  21. package/dist/backend/backend/src/constants.js.map +1 -1
  22. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts +9 -0
  23. package/dist/backend/backend/src/controllers/apps/apps.controller.d.ts.map +1 -1
  24. package/dist/backend/backend/src/controllers/apps/apps.controller.js +21 -0
  25. package/dist/backend/backend/src/controllers/apps/apps.controller.js.map +1 -1
  26. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts +2 -0
  27. package/dist/backend/backend/src/controllers/apps/apps.routes.d.ts.map +1 -1
  28. package/dist/backend/backend/src/controllers/apps/apps.routes.js +7 -1
  29. package/dist/backend/backend/src/controllers/apps/apps.routes.js.map +1 -1
  30. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts +36 -0
  31. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.d.ts.map +1 -0
  32. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js +158 -0
  33. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.controller.js.map +1 -0
  34. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts +15 -0
  35. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.d.ts.map +1 -0
  36. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js +26 -0
  37. package/dist/backend/backend/src/controllers/usage-ledger/usage-ledger.routes.js.map +1 -0
  38. package/dist/backend/backend/src/index.d.ts +10 -0
  39. package/dist/backend/backend/src/index.d.ts.map +1 -1
  40. package/dist/backend/backend/src/index.js +75 -17
  41. package/dist/backend/backend/src/index.js.map +1 -1
  42. package/dist/backend/backend/src/routes/api.routes.d.ts.map +1 -1
  43. package/dist/backend/backend/src/routes/api.routes.js +3 -0
  44. package/dist/backend/backend/src/routes/api.routes.js.map +1 -1
  45. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts +32 -0
  46. package/dist/backend/backend/src/services/agent/follow-through.wiring.d.ts.map +1 -1
  47. package/dist/backend/backend/src/services/agent/follow-through.wiring.js +42 -1
  48. package/dist/backend/backend/src/services/agent/follow-through.wiring.js.map +1 -1
  49. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  50. package/dist/backend/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  51. package/dist/backend/backend/src/services/agent/resource-mode.service.js +59 -5
  52. package/dist/backend/backend/src/services/agent/resource-mode.service.js.map +1 -1
  53. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  54. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  55. package/dist/backend/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  56. package/dist/backend/backend/src/services/apps/apps.wiring.d.ts.map +1 -1
  57. package/dist/backend/backend/src/services/apps/apps.wiring.js +14 -0
  58. package/dist/backend/backend/src/services/apps/apps.wiring.js.map +1 -1
  59. package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts +88 -0
  60. package/dist/backend/backend/src/services/apps/file-request-watch.service.d.ts.map +1 -0
  61. package/dist/backend/backend/src/services/apps/file-request-watch.service.js +170 -0
  62. package/dist/backend/backend/src/services/apps/file-request-watch.service.js.map +1 -0
  63. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  64. package/dist/backend/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  65. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js +101 -0
  66. package/dist/backend/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  67. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts +58 -0
  68. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.d.ts.map +1 -0
  69. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js +100 -0
  70. package/dist/backend/backend/src/services/monitoring/background-work-watchdog.js.map +1 -0
  71. package/dist/backend/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  72. package/dist/backend/backend/src/services/monitoring/token-usage.service.js +3 -0
  73. package/dist/backend/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  74. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts +7 -0
  75. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.d.ts.map +1 -1
  76. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js +15 -0
  77. package/dist/backend/backend/src/services/reconciler/reconciler-data-provider.js.map +1 -1
  78. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  79. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
  80. package/dist/backend/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  81. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  82. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  83. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  84. package/dist/backend/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  85. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts +42 -2
  86. package/dist/backend/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  87. package/dist/backend/backend/src/services/trace/trace-context.service.js +66 -5
  88. package/dist/backend/backend/src/services/trace/trace-context.service.js.map +1 -1
  89. package/dist/backend/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  90. package/dist/backend/backend/src/services/trace/trace-recorder.js +11 -0
  91. package/dist/backend/backend/src/services/trace/trace-recorder.js.map +1 -1
  92. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts +28 -0
  93. package/dist/backend/backend/src/services/usage-ledger/categories-param.d.ts.map +1 -0
  94. package/dist/backend/backend/src/services/usage-ledger/categories-param.js +67 -0
  95. package/dist/backend/backend/src/services/usage-ledger/categories-param.js.map +1 -0
  96. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts +37 -0
  97. package/dist/backend/backend/src/services/usage-ledger/excerpt.d.ts.map +1 -0
  98. package/dist/backend/backend/src/services/usage-ledger/excerpt.js +49 -0
  99. package/dist/backend/backend/src/services/usage-ledger/excerpt.js.map +1 -0
  100. package/dist/backend/backend/src/services/usage-ledger/index.d.ts +14 -0
  101. package/dist/backend/backend/src/services/usage-ledger/index.d.ts.map +1 -0
  102. package/dist/backend/backend/src/services/usage-ledger/index.js +14 -0
  103. package/dist/backend/backend/src/services/usage-ledger/index.js.map +1 -0
  104. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  105. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  106. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  107. package/dist/backend/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  108. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts +100 -0
  109. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.d.ts.map +1 -0
  110. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js +189 -0
  111. package/dist/backend/backend/src/services/usage-ledger/ledger-rollup.js.map +1 -0
  112. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts +41 -0
  113. package/dist/backend/backend/src/services/usage-ledger/owner-spans.d.ts.map +1 -0
  114. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js +71 -0
  115. package/dist/backend/backend/src/services/usage-ledger/owner-spans.js.map +1 -0
  116. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts +47 -0
  117. package/dist/backend/backend/src/services/usage-ledger/team-directory.d.ts.map +1 -0
  118. package/dist/backend/backend/src/services/usage-ledger/team-directory.js +106 -0
  119. package/dist/backend/backend/src/services/usage-ledger/team-directory.js.map +1 -0
  120. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts +101 -0
  121. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.d.ts.map +1 -0
  122. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js +263 -0
  123. package/dist/backend/backend/src/services/usage-ledger/unit-classifier.js.map +1 -0
  124. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts +96 -0
  125. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.d.ts.map +1 -0
  126. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js +166 -0
  127. package/dist/backend/backend/src/services/usage-ledger/usage-categories.store.js.map +1 -0
  128. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts +90 -0
  129. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.d.ts.map +1 -0
  130. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js +294 -0
  131. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.backfill.js.map +1 -0
  132. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts +102 -0
  133. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.d.ts.map +1 -0
  134. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js +314 -0
  135. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.recorder.js.map +1 -0
  136. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts +62 -0
  137. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.d.ts.map +1 -0
  138. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js +106 -0
  139. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.runtime.js.map +1 -0
  140. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts +52 -0
  141. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.d.ts.map +1 -0
  142. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js +95 -0
  143. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.store.js.map +1 -0
  144. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts +127 -0
  145. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.d.ts.map +1 -0
  146. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js +19 -0
  147. package/dist/backend/backend/src/services/usage-ledger/usage-ledger.types.js.map +1 -0
  148. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts +124 -0
  149. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.d.ts.map +1 -0
  150. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js +372 -0
  151. package/dist/backend/backend/src/services/usage-ledger/usage-report.service.js.map +1 -0
  152. package/dist/backend/backend/src/types/slack.types.d.ts +8 -0
  153. package/dist/backend/backend/src/types/slack.types.d.ts.map +1 -1
  154. package/dist/backend/backend/src/types/slack.types.js.map +1 -1
  155. package/dist/backend/build-info.json +2 -2
  156. package/dist/cli/backend/src/constants.d.ts +24 -4
  157. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  158. package/dist/cli/backend/src/constants.js +29 -6
  159. package/dist/cli/backend/src/constants.js.map +1 -1
  160. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts +26 -6
  161. package/dist/cli/backend/src/services/agent/resource-mode.service.d.ts.map +1 -1
  162. package/dist/cli/backend/src/services/agent/resource-mode.service.js +59 -5
  163. package/dist/cli/backend/src/services/agent/resource-mode.service.js.map +1 -1
  164. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.d.ts.map +1 -1
  165. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js +1 -0
  166. package/dist/cli/backend/src/services/ai/prompt-modules/skills-reference.module.js.map +1 -1
  167. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts +35 -0
  168. package/dist/cli/backend/src/services/monitoring/agent-turn-state.d.ts.map +1 -1
  169. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js +101 -0
  170. package/dist/cli/backend/src/services/monitoring/agent-turn-state.js.map +1 -1
  171. package/dist/cli/backend/src/services/monitoring/token-usage.service.d.ts.map +1 -1
  172. package/dist/cli/backend/src/services/monitoring/token-usage.service.js +3 -0
  173. package/dist/cli/backend/src/services/monitoring/token-usage.service.js.map +1 -1
  174. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.d.ts.map +1 -1
  175. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js +51 -33
  176. package/dist/cli/backend/src/services/slack/slack-orchestrator-bridge.js.map +1 -1
  177. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts +24 -0
  178. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.d.ts.map +1 -1
  179. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js +63 -1
  180. package/dist/cli/backend/src/services/slack/slack-typing-placeholder.service.js.map +1 -1
  181. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts +42 -2
  182. package/dist/cli/backend/src/services/trace/trace-context.service.d.ts.map +1 -1
  183. package/dist/cli/backend/src/services/trace/trace-context.service.js +66 -5
  184. package/dist/cli/backend/src/services/trace/trace-context.service.js.map +1 -1
  185. package/dist/cli/backend/src/services/trace/trace-recorder.d.ts.map +1 -1
  186. package/dist/cli/backend/src/services/trace/trace-recorder.js +11 -0
  187. package/dist/cli/backend/src/services/trace/trace-recorder.js.map +1 -1
  188. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts +44 -0
  189. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.d.ts.map +1 -0
  190. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js +52 -0
  191. package/dist/cli/backend/src/services/usage-ledger/ledger-hooks.js.map +1 -0
  192. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts +109 -0
  193. package/dist/cli/backend/src/services/worktree/low-disk-guard.d.ts.map +1 -0
  194. package/dist/cli/backend/src/services/worktree/low-disk-guard.js +148 -0
  195. package/dist/cli/backend/src/services/worktree/low-disk-guard.js.map +1 -0
  196. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts +224 -0
  197. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.d.ts.map +1 -0
  198. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js +449 -0
  199. package/dist/cli/backend/src/services/worktree/worktree-janitor.git.js.map +1 -0
  200. package/dist/cli/backend/src/types/slack.types.d.ts +8 -0
  201. package/dist/cli/backend/src/types/slack.types.d.ts.map +1 -1
  202. package/dist/cli/backend/src/types/slack.types.js.map +1 -1
  203. package/package.json +1 -1
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Worktree janitor — low-level helpers.
3
+ *
4
+ * A command runner that never throws, a parser for `git worktree list
5
+ * --porcelain`, path helpers, and the process-cwd probe used to tell whether
6
+ * anything is still working inside a worktree. Kept apart from the service so
7
+ * each piece can be tested on its own.
8
+ *
9
+ * @module services/worktree/worktree-janitor.git
10
+ */
11
+ /** Result of one external command. `code` is null when it could not start or timed out. */
12
+ export interface CommandResult {
13
+ /** Exit code, or null when the process did not exit normally */
14
+ code: number | null;
15
+ /** Captured stdout */
16
+ stdout: string;
17
+ /** Captured stderr (or the spawn error message) */
18
+ stderr: string;
19
+ }
20
+ /** Options for {@link runCommand}. */
21
+ export interface RunCommandOptions {
22
+ /** Working directory */
23
+ cwd?: string;
24
+ /** Timeout in ms (default: GIT_TIMEOUT_MS) */
25
+ timeoutMs?: number;
26
+ /** Extra environment variables merged over process.env */
27
+ env?: Record<string, string>;
28
+ /** Text written to the command's stdin (then closed) */
29
+ input?: string;
30
+ }
31
+ /**
32
+ * Run a command without a shell and never throw.
33
+ *
34
+ * Git and gh are told never to prompt (no credential dialogs, no pager), so a
35
+ * periodic background job cannot hang on input, and git takes no optional
36
+ * locks (a status never rewrites the index).
37
+ *
38
+ * @param file - Executable (e.g. `git`, `gh`, or an absolute path)
39
+ * @param args - Arguments
40
+ * @param options - cwd / timeout / env
41
+ * @returns Exit code and output; `code` is null on spawn failure or timeout
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * const r = await runCommand('git', ['-C', repo, 'status', '--porcelain']);
46
+ * if (r.code === 0 && r.stdout === '') console.log('clean');
47
+ * ```
48
+ */
49
+ export declare function runCommand(file: string, args: string[], options?: RunCommandOptions): Promise<CommandResult>;
50
+ /** One entry of `git worktree list --porcelain`. */
51
+ export interface PorcelainWorktree {
52
+ /** Absolute worktree path as git reports it */
53
+ path: string;
54
+ /** HEAD commit, or null (bare) */
55
+ head: string | null;
56
+ /** Short branch name (`refs/heads/` stripped), or null when detached */
57
+ branch: string | null;
58
+ /** True for the bare entry */
59
+ bare: boolean;
60
+ /** True when HEAD is detached */
61
+ detached: boolean;
62
+ /** True when the worktree is locked (`git worktree lock`) */
63
+ locked: boolean;
64
+ /** Lock reason, when given */
65
+ lockReason?: string;
66
+ /** True when git reports the directory as gone (`prunable`) */
67
+ prunable: boolean;
68
+ /** True for the first entry — the main worktree */
69
+ isMain: boolean;
70
+ }
71
+ /**
72
+ * Parse `git worktree list --porcelain` output.
73
+ *
74
+ * Entries are separated by blank lines; the first entry is the main worktree.
75
+ *
76
+ * @param output - Raw porcelain output
77
+ * @returns Parsed entries in git's order
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * parsePorcelainWorktrees('worktree /r\nHEAD abc\nbranch refs/heads/main\n');
82
+ * // [{ path: '/r', head: 'abc', branch: 'main', isMain: true, ... }]
83
+ * ```
84
+ */
85
+ export declare function parsePorcelainWorktrees(output: string): PorcelainWorktree[];
86
+ /**
87
+ * Real path of `p`, or its resolved form when it does not exist.
88
+ *
89
+ * @param p - Path
90
+ * @returns Canonical path for comparisons
91
+ */
92
+ export declare function canonicalPath(p: string): string;
93
+ /**
94
+ * Whether `child` is `parent` or inside it (both canonicalised first).
95
+ *
96
+ * @param child - Candidate path
97
+ * @param parent - Directory
98
+ * @returns True when child === parent or child is under parent
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * isPathInside('/r/.claude/worktrees/a/src', '/r/.claude/worktrees/a'); // true
103
+ * isPathInside('/r/.claude/worktrees/ab', '/r/.claude/worktrees/a'); // false
104
+ * ```
105
+ */
106
+ export declare function isPathInside(child: string, parent: string): boolean;
107
+ /**
108
+ * Latest modification time (ms) of a worktree: the directory itself plus its
109
+ * private git dir's `index`, `HEAD` and `logs/HEAD` — every commit, checkout,
110
+ * stage or reset touches one of those. Uncommitted edits are caught by the
111
+ * dirty check instead.
112
+ *
113
+ * @param worktreePath - Worktree root
114
+ * @param gitDir - Its private git dir (`.git/worktrees/<name>`), when known
115
+ * @returns Newest mtime in ms, or null when nothing could be stat'ed
116
+ */
117
+ export declare function latestWorktreeMtimeMs(worktreePath: string, gitDir: string | null): number | null;
118
+ /**
119
+ * Current working directories of every process this user can see.
120
+ *
121
+ * macOS: `lsof -a -d cwd -u <uid> -Fn`. Linux: `/proc/<pid>/cwd`. Returns
122
+ * null when the probe cannot run, so the caller can treat "unknown" as busy.
123
+ *
124
+ * @returns Absolute cwd paths (deduplicated), or null when unavailable
125
+ */
126
+ export declare function listProcessCwds(): Promise<string[] | null>;
127
+ /**
128
+ * Whether the kill switch disables the janitor.
129
+ *
130
+ * @param env - Environment (defaults to process.env)
131
+ * @returns True when `CREWLY_WORKTREE_JANITOR` is `0`/`off`/`false`/`no`
132
+ */
133
+ export declare function isJanitorDisabled(env?: NodeJS.ProcessEnv): boolean;
134
+ /**
135
+ * Yield to the event loop every YIELD_EVERY_ENTRIES calls, so a long
136
+ * filesystem walk never holds up Slack, relay polls or PTY I/O.
137
+ */
138
+ export declare class EventLoopYielder {
139
+ private count;
140
+ /**
141
+ * Count one unit of work; every YIELD_EVERY_ENTRIES units, wait one
142
+ * `setImmediate` turn.
143
+ *
144
+ * @returns Resolves when the caller may continue
145
+ */
146
+ tick(): Promise<void>;
147
+ }
148
+ /**
149
+ * Git dir a `.git` file points to (`gitdir: <path>`), resolved against `dir`.
150
+ *
151
+ * @param dir - Directory holding the `.git` file
152
+ * @returns Absolute git dir, or null when there is no readable `.git` file
153
+ */
154
+ export declare function readGitFilePointer(dir: string): Promise<string | null>;
155
+ /**
156
+ * Main worktree of the repo a linked worktree belongs to, read from the
157
+ * worktree's `.git` file (`gitdir: <main>/.git/worktrees/<name>`). Returns
158
+ * null for anything else: a `.git` directory (a normal clone), a submodule
159
+ * (`gitdir: …/.git/modules/…`), or an unreadable file.
160
+ *
161
+ * @param dir - Directory that may be a linked worktree
162
+ * @returns Absolute path of the main worktree, or null
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * // /tmp/visa-cm-wt/.git contains "gitdir: /src/ce-core/.git/worktrees/visa-cm-wt"
167
+ * await mainRepoOfLinkedWorktree('/tmp/visa-cm-wt'); // '/src/ce-core'
168
+ * ```
169
+ */
170
+ export declare function mainRepoOfLinkedWorktree(dir: string): Promise<string | null>;
171
+ /** A git checkout found on disk. */
172
+ export interface FoundRepo {
173
+ /** Directory holding `.git` */
174
+ path: string;
175
+ /** `dir` for a `.git` directory (clone / main worktree), `file` for a `.git` file (linked worktree or submodule) */
176
+ kind: 'dir' | 'file';
177
+ }
178
+ /**
179
+ * Find git checkouts under `root` without following symlinks. A directory
180
+ * with a `.git` directory or file is a checkout and is not descended into;
181
+ * `node_modules` and `.git` are never descended into. Fully async; yields to
182
+ * the event loop every few directories.
183
+ *
184
+ * @param root - Directory to search (itself included, at depth 0)
185
+ * @param maxDepth - Deepest level searched (root = 0)
186
+ * @param maxDirs - Stop after visiting this many directories
187
+ * @returns Checkouts found, in walk order
188
+ */
189
+ export declare function findGitCheckouts(root: string, maxDepth: number, maxDirs?: number): Promise<FoundRepo[]>;
190
+ /**
191
+ * Main worktrees of every repo that owns a linked worktree somewhere under
192
+ * the given roots (e.g. `/tmp/claude-501/visa-cm-wt` → `~/src/ce-core`), so
193
+ * repos that are not registered projects still get their temp worktrees
194
+ * cleaned. Visits at most DISCOVERY_MAX_DIRS directories per root.
195
+ *
196
+ * @param roots - Temp roots to scan (missing roots are skipped)
197
+ * @returns De-duplicated main-worktree paths
198
+ */
199
+ export declare function discoverLinkedWorktreeRepos(roots: readonly string[]): Promise<string[]>;
200
+ /**
201
+ * Newest mtime (ms) of `dir` and everything within `depth` levels below it,
202
+ * using lstat (symlinks are not followed). Async; yields every few entries.
203
+ *
204
+ * @param dir - Directory
205
+ * @param depth - Levels below `dir` to include (0 = only `dir`)
206
+ * @returns Newest mtime in ms, or null when `dir` cannot be stat'ed
207
+ */
208
+ export declare function latestTreeMtimeMs(dir: string, depth: number): Promise<number | null>;
209
+ /**
210
+ * Disk usage of a path in bytes (`du -sk` in a child process, so the event
211
+ * loop is never blocked; `du` does not follow symlinks).
212
+ *
213
+ * @param p - File or directory
214
+ * @returns Bytes, or null when `du` failed or timed out
215
+ */
216
+ export declare function diskUsageBytes(p: string): Promise<number | null>;
217
+ /**
218
+ * Human-readable size (`1.8 GB`, `512 MB`).
219
+ *
220
+ * @param bytes - Size in bytes
221
+ * @returns Short string with one decimal for GB
222
+ */
223
+ export declare function formatBytes(bytes: number): string;
224
+ //# sourceMappingURL=worktree-janitor.git.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worktree-janitor.git.d.ts","sourceRoot":"","sources":["../../../../../../backend/src/services/worktree/worktree-janitor.git.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,449 @@
1
+ /**
2
+ * Worktree janitor — low-level helpers.
3
+ *
4
+ * A command runner that never throws, a parser for `git worktree list
5
+ * --porcelain`, path helpers, and the process-cwd probe used to tell whether
6
+ * anything is still working inside a worktree. Kept apart from the service so
7
+ * each piece can be tested on its own.
8
+ *
9
+ * @module services/worktree/worktree-janitor.git
10
+ */
11
+ import { execFile } from 'child_process';
12
+ import * as fs from 'fs';
13
+ import * as os from 'os';
14
+ import * as path from 'path';
15
+ import { WORKTREE_JANITOR_CONSTANTS } from '../../constants.js';
16
+ /**
17
+ * Run a command without a shell and never throw.
18
+ *
19
+ * Git and gh are told never to prompt (no credential dialogs, no pager), so a
20
+ * periodic background job cannot hang on input, and git takes no optional
21
+ * locks (a status never rewrites the index).
22
+ *
23
+ * @param file - Executable (e.g. `git`, `gh`, or an absolute path)
24
+ * @param args - Arguments
25
+ * @param options - cwd / timeout / env
26
+ * @returns Exit code and output; `code` is null on spawn failure or timeout
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const r = await runCommand('git', ['-C', repo, 'status', '--porcelain']);
31
+ * if (r.code === 0 && r.stdout === '') console.log('clean');
32
+ * ```
33
+ */
34
+ export function runCommand(file, args, options = {}) {
35
+ return new Promise((resolve) => {
36
+ try {
37
+ const child = execFile(file, args, {
38
+ cwd: options.cwd,
39
+ timeout: options.timeoutMs ?? WORKTREE_JANITOR_CONSTANTS.GIT_TIMEOUT_MS,
40
+ maxBuffer: WORKTREE_JANITOR_CONSTANTS.MAX_OUTPUT_BYTES,
41
+ env: {
42
+ ...process.env,
43
+ GIT_TERMINAL_PROMPT: '0',
44
+ GIT_PAGER: 'cat',
45
+ // `git status` must not rewrite the index: that would bump its
46
+ // mtime (resetting the idle clock) and contend with agents' git.
47
+ GIT_OPTIONAL_LOCKS: '0',
48
+ GH_PROMPT_DISABLED: '1',
49
+ GH_NO_UPDATE_NOTIFIER: '1',
50
+ ...(options.env ?? {}),
51
+ },
52
+ }, (error, stdout, stderr) => {
53
+ const out = typeof stdout === 'string' ? stdout : String(stdout ?? '');
54
+ const err = typeof stderr === 'string' ? stderr : String(stderr ?? '');
55
+ if (!error) {
56
+ resolve({ code: 0, stdout: out, stderr: err });
57
+ return;
58
+ }
59
+ const errCode = error.code;
60
+ const code = typeof errCode === 'number' && !error.killed ? errCode : null;
61
+ resolve({ code, stdout: out, stderr: err || error.message });
62
+ });
63
+ if (options.input !== undefined) {
64
+ // A command that exits early closes the pipe: ignore EPIPE.
65
+ child.stdin?.on('error', () => undefined);
66
+ child.stdin?.end(options.input);
67
+ }
68
+ }
69
+ catch (spawnErr) {
70
+ resolve({ code: null, stdout: '', stderr: spawnErr instanceof Error ? spawnErr.message : String(spawnErr) });
71
+ }
72
+ });
73
+ }
74
+ /**
75
+ * Parse `git worktree list --porcelain` output.
76
+ *
77
+ * Entries are separated by blank lines; the first entry is the main worktree.
78
+ *
79
+ * @param output - Raw porcelain output
80
+ * @returns Parsed entries in git's order
81
+ *
82
+ * @example
83
+ * ```typescript
84
+ * parsePorcelainWorktrees('worktree /r\nHEAD abc\nbranch refs/heads/main\n');
85
+ * // [{ path: '/r', head: 'abc', branch: 'main', isMain: true, ... }]
86
+ * ```
87
+ */
88
+ export function parsePorcelainWorktrees(output) {
89
+ const result = [];
90
+ let current = null;
91
+ for (const rawLine of output.split('\n')) {
92
+ const line = rawLine.replace(/\r$/, '');
93
+ if (line.startsWith('worktree ')) {
94
+ current = {
95
+ path: line.slice('worktree '.length),
96
+ head: null,
97
+ branch: null,
98
+ bare: false,
99
+ detached: false,
100
+ locked: false,
101
+ prunable: false,
102
+ isMain: result.length === 0,
103
+ };
104
+ result.push(current);
105
+ continue;
106
+ }
107
+ if (!current || line === '')
108
+ continue;
109
+ if (line.startsWith('HEAD '))
110
+ current.head = line.slice('HEAD '.length);
111
+ else if (line.startsWith('branch '))
112
+ current.branch = line.slice('branch '.length).replace(/^refs\/heads\//, '');
113
+ else if (line === 'bare')
114
+ current.bare = true;
115
+ else if (line === 'detached')
116
+ current.detached = true;
117
+ else if (line === 'locked' || line.startsWith('locked ')) {
118
+ current.locked = true;
119
+ const reason = line.slice('locked'.length).trim();
120
+ if (reason)
121
+ current.lockReason = reason;
122
+ }
123
+ else if (line === 'prunable' || line.startsWith('prunable '))
124
+ current.prunable = true;
125
+ }
126
+ return result;
127
+ }
128
+ /**
129
+ * Real path of `p`, or its resolved form when it does not exist.
130
+ *
131
+ * @param p - Path
132
+ * @returns Canonical path for comparisons
133
+ */
134
+ export function canonicalPath(p) {
135
+ try {
136
+ return fs.realpathSync(p);
137
+ }
138
+ catch {
139
+ return path.resolve(p);
140
+ }
141
+ }
142
+ /**
143
+ * Whether `child` is `parent` or inside it (both canonicalised first).
144
+ *
145
+ * @param child - Candidate path
146
+ * @param parent - Directory
147
+ * @returns True when child === parent or child is under parent
148
+ *
149
+ * @example
150
+ * ```typescript
151
+ * isPathInside('/r/.claude/worktrees/a/src', '/r/.claude/worktrees/a'); // true
152
+ * isPathInside('/r/.claude/worktrees/ab', '/r/.claude/worktrees/a'); // false
153
+ * ```
154
+ */
155
+ export function isPathInside(child, parent) {
156
+ const c = canonicalPath(child);
157
+ const p = canonicalPath(parent);
158
+ if (c === p)
159
+ return true;
160
+ const withSep = p.endsWith(path.sep) ? p : p + path.sep;
161
+ return c.startsWith(withSep);
162
+ }
163
+ /**
164
+ * Latest modification time (ms) of a worktree: the directory itself plus its
165
+ * private git dir's `index`, `HEAD` and `logs/HEAD` — every commit, checkout,
166
+ * stage or reset touches one of those. Uncommitted edits are caught by the
167
+ * dirty check instead.
168
+ *
169
+ * @param worktreePath - Worktree root
170
+ * @param gitDir - Its private git dir (`.git/worktrees/<name>`), when known
171
+ * @returns Newest mtime in ms, or null when nothing could be stat'ed
172
+ */
173
+ export function latestWorktreeMtimeMs(worktreePath, gitDir) {
174
+ const candidates = [worktreePath, path.join(worktreePath, '.git')];
175
+ if (gitDir) {
176
+ candidates.push(gitDir, path.join(gitDir, 'index'), path.join(gitDir, 'HEAD'), path.join(gitDir, 'logs', 'HEAD'));
177
+ }
178
+ let newest = null;
179
+ for (const c of candidates) {
180
+ try {
181
+ const m = fs.lstatSync(c).mtimeMs;
182
+ if (newest === null || m > newest)
183
+ newest = m;
184
+ }
185
+ catch {
186
+ // missing — ignore
187
+ }
188
+ }
189
+ return newest;
190
+ }
191
+ /**
192
+ * Current working directories of every process this user can see.
193
+ *
194
+ * macOS: `lsof -a -d cwd -u <uid> -Fn`. Linux: `/proc/<pid>/cwd`. Returns
195
+ * null when the probe cannot run, so the caller can treat "unknown" as busy.
196
+ *
197
+ * @returns Absolute cwd paths (deduplicated), or null when unavailable
198
+ */
199
+ export async function listProcessCwds() {
200
+ if (process.platform === 'linux') {
201
+ try {
202
+ const out = new Set();
203
+ for (const entry of fs.readdirSync('/proc')) {
204
+ if (!/^\d+$/.test(entry))
205
+ continue;
206
+ try {
207
+ out.add(fs.readlinkSync(path.join('/proc', entry, 'cwd')));
208
+ }
209
+ catch {
210
+ // other user's process or already gone
211
+ }
212
+ }
213
+ return [...out];
214
+ }
215
+ catch {
216
+ return null;
217
+ }
218
+ }
219
+ if (process.platform === 'win32')
220
+ return null;
221
+ const uid = typeof process.getuid === 'function' ? String(process.getuid()) : String(os.userInfo().uid);
222
+ const r = await runCommand('lsof', ['-a', '-d', 'cwd', '-u', uid, '-Fn', '-w'], {
223
+ timeoutMs: WORKTREE_JANITOR_CONSTANTS.CWD_PROBE_TIMEOUT_MS,
224
+ });
225
+ // lsof exits 1 when some listed item had no match; the output is still valid.
226
+ if (r.code === null || !r.stdout)
227
+ return null;
228
+ const out = new Set();
229
+ for (const line of r.stdout.split('\n')) {
230
+ if (line.startsWith('n/'))
231
+ out.add(line.slice(1));
232
+ }
233
+ return out.size > 0 ? [...out] : null;
234
+ }
235
+ /**
236
+ * Whether the kill switch disables the janitor.
237
+ *
238
+ * @param env - Environment (defaults to process.env)
239
+ * @returns True when `CREWLY_WORKTREE_JANITOR` is `0`/`off`/`false`/`no`
240
+ */
241
+ export function isJanitorDisabled(env = process.env) {
242
+ const raw = env[WORKTREE_JANITOR_CONSTANTS.ENV_VAR];
243
+ if (raw === undefined)
244
+ return false;
245
+ return WORKTREE_JANITOR_CONSTANTS.DISABLED_VALUES.includes(raw.trim().toLowerCase());
246
+ }
247
+ /**
248
+ * Yield to the event loop every YIELD_EVERY_ENTRIES calls, so a long
249
+ * filesystem walk never holds up Slack, relay polls or PTY I/O.
250
+ */
251
+ export class EventLoopYielder {
252
+ count = 0;
253
+ /**
254
+ * Count one unit of work; every YIELD_EVERY_ENTRIES units, wait one
255
+ * `setImmediate` turn.
256
+ *
257
+ * @returns Resolves when the caller may continue
258
+ */
259
+ async tick() {
260
+ this.count++;
261
+ if (this.count % WORKTREE_JANITOR_CONSTANTS.YIELD_EVERY_ENTRIES === 0) {
262
+ await new Promise((resolve) => setImmediate(resolve));
263
+ }
264
+ }
265
+ }
266
+ /** lstat that resolves null instead of throwing. */
267
+ async function lstatOrNull(p) {
268
+ try {
269
+ return await fs.promises.lstat(p);
270
+ }
271
+ catch {
272
+ return null;
273
+ }
274
+ }
275
+ /**
276
+ * Git dir a `.git` file points to (`gitdir: <path>`), resolved against `dir`.
277
+ *
278
+ * @param dir - Directory holding the `.git` file
279
+ * @returns Absolute git dir, or null when there is no readable `.git` file
280
+ */
281
+ export async function readGitFilePointer(dir) {
282
+ try {
283
+ const m = /^gitdir:\s*(.+?)\s*$/m.exec(await fs.promises.readFile(path.join(dir, '.git'), 'utf-8'));
284
+ return m ? path.resolve(dir, m[1]) : null;
285
+ }
286
+ catch {
287
+ return null;
288
+ }
289
+ }
290
+ /**
291
+ * Main worktree of the repo a linked worktree belongs to, read from the
292
+ * worktree's `.git` file (`gitdir: <main>/.git/worktrees/<name>`). Returns
293
+ * null for anything else: a `.git` directory (a normal clone), a submodule
294
+ * (`gitdir: …/.git/modules/…`), or an unreadable file.
295
+ *
296
+ * @param dir - Directory that may be a linked worktree
297
+ * @returns Absolute path of the main worktree, or null
298
+ *
299
+ * @example
300
+ * ```typescript
301
+ * // /tmp/visa-cm-wt/.git contains "gitdir: /src/ce-core/.git/worktrees/visa-cm-wt"
302
+ * await mainRepoOfLinkedWorktree('/tmp/visa-cm-wt'); // '/src/ce-core'
303
+ * ```
304
+ */
305
+ export async function mainRepoOfLinkedWorktree(dir) {
306
+ const st = await lstatOrNull(path.join(dir, '.git'));
307
+ if (!st?.isFile())
308
+ return null;
309
+ const gitDir = await readGitFilePointer(dir);
310
+ if (!gitDir)
311
+ return null;
312
+ const worktreesDir = path.dirname(gitDir);
313
+ if (path.basename(worktreesDir) !== 'worktrees')
314
+ return null;
315
+ const commonDir = path.dirname(worktreesDir);
316
+ if (path.basename(commonDir) !== '.git')
317
+ return null;
318
+ return path.dirname(commonDir);
319
+ }
320
+ /**
321
+ * Find git checkouts under `root` without following symlinks. A directory
322
+ * with a `.git` directory or file is a checkout and is not descended into;
323
+ * `node_modules` and `.git` are never descended into. Fully async; yields to
324
+ * the event loop every few directories.
325
+ *
326
+ * @param root - Directory to search (itself included, at depth 0)
327
+ * @param maxDepth - Deepest level searched (root = 0)
328
+ * @param maxDirs - Stop after visiting this many directories
329
+ * @returns Checkouts found, in walk order
330
+ */
331
+ export async function findGitCheckouts(root, maxDepth, maxDirs = Number.POSITIVE_INFINITY) {
332
+ const out = [];
333
+ const yielder = new EventLoopYielder();
334
+ let visited = 0;
335
+ const walk = async (dir, depth) => {
336
+ if (visited >= maxDirs)
337
+ return;
338
+ visited++;
339
+ await yielder.tick();
340
+ const dotGit = await lstatOrNull(path.join(dir, '.git'));
341
+ if (dotGit && (dotGit.isDirectory() || dotGit.isFile())) {
342
+ out.push({ path: dir, kind: dotGit.isDirectory() ? 'dir' : 'file' });
343
+ return;
344
+ }
345
+ if (depth >= maxDepth)
346
+ return;
347
+ let entries;
348
+ try {
349
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
350
+ }
351
+ catch {
352
+ return;
353
+ }
354
+ for (const e of entries) {
355
+ // Dirent.isDirectory() is false for symlinks, so links are never followed.
356
+ if (!e.isDirectory() || WORKTREE_JANITOR_CONSTANTS.SEARCH_SKIP_DIRS.includes(e.name))
357
+ continue;
358
+ await walk(path.join(dir, e.name), depth + 1);
359
+ }
360
+ };
361
+ await walk(root, 0);
362
+ return out;
363
+ }
364
+ /**
365
+ * Main worktrees of every repo that owns a linked worktree somewhere under
366
+ * the given roots (e.g. `/tmp/claude-501/visa-cm-wt` → `~/src/ce-core`), so
367
+ * repos that are not registered projects still get their temp worktrees
368
+ * cleaned. Visits at most DISCOVERY_MAX_DIRS directories per root.
369
+ *
370
+ * @param roots - Temp roots to scan (missing roots are skipped)
371
+ * @returns De-duplicated main-worktree paths
372
+ */
373
+ export async function discoverLinkedWorktreeRepos(roots) {
374
+ const out = new Set();
375
+ for (const root of roots) {
376
+ if (!(await lstatOrNull(root)))
377
+ continue;
378
+ const found = await findGitCheckouts(root, WORKTREE_JANITOR_CONSTANTS.DISCOVERY_DEPTH, WORKTREE_JANITOR_CONSTANTS.DISCOVERY_MAX_DIRS);
379
+ for (const f of found) {
380
+ if (f.kind !== 'file')
381
+ continue;
382
+ const main = await mainRepoOfLinkedWorktree(f.path);
383
+ if (main && (await lstatOrNull(main)))
384
+ out.add(main);
385
+ }
386
+ }
387
+ return [...out];
388
+ }
389
+ /**
390
+ * Newest mtime (ms) of `dir` and everything within `depth` levels below it,
391
+ * using lstat (symlinks are not followed). Async; yields every few entries.
392
+ *
393
+ * @param dir - Directory
394
+ * @param depth - Levels below `dir` to include (0 = only `dir`)
395
+ * @returns Newest mtime in ms, or null when `dir` cannot be stat'ed
396
+ */
397
+ export async function latestTreeMtimeMs(dir, depth) {
398
+ let newest = null;
399
+ const yielder = new EventLoopYielder();
400
+ const visit = async (p, level) => {
401
+ await yielder.tick();
402
+ const st = await lstatOrNull(p);
403
+ if (!st)
404
+ return;
405
+ if (newest === null || st.mtimeMs > newest)
406
+ newest = st.mtimeMs;
407
+ if (level >= depth || !st.isDirectory())
408
+ return;
409
+ let names;
410
+ try {
411
+ names = await fs.promises.readdir(p);
412
+ }
413
+ catch {
414
+ return;
415
+ }
416
+ for (const n of names)
417
+ await visit(path.join(p, n), level + 1);
418
+ };
419
+ await visit(dir, 0);
420
+ return newest;
421
+ }
422
+ /**
423
+ * Disk usage of a path in bytes (`du -sk` in a child process, so the event
424
+ * loop is never blocked; `du` does not follow symlinks).
425
+ *
426
+ * @param p - File or directory
427
+ * @returns Bytes, or null when `du` failed or timed out
428
+ */
429
+ export async function diskUsageBytes(p) {
430
+ const r = await runCommand('du', ['-sk', p], { timeoutMs: WORKTREE_JANITOR_CONSTANTS.DU_TIMEOUT_MS });
431
+ const kb = Number.parseInt(r.stdout.trim().split(/\s+/)[0] ?? '', 10);
432
+ return Number.isFinite(kb) ? kb * 1024 : null;
433
+ }
434
+ /**
435
+ * Human-readable size (`1.8 GB`, `512 MB`).
436
+ *
437
+ * @param bytes - Size in bytes
438
+ * @returns Short string with one decimal for GB
439
+ */
440
+ export function formatBytes(bytes) {
441
+ const gb = bytes / 1024 ** 3;
442
+ if (gb >= 1)
443
+ return `${gb.toFixed(1)} GB`;
444
+ const mb = bytes / 1024 ** 2;
445
+ if (mb >= 1)
446
+ return `${Math.round(mb)} MB`;
447
+ return `${Math.round(bytes / 1024)} KB`;
448
+ }
449
+ //# sourceMappingURL=worktree-janitor.git.js.map