create-walle 0.9.21 → 0.9.23

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 (500) hide show
  1. package/README.md +27 -5
  2. package/package.json +2 -2
  3. package/template/CLAUDE.md +2 -2
  4. package/template/LICENSE +1 -1
  5. package/template/bin/ctm-dev-cleanup.js +24 -3
  6. package/template/bin/ctm-launch.sh +13 -0
  7. package/template/bin/dev.sh +156 -18
  8. package/template/bin/node-bin.sh +84 -0
  9. package/template/bin/pin-node.sh +51 -0
  10. package/template/claude-task-manager/api-prompts.js +1203 -182
  11. package/template/claude-task-manager/api-reviews.js +109 -15
  12. package/template/claude-task-manager/approval-agent.js +1360 -280
  13. package/template/claude-task-manager/bin/restart-ctm.sh +64 -23
  14. package/template/claude-task-manager/bin/storage-migration-supervisor.js +338 -0
  15. package/template/claude-task-manager/db.js +4417 -295
  16. package/template/claude-task-manager/docs/app-update-refresh-protocol.md +69 -0
  17. package/template/claude-task-manager/docs/approval-ai-refinement.md +138 -0
  18. package/template/claude-task-manager/docs/approval-rescue-loop.md +74 -0
  19. package/template/claude-task-manager/docs/codex-operational-warning-health.md +107 -0
  20. package/template/claude-task-manager/docs/codex-resume-state-guard-design.md +17 -12
  21. package/template/claude-task-manager/docs/codex-terminal-render-controller-handoff.md +311 -0
  22. package/template/claude-task-manager/docs/coding-agent-hooks-architecture.md +418 -0
  23. package/template/claude-task-manager/docs/conversation-import-freshness.md +20 -0
  24. package/template/claude-task-manager/docs/google-workspace-auth-health.md +77 -0
  25. package/template/claude-task-manager/docs/image-paste-ux.md +13 -0
  26. package/template/claude-task-manager/docs/ipad-web-preview.md +88 -0
  27. package/template/claude-task-manager/docs/main-loop-offload-architecture.md +66 -0
  28. package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +274 -519
  29. package/template/claude-task-manager/docs/mobile-live-streaming.md +27 -5
  30. package/template/claude-task-manager/docs/mobile-remote-submission-lifecycle.md +69 -0
  31. package/template/claude-task-manager/docs/phone-access-design.md +53 -15
  32. package/template/claude-task-manager/docs/phone-passkey-identity.md +122 -0
  33. package/template/claude-task-manager/docs/phone-setup.md +3 -0
  34. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +25 -1
  35. package/template/claude-task-manager/docs/remote-desktop-access-design.md +268 -0
  36. package/template/claude-task-manager/docs/restart-lifecycle-architecture.md +95 -0
  37. package/template/claude-task-manager/docs/runtime-work-control-plane.md +53 -0
  38. package/template/claude-task-manager/docs/session-interactive-wait-surfaces.md +38 -0
  39. package/template/claude-task-manager/docs/session-needs-you-dismissal.md +84 -0
  40. package/template/claude-task-manager/docs/session-render-state-management-design.md +91 -3
  41. package/template/claude-task-manager/docs/session-standup-command-center-design.md +25 -1
  42. package/template/claude-task-manager/docs/session-title-authority.md +32 -0
  43. package/template/claude-task-manager/docs/session-workspace-binding.md +33 -0
  44. package/template/claude-task-manager/docs/skill-intent-resolution-design.md +72 -0
  45. package/template/claude-task-manager/docs/walle-mcp-supervisor-health.md +86 -0
  46. package/template/claude-task-manager/docs/walle-relay-phone-access-design.md +24 -15
  47. package/template/claude-task-manager/docs/walle-session-history-hydration.md +114 -0
  48. package/template/claude-task-manager/docs/walle-session-input-queue.md +104 -0
  49. package/template/claude-task-manager/docs/walle-session-model-catalog.md +90 -0
  50. package/template/claude-task-manager/docs/walle-session-model-preferences.md +15 -6
  51. package/template/claude-task-manager/git-utils.js +897 -27
  52. package/template/claude-task-manager/lib/agent-capabilities.js +33 -0
  53. package/template/claude-task-manager/lib/agent-cli-cache.js +37 -7
  54. package/template/claude-task-manager/lib/agent-hooks-installer.js +26 -2
  55. package/template/claude-task-manager/lib/agent-presets.js +17 -1
  56. package/template/claude-task-manager/lib/all-sessions-query.js +108 -0
  57. package/template/claude-task-manager/lib/approval-ai-refinement.js +488 -0
  58. package/template/claude-task-manager/lib/approval-self-adapt.js +168 -0
  59. package/template/claude-task-manager/lib/async-semaphore.js +44 -0
  60. package/template/claude-task-manager/lib/auth-context.js +5 -0
  61. package/template/claude-task-manager/lib/auth-rate-limit.js +47 -4
  62. package/template/claude-task-manager/lib/auth-rules.js +29 -2
  63. package/template/claude-task-manager/lib/auto-approval-verifier.js +129 -16
  64. package/template/claude-task-manager/lib/background-llm.js +144 -17
  65. package/template/claude-task-manager/lib/branch-inventory.js +212 -0
  66. package/template/claude-task-manager/lib/claude-desktop-sessions.js +15 -3
  67. package/template/claude-task-manager/lib/coalesce-sync-frames.js +151 -0
  68. package/template/claude-task-manager/lib/codex-launch-health.js +762 -0
  69. package/template/claude-task-manager/lib/codex-transcript-pager.js +51 -0
  70. package/template/claude-task-manager/lib/codex-zst.js +124 -0
  71. package/template/claude-task-manager/lib/coding-agent-models.js +233 -30
  72. package/template/claude-task-manager/lib/connection-health.js +232 -0
  73. package/template/claude-task-manager/lib/conversation-blob-parser.js +42 -0
  74. package/template/claude-task-manager/lib/conversation-tail-merge.js +89 -26
  75. package/template/claude-task-manager/lib/ctm-session-context-api.js +39 -10
  76. package/template/claude-task-manager/lib/cursor-conversation-store.js +354 -0
  77. package/template/claude-task-manager/lib/db-owner-worker-client.js +315 -0
  78. package/template/claude-task-manager/lib/document-review.js +141 -6
  79. package/template/claude-task-manager/lib/escalation-review.js +152 -0
  80. package/template/claude-task-manager/lib/graceful-shutdown.js +159 -0
  81. package/template/claude-task-manager/lib/headless-term-service.js +678 -0
  82. package/template/claude-task-manager/lib/heavy-worker-fallback.js +38 -0
  83. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +542 -0
  84. package/template/claude-task-manager/lib/jsonl-range-reader.js +112 -0
  85. package/template/claude-task-manager/lib/main-db-census.js +216 -0
  86. package/template/claude-task-manager/lib/message-pagination.js +106 -4
  87. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +750 -26
  88. package/template/claude-task-manager/lib/mobile-auth-api.js +274 -7
  89. package/template/claude-task-manager/lib/mobile-auth-store.js +592 -10
  90. package/template/claude-task-manager/lib/mobile-notification-dispatcher.js +15 -0
  91. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +311 -0
  92. package/template/claude-task-manager/lib/model-overview-cache.js +141 -0
  93. package/template/claude-task-manager/lib/models-health-routing-notice.js +126 -0
  94. package/template/claude-task-manager/lib/node-pin-guard.js +93 -0
  95. package/template/claude-task-manager/lib/perf-tracker.js +242 -6
  96. package/template/claude-task-manager/lib/permission-match.js +76 -0
  97. package/template/claude-task-manager/lib/permission-sync.js +133 -20
  98. package/template/claude-task-manager/lib/process-title.js +35 -0
  99. package/template/claude-task-manager/lib/prompt-executions-query.js +25 -0
  100. package/template/claude-task-manager/lib/prompt-index-disk-cache.js +44 -0
  101. package/template/claude-task-manager/lib/prompt-intent.js +132 -0
  102. package/template/claude-task-manager/lib/provider-user-context.js +34 -0
  103. package/template/claude-task-manager/lib/read-pool-client.js +313 -0
  104. package/template/claude-task-manager/lib/readpool-breaker.js +31 -0
  105. package/template/claude-task-manager/lib/recent-sessions-breaker.js +12 -0
  106. package/template/claude-task-manager/lib/remote-feedback-client.js +72 -0
  107. package/template/claude-task-manager/lib/remote-relay-protocol.js +37 -4
  108. package/template/claude-task-manager/lib/remote-relay-store.js +159 -0
  109. package/template/claude-task-manager/lib/remote-submission-observer.js +278 -0
  110. package/template/claude-task-manager/lib/restart-guard.js +109 -0
  111. package/template/claude-task-manager/lib/restore-interruption-detector.js +439 -0
  112. package/template/claude-task-manager/lib/restore-policy.js +13 -0
  113. package/template/claude-task-manager/lib/restore-resume-batch.js +74 -0
  114. package/template/claude-task-manager/lib/restore-runtime.js +68 -0
  115. package/template/claude-task-manager/lib/restore-storm.js +34 -0
  116. package/template/claude-task-manager/lib/resume-cwd.js +36 -0
  117. package/template/claude-task-manager/lib/resume-preflight.js +313 -0
  118. package/template/claude-task-manager/lib/runtime-work-registry.js +444 -0
  119. package/template/claude-task-manager/lib/sanitize-openai-auth.js +31 -0
  120. package/template/claude-task-manager/lib/scheduler.js +21 -1
  121. package/template/claude-task-manager/lib/scrollback-snapshot-store.js +159 -0
  122. package/template/claude-task-manager/lib/serial-task-queue.js +64 -0
  123. package/template/claude-task-manager/lib/server-listeners.js +239 -0
  124. package/template/claude-task-manager/lib/session-capture.js +42 -7
  125. package/template/claude-task-manager/lib/session-content-backfill.js +131 -0
  126. package/template/claude-task-manager/lib/session-history.js +388 -43
  127. package/template/claude-task-manager/lib/session-host-manager.js +287 -0
  128. package/template/claude-task-manager/lib/session-image-refs.js +209 -0
  129. package/template/claude-task-manager/lib/session-jobs.js +399 -59
  130. package/template/claude-task-manager/lib/session-prompt-index.js +137 -0
  131. package/template/claude-task-manager/lib/session-restore.js +53 -0
  132. package/template/claude-task-manager/lib/session-standup.js +123 -23
  133. package/template/claude-task-manager/lib/session-state-bus.js +14 -0
  134. package/template/claude-task-manager/lib/session-stream.js +64 -16
  135. package/template/claude-task-manager/lib/session-timeline-summary.js +260 -0
  136. package/template/claude-task-manager/lib/session-token-usage.js +494 -0
  137. package/template/claude-task-manager/lib/session-workspace-binding.js +356 -0
  138. package/template/claude-task-manager/lib/setup-network-config.js +9 -0
  139. package/template/claude-task-manager/lib/size-cap.js +45 -0
  140. package/template/claude-task-manager/lib/size-cap.test.js +62 -0
  141. package/template/claude-task-manager/lib/skill-autocomplete.js +180 -1
  142. package/template/claude-task-manager/lib/skill-intent-resolver.js +304 -0
  143. package/template/claude-task-manager/lib/sqlite-driver.js +19 -3
  144. package/template/claude-task-manager/lib/standup-attention.js +7 -3
  145. package/template/claude-task-manager/lib/status-authority.js +39 -0
  146. package/template/claude-task-manager/lib/status-hooks.js +4 -0
  147. package/template/claude-task-manager/lib/storage-migration.js +235 -0
  148. package/template/claude-task-manager/lib/structured-capture.js +298 -0
  149. package/template/claude-task-manager/lib/sync-io-census.js +163 -0
  150. package/template/claude-task-manager/lib/tailscale-setup.js +6 -0
  151. package/template/claude-task-manager/lib/terminal-activity-evidence.js +33 -0
  152. package/template/claude-task-manager/lib/terminal-choice.js +364 -0
  153. package/template/claude-task-manager/lib/terminal-control-sanitize.js +17 -0
  154. package/template/claude-task-manager/lib/terminal-fingerprint.js +48 -0
  155. package/template/claude-task-manager/lib/terminal-output-flush.js +84 -0
  156. package/template/claude-task-manager/lib/timeline-order.js +122 -0
  157. package/template/claude-task-manager/lib/transcript-store.js +348 -43
  158. package/template/claude-task-manager/lib/transport-security.js +84 -1
  159. package/template/claude-task-manager/lib/wait-state.js +184 -0
  160. package/template/claude-task-manager/lib/walle-client.js +47 -5
  161. package/template/claude-task-manager/lib/walle-ctm-history.js +564 -4
  162. package/template/claude-task-manager/lib/walle-external-actions.js +135 -16
  163. package/template/claude-task-manager/lib/walle-history-hydration.js +46 -0
  164. package/template/claude-task-manager/lib/walle-native-health.js +403 -0
  165. package/template/claude-task-manager/lib/walle-repair.js +701 -0
  166. package/template/claude-task-manager/lib/walle-session-cache.js +109 -0
  167. package/template/claude-task-manager/lib/walle-session-context.js +57 -21
  168. package/template/claude-task-manager/lib/walle-session-model-catalog.js +34 -0
  169. package/template/claude-task-manager/lib/walle-supervisor.js +539 -63
  170. package/template/claude-task-manager/lib/walle-transcript.js +52 -0
  171. package/template/claude-task-manager/lib/worktree-active-sync.js +11 -7
  172. package/template/claude-task-manager/lib/worktree-cwd.js +32 -1
  173. package/template/claude-task-manager/package.json +1 -1
  174. package/template/claude-task-manager/prompt-harvest.js +89 -66
  175. package/template/claude-task-manager/providers/claude-code.js +51 -3
  176. package/template/claude-task-manager/providers/cursor.js +140 -45
  177. package/template/claude-task-manager/public/css/reviews.css +551 -61
  178. package/template/claude-task-manager/public/css/setup.css +191 -0
  179. package/template/claude-task-manager/public/css/walle-session.css +865 -10
  180. package/template/claude-task-manager/public/css/walle.css +154 -0
  181. package/template/claude-task-manager/public/designs/ai-providers-consolidation-v2.html +830 -0
  182. package/template/claude-task-manager/public/index.html +18516 -2058
  183. package/template/claude-task-manager/public/ipad.html +363 -0
  184. package/template/claude-task-manager/public/js/document-review-links.js +301 -0
  185. package/template/claude-task-manager/public/js/image-normalize.js +69 -36
  186. package/template/claude-task-manager/public/js/message-renderer.js +1265 -77
  187. package/template/claude-task-manager/public/js/prompts.js +66 -29
  188. package/template/claude-task-manager/public/js/reviews.js +901 -133
  189. package/template/claude-task-manager/public/js/session-activity-utils.js +11 -1
  190. package/template/claude-task-manager/public/js/session-search-utils.js +94 -10
  191. package/template/claude-task-manager/public/js/session-status-precedence.js +23 -5
  192. package/template/claude-task-manager/public/js/setup.js +1273 -176
  193. package/template/claude-task-manager/public/js/stream-view.js +691 -73
  194. package/template/claude-task-manager/public/js/terminal-reconciler.js +210 -0
  195. package/template/claude-task-manager/public/js/walle-session.js +2455 -158
  196. package/template/claude-task-manager/public/js/walle.js +455 -28
  197. package/template/claude-task-manager/public/m/app.css +2909 -262
  198. package/template/claude-task-manager/public/m/app.js +6601 -398
  199. package/template/claude-task-manager/public/m/claim.html +224 -17
  200. package/template/claude-task-manager/public/m/index.html +117 -21
  201. package/template/claude-task-manager/public/m/sw.js +3 -1
  202. package/template/claude-task-manager/public/manifest.json +2 -2
  203. package/template/claude-task-manager/public/prompts.html +30 -14
  204. package/template/claude-task-manager/queue-engine.js +507 -28
  205. package/template/claude-task-manager/scripts/repair-claude-session-images.js +27 -8
  206. package/template/claude-task-manager/server.js +14341 -2197
  207. package/template/claude-task-manager/session-integrity.js +160 -18
  208. package/template/claude-task-manager/session-search-ranking.js +1 -0
  209. package/template/claude-task-manager/session-utils.js +25 -5
  210. package/template/claude-task-manager/workers/approval-blocklist.js +96 -6
  211. package/template/claude-task-manager/workers/approval-widget-validator.js +14 -8
  212. package/template/claude-task-manager/workers/conversation-import-worker.js +11 -50
  213. package/template/claude-task-manager/workers/db-owner-worker.js +386 -0
  214. package/template/claude-task-manager/workers/harvest-worker.js +9 -55
  215. package/template/claude-task-manager/workers/headless-term-worker.js +9 -530
  216. package/template/claude-task-manager/workers/read-pool-worker.js +387 -0
  217. package/template/claude-task-manager/workers/scrollback-worker.js +11 -72
  218. package/template/claude-task-manager/workers/session-host-process.js +146 -0
  219. package/template/claude-task-manager/workers/session-integrity-worker.js +10 -54
  220. package/template/claude-task-manager/workers/state-detectors/base.js +18 -1
  221. package/template/claude-task-manager/workers/state-detectors/claude-code.js +182 -9
  222. package/template/claude-task-manager/workers/state-detectors/codex.js +150 -2
  223. package/template/claude-task-manager/workers/state-detectors/cursor.js +127 -0
  224. package/template/claude-task-manager/workers/state-detectors/gemini.js +21 -0
  225. package/template/claude-task-manager/workers/state-detectors/index.js +29 -0
  226. package/template/claude-task-manager/workers/state-detectors/opencode.js +103 -0
  227. package/template/docs/design/markdown-review-pane.md +206 -0
  228. package/template/docs/designs/2026-05-17-portkey-gateway-provider-ux.md +129 -38
  229. package/template/docs/designs/2026-05-20-mobile-worktree-finish-command.md +27 -0
  230. package/template/docs/designs/2026-05-22-ai-configuration-consolidation.md +248 -0
  231. package/template/docs/designs/ai-configuration-consolidation-mock.html +812 -0
  232. package/template/docs/private-memory-and-pii-policy.md +69 -0
  233. package/template/package.json +2 -1
  234. package/template/scripts/check-private-data.js +201 -0
  235. package/template/shared/sqlite-owner-guard.js +30 -0
  236. package/template/shared/sqlite-owner-write-queue.js +225 -0
  237. package/template/shared/sqlite-storage-policy.js +111 -0
  238. package/template/shared/sqlite-write-lock.js +428 -0
  239. package/template/wall-e/agent-runners/claude-code.js +5 -0
  240. package/template/wall-e/agent.js +166 -22
  241. package/template/wall-e/api-walle.js +524 -70
  242. package/template/wall-e/auth/provider-flows.js +11 -1
  243. package/template/wall-e/bin/walle-mcp-stdio.js +341 -17
  244. package/template/wall-e/brain.js +1614 -141
  245. package/template/wall-e/chat/attachment-blocks.js +96 -0
  246. package/template/wall-e/chat/attachments.js +2 -1
  247. package/template/wall-e/chat/capability-resolver.js +7 -7
  248. package/template/wall-e/chat/context-messages.js +28 -0
  249. package/template/wall-e/chat/conversation-frame.js +630 -0
  250. package/template/wall-e/chat/provider-messages.js +125 -0
  251. package/template/wall-e/chat.js +1002 -233
  252. package/template/wall-e/coding/acceptance-contract.js +170 -0
  253. package/template/wall-e/coding/acp-adapter.js +1 -1
  254. package/template/wall-e/coding/agent-catalog.js +3 -0
  255. package/template/wall-e/coding/artifact-store.js +93 -0
  256. package/template/wall-e/coding/capability-router.js +120 -0
  257. package/template/wall-e/coding/coding-run-controller.js +423 -0
  258. package/template/wall-e/coding/compaction-service.js +157 -12
  259. package/template/wall-e/coding/frontend-verification.js +258 -0
  260. package/template/wall-e/coding/lifecycle-hooks.js +75 -0
  261. package/template/wall-e/coding/local-preview-contract.js +157 -0
  262. package/template/wall-e/coding/permission-service.js +57 -13
  263. package/template/wall-e/coding/prompt-bundle.js +19 -1
  264. package/template/wall-e/coding/prompt-section-registry.js +227 -0
  265. package/template/wall-e/coding/provider-compat.js +15 -0
  266. package/template/wall-e/coding/runtime-events.js +224 -0
  267. package/template/wall-e/coding/runtime-mode.js +3 -0
  268. package/template/wall-e/coding/side-git-snapshot.js +160 -4
  269. package/template/wall-e/coding/snapshot-service.js +143 -1
  270. package/template/wall-e/coding/stream-processor.js +388 -34
  271. package/template/wall-e/coding/task-tool.js +141 -4
  272. package/template/wall-e/coding/tool-execution-controller.js +365 -0
  273. package/template/wall-e/coding/tool-registry.js +43 -5
  274. package/template/wall-e/coding/user-hooks.js +217 -0
  275. package/template/wall-e/coding-orchestrator.js +1330 -221
  276. package/template/wall-e/coding-prompts.js +20 -4
  277. package/template/wall-e/context/context-builder.js +15 -2
  278. package/template/wall-e/decision/confidence.js +1 -1
  279. package/template/wall-e/docs/coding-acceptance-contract.md +41 -0
  280. package/template/wall-e/docs/external-action-controller.md +26 -6
  281. package/template/wall-e/docs/telemetry-lifecycle.md +8 -2
  282. package/template/wall-e/embeddings.js +591 -53
  283. package/template/wall-e/external-action-controller.js +12 -0
  284. package/template/wall-e/http/auth.js +1 -0
  285. package/template/wall-e/http/chat-api.js +46 -11
  286. package/template/wall-e/http/model-admin.js +836 -34
  287. package/template/wall-e/lib/boot-profile.js +88 -0
  288. package/template/wall-e/lib/event-loop-monitor.js +93 -0
  289. package/template/wall-e/lib/service-health.js +194 -0
  290. package/template/wall-e/llm/anthropic.js +130 -5
  291. package/template/wall-e/llm/client.js +266 -63
  292. package/template/wall-e/llm/default-fallback.js +382 -0
  293. package/template/wall-e/llm/health.js +19 -0
  294. package/template/wall-e/llm/message-guard.js +78 -0
  295. package/template/wall-e/llm/model-catalog.js +252 -1
  296. package/template/wall-e/llm/openai.js +26 -4
  297. package/template/wall-e/llm/portkey-sync.js +654 -0
  298. package/template/wall-e/llm/provider-error.js +30 -2
  299. package/template/wall-e/llm/registry.js +5 -1
  300. package/template/wall-e/llm/request-compat.js +67 -0
  301. package/template/wall-e/loops/backfill.js +79 -23
  302. package/template/wall-e/loops/brain-optimize.js +67 -0
  303. package/template/wall-e/loops/ingest.js +25 -10
  304. package/template/wall-e/loops/question-digest.js +160 -0
  305. package/template/wall-e/loops/reflect.js +6 -4
  306. package/template/wall-e/loops/think.js +39 -12
  307. package/template/wall-e/mcp-server.js +318 -36
  308. package/template/wall-e/memory/ctm-context-client.js +52 -14
  309. package/template/wall-e/memory/ctm-operational-context.js +237 -0
  310. package/template/wall-e/memory/ctm-prompt-executions-client.js +128 -0
  311. package/template/wall-e/memory/ctm-session-context.js +111 -63
  312. package/template/wall-e/prompts/coding/deepseek.txt +3 -0
  313. package/template/wall-e/prompts/coding/gemini.txt +6 -0
  314. package/template/wall-e/prompts/coding/gpt.txt +6 -0
  315. package/template/wall-e/prompts/coding/local.txt +7 -0
  316. package/template/wall-e/runtime/decision-hooks.js +115 -0
  317. package/template/wall-e/runtime/devbox-gateway.js +82 -8
  318. package/template/wall-e/runtime/prompt-manifest.js +86 -0
  319. package/template/wall-e/runtime/tool-executor.js +269 -0
  320. package/template/wall-e/runtime/tool-result-envelope.js +138 -0
  321. package/template/wall-e/runtime/transcript-projection.js +60 -0
  322. package/template/wall-e/runtime/walle-runtime.js +224 -0
  323. package/template/wall-e/scripts/db-optimize/migrate.js +162 -0
  324. package/template/wall-e/scripts/db-optimize/recall-eval.js +117 -0
  325. package/template/wall-e/server.js +15 -0
  326. package/template/wall-e/session-files.js +9 -0
  327. package/template/wall-e/skills/_bundled/google-calendar/run.js +1 -1
  328. package/template/wall-e/skills/_bundled/gws-workspace/run.js +1 -1
  329. package/template/wall-e/skills/_bundled/slack-mentions/run.js +76 -6
  330. package/template/wall-e/skills/claude-code-reader.js +7 -3
  331. package/template/wall-e/skills/script-skill-runner.js +10 -0
  332. package/template/wall-e/skills/skill-planner.js +38 -0
  333. package/template/wall-e/tools/builtin-middleware.js +19 -9
  334. package/template/wall-e/tools/local-tools.js +1428 -16
  335. package/template/wall-e/tools/permission-checker.js +73 -5
  336. package/template/wall-e/tools/question-manager.js +117 -7
  337. package/template/wall-e/training/harvester.js +12 -28
  338. package/template/wall-e/training/replay.js +25 -80
  339. package/template/website/index.html +10 -10
  340. package/template/wall-e/eval/ab-test.js +0 -203
  341. package/template/wall-e/eval/agent-runner.js +0 -772
  342. package/template/wall-e/eval/agent-scorer.js +0 -461
  343. package/template/wall-e/eval/aggregator.js +0 -414
  344. package/template/wall-e/eval/allowed-test-commands.js +0 -34
  345. package/template/wall-e/eval/benchmark-generator.js +0 -113
  346. package/template/wall-e/eval/benchmarks/chat-eval.json +0 -1662
  347. package/template/wall-e/eval/benchmarks/chat.json +0 -82
  348. package/template/wall-e/eval/benchmarks/coding-agent-real.json +0 -1
  349. package/template/wall-e/eval/benchmarks/coding-agent.json +0 -1581
  350. package/template/wall-e/eval/benchmarks/coding.json +0 -122
  351. package/template/wall-e/eval/benchmarks/memory-retrieval.json +0 -234
  352. package/template/wall-e/eval/benchmarks/reasoning.json +0 -82
  353. package/template/wall-e/eval/benchmarks/swebench-lite-30.json +0 -212
  354. package/template/wall-e/eval/benchmarks.js +0 -669
  355. package/template/wall-e/eval/cc-replay.js +0 -719
  356. package/template/wall-e/eval/chat-eval.js +0 -525
  357. package/template/wall-e/eval/check-keys.js +0 -15
  358. package/template/wall-e/eval/check-providers.js +0 -42
  359. package/template/wall-e/eval/codex-cli-baseline.js +0 -669
  360. package/template/wall-e/eval/coding-agent-real.js +0 -570
  361. package/template/wall-e/eval/context-compactor.js +0 -251
  362. package/template/wall-e/eval/debug-agent003.js +0 -68
  363. package/template/wall-e/eval/diagnostics.js +0 -216
  364. package/template/wall-e/eval/eval-orchestrator.js +0 -642
  365. package/template/wall-e/eval/evaluate.js +0 -202
  366. package/template/wall-e/eval/evaluator.js +0 -373
  367. package/template/wall-e/eval/exporter.js +0 -212
  368. package/template/wall-e/eval/fixtures/express-basic/package.json +0 -9
  369. package/template/wall-e/eval/fixtures/express-basic/server.js +0 -115
  370. package/template/wall-e/eval/fixtures/express-basic/test.js +0 -83
  371. package/template/wall-e/eval/fixtures/express-buggy/package.json +0 -9
  372. package/template/wall-e/eval/fixtures/express-buggy/server.js +0 -113
  373. package/template/wall-e/eval/fixtures/express-buggy/test.js +0 -83
  374. package/template/wall-e/eval/fixtures/express-buggy-items/package.json +0 -9
  375. package/template/wall-e/eval/fixtures/express-buggy-items/server.js +0 -112
  376. package/template/wall-e/eval/fixtures/express-buggy-items/test.js +0 -83
  377. package/template/wall-e/eval/fixtures/express-buggy-search/package.json +0 -9
  378. package/template/wall-e/eval/fixtures/express-buggy-search/server.js +0 -121
  379. package/template/wall-e/eval/fixtures/express-buggy-search/test.js +0 -83
  380. package/template/wall-e/eval/fixtures/express-rename-data/data.js +0 -34
  381. package/template/wall-e/eval/fixtures/express-rename-data/package.json +0 -9
  382. package/template/wall-e/eval/fixtures/express-rename-data/server.js +0 -97
  383. package/template/wall-e/eval/fixtures/express-rename-data/test.js +0 -88
  384. package/template/wall-e/eval/fixtures/express-xss/package.json +0 -12
  385. package/template/wall-e/eval/fixtures/express-xss/server.js +0 -90
  386. package/template/wall-e/eval/fixtures/express-xss/test.js +0 -67
  387. package/template/wall-e/eval/fixtures/express-xss/views/profile.ejs +0 -9
  388. package/template/wall-e/eval/fixtures/fullstack-app/config/default.js +0 -9
  389. package/template/wall-e/eval/fixtures/fullstack-app/config/test.js +0 -13
  390. package/template/wall-e/eval/fixtures/fullstack-app/package.json +0 -11
  391. package/template/wall-e/eval/fixtures/fullstack-app/public/css/style.css +0 -137
  392. package/template/wall-e/eval/fixtures/fullstack-app/public/index.html +0 -46
  393. package/template/wall-e/eval/fixtures/fullstack-app/public/js/app.js +0 -121
  394. package/template/wall-e/eval/fixtures/fullstack-app/public/js/auth.js +0 -71
  395. package/template/wall-e/eval/fixtures/fullstack-app/public/js/items.js +0 -80
  396. package/template/wall-e/eval/fixtures/fullstack-app/public/js/users.js +0 -46
  397. package/template/wall-e/eval/fixtures/fullstack-app/public/login.html +0 -45
  398. package/template/wall-e/eval/fixtures/fullstack-app/public/register.html +0 -38
  399. package/template/wall-e/eval/fixtures/fullstack-app/scripts/migrate.js +0 -23
  400. package/template/wall-e/eval/fixtures/fullstack-app/scripts/seed.js +0 -46
  401. package/template/wall-e/eval/fixtures/fullstack-app/server/db.js +0 -99
  402. package/template/wall-e/eval/fixtures/fullstack-app/server/index.js +0 -94
  403. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/auth.js +0 -19
  404. package/template/wall-e/eval/fixtures/fullstack-app/server/middleware/logger.js +0 -19
  405. package/template/wall-e/eval/fixtures/fullstack-app/server/router.js +0 -50
  406. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/auth.js +0 -69
  407. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/health.js +0 -23
  408. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/items.js +0 -88
  409. package/template/wall-e/eval/fixtures/fullstack-app/server/routes/users.js +0 -75
  410. package/template/wall-e/eval/fixtures/fullstack-app/server/test.js +0 -198
  411. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/response.js +0 -34
  412. package/template/wall-e/eval/fixtures/fullstack-app/server/utils/validate.js +0 -26
  413. package/template/wall-e/eval/fixtures/fullstack-app/server.js +0 -8
  414. package/template/wall-e/eval/fixtures/fullstack-app/test.js +0 -12
  415. package/template/wall-e/eval/fixtures/monorepo-basic/package.json +0 -8
  416. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/data.js +0 -58
  417. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/middleware.js +0 -46
  418. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/package.json +0 -8
  419. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/routes.js +0 -64
  420. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/server.js +0 -56
  421. package/template/wall-e/eval/fixtures/monorepo-basic/packages/api/test.js +0 -116
  422. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/commands.js +0 -61
  423. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/index.js +0 -62
  424. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/output.js +0 -43
  425. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/package.json +0 -11
  426. package/template/wall-e/eval/fixtures/monorepo-basic/packages/cli/test.js +0 -44
  427. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/formatters.js +0 -43
  428. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/index.js +0 -12
  429. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/package.json +0 -5
  430. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/test.js +0 -55
  431. package/template/wall-e/eval/fixtures/monorepo-basic/packages/shared/validators.js +0 -29
  432. package/template/wall-e/eval/fixtures/monorepo-basic/test.js +0 -46
  433. package/template/wall-e/eval/fixtures/node-cli/index.js +0 -78
  434. package/template/wall-e/eval/fixtures/node-cli/package.json +0 -10
  435. package/template/wall-e/eval/fixtures/node-cli/test.js +0 -57
  436. package/template/wall-e/eval/fixtures/node-typed/package.json +0 -8
  437. package/template/wall-e/eval/fixtures/node-typed/src/handlers.js +0 -31
  438. package/template/wall-e/eval/fixtures/node-typed/src/utils.js +0 -33
  439. package/template/wall-e/eval/fixtures/node-typed/test.js +0 -36
  440. package/template/wall-e/eval/fixtures/python-flask/app.py +0 -14
  441. package/template/wall-e/eval/fixtures/python-flask/requirements.txt +0 -2
  442. package/template/wall-e/eval/fixtures/python-flask/test_app.py +0 -25
  443. package/template/wall-e/eval/fixtures/wall-e-subset/brain.js +0 -105
  444. package/template/wall-e/eval/fixtures/wall-e-subset/eval/aggregator.js +0 -101
  445. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/chat.json +0 -20
  446. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks/coding.json +0 -32
  447. package/template/wall-e/eval/fixtures/wall-e-subset/eval/benchmarks.js +0 -64
  448. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/package.json +0 -6
  449. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/server.js +0 -31
  450. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/test.js +0 -18
  451. package/template/wall-e/eval/fixtures/wall-e-subset/eval/fixtures/simple-project/utils.js +0 -34
  452. package/template/wall-e/eval/fixtures/wall-e-subset/eval/runner.js +0 -104
  453. package/template/wall-e/eval/fixtures/wall-e-subset/eval/scorer.js +0 -73
  454. package/template/wall-e/eval/fixtures/wall-e-subset/eval/test.js +0 -134
  455. package/template/wall-e/eval/fixtures/wall-e-subset/llm/client.js +0 -99
  456. package/template/wall-e/eval/fixtures/wall-e-subset/llm/providers.js +0 -63
  457. package/template/wall-e/eval/fixtures/wall-e-subset/llm/test.js +0 -70
  458. package/template/wall-e/eval/fixtures/wall-e-subset/package.json +0 -10
  459. package/template/wall-e/eval/fixtures/wall-e-subset/test.js +0 -86
  460. package/template/wall-e/eval/harvester.js +0 -685
  461. package/template/wall-e/eval/head-to-head.js +0 -388
  462. package/template/wall-e/eval/humaneval-adapter.js +0 -321
  463. package/template/wall-e/eval/list-models.js +0 -31
  464. package/template/wall-e/eval/livecodebench-adapter.js +0 -291
  465. package/template/wall-e/eval/mail-integration.js +0 -443
  466. package/template/wall-e/eval/manifest.js +0 -186
  467. package/template/wall-e/eval/meta-harness/adapters/coding-agent.js +0 -57
  468. package/template/wall-e/eval/meta-harness/bootstrap-snapshot.js +0 -149
  469. package/template/wall-e/eval/meta-harness/candidate-store.js +0 -117
  470. package/template/wall-e/eval/meta-harness/cli.js +0 -86
  471. package/template/wall-e/eval/meta-harness/domain-spec.js +0 -154
  472. package/template/wall-e/eval/meta-harness/domains/coding-agent.domain.json +0 -84
  473. package/template/wall-e/eval/meta-harness/examples/env-bootstrap-candidate.js +0 -29
  474. package/template/wall-e/eval/meta-harness/experience-store.js +0 -174
  475. package/template/wall-e/eval/meta-harness/frontier.js +0 -96
  476. package/template/wall-e/eval/meta-harness/harness-interface.js +0 -90
  477. package/template/wall-e/eval/meta-harness/leakage-guard.js +0 -80
  478. package/template/wall-e/eval/meta-harness/optimizer.js +0 -207
  479. package/template/wall-e/eval/meta-harness/proposer-runner.js +0 -110
  480. package/template/wall-e/eval/meta-harness/reporting.js +0 -58
  481. package/template/wall-e/eval/meta-harness/telemetry.js +0 -27
  482. package/template/wall-e/eval/meta-harness/validation.js +0 -81
  483. package/template/wall-e/eval/promoter.js +0 -228
  484. package/template/wall-e/eval/provider-normalizer.js +0 -33
  485. package/template/wall-e/eval/replay.js +0 -395
  486. package/template/wall-e/eval/run-agent-benchmarks.js +0 -386
  487. package/template/wall-e/eval/run-codex-cli-baseline.js +0 -177
  488. package/template/wall-e/eval/run-coding-agent-real.js +0 -187
  489. package/template/wall-e/eval/run-eval.js +0 -435
  490. package/template/wall-e/eval/run-model-comparison.js +0 -142
  491. package/template/wall-e/eval/session-evaluator.js +0 -187
  492. package/template/wall-e/eval/session-miner.js +0 -207
  493. package/template/wall-e/eval/session-retrieval-benchmark.js +0 -150
  494. package/template/wall-e/eval/session-transcripts.js +0 -509
  495. package/template/wall-e/eval/shadow.js +0 -161
  496. package/template/wall-e/eval/swebench-adapter.js +0 -345
  497. package/template/wall-e/eval/swebench-docker.js +0 -192
  498. package/template/wall-e/eval/train.py +0 -320
  499. package/template/wall-e/eval/trainer.js +0 -232
  500. package/template/wall-e/eval/weekly-eval-loop.js +0 -241
@@ -1,7 +1,276 @@
1
1
  'use strict';
2
- const { execFile } = require('child_process');
2
+ const { execFile, spawn } = require('child_process');
3
3
  const path = require('path');
4
4
 
5
+ const DEFAULT_GIT_TIMEOUT_MS = _positiveInt(process.env.CTM_GIT_TIMEOUT_MS, 15000);
6
+ const DEFAULT_WORKTREE_CREATE_TIMEOUT_MS = Math.max(
7
+ DEFAULT_GIT_TIMEOUT_MS,
8
+ _positiveInt(process.env.CTM_WORKTREE_CREATE_TIMEOUT_MS, 120000)
9
+ );
10
+ const WORKTREE_CLEANUP_TIMEOUT_MS = Math.max(
11
+ DEFAULT_GIT_TIMEOUT_MS,
12
+ _positiveInt(process.env.CTM_WORKTREE_CLEANUP_TIMEOUT_MS, 60000)
13
+ );
14
+ const GIT_FORCE_KILL_DELAY_MS = 2500;
15
+
16
+ function _positiveInt(value, fallback) {
17
+ const n = Number(value);
18
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
19
+ }
20
+
21
+ function _normalizeGitOptions(maxBuffer, options) {
22
+ if (maxBuffer && typeof maxBuffer === 'object') {
23
+ options = options || {};
24
+ return {
25
+ maxBuffer: maxBuffer.maxBuffer || 1024 * 1024 * 5,
26
+ timeoutMs: _positiveInt(maxBuffer.timeoutMs ?? options.timeoutMs, DEFAULT_GIT_TIMEOUT_MS),
27
+ operation: maxBuffer.operation || options.operation || '',
28
+ timeoutCode: maxBuffer.timeoutCode || options.timeoutCode || 'GIT_COMMAND_TIMEOUT',
29
+ cancelCode: maxBuffer.cancelCode || options.cancelCode || 'GIT_COMMAND_CANCELLED',
30
+ allowStdoutOnError: !!(maxBuffer.allowStdoutOnError || options.allowStdoutOnError),
31
+ signal: maxBuffer.signal || options.signal || null,
32
+ onProgress: typeof maxBuffer.onProgress === 'function' ? maxBuffer.onProgress : options.onProgress,
33
+ heartbeatMs: _positiveInt(maxBuffer.heartbeatMs ?? options.heartbeatMs, 1000),
34
+ };
35
+ }
36
+ options = options || {};
37
+ return {
38
+ maxBuffer: maxBuffer || 1024 * 1024 * 5,
39
+ timeoutMs: _positiveInt(options.timeoutMs, DEFAULT_GIT_TIMEOUT_MS),
40
+ operation: options.operation || '',
41
+ timeoutCode: options.timeoutCode || 'GIT_COMMAND_TIMEOUT',
42
+ cancelCode: options.cancelCode || 'GIT_COMMAND_CANCELLED',
43
+ allowStdoutOnError: !!options.allowStdoutOnError,
44
+ signal: options.signal || null,
45
+ onProgress: typeof options.onProgress === 'function' ? options.onProgress : null,
46
+ heartbeatMs: _positiveInt(options.heartbeatMs, 1000),
47
+ };
48
+ }
49
+
50
+ function _isExecTimeout(err) {
51
+ if (!err) return false;
52
+ if (err.timedOut === true) return true;
53
+ return err.killed === true
54
+ || err.code === 'ETIMEDOUT'
55
+ || err.signal === 'SIGTERM';
56
+ }
57
+
58
+ function _isExecCancelled(err) {
59
+ if (!err) return false;
60
+ return err.cancelled === true || err.name === 'AbortError';
61
+ }
62
+
63
+ function _gitCommandForMessage(args) {
64
+ return `git ${args.map(String).join(' ')}`;
65
+ }
66
+
67
+ function _wrapGitError(err, args, opts, stdout, stderr) {
68
+ const cancelled = _isExecCancelled(err);
69
+ const timedOut = !cancelled && _isExecTimeout(err);
70
+ const command = _gitCommandForMessage(args);
71
+ const rawMessage = String(stderr || stdout || err?.message || 'git command failed').trim();
72
+ const message = cancelled
73
+ ? `${opts.operation || command} was cancelled`
74
+ : timedOut
75
+ ? `${opts.operation || command} timed out after ${opts.timeoutMs}ms`
76
+ : (rawMessage || `${command} failed`);
77
+ const wrapped = new Error(message);
78
+ wrapped.code = cancelled ? opts.cancelCode : timedOut ? opts.timeoutCode : err?.code;
79
+ wrapped.originalCode = err?.code;
80
+ wrapped.signal = err?.signal;
81
+ wrapped.killed = err?.killed === true;
82
+ wrapped.timedOut = timedOut;
83
+ wrapped.cancelled = cancelled;
84
+ wrapped.timeoutMs = opts.timeoutMs;
85
+ wrapped.gitArgs = args.slice();
86
+ wrapped.gitCommand = command;
87
+ wrapped.stdout = stdout || '';
88
+ wrapped.stderr = stderr || '';
89
+ return wrapped;
90
+ }
91
+
92
+ function _emitGitProgress(onProgress, event) {
93
+ if (typeof onProgress !== 'function') return;
94
+ try {
95
+ onProgress(event);
96
+ } catch (_) {}
97
+ }
98
+
99
+ function _runGitSpawn(cwd, args, maxBuffer, options) {
100
+ const opts = _normalizeGitOptions(maxBuffer, options);
101
+ const command = _gitCommandForMessage(args);
102
+ return new Promise((resolve, reject) => {
103
+ const startedAt = Date.now();
104
+ let child = null;
105
+ let stdout = '';
106
+ let stderr = '';
107
+ let stdoutBytes = 0;
108
+ let stderrBytes = 0;
109
+ let lastOutput = '';
110
+ let lastOutputAt = startedAt;
111
+ let settled = false;
112
+ let timedOut = false;
113
+ let cancelled = false;
114
+ let bufferOverflow = false;
115
+ let timeoutTimer = null;
116
+ let heartbeatTimer = null;
117
+ let forceKillTimer = null;
118
+
119
+ function elapsedMs() {
120
+ return Date.now() - startedAt;
121
+ }
122
+
123
+ function progress(extra) {
124
+ _emitGitProgress(opts.onProgress, {
125
+ type: 'heartbeat',
126
+ status: cancelled ? 'cancelling' : 'running',
127
+ operation: opts.operation || command,
128
+ gitCommand: command,
129
+ gitArgs: args.slice(),
130
+ pid: child && child.pid,
131
+ startedAt,
132
+ elapsedMs: elapsedMs(),
133
+ quietMs: Date.now() - lastOutputAt,
134
+ lastOutputAt,
135
+ lastOutput,
136
+ ...(extra || {}),
137
+ });
138
+ }
139
+
140
+ function cleanupListeners() {
141
+ if (timeoutTimer) clearTimeout(timeoutTimer);
142
+ if (heartbeatTimer) clearInterval(heartbeatTimer);
143
+ if (forceKillTimer) clearTimeout(forceKillTimer);
144
+ if (opts.signal && abortListener) {
145
+ try { opts.signal.removeEventListener('abort', abortListener); } catch (_) {}
146
+ }
147
+ }
148
+
149
+ function finish(err) {
150
+ if (settled) return;
151
+ settled = true;
152
+ cleanupListeners();
153
+ if (err) reject(err);
154
+ else resolve(stdout);
155
+ }
156
+
157
+ function killChild(reason) {
158
+ if (!child || child.killed) return;
159
+ progress({
160
+ type: reason || 'kill',
161
+ status: cancelled ? 'cancelling' : timedOut ? 'timeout' : 'running',
162
+ message: cancelled ? 'Cancel requested; stopping git checkout.' : timedOut ? 'Checkout exceeded the safety timeout; stopping git.' : 'Stopping git.',
163
+ });
164
+ try { child.kill('SIGTERM'); } catch (_) {}
165
+ forceKillTimer = setTimeout(() => {
166
+ if (!settled && child && !child.killed) {
167
+ try { child.kill('SIGKILL'); } catch (_) {}
168
+ }
169
+ }, GIT_FORCE_KILL_DELAY_MS);
170
+ }
171
+
172
+ const abortListener = () => {
173
+ cancelled = true;
174
+ killChild('cancel');
175
+ };
176
+
177
+ if (opts.signal && opts.signal.aborted) {
178
+ const err = _wrapGitError({ cancelled: true }, args, opts, stdout, stderr);
179
+ return finish(err);
180
+ }
181
+
182
+ try {
183
+ child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
184
+ } catch (err) {
185
+ return finish(_wrapGitError(err, args, opts, stdout, stderr));
186
+ }
187
+
188
+ if (opts.signal) {
189
+ try { opts.signal.addEventListener('abort', abortListener, { once: true }); } catch (_) {}
190
+ }
191
+
192
+ progress({ type: 'start', message: `Started ${opts.operation || command}` });
193
+
194
+ function append(stream, chunk) {
195
+ if (settled) return;
196
+ const text = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk || '');
197
+ const bytes = Buffer.byteLength(text);
198
+ if (stream === 'stdout') {
199
+ stdout += text;
200
+ stdoutBytes += bytes;
201
+ } else {
202
+ stderr += text;
203
+ stderrBytes += bytes;
204
+ }
205
+ const trimmed = text.trim();
206
+ if (trimmed) lastOutput = trimmed.split(/\r?\n/).slice(-1)[0].slice(0, 1000);
207
+ lastOutputAt = Date.now();
208
+ progress({ type: stream, stream, text: trimmed.slice(0, 2000), lastOutput });
209
+ if (stdoutBytes + stderrBytes > opts.maxBuffer) {
210
+ bufferOverflow = true;
211
+ killChild('maxBuffer');
212
+ }
213
+ }
214
+
215
+ child.stdout.on('data', chunk => append('stdout', chunk));
216
+ child.stderr.on('data', chunk => append('stderr', chunk));
217
+ child.on('error', err => finish(_wrapGitError(err, args, opts, stdout, stderr)));
218
+ child.on('close', (code, signal) => {
219
+ if (bufferOverflow) {
220
+ const err = new Error(`${opts.operation || command} exceeded output buffer`);
221
+ err.code = 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER';
222
+ err.signal = signal;
223
+ finish(_wrapGitError(err, args, opts, stdout, stderr));
224
+ return;
225
+ }
226
+ if (cancelled) {
227
+ const err = _wrapGitError({ cancelled: true, signal, killed: true }, args, opts, stdout, stderr);
228
+ finish(err);
229
+ return;
230
+ }
231
+ if (timedOut) {
232
+ const err = _wrapGitError({ timedOut: true, signal, killed: true }, args, opts, stdout, stderr);
233
+ finish(err);
234
+ return;
235
+ }
236
+ if (code !== 0) {
237
+ const err = new Error(stderr || stdout || `${command} exited with code ${code}`);
238
+ err.code = code;
239
+ err.signal = signal;
240
+ finish(_wrapGitError(err, args, opts, stdout, stderr));
241
+ return;
242
+ }
243
+ progress({ type: 'complete', status: 'succeeded', message: `${opts.operation || command} completed` });
244
+ finish();
245
+ });
246
+
247
+ if (opts.timeoutMs > 0) {
248
+ timeoutTimer = setTimeout(() => {
249
+ timedOut = true;
250
+ killChild('timeout');
251
+ }, opts.timeoutMs);
252
+ }
253
+ if (opts.heartbeatMs > 0) {
254
+ heartbeatTimer = setInterval(() => {
255
+ if (!settled) progress({ type: 'heartbeat' });
256
+ }, opts.heartbeatMs);
257
+ if (heartbeatTimer.unref) heartbeatTimer.unref();
258
+ }
259
+ });
260
+ }
261
+
262
+ function _runGit(cwd, args, maxBuffer, options) {
263
+ const opts = _normalizeGitOptions(maxBuffer, options);
264
+ return new Promise((resolve, reject) => {
265
+ execFile('git', args, { cwd, maxBuffer: opts.maxBuffer, timeout: opts.timeoutMs }, (err, stdout, stderr) => {
266
+ if (!err) return resolve(stdout);
267
+ const wrapped = _wrapGitError(err, args, opts, stdout, stderr);
268
+ if (opts.allowStdoutOnError && stdout && !wrapped.timedOut) return resolve(stdout);
269
+ reject(wrapped);
270
+ });
271
+ });
272
+ }
273
+
5
274
  function _worktreeNamespace(options) {
6
275
  const raw = typeof options === 'string'
7
276
  ? options
@@ -16,11 +285,19 @@ function _worktreeParentDir(cwd, options) {
16
285
  }
17
286
 
18
287
  // Run a git command in a given project directory
19
- function git(cwd, args, maxBuffer = 1024 * 1024 * 5) {
20
- return new Promise((resolve, reject) => {
21
- execFile('git', args, { cwd, maxBuffer, timeout: 15000 }, (err, stdout, stderr) => {
22
- if (err && !stdout) return reject(err);
23
- resolve(stdout);
288
+ function git(cwd, args, maxBuffer = 1024 * 1024 * 5, options) {
289
+ return _runGit(cwd, args, maxBuffer, { ...(options || {}), allowStdoutOnError: true });
290
+ }
291
+
292
+ function gitStrict(cwd, args, maxBuffer = 1024 * 1024 * 5, options) {
293
+ return _runGit(cwd, args, maxBuffer, options);
294
+ }
295
+
296
+ function gitExitCode(cwd, args, maxBuffer = 1024 * 1024 * 5) {
297
+ return new Promise((resolve) => {
298
+ const opts = _normalizeGitOptions(maxBuffer, {});
299
+ execFile('git', args, { cwd, maxBuffer: opts.maxBuffer, timeout: opts.timeoutMs }, (err) => {
300
+ resolve(err ? (_isExecTimeout(err) ? 124 : Number(err.code || 1)) : 0);
24
301
  });
25
302
  });
26
303
  }
@@ -142,6 +419,117 @@ async function getStagedDiff(cwd) {
142
419
  return parseDiff(raw);
143
420
  }
144
421
 
422
+ // --- Stable per-repo git fact cache --------------------------------------
423
+ // resolveMainBranch / getDefaultReviewBase / getDefaultDiffStat each spawn
424
+ // 3-7 git subprocesses. A CPU profile of the live primary showed ~8.4s of
425
+ // synchronous spawn() fork/exec prefix on the main loop over ~4 min — fanned
426
+ // out by the Review tab/chooser polling these on demand AND the 30s
427
+ // file-change sweep calling getDefaultDiffStat per project. The underlying
428
+ // facts (main branch, merge-base, diff stat) are stable for seconds, so a
429
+ // short per-cwd TTL cache collapses the fan-out. The IN-FLIGHT promise is
430
+ // cached (not just the resolved value) so a burst of concurrent callers for
431
+ // the same repo shares ONE git invocation. Disable with CTM_GIT_CACHE=0.
432
+ const _GIT_CACHE_ENABLED = process.env.CTM_GIT_CACHE !== '0';
433
+ function _ttlPromiseCache(ttlMs) {
434
+ const m = new Map(); // key -> { at, p }
435
+ const get = (key, producer) => {
436
+ if (!_GIT_CACHE_ENABLED || ttlMs <= 0) return Promise.resolve().then(producer);
437
+ const now = Date.now();
438
+ const e = m.get(key);
439
+ if (e && (now - e.at) < ttlMs) return e.p;
440
+ const p = Promise.resolve().then(producer);
441
+ m.set(key, { at: now, p });
442
+ // Drop the entry if the producer rejects so the next call retries instead
443
+ // of pinning a failed result for the whole TTL window.
444
+ p.catch(() => { const cur = m.get(key); if (cur && cur.p === p) m.delete(key); });
445
+ return p;
446
+ };
447
+ get.clear = () => m.clear();
448
+ return get;
449
+ }
450
+ const _MAIN_BRANCH_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_MAIN_BRANCH_TTL_MS) || 60000);
451
+ const _REVIEW_BASE_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_REVIEW_BASE_TTL_MS) || 5000);
452
+ const _DIFF_STAT_TTL_MS = Math.max(0, Number(process.env.CTM_GIT_DIFF_STAT_TTL_MS) || 3000);
453
+ const _mainBranchCache = _ttlPromiseCache(_MAIN_BRANCH_TTL_MS);
454
+ const _reviewBaseCache = _ttlPromiseCache(_REVIEW_BASE_TTL_MS);
455
+ const _diffStatCache = _ttlPromiseCache(_DIFF_STAT_TTL_MS);
456
+ // Drop cached facts (all repos) after an operation that changes branch/HEAD/
457
+ // working tree so the next read reflects it immediately.
458
+ function _invalidateGitFactCache() {
459
+ _mainBranchCache.clear(); _reviewBaseCache.clear(); _diffStatCache.clear();
460
+ }
461
+
462
+ // Resolve the repo's main branch: origin/HEAD target, else local main, else master.
463
+ async function resolveMainBranch(cwd) {
464
+ return _mainBranchCache(cwd || '', () => _resolveMainBranchUncached(cwd));
465
+ }
466
+ async function _resolveMainBranchUncached(cwd) {
467
+ const originHead = await _gitSafe(cwd, ['symbolic-ref', '--quiet', '--short', 'refs/remotes/origin/HEAD']);
468
+ if (originHead) {
469
+ const b = originHead.replace(/^origin\//, '').trim();
470
+ if (b) return b;
471
+ }
472
+ for (const cand of ['main', 'master']) {
473
+ if (await _gitSafe(cwd, ['rev-parse', '--verify', '--quiet', cand])) return cand;
474
+ }
475
+ return 'main';
476
+ }
477
+
478
+ // Decide the default review base for a project (the PR-style "all branch work" model):
479
+ // - on a branch ahead of / diverged from main → diff working tree vs merge-base(main, HEAD)
480
+ // - on main / detached / no main → all uncommitted work (vs HEAD)
481
+ // Returns { kind:'vs-main'|'uncommitted', sentinel, ref, mainBranch, mergeBase }.
482
+ // `sentinel` is the value the client puts in crState.baseRef; `ref` is the concrete git ref.
483
+ async function getDefaultReviewBase(cwd) {
484
+ return _reviewBaseCache(cwd || '', () => _getDefaultReviewBaseUncached(cwd));
485
+ }
486
+ async function _getDefaultReviewBaseUncached(cwd) {
487
+ const mainBranch = await resolveMainBranch(cwd);
488
+ const head = await _gitSafe(cwd, ['rev-parse', 'HEAD']);
489
+ const mainSha = await _gitSafe(cwd, ['rev-parse', '--verify', '--quiet', mainBranch]);
490
+ if (head && mainSha && head !== mainSha) {
491
+ const mergeBase = await _gitSafe(cwd, ['merge-base', mainBranch, 'HEAD']);
492
+ if (mergeBase && mergeBase !== head) {
493
+ return { kind: 'vs-main', sentinel: '--vs-main', ref: mergeBase, mainBranch, mergeBase };
494
+ }
495
+ }
496
+ return { kind: 'uncommitted', sentinel: '--uncommitted', ref: 'HEAD', mainBranch, mergeBase: null };
497
+ }
498
+
499
+ // Working tree vs an arbitrary ref (merge-base SHA or HEAD) — includes committed +
500
+ // staged + unstaged relative to that ref. Distinct from getFullDiff's commit mode,
501
+ // which shows a single commit's parent..commit diff.
502
+ async function getDiffVsRef(cwd, ref) {
503
+ const raw = await git(cwd, ['diff', '--relative', '-U5', ref]);
504
+ return parseDiff(raw);
505
+ }
506
+ async function getDiffStatVsRef(cwd, ref) {
507
+ const out = await git(cwd, ['diff', '--relative', '--stat', '--numstat', ref]);
508
+ return parseNumstat(out);
509
+ }
510
+
511
+ // Diff-stat for the default review base (used so the chooser badge matches what opens).
512
+ async function getDefaultDiffStat(cwd) {
513
+ return _diffStatCache(cwd || '', () => _getDefaultDiffStatUncached(cwd));
514
+ }
515
+ async function _getDefaultDiffStatUncached(cwd) {
516
+ const base = await getDefaultReviewBase(cwd);
517
+ const files = await getDiffStatVsRef(cwd, base.ref);
518
+ return { base, files };
519
+ }
520
+
521
+ // Read a file's full contents for the "new" side of a diff, so the UI can expand context
522
+ // lines hidden between hunks. For a commit SHA base the new side is the file at that commit;
523
+ // for staged it's the index version; otherwise (working tree / vs-main / uncommitted) it's
524
+ // the current working-tree file on disk.
525
+ async function getFileAtRef(cwd, relPath, ref) {
526
+ if (relPath.includes('..') || relPath.startsWith('/')) throw new Error('invalid path');
527
+ if (ref && /^[0-9a-f]{7,40}$/.test(ref)) return git(cwd, ['show', `${ref}:${relPath}`]);
528
+ if (ref === '--staged') return git(cwd, ['show', `:${relPath}`]);
529
+ const fs = require('fs'); const path = require('path');
530
+ return fs.promises.readFile(path.join(cwd, relPath), 'utf8');
531
+ }
532
+
145
533
  // Parse unified diff format into structured data
146
534
  function parseDiff(raw) {
147
535
  const files = [];
@@ -305,6 +693,144 @@ async function _branchExists(cwd, branch) {
305
693
  }
306
694
  }
307
695
 
696
+ function _formatDuration(ms) {
697
+ if (ms >= 1000 && ms % 1000 === 0) return `${ms / 1000}s`;
698
+ if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`;
699
+ return `${ms}ms`;
700
+ }
701
+
702
+ async function _cleanupPartialWorktree(cwd, worktreePath, branchName, options = {}) {
703
+ const fs = require('fs');
704
+ const cleanup = {
705
+ attempted: true,
706
+ worktreeRemoved: false,
707
+ pathRemoved: false,
708
+ pathAlreadyGone: false,
709
+ pruned: false,
710
+ branchDeleted: false,
711
+ errors: [],
712
+ };
713
+
714
+ await new Promise(resolve => setTimeout(resolve, 150));
715
+
716
+ try {
717
+ await gitStrict(cwd, ['worktree', 'remove', '--force', worktreePath], 1024 * 1024, {
718
+ timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
719
+ operation: `cleanup partial worktree ${worktreePath}`,
720
+ });
721
+ cleanup.worktreeRemoved = true;
722
+ } catch (e) {
723
+ const notRegistered = /not a working tree|is not a working tree/i.test(String(e.message || ''));
724
+ if (fs.existsSync(worktreePath)) {
725
+ try {
726
+ fs.rmSync(worktreePath, { recursive: true, force: true });
727
+ cleanup.pathRemoved = true;
728
+ } catch (rmErr) {
729
+ cleanup.errors.push(`path remove: ${rmErr.message}`);
730
+ }
731
+ } else {
732
+ cleanup.pathAlreadyGone = true;
733
+ }
734
+ if (!notRegistered) {
735
+ cleanup.errors.push(`worktree remove: ${e.message}`);
736
+ }
737
+ }
738
+
739
+ try {
740
+ await gitStrict(cwd, ['worktree', 'prune'], 1024 * 1024, {
741
+ timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
742
+ operation: 'prune partial worktree metadata',
743
+ });
744
+ cleanup.pruned = true;
745
+ } catch (e) {
746
+ cleanup.errors.push(`worktree prune: ${e.message}`);
747
+ }
748
+
749
+ if (options.deleteBranch && branchName && await _branchExists(cwd, branchName)) {
750
+ try {
751
+ await gitStrict(cwd, ['branch', '-D', branchName], 1024 * 1024, {
752
+ timeoutMs: WORKTREE_CLEANUP_TIMEOUT_MS,
753
+ operation: `delete partial worktree branch ${branchName}`,
754
+ });
755
+ cleanup.branchDeleted = true;
756
+ } catch (e) {
757
+ cleanup.errors.push(`branch delete: ${e.message}`);
758
+ }
759
+ }
760
+
761
+ return cleanup;
762
+ }
763
+
764
+ async function _gitWorktreeAdd(cwd, args, context) {
765
+ const timeoutMs = _positiveInt(context?.timeoutMs, DEFAULT_WORKTREE_CREATE_TIMEOUT_MS);
766
+ const onProgress = typeof context?.onProgress === 'function' ? context.onProgress : null;
767
+ const branchName = context?.branchName || '';
768
+ const worktreePath = context?.worktreePath || '';
769
+ _emitGitProgress(onProgress, {
770
+ stage: 'git_worktree_add',
771
+ status: 'running',
772
+ branch: branchName,
773
+ worktreePath,
774
+ message: `Starting git worktree checkout for "${branchName}".`,
775
+ });
776
+ try {
777
+ await _runGitSpawn(cwd, args, 1024 * 1024 * 5, {
778
+ timeoutMs,
779
+ operation: `create worktree ${branchName || ''}`.trim(),
780
+ timeoutCode: 'WORKTREE_CREATE_TIMEOUT',
781
+ cancelCode: 'WORKTREE_CREATE_CANCELLED',
782
+ signal: context?.signal,
783
+ heartbeatMs: context?.heartbeatMs || 1000,
784
+ onProgress: (event) => _emitGitProgress(onProgress, {
785
+ stage: 'git_worktree_add',
786
+ branch: branchName,
787
+ worktreePath,
788
+ ...event,
789
+ }),
790
+ });
791
+ } catch (e) {
792
+ e.stage = 'git_worktree_add';
793
+ e.branch = branchName;
794
+ e.worktreePath = worktreePath;
795
+ if (e.code === 'WORKTREE_CREATE_CANCELLED' || e.cancelled) {
796
+ e.code = 'WORKTREE_CREATE_CANCELLED';
797
+ e.statusCode = 499;
798
+ e.cleanup = await _cleanupPartialWorktree(cwd, e.worktreePath, e.branch, {
799
+ deleteBranch: !!context?.deleteCreatedBranch,
800
+ });
801
+ e.message = `Worktree creation was cancelled while checking out "${e.branch}".`;
802
+ e.message += e.cleanup?.errors?.length
803
+ ? ' CTM attempted cleanup, but cleanup was incomplete; run Worktrees > Prune if the branch/path remains.'
804
+ : ' CTM cleaned up the partial checkout; retrying is safe.';
805
+ }
806
+ if (e.code === 'WORKTREE_CREATE_TIMEOUT' || e.timedOut) {
807
+ e.code = 'WORKTREE_CREATE_TIMEOUT';
808
+ e.statusCode = 504;
809
+ e.timeoutMs = timeoutMs;
810
+ e.cleanup = await _cleanupPartialWorktree(cwd, e.worktreePath, e.branch, {
811
+ deleteBranch: !!context?.deleteCreatedBranch,
812
+ });
813
+ e.message = `Worktree creation timed out after ${_formatDuration(timeoutMs)} while checking out "${e.branch}".`;
814
+ e.message += e.cleanup?.errors?.length
815
+ ? ' CTM attempted cleanup, but cleanup was incomplete; run Worktrees > Prune if the branch/path remains.'
816
+ : ' CTM cleaned up the partial checkout; retrying is safe.';
817
+ }
818
+ throw e;
819
+ }
820
+ }
821
+
822
+ function _abortError(message) {
823
+ const err = new Error(message || 'Worktree creation was cancelled.');
824
+ err.code = 'WORKTREE_CREATE_CANCELLED';
825
+ err.cancelled = true;
826
+ err.statusCode = 499;
827
+ return err;
828
+ }
829
+
830
+ function _throwIfAborted(signal) {
831
+ if (signal && signal.aborted) throw _abortError();
832
+ }
833
+
308
834
  // Create or attach to a worktree. The user shouldn't be blocked when a
309
835
  // worktree at this name already exists from a prior session — they almost
310
836
  // always want to reuse it. Behavior:
@@ -322,9 +848,21 @@ async function createWorktree(cwd, name, baseBranch, options) {
322
848
  }
323
849
  const base = baseBranch || 'HEAD';
324
850
  const namespace = _worktreeNamespace(options);
851
+ const createTimeoutMs = _positiveInt(options?.createTimeoutMs, DEFAULT_WORKTREE_CREATE_TIMEOUT_MS);
852
+ const signal = options?.signal || null;
853
+ const onProgress = typeof options?.onProgress === 'function' ? options.onProgress : null;
854
+ const emitProgress = (event) => _emitGitProgress(onProgress, {
855
+ stage: event?.stage || 'create_worktree',
856
+ status: event?.status || 'running',
857
+ requestedName: name,
858
+ namespace,
859
+ ...(event || {}),
860
+ });
861
+ _throwIfAborted(signal);
325
862
  const parentDir = _worktreeParentDir(cwd, namespace);
326
863
  fs.mkdirSync(parentDir, { recursive: true });
327
864
 
865
+ emitProgress({ stage: 'scan_existing', message: 'Inspecting existing worktrees and branches.' });
328
866
  const worktrees = await listWorktrees(cwd).catch(() => []);
329
867
  const _real = (p) => { try { return fs.realpathSync(p); } catch (_) { return p; } };
330
868
 
@@ -333,6 +871,13 @@ async function createWorktree(cwd, name, baseBranch, options) {
333
871
  const initialReal = _real(initialPath);
334
872
  const existingAtPath = worktrees.find(w => _real(w.path) === initialReal);
335
873
  if (existingAtPath && existingAtPath.branch === name) {
874
+ emitProgress({
875
+ stage: 'reuse_existing',
876
+ status: 'succeeded',
877
+ branch: existingAtPath.branch,
878
+ worktreePath: existingAtPath.path,
879
+ message: `Reusing existing worktree "${name}".`,
880
+ });
336
881
  return { path: existingAtPath.path, branch: existingAtPath.branch, reused: true, namespace };
337
882
  }
338
883
 
@@ -340,12 +885,28 @@ async function createWorktree(cwd, name, baseBranch, options) {
340
885
  // was removed but its branch was kept.
341
886
  if (!existingAtPath && !fs.existsSync(initialPath) && await _branchExists(cwd, name)
342
887
  && !worktrees.some(w => w.branch === name)) {
343
- await git(cwd, ['worktree', 'add', initialPath, name]);
888
+ _throwIfAborted(signal);
889
+ await _gitWorktreeAdd(cwd, ['worktree', 'add', initialPath, name], {
890
+ timeoutMs: createTimeoutMs,
891
+ branchName: name,
892
+ worktreePath: initialPath,
893
+ deleteCreatedBranch: false,
894
+ signal,
895
+ onProgress: emitProgress,
896
+ });
897
+ emitProgress({
898
+ stage: 'attach_existing_branch',
899
+ status: 'succeeded',
900
+ branch: name,
901
+ worktreePath: initialPath,
902
+ message: `Attached worktree to existing branch "${name}".`,
903
+ });
344
904
  return { path: initialPath, branch: name, attached: true, namespace };
345
905
  }
346
906
 
347
907
  // Pick a free (path, branch) pair. Suffix until both slots are clear.
348
908
  for (let attempt = 1; attempt <= 20; attempt++) {
909
+ _throwIfAborted(signal);
349
910
  const candidateName = attempt === 1 ? name : `${name}-${attempt}`;
350
911
  const candidatePath = path.join(parentDir, candidateName);
351
912
  const candidateReal = _real(candidatePath);
@@ -353,7 +914,21 @@ async function createWorktree(cwd, name, baseBranch, options) {
353
914
  const branchInUse = await _branchExists(cwd, candidateName);
354
915
  const pathOnDisk = fs.existsSync(candidatePath);
355
916
  if (pathInUse || branchInUse || pathOnDisk) continue;
356
- await git(cwd, ['worktree', 'add', '-b', candidateName, candidatePath, base]);
917
+ await _gitWorktreeAdd(cwd, ['worktree', 'add', '-b', candidateName, candidatePath, base], {
918
+ timeoutMs: createTimeoutMs,
919
+ branchName: candidateName,
920
+ worktreePath: candidatePath,
921
+ deleteCreatedBranch: true,
922
+ signal,
923
+ onProgress: emitProgress,
924
+ });
925
+ emitProgress({
926
+ stage: 'create_branch',
927
+ status: 'succeeded',
928
+ branch: candidateName,
929
+ worktreePath: candidatePath,
930
+ message: `Created worktree "${candidateName}".`,
931
+ });
357
932
  return {
358
933
  path: candidatePath,
359
934
  branch: candidateName,
@@ -412,6 +987,105 @@ async function mergeWorktree(cwd, branchName, targetBranch, strategy) {
412
987
  return { merged: true, branch: branchName, into: targetBranch };
413
988
  }
414
989
 
990
+ function _oneLineCommitMessage(message, fallback) {
991
+ const value = String(message || fallback || 'Finish local work')
992
+ .replace(/[\r\n]+/g, ' ')
993
+ .replace(/\s+/g, ' ')
994
+ .trim()
995
+ .slice(0, 180);
996
+ return value || 'Finish local work';
997
+ }
998
+
999
+ async function commitWorktreeChanges(worktreePath, message) {
1000
+ const status = String(await gitStrict(worktreePath, ['status', '--porcelain=v1', '--untracked-files=all']) || '');
1001
+ if (!status.trim()) {
1002
+ return { committed: false, dirty: false };
1003
+ }
1004
+ await gitStrict(worktreePath, [
1005
+ 'add', '-A', '--', '.',
1006
+ ':(exclude).claude/worktrees',
1007
+ ':(exclude).claude/worktrees/**',
1008
+ ':(exclude).walle/worktrees',
1009
+ ':(exclude).walle/worktrees/**',
1010
+ ]);
1011
+ const stagedExit = await gitExitCode(worktreePath, ['diff', '--cached', '--quiet']);
1012
+ if (stagedExit === 0) {
1013
+ return { committed: false, dirty: true, ignoredOnly: true };
1014
+ }
1015
+ if (stagedExit !== 1) {
1016
+ throw new Error('Could not inspect staged worktree changes before committing.');
1017
+ }
1018
+ const subject = _oneLineCommitMessage(message, 'Finish local work');
1019
+ await gitStrict(worktreePath, ['commit', '-m', subject]);
1020
+ const commit = String(await gitStrict(worktreePath, ['rev-parse', '--short', 'HEAD']) || '').trim();
1021
+ return { committed: true, dirty: true, commit, message: subject };
1022
+ }
1023
+
1024
+ async function _findWorktreeForFinish(cwd, branchName, opts) {
1025
+ const worktreePath = String(opts?.worktreePath || '').trim();
1026
+ const wantedPath = worktreePath ? _realpath(worktreePath) : '';
1027
+ const worktrees = await listWorktrees(cwd);
1028
+ let wt = null;
1029
+ if (wantedPath) {
1030
+ wt = worktrees.find(item => _realpath(item.path) === wantedPath);
1031
+ }
1032
+ if (!wt && branchName) {
1033
+ wt = worktrees.find(item => item.branch === branchName || item.worktreeName === branchName);
1034
+ }
1035
+ if (!wt && branchName === 'main') {
1036
+ wt = worktrees.find(item => item.isMain);
1037
+ }
1038
+ if (!wt) throw new Error(`Worktree not found for branch: ${branchName || worktreePath}`);
1039
+ return wt;
1040
+ }
1041
+
1042
+ async function finishWorktree(cwd, branchName, opts) {
1043
+ opts = opts || {};
1044
+ const targetBranch = opts.targetBranch || 'main';
1045
+ const strategy = opts.strategy;
1046
+ const commitDirty = opts.commitDirty !== false;
1047
+ const wt = await _findWorktreeForFinish(cwd, branchName, opts);
1048
+ const branch = wt.branch || branchName || await getBranch(wt.path);
1049
+ if (!branch || branch === 'HEAD') throw new Error('Cannot finish a detached worktree from the phone. Recover a branch first.');
1050
+
1051
+ let commitResult = { committed: false, dirty: false };
1052
+ const dirtyStatus = String(await gitStrict(wt.path, ['status', '--porcelain=v1', '--untracked-files=all']) || '');
1053
+ if (dirtyStatus.trim()) {
1054
+ if (!commitDirty) {
1055
+ throw new Error('Worktree has uncommitted changes. Commit or stash before merging.');
1056
+ }
1057
+ commitResult = await commitWorktreeChanges(wt.path, opts.commitMessage || `Finish ${branch} work`);
1058
+ }
1059
+
1060
+ if (branch === targetBranch) {
1061
+ return {
1062
+ merged: false,
1063
+ alreadyMain: true,
1064
+ branch,
1065
+ into: targetBranch,
1066
+ worktreePath: wt.path,
1067
+ committed: !!commitResult.committed,
1068
+ commit: commitResult.commit || '',
1069
+ commitMessage: commitResult.message || '',
1070
+ };
1071
+ }
1072
+
1073
+ const pre = await mergeWorktreePreCheck(cwd, branch, targetBranch);
1074
+ if (pre.conflicts) {
1075
+ const err = new Error('Merge would have conflicts. Resolve manually.');
1076
+ err.code = 'MERGE_CONFLICTS';
1077
+ throw err;
1078
+ }
1079
+ const merged = await mergeWorktree(cwd, branch, targetBranch, strategy);
1080
+ return {
1081
+ ...merged,
1082
+ worktreePath: wt.path,
1083
+ committed: !!commitResult.committed,
1084
+ commit: commitResult.commit || '',
1085
+ commitMessage: commitResult.message || '',
1086
+ };
1087
+ }
1088
+
415
1089
  // Remove a worktree and optionally delete its branch
416
1090
  async function removeWorktree(cwd, worktreePath, deleteBranch) {
417
1091
  // Get branch name before removing. Match tolerates /var ↔ /private/var
@@ -446,6 +1120,15 @@ async function removeWorktree(cwd, worktreePath, deleteBranch) {
446
1120
  return { removed: true, path: worktreePath, branchDeleted };
447
1121
  }
448
1122
 
1123
+ // Delete a local branch. `-d` refuses to drop unmerged work; `force` uses `-D`.
1124
+ // Caller is responsible for ensuring the branch has no worktree and is not in use
1125
+ // by a live session. Throws on git failure (e.g. unmerged without force).
1126
+ async function deleteBranch(cwd, branchName, force) {
1127
+ const flag = force ? '-D' : '-d';
1128
+ await gitStrict(cwd, ['branch', flag, branchName]);
1129
+ return { deleted: true, branch: branchName, forced: !!force };
1130
+ }
1131
+
449
1132
  // Best-effort realpath resolution — falls back to the input if it can't be
450
1133
  // resolved (e.g. already deleted, permission issue).
451
1134
  function _realpath(p) {
@@ -459,7 +1142,9 @@ function _realpath(p) {
459
1142
  // - isCanonical: path lives under an agent-owned <repo>/.claude|.walle/worktrees/<name>
460
1143
  // - isGhost: path contains /~/ corruption OR path doesn't exist on disk
461
1144
  // - ahead/behind vs mainBranch
462
- // - dirtyFiles: count of modified+untracked
1145
+ // - dirtyFiles: count of tracked dirty + untracked files
1146
+ // - trackedDirtyFiles/untrackedFiles: split counts so untracked-only
1147
+ // generated artifacts do not block safe sync from main
463
1148
  // - unmergedCommits: rev-list count main..HEAD (0 ⇒ safe to delete)
464
1149
  // - lastActivity: ISO timestamp of HEAD commit
465
1150
  // - summary: 1-line human-readable status
@@ -476,6 +1161,98 @@ async function _gitSafe(cwd, args, timeoutMs = 5000) {
476
1161
  });
477
1162
  }
478
1163
 
1164
+ async function _gitSafeRaw(cwd, args, timeoutMs = 5000) {
1165
+ return new Promise((resolve) => {
1166
+ execFile('git', args, { cwd, timeout: timeoutMs, maxBuffer: 1024 * 1024 }, (err, stdout) => {
1167
+ if (err) return resolve(null);
1168
+ resolve(String(stdout || ''));
1169
+ });
1170
+ });
1171
+ }
1172
+
1173
+ function _parsePorcelainStatusZ(out) {
1174
+ const entries = String(out || '').split('\0').filter(Boolean);
1175
+ const result = {
1176
+ dirtyFiles: 0,
1177
+ trackedDirtyFiles: 0,
1178
+ untrackedFiles: 0,
1179
+ ignoredFiles: 0,
1180
+ unmergedFiles: 0,
1181
+ stagedFiles: 0,
1182
+ unstagedFiles: 0,
1183
+ untrackedPaths: [],
1184
+ };
1185
+
1186
+ for (let i = 0; i < entries.length; i++) {
1187
+ const entry = entries[i];
1188
+ if (entry.length < 3) continue;
1189
+ const xy = entry.slice(0, 2);
1190
+ const filePath = entry.slice(3);
1191
+ if (xy === '??') {
1192
+ result.untrackedFiles += 1;
1193
+ result.untrackedPaths.push(filePath);
1194
+ continue;
1195
+ }
1196
+ if (xy === '!!') {
1197
+ result.ignoredFiles += 1;
1198
+ continue;
1199
+ }
1200
+
1201
+ result.trackedDirtyFiles += 1;
1202
+ if (xy[0] && xy[0] !== ' ') result.stagedFiles += 1;
1203
+ if (xy[1] && xy[1] !== ' ') result.unstagedFiles += 1;
1204
+ if (xy.includes('U') || ['AA', 'DD'].includes(xy)) result.unmergedFiles += 1;
1205
+ // In porcelain v1 -z, rename/copy records include the source path as a
1206
+ // second NUL-terminated field. Skip it so one rename counts once.
1207
+ if (xy[0] === 'R' || xy[0] === 'C') i += 1;
1208
+ }
1209
+
1210
+ result.dirtyFiles = result.trackedDirtyFiles + result.untrackedFiles;
1211
+ result.hasOnlyUntrackedFiles = result.untrackedFiles > 0 && result.trackedDirtyFiles === 0;
1212
+ return result;
1213
+ }
1214
+
1215
+ function _trackedDirtyCount(wt) {
1216
+ if (!wt) return 0;
1217
+ if (wt.trackedDirtyFiles != null) return Number(wt.trackedDirtyFiles || 0);
1218
+ const dirty = Number(wt.dirtyFiles || 0);
1219
+ const untracked = Number(wt.untrackedFiles || 0);
1220
+ return Math.max(0, dirty - untracked);
1221
+ }
1222
+
1223
+ function _hasOnlyUntrackedFiles(wt) {
1224
+ if (!wt) return false;
1225
+ const dirty = Number(wt.dirtyFiles || 0);
1226
+ return dirty > 0 && _trackedDirtyCount(wt) === 0;
1227
+ }
1228
+
1229
+ function _splitNulPaths(out) {
1230
+ return String(out || '').split('\0').filter(Boolean);
1231
+ }
1232
+
1233
+ function _pathsOverlap(a, b) {
1234
+ const left = String(a || '').replace(/\/+$/, '');
1235
+ const right = String(b || '').replace(/\/+$/, '');
1236
+ if (!left || !right) return false;
1237
+ return left === right || left.startsWith(right + '/') || right.startsWith(left + '/');
1238
+ }
1239
+
1240
+ async function _untrackedSyncCollisions(worktreePath, targetBranch) {
1241
+ const status = _parsePorcelainStatusZ(await _gitSafeRaw(worktreePath, ['status', '--porcelain=v1', '-uall', '-z']) || '');
1242
+ if (status.trackedDirtyFiles > 0 || status.untrackedPaths.length === 0) {
1243
+ return { status, collisions: [] };
1244
+ }
1245
+ const changedOut = await _gitSafeRaw(worktreePath, ['diff', '--name-only', '-z', 'HEAD', targetBranch], 8000);
1246
+ const changedPaths = _splitNulPaths(changedOut);
1247
+ if (changedPaths.length === 0) return { status, collisions: [] };
1248
+ const collisions = [];
1249
+ for (const untrackedPath of status.untrackedPaths) {
1250
+ const hit = changedPaths.find(changedPath => _pathsOverlap(untrackedPath, changedPath));
1251
+ if (hit) collisions.push({ untrackedPath, incomingPath: hit });
1252
+ }
1253
+ return { status, collisions };
1254
+ }
1255
+
479
1256
  function _checkpointRefSlug(branchName) {
480
1257
  let slug = String(branchName || 'branch')
481
1258
  .replace(/[^A-Za-z0-9._-]+/g, '-')
@@ -621,7 +1398,7 @@ function _classifyState(wt) {
621
1398
  if (wt.isGhost) return 'ghost';
622
1399
  if (wt.isMain) return 'primary';
623
1400
  if (!wt.branch || wt.branch === 'HEAD') return 'detached';
624
- if (wt.dirtyFiles > 0) return 'dirty';
1401
+ if (_trackedDirtyCount(wt) > 0) return 'dirty';
625
1402
  const ahead = wt.ahead || 0;
626
1403
  const behind = wt.behind || 0;
627
1404
  if (ahead > 0 && behind > 0) return 'diverged';
@@ -654,16 +1431,20 @@ function _buildSummary(wt) {
654
1431
  return 'Primary worktree';
655
1432
  }
656
1433
  if (wt.state === 'detached') return 'Detached HEAD — commits here are at risk. Click Recover branch.';
1434
+ const trackedDirty = _trackedDirtyCount(wt);
1435
+ const untrackedFiles = Number(wt.untrackedFiles || 0);
657
1436
  const parts = [];
658
1437
  if (wt.ahead > 0) parts.push(`${wt.ahead} ahead`);
659
1438
  if (wt.behind > 0) parts.push(`${wt.behind} behind`);
660
- if (wt.dirtyFiles > 0) parts.push(`${wt.dirtyFiles} dirty`);
1439
+ if (trackedDirty > 0) parts.push(`${trackedDirty} dirty`);
1440
+ if (untrackedFiles > 0) parts.push(`${untrackedFiles} untracked`);
661
1441
  if (parts.length === 0) return 'Clean — synced with main';
662
1442
  let suffix = '';
663
1443
  if (wt.state === 'ahead') suffix = ' — ready to merge';
664
1444
  else if (wt.state === 'behind') suffix = ' — sync from main';
665
1445
  else if (wt.state === 'diverged') suffix = ' — needs sync first';
666
1446
  else if (wt.state === 'dirty') suffix = ' — commit or stash';
1447
+ else if (wt.state === 'clean' && untrackedFiles > 0) suffix = ' — untracked files kept local';
667
1448
  return parts.join(', ') + suffix;
668
1449
  }
669
1450
 
@@ -729,10 +1510,12 @@ function _recommendedAction(wt) {
729
1510
  return { kind: 'primary', label: 'Primary', tone: 'neutral', reason: 'Main checkout.' };
730
1511
  }
731
1512
  if (!wt.branch || wt.branch === 'HEAD') return { kind: 'recover_branch', label: 'Recover branch', tone: 'danger', reason: 'Detached HEAD commits can become hard to find.' };
732
- if (wt.dirtyFiles > 0) return { kind: 'review_dirty', label: 'Open session', tone: 'warning', reason: `${wt.dirtyFiles} uncommitted file(s).` };
1513
+ const trackedDirty = _trackedDirtyCount(wt);
1514
+ if (trackedDirty > 0) return { kind: 'review_dirty', label: 'Open session', tone: 'warning', reason: `${trackedDirty} tracked dirty file(s).` };
733
1515
  if ((wt.ahead || 0) > 0 && (wt.behind || 0) > 0) return { kind: 'sync_branch', label: 'Sync first', tone: 'warning', reason: 'Branch has commits and is behind main.' };
734
1516
  if ((wt.behind || 0) > 0) return { kind: 'sync_branch', label: 'Sync', tone: 'warning', reason: 'Branch is behind main.' };
735
1517
  if ((wt.ahead || 0) > 0) return { kind: 'finish_work', label: 'Finish', tone: 'success', reason: 'Branch has committed work not on main.' };
1518
+ if (_hasOnlyUntrackedFiles(wt)) return { kind: 'review_dirty', label: 'Open session', tone: 'warning', reason: `${wt.untrackedFiles || wt.dirtyFiles} untracked file(s).` };
736
1519
  return { kind: 'cleanup', label: 'Clean up', tone: 'neutral', reason: 'No unmerged commits or dirty files.' };
737
1520
  }
738
1521
 
@@ -830,6 +1613,37 @@ async function syncWorktree(cwd, branchName, strategy, opts) {
830
1613
  message: 'Worktree HEAD changed before sync could start.',
831
1614
  };
832
1615
  }
1616
+ // Make sure main is up to date locally — try a fast-forward fetch but
1617
+ // don't fail if there's no remote.
1618
+ await _gitSafe(repoRoot, ['fetch', 'origin', 'main', '--quiet'], 8000);
1619
+
1620
+ // Pre-check for conflicts using merge-tree (git 2.38+).
1621
+ const preCheck = await mergeWorktreePreCheck(repoRoot, 'main', branchName).catch(() => ({ conflicts: false }));
1622
+ if (preCheck.conflicts) {
1623
+ return { merged: false, conflicts: true, message: 'Merge would conflict — open the worktree in a terminal to resolve.' };
1624
+ }
1625
+
1626
+ const syncSafety = await _untrackedSyncCollisions(wt.path, 'main');
1627
+ if (syncSafety.status.trackedDirtyFiles > 0) {
1628
+ return {
1629
+ merged: false,
1630
+ blocked: true,
1631
+ code: 'TRACKED_DIRTY',
1632
+ beforeHead,
1633
+ message: 'Commit or stash tracked dirty files before syncing from main.',
1634
+ };
1635
+ }
1636
+ if (syncSafety.collisions.length > 0) {
1637
+ return {
1638
+ merged: false,
1639
+ blocked: true,
1640
+ code: 'UNTRACKED_COLLISION',
1641
+ beforeHead,
1642
+ untrackedCollisions: syncSafety.collisions,
1643
+ message: `Sync would overwrite untracked file(s): ${syncSafety.collisions.map(c => c.untrackedPath).slice(0, 3).join(', ')}`,
1644
+ };
1645
+ }
1646
+
833
1647
  let checkpointRef = '';
834
1648
  if (opts.createCheckpoint) {
835
1649
  checkpointRef = await _createWorktreeCheckpoint(repoRoot, branchName, beforeHead);
@@ -844,16 +1658,6 @@ async function syncWorktree(cwd, branchName, strategy, opts) {
844
1658
  }
845
1659
  }
846
1660
 
847
- // Make sure main is up to date locally — try a fast-forward fetch but
848
- // don't fail if there's no remote.
849
- await _gitSafe(repoRoot, ['fetch', 'origin', 'main', '--quiet'], 8000);
850
-
851
- // Pre-check for conflicts using merge-tree (git 2.38+).
852
- const preCheck = await mergeWorktreePreCheck(repoRoot, 'main', branchName).catch(() => ({ conflicts: false }));
853
- if (preCheck.conflicts) {
854
- return { merged: false, conflicts: true, message: 'Merge would conflict — open the worktree in a terminal to resolve.' };
855
- }
856
-
857
1661
  const args = strategy === 'rebase' ? ['rebase', 'main'] : ['merge', 'main', '--no-edit'];
858
1662
  const out = await _gitSafe(wt.path, args, 30000);
859
1663
  if (out === null) {
@@ -873,7 +1677,8 @@ function _syncAllEligibility(wt) {
873
1677
  if (!wt || wt.isMain) return { eligible: false, reason: 'primary checkout' };
874
1678
  if (wt.isGhost || wt.state === 'ghost') return { eligible: false, reason: 'ghost worktree' };
875
1679
  if (!wt.branch || wt.branch === 'HEAD' || wt.state === 'detached') return { eligible: false, reason: 'detached HEAD' };
876
- if ((wt.dirtyFiles || 0) > 0) return { eligible: false, reason: `${wt.dirtyFiles} dirty file(s)` };
1680
+ const trackedDirty = _trackedDirtyCount(wt);
1681
+ if (trackedDirty > 0) return { eligible: false, reason: `${trackedDirty} tracked dirty file(s)` };
877
1682
  if ((wt.behind || 0) <= 0) return { eligible: false, reason: 'not behind main' };
878
1683
  return { eligible: true, reason: '' };
879
1684
  }
@@ -1058,7 +1863,7 @@ async function listRichWorktrees(cwd, opts) {
1058
1863
  // Gather status fields in parallel.
1059
1864
  const [revlist, statusOut, lastIso, unmergedOut] = await Promise.all([
1060
1865
  wt.branch ? _gitSafe(wt.path, ['rev-list', '--left-right', '--count', `${mainBranch}...${wt.branch}`]) : Promise.resolve(null),
1061
- _gitSafe(wt.path, ['status', '--porcelain']),
1866
+ _gitSafeRaw(wt.path, ['status', '--porcelain=v1', '-uall', '-z']),
1062
1867
  _gitSafe(wt.path, ['log', '-1', '--format=%cI', 'HEAD']),
1063
1868
  wt.branch && !wt.isMain ? _gitSafe(wt.path, ['rev-list', '--count', `${mainBranch}..HEAD`]) : Promise.resolve('0'),
1064
1869
  ]);
@@ -1071,11 +1876,18 @@ async function listRichWorktrees(cwd, opts) {
1071
1876
  ahead = parseInt(parts[1], 10) || 0;
1072
1877
  }
1073
1878
  }
1074
- const dirtyFiles = statusOut ? statusOut.split('\n').filter(Boolean).length : 0;
1879
+ const status = _parsePorcelainStatusZ(statusOut || '');
1880
+ const dirtyFiles = status.dirtyFiles;
1075
1881
  const unmergedCommits = parseInt(unmergedOut, 10) || 0;
1076
1882
 
1077
1883
  const out = {
1078
1884
  ...wt, isGhost: false, isCanonical, ahead, behind, dirtyFiles, unmergedCommits,
1885
+ trackedDirtyFiles: status.trackedDirtyFiles,
1886
+ untrackedFiles: status.untrackedFiles,
1887
+ unmergedFiles: status.unmergedFiles,
1888
+ stagedFiles: status.stagedFiles,
1889
+ unstagedFiles: status.unstagedFiles,
1890
+ hasOnlyUntrackedFiles: status.hasOnlyUntrackedFiles,
1079
1891
  lastActivity: lastIso || null,
1080
1892
  lastActivityRel: _formatRelativeTime(lastIso),
1081
1893
  mainBranch,
@@ -1104,6 +1916,62 @@ async function listRichWorktrees(cwd, opts) {
1104
1916
  return enriched;
1105
1917
  }
1106
1918
 
1919
+ // Lean single-worktree status for ONE path — the fast counterpart to
1920
+ // listRichWorktrees(). Used by the on-turn-finish refresh so the sidebar
1921
+ // dirty/commit badge updates in ~seconds instead of waiting on the 30s
1922
+ // listRichWorktrees() fan-out (a documented CPU hog: ~4 git spawns PER worktree
1923
+ // across ~40 worktrees). This does those ~4 git calls for just the one worktree.
1924
+ // Computes only the fields the session worktree-status badge needs (live branch,
1925
+ // ahead/behind vs main, dirty counts, unmerged commits, state). Best-effort:
1926
+ // returns null if the path is missing or not a git worktree. mainRemote is a
1927
+ // stub here (only meaningful for the primary worktree, which never shows the
1928
+ // attention badge); callers needing remote state use listRichWorktrees().
1929
+ async function richWorktreeStatusForPath(worktreePath, opts = {}) {
1930
+ const mainBranch = opts.mainBranch || 'main';
1931
+ if (!worktreePath || _isGhostPath(worktreePath)) return null;
1932
+ const branch = await _gitSafe(worktreePath, ['rev-parse', '--abbrev-ref', 'HEAD']);
1933
+ if (!branch) return null;
1934
+ const detached = branch === 'HEAD';
1935
+ const isMain = !detached && (branch === mainBranch || branch === 'main' || branch === 'master');
1936
+ const [revlist, statusOut, lastIso, unmergedOut] = await Promise.all([
1937
+ !detached ? _gitSafe(worktreePath, ['rev-list', '--left-right', '--count', `${mainBranch}...${branch}`]) : Promise.resolve(null),
1938
+ _gitSafeRaw(worktreePath, ['status', '--porcelain=v1', '-uall', '-z']),
1939
+ _gitSafe(worktreePath, ['log', '-1', '--format=%cI', 'HEAD']),
1940
+ (!isMain && !detached) ? _gitSafe(worktreePath, ['rev-list', '--count', `${mainBranch}..HEAD`]) : Promise.resolve('0'),
1941
+ ]);
1942
+ let ahead = 0, behind = 0;
1943
+ if (revlist) {
1944
+ const parts = revlist.split(/\s+/).filter(Boolean);
1945
+ if (parts.length >= 2) { behind = parseInt(parts[0], 10) || 0; ahead = parseInt(parts[1], 10) || 0; }
1946
+ }
1947
+ const status = _parsePorcelainStatusZ(statusOut || '');
1948
+ const out = {
1949
+ path: worktreePath,
1950
+ worktreeName: path.basename(worktreePath || ''),
1951
+ branch: detached ? '' : branch,
1952
+ head: null,
1953
+ isMain,
1954
+ isGhost: false,
1955
+ isCanonical: true,
1956
+ ahead, behind,
1957
+ dirtyFiles: status.dirtyFiles,
1958
+ trackedDirtyFiles: status.trackedDirtyFiles,
1959
+ untrackedFiles: status.untrackedFiles,
1960
+ unmergedFiles: status.unmergedFiles,
1961
+ stagedFiles: status.stagedFiles,
1962
+ unstagedFiles: status.unstagedFiles,
1963
+ hasOnlyUntrackedFiles: status.hasOnlyUntrackedFiles,
1964
+ unmergedCommits: parseInt(unmergedOut, 10) || 0,
1965
+ lastActivity: lastIso || null,
1966
+ lastActivityRel: _formatRelativeTime(lastIso),
1967
+ mainBranch,
1968
+ mainRemote: { branch: mainBranch, remote: null, ahead: 0, behind: 0, state: 'unknown' },
1969
+ };
1970
+ out.state = _classifyState(out);
1971
+ out.summary = _buildSummary(out);
1972
+ return out;
1973
+ }
1974
+
1107
1975
  // Get the list of unmerged commits between main and a worktree branch
1108
1976
  // (used by safe-delete and PR-body generation).
1109
1977
  async function getUnmergedCommitList(cwd, branchName, baseBranch) {
@@ -1175,13 +2043,15 @@ async function pushAndCreatePR(cwd, branchName, opts) {
1175
2043
 
1176
2044
  module.exports = {
1177
2045
  git, getCommits, getCommitLog, getBranch, getDiffStat, getFullDiff, getStagedDiff, parseDiff,
1178
- listWorktrees, createWorktree, mergeWorktreePreCheck, mergeWorktree, removeWorktree,
2046
+ resolveMainBranch, getDefaultReviewBase, getDiffVsRef, getDiffStatVsRef, getDefaultDiffStat, getFileAtRef,
2047
+ listWorktrees, createWorktree, mergeWorktreePreCheck, mergeWorktree, commitWorktreeChanges, finishWorktree, removeWorktree, deleteBranch,
1179
2048
  // Rich-status + new operations
1180
- listRichWorktrees, syncWorktree, syncAllWorktrees, pruneGhosts, recoverDetachedHead, recoverWorktree,
2049
+ listRichWorktrees, richWorktreeStatusForPath, syncWorktree, syncAllWorktrees, pruneGhosts, recoverDetachedHead, recoverWorktree,
1181
2050
  getUnmergedCommitList, pushAndCreatePR,
1182
2051
  // Internal helpers exposed for tests
1183
2052
  _classifyState, _buildSummary, _isGhostPath, _isCanonicalPath, STATE_SORT_RANK,
1184
2053
  _classifyMainRemote, _recommendedAction, _syncAllEligibility, _sameWorktreePath,
1185
2054
  _parseGitHubRemoteUrl, _githubRemoteSpec, _buildGhPrCreateArgs,
1186
2055
  _worktreeNamespace, _worktreeParentDir, _checkpointRefName, _checkpointRefSlug,
2056
+ _invalidateGitFactCache,
1187
2057
  };