@the-open-engine/zeroshot 6.38.1 → 6.39.1

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 (383) hide show
  1. package/README.md +6 -0
  2. package/cli/event-copy.js +8 -7
  3. package/cli/event-copy.ts +12 -0
  4. package/cli/index.js +67 -48
  5. package/cli/json-export.js +61 -64
  6. package/cli/json-export.ts +112 -0
  7. package/cli/message-formatter-utils.js +68 -56
  8. package/cli/message-formatter-utils.ts +121 -0
  9. package/cli/message-formatters-normal.js +124 -259
  10. package/cli/message-formatters-normal.ts +250 -0
  11. package/cli/message-formatters-watch.js +81 -84
  12. package/cli/message-formatters-watch.ts +189 -0
  13. package/lib/cluster/generated/protocol-schema.cjs +1 -1
  14. package/lib/cluster/generated/protocol-schema.mjs +1 -1
  15. package/lib/cluster/generated/protocol.d.cts +8 -0
  16. package/lib/cluster/generated/protocol.d.mts +8 -0
  17. package/lib/cluster/generated/protocol.d.ts +8 -0
  18. package/lib/clusters-registry.d.ts +23 -0
  19. package/lib/clusters-registry.js +17 -29
  20. package/lib/completion.d.ts +12 -0
  21. package/lib/completion.js +84 -94
  22. package/lib/compose-utils.d.ts +32 -0
  23. package/lib/compose-utils.js +57 -72
  24. package/lib/detached-startup.d.ts +84 -0
  25. package/lib/detached-startup.js +232 -302
  26. package/lib/docker-config.d.ts +43 -0
  27. package/lib/docker-config.js +150 -325
  28. package/lib/docker-env-auth.d.ts +17 -0
  29. package/lib/docker-env-auth.js +75 -0
  30. package/lib/git-remote-utils.d.ts +48 -0
  31. package/lib/git-remote-utils.js +147 -213
  32. package/lib/id-detector.d.ts +15 -0
  33. package/lib/id-detector.js +37 -41
  34. package/lib/path-check.d.ts +30 -0
  35. package/lib/path-check.js +42 -45
  36. package/lib/process-liveness.d.ts +16 -0
  37. package/lib/process-liveness.js +19 -12
  38. package/lib/provider-credential-path.d.ts +16 -0
  39. package/lib/provider-credential-path.js +31 -29
  40. package/lib/provider-defaults.d.ts +30 -0
  41. package/lib/provider-defaults.js +47 -42
  42. package/lib/provider-detection.d.ts +11 -0
  43. package/lib/provider-detection.js +91 -54
  44. package/lib/provider-names.d.ts +38 -0
  45. package/lib/provider-names.js +56 -64
  46. package/lib/repo-settings.d.ts +28 -0
  47. package/lib/repo-settings.js +83 -59
  48. package/lib/run-mode.d.ts +21 -0
  49. package/lib/run-mode.js +21 -20
  50. package/lib/run-plan.d.ts +28 -0
  51. package/lib/run-plan.js +17 -16
  52. package/lib/settings/claude-auth.d.ts +35 -0
  53. package/lib/settings/claude-auth.js +30 -47
  54. package/lib/settings-defaults.d.ts +20 -0
  55. package/lib/settings-defaults.js +149 -0
  56. package/lib/settings-error.d.ts +7 -0
  57. package/lib/settings-error.js +8 -0
  58. package/lib/settings-issue-providers.d.ts +10 -0
  59. package/lib/settings-issue-providers.js +12 -0
  60. package/lib/settings-models.d.ts +22 -0
  61. package/lib/settings-models.js +93 -0
  62. package/lib/settings-storage.d.ts +23 -0
  63. package/lib/settings-storage.js +205 -0
  64. package/lib/settings-validation.d.ts +7 -0
  65. package/lib/settings-validation.js +182 -0
  66. package/lib/settings.d.ts +29 -0
  67. package/lib/settings.js +41 -736
  68. package/lib/setup-apply-types.d.ts +66 -0
  69. package/lib/setup-apply-types.js +2 -0
  70. package/lib/setup-apply.d.ts +39 -0
  71. package/lib/setup-apply.js +267 -264
  72. package/lib/setup-journal.d.ts +41 -0
  73. package/lib/setup-journal.js +119 -76
  74. package/lib/setup-plan-types.d.ts +102 -0
  75. package/lib/setup-plan-types.js +2 -0
  76. package/lib/setup-plan.d.ts +30 -0
  77. package/lib/setup-plan.js +300 -324
  78. package/lib/setup-undo.d.ts +44 -0
  79. package/lib/setup-undo.js +61 -74
  80. package/lib/start-cluster-config.d.ts +26 -0
  81. package/lib/start-cluster-config.js +56 -0
  82. package/lib/start-cluster-environment.d.ts +41 -0
  83. package/lib/start-cluster-environment.js +134 -0
  84. package/lib/start-cluster-run-options.d.ts +54 -0
  85. package/lib/start-cluster-run-options.js +148 -0
  86. package/lib/start-cluster.d.ts +64 -0
  87. package/lib/start-cluster.js +98 -454
  88. package/lib/stream-json-parser.d.ts +17 -0
  89. package/lib/stream-json-parser.js +51 -52
  90. package/npm-shrinkwrap.json +6 -8
  91. package/package.json +14 -8
  92. package/scripts/postinstall.js +27 -0
  93. package/scripts/rust-distribution.js +36 -45
  94. package/src/agent/agent-command-proofs-context.js +35 -45
  95. package/src/agent/agent-command-proofs-context.ts +87 -0
  96. package/src/agent/agent-config-settings.js +19 -0
  97. package/src/agent/agent-config-settings.ts +41 -0
  98. package/src/agent/agent-config.js +122 -231
  99. package/src/agent/agent-config.ts +285 -0
  100. package/src/agent/agent-context-builder.js +38 -273
  101. package/src/agent/agent-context-builder.ts +51 -0
  102. package/src/agent/agent-context-environment-sections.js +105 -0
  103. package/src/agent/agent-context-environment-sections.ts +134 -0
  104. package/src/agent/agent-context-packs.js +165 -0
  105. package/src/agent/agent-context-packs.ts +296 -0
  106. package/src/agent/agent-context-prompt-sections.js +100 -0
  107. package/src/agent/agent-context-prompt-sections.ts +148 -0
  108. package/src/agent/agent-context-sections.js +25 -334
  109. package/src/agent/agent-context-sections.ts +36 -0
  110. package/src/agent/agent-context-sources.js +96 -149
  111. package/src/agent/agent-context-sources.ts +253 -0
  112. package/src/agent/agent-context-types.js +2 -0
  113. package/src/agent/agent-context-types.ts +132 -0
  114. package/src/agent/agent-context-validation-sections.js +73 -0
  115. package/src/agent/agent-context-validation-sections.ts +129 -0
  116. package/src/agent/agent-input-injector.js +99 -118
  117. package/src/agent/agent-input-injector.ts +195 -0
  118. package/src/agent/agent-liveness-poll.js +74 -78
  119. package/src/agent/agent-liveness-poll.ts +163 -0
  120. package/src/agent/agent-quality-gate-schema.js +77 -86
  121. package/src/agent/agent-quality-gate-schema.ts +135 -0
  122. package/src/agent/agent-quality-gates-context.js +44 -50
  123. package/src/agent/agent-quality-gates-context.ts +91 -0
  124. package/src/agent/agent-stuck-detector.js +171 -239
  125. package/src/agent/agent-stuck-detector.ts +288 -0
  126. package/src/agent/agent-trigger-evaluator.js +29 -46
  127. package/src/agent/agent-trigger-evaluator.ts +98 -0
  128. package/src/agent/context-metrics.js +110 -141
  129. package/src/agent/context-metrics.ts +261 -0
  130. package/src/agent/context-pack-builder.js +72 -358
  131. package/src/agent/context-pack-builder.ts +105 -0
  132. package/src/agent/context-pack-core.js +70 -0
  133. package/src/agent/context-pack-core.ts +96 -0
  134. package/src/agent/context-pack-selection.js +119 -0
  135. package/src/agent/context-pack-selection.ts +187 -0
  136. package/src/agent/context-pack-truncation.js +139 -0
  137. package/src/agent/context-pack-truncation.ts +208 -0
  138. package/src/agent/context-pack-types.js +2 -0
  139. package/src/agent/context-pack-types.ts +98 -0
  140. package/src/agent/context-replay-policy.js +30 -39
  141. package/src/agent/context-replay-policy.ts +71 -0
  142. package/src/agent/critical-agent-policy.js +6 -10
  143. package/src/agent/critical-agent-policy.ts +20 -0
  144. package/src/agent/guidance-queue.js +74 -87
  145. package/src/agent/guidance-queue.ts +166 -0
  146. package/src/agent/output-extraction-error-detail.js +45 -0
  147. package/src/agent/output-extraction-error-detail.ts +49 -0
  148. package/src/agent/output-extraction-failures.js +125 -0
  149. package/src/agent/output-extraction-failures.ts +149 -0
  150. package/src/agent/output-extraction-json.js +234 -0
  151. package/src/agent/output-extraction-json.ts +235 -0
  152. package/src/agent/output-extraction-pi.js +124 -0
  153. package/src/agent/output-extraction-pi.ts +137 -0
  154. package/src/agent/output-extraction-types.js +2 -0
  155. package/src/agent/output-extraction-types.ts +49 -0
  156. package/src/agent/output-extraction.js +17 -617
  157. package/src/agent/output-extraction.ts +30 -0
  158. package/src/agent/output-reformatter-errors.js +83 -0
  159. package/src/agent/output-reformatter-errors.ts +106 -0
  160. package/src/agent/output-reformatter-types.js +2 -0
  161. package/src/agent/output-reformatter-types.ts +86 -0
  162. package/src/agent/output-reformatter.js +140 -185
  163. package/src/agent/output-reformatter.ts +238 -0
  164. package/src/agent/pi-terminal-lifecycle-types.js +2 -0
  165. package/src/agent/pi-terminal-lifecycle-types.ts +41 -0
  166. package/src/agent/pi-terminal-lifecycle.js +186 -206
  167. package/src/agent/pi-terminal-lifecycle.ts +297 -0
  168. package/src/agent/provider-control-plane.js +22 -21
  169. package/src/agent/provider-control-plane.ts +52 -0
  170. package/src/agent/provider-terminal-failure-types.js +2 -0
  171. package/src/agent/provider-terminal-failure-types.ts +60 -0
  172. package/src/agent/provider-terminal-failure.js +194 -191
  173. package/src/agent/provider-terminal-failure.ts +262 -0
  174. package/src/agent/rate-limit-backoff.js +31 -74
  175. package/src/agent/rate-limit-backoff.ts +71 -0
  176. package/src/agent/schema-utils.js +85 -125
  177. package/src/agent/schema-utils.ts +148 -0
  178. package/src/agent/structured-output-error.js +34 -33
  179. package/src/agent/structured-output-error.ts +105 -0
  180. package/src/agent/validation-platform.js +27 -27
  181. package/src/agent/validation-platform.ts +49 -0
  182. package/src/agents/git-pusher-template.js +17 -34
  183. package/src/attach/attach-client.js +232 -405
  184. package/src/attach/attach-client.ts +300 -0
  185. package/src/attach/attach-server-cleanup.js +43 -0
  186. package/src/attach/attach-server-cleanup.ts +39 -0
  187. package/src/attach/attach-server-clients.js +117 -0
  188. package/src/attach/attach-server-clients.ts +122 -0
  189. package/src/attach/attach-server-events.js +38 -0
  190. package/src/attach/attach-server-events.ts +36 -0
  191. package/src/attach/attach-server-pty.js +45 -0
  192. package/src/attach/attach-server-pty.ts +64 -0
  193. package/src/attach/attach-server-runtime.js +73 -0
  194. package/src/attach/attach-server-runtime.ts +62 -0
  195. package/src/attach/attach-server-socket.js +28 -0
  196. package/src/attach/attach-server-socket.ts +23 -0
  197. package/src/attach/attach-server-types.js +2 -0
  198. package/src/attach/attach-server-types.ts +82 -0
  199. package/src/attach/attach-server.js +106 -523
  200. package/src/attach/attach-server.ts +158 -0
  201. package/src/attach/index.js +18 -35
  202. package/src/attach/index.ts +18 -0
  203. package/src/attach/protocol.js +95 -184
  204. package/src/attach/protocol.ts +181 -0
  205. package/src/attach/ring-buffer.js +76 -102
  206. package/src/attach/ring-buffer.ts +109 -0
  207. package/src/attach/send-input.js +64 -75
  208. package/src/attach/send-input.ts +94 -0
  209. package/src/attach/socket-discovery.js +164 -218
  210. package/src/attach/socket-discovery.ts +230 -0
  211. package/src/attach/socket-paths.js +42 -61
  212. package/src/attach/socket-paths.ts +81 -0
  213. package/src/claude-credentials.js +51 -54
  214. package/src/claude-credentials.ts +89 -0
  215. package/src/cluster/generated/protocol-schema.ts +1 -1
  216. package/src/cluster/generated/protocol.ts +2 -1
  217. package/src/command-proofs.js +110 -141
  218. package/src/command-proofs.ts +216 -0
  219. package/src/config-router.js +81 -73
  220. package/src/config-router.ts +131 -0
  221. package/src/copy-containment.js +191 -0
  222. package/src/copy-containment.ts +259 -0
  223. package/src/copy-worker.js +53 -30
  224. package/src/copy-worker.ts +88 -0
  225. package/src/darwin-keychain-boundary.js +80 -111
  226. package/src/darwin-keychain-boundary.ts +148 -0
  227. package/src/guidance-topics.js +4 -5
  228. package/src/guidance-topics.ts +10 -0
  229. package/src/input-helpers.js +33 -54
  230. package/src/input-helpers.ts +58 -0
  231. package/src/isolation-manager.js +59 -20
  232. package/src/ledger-sequence.js +45 -48
  233. package/src/ledger-sequence.ts +63 -0
  234. package/src/legacy-lib/clusters-registry.ts +40 -0
  235. package/src/legacy-lib/completion.ts +148 -0
  236. package/src/legacy-lib/compose-utils.ts +102 -0
  237. package/src/legacy-lib/detached-startup.ts +451 -0
  238. package/src/legacy-lib/docker-config.ts +282 -0
  239. package/src/legacy-lib/docker-env-auth.ts +116 -0
  240. package/src/legacy-lib/git-remote-utils.ts +244 -0
  241. package/src/legacy-lib/id-detector.ts +57 -0
  242. package/src/legacy-lib/js-yaml.d.ts +3 -0
  243. package/src/legacy-lib/omelette.d.ts +15 -0
  244. package/src/legacy-lib/path-check.ts +89 -0
  245. package/src/legacy-lib/process-liveness.ts +36 -0
  246. package/src/legacy-lib/provider-credential-path.ts +57 -0
  247. package/src/legacy-lib/provider-defaults.ts +114 -0
  248. package/src/legacy-lib/provider-detection.ts +67 -0
  249. package/src/legacy-lib/provider-names.ts +126 -0
  250. package/src/legacy-lib/repo-settings.ts +88 -0
  251. package/src/legacy-lib/run-mode.ts +58 -0
  252. package/src/legacy-lib/run-plan.ts +49 -0
  253. package/src/legacy-lib/settings/claude-auth.ts +80 -0
  254. package/src/legacy-lib/settings-defaults.ts +212 -0
  255. package/src/legacy-lib/settings-error.ts +8 -0
  256. package/src/legacy-lib/settings-issue-providers.ts +18 -0
  257. package/src/legacy-lib/settings-models.ts +124 -0
  258. package/src/legacy-lib/settings-storage.ts +277 -0
  259. package/src/legacy-lib/settings-validation.ts +220 -0
  260. package/src/legacy-lib/settings.ts +87 -0
  261. package/src/legacy-lib/setup-apply-types.ts +70 -0
  262. package/src/legacy-lib/setup-apply.ts +456 -0
  263. package/src/legacy-lib/setup-journal.ts +185 -0
  264. package/src/legacy-lib/setup-plan-types.ts +96 -0
  265. package/src/legacy-lib/setup-plan.ts +500 -0
  266. package/src/legacy-lib/setup-undo.ts +142 -0
  267. package/src/legacy-lib/start-cluster-config.ts +124 -0
  268. package/src/legacy-lib/start-cluster-environment.ts +166 -0
  269. package/src/legacy-lib/start-cluster-run-options.ts +281 -0
  270. package/src/legacy-lib/start-cluster.ts +237 -0
  271. package/src/legacy-lib/stream-json-parser.ts +88 -0
  272. package/src/message-buffer.js +55 -71
  273. package/src/message-buffer.ts +107 -0
  274. package/src/message-bus-bridge.js +92 -133
  275. package/src/message-bus-bridge.ts +149 -0
  276. package/src/name-generator-shared.js +220 -0
  277. package/src/name-generator-shared.ts +231 -0
  278. package/src/name-generator.js +8 -217
  279. package/src/name-generator.ts +27 -0
  280. package/src/omp-blob-root.js +83 -64
  281. package/src/omp-blob-root.ts +116 -0
  282. package/src/omp-config-overlay.js +65 -38
  283. package/src/omp-config-overlay.ts +110 -0
  284. package/src/omp-execution-fingerprint.js +38 -43
  285. package/src/omp-execution-fingerprint.ts +92 -0
  286. package/src/omp-session-limits.js +10 -15
  287. package/src/omp-session-limits.ts +53 -0
  288. package/src/omp-session-partition.js +2 -2
  289. package/src/omp-session-verifier.js +1 -1
  290. package/src/orchestrator.js +32 -7
  291. package/src/pr-body-template.js +71 -0
  292. package/src/providers/anthropic/index.js +3 -1
  293. package/src/providers/anthropic/index.ts +13 -0
  294. package/src/providers/capabilities.js +19 -27
  295. package/src/providers/capabilities.ts +44 -0
  296. package/src/providers/google/index.js +3 -1
  297. package/src/providers/google/index.ts +13 -0
  298. package/src/providers/openai/index.js +3 -1
  299. package/src/providers/openai/index.ts +13 -0
  300. package/src/providers/opencode/index.js +3 -1
  301. package/src/providers/opencode/index.ts +13 -0
  302. package/src/quality-gates.js +67 -127
  303. package/src/quality-gates.ts +149 -0
  304. package/src/repo-settings-access.js +40 -0
  305. package/src/repo-settings-access.ts +64 -0
  306. package/src/schemas/sub-cluster.js +127 -183
  307. package/src/schemas/sub-cluster.ts +238 -0
  308. package/src/state-snapshot-normalization.js +224 -0
  309. package/src/state-snapshot-normalization.ts +222 -0
  310. package/src/state-snapshot.js +192 -359
  311. package/src/state-snapshot.ts +291 -0
  312. package/src/state-snapshotter.js +118 -130
  313. package/src/state-snapshotter.ts +167 -0
  314. package/src/sub-cluster-wrapper.js +4 -13
  315. package/src/task-run-model-args.js +87 -124
  316. package/src/task-run-model-args.ts +204 -0
  317. package/src/task-runner.js +5 -25
  318. package/src/task-runner.ts +28 -0
  319. package/src/task-startup-error.js +73 -58
  320. package/src/task-startup-error.ts +126 -0
  321. package/src/template-validation/consensus-gate-contracts.js +2 -0
  322. package/src/template-validation/consensus-gate-contracts.ts +70 -0
  323. package/src/template-validation/consensus-gate-scenarios.js +76 -0
  324. package/src/template-validation/consensus-gate-scenarios.ts +130 -0
  325. package/src/template-validation/consensus-gate-stage.js +34 -0
  326. package/src/template-validation/consensus-gate-stage.ts +56 -0
  327. package/src/template-validation/random-topology-contracts.js +2 -0
  328. package/src/template-validation/random-topology-contracts.ts +54 -0
  329. package/src/template-validation/random-topology-dispatch.js +25 -0
  330. package/src/template-validation/random-topology-dispatch.ts +33 -0
  331. package/src/template-validation/random-topology-message-loop.js +53 -0
  332. package/src/template-validation/random-topology-message-loop.ts +73 -0
  333. package/src/template-validation/random-topology-operations.js +79 -0
  334. package/src/template-validation/random-topology-operations.ts +101 -0
  335. package/src/template-validation/random-topology-runtime.js +25 -0
  336. package/src/template-validation/random-topology-runtime.ts +43 -0
  337. package/src/template-validation/random-topology-scenario.js +52 -0
  338. package/src/template-validation/random-topology-scenario.ts +57 -0
  339. package/src/template-validation/random-topology-schema.js +159 -0
  340. package/src/template-validation/random-topology-schema.ts +190 -0
  341. package/src/template-validation/random-topology-state.js +28 -0
  342. package/src/template-validation/random-topology-state.ts +30 -0
  343. package/src/template-validation/random-topology-trigger-evaluation.js +45 -0
  344. package/src/template-validation/random-topology-trigger-evaluation.ts +65 -0
  345. package/src/template-validation/random-topology-trigger.js +75 -0
  346. package/src/template-validation/random-topology-trigger.ts +88 -0
  347. package/src/template-validation/report-formatter.js +40 -46
  348. package/src/template-validation/report-formatter.ts +101 -0
  349. package/src/template-validation/simulate-consensus-gates.js +111 -302
  350. package/src/template-validation/simulate-consensus-gates.ts +196 -0
  351. package/src/template-validation/simulate-random-topology.js +26 -519
  352. package/src/template-validation/simulate-random-topology.ts +57 -0
  353. package/src/template-validation/simulate-two-stage-validation.js +41 -265
  354. package/src/template-validation/simulate-two-stage-validation.ts +55 -0
  355. package/src/template-validation/simulation-agent-runtime.js +25 -0
  356. package/src/template-validation/simulation-agent-runtime.ts +40 -0
  357. package/src/template-validation/simulation-agent.js +29 -0
  358. package/src/template-validation/simulation-agent.ts +73 -0
  359. package/src/template-validation/simulation-runtime.js +34 -0
  360. package/src/template-validation/simulation-runtime.ts +72 -0
  361. package/src/template-validation/two-stage-contracts.js +2 -0
  362. package/src/template-validation/two-stage-contracts.ts +41 -0
  363. package/src/template-validation/two-stage-inputs.js +21 -0
  364. package/src/template-validation/two-stage-inputs.ts +29 -0
  365. package/src/template-validation/two-stage-results.js +30 -0
  366. package/src/template-validation/two-stage-results.ts +40 -0
  367. package/src/template-validation/two-stage-scenario.js +86 -0
  368. package/src/template-validation/two-stage-scenario.ts +120 -0
  369. package/src/worktree-claude-config.js +182 -220
  370. package/src/worktree-claude-config.ts +272 -0
  371. package/src/worktree-tooling-env.js +103 -125
  372. package/src/worktree-tooling-env.ts +142 -0
  373. package/task-lib/completion.js +2 -3
  374. package/task-lib/completion.ts +9 -0
  375. package/task-lib/config.js +1 -4
  376. package/task-lib/config.ts +11 -0
  377. package/task-lib/name-generator.js +4 -226
  378. package/task-lib/name-generator.ts +11 -0
  379. package/task-lib/omp-session-cleanup.js +1 -1
  380. package/task-lib/omp-storage-root.js +8 -12
  381. package/task-lib/omp-storage-root.ts +62 -0
  382. package/task-lib/process-termination.js +143 -166
  383. package/task-lib/process-termination.ts +268 -0
@@ -34,6 +34,14 @@ const { resolveProviderCredentialPaths } = require('../lib/provider-credential-p
34
34
  const { getProvider } = require('./providers');
35
35
  const { readRepoSettings } = require('../lib/repo-settings');
36
36
  const { provisionClaudeCredentials } = require('./claude-credentials');
37
+ const {
38
+ CopyContainmentError,
39
+ copyErrorFromPayload,
40
+ createCopyBoundary,
41
+ resolveCopyPath,
42
+ resolveSourcePath,
43
+ validateRelativePath,
44
+ } = require('./copy-containment');
37
45
 
38
46
  const DEFAULT_WORKTREE_SETUP_TIMEOUT_MS = 15 * 60 * 1000;
39
47
  const FRESH_BASE_REF_PREFIX = 'refs/zeroshot/base-fetch';
@@ -1298,6 +1306,7 @@ class IsolationManager {
1298
1306
  // Phase 1: Collect all files and directories
1299
1307
  const files = [];
1300
1308
  const directories = new Set();
1309
+ const copyBoundary = createCopyBoundary(src, dest);
1301
1310
 
1302
1311
  const shouldIgnoreFsError = (err) =>
1303
1312
  err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'ENOENT';
@@ -1328,12 +1337,13 @@ class IsolationManager {
1328
1337
  }
1329
1338
  };
1330
1339
 
1331
- function handleEntry(entry, srcPath, relPath, relativePath) {
1340
+ function handleEntry(entry, relPath, relativePath, ancestorDirectories) {
1332
1341
  if (entry.isSymbolicLink()) {
1333
- const targetStats = fs.statSync(srcPath);
1342
+ const resolvedSourcePath = resolveSourcePath(copyBoundary, relPath);
1343
+ const targetStats = fs.statSync(resolvedSourcePath);
1334
1344
  if (targetStats.isDirectory()) {
1335
1345
  directories.add(relPath);
1336
- collectFiles(srcPath, relPath);
1346
+ collectFiles(relPath, ancestorDirectories);
1337
1347
  return;
1338
1348
  }
1339
1349
 
@@ -1344,7 +1354,7 @@ class IsolationManager {
1344
1354
 
1345
1355
  if (entry.isDirectory()) {
1346
1356
  directories.add(relPath);
1347
- collectFiles(srcPath, relPath);
1357
+ collectFiles(relPath, ancestorDirectories);
1348
1358
  return;
1349
1359
  }
1350
1360
 
@@ -1352,7 +1362,15 @@ class IsolationManager {
1352
1362
  ensureParentDirTracked(relativePath);
1353
1363
  }
1354
1364
 
1355
- function collectFiles(currentSrc, relativePath = '') {
1365
+ function collectFiles(relativePath = '', ancestorDirectories = new Set()) {
1366
+ const currentSrc = relativePath
1367
+ ? resolveSourcePath(copyBoundary, relativePath)
1368
+ : copyBoundary.sourceRoot.canonicalPath;
1369
+ if (ancestorDirectories.has(currentSrc)) {
1370
+ throw new CopyContainmentError(relativePath, 'source directory symlink creates a cycle');
1371
+ }
1372
+ const childAncestors = new Set(ancestorDirectories);
1373
+ childAncestors.add(currentSrc);
1356
1374
  const entries = readEntries(currentSrc);
1357
1375
 
1358
1376
  for (const entry of entries) {
@@ -1360,11 +1378,11 @@ class IsolationManager {
1360
1378
  continue;
1361
1379
  }
1362
1380
 
1363
- const srcPath = path.join(currentSrc, entry.name);
1364
- const relPath = relativePath ? path.join(relativePath, entry.name) : entry.name;
1381
+ const entryName = validateRelativePath(entry.name);
1382
+ const relPath = relativePath ? path.join(relativePath, entryName) : entryName;
1365
1383
 
1366
1384
  try {
1367
- handleEntry(entry, srcPath, relPath, relativePath);
1385
+ handleEntry(entry, relPath, relativePath, childAncestors);
1368
1386
  } catch (err) {
1369
1387
  if (shouldIgnoreFsError(err)) {
1370
1388
  continue;
@@ -1374,7 +1392,7 @@ class IsolationManager {
1374
1392
  }
1375
1393
  }
1376
1394
 
1377
- collectFiles(src);
1395
+ collectFiles();
1378
1396
 
1379
1397
  // Phase 2: Create directory structure (sequential - must exist before file copy)
1380
1398
  // Sort directories by depth to ensure parents are created before children
@@ -1385,7 +1403,7 @@ class IsolationManager {
1385
1403
  });
1386
1404
 
1387
1405
  for (const dir of sortedDirs) {
1388
- const destDir = path.join(dest, dir);
1406
+ const { destinationPath: destDir } = resolveCopyPath(copyBoundary, dir);
1389
1407
  try {
1390
1408
  fs.mkdirSync(destDir, { recursive: true });
1391
1409
  } catch (err) {
@@ -1399,10 +1417,9 @@ class IsolationManager {
1399
1417
  // For small file counts (<100), use synchronous copy (worker overhead not worth it)
1400
1418
  if (files.length < 100) {
1401
1419
  for (const relPath of files) {
1402
- const srcPath = path.join(src, relPath);
1403
- const destPath = path.join(dest, relPath);
1404
1420
  try {
1405
- fs.copyFileSync(srcPath, destPath);
1421
+ const { sourcePath, destinationPath } = resolveCopyPath(copyBoundary, relPath);
1422
+ fs.copyFileSync(sourcePath, destinationPath);
1406
1423
  } catch (err) {
1407
1424
  if (err.code !== 'EACCES' && err.code !== 'EPERM' && err.code !== 'ENOENT') {
1408
1425
  throw err;
@@ -1424,18 +1441,22 @@ class IsolationManager {
1424
1441
  }
1425
1442
 
1426
1443
  // Spawn workers and wait for completion
1444
+ const workers = [];
1427
1445
  const workerPromises = chunks.map((chunk) => {
1428
1446
  return new Promise((resolve, reject) => {
1429
1447
  const worker = new Worker(workerPath, {
1430
1448
  workerData: {
1431
1449
  files: chunk,
1432
- sourceBase: src,
1433
- destBase: dest,
1450
+ sourceBase: copyBoundary.sourceRoot.canonicalPath,
1451
+ destBase: copyBoundary.destinationRoot.canonicalPath,
1452
+ expectedBoundary: copyBoundary,
1434
1453
  },
1435
1454
  });
1455
+ workers.push(worker);
1456
+ let result = null;
1436
1457
 
1437
- worker.on('message', (result) => {
1438
- resolve(result);
1458
+ worker.on('message', (workerResult) => {
1459
+ result = workerResult;
1439
1460
  });
1440
1461
 
1441
1462
  worker.on('error', (err) => {
@@ -1445,6 +1466,12 @@ class IsolationManager {
1445
1466
  worker.on('exit', (code) => {
1446
1467
  if (code !== 0) {
1447
1468
  reject(new Error(`Worker exited with code ${code}`));
1469
+ } else if (!result) {
1470
+ reject(new Error('Copy worker exited without reporting a result'));
1471
+ } else if (result.error) {
1472
+ reject(copyErrorFromPayload(result.error));
1473
+ } else {
1474
+ resolve(result);
1448
1475
  }
1449
1476
  });
1450
1477
  });
@@ -1453,7 +1480,12 @@ class IsolationManager {
1453
1480
  // Wait for all workers to complete (proper async/await - no busy-wait!)
1454
1481
  // FIX: Previous version used busy-wait which blocked the event loop,
1455
1482
  // preventing worker thread messages from being processed (timeout bug)
1456
- await Promise.all(workerPromises);
1483
+ try {
1484
+ await Promise.all(workerPromises);
1485
+ } catch (err) {
1486
+ await Promise.all(workers.map((worker) => worker.terminate().catch(() => undefined)));
1487
+ throw err;
1488
+ }
1457
1489
  }
1458
1490
 
1459
1491
  /**
@@ -2383,8 +2415,15 @@ class IsolationManager {
2383
2415
  // NEVER pass --volumes (irreversible data loss) and NEVER tear down a pinned/shared
2384
2416
  // Compose project — only a project scoped to the worktree directory basename, which is
2385
2417
  // the only kind zeroshot could itself have created, is touched.
2386
- const { resolveWorktreeComposeTeardown } = require('../lib/compose-utils');
2387
- const teardown = resolveWorktreeComposeTeardown(worktreeInfo.path);
2418
+ let teardown = { shouldTeardown: false };
2419
+ try {
2420
+ const { resolveWorktreeComposeTeardown } = require('../lib/compose-utils');
2421
+ teardown = resolveWorktreeComposeTeardown(worktreeInfo.path);
2422
+ } catch (error) {
2423
+ console.warn(
2424
+ `[IsolationManager] Skipping Docker Compose teardown in ${worktreeInfo.path}: ${error.message}`
2425
+ );
2426
+ }
2388
2427
  if (teardown.shouldTeardown) {
2389
2428
  try {
2390
2429
  runSync('docker', teardown.args, {
@@ -1,63 +1,60 @@
1
+ "use strict";
1
2
  const MAX_SQLITE_ROWID = 9223372036854775807n;
2
3
  const CANONICAL_SEQUENCE = /^(0|[1-9][0-9]*)$/;
3
-
4
4
  function canonicalMessageSequence(value, name = 'message sequence') {
5
- let sequence;
6
- if (typeof value === 'string' && CANONICAL_SEQUENCE.test(value)) {
7
- sequence = value;
8
- } else if (Number.isSafeInteger(value) && value >= 0) {
9
- // Accept legacy JSON state while normalizing every new boundary to a
10
- // JSON-safe decimal string.
11
- sequence = String(value);
12
- } else {
13
- throw new TypeError(`${name} must be a canonical non-negative decimal string`);
14
- }
15
-
16
- if (BigInt(sequence) > MAX_SQLITE_ROWID) {
17
- throw new RangeError(`${name} exceeds the SQLite rowid range`);
18
- }
19
- return sequence;
5
+ let sequence;
6
+ if (typeof value === 'string' && CANONICAL_SEQUENCE.test(value)) {
7
+ sequence = value;
8
+ }
9
+ else if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
10
+ // Accept legacy JSON state while normalizing every new boundary to a
11
+ // JSON-safe decimal string.
12
+ sequence = String(value);
13
+ }
14
+ else {
15
+ throw new TypeError(`${name} must be a canonical non-negative decimal string`);
16
+ }
17
+ if (BigInt(sequence) > MAX_SQLITE_ROWID) {
18
+ throw new RangeError(`${name} exceeds the SQLite rowid range`);
19
+ }
20
+ return sequence;
20
21
  }
21
-
22
22
  function messageSequenceFromSql(value, name = 'message sequence') {
23
- if (typeof value === 'bigint') {
24
- if (value < 0n || value > MAX_SQLITE_ROWID) {
25
- throw new RangeError(`${name} is outside the SQLite rowid range`);
23
+ if (typeof value === 'bigint') {
24
+ if (value < 0n || value > MAX_SQLITE_ROWID) {
25
+ throw new RangeError(`${name} is outside the SQLite rowid range`);
26
+ }
27
+ return value.toString();
26
28
  }
27
- return value.toString();
28
- }
29
- return canonicalMessageSequence(value, name);
29
+ return canonicalMessageSequence(value, name);
30
30
  }
31
-
32
31
  function messageSequenceToSql(value, name = 'message sequence') {
33
- return BigInt(canonicalMessageSequence(value, name));
32
+ return BigInt(canonicalMessageSequence(value, name));
34
33
  }
35
-
36
34
  function compareMessageSequences(left, right) {
37
- const leftValue = messageSequenceToSql(left, 'left message sequence');
38
- const rightValue = messageSequenceToSql(right, 'right message sequence');
39
- if (leftValue < rightValue) {
40
- return -1;
41
- }
42
- if (leftValue > rightValue) {
43
- return 1;
44
- }
45
- return 0;
35
+ const leftValue = messageSequenceToSql(left, 'left message sequence');
36
+ const rightValue = messageSequenceToSql(right, 'right message sequence');
37
+ if (leftValue < rightValue) {
38
+ return -1;
39
+ }
40
+ if (leftValue > rightValue) {
41
+ return 1;
42
+ }
43
+ return 0;
46
44
  }
47
-
48
45
  function tryCanonicalMessageSequence(value) {
49
- try {
50
- return canonicalMessageSequence(value);
51
- } catch {
52
- return null;
53
- }
46
+ try {
47
+ return canonicalMessageSequence(value);
48
+ }
49
+ catch {
50
+ return null;
51
+ }
54
52
  }
55
-
56
53
  module.exports = {
57
- MAX_SQLITE_ROWID,
58
- canonicalMessageSequence,
59
- compareMessageSequences,
60
- messageSequenceFromSql,
61
- messageSequenceToSql,
62
- tryCanonicalMessageSequence,
54
+ MAX_SQLITE_ROWID,
55
+ canonicalMessageSequence,
56
+ compareMessageSequences,
57
+ messageSequenceFromSql,
58
+ messageSequenceToSql,
59
+ tryCanonicalMessageSequence,
63
60
  };
@@ -0,0 +1,63 @@
1
+ const MAX_SQLITE_ROWID = 9223372036854775807n;
2
+ const CANONICAL_SEQUENCE = /^(0|[1-9][0-9]*)$/;
3
+
4
+ function canonicalMessageSequence(value: unknown, name = 'message sequence'): string {
5
+ let sequence: string;
6
+ if (typeof value === 'string' && CANONICAL_SEQUENCE.test(value)) {
7
+ sequence = value;
8
+ } else if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0) {
9
+ // Accept legacy JSON state while normalizing every new boundary to a
10
+ // JSON-safe decimal string.
11
+ sequence = String(value);
12
+ } else {
13
+ throw new TypeError(`${name} must be a canonical non-negative decimal string`);
14
+ }
15
+
16
+ if (BigInt(sequence) > MAX_SQLITE_ROWID) {
17
+ throw new RangeError(`${name} exceeds the SQLite rowid range`);
18
+ }
19
+ return sequence;
20
+ }
21
+
22
+ function messageSequenceFromSql(value: unknown, name = 'message sequence'): string {
23
+ if (typeof value === 'bigint') {
24
+ if (value < 0n || value > MAX_SQLITE_ROWID) {
25
+ throw new RangeError(`${name} is outside the SQLite rowid range`);
26
+ }
27
+ return value.toString();
28
+ }
29
+ return canonicalMessageSequence(value, name);
30
+ }
31
+
32
+ function messageSequenceToSql(value: unknown, name = 'message sequence'): bigint {
33
+ return BigInt(canonicalMessageSequence(value, name));
34
+ }
35
+
36
+ function compareMessageSequences(left: unknown, right: unknown): -1 | 0 | 1 {
37
+ const leftValue = messageSequenceToSql(left, 'left message sequence');
38
+ const rightValue = messageSequenceToSql(right, 'right message sequence');
39
+ if (leftValue < rightValue) {
40
+ return -1;
41
+ }
42
+ if (leftValue > rightValue) {
43
+ return 1;
44
+ }
45
+ return 0;
46
+ }
47
+
48
+ function tryCanonicalMessageSequence(value: unknown): string | null {
49
+ try {
50
+ return canonicalMessageSequence(value);
51
+ } catch {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ export = {
57
+ MAX_SQLITE_ROWID,
58
+ canonicalMessageSequence,
59
+ compareMessageSequences,
60
+ messageSequenceFromSql,
61
+ messageSequenceToSql,
62
+ tryCanonicalMessageSequence,
63
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Single read/write path for clusters.json.
3
+ *
4
+ * All callers that need the raw registry (Orchestrator, id-detector, gc,
5
+ * socket-discovery, CLI) go through readClustersFileSync/writeClustersFileAtomic
6
+ * instead of ad-hoc JSON.parse(fs.readFileSync(...)) / fs.writeFileSync(...).
7
+ *
8
+ * Writes are atomic (temp file + rename) so a reader can never observe a
9
+ * partially-written file, even without taking the write lock. Callers that
10
+ * read-modify-write (Orchestrator._saveClusters) still need proper-lockfile
11
+ * around the whole operation to avoid losing concurrent updates.
12
+ */
13
+
14
+ import fs = require('fs');
15
+ import path = require('path');
16
+
17
+ function clustersFilePath(storageDir: string): string {
18
+ return path.join(storageDir, 'clusters.json');
19
+ }
20
+
21
+ /** Read clusters.json. Returns an empty registry if the file is missing. */
22
+ function readClustersFileSync(storageDir: string): unknown {
23
+ const clustersFile = clustersFilePath(storageDir);
24
+ if (!fs.existsSync(clustersFile)) {
25
+ return {};
26
+ }
27
+ const raw = fs.readFileSync(clustersFile, 'utf8');
28
+ const parsed: unknown = JSON.parse(raw);
29
+ return parsed;
30
+ }
31
+
32
+ /** Write clusters.json atomically through a process-scoped temporary file. */
33
+ function writeClustersFileAtomic(storageDir: string, data: unknown): void {
34
+ const clustersFile = clustersFilePath(storageDir);
35
+ const tmpPath = `${clustersFile}.tmp-${process.pid}`;
36
+ fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2));
37
+ fs.renameSync(tmpPath, clustersFile);
38
+ }
39
+
40
+ export = { clustersFilePath, readClustersFileSync, writeClustersFileAtomic };
@@ -0,0 +1,148 @@
1
+ import os = require('os');
2
+ import path = require('path');
3
+ import type { Command, Option } from 'commander';
4
+ import omelette = require('omelette');
5
+ import clustersRegistry = require('./clusters-registry');
6
+
7
+ interface ClustersRegistryFacade {
8
+ readClustersFileSync(storageDir: string): unknown;
9
+ }
10
+
11
+ interface CompletionDependencies {
12
+ listClusterIds?: () => unknown;
13
+ }
14
+
15
+ const { readClustersFileSync }: ClustersRegistryFacade = clustersRegistry;
16
+
17
+ const CLUSTER_ID_COMMANDS = new Set([
18
+ 'attach',
19
+ 'export',
20
+ 'finish',
21
+ 'kill',
22
+ 'logs',
23
+ 'resume',
24
+ 'status',
25
+ 'stop',
26
+ ]);
27
+
28
+ function commandNames(command: Command): string[] {
29
+ return [command.name(), ...command.aliases()];
30
+ }
31
+
32
+ function visibleCommands(command: Command): Command[] {
33
+ return command.createHelp().visibleCommands(command);
34
+ }
35
+
36
+ function visibleOptions(command: Command): Option[] {
37
+ return command.createHelp().visibleOptions(command);
38
+ }
39
+
40
+ function optionNames(option: Option): string[] {
41
+ return [option.short, option.long].filter((name): name is string => Boolean(name));
42
+ }
43
+
44
+ function findSubcommand(command: Command, token: string): Command | undefined {
45
+ return visibleCommands(command).find((candidate) => commandNames(candidate).includes(token));
46
+ }
47
+
48
+ function findOption(command: Command, token: string): Option | undefined {
49
+ const optionToken = token.split('=', 1)[0] ?? '';
50
+ return visibleOptions(command).find((option) => optionNames(option).includes(optionToken));
51
+ }
52
+
53
+ function parseCompletionLine(line: string): { completed: string[]; current: string } {
54
+ const trailingSpace = /\s$/.test(line);
55
+ const tokens = line.trim().split(/\s+/).filter(Boolean);
56
+ tokens.shift();
57
+ const current = trailingSpace ? '' : (tokens.pop() ?? '');
58
+ return { completed: tokens, current };
59
+ }
60
+
61
+ function resolveCommandContext(
62
+ program: Command,
63
+ tokens: string[]
64
+ ): { command: Command; commandPath: string } {
65
+ let command = program;
66
+ const pathParts: string[] = [];
67
+ let expectsOptionValue = false;
68
+ let sawPositional = false;
69
+
70
+ for (const token of tokens) {
71
+ if (expectsOptionValue) {
72
+ expectsOptionValue = false;
73
+ continue;
74
+ }
75
+
76
+ const option = findOption(command, token);
77
+ if (option) {
78
+ expectsOptionValue = !token.includes('=') && (option.required || option.optional);
79
+ continue;
80
+ }
81
+ if (token.startsWith('-')) continue;
82
+
83
+ const subcommand = sawPositional ? undefined : findSubcommand(command, token);
84
+ if (subcommand) {
85
+ command = subcommand;
86
+ pathParts.push(subcommand.name());
87
+ continue;
88
+ }
89
+ sawPositional = true;
90
+ }
91
+
92
+ return { command, commandPath: pathParts.join(' ') };
93
+ }
94
+
95
+ function defaultListClusterIds(): string[] {
96
+ const homeDir =
97
+ process.env.ZEROSHOT_HOME || process.env.HOME || process.env.USERPROFILE || os.homedir();
98
+ const registry = readClustersFileSync(path.join(homeDir, '.zeroshot'));
99
+ // Preserve the legacy failure behavior if a corrupt registry is not an object.
100
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
101
+ return Object.keys(registry as object);
102
+ }
103
+
104
+ function safeDynamicIds(commandPath: string, listClusterIds: () => unknown): string[] {
105
+ if (!CLUSTER_ID_COMMANDS.has(commandPath)) return [];
106
+ try {
107
+ const ids = listClusterIds();
108
+ if (!Array.isArray(ids)) return [];
109
+ const unknownIds: unknown[] = ids;
110
+ return unknownIds.filter((id): id is string => typeof id === 'string' && id.length > 0);
111
+ } catch {
112
+ return [];
113
+ }
114
+ }
115
+
116
+ function getCompletionCandidates(
117
+ program: Command,
118
+ line: string,
119
+ deps: CompletionDependencies = {}
120
+ ): string[] {
121
+ const { completed, current } = parseCompletionLine(line);
122
+ const { command, commandPath } = resolveCommandContext(program, completed);
123
+ const listClusterIds = deps.listClusterIds || defaultListClusterIds;
124
+ const candidates = [
125
+ ...visibleCommands(command).flatMap(commandNames),
126
+ ...visibleOptions(command).flatMap(optionNames),
127
+ ...safeDynamicIds(commandPath, listClusterIds),
128
+ ];
129
+
130
+ return [...new Set(candidates)].filter((candidate) => candidate.startsWith(current));
131
+ }
132
+
133
+ function setupCompletion(
134
+ program: Command,
135
+ deps: CompletionDependencies = {}
136
+ ): ReturnType<typeof omelette> {
137
+ const complete = omelette('zeroshot');
138
+ complete.on('complete', (_fragment, data) => {
139
+ data.reply(getCompletionCandidates(program, data.line, deps));
140
+ });
141
+ complete.init();
142
+ return complete;
143
+ }
144
+
145
+ export = {
146
+ getCompletionCandidates,
147
+ setupCompletion,
148
+ };
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Docker Compose teardown resolution for worktree cleanup.
3
+ *
4
+ * Zeroshot never runs `docker compose up` — any compose file found in a worktree
5
+ * belongs to the target repo, not to zeroshot. Tearing it down is only safe when
6
+ * the resolved Compose project name is scoped to the worktree directory itself
7
+ * (i.e. nothing pins it to the host's real, possibly-already-running project).
8
+ */
9
+
10
+ import fs = require('fs');
11
+ import path = require('path');
12
+ import yaml = require('js-yaml');
13
+
14
+ const COMPOSE_FILENAMES = [
15
+ 'docker-compose.yml',
16
+ 'docker-compose.yaml',
17
+ 'compose.yml',
18
+ 'compose.yaml',
19
+ ] as const;
20
+
21
+ interface ComposeTeardownResolution {
22
+ readonly args?: string[];
23
+ readonly composePath?: string;
24
+ readonly reason?: string;
25
+ readonly shouldTeardown: boolean;
26
+ }
27
+
28
+ /**
29
+ * Reproduce Docker Compose's default project-name normalization
30
+ * (compose-go NormalizeProjectName: lowercase -> keep [a-z0-9_-] -> trim leading '_'/'-').
31
+ */
32
+ function normalizeComposeProjectName(name: string): string {
33
+ return name
34
+ .toLowerCase()
35
+ .replace(/[^a-z0-9_-]/g, '')
36
+ .replace(/^[-_]+/, '');
37
+ }
38
+
39
+ /**
40
+ * Decide whether it's safe to run `docker compose down` in a worktree, and with what args.
41
+ * Never includes `--volumes` — automatic cleanup must not delete named volumes.
42
+ * Skips teardown entirely when the Compose project name is pinned (top-level `name:`
43
+ * in the compose file, or `COMPOSE_PROJECT_NAME` env var), since that resolves to a
44
+ * shared/host project zeroshot did not create.
45
+ */
46
+ function resolveWorktreeComposeTeardown(worktreePath: string): ComposeTeardownResolution {
47
+ let composePath: string | null = null;
48
+ for (const filename of COMPOSE_FILENAMES) {
49
+ const candidate = path.join(worktreePath, filename);
50
+ if (fs.existsSync(candidate)) {
51
+ composePath = candidate;
52
+ break;
53
+ }
54
+ }
55
+
56
+ if (!composePath) {
57
+ return { shouldTeardown: false, reason: 'no compose file' };
58
+ }
59
+
60
+ if (
61
+ typeof process.env.COMPOSE_PROJECT_NAME === 'string' &&
62
+ process.env.COMPOSE_PROJECT_NAME.trim() !== ''
63
+ ) {
64
+ return {
65
+ shouldTeardown: false,
66
+ reason: 'pinned compose project name (shared host project)',
67
+ composePath,
68
+ };
69
+ }
70
+
71
+ try {
72
+ const contents = fs.readFileSync(composePath, 'utf8');
73
+ const parsed = yaml.load(contents);
74
+ if (parsed && typeof parsed === 'object' && Reflect.get(parsed, 'name')) {
75
+ return {
76
+ shouldTeardown: false,
77
+ reason: 'pinned compose project name (shared host project)',
78
+ composePath,
79
+ };
80
+ }
81
+ } catch {
82
+ // Fail safe: if the compose file can't be read/parsed, we cannot confirm project
83
+ // identity is worktree-scoped, so never tear down.
84
+ return { shouldTeardown: false, reason: 'compose file unreadable or unparsable', composePath };
85
+ }
86
+
87
+ return {
88
+ shouldTeardown: true,
89
+ composePath,
90
+ args: [
91
+ 'compose',
92
+ '-p',
93
+ normalizeComposeProjectName(path.basename(worktreePath)),
94
+ 'down',
95
+ '--remove-orphans',
96
+ '--timeout',
97
+ '10',
98
+ ],
99
+ };
100
+ }
101
+
102
+ export = { resolveWorktreeComposeTeardown, normalizeComposeProjectName };