@pennixrv/trellis 0.7.1 → 0.7.2-beta.0

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 (318) hide show
  1. package/README.md +10 -7
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +6 -4
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/ablate.d.ts.map +1 -1
  6. package/dist/commands/ablate.js +2 -0
  7. package/dist/commands/ablate.js.map +1 -1
  8. package/dist/commands/backup.d.ts +4 -0
  9. package/dist/commands/backup.d.ts.map +1 -0
  10. package/dist/commands/backup.js +75 -0
  11. package/dist/commands/backup.js.map +1 -0
  12. package/dist/commands/init.d.ts.map +1 -1
  13. package/dist/commands/init.js +705 -681
  14. package/dist/commands/init.js.map +1 -1
  15. package/dist/commands/mem.d.ts.map +1 -1
  16. package/dist/commands/mem.js +49 -7
  17. package/dist/commands/mem.js.map +1 -1
  18. package/dist/commands/task-progress.d.ts +1 -0
  19. package/dist/commands/task-progress.d.ts.map +1 -1
  20. package/dist/commands/task-progress.js +67 -25
  21. package/dist/commands/task-progress.js.map +1 -1
  22. package/dist/commands/teamwork/adapters/acp.d.ts +20 -0
  23. package/dist/commands/teamwork/adapters/acp.d.ts.map +1 -0
  24. package/dist/commands/teamwork/adapters/acp.js +268 -0
  25. package/dist/commands/teamwork/adapters/acp.js.map +1 -0
  26. package/dist/commands/teamwork/adapters/claude.d.ts +1 -0
  27. package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -1
  28. package/dist/commands/teamwork/adapters/claude.js +5 -0
  29. package/dist/commands/teamwork/adapters/claude.js.map +1 -1
  30. package/dist/commands/teamwork/adapters/codex.d.ts +3 -2
  31. package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -1
  32. package/dist/commands/teamwork/adapters/codex.js +14 -15
  33. package/dist/commands/teamwork/adapters/codex.js.map +1 -1
  34. package/dist/commands/teamwork/adapters/index.d.ts +23 -1
  35. package/dist/commands/teamwork/adapters/index.d.ts.map +1 -1
  36. package/dist/commands/teamwork/adapters/index.js +163 -24
  37. package/dist/commands/teamwork/adapters/index.js.map +1 -1
  38. package/dist/commands/teamwork/agent-loader.d.ts +2 -1
  39. package/dist/commands/teamwork/agent-loader.d.ts.map +1 -1
  40. package/dist/commands/teamwork/agent-loader.js +2 -1
  41. package/dist/commands/teamwork/agent-loader.js.map +1 -1
  42. package/dist/commands/teamwork/artifact.d.ts +33 -10
  43. package/dist/commands/teamwork/artifact.d.ts.map +1 -1
  44. package/dist/commands/teamwork/artifact.js +182 -50
  45. package/dist/commands/teamwork/artifact.js.map +1 -1
  46. package/dist/commands/teamwork/batch-reservation.d.ts +16 -0
  47. package/dist/commands/teamwork/batch-reservation.d.ts.map +1 -0
  48. package/dist/commands/teamwork/batch-reservation.js +43 -0
  49. package/dist/commands/teamwork/batch-reservation.js.map +1 -0
  50. package/dist/commands/teamwork/batch.d.ts +22 -0
  51. package/dist/commands/teamwork/batch.d.ts.map +1 -0
  52. package/dist/commands/teamwork/batch.js +394 -0
  53. package/dist/commands/teamwork/batch.js.map +1 -0
  54. package/dist/commands/teamwork/context-loader.d.ts +1 -1
  55. package/dist/commands/teamwork/context-loader.d.ts.map +1 -1
  56. package/dist/commands/teamwork/context-loader.js +19 -6
  57. package/dist/commands/teamwork/context-loader.js.map +1 -1
  58. package/dist/commands/teamwork/coordination.d.ts +88 -0
  59. package/dist/commands/teamwork/coordination.d.ts.map +1 -0
  60. package/dist/commands/teamwork/coordination.js +153 -0
  61. package/dist/commands/teamwork/coordination.js.map +1 -0
  62. package/dist/commands/teamwork/coordinator.d.ts +17 -3
  63. package/dist/commands/teamwork/coordinator.d.ts.map +1 -1
  64. package/dist/commands/teamwork/coordinator.js +772 -289
  65. package/dist/commands/teamwork/coordinator.js.map +1 -1
  66. package/dist/commands/teamwork/create.d.ts +1 -0
  67. package/dist/commands/teamwork/create.d.ts.map +1 -1
  68. package/dist/commands/teamwork/create.js +2 -1
  69. package/dist/commands/teamwork/create.js.map +1 -1
  70. package/dist/commands/teamwork/dev-parse-trace.d.ts +2 -1
  71. package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -1
  72. package/dist/commands/teamwork/dev-parse-trace.js +2 -0
  73. package/dist/commands/teamwork/dev-parse-trace.js.map +1 -1
  74. package/dist/commands/teamwork/foreground-wait.d.ts +6 -0
  75. package/dist/commands/teamwork/foreground-wait.d.ts.map +1 -0
  76. package/dist/commands/teamwork/foreground-wait.js +65 -0
  77. package/dist/commands/teamwork/foreground-wait.js.map +1 -0
  78. package/dist/commands/teamwork/guard.d.ts +2 -2
  79. package/dist/commands/teamwork/guard.d.ts.map +1 -1
  80. package/dist/commands/teamwork/guard.js +23 -5
  81. package/dist/commands/teamwork/guard.js.map +1 -1
  82. package/dist/commands/teamwork/index.d.ts.map +1 -1
  83. package/dist/commands/teamwork/index.js +139 -10
  84. package/dist/commands/teamwork/index.js.map +1 -1
  85. package/dist/commands/teamwork/kill.d.ts +2 -0
  86. package/dist/commands/teamwork/kill.d.ts.map +1 -1
  87. package/dist/commands/teamwork/kill.js +79 -38
  88. package/dist/commands/teamwork/kill.js.map +1 -1
  89. package/dist/commands/teamwork/messages.d.ts.map +1 -1
  90. package/dist/commands/teamwork/messages.js +3 -0
  91. package/dist/commands/teamwork/messages.js.map +1 -1
  92. package/dist/commands/teamwork/plan.d.ts +22 -0
  93. package/dist/commands/teamwork/plan.d.ts.map +1 -0
  94. package/dist/commands/teamwork/plan.js +288 -0
  95. package/dist/commands/teamwork/plan.js.map +1 -0
  96. package/dist/commands/teamwork/process-identity.d.ts +3 -0
  97. package/dist/commands/teamwork/process-identity.d.ts.map +1 -0
  98. package/dist/commands/teamwork/process-identity.js +24 -0
  99. package/dist/commands/teamwork/process-identity.js.map +1 -0
  100. package/dist/commands/teamwork/profiles.d.ts +43 -6
  101. package/dist/commands/teamwork/profiles.d.ts.map +1 -1
  102. package/dist/commands/teamwork/profiles.js +138 -110
  103. package/dist/commands/teamwork/profiles.js.map +1 -1
  104. package/dist/commands/teamwork/prompt.d.ts +4 -0
  105. package/dist/commands/teamwork/prompt.d.ts.map +1 -0
  106. package/dist/commands/teamwork/prompt.js +33 -0
  107. package/dist/commands/teamwork/prompt.js.map +1 -0
  108. package/dist/commands/teamwork/result.d.ts +54 -0
  109. package/dist/commands/teamwork/result.d.ts.map +1 -0
  110. package/dist/commands/teamwork/result.js +94 -0
  111. package/dist/commands/teamwork/result.js.map +1 -0
  112. package/dist/commands/teamwork/rm.d.ts.map +1 -1
  113. package/dist/commands/teamwork/rm.js +64 -64
  114. package/dist/commands/teamwork/rm.js.map +1 -1
  115. package/dist/commands/teamwork/run.d.ts +5 -0
  116. package/dist/commands/teamwork/run.d.ts.map +1 -1
  117. package/dist/commands/teamwork/run.js +120 -24
  118. package/dist/commands/teamwork/run.js.map +1 -1
  119. package/dist/commands/teamwork/runtime.d.ts +9 -0
  120. package/dist/commands/teamwork/runtime.d.ts.map +1 -0
  121. package/dist/commands/teamwork/runtime.js +372 -0
  122. package/dist/commands/teamwork/runtime.js.map +1 -0
  123. package/dist/commands/teamwork/send.d.ts +1 -0
  124. package/dist/commands/teamwork/send.d.ts.map +1 -1
  125. package/dist/commands/teamwork/send.js +2 -1
  126. package/dist/commands/teamwork/send.js.map +1 -1
  127. package/dist/commands/teamwork/spawn.d.ts +10 -3
  128. package/dist/commands/teamwork/spawn.d.ts.map +1 -1
  129. package/dist/commands/teamwork/spawn.js +42 -27
  130. package/dist/commands/teamwork/spawn.js.map +1 -1
  131. package/dist/commands/teamwork/store/lock.js +28 -4
  132. package/dist/commands/teamwork/store/lock.js.map +1 -1
  133. package/dist/commands/teamwork/supervisor/inbox.d.ts +1 -0
  134. package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -1
  135. package/dist/commands/teamwork/supervisor/inbox.js +28 -9
  136. package/dist/commands/teamwork/supervisor/inbox.js.map +1 -1
  137. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -1
  138. package/dist/commands/teamwork/supervisor/stdout.js +2 -3
  139. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -1
  140. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -1
  141. package/dist/commands/teamwork/supervisor/warning.js +2 -3
  142. package/dist/commands/teamwork/supervisor/warning.js.map +1 -1
  143. package/dist/commands/teamwork/supervisor.d.ts +5 -0
  144. package/dist/commands/teamwork/supervisor.d.ts.map +1 -1
  145. package/dist/commands/teamwork/supervisor.js +21 -12
  146. package/dist/commands/teamwork/supervisor.js.map +1 -1
  147. package/dist/commands/teamwork/teammate-adapters.d.ts +0 -5
  148. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -1
  149. package/dist/commands/teamwork/teammate-adapters.js +9 -56
  150. package/dist/commands/teamwork/teammate-adapters.js.map +1 -1
  151. package/dist/commands/teamwork/teammate-config.d.ts +5 -1
  152. package/dist/commands/teamwork/teammate-config.d.ts.map +1 -1
  153. package/dist/commands/teamwork/teammate-config.js +62 -19
  154. package/dist/commands/teamwork/teammate-config.js.map +1 -1
  155. package/dist/commands/teamwork/teammate-launch.d.ts +15 -6
  156. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -1
  157. package/dist/commands/teamwork/teammate-launch.js +129 -22
  158. package/dist/commands/teamwork/teammate-launch.js.map +1 -1
  159. package/dist/commands/teamwork/wait.d.ts +2 -0
  160. package/dist/commands/teamwork/wait.d.ts.map +1 -1
  161. package/dist/commands/teamwork/wait.js +44 -3
  162. package/dist/commands/teamwork/wait.js.map +1 -1
  163. package/dist/commands/uninstall.d.ts.map +1 -1
  164. package/dist/commands/uninstall.js +92 -84
  165. package/dist/commands/uninstall.js.map +1 -1
  166. package/dist/commands/update.d.ts.map +1 -1
  167. package/dist/commands/update.js +635 -592
  168. package/dist/commands/update.js.map +1 -1
  169. package/dist/commands/workflow.d.ts.map +1 -1
  170. package/dist/commands/workflow.js +94 -89
  171. package/dist/commands/workflow.js.map +1 -1
  172. package/dist/configurators/shared.js +2 -2
  173. package/dist/configurators/shared.js.map +1 -1
  174. package/dist/configurators/workflow.d.ts.map +1 -1
  175. package/dist/configurators/workflow.js +14 -32
  176. package/dist/configurators/workflow.js.map +1 -1
  177. package/dist/constants/paths.d.ts +3 -10
  178. package/dist/constants/paths.d.ts.map +1 -1
  179. package/dist/constants/paths.js +4 -13
  180. package/dist/constants/paths.js.map +1 -1
  181. package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
  182. package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
  183. package/dist/migrations/manifests/0.7.1-beta.10.json +9 -0
  184. package/dist/migrations/manifests/0.7.1-beta.11.json +9 -0
  185. package/dist/migrations/manifests/0.7.1-beta.12.json +9 -0
  186. package/dist/migrations/manifests/0.7.1-beta.13.json +9 -0
  187. package/dist/migrations/manifests/0.7.1-beta.14.json +11 -0
  188. package/dist/migrations/manifests/0.7.1-beta.15.json +9 -0
  189. package/dist/migrations/manifests/0.7.1-beta.16.json +9 -0
  190. package/dist/migrations/manifests/0.7.1-beta.17.json +9 -0
  191. package/dist/migrations/manifests/0.7.1-beta.18.json +9 -0
  192. package/dist/migrations/manifests/0.7.1-beta.19.json +9 -0
  193. package/dist/migrations/manifests/0.7.1-beta.2.json +522 -0
  194. package/dist/migrations/manifests/0.7.1-beta.3.json +9 -0
  195. package/dist/migrations/manifests/0.7.1-beta.4.json +9 -0
  196. package/dist/migrations/manifests/0.7.1-beta.5.json +9 -0
  197. package/dist/migrations/manifests/0.7.1-beta.6.json +10 -0
  198. package/dist/migrations/manifests/0.7.1-beta.7.json +8 -0
  199. package/dist/migrations/manifests/0.7.1-beta.8.json +481 -0
  200. package/dist/migrations/manifests/0.7.1-beta.9.json +191 -0
  201. package/dist/migrations/manifests/0.7.2-beta.0.json +11 -0
  202. package/dist/templates/claude/hooks/statusline.py +9 -4
  203. package/dist/templates/codex/hooks/session-start.py +14 -11
  204. package/dist/templates/codex/hooks.json +10 -0
  205. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +2 -0
  206. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +2 -0
  207. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +1 -1
  208. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +1 -1
  209. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +5 -5
  210. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +3 -2
  211. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  212. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +1 -1
  213. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +14 -2
  214. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +8 -2
  215. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +2 -1
  216. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +2 -2
  217. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +8 -0
  218. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +4 -2
  219. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +2 -2
  220. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +6 -0
  221. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +18 -3
  222. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +34 -3
  223. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +37 -26
  224. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +128 -14
  225. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +152 -20
  226. package/dist/templates/common/commands/start.md +2 -2
  227. package/dist/templates/common/skills/before-dev.md +1 -4
  228. package/dist/templates/common/skills/break-loop.md +7 -18
  229. package/dist/templates/common/skills/check.md +7 -11
  230. package/dist/templates/common/skills/{brainstorm.md → request-intake.md} +5 -3
  231. package/dist/templates/common/skills/update-spec.md +5 -7
  232. package/dist/templates/copilot/hooks/session-start.py +14 -11
  233. package/dist/templates/copilot/index.d.ts +1 -6
  234. package/dist/templates/copilot/index.d.ts.map +1 -1
  235. package/dist/templates/copilot/index.js +1 -14
  236. package/dist/templates/copilot/index.js.map +1 -1
  237. package/dist/templates/extract.js +4 -0
  238. package/dist/templates/extract.js.map +1 -1
  239. package/dist/templates/markdown/spec/backend/index.md.txt +4 -35
  240. package/dist/templates/markdown/spec/frontend/index.md.txt +4 -36
  241. package/dist/templates/omp/extensions/trellis/index.ts.txt +2 -2
  242. package/dist/templates/opencode/lib/session-utils.js +301 -209
  243. package/dist/templates/pi/extensions/trellis/index.ts.txt +17 -1
  244. package/dist/templates/shared-hooks/guard-teamwork-wait.py +53 -0
  245. package/dist/templates/shared-hooks/index.d.ts +1 -1
  246. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  247. package/dist/templates/shared-hooks/index.js +1 -0
  248. package/dist/templates/shared-hooks/index.js.map +1 -1
  249. package/dist/templates/shared-hooks/inject-workflow-state.py +8 -3
  250. package/dist/templates/shared-hooks/session-start.py +16 -12
  251. package/dist/templates/snow/SNOW.md +1 -1
  252. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +9 -0
  253. package/dist/templates/trellis/agents/profiles/architecture.md +9 -0
  254. package/dist/templates/trellis/agents/profiles/diagnosis.md +9 -0
  255. package/dist/templates/trellis/agents/profiles/exploration.md +9 -0
  256. package/dist/templates/trellis/agents/profiles/research.md +9 -0
  257. package/dist/templates/trellis/agents/profiles/review.md +9 -0
  258. package/dist/templates/trellis/agents/profiles/verification.md +9 -0
  259. package/dist/templates/trellis/agents/teammate-profiles.json +56 -4
  260. package/dist/templates/trellis/agents/teammate.md +4 -52
  261. package/dist/templates/trellis/config.yaml +2 -5
  262. package/dist/templates/trellis/index.d.ts +4 -0
  263. package/dist/templates/trellis/index.d.ts.map +1 -1
  264. package/dist/templates/trellis/index.js +22 -4
  265. package/dist/templates/trellis/index.js.map +1 -1
  266. package/dist/templates/trellis/scripts/add_session.py +1 -1
  267. package/dist/templates/trellis/scripts/common/active_task.py +3 -47
  268. package/dist/templates/trellis/scripts/common/paths.py +9 -25
  269. package/dist/templates/trellis/scripts/common/safe_commit.py +8 -8
  270. package/dist/templates/trellis/scripts/common/task_context.py +11 -51
  271. package/dist/templates/trellis/scripts/common/task_continuity.py +129 -60
  272. package/dist/templates/trellis/scripts/common/task_layout.py +80 -0
  273. package/dist/templates/trellis/scripts/common/task_move.py +345 -0
  274. package/dist/templates/trellis/scripts/common/task_store.py +35 -57
  275. package/dist/templates/trellis/scripts/common/task_utils.py +23 -19
  276. package/dist/templates/trellis/scripts/common/task_workflow.py +339 -46
  277. package/dist/templates/trellis/scripts/common/tasks.py +6 -5
  278. package/dist/templates/trellis/scripts/hooks/linear_sync.py +13 -3
  279. package/dist/templates/trellis/scripts/task.py +71 -24
  280. package/dist/templates/trellis/teamwork.json +10 -0
  281. package/dist/templates/trellis/workflow.md +11 -2
  282. package/dist/utils/ablation-store.d.ts +1 -1
  283. package/dist/utils/ablation-store.d.ts.map +1 -1
  284. package/dist/utils/ablation-store.js +58 -24
  285. package/dist/utils/ablation-store.js.map +1 -1
  286. package/dist/utils/asset-backup.d.ts +138 -0
  287. package/dist/utils/asset-backup.d.ts.map +1 -0
  288. package/dist/utils/asset-backup.js +513 -0
  289. package/dist/utils/asset-backup.js.map +1 -0
  290. package/dist/utils/atomic-write.d.ts +1 -1
  291. package/dist/utils/atomic-write.d.ts.map +1 -1
  292. package/dist/utils/atomic-write.js +6 -3
  293. package/dist/utils/atomic-write.js.map +1 -1
  294. package/dist/utils/manifest-prune.d.ts +4 -1
  295. package/dist/utils/manifest-prune.d.ts.map +1 -1
  296. package/dist/utils/manifest-prune.js +17 -6
  297. package/dist/utils/manifest-prune.js.map +1 -1
  298. package/dist/utils/template-fetcher.d.ts +4 -19
  299. package/dist/utils/template-fetcher.d.ts.map +1 -1
  300. package/dist/utils/template-fetcher.js +9 -45
  301. package/dist/utils/template-fetcher.js.map +1 -1
  302. package/dist/utils/template-hash.d.ts +5 -4
  303. package/dist/utils/template-hash.d.ts.map +1 -1
  304. package/dist/utils/template-hash.js +24 -17
  305. package/dist/utils/template-hash.js.map +1 -1
  306. package/package.json +3 -2
  307. package/dist/templates/copilot/prompts/before-dev.prompt.md +0 -39
  308. package/dist/templates/copilot/prompts/brainstorm.prompt.md +0 -13
  309. package/dist/templates/copilot/prompts/break-loop.prompt.md +0 -129
  310. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +0 -170
  311. package/dist/templates/copilot/prompts/check.prompt.md +0 -110
  312. package/dist/templates/copilot/prompts/create-command.prompt.md +0 -116
  313. package/dist/templates/copilot/prompts/finish-work.prompt.md +0 -22
  314. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +0 -223
  315. package/dist/templates/copilot/prompts/onboard.prompt.md +0 -362
  316. package/dist/templates/copilot/prompts/record-session.prompt.md +0 -58
  317. package/dist/templates/copilot/prompts/start.prompt.md +0 -65
  318. package/dist/templates/copilot/prompts/update-spec.prompt.md +0 -358
@@ -11,13 +11,14 @@ import { compareVersions } from "../utils/compare-versions.js";
11
11
  import { toPosix } from "../utils/posix.js";
12
12
  import { setupProxy } from "../utils/proxy.js";
13
13
  import { emptyTaskJson } from "../utils/task-json.js";
14
+ import { assetBackupScope, bindAssetUpdate, createAssetBackup, withAssetMaintenance, } from "../utils/asset-backup.js";
14
15
  // Import templates for comparison
15
16
  import { getAllScripts, getAllAgents,
16
17
  // Configuration
17
- configYamlTemplate, gitignoreTemplate, workflowMdTemplate, } from "../templates/trellis/index.js";
18
+ configYamlTemplate, teamworkConfigTemplate, gitignoreTemplate, workflowMdTemplate, } from "../templates/trellis/index.js";
18
19
  import { agentsMdContent } from "../templates/markdown/index.js";
19
20
  import { COPILOT_INSTRUCTIONS_BLOCK_END, COPILOT_INSTRUCTIONS_BLOCK_START, COPILOT_INSTRUCTIONS_PATH, getCopilotInstructions, } from "../templates/copilot/index.js";
20
- import { ALL_MANAGED_DIRS, getConfiguredPlatforms, collectPlatformTemplates, } from "../configurators/index.js";
21
+ import { getConfiguredPlatforms, collectPlatformTemplates, } from "../configurators/index.js";
21
22
  import { replacePythonCommandLiterals } from "../configurators/shared.js";
22
23
  import { preserveCodexAgentModelKeys } from "../configurators/codex.js";
23
24
  import { printZcodeSetupHint } from "../configurators/zcode.js";
@@ -187,6 +188,50 @@ bypassUpdateSkip = false) {
187
188
  results.push({ item, action: "skip-missing" });
188
189
  }
189
190
  }
191
+ for (const { item, action } of [...results]) {
192
+ if (!item.from.startsWith(".trellis/scripts/") ||
193
+ !item.from.endsWith(".py") ||
194
+ !item.allowed_hashes?.length ||
195
+ (action !== "delete" &&
196
+ !(action === "skip-missing" && !fs.existsSync(path.join(cwd, item.from)))))
197
+ continue;
198
+ const cacheDir = path.join(path.dirname(item.from), "__pycache__");
199
+ try {
200
+ const fullCacheDir = path.join(cwd, cacheDir);
201
+ if (fs.lstatSync(fullCacheDir).isSymbolicLink())
202
+ continue;
203
+ if (fs.realpathSync(fullCacheDir) !== path.resolve(fullCacheDir))
204
+ continue;
205
+ for (const entry of fs.readdirSync(fullCacheDir, {
206
+ withFileTypes: true,
207
+ })) {
208
+ const parts = entry.name.split(".");
209
+ if (!entry.isFile() ||
210
+ parts[0] !== path.basename(item.from, ".py") ||
211
+ !/^cpython-\d+$/.test(parts[1] ?? "") ||
212
+ !((parts.length === 3 && parts[2] === "pyc") ||
213
+ (parts.length === 4 &&
214
+ /^opt-[12]$/.test(parts[2] ?? "") &&
215
+ parts[3] === "pyc")))
216
+ continue;
217
+ const from = toPosix(path.join(cacheDir, entry.name));
218
+ const skipped = !bypassUpdateSkip &&
219
+ skipPaths.some((skip) => [item.from, from].some((file) => file === skip ||
220
+ file.startsWith(skip.endsWith("/") ? skip : skip + "/")));
221
+ results.push({
222
+ item: {
223
+ type: "safe-file-delete",
224
+ from,
225
+ description: "Bytecode cache of a retired generated script",
226
+ },
227
+ action: skipped ? "skip-update-skip" : "delete",
228
+ });
229
+ }
230
+ }
231
+ catch {
232
+ // Missing or unreadable caches are not cleanup candidates.
233
+ }
234
+ }
190
235
  return results;
191
236
  }
192
237
  /**
@@ -559,6 +604,7 @@ bypassUpdateSkip = false) {
559
604
  files.set(`${PATHS.AGENTS}/${agentFile}`, content);
560
605
  }
561
606
  // Configuration
607
+ files.set(`${DIR_NAMES.WORKFLOW}/teamwork.json`, teamworkConfigTemplate);
562
608
  files.set(`${DIR_NAMES.WORKFLOW}/config.yaml`, preserveExistingRegistryConfig(cwd, configYamlTemplate));
563
609
  files.set(`${DIR_NAMES.WORKFLOW}/.gitignore`, gitignoreTemplate);
564
610
  // workflow.md is included here because it is runtime-parsed by
@@ -842,30 +888,6 @@ async function promptConflictResolution(file, options, applyToAll) {
842
888
  }
843
889
  return action;
844
890
  }
845
- /**
846
- * Create a timestamped backup directory path
847
- */
848
- function createBackupDirPath(cwd) {
849
- const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
850
- return path.join(cwd, DIR_NAMES.WORKFLOW, `.backup-${timestamp}`);
851
- }
852
- /**
853
- * Backup a single file to the backup directory
854
- */
855
- function backupFile(cwd, backupDir, relativePath) {
856
- const srcPath = path.join(cwd, relativePath);
857
- if (!fs.existsSync(srcPath))
858
- return;
859
- const backupPath = path.join(backupDir, relativePath);
860
- fs.mkdirSync(path.dirname(backupPath), { recursive: true });
861
- fs.copyFileSync(srcPath, backupPath);
862
- }
863
- /**
864
- * Directories to backup as complete snapshot (derived from platform registry)
865
- */
866
- const BACKUP_DIRS = ALL_MANAGED_DIRS;
867
- /** Root-level managed files to include in update backups. */
868
- const BACKUP_FILES = [FILE_NAMES.AGENTS];
869
891
  /**
870
892
  * Patterns to exclude from backup (user data that shouldn't be backed up)
871
893
  */
@@ -877,6 +899,8 @@ const BACKUP_EXCLUDE_PATTERNS = [
877
899
  "/spec/", // Spec files (user-customized content)
878
900
  "/backlog/", // Backlog data (user data)
879
901
  "/agent-traces/", // Agent traces (user data, legacy name)
902
+ "/session-handoffs/",
903
+ "/sessions/",
880
904
  // Platform-native worktree dirs — these are full sub-repos the CLI
881
905
  // spawns for parallel sessions. Backing them up on every update would
882
906
  // snapshot the entire nested working tree. Confirmed conventions:
@@ -908,44 +932,11 @@ export function shouldExcludeFromBackup(relativePath) {
908
932
  return false;
909
933
  }
910
934
  /**
911
- * Create complete snapshot backup of all managed directories
912
- * Backs up all managed platform/workflow directories entirely
913
- * (excluding user data like workspace/, tasks/, backlog/)
935
+ * Snapshot only positively owned assets and the current update scope.
914
936
  */
915
- function createFullBackup(cwd) {
916
- const backupDir = createBackupDirPath(cwd);
917
- let hasFiles = false;
918
- for (const dir of BACKUP_DIRS) {
919
- const dirPath = path.join(cwd, dir);
920
- if (!fs.existsSync(dirPath))
921
- continue;
922
- const files = collectAllFiles(dirPath, cwd);
923
- for (const fullPath of files) {
924
- const relativePath = path.relative(cwd, fullPath);
925
- // Skip excluded paths
926
- if (shouldExcludeFromBackup(relativePath))
927
- continue;
928
- // Create backup
929
- if (!hasFiles) {
930
- fs.mkdirSync(backupDir, { recursive: true });
931
- hasFiles = true;
932
- }
933
- backupFile(cwd, backupDir, relativePath);
934
- }
935
- }
936
- for (const relativePath of BACKUP_FILES) {
937
- const fullPath = path.join(cwd, relativePath);
938
- if (!fs.existsSync(fullPath))
939
- continue;
940
- if (shouldExcludeFromBackup(relativePath))
941
- continue;
942
- if (!hasFiles) {
943
- fs.mkdirSync(backupDir, { recursive: true });
944
- hasFiles = true;
945
- }
946
- backupFile(cwd, backupDir, relativePath);
947
- }
948
- return hasFiles ? backupDir : null;
937
+ function createFullBackup(cwd, scope) {
938
+ const backup = createAssetBackup(cwd, "update", assetBackupScope(cwd, scope));
939
+ return path.join(cwd, DIR_NAMES.WORKFLOW, backup.id);
949
940
  }
950
941
  /**
951
942
  * Update version file
@@ -1584,580 +1575,632 @@ export function renameTracesToJournal(workspaceDir) {
1584
1575
  */
1585
1576
  export async function update(options) {
1586
1577
  const cwd = process.cwd();
1587
- // Check if Trellis is initialized
1588
- if (!fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW))) {
1589
- console.log(chalk.red("Error: Trellis not initialized in this directory."));
1590
- console.log(chalk.gray("Run 'trellis init' first."));
1591
- return;
1592
- }
1593
- console.log(chalk.cyan("\nTrellis Update"));
1594
- console.log(chalk.cyan("══════════════\n"));
1595
- // Set up proxy before any network calls (npm version check)
1596
- setupProxy();
1597
- // Get versions
1598
- const projectVersion = getInstalledVersion(cwd);
1599
- const cliVersion = VERSION;
1600
- const latestNpmVersion = await getLatestNpmVersion();
1601
- // Version comparison
1602
- const cliVsProject = compareVersions(cliVersion, projectVersion);
1603
- const cliVsNpm = latestNpmVersion
1604
- ? compareVersions(cliVersion, latestNpmVersion)
1605
- : 0;
1606
- // Display versions with context
1607
- console.log(`Project version: ${chalk.white(projectVersion)}`);
1608
- console.log(`CLI version: ${chalk.white(cliVersion)}`);
1609
- if (latestNpmVersion) {
1610
- console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
1611
- }
1612
- else {
1613
- console.log(chalk.gray("Latest on npm: (unable to fetch)"));
1614
- }
1615
- console.log("");
1616
- // Check if CLI is outdated compared to npm
1617
- if (cliVsNpm < 0 && latestNpmVersion) {
1618
- console.log(chalk.yellow(`⚠️ Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
1619
- console.log(chalk.yellow(` Run: trellis upgrade\n`));
1620
- }
1621
- // Check for downgrade situation
1622
- if (cliVsProject < 0) {
1623
- console.log(chalk.red(`❌ Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
1624
- console.log(chalk.red(` This would DOWNGRADE your project!\n`));
1625
- if (!options.allowDowngrade) {
1626
- console.log(chalk.gray("Solutions:"));
1627
- console.log(chalk.gray(` 1. Update your CLI: trellis upgrade`));
1628
- console.log(chalk.gray(` 2. Force downgrade: trellis update --allow-downgrade\n`));
1578
+ return withAssetMaintenance(cwd, async () => {
1579
+ // Check if Trellis is initialized
1580
+ if (!fs.existsSync(path.join(cwd, DIR_NAMES.WORKFLOW))) {
1581
+ console.log(chalk.red("Error: Trellis not initialized in this directory."));
1582
+ console.log(chalk.gray("Run 'trellis init' first."));
1629
1583
  return;
1630
1584
  }
1631
- console.log(chalk.yellow("⚠️ --allow-downgrade flag set. Proceeding with downgrade...\n"));
1632
- }
1633
- // Migration metadata is displayed at the end to prevent scrolling off screen
1634
- // Load template hashes for modification detection
1635
- let hashes = loadHashes(cwd);
1636
- const zcodeConfigured = getConfiguredPlatforms(cwd).has("zcode");
1637
- const isFirstHashTracking = Object.keys(hashes).length === 0;
1638
- // Handle unknown version - skip regular migrations but safe-file-delete still runs
1639
- const isUnknownVersion = projectVersion === "unknown";
1640
- if (isUnknownVersion) {
1641
- console.log(chalk.yellow("⚠️ No version file found. Skipping migrations — run trellis init to fix."));
1642
- console.log(chalk.gray(" Template updates will still be applied."));
1643
- console.log(chalk.gray(" Safe file cleanup will still run (hash-verified).\n"));
1644
- }
1645
- // Detect legacy Codex (has .agents/skills/ tracked by Trellis but no .codex/)
1646
- // NOTE: this MUST happen before pruneOrphanManifestKeys below, since the
1647
- // detector reads the raw manifest looking for .agents/skills/ markers that
1648
- // the prune step would otherwise consider orphans (codex hasn't been added
1649
- // to configuredPlatforms yet at this point).
1650
- const codexUpgradeNeeded = needsCodexUpgrade(cwd);
1651
- if (codexUpgradeNeeded) {
1652
- console.log(chalk.yellow(" Legacy Codex detected: .agents/skills/ tracked without .codex/ — will create .codex/ directory"));
1653
- }
1654
- // Self-heal poisoned manifests: prune entries that no current platform
1655
- // configurator owns. This silently removes user-owned paths that early
1656
- // buggy versions of `trellis init` over-hashed (e.g. .codex/sessions/*).
1657
- // Include codex in known-platforms when codexUpgradeNeeded so legacy Codex
1658
- // markers under .agents/skills/ survive into the upgrade flow.
1659
- {
1660
- const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
1661
- if (codexUpgradeNeeded)
1662
- configuredPlatforms.add("codex");
1663
- const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes);
1664
- if (prune.pruned.length > 0) {
1665
- console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
1666
- hashes = prune.hashes;
1667
- }
1668
- }
1669
- // For breaking releases with recommendMigrate + --migrate, bypass update.skip
1670
- // across the board (safe-file-delete, new file writes, template updates).
1671
- // Why: honoring skip here leaves users forever half-migrated — old deprecated
1672
- // files persist under skip-protected paths, new commands like `continue.md`
1673
- // never land, and every future update re-flags the same mess. Rename
1674
- // migrations already ignore update.skip; this makes the rest consistent
1675
- // during a breaking upgrade. User customizations are still guarded by the
1676
- // per-file conflict prompt ("Modified by you") at write time.
1677
- const breakingBypass = options.migrate === true &&
1678
- cliVsProject > 0 &&
1679
- projectVersion !== "unknown" &&
1680
- (() => {
1681
- const md = getMigrationMetadata(projectVersion, cliVersion);
1682
- return md.breaking && md.recommendMigrate;
1683
- })();
1684
- // Collect templates (used for both migration classification and change analysis)
1685
- const templates = await collectTemplateFiles(cwd, codexUpgradeNeeded ? new Set(["codex"]) : undefined, breakingBypass);
1686
- // Load update.skip paths (used for both safe-file-delete and template collection)
1687
- const skipPaths = loadUpdateSkipPaths(cwd);
1688
- // Collect safe-file-delete items from ALL manifests (hash match is the safety net)
1689
- // This runs regardless of version — unknown version still gets safe cleanup
1690
- const allMigrations = getAllMigrations();
1691
- const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, new Set(templates.keys()), breakingBypass);
1692
- const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
1693
- // Check for pending regular migrations (skip if unknown version)
1694
- let pendingMigrations = isUnknownVersion
1695
- ? []
1696
- : getMigrationsForVersion(projectVersion, cliVersion);
1697
- // Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
1698
- // This handles cases where version was updated but migrations weren't applied
1699
- const orphanedMigrations = allMigrations.filter((item) => {
1700
- // Only check rename and rename-dir migrations
1701
- if (item.type !== "rename" && item.type !== "rename-dir")
1702
- return false;
1703
- if (!item.from || !item.to)
1704
- return false;
1705
- const oldPath = path.join(cwd, item.from);
1706
- const newPath = path.join(cwd, item.to);
1707
- // Orphaned if: source exists AND target doesn't exist
1708
- // AND this migration isn't already in pendingMigrations
1709
- const sourceExists = fs.existsSync(oldPath);
1710
- const targetExists = fs.existsSync(newPath);
1711
- const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
1712
- return sourceExists && !targetExists && !alreadyPending;
1713
- });
1714
- // Add orphaned migrations to pending (they need to be applied)
1715
- if (orphanedMigrations.length > 0) {
1716
- console.log(chalk.yellow("⚠️ Detected incomplete migrations from previous updates:"));
1717
- for (const item of orphanedMigrations) {
1718
- console.log(chalk.yellow(` ${item.from} → ${item.to}`));
1585
+ console.log(chalk.cyan("\nTrellis Update"));
1586
+ console.log(chalk.cyan("══════════════\n"));
1587
+ // Set up proxy before any network calls (npm version check)
1588
+ setupProxy();
1589
+ // Get versions
1590
+ const projectVersion = getInstalledVersion(cwd);
1591
+ const cliVersion = VERSION;
1592
+ const latestNpmVersion = await getLatestNpmVersion();
1593
+ // Version comparison
1594
+ const cliVsProject = compareVersions(cliVersion, projectVersion);
1595
+ const cliVsNpm = latestNpmVersion
1596
+ ? compareVersions(cliVersion, latestNpmVersion)
1597
+ : 0;
1598
+ // Display versions with context
1599
+ console.log(`Project version: ${chalk.white(projectVersion)}`);
1600
+ console.log(`CLI version: ${chalk.white(cliVersion)}`);
1601
+ if (latestNpmVersion) {
1602
+ console.log(`Latest on npm: ${chalk.white(latestNpmVersion)}`);
1603
+ }
1604
+ else {
1605
+ console.log(chalk.gray("Latest on npm: (unable to fetch)"));
1719
1606
  }
1720
1607
  console.log("");
1721
- pendingMigrations = [...pendingMigrations, ...orphanedMigrations];
1722
- }
1723
- const hasMigrations = pendingMigrations.length > 0;
1724
- // Classify migrations (stored for later backup creation)
1725
- let classifiedMigrations = null;
1726
- if (hasMigrations) {
1727
- console.log(chalk.cyan("Analyzing migrations...\n"));
1728
- classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
1729
- printMigrationSummary(classifiedMigrations);
1730
- // Hard-stop: pending rename/delete work from a breaking release requires --migrate.
1731
- // Why: without --migrate, those entries are skipped and update()'s later path silently
1732
- // bumps the version stamp, leaving old paths orphaned next to new templates. Force
1733
- // explicit opt-in so the user can't half-migrate by accident.
1734
- const pendingMigrationCount = classifiedMigrations.auto.length +
1735
- classifiedMigrations.confirm.length +
1736
- classifiedMigrations.conflict.length;
1737
- if (pendingMigrationCount > 0 &&
1738
- !options.migrate &&
1739
- !options.dryRun &&
1740
- cliVsProject > 0 &&
1741
- projectVersion !== "unknown") {
1742
- const gateMetadata = getMigrationMetadata(projectVersion, cliVersion);
1743
- if (gateMetadata.breaking && gateMetadata.recommendMigrate) {
1744
- console.log(chalk.bgRed.white.bold(" ✖ MIGRATION REQUIRED ") +
1745
- chalk.red(` Breaking changes between ${projectVersion} → ${cliVersion} require --migrate.`));
1746
- console.log("");
1747
- console.log(chalk.yellow(` Run: trellis update --migrate`));
1748
- console.log("");
1749
- console.log(chalk.gray(" Without --migrate, renamed/relocated files from breaking releases aren't moved,\n" +
1750
- " leaving your project with stale paths alongside new templates.\n" +
1751
- " Use --dry-run to preview what --migrate will do."));
1752
- process.exit(1);
1608
+ // Check if CLI is outdated compared to npm
1609
+ if (cliVsNpm < 0 && latestNpmVersion) {
1610
+ console.log(chalk.yellow(`⚠️ Your CLI (${cliVersion}) is behind npm (${latestNpmVersion}).`));
1611
+ console.log(chalk.yellow(` Run: trellis upgrade\n`));
1612
+ }
1613
+ // Check for downgrade situation
1614
+ if (cliVsProject < 0) {
1615
+ console.log(chalk.red(`❌ Cannot update: CLI version (${cliVersion}) < project version (${projectVersion})`));
1616
+ console.log(chalk.red(` This would DOWNGRADE your project!\n`));
1617
+ if (!options.allowDowngrade) {
1618
+ console.log(chalk.gray("Solutions:"));
1619
+ console.log(chalk.gray(` 1. Update your CLI: trellis upgrade`));
1620
+ console.log(chalk.gray(` 2. Force downgrade: trellis update --allow-downgrade\n`));
1621
+ return;
1622
+ }
1623
+ console.log(chalk.yellow("⚠️ --allow-downgrade flag set. Proceeding with downgrade...\n"));
1624
+ }
1625
+ // Migration metadata is displayed at the end to prevent scrolling off screen
1626
+ // Load template hashes for modification detection
1627
+ let hashes = loadHashes(cwd);
1628
+ const zcodeConfigured = getConfiguredPlatforms(cwd).has("zcode");
1629
+ const isFirstHashTracking = Object.keys(hashes).length === 0;
1630
+ // Handle unknown version - skip regular migrations but safe-file-delete still runs
1631
+ const isUnknownVersion = projectVersion === "unknown";
1632
+ if (isUnknownVersion) {
1633
+ console.log(chalk.yellow("⚠️ No version file found. Skipping migrations — run trellis init to fix."));
1634
+ console.log(chalk.gray(" Template updates will still be applied."));
1635
+ console.log(chalk.gray(" Safe file cleanup will still run (hash-verified).\n"));
1636
+ }
1637
+ // Detect legacy Codex (has .agents/skills/ tracked by Trellis but no .codex/)
1638
+ // NOTE: this MUST happen before pruneOrphanManifestKeys below, since the
1639
+ // detector reads the raw manifest looking for .agents/skills/ markers that
1640
+ // the prune step would otherwise consider orphans (codex hasn't been added
1641
+ // to configuredPlatforms yet at this point).
1642
+ const codexUpgradeNeeded = needsCodexUpgrade(cwd);
1643
+ if (codexUpgradeNeeded) {
1644
+ console.log(chalk.yellow(" Legacy Codex detected: .agents/skills/ tracked without .codex/ — will create .codex/ directory"));
1645
+ }
1646
+ // Self-heal poisoned manifests: prune entries that no current platform
1647
+ // configurator owns. This silently removes user-owned paths that early
1648
+ // buggy versions of `trellis init` over-hashed (e.g. .codex/sessions/*).
1649
+ // Include codex in known-platforms when codexUpgradeNeeded so legacy Codex
1650
+ // markers under .agents/skills/ survive into the upgrade flow.
1651
+ {
1652
+ const configuredPlatforms = new Set(getConfiguredPlatforms(cwd));
1653
+ if (codexUpgradeNeeded)
1654
+ configuredPlatforms.add("codex");
1655
+ const prune = pruneOrphanManifestKeys(cwd, [...configuredPlatforms], hashes, { persist: !options.dryRun });
1656
+ if (prune.pruned.length > 0) {
1657
+ console.log(chalk.gray(` Pruned ${prune.pruned.length} orphan manifest entries from .template-hashes.json`));
1658
+ hashes = prune.hashes;
1753
1659
  }
1754
1660
  }
1755
- // Soft hint: non-breaking migrations or projects that chose not to set recommendMigrate
1756
- if (!options.migrate) {
1757
- const autoCount = classifiedMigrations.auto.length;
1758
- const confirmCount = classifiedMigrations.confirm.length;
1759
- if (autoCount > 0 || confirmCount > 0) {
1760
- console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
1761
- if (confirmCount > 0) {
1762
- console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
1763
- }
1764
- else {
1661
+ // For breaking releases with recommendMigrate + --migrate, bypass update.skip
1662
+ // across the board (safe-file-delete, new file writes, template updates).
1663
+ // Why: honoring skip here leaves users forever half-migrated — old deprecated
1664
+ // files persist under skip-protected paths, new commands like `continue.md`
1665
+ // never land, and every future update re-flags the same mess. Rename
1666
+ // migrations already ignore update.skip; this makes the rest consistent
1667
+ // during a breaking upgrade. User customizations are still guarded by the
1668
+ // per-file conflict prompt ("Modified by you") at write time.
1669
+ const breakingBypass = options.migrate === true &&
1670
+ cliVsProject > 0 &&
1671
+ projectVersion !== "unknown" &&
1672
+ (() => {
1673
+ const md = getMigrationMetadata(projectVersion, cliVersion);
1674
+ return md.breaking && md.recommendMigrate;
1675
+ })();
1676
+ // Collect templates (used for both migration classification and change analysis)
1677
+ const templates = await collectTemplateFiles(cwd, codexUpgradeNeeded ? new Set(["codex"]) : undefined, breakingBypass);
1678
+ // Load update.skip paths (used for both safe-file-delete and template collection)
1679
+ const skipPaths = loadUpdateSkipPaths(cwd);
1680
+ // Collect safe-file-delete items from ALL manifests (hash match is the safety net)
1681
+ // This runs regardless of version — unknown version still gets safe cleanup
1682
+ const allMigrations = getAllMigrations();
1683
+ const safeFileDeletes = collectSafeFileDeletes(allMigrations, cwd, skipPaths, new Set(templates.keys()), breakingBypass);
1684
+ const hasSafeDeletes = safeFileDeletes.filter((c) => c.action === "delete").length > 0;
1685
+ // Check for pending regular migrations (skip if unknown version)
1686
+ let pendingMigrations = isUnknownVersion
1687
+ ? []
1688
+ : getMigrationsForVersion(projectVersion, cliVersion);
1689
+ // Also check for "orphaned" migrations - where source still exists but version says we shouldn't migrate
1690
+ // This handles cases where version was updated but migrations weren't applied
1691
+ const orphanedMigrations = allMigrations.filter((item) => {
1692
+ // Only check rename and rename-dir migrations
1693
+ if (item.type !== "rename" && item.type !== "rename-dir")
1694
+ return false;
1695
+ if (!item.from || !item.to)
1696
+ return false;
1697
+ const oldPath = path.join(cwd, item.from);
1698
+ const newPath = path.join(cwd, item.to);
1699
+ // Orphaned if: source exists AND target doesn't exist
1700
+ // AND this migration isn't already in pendingMigrations
1701
+ const sourceExists = fs.existsSync(oldPath);
1702
+ const targetExists = fs.existsSync(newPath);
1703
+ const alreadyPending = pendingMigrations.some((m) => m.from === item.from && m.to === item.to);
1704
+ return sourceExists && !targetExists && !alreadyPending;
1705
+ });
1706
+ // Add orphaned migrations to pending (they need to be applied)
1707
+ if (orphanedMigrations.length > 0) {
1708
+ console.log(chalk.yellow("⚠️ Detected incomplete migrations from previous updates:"));
1709
+ for (const item of orphanedMigrations) {
1710
+ console.log(chalk.yellow(` ${item.from} → ${item.to}`));
1711
+ }
1712
+ console.log("");
1713
+ pendingMigrations = [...pendingMigrations, ...orphanedMigrations];
1714
+ }
1715
+ const hasMigrations = pendingMigrations.length > 0;
1716
+ // Classify migrations (stored for later backup creation)
1717
+ let classifiedMigrations = null;
1718
+ if (hasMigrations) {
1719
+ console.log(chalk.cyan("Analyzing migrations...\n"));
1720
+ classifiedMigrations = classifyMigrations(pendingMigrations, cwd, hashes, templates);
1721
+ printMigrationSummary(classifiedMigrations);
1722
+ // Hard-stop: pending rename/delete work from a breaking release requires --migrate.
1723
+ // Why: without --migrate, those entries are skipped and update()'s later path silently
1724
+ // bumps the version stamp, leaving old paths orphaned next to new templates. Force
1725
+ // explicit opt-in so the user can't half-migrate by accident.
1726
+ const pendingMigrationCount = classifiedMigrations.auto.length +
1727
+ classifiedMigrations.confirm.length +
1728
+ classifiedMigrations.conflict.length;
1729
+ if (pendingMigrationCount > 0 &&
1730
+ !options.migrate &&
1731
+ !options.dryRun &&
1732
+ cliVsProject > 0 &&
1733
+ projectVersion !== "unknown") {
1734
+ const gateMetadata = getMigrationMetadata(projectVersion, cliVersion);
1735
+ if (gateMetadata.breaking && gateMetadata.recommendMigrate) {
1736
+ console.log(chalk.bgRed.white.bold(" ✖ MIGRATION REQUIRED ") +
1737
+ chalk.red(` Breaking changes between ${projectVersion} → ${cliVersion} require --migrate.`));
1765
1738
  console.log("");
1739
+ console.log(chalk.yellow(` Run: trellis update --migrate`));
1740
+ console.log("");
1741
+ console.log(chalk.gray(" Without --migrate, renamed/relocated files from breaking releases aren't moved,\n" +
1742
+ " leaving your project with stale paths alongside new templates.\n" +
1743
+ " Use --dry-run to preview what --migrate will do."));
1744
+ process.exit(1);
1766
1745
  }
1767
1746
  }
1768
- }
1769
- }
1770
- // Print safe-file-delete summary (always shown, runs without --migrate)
1771
- if (safeFileDeletes.length > 0) {
1772
- printSafeFileDeleteSummary(safeFileDeletes);
1773
- }
1774
- // Analyze changes (pass hashes for modification detection)
1775
- const changes = analyzeChanges(cwd, hashes, templates);
1776
- const unchangedFileHashRepairs = collectUnchangedFileHashRepairs(changes, hashes);
1777
- // Print summary
1778
- printChangeSummary(changes);
1779
- // First-time hash tracking hint
1780
- if (isFirstHashTracking && changes.changedFiles.length > 0) {
1781
- console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
1782
- console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
1783
- console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
1784
- }
1785
- // Ensure project-root .gitattributes carries the journal merge=union rule.
1786
- // Additive-only (see ensureGitattributes) — runs regardless of whether
1787
- // other template files changed, so it must sit before the "nothing to do"
1788
- // early-return below. Never touches disk in --dry-run.
1789
- if (!options.dryRun) {
1790
- ensureGitattributes(cwd);
1791
- }
1792
- // Check if there's anything to do
1793
- const isUpgrade = cliVsProject > 0;
1794
- const isDowngrade = cliVsProject < 0;
1795
- const isSameVersion = cliVsProject === 0;
1796
- // Check if we have pending migrations that need to be applied
1797
- const hasPendingMigrations = options.migrate &&
1798
- classifiedMigrations &&
1799
- (classifiedMigrations.auto.length > 0 ||
1800
- classifiedMigrations.confirm.length > 0);
1801
- if (changes.newFiles.length === 0 &&
1802
- changes.autoUpdateFiles.length === 0 &&
1803
- changes.changedFiles.length === 0 &&
1804
- !hasPendingMigrations &&
1805
- !hasSafeDeletes) {
1806
- // The "already up to date" exit still has to repair the receipt: this is
1807
- // exactly the clean tree where every file is `unchanged`, so it is the run
1808
- // where a wrong entry would otherwise be skipped again.
1809
- if (!options.dryRun && unchangedFileHashRepairs.size > 0) {
1810
- updateHashes(cwd, unchangedFileHashRepairs);
1811
- }
1812
- if (isSameVersion) {
1813
- console.log(chalk.green("✓ Already up to date!"));
1814
- }
1815
- else {
1816
- // Version changed but no file changes needed — still update the version stamp
1817
- updateVersionFile(cwd);
1818
- if (isUpgrade) {
1819
- console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion}`));
1820
- }
1821
- else if (isDowngrade) {
1822
- console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion} (downgrade)`));
1747
+ // Soft hint: non-breaking migrations or projects that chose not to set recommendMigrate
1748
+ if (!options.migrate) {
1749
+ const autoCount = classifiedMigrations.auto.length;
1750
+ const confirmCount = classifiedMigrations.confirm.length;
1751
+ if (autoCount > 0 || confirmCount > 0) {
1752
+ console.log(chalk.gray(`Tip: Use --migrate to apply migrations (prompts for modified files).`));
1753
+ if (confirmCount > 0) {
1754
+ console.log(chalk.gray(` Use --migrate -f to force all, or --migrate -s to skip modified.\n`));
1755
+ }
1756
+ else {
1757
+ console.log("");
1758
+ }
1759
+ }
1823
1760
  }
1824
1761
  }
1825
- if (zcodeConfigured)
1826
- printZcodeSetupHint();
1827
- return;
1828
- }
1829
- // Show what this operation will do
1830
- if (isUpgrade) {
1831
- console.log(chalk.green(`This will UPGRADE: ${projectVersion} → ${cliVersion}\n`));
1832
- }
1833
- else if (isDowngrade) {
1834
- console.log(chalk.red(`⚠️ This will DOWNGRADE: ${projectVersion} → ${cliVersion}\n`));
1835
- }
1836
- // Show breaking change warning before confirm
1837
- if (cliVsProject > 0 && projectVersion !== "unknown") {
1838
- const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
1839
- if (preConfirmMetadata.breaking) {
1840
- console.log(chalk.cyan("═".repeat(60)));
1841
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
1842
- chalk.red.bold(" Review the changes above carefully!"));
1843
- if (preConfirmMetadata.changelog.length > 0) {
1844
- console.log("");
1845
- console.log(chalk.white(preConfirmMetadata.changelog[0]));
1762
+ // Print safe-file-delete summary (always shown, runs without --migrate)
1763
+ if (safeFileDeletes.length > 0) {
1764
+ printSafeFileDeleteSummary(safeFileDeletes);
1765
+ }
1766
+ // Analyze changes (pass hashes for modification detection)
1767
+ const changes = analyzeChanges(cwd, hashes, templates);
1768
+ const unchangedFileHashRepairs = collectUnchangedFileHashRepairs(changes, hashes);
1769
+ // Print summary
1770
+ printChangeSummary(changes);
1771
+ // First-time hash tracking hint
1772
+ if (isFirstHashTracking && changes.changedFiles.length > 0) {
1773
+ console.log(chalk.cyan("ℹ️ First update with hash tracking enabled."));
1774
+ console.log(chalk.gray(" Changed files shown above may not be actual user modifications."));
1775
+ console.log(chalk.gray(" After this update, hash tracking will accurately detect changes.\n"));
1776
+ }
1777
+ // Ensure project-root .gitattributes carries the journal merge=union rule.
1778
+ // Additive-only (see ensureGitattributes) — runs regardless of whether
1779
+ // other template files changed, so it must sit before the "nothing to do"
1780
+ // early-return below. Never touches disk in --dry-run.
1781
+ if (!options.dryRun) {
1782
+ ensureGitattributes(cwd);
1783
+ }
1784
+ // Check if there's anything to do
1785
+ const isUpgrade = cliVsProject > 0;
1786
+ const isDowngrade = cliVsProject < 0;
1787
+ const isSameVersion = cliVsProject === 0;
1788
+ // Check if we have pending migrations that need to be applied
1789
+ const hasPendingMigrations = options.migrate &&
1790
+ classifiedMigrations &&
1791
+ (classifiedMigrations.auto.length > 0 ||
1792
+ classifiedMigrations.confirm.length > 0);
1793
+ if (changes.newFiles.length === 0 &&
1794
+ changes.autoUpdateFiles.length === 0 &&
1795
+ changes.changedFiles.length === 0 &&
1796
+ !hasPendingMigrations &&
1797
+ !hasSafeDeletes) {
1798
+ // The "already up to date" exit still has to repair the receipt: this is
1799
+ // exactly the clean tree where every file is `unchanged`, so it is the run
1800
+ // where a wrong entry would otherwise be skipped again.
1801
+ if (!options.dryRun && unchangedFileHashRepairs.size > 0) {
1802
+ updateHashes(cwd, unchangedFileHashRepairs);
1846
1803
  }
1847
- if (preConfirmMetadata.recommendMigrate && !options.migrate) {
1848
- console.log("");
1849
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
1850
- chalk.green.bold(" Run with --migrate to complete the migration"));
1804
+ if (isSameVersion) {
1805
+ console.log(chalk.green("✓ Already up to date!"));
1851
1806
  }
1852
- // Notice when update.skip is bypassed so user isn't surprised when
1853
- // skipPaths-protected files get cleaned up during this breaking upgrade.
1854
- if (breakingBypass && skipPaths.length > 0) {
1855
- const willBypass = safeFileDeletes.filter((c) => c.action === "delete" &&
1856
- skipPaths.some((skip) => c.item.from === skip ||
1857
- c.item.from.startsWith(skip.endsWith("/") ? skip : skip + "/")));
1858
- if (willBypass.length > 0) {
1807
+ else {
1808
+ // Version changed but no file changes needed — still update the version stamp
1809
+ updateVersionFile(cwd);
1810
+ if (isUpgrade) {
1811
+ console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion}`));
1812
+ }
1813
+ else if (isDowngrade) {
1814
+ console.log(chalk.green(`✓ No file changes needed for ${projectVersion} → ${cliVersion} (downgrade)`));
1815
+ }
1816
+ }
1817
+ if (zcodeConfigured)
1818
+ printZcodeSetupHint();
1819
+ return;
1820
+ }
1821
+ // Show what this operation will do
1822
+ if (isUpgrade) {
1823
+ console.log(chalk.green(`This will UPGRADE: ${projectVersion} → ${cliVersion}\n`));
1824
+ }
1825
+ else if (isDowngrade) {
1826
+ console.log(chalk.red(`⚠️ This will DOWNGRADE: ${projectVersion} → ${cliVersion}\n`));
1827
+ }
1828
+ // Show breaking change warning before confirm
1829
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
1830
+ const preConfirmMetadata = getMigrationMetadata(projectVersion, cliVersion);
1831
+ if (preConfirmMetadata.breaking) {
1832
+ console.log(chalk.cyan("═".repeat(60)));
1833
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
1834
+ chalk.red.bold(" Review the changes above carefully!"));
1835
+ if (preConfirmMetadata.changelog.length > 0) {
1859
1836
  console.log("");
1860
- console.log(chalk.bgYellow.black.bold(" ⚠ update.skip BYPASSED ") +
1861
- chalk.yellow.bold(` Breaking release — ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
1862
- console.log(chalk.gray(" Hash-verified: only files matching known Trellis templates are deleted. Your local customizations (hash mismatch) are still preserved."));
1837
+ console.log(chalk.white(preConfirmMetadata.changelog[0]));
1863
1838
  }
1839
+ if (preConfirmMetadata.recommendMigrate && !options.migrate) {
1840
+ console.log("");
1841
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
1842
+ chalk.green.bold(" Review the migration report and use --migrate for supported migrations"));
1843
+ }
1844
+ // Notice when update.skip is bypassed so user isn't surprised when
1845
+ // skipPaths-protected files get cleaned up during this breaking upgrade.
1846
+ if (breakingBypass && skipPaths.length > 0) {
1847
+ const willBypass = safeFileDeletes.filter((c) => c.action === "delete" &&
1848
+ skipPaths.some((skip) => c.item.from === skip ||
1849
+ c.item.from.startsWith(skip.endsWith("/") ? skip : skip + "/")));
1850
+ if (willBypass.length > 0) {
1851
+ console.log("");
1852
+ console.log(chalk.bgYellow.black.bold(" ⚠ update.skip BYPASSED ") +
1853
+ chalk.yellow.bold(` Breaking release — ${willBypass.length.toString()} file(s) under your update.skip paths will be cleaned up.`));
1854
+ console.log(chalk.gray(" Hash-verified: only files matching known Trellis templates are deleted. Your local customizations (hash mismatch) are still preserved."));
1855
+ }
1856
+ }
1857
+ console.log(chalk.cyan("═".repeat(60)));
1858
+ console.log("");
1864
1859
  }
1865
- console.log(chalk.cyan("═".repeat(60)));
1866
- console.log("");
1867
1860
  }
1868
- }
1869
- // Dry run mode
1870
- if (options.dryRun) {
1871
- console.log(chalk.gray("[Dry run] No changes made."));
1872
- return;
1873
- }
1874
- // Batch-resolution flags are explicit consent for non-interactive runs.
1875
- // Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
1876
- if (!options.force && !options.skipAll && !options.createNew) {
1877
- const { proceed } = await inquirer.prompt([
1878
- {
1879
- type: "confirm",
1880
- name: "proceed",
1881
- message: "Proceed?",
1882
- default: true,
1883
- },
1884
- ]);
1885
- if (!proceed) {
1886
- console.log(chalk.yellow("Update cancelled."));
1861
+ // Dry run mode
1862
+ if (options.dryRun) {
1863
+ console.log(chalk.gray("[Dry run] No changes made."));
1887
1864
  return;
1888
1865
  }
1889
- }
1890
- // Create complete backup of all managed platform/workflow directories
1891
- const backupDir = createFullBackup(cwd);
1892
- if (backupDir) {
1893
- console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
1894
- }
1895
- // Execute migrations if --migrate flag is set
1896
- if (options.migrate && classifiedMigrations) {
1897
- const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
1898
- force: options.force,
1899
- skipAll: options.skipAll,
1900
- }, templates);
1901
- printMigrationResult(migrationResult);
1902
- // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
1903
- // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
1904
- // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
1905
- // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
1906
- // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
1907
- const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
1908
- const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
1909
- if (journalRenamed > 0) {
1910
- console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
1911
- }
1912
- for (const oldPath of journalSkipped) {
1913
- console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
1914
- }
1915
- }
1916
- // Execute safe-file-delete (after backup, before template writes)
1917
- let safeDeleted = 0;
1918
- if (hasSafeDeletes) {
1919
- safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
1920
- if (safeDeleted > 0) {
1921
- console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
1866
+ // Batch-resolution flags are explicit consent for non-interactive runs.
1867
+ // Prompting here breaks CI and `node ... update --force --migrate` smoke tests.
1868
+ if (!options.force && !options.skipAll && !options.createNew) {
1869
+ const { proceed } = await inquirer.prompt([
1870
+ {
1871
+ type: "confirm",
1872
+ name: "proceed",
1873
+ message: "Proceed?",
1874
+ default: true,
1875
+ },
1876
+ ]);
1877
+ if (!proceed) {
1878
+ console.log(chalk.yellow("Update cancelled."));
1879
+ return;
1880
+ }
1922
1881
  }
1923
- }
1924
- // Track results
1925
- let added = 0;
1926
- let autoUpdated = 0;
1927
- let updated = 0;
1928
- let skipped = 0;
1929
- let createdNew = 0;
1930
- // Add new files
1931
- if (changes.newFiles.length > 0) {
1932
- console.log(chalk.blue("\nAdding new files..."));
1933
- for (const file of changes.newFiles) {
1934
- const dir = path.dirname(file.path);
1935
- fs.mkdirSync(dir, { recursive: true });
1936
- fs.writeFileSync(file.path, file.newContent);
1937
- // Make scripts executable
1938
- if (file.relativePath.endsWith(".sh") ||
1939
- file.relativePath.endsWith(".py")) {
1940
- fs.chmodSync(file.path, "755");
1882
+ const assetMigrations = options.migrate && classifiedMigrations
1883
+ ? [
1884
+ ...classifiedMigrations.auto,
1885
+ ...classifiedMigrations.confirm,
1886
+ ...classifiedMigrations.conflict,
1887
+ ]
1888
+ : [];
1889
+ const migrationScope = assetMigrations.flatMap((item) => {
1890
+ if (item.type !== "rename-dir")
1891
+ return [
1892
+ item.from,
1893
+ `${item.from}.backup`,
1894
+ ...(item.to ? [item.to, `${item.to}.backup`] : []),
1895
+ ];
1896
+ const source = collectAllFiles(path.join(cwd, item.from), cwd).map((file) => toPosix(path.relative(cwd, file)));
1897
+ const target = item.to;
1898
+ return [
1899
+ ...source,
1900
+ ...(target
1901
+ ? source.map((file) => `${target}${file.slice(item.from.length)}`)
1902
+ : []),
1903
+ ...(target
1904
+ ? collectAllFiles(path.join(cwd, target), cwd).map((file) => toPosix(path.relative(cwd, file)))
1905
+ : []),
1906
+ ];
1907
+ });
1908
+ const backupDir = createFullBackup(cwd, [
1909
+ ...templates.keys(),
1910
+ ...changes.changedFiles.map((file) => `${file.relativePath}.new`),
1911
+ ...migrationScope,
1912
+ ...safeFileDeletes
1913
+ .filter((item) => item.action === "delete")
1914
+ .map((item) => item.item.from),
1915
+ ]);
1916
+ if (backupDir) {
1917
+ console.log(chalk.gray(`\nBackup created: ${path.relative(cwd, backupDir)}/`));
1918
+ }
1919
+ // Execute migrations if --migrate flag is set
1920
+ if (options.migrate && classifiedMigrations) {
1921
+ const migrationResult = await executeMigrations(classifiedMigrations, cwd, {
1922
+ force: options.force,
1923
+ skipAll: options.skipAll,
1924
+ }, templates);
1925
+ printMigrationResult(migrationResult);
1926
+ // Hardcoded: Rename traces-*.md to journal-*.md in workspace directories
1927
+ // Why hardcoded: The migration system only supports fixed path renames, not pattern-based.
1928
+ // traces-*.md files are in .trellis/workspace/{developer}/ with variable developer names
1929
+ // and variable file numbers (traces-1.md, traces-2.md, etc.), so we can't enumerate them
1930
+ // in the migration manifest. This is a one-time migration for the 0.2.0 naming redesign.
1931
+ const workspaceDir = path.join(cwd, PATHS.WORKSPACE);
1932
+ const { renamed: journalRenamed, skipped: journalSkipped } = renameTracesToJournal(workspaceDir);
1933
+ if (journalRenamed > 0) {
1934
+ console.log(chalk.cyan(`Renamed ${journalRenamed} traces file(s) to journal`));
1935
+ }
1936
+ for (const oldPath of journalSkipped) {
1937
+ console.warn(chalk.yellow(`Kept ${path.relative(cwd, oldPath)}: its journal target already exists`));
1941
1938
  }
1942
- console.log(chalk.green(` + ${file.relativePath}`));
1943
- added++;
1944
1939
  }
1945
- }
1946
- // Auto-update files (template updated, user didn't modify)
1947
- if (changes.autoUpdateFiles.length > 0) {
1948
- console.log(chalk.blue("\nAuto-updating template files..."));
1949
- for (const file of changes.autoUpdateFiles) {
1950
- fs.writeFileSync(file.path, file.newContent);
1951
- // Make scripts executable
1952
- if (file.relativePath.endsWith(".sh") ||
1953
- file.relativePath.endsWith(".py")) {
1954
- fs.chmodSync(file.path, "755");
1940
+ // Execute safe-file-delete (after backup, before template writes)
1941
+ let safeDeleted = 0;
1942
+ if (hasSafeDeletes) {
1943
+ safeDeleted = executeSafeFileDeletes(safeFileDeletes, cwd);
1944
+ if (safeDeleted > 0) {
1945
+ console.log(chalk.cyan(`\nCleaned up ${safeDeleted} deprecated command file(s)`));
1955
1946
  }
1956
- console.log(chalk.cyan(` ↑ ${file.relativePath}`));
1957
- autoUpdated++;
1958
1947
  }
1959
- }
1960
- // Handle changed files
1961
- if (changes.changedFiles.length > 0) {
1962
- console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
1963
- const applyToAll = { action: null };
1964
- for (const file of changes.changedFiles) {
1965
- const action = await promptConflictResolution(file, options, applyToAll);
1966
- if (action === "overwrite") {
1948
+ // Track results
1949
+ let added = 0;
1950
+ let autoUpdated = 0;
1951
+ let updated = 0;
1952
+ let skipped = 0;
1953
+ let createdNew = 0;
1954
+ // Add new files
1955
+ if (changes.newFiles.length > 0) {
1956
+ console.log(chalk.blue("\nAdding new files..."));
1957
+ for (const file of changes.newFiles) {
1958
+ const dir = path.dirname(file.path);
1959
+ fs.mkdirSync(dir, { recursive: true });
1967
1960
  fs.writeFileSync(file.path, file.newContent);
1961
+ // Make scripts executable
1968
1962
  if (file.relativePath.endsWith(".sh") ||
1969
1963
  file.relativePath.endsWith(".py")) {
1970
1964
  fs.chmodSync(file.path, "755");
1971
1965
  }
1972
- console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
1973
- updated++;
1966
+ console.log(chalk.green(` + ${file.relativePath}`));
1967
+ added++;
1974
1968
  }
1975
- else if (action === "create-new") {
1976
- const newPath = file.path + ".new";
1977
- fs.writeFileSync(newPath, file.newContent);
1978
- console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
1979
- createdNew++;
1969
+ }
1970
+ // Auto-update files (template updated, user didn't modify)
1971
+ if (changes.autoUpdateFiles.length > 0) {
1972
+ console.log(chalk.blue("\nAuto-updating template files..."));
1973
+ for (const file of changes.autoUpdateFiles) {
1974
+ fs.writeFileSync(file.path, file.newContent);
1975
+ // Make scripts executable
1976
+ if (file.relativePath.endsWith(".sh") ||
1977
+ file.relativePath.endsWith(".py")) {
1978
+ fs.chmodSync(file.path, "755");
1979
+ }
1980
+ console.log(chalk.cyan(` ↑ ${file.relativePath}`));
1981
+ autoUpdated++;
1980
1982
  }
1981
- else {
1982
- console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
1983
- skipped++;
1983
+ }
1984
+ // Handle changed files
1985
+ if (changes.changedFiles.length > 0) {
1986
+ console.log(chalk.blue("\n--- Resolving conflicts ---\n"));
1987
+ const applyToAll = { action: null };
1988
+ for (const file of changes.changedFiles) {
1989
+ const action = await promptConflictResolution(file, options, applyToAll);
1990
+ if (action === "overwrite") {
1991
+ fs.writeFileSync(file.path, file.newContent);
1992
+ if (file.relativePath.endsWith(".sh") ||
1993
+ file.relativePath.endsWith(".py")) {
1994
+ fs.chmodSync(file.path, "755");
1995
+ }
1996
+ console.log(chalk.yellow(` ✓ Overwritten: ${file.relativePath}`));
1997
+ updated++;
1998
+ }
1999
+ else if (action === "create-new") {
2000
+ const newPath = file.path + ".new";
2001
+ fs.writeFileSync(newPath, file.newContent);
2002
+ console.log(chalk.blue(` ✓ Created: ${file.relativePath}.new`));
2003
+ createdNew++;
2004
+ }
2005
+ else {
2006
+ console.log(chalk.gray(` ○ Skipped: ${file.relativePath}`));
2007
+ skipped++;
2008
+ }
1984
2009
  }
1985
2010
  }
1986
- }
1987
- // Append additive config.yaml sections introduced between versions.
1988
- // Sentinel-gated, so users keep their customizations and re-running update
1989
- // on already-migrated files is a no-op. Skipped on unknown / downgrade.
1990
- let configSectionsAppended = 0;
1991
- if (cliVsProject > 0 && projectVersion !== "unknown") {
1992
- const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
1993
- if (sectionEntries.length > 0) {
1994
- const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
1995
- configSectionsAppended = appended;
1996
- }
1997
- }
1998
- // Update version file
1999
- updateVersionFile(cwd);
2000
- // Update template hashes for new, auto-updated, and overwritten files
2001
- const filesToHash = new Map(unchangedFileHashRepairs);
2002
- for (const file of changes.newFiles) {
2003
- filesToHash.set(file.relativePath, file.newContent);
2004
- }
2005
- // Auto-updated files always get new hash
2006
- for (const file of changes.autoUpdateFiles) {
2007
- filesToHash.set(file.relativePath, file.newContent);
2008
- }
2009
- // Only hash overwritten files (not skipped or .new copies)
2010
- for (const file of changes.changedFiles) {
2011
- const fullPath = path.join(cwd, file.relativePath);
2012
- if (fs.existsSync(fullPath)) {
2013
- const content = fs.readFileSync(fullPath, "utf-8");
2014
- if (content === file.newContent) {
2015
- filesToHash.set(file.relativePath, file.newContent);
2011
+ // Append additive config.yaml sections introduced between versions.
2012
+ // Sentinel-gated, so users keep their customizations and re-running update
2013
+ // on already-migrated files is a no-op. Skipped on unknown / downgrade.
2014
+ let configSectionsAppended = 0;
2015
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2016
+ const sectionEntries = getConfigSectionsAddedBetween(projectVersion, cliVersion);
2017
+ if (sectionEntries.length > 0) {
2018
+ const { appended } = applyConfigSectionsAdded(sectionEntries, cwd, templates);
2019
+ configSectionsAppended = appended;
2016
2020
  }
2017
2021
  }
2018
- }
2019
- if (filesToHash.size > 0) {
2020
- updateHashes(cwd, filesToHash);
2021
- }
2022
- // Print summary
2023
- console.log(chalk.cyan("\n--- Summary ---\n"));
2024
- if (added > 0) {
2025
- console.log(` Added: ${added} file(s)`);
2026
- }
2027
- if (autoUpdated > 0) {
2028
- console.log(` Auto-updated: ${autoUpdated} file(s)`);
2029
- }
2030
- if (updated > 0) {
2031
- console.log(` Updated: ${updated} file(s)`);
2032
- }
2033
- if (skipped > 0) {
2034
- console.log(` Skipped: ${skipped} file(s)`);
2035
- }
2036
- if (createdNew > 0) {
2037
- console.log(` Created .new copies: ${createdNew} file(s)`);
2038
- }
2039
- if (safeDeleted > 0) {
2040
- console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2041
- }
2042
- if (configSectionsAppended > 0) {
2043
- console.log(` Config sections added: ${configSectionsAppended}`);
2044
- }
2045
- if (backupDir) {
2046
- console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2047
- }
2048
- const actionWord = isDowngrade ? "Downgrade" : "Update";
2049
- console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2050
- if (createdNew > 0) {
2051
- console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2052
- }
2053
- // Create migration task if there are breaking changes with migration guides
2054
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2055
- const metadata = getMigrationMetadata(projectVersion, cliVersion);
2056
- if (metadata.breaking && metadata.migrationGuides.length > 0) {
2057
- // Create task directory
2058
- const today = new Date();
2059
- const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2060
- const taskSlug = `migrate-to-${cliVersion}`;
2061
- const taskDirName = `${monthDay}-${taskSlug}`;
2062
- const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2063
- const taskDir = path.join(tasksDir, taskDirName);
2064
- // Check if task already exists
2065
- if (!fs.existsSync(taskDir)) {
2066
- fs.mkdirSync(taskDir, { recursive: true });
2067
- // Get current developer for assignee.
2068
- // `.developer` is a key=value file (written by init_developer.py):
2069
- // name=<developer-name>
2070
- // initialized_at=<iso8601>
2071
- // Reading it raw and .trim()-ing embeds the entire file contents
2072
- // (including the `name=` prefix and the `initialized_at` line) into
2073
- // the assignee field, producing bogus assignees like
2074
- // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2075
- // later break session-start task rendering.
2076
- const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2077
- let currentDeveloper = "unknown";
2078
- if (fs.existsSync(developerFile)) {
2079
- const raw = fs.readFileSync(developerFile, "utf-8");
2080
- const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2081
- if (nameMatch) {
2082
- currentDeveloper = nameMatch[1];
2022
+ // Update version file
2023
+ updateVersionFile(cwd);
2024
+ // Update template hashes for new, auto-updated, and overwritten files
2025
+ const filesToHash = new Map(unchangedFileHashRepairs);
2026
+ for (const file of changes.newFiles) {
2027
+ filesToHash.set(file.relativePath, file.newContent);
2028
+ }
2029
+ // Auto-updated files always get new hash
2030
+ for (const file of changes.autoUpdateFiles) {
2031
+ filesToHash.set(file.relativePath, file.newContent);
2032
+ }
2033
+ // Only hash overwritten files (not skipped or .new copies)
2034
+ for (const file of changes.changedFiles) {
2035
+ const fullPath = path.join(cwd, file.relativePath);
2036
+ if (fs.existsSync(fullPath)) {
2037
+ const content = fs.readFileSync(fullPath, "utf-8");
2038
+ if (content === file.newContent) {
2039
+ filesToHash.set(file.relativePath, file.newContent);
2040
+ }
2041
+ }
2042
+ }
2043
+ if (filesToHash.size > 0) {
2044
+ updateHashes(cwd, filesToHash);
2045
+ }
2046
+ // Print summary
2047
+ console.log(chalk.cyan("\n--- Summary ---\n"));
2048
+ if (added > 0) {
2049
+ console.log(` Added: ${added} file(s)`);
2050
+ }
2051
+ if (autoUpdated > 0) {
2052
+ console.log(` Auto-updated: ${autoUpdated} file(s)`);
2053
+ }
2054
+ if (updated > 0) {
2055
+ console.log(` Updated: ${updated} file(s)`);
2056
+ }
2057
+ if (skipped > 0) {
2058
+ console.log(` Skipped: ${skipped} file(s)`);
2059
+ }
2060
+ if (createdNew > 0) {
2061
+ console.log(` Created .new copies: ${createdNew} file(s)`);
2062
+ }
2063
+ if (safeDeleted > 0) {
2064
+ console.log(` Cleaned up: ${safeDeleted} deprecated file(s)`);
2065
+ }
2066
+ if (configSectionsAppended > 0) {
2067
+ console.log(` Config sections added: ${configSectionsAppended}`);
2068
+ }
2069
+ if (backupDir) {
2070
+ console.log(` Backup: ${path.relative(cwd, backupDir)}/`);
2071
+ }
2072
+ const actionWord = isDowngrade ? "Downgrade" : "Update";
2073
+ console.log(chalk.green(`\n✅ ${actionWord} complete! (${projectVersion} → ${cliVersion})`));
2074
+ if (createdNew > 0) {
2075
+ console.log(chalk.gray("\nTip: Review .new files and merge changes manually if needed."));
2076
+ }
2077
+ // Create migration task if there are breaking changes with migration guides
2078
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2079
+ const metadata = getMigrationMetadata(projectVersion, cliVersion);
2080
+ if (metadata.breaking && metadata.migrationGuides.length > 0) {
2081
+ // Create task directory
2082
+ const today = new Date();
2083
+ const monthDay = `${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
2084
+ const taskSlug = `migrate-to-${cliVersion}`;
2085
+ const taskDirName = `${monthDay}-${taskSlug}`;
2086
+ const tasksDir = path.join(cwd, DIR_NAMES.WORKFLOW, DIR_NAMES.TASKS);
2087
+ const taskDir = path.join(tasksDir, "planning", taskDirName);
2088
+ // A prior migration task may have moved to another lifecycle directory.
2089
+ const taskLocations = [
2090
+ tasksDir,
2091
+ ...["planning", "in_progress", "cancelled"].map((status) => path.join(tasksDir, status)),
2092
+ ];
2093
+ for (const status of ["completed", "archive"]) {
2094
+ const statusDir = path.join(tasksDir, status);
2095
+ if (!fs.existsSync(statusDir))
2096
+ continue;
2097
+ for (const month of fs.readdirSync(statusDir, {
2098
+ withFileTypes: true,
2099
+ })) {
2100
+ if (month.isDirectory() &&
2101
+ /^\d{4}-(0[1-9]|1[0-2])$/.test(month.name)) {
2102
+ taskLocations.push(path.join(statusDir, month.name));
2103
+ }
2083
2104
  }
2084
2105
  }
2085
- // Build task.json — canonical 24-field shape via shared factory.
2086
- const taskTitle = `Migrate to v${cliVersion}`;
2087
- const todayStr = today.toISOString().split("T")[0];
2088
- const taskJson = emptyTaskJson({
2089
- id: taskSlug,
2090
- name: taskSlug,
2091
- title: taskTitle,
2092
- description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2093
- status: "planning",
2094
- scope: "migration",
2095
- priority: "P1",
2096
- creator: "trellis-update",
2097
- assignee: currentDeveloper,
2098
- createdAt: todayStr,
2099
- });
2100
- // Write task.json
2101
- const taskJsonPath = path.join(taskDir, "task.json");
2102
- fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2103
- // Build PRD content
2104
- let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2105
- prdContent += `**Created**: ${todayStr}\n`;
2106
- prdContent += `**From Version**: ${projectVersion}\n`;
2107
- prdContent += `**To Version**: ${cliVersion}\n`;
2108
- prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2109
- prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2110
- for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2111
- prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2112
- prdContent += guide;
2113
- prdContent += "\n\n";
2114
- if (aiInstructions) {
2115
- prdContent += `### AI Assistant Instructions\n\n`;
2116
- prdContent += `When helping with this migration:\n\n`;
2117
- prdContent += aiInstructions;
2106
+ if (!taskLocations.some((directory) => fs.existsSync(path.join(directory, taskDirName)))) {
2107
+ fs.mkdirSync(taskDir, { recursive: true });
2108
+ // Get current developer for assignee.
2109
+ // `.developer` is a key=value file (written by init_developer.py):
2110
+ // name=<developer-name>
2111
+ // initialized_at=<iso8601>
2112
+ // Reading it raw and .trim()-ing embeds the entire file contents
2113
+ // (including the `name=` prefix and the `initialized_at` line) into
2114
+ // the assignee field, producing bogus assignees like
2115
+ // "name=suyuan\ninitialized_at=2026-04-07T23:41:21.978312" that
2116
+ // later break session-start task rendering.
2117
+ const developerFile = path.join(cwd, DIR_NAMES.WORKFLOW, ".developer");
2118
+ let currentDeveloper = "unknown";
2119
+ if (fs.existsSync(developerFile)) {
2120
+ const raw = fs.readFileSync(developerFile, "utf-8");
2121
+ const nameMatch = raw.match(/^\s*name\s*=\s*(.+?)\s*$/m);
2122
+ if (nameMatch) {
2123
+ currentDeveloper = nameMatch[1];
2124
+ }
2125
+ }
2126
+ // Build task.json — canonical 24-field shape via shared factory.
2127
+ const taskTitle = `Migrate to v${cliVersion}`;
2128
+ const todayStr = today.toISOString().split("T")[0];
2129
+ const taskJson = emptyTaskJson({
2130
+ id: taskSlug,
2131
+ name: taskSlug,
2132
+ title: taskTitle,
2133
+ description: `Breaking change migration from v${projectVersion} to v${cliVersion}`,
2134
+ status: "planning",
2135
+ scope: "migration",
2136
+ priority: "P1",
2137
+ creator: "trellis-update",
2138
+ assignee: currentDeveloper,
2139
+ createdAt: todayStr,
2140
+ });
2141
+ // Write task.json
2142
+ const taskJsonPath = path.join(taskDir, "task.json");
2143
+ fs.writeFileSync(taskJsonPath, JSON.stringify(taskJson, null, 2));
2144
+ // Build PRD content
2145
+ let prdContent = `# Migration Task: Upgrade to v${cliVersion}\n\n`;
2146
+ prdContent += `**Created**: ${todayStr}\n`;
2147
+ prdContent += `**From Version**: ${projectVersion}\n`;
2148
+ prdContent += `**To Version**: ${cliVersion}\n`;
2149
+ prdContent += `**Assignee**: ${currentDeveloper}\n\n`;
2150
+ prdContent += `## Status\n\n- [ ] Review migration guide\n- [ ] Update custom files\n- [ ] Run \`trellis update --migrate\`\n- [ ] Test workflows\n\n`;
2151
+ for (const { version, guide, aiInstructions, } of metadata.migrationGuides) {
2152
+ prdContent += `---\n\n## v${version} Migration Guide\n\n`;
2153
+ prdContent += guide;
2118
2154
  prdContent += "\n\n";
2155
+ if (aiInstructions) {
2156
+ prdContent += `### AI Assistant Instructions\n\n`;
2157
+ prdContent += `When helping with this migration:\n\n`;
2158
+ prdContent += aiInstructions;
2159
+ prdContent += "\n\n";
2160
+ }
2119
2161
  }
2162
+ // Write PRD
2163
+ const prdPath = path.join(taskDir, "prd.md");
2164
+ fs.writeFileSync(prdPath, prdContent);
2165
+ console.log("");
2166
+ console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2167
+ console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2168
+ console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/planning/${taskDirName}/`));
2169
+ console.log("");
2170
+ console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2120
2171
  }
2121
- // Write PRD
2122
- const prdPath = path.join(taskDir, "prd.md");
2123
- fs.writeFileSync(prdPath, prdContent);
2124
- console.log("");
2125
- console.log(chalk.bgCyan.black.bold(" 📋 MIGRATION TASK CREATED "));
2126
- console.log(chalk.cyan(`A task has been created to help you complete the migration:`));
2127
- console.log(chalk.white(` ${DIR_NAMES.WORKFLOW}/${DIR_NAMES.TASKS}/${taskDirName}/`));
2128
- console.log("");
2129
- console.log(chalk.gray("Use AI to help: Ask Claude/Cursor to read the task and fix your custom files."));
2130
2172
  }
2131
2173
  }
2132
- }
2133
- if (zcodeConfigured)
2134
- printZcodeSetupHint();
2135
- // Display breaking change warnings at the very end (so they don't scroll off screen)
2136
- if (cliVsProject > 0 && projectVersion !== "unknown") {
2137
- const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2138
- if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2139
- console.log("");
2140
- console.log(chalk.cyan("═".repeat(60)));
2141
- if (finalMetadata.breaking) {
2142
- console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2143
- chalk.red.bold(" This update contains breaking changes!"));
2174
+ bindAssetUpdate(cwd, path.basename(backupDir));
2175
+ if (zcodeConfigured)
2176
+ printZcodeSetupHint();
2177
+ // Display breaking change warnings at the very end (so they don't scroll off screen)
2178
+ if (cliVsProject > 0 && projectVersion !== "unknown") {
2179
+ const finalMetadata = getMigrationMetadata(projectVersion, cliVersion);
2180
+ if (finalMetadata.breaking || finalMetadata.changelog.length > 0) {
2144
2181
  console.log("");
2145
- }
2146
- if (finalMetadata.changelog.length > 0) {
2147
- console.log(chalk.cyan.bold("📋 What's Changed:"));
2148
- for (const entry of finalMetadata.changelog) {
2149
- console.log(chalk.white(` ${entry}`));
2182
+ console.log(chalk.cyan("═".repeat(60)));
2183
+ if (finalMetadata.breaking) {
2184
+ console.log(chalk.bgRed.white.bold(" ⚠️ BREAKING CHANGES ") +
2185
+ chalk.red.bold(" This update contains breaking changes!"));
2186
+ console.log("");
2150
2187
  }
2151
- console.log("");
2152
- }
2153
- if (finalMetadata.recommendMigrate && !options.migrate) {
2154
- console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2155
- chalk.green.bold(" Run with --migrate to complete the migration"));
2156
- console.log(chalk.gray(" This will remove legacy files and apply all changes."));
2157
- console.log("");
2188
+ if (finalMetadata.changelog.length > 0) {
2189
+ console.log(chalk.cyan.bold("📋 What's Changed:"));
2190
+ for (const entry of finalMetadata.changelog) {
2191
+ console.log(chalk.white(` ${entry}`));
2192
+ }
2193
+ console.log("");
2194
+ }
2195
+ if (finalMetadata.recommendMigrate && !options.migrate) {
2196
+ console.log(chalk.bgGreen.black.bold(" 💡 RECOMMENDED ") +
2197
+ chalk.green.bold(" Run with --migrate to complete the migration"));
2198
+ console.log(chalk.gray(" Retired artifacts may require an explicit cleanup; --migrate does not remove every legacy file."));
2199
+ console.log("");
2200
+ }
2201
+ console.log(chalk.cyan("═".repeat(60)));
2158
2202
  }
2159
- console.log(chalk.cyan("═".repeat(60)));
2160
2203
  }
2161
- }
2204
+ }, options.dryRun);
2162
2205
  }
2163
2206
  //# sourceMappingURL=update.js.map