@pennixrv/trellis 0.6.17

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 (831) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +168 -0
  3. package/bin/trellis.js +3 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +299 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/commands/ablate.d.ts +14 -0
  9. package/dist/commands/ablate.d.ts.map +1 -0
  10. package/dist/commands/ablate.js +307 -0
  11. package/dist/commands/ablate.js.map +1 -0
  12. package/dist/commands/channel/adapters/claude.d.ts +53 -0
  13. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  14. package/dist/commands/channel/adapters/claude.js +225 -0
  15. package/dist/commands/channel/adapters/claude.js.map +1 -0
  16. package/dist/commands/channel/adapters/codex.d.ts +93 -0
  17. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  18. package/dist/commands/channel/adapters/codex.js +573 -0
  19. package/dist/commands/channel/adapters/codex.js.map +1 -0
  20. package/dist/commands/channel/adapters/index.d.ts +94 -0
  21. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  22. package/dist/commands/channel/adapters/index.js +116 -0
  23. package/dist/commands/channel/adapters/index.js.map +1 -0
  24. package/dist/commands/channel/adapters/types.d.ts +33 -0
  25. package/dist/commands/channel/adapters/types.d.ts.map +1 -0
  26. package/dist/commands/channel/adapters/types.js +2 -0
  27. package/dist/commands/channel/adapters/types.js.map +1 -0
  28. package/dist/commands/channel/agent-loader.d.ts +32 -0
  29. package/dist/commands/channel/agent-loader.d.ts.map +1 -0
  30. package/dist/commands/channel/agent-loader.js +158 -0
  31. package/dist/commands/channel/agent-loader.js.map +1 -0
  32. package/dist/commands/channel/context-loader.d.ts +26 -0
  33. package/dist/commands/channel/context-loader.d.ts.map +1 -0
  34. package/dist/commands/channel/context-loader.js +311 -0
  35. package/dist/commands/channel/context-loader.js.map +1 -0
  36. package/dist/commands/channel/context-trust.d.ts +31 -0
  37. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  38. package/dist/commands/channel/context-trust.js +139 -0
  39. package/dist/commands/channel/context-trust.js.map +1 -0
  40. package/dist/commands/channel/context.d.ts +16 -0
  41. package/dist/commands/channel/context.d.ts.map +1 -0
  42. package/dist/commands/channel/context.js +83 -0
  43. package/dist/commands/channel/context.js.map +1 -0
  44. package/dist/commands/channel/create.d.ts +27 -0
  45. package/dist/commands/channel/create.d.ts.map +1 -0
  46. package/dist/commands/channel/create.js +39 -0
  47. package/dist/commands/channel/create.js.map +1 -0
  48. package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
  49. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
  50. package/dist/commands/channel/dev-parse-trace.js +70 -0
  51. package/dist/commands/channel/dev-parse-trace.js.map +1 -0
  52. package/dist/commands/channel/guard.d.ts +150 -0
  53. package/dist/commands/channel/guard.d.ts.map +1 -0
  54. package/dist/commands/channel/guard.js +479 -0
  55. package/dist/commands/channel/guard.js.map +1 -0
  56. package/dist/commands/channel/index.d.ts +3 -0
  57. package/dist/commands/channel/index.d.ts.map +1 -0
  58. package/dist/commands/channel/index.js +535 -0
  59. package/dist/commands/channel/index.js.map +1 -0
  60. package/dist/commands/channel/interrupt.d.ts +10 -0
  61. package/dist/commands/channel/interrupt.d.ts.map +1 -0
  62. package/dist/commands/channel/interrupt.js +22 -0
  63. package/dist/commands/channel/interrupt.js.map +1 -0
  64. package/dist/commands/channel/kill.d.ts +7 -0
  65. package/dist/commands/channel/kill.d.ts.map +1 -0
  66. package/dist/commands/channel/kill.js +130 -0
  67. package/dist/commands/channel/kill.js.map +1 -0
  68. package/dist/commands/channel/list.d.ts +17 -0
  69. package/dist/commands/channel/list.d.ts.map +1 -0
  70. package/dist/commands/channel/list.js +233 -0
  71. package/dist/commands/channel/list.js.map +1 -0
  72. package/dist/commands/channel/messages.d.ts +15 -0
  73. package/dist/commands/channel/messages.d.ts.map +1 -0
  74. package/dist/commands/channel/messages.js +245 -0
  75. package/dist/commands/channel/messages.js.map +1 -0
  76. package/dist/commands/channel/rm.d.ts +27 -0
  77. package/dist/commands/channel/rm.d.ts.map +1 -0
  78. package/dist/commands/channel/rm.js +216 -0
  79. package/dist/commands/channel/rm.js.map +1 -0
  80. package/dist/commands/channel/run.d.ts +30 -0
  81. package/dist/commands/channel/run.d.ts.map +1 -0
  82. package/dist/commands/channel/run.js +134 -0
  83. package/dist/commands/channel/run.js.map +1 -0
  84. package/dist/commands/channel/send.d.ts +11 -0
  85. package/dist/commands/channel/send.d.ts.map +1 -0
  86. package/dist/commands/channel/send.js +24 -0
  87. package/dist/commands/channel/send.js.map +1 -0
  88. package/dist/commands/channel/spawn.d.ts +43 -0
  89. package/dist/commands/channel/spawn.d.ts.map +1 -0
  90. package/dist/commands/channel/spawn.js +247 -0
  91. package/dist/commands/channel/spawn.js.map +1 -0
  92. package/dist/commands/channel/store/events.d.ts +22 -0
  93. package/dist/commands/channel/store/events.d.ts.map +1 -0
  94. package/dist/commands/channel/store/events.js +43 -0
  95. package/dist/commands/channel/store/events.js.map +1 -0
  96. package/dist/commands/channel/store/filter.d.ts +3 -0
  97. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  98. package/dist/commands/channel/store/filter.js +2 -0
  99. package/dist/commands/channel/store/filter.js.map +1 -0
  100. package/dist/commands/channel/store/lock.d.ts +23 -0
  101. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  102. package/dist/commands/channel/store/lock.js +99 -0
  103. package/dist/commands/channel/store/lock.js.map +1 -0
  104. package/dist/commands/channel/store/paths.d.ts +77 -0
  105. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  106. package/dist/commands/channel/store/paths.js +272 -0
  107. package/dist/commands/channel/store/paths.js.map +1 -0
  108. package/dist/commands/channel/store/schema.d.ts +27 -0
  109. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  110. package/dist/commands/channel/store/schema.js +34 -0
  111. package/dist/commands/channel/store/schema.js.map +1 -0
  112. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  113. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  114. package/dist/commands/channel/store/thread-state.js +16 -0
  115. package/dist/commands/channel/store/thread-state.js.map +1 -0
  116. package/dist/commands/channel/store/watch.d.ts +19 -0
  117. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  118. package/dist/commands/channel/store/watch.js +153 -0
  119. package/dist/commands/channel/store/watch.js.map +1 -0
  120. package/dist/commands/channel/supervisor/idle.d.ts +45 -0
  121. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
  122. package/dist/commands/channel/supervisor/idle.js +70 -0
  123. package/dist/commands/channel/supervisor/idle.js.map +1 -0
  124. package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
  125. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  126. package/dist/commands/channel/supervisor/inbox.js +160 -0
  127. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  128. package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
  129. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  130. package/dist/commands/channel/supervisor/shutdown.js +146 -0
  131. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  132. package/dist/commands/channel/supervisor/stdout.d.ts +71 -0
  133. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  134. package/dist/commands/channel/supervisor/stdout.js +229 -0
  135. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  136. package/dist/commands/channel/supervisor/turns.d.ts +31 -0
  137. package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
  138. package/dist/commands/channel/supervisor/turns.js +45 -0
  139. package/dist/commands/channel/supervisor/turns.js.map +1 -0
  140. package/dist/commands/channel/supervisor/warning.d.ts +48 -0
  141. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
  142. package/dist/commands/channel/supervisor/warning.js +77 -0
  143. package/dist/commands/channel/supervisor/warning.js.map +1 -0
  144. package/dist/commands/channel/supervisor.d.ts +91 -0
  145. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  146. package/dist/commands/channel/supervisor.js +463 -0
  147. package/dist/commands/channel/supervisor.js.map +1 -0
  148. package/dist/commands/channel/text-body.d.ts +13 -0
  149. package/dist/commands/channel/text-body.d.ts.map +1 -0
  150. package/dist/commands/channel/text-body.js +47 -0
  151. package/dist/commands/channel/text-body.js.map +1 -0
  152. package/dist/commands/channel/threads.d.ts +39 -0
  153. package/dist/commands/channel/threads.d.ts.map +1 -0
  154. package/dist/commands/channel/threads.js +106 -0
  155. package/dist/commands/channel/threads.js.map +1 -0
  156. package/dist/commands/channel/title.d.ts +12 -0
  157. package/dist/commands/channel/title.d.ts.map +1 -0
  158. package/dist/commands/channel/title.js +24 -0
  159. package/dist/commands/channel/title.js.map +1 -0
  160. package/dist/commands/channel/wait.d.ts +17 -0
  161. package/dist/commands/channel/wait.d.ts.map +1 -0
  162. package/dist/commands/channel/wait.js +81 -0
  163. package/dist/commands/channel/wait.js.map +1 -0
  164. package/dist/commands/init.d.ts +71 -0
  165. package/dist/commands/init.d.ts.map +1 -0
  166. package/dist/commands/init.js +1603 -0
  167. package/dist/commands/init.js.map +1 -0
  168. package/dist/commands/mem.d.ts +30 -0
  169. package/dist/commands/mem.d.ts.map +1 -0
  170. package/dist/commands/mem.js +423 -0
  171. package/dist/commands/mem.js.map +1 -0
  172. package/dist/commands/uninstall.d.ts +36 -0
  173. package/dist/commands/uninstall.d.ts.map +1 -0
  174. package/dist/commands/uninstall.js +205 -0
  175. package/dist/commands/uninstall.js.map +1 -0
  176. package/dist/commands/update.d.ts +104 -0
  177. package/dist/commands/update.d.ts.map +1 -0
  178. package/dist/commands/update.js +2163 -0
  179. package/dist/commands/update.js.map +1 -0
  180. package/dist/commands/upgrade.d.ts +28 -0
  181. package/dist/commands/upgrade.d.ts.map +1 -0
  182. package/dist/commands/upgrade.js +84 -0
  183. package/dist/commands/upgrade.js.map +1 -0
  184. package/dist/commands/workflow.d.ts +35 -0
  185. package/dist/commands/workflow.d.ts.map +1 -0
  186. package/dist/commands/workflow.js +232 -0
  187. package/dist/commands/workflow.js.map +1 -0
  188. package/dist/configurators/antigravity.d.ts +7 -0
  189. package/dist/configurators/antigravity.d.ts.map +1 -0
  190. package/dist/configurators/antigravity.js +11 -0
  191. package/dist/configurators/antigravity.js.map +1 -0
  192. package/dist/configurators/claude.d.ts +23 -0
  193. package/dist/configurators/claude.d.ts.map +1 -0
  194. package/dist/configurators/claude.js +123 -0
  195. package/dist/configurators/claude.js.map +1 -0
  196. package/dist/configurators/codebuddy.d.ts +10 -0
  197. package/dist/configurators/codebuddy.d.ts.map +1 -0
  198. package/dist/configurators/codebuddy.js +24 -0
  199. package/dist/configurators/codebuddy.js.map +1 -0
  200. package/dist/configurators/codex.d.ts +57 -0
  201. package/dist/configurators/codex.d.ts.map +1 -0
  202. package/dist/configurators/codex.js +170 -0
  203. package/dist/configurators/codex.js.map +1 -0
  204. package/dist/configurators/copilot.d.ts +14 -0
  205. package/dist/configurators/copilot.d.ts.map +1 -0
  206. package/dist/configurators/copilot.js +46 -0
  207. package/dist/configurators/copilot.js.map +1 -0
  208. package/dist/configurators/cursor.d.ts +10 -0
  209. package/dist/configurators/cursor.d.ts.map +1 -0
  210. package/dist/configurators/cursor.js +23 -0
  211. package/dist/configurators/cursor.js.map +1 -0
  212. package/dist/configurators/devin.d.ts +8 -0
  213. package/dist/configurators/devin.d.ts.map +1 -0
  214. package/dist/configurators/devin.js +12 -0
  215. package/dist/configurators/devin.js.map +1 -0
  216. package/dist/configurators/droid.d.ts +10 -0
  217. package/dist/configurators/droid.d.ts.map +1 -0
  218. package/dist/configurators/droid.js +24 -0
  219. package/dist/configurators/droid.js.map +1 -0
  220. package/dist/configurators/dsh.d.ts +29 -0
  221. package/dist/configurators/dsh.d.ts.map +1 -0
  222. package/dist/configurators/dsh.js +64 -0
  223. package/dist/configurators/dsh.js.map +1 -0
  224. package/dist/configurators/gemini.d.ts +18 -0
  225. package/dist/configurators/gemini.d.ts.map +1 -0
  226. package/dist/configurators/gemini.js +39 -0
  227. package/dist/configurators/gemini.js.map +1 -0
  228. package/dist/configurators/grok.d.ts +17 -0
  229. package/dist/configurators/grok.d.ts.map +1 -0
  230. package/dist/configurators/grok.js +38 -0
  231. package/dist/configurators/grok.js.map +1 -0
  232. package/dist/configurators/index.d.ts +68 -0
  233. package/dist/configurators/index.d.ts.map +1 -0
  234. package/dist/configurators/index.js +191 -0
  235. package/dist/configurators/index.js.map +1 -0
  236. package/dist/configurators/kilo.d.ts +7 -0
  237. package/dist/configurators/kilo.d.ts.map +1 -0
  238. package/dist/configurators/kilo.js +11 -0
  239. package/dist/configurators/kilo.js.map +1 -0
  240. package/dist/configurators/kimi.d.ts +26 -0
  241. package/dist/configurators/kimi.d.ts.map +1 -0
  242. package/dist/configurators/kimi.js +74 -0
  243. package/dist/configurators/kimi.js.map +1 -0
  244. package/dist/configurators/kiro.d.ts +11 -0
  245. package/dist/configurators/kiro.d.ts.map +1 -0
  246. package/dist/configurators/kiro.js +30 -0
  247. package/dist/configurators/kiro.js.map +1 -0
  248. package/dist/configurators/omp.d.ts +6 -0
  249. package/dist/configurators/omp.d.ts.map +1 -0
  250. package/dist/configurators/omp.js +28 -0
  251. package/dist/configurators/omp.js.map +1 -0
  252. package/dist/configurators/opencode.d.ts +5 -0
  253. package/dist/configurators/opencode.d.ts.map +1 -0
  254. package/dist/configurators/opencode.js +80 -0
  255. package/dist/configurators/opencode.js.map +1 -0
  256. package/dist/configurators/pi.d.ts +5 -0
  257. package/dist/configurators/pi.d.ts.map +1 -0
  258. package/dist/configurators/pi.js +39 -0
  259. package/dist/configurators/pi.js.map +1 -0
  260. package/dist/configurators/qoder.d.ts +12 -0
  261. package/dist/configurators/qoder.d.ts.map +1 -0
  262. package/dist/configurators/qoder.js +30 -0
  263. package/dist/configurators/qoder.js.map +1 -0
  264. package/dist/configurators/reasonix.d.ts +17 -0
  265. package/dist/configurators/reasonix.d.ts.map +1 -0
  266. package/dist/configurators/reasonix.js +37 -0
  267. package/dist/configurators/reasonix.js.map +1 -0
  268. package/dist/configurators/shared.d.ts +187 -0
  269. package/dist/configurators/shared.d.ts.map +1 -0
  270. package/dist/configurators/shared.js +589 -0
  271. package/dist/configurators/shared.js.map +1 -0
  272. package/dist/configurators/snow.d.ts +25 -0
  273. package/dist/configurators/snow.d.ts.map +1 -0
  274. package/dist/configurators/snow.js +69 -0
  275. package/dist/configurators/snow.js.map +1 -0
  276. package/dist/configurators/trae.d.ts +16 -0
  277. package/dist/configurators/trae.d.ts.map +1 -0
  278. package/dist/configurators/trae.js +34 -0
  279. package/dist/configurators/trae.js.map +1 -0
  280. package/dist/configurators/workflow.d.ts +51 -0
  281. package/dist/configurators/workflow.d.ts.map +1 -0
  282. package/dist/configurators/workflow.js +210 -0
  283. package/dist/configurators/workflow.js.map +1 -0
  284. package/dist/configurators/zcode.d.ts +25 -0
  285. package/dist/configurators/zcode.d.ts.map +1 -0
  286. package/dist/configurators/zcode.js +67 -0
  287. package/dist/configurators/zcode.js.map +1 -0
  288. package/dist/constants/paths.d.ts +74 -0
  289. package/dist/constants/paths.d.ts.map +1 -0
  290. package/dist/constants/paths.js +83 -0
  291. package/dist/constants/paths.js.map +1 -0
  292. package/dist/constants/version.d.ts +9 -0
  293. package/dist/constants/version.d.ts.map +1 -0
  294. package/dist/constants/version.js +15 -0
  295. package/dist/constants/version.js.map +1 -0
  296. package/dist/index.d.ts +9 -0
  297. package/dist/index.d.ts.map +1 -0
  298. package/dist/index.js +9 -0
  299. package/dist/index.js.map +1 -0
  300. package/dist/migrations/index.d.ts +62 -0
  301. package/dist/migrations/index.d.ts.map +1 -0
  302. package/dist/migrations/index.js +187 -0
  303. package/dist/migrations/index.js.map +1 -0
  304. package/dist/migrations/manifests/0.1.9.json +30 -0
  305. package/dist/migrations/manifests/0.2.0.json +49 -0
  306. package/dist/migrations/manifests/0.2.12.json +9 -0
  307. package/dist/migrations/manifests/0.2.13.json +9 -0
  308. package/dist/migrations/manifests/0.2.14.json +175 -0
  309. package/dist/migrations/manifests/0.2.15.json +33 -0
  310. package/dist/migrations/manifests/0.3.0-beta.0.json +297 -0
  311. package/dist/migrations/manifests/0.3.0-beta.1.json +9 -0
  312. package/dist/migrations/manifests/0.3.0-beta.10.json +9 -0
  313. package/dist/migrations/manifests/0.3.0-beta.11.json +9 -0
  314. package/dist/migrations/manifests/0.3.0-beta.12.json +9 -0
  315. package/dist/migrations/manifests/0.3.0-beta.13.json +9 -0
  316. package/dist/migrations/manifests/0.3.0-beta.14.json +9 -0
  317. package/dist/migrations/manifests/0.3.0-beta.15.json +9 -0
  318. package/dist/migrations/manifests/0.3.0-beta.16.json +9 -0
  319. package/dist/migrations/manifests/0.3.0-beta.2.json +9 -0
  320. package/dist/migrations/manifests/0.3.0-beta.3.json +9 -0
  321. package/dist/migrations/manifests/0.3.0-beta.4.json +9 -0
  322. package/dist/migrations/manifests/0.3.0-beta.5.json +9 -0
  323. package/dist/migrations/manifests/0.3.0-beta.6.json +9 -0
  324. package/dist/migrations/manifests/0.3.0-beta.7.json +11 -0
  325. package/dist/migrations/manifests/0.3.0-beta.8.json +9 -0
  326. package/dist/migrations/manifests/0.3.0-beta.9.json +9 -0
  327. package/dist/migrations/manifests/0.3.0-rc.0.json +9 -0
  328. package/dist/migrations/manifests/0.3.0-rc.1.json +9 -0
  329. package/dist/migrations/manifests/0.3.0-rc.2.json +9 -0
  330. package/dist/migrations/manifests/0.3.0-rc.3.json +9 -0
  331. package/dist/migrations/manifests/0.3.0-rc.4.json +9 -0
  332. package/dist/migrations/manifests/0.3.0-rc.5.json +9 -0
  333. package/dist/migrations/manifests/0.3.0-rc.6.json +9 -0
  334. package/dist/migrations/manifests/0.3.0.json +11 -0
  335. package/dist/migrations/manifests/0.3.1.json +9 -0
  336. package/dist/migrations/manifests/0.3.10.json +9 -0
  337. package/dist/migrations/manifests/0.3.2.json +9 -0
  338. package/dist/migrations/manifests/0.3.3.json +9 -0
  339. package/dist/migrations/manifests/0.3.4.json +21 -0
  340. package/dist/migrations/manifests/0.3.5.json +9 -0
  341. package/dist/migrations/manifests/0.3.6.json +9 -0
  342. package/dist/migrations/manifests/0.3.7.json +9 -0
  343. package/dist/migrations/manifests/0.3.8.json +9 -0
  344. package/dist/migrations/manifests/0.3.9.json +9 -0
  345. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  346. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  347. package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
  348. package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
  349. package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
  350. package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
  351. package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
  352. package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
  353. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  354. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  355. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  356. package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
  357. package/dist/migrations/manifests/0.4.0.json +9 -0
  358. package/dist/migrations/manifests/0.5.0-beta.0.json +1646 -0
  359. package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
  360. package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
  361. package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
  362. package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
  363. package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
  364. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  365. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  366. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  367. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  368. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  369. package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
  370. package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
  371. package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
  372. package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
  373. package/dist/migrations/manifests/0.5.0-beta.5.json +222 -0
  374. package/dist/migrations/manifests/0.5.0-beta.6.json +9 -0
  375. package/dist/migrations/manifests/0.5.0-beta.7.json +9 -0
  376. package/dist/migrations/manifests/0.5.0-beta.8.json +9 -0
  377. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  378. package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
  379. package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
  380. package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
  381. package/dist/migrations/manifests/0.5.0-rc.3.json +9 -0
  382. package/dist/migrations/manifests/0.5.0-rc.4.json +9 -0
  383. package/dist/migrations/manifests/0.5.0-rc.5.json +9 -0
  384. package/dist/migrations/manifests/0.5.0-rc.6.json +9 -0
  385. package/dist/migrations/manifests/0.5.0-rc.7.json +9 -0
  386. package/dist/migrations/manifests/0.5.0.json +9 -0
  387. package/dist/migrations/manifests/0.5.1.json +9 -0
  388. package/dist/migrations/manifests/0.5.10.json +9 -0
  389. package/dist/migrations/manifests/0.5.11.json +16 -0
  390. package/dist/migrations/manifests/0.5.12.json +9 -0
  391. package/dist/migrations/manifests/0.5.13.json +9 -0
  392. package/dist/migrations/manifests/0.5.14.json +9 -0
  393. package/dist/migrations/manifests/0.5.15.json +9 -0
  394. package/dist/migrations/manifests/0.5.16.json +9 -0
  395. package/dist/migrations/manifests/0.5.17.json +9 -0
  396. package/dist/migrations/manifests/0.5.18.json +9 -0
  397. package/dist/migrations/manifests/0.5.19.json +9 -0
  398. package/dist/migrations/manifests/0.5.2.json +9 -0
  399. package/dist/migrations/manifests/0.5.3.json +9 -0
  400. package/dist/migrations/manifests/0.5.4.json +9 -0
  401. package/dist/migrations/manifests/0.5.5.json +9 -0
  402. package/dist/migrations/manifests/0.5.6.json +9 -0
  403. package/dist/migrations/manifests/0.5.7.json +16 -0
  404. package/dist/migrations/manifests/0.5.8.json +9 -0
  405. package/dist/migrations/manifests/0.5.9.json +9 -0
  406. package/dist/migrations/manifests/0.6.0-beta.0.json +16 -0
  407. package/dist/migrations/manifests/0.6.0-beta.1.json +9 -0
  408. package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
  409. package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
  410. package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
  411. package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
  412. package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
  413. package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
  414. package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
  415. package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
  416. package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
  417. package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
  418. package/dist/migrations/manifests/0.6.0-beta.2.json +9 -0
  419. package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
  420. package/dist/migrations/manifests/0.6.0-beta.21.json +9 -0
  421. package/dist/migrations/manifests/0.6.0-beta.22.json +9 -0
  422. package/dist/migrations/manifests/0.6.0-beta.23.json +88 -0
  423. package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
  424. package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
  425. package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
  426. package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
  427. package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
  428. package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
  429. package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
  430. package/dist/migrations/manifests/0.6.0-rc.0.json +9 -0
  431. package/dist/migrations/manifests/0.6.0.json +1 -0
  432. package/dist/migrations/manifests/0.6.1.json +9 -0
  433. package/dist/migrations/manifests/0.6.10.json +9 -0
  434. package/dist/migrations/manifests/0.6.11.json +9 -0
  435. package/dist/migrations/manifests/0.6.12.json +9 -0
  436. package/dist/migrations/manifests/0.6.13.json +9 -0
  437. package/dist/migrations/manifests/0.6.14.json +9 -0
  438. package/dist/migrations/manifests/0.6.15.json +9 -0
  439. package/dist/migrations/manifests/0.6.16.json +9 -0
  440. package/dist/migrations/manifests/0.6.17.json +9 -0
  441. package/dist/migrations/manifests/0.6.2.json +9 -0
  442. package/dist/migrations/manifests/0.6.3.json +24 -0
  443. package/dist/migrations/manifests/0.6.4.json +9 -0
  444. package/dist/migrations/manifests/0.6.5.json +9 -0
  445. package/dist/migrations/manifests/0.6.6.json +17 -0
  446. package/dist/migrations/manifests/0.6.7.json +9 -0
  447. package/dist/migrations/manifests/0.6.8.json +17 -0
  448. package/dist/migrations/manifests/0.6.9.json +9 -0
  449. package/dist/migrations/manifests/0.7.0-beta.0.json +9 -0
  450. package/dist/migrations/manifests/0.7.0-beta.1.json +9 -0
  451. package/dist/migrations/manifests/0.7.0-beta.2.json +9 -0
  452. package/dist/migrations/manifests/0.7.0-beta.3.json +9 -0
  453. package/dist/templates/claude/agents/trellis-check.md +115 -0
  454. package/dist/templates/claude/agents/trellis-implement.md +110 -0
  455. package/dist/templates/claude/agents/trellis-research.md +137 -0
  456. package/dist/templates/claude/hooks/statusline.py +332 -0
  457. package/dist/templates/claude/index.d.ts +31 -0
  458. package/dist/templates/claude/index.d.ts.map +1 -0
  459. package/dist/templates/claude/index.js +57 -0
  460. package/dist/templates/claude/index.js.map +1 -0
  461. package/dist/templates/claude/settings.json +73 -0
  462. package/dist/templates/codebuddy/agents/trellis-check.md +115 -0
  463. package/dist/templates/codebuddy/agents/trellis-implement.md +110 -0
  464. package/dist/templates/codebuddy/agents/trellis-research.md +137 -0
  465. package/dist/templates/codebuddy/index.d.ts +15 -0
  466. package/dist/templates/codebuddy/index.d.ts.map +1 -0
  467. package/dist/templates/codebuddy/index.js +15 -0
  468. package/dist/templates/codebuddy/index.js.map +1 -0
  469. package/dist/templates/codebuddy/settings.json +69 -0
  470. package/dist/templates/codex/agents/trellis-check.toml +56 -0
  471. package/dist/templates/codex/agents/trellis-implement.toml +37 -0
  472. package/dist/templates/codex/agents/trellis-research.toml +78 -0
  473. package/dist/templates/codex/config.toml +39 -0
  474. package/dist/templates/codex/hooks/session-start.py +552 -0
  475. package/dist/templates/codex/hooks.json +27 -0
  476. package/dist/templates/codex/index.d.ts +35 -0
  477. package/dist/templates/codex/index.d.ts.map +1 -0
  478. package/dist/templates/codex/index.js +67 -0
  479. package/dist/templates/codex/index.js.map +1 -0
  480. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +67 -0
  481. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +480 -0
  482. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +233 -0
  483. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +226 -0
  484. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +276 -0
  485. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +128 -0
  486. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +85 -0
  487. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  488. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +56 -0
  489. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +84 -0
  490. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  491. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +123 -0
  492. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  493. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  494. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +65 -0
  495. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  496. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +159 -0
  497. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  498. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  499. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +69 -0
  500. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  501. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  502. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +130 -0
  503. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  504. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  505. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +83 -0
  506. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +72 -0
  507. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  508. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +112 -0
  509. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +86 -0
  510. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +81 -0
  511. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +65 -0
  512. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +93 -0
  513. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/SKILL.md +41 -0
  514. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +90 -0
  515. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/repository-analysis.md +59 -0
  516. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +61 -0
  517. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-writing.md +70 -0
  518. package/dist/templates/common/commands/continue.md +56 -0
  519. package/dist/templates/common/commands/finish-work.md +66 -0
  520. package/dist/templates/common/commands/start.md +59 -0
  521. package/dist/templates/common/index.d.ts +48 -0
  522. package/dist/templates/common/index.d.ts.map +1 -0
  523. package/dist/templates/common/index.js +104 -0
  524. package/dist/templates/common/index.js.map +1 -0
  525. package/dist/templates/common/skills/before-dev.md +46 -0
  526. package/dist/templates/common/skills/brainstorm.md +195 -0
  527. package/dist/templates/common/skills/break-loop.md +184 -0
  528. package/dist/templates/common/skills/check.md +106 -0
  529. package/dist/templates/common/skills/update-spec.md +351 -0
  530. package/dist/templates/copilot/copilot-instructions.md +11 -0
  531. package/dist/templates/copilot/hooks/session-start.py +548 -0
  532. package/dist/templates/copilot/hooks.json +19 -0
  533. package/dist/templates/copilot/index.d.ts +28 -0
  534. package/dist/templates/copilot/index.d.ts.map +1 -0
  535. package/dist/templates/copilot/index.js +61 -0
  536. package/dist/templates/copilot/index.js.map +1 -0
  537. package/dist/templates/copilot/prompts/before-dev.prompt.md +39 -0
  538. package/dist/templates/copilot/prompts/brainstorm.prompt.md +156 -0
  539. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  540. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +170 -0
  541. package/dist/templates/copilot/prompts/check.prompt.md +110 -0
  542. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  543. package/dist/templates/copilot/prompts/finish-work.prompt.md +99 -0
  544. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  545. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  546. package/dist/templates/copilot/prompts/parallel.prompt.md +204 -0
  547. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  548. package/dist/templates/copilot/prompts/start.prompt.md +63 -0
  549. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  550. package/dist/templates/cursor/agents/trellis-check.md +114 -0
  551. package/dist/templates/cursor/agents/trellis-implement.md +109 -0
  552. package/dist/templates/cursor/agents/trellis-research.md +136 -0
  553. package/dist/templates/cursor/hooks.json +24 -0
  554. package/dist/templates/cursor/index.d.ts +13 -0
  555. package/dist/templates/cursor/index.d.ts.map +1 -0
  556. package/dist/templates/cursor/index.js +13 -0
  557. package/dist/templates/cursor/index.js.map +1 -0
  558. package/dist/templates/droid/droids/trellis-check.md +107 -0
  559. package/dist/templates/droid/droids/trellis-implement.md +102 -0
  560. package/dist/templates/droid/droids/trellis-research.md +137 -0
  561. package/dist/templates/droid/index.d.ts +15 -0
  562. package/dist/templates/droid/index.d.ts.map +1 -0
  563. package/dist/templates/droid/index.js +15 -0
  564. package/dist/templates/droid/index.js.map +1 -0
  565. package/dist/templates/droid/settings.json +69 -0
  566. package/dist/templates/dsh/DSH.md +56 -0
  567. package/dist/templates/dsh/index.d.ts +21 -0
  568. package/dist/templates/dsh/index.d.ts.map +1 -0
  569. package/dist/templates/dsh/index.js +25 -0
  570. package/dist/templates/dsh/index.js.map +1 -0
  571. package/dist/templates/extract.d.ts +40 -0
  572. package/dist/templates/extract.d.ts.map +1 -0
  573. package/dist/templates/extract.js +106 -0
  574. package/dist/templates/extract.js.map +1 -0
  575. package/dist/templates/gemini/agents/trellis-check.md +107 -0
  576. package/dist/templates/gemini/agents/trellis-implement.md +102 -0
  577. package/dist/templates/gemini/agents/trellis-research.md +136 -0
  578. package/dist/templates/gemini/index.d.ts +13 -0
  579. package/dist/templates/gemini/index.d.ts.map +1 -0
  580. package/dist/templates/gemini/index.js +13 -0
  581. package/dist/templates/gemini/index.js.map +1 -0
  582. package/dist/templates/gemini/settings.json +40 -0
  583. package/dist/templates/grok/agents/trellis-check.md +105 -0
  584. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  585. package/dist/templates/grok/agents/trellis-research.md +146 -0
  586. package/dist/templates/grok/index.d.ts +16 -0
  587. package/dist/templates/grok/index.d.ts.map +1 -0
  588. package/dist/templates/grok/index.js +19 -0
  589. package/dist/templates/grok/index.js.map +1 -0
  590. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  591. package/dist/templates/kimi/agents/trellis-implement.md +115 -0
  592. package/dist/templates/kimi/agents/trellis-research.md +144 -0
  593. package/dist/templates/kimi/index.d.ts +20 -0
  594. package/dist/templates/kimi/index.d.ts.map +1 -0
  595. package/dist/templates/kimi/index.js +23 -0
  596. package/dist/templates/kimi/index.js.map +1 -0
  597. package/dist/templates/kiro/agents/trellis-check.json +26 -0
  598. package/dist/templates/kiro/agents/trellis-implement.json +26 -0
  599. package/dist/templates/kiro/agents/trellis-research.json +30 -0
  600. package/dist/templates/kiro/agents/trellis.json +34 -0
  601. package/dist/templates/kiro/hooks/trellis-workflow-state.kiro.hook +14 -0
  602. package/dist/templates/kiro/index.d.ts +32 -0
  603. package/dist/templates/kiro/index.d.ts.map +1 -0
  604. package/dist/templates/kiro/index.js +28 -0
  605. package/dist/templates/kiro/index.js.map +1 -0
  606. package/dist/templates/markdown/agents.md +21 -0
  607. package/dist/templates/markdown/gitignore.txt +15 -0
  608. package/dist/templates/markdown/index.d.ts +27 -0
  609. package/dist/templates/markdown/index.d.ts.map +1 -0
  610. package/dist/templates/markdown/index.js +52 -0
  611. package/dist/templates/markdown/index.js.map +1 -0
  612. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
  613. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
  614. package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
  615. package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
  616. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
  617. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
  618. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
  619. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
  620. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
  621. package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
  622. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
  623. package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
  624. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
  625. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +223 -0
  626. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +327 -0
  627. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +633 -0
  628. package/dist/templates/markdown/spec/guides/index.md.txt +97 -0
  629. package/dist/templates/markdown/workspace-index.md +125 -0
  630. package/dist/templates/markdown/worktree.yaml.txt +58 -0
  631. package/dist/templates/omp/agents/trellis-check.md +41 -0
  632. package/dist/templates/omp/agents/trellis-implement.md +45 -0
  633. package/dist/templates/omp/agents/trellis-research.md +30 -0
  634. package/dist/templates/omp/extensions/trellis/index.ts.txt +1105 -0
  635. package/dist/templates/omp/index.d.ts +4 -0
  636. package/dist/templates/omp/index.d.ts.map +1 -0
  637. package/dist/templates/omp/index.js +9 -0
  638. package/dist/templates/omp/index.js.map +1 -0
  639. package/dist/templates/opencode/agents/trellis-check.md +122 -0
  640. package/dist/templates/opencode/agents/trellis-implement.md +118 -0
  641. package/dist/templates/opencode/agents/trellis-research.md +145 -0
  642. package/dist/templates/opencode/lib/context-visibility.js +148 -0
  643. package/dist/templates/opencode/lib/session-utils.js +527 -0
  644. package/dist/templates/opencode/lib/trellis-context.js +627 -0
  645. package/dist/templates/opencode/package.json +5 -0
  646. package/dist/templates/opencode/plugins/inject-subagent-context.js +569 -0
  647. package/dist/templates/opencode/plugins/inject-workflow-state.js +240 -0
  648. package/dist/templates/opencode/plugins/session-start.js +65 -0
  649. package/dist/templates/pi/agents/trellis-check.md +37 -0
  650. package/dist/templates/pi/agents/trellis-implement.md +42 -0
  651. package/dist/templates/pi/agents/trellis-research.md +25 -0
  652. package/dist/templates/pi/extensions/trellis/index.ts.txt +2027 -0
  653. package/dist/templates/pi/index.d.ts +5 -0
  654. package/dist/templates/pi/index.d.ts.map +1 -0
  655. package/dist/templates/pi/index.js +12 -0
  656. package/dist/templates/pi/index.js.map +1 -0
  657. package/dist/templates/pi/settings.json +9 -0
  658. package/dist/templates/qoder/agents/trellis-check.md +108 -0
  659. package/dist/templates/qoder/agents/trellis-implement.md +103 -0
  660. package/dist/templates/qoder/agents/trellis-research.md +137 -0
  661. package/dist/templates/qoder/index.d.ts +15 -0
  662. package/dist/templates/qoder/index.d.ts.map +1 -0
  663. package/dist/templates/qoder/index.js +15 -0
  664. package/dist/templates/qoder/index.js.map +1 -0
  665. package/dist/templates/qoder/settings.json +59 -0
  666. package/dist/templates/reasonix/agents/trellis-check.md +36 -0
  667. package/dist/templates/reasonix/agents/trellis-implement.md +41 -0
  668. package/dist/templates/reasonix/index.d.ts +13 -0
  669. package/dist/templates/reasonix/index.d.ts.map +1 -0
  670. package/dist/templates/reasonix/index.js +16 -0
  671. package/dist/templates/reasonix/index.js.map +1 -0
  672. package/dist/templates/shared-hooks/index.d.ts +77 -0
  673. package/dist/templates/shared-hooks/index.d.ts.map +1 -0
  674. package/dist/templates/shared-hooks/index.js +140 -0
  675. package/dist/templates/shared-hooks/index.js.map +1 -0
  676. package/dist/templates/shared-hooks/inject-shell-session-context.py +292 -0
  677. package/dist/templates/shared-hooks/inject-subagent-context.py +1212 -0
  678. package/dist/templates/shared-hooks/inject-workflow-state.py +488 -0
  679. package/dist/templates/shared-hooks/session-start.py +949 -0
  680. package/dist/templates/snow/SNOW.md +86 -0
  681. package/dist/templates/snow/agents/trellis-check.md +135 -0
  682. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  683. package/dist/templates/snow/agents/trellis-research.md +114 -0
  684. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  685. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  686. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  687. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  688. package/dist/templates/snow/index.d.ts +26 -0
  689. package/dist/templates/snow/index.d.ts.map +1 -0
  690. package/dist/templates/snow/index.js +36 -0
  691. package/dist/templates/snow/index.js.map +1 -0
  692. package/dist/templates/template-utils.d.ts +26 -0
  693. package/dist/templates/template-utils.d.ts.map +1 -0
  694. package/dist/templates/template-utils.js +64 -0
  695. package/dist/templates/template-utils.js.map +1 -0
  696. package/dist/templates/trae/agents/trellis-check.md +108 -0
  697. package/dist/templates/trae/agents/trellis-implement.md +103 -0
  698. package/dist/templates/trae/agents/trellis-research.md +137 -0
  699. package/dist/templates/trae/hooks.json +39 -0
  700. package/dist/templates/trae/index.d.ts +15 -0
  701. package/dist/templates/trae/index.d.ts.map +1 -0
  702. package/dist/templates/trae/index.js +15 -0
  703. package/dist/templates/trae/index.js.map +1 -0
  704. package/dist/templates/trellis/agents/check.md +70 -0
  705. package/dist/templates/trellis/agents/implement.md +71 -0
  706. package/dist/templates/trellis/config.yaml +158 -0
  707. package/dist/templates/trellis/gitattributes.txt +9 -0
  708. package/dist/templates/trellis/gitignore.txt +32 -0
  709. package/dist/templates/trellis/index.d.ts +67 -0
  710. package/dist/templates/trellis/index.d.ts.map +1 -0
  711. package/dist/templates/trellis/index.js +121 -0
  712. package/dist/templates/trellis/index.js.map +1 -0
  713. package/dist/templates/trellis/scripts/__init__.py +5 -0
  714. package/dist/templates/trellis/scripts/add_session.py +1545 -0
  715. package/dist/templates/trellis/scripts/common/__init__.py +94 -0
  716. package/dist/templates/trellis/scripts/common/active_task.py +857 -0
  717. package/dist/templates/trellis/scripts/common/cli_adapter.py +950 -0
  718. package/dist/templates/trellis/scripts/common/config.py +482 -0
  719. package/dist/templates/trellis/scripts/common/developer.py +192 -0
  720. package/dist/templates/trellis/scripts/common/git.py +203 -0
  721. package/dist/templates/trellis/scripts/common/git_context.py +106 -0
  722. package/dist/templates/trellis/scripts/common/io.py +160 -0
  723. package/dist/templates/trellis/scripts/common/log.py +45 -0
  724. package/dist/templates/trellis/scripts/common/packages_context.py +238 -0
  725. package/dist/templates/trellis/scripts/common/paths.py +543 -0
  726. package/dist/templates/trellis/scripts/common/safe_commit.py +322 -0
  727. package/dist/templates/trellis/scripts/common/session_context.py +892 -0
  728. package/dist/templates/trellis/scripts/common/task_context.py +470 -0
  729. package/dist/templates/trellis/scripts/common/task_queue.py +188 -0
  730. package/dist/templates/trellis/scripts/common/task_store.py +1874 -0
  731. package/dist/templates/trellis/scripts/common/task_utils.py +498 -0
  732. package/dist/templates/trellis/scripts/common/tasks.py +122 -0
  733. package/dist/templates/trellis/scripts/common/trellis_config.py +239 -0
  734. package/dist/templates/trellis/scripts/common/types.py +110 -0
  735. package/dist/templates/trellis/scripts/common/workflow_phase.py +242 -0
  736. package/dist/templates/trellis/scripts/get_context.py +16 -0
  737. package/dist/templates/trellis/scripts/get_developer.py +26 -0
  738. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  739. package/dist/templates/trellis/scripts/init_developer.py +51 -0
  740. package/dist/templates/trellis/scripts/task.py +801 -0
  741. package/dist/templates/trellis/tasks/.gitkeep +0 -0
  742. package/dist/templates/trellis/workflow.md +721 -0
  743. package/dist/templates/zcode/agents/trellis-check.md +109 -0
  744. package/dist/templates/zcode/agents/trellis-implement.md +111 -0
  745. package/dist/templates/zcode/agents/trellis-research.md +133 -0
  746. package/dist/templates/zcode/config.json +52 -0
  747. package/dist/templates/zcode/index.d.ts +25 -0
  748. package/dist/templates/zcode/index.d.ts.map +1 -0
  749. package/dist/templates/zcode/index.js +29 -0
  750. package/dist/templates/zcode/index.js.map +1 -0
  751. package/dist/types/ai-tools.d.ts +105 -0
  752. package/dist/types/ai-tools.d.ts.map +1 -0
  753. package/dist/types/ai-tools.js +502 -0
  754. package/dist/types/ai-tools.js.map +1 -0
  755. package/dist/types/migration.d.ts +125 -0
  756. package/dist/types/migration.d.ts.map +1 -0
  757. package/dist/types/migration.js +8 -0
  758. package/dist/types/migration.js.map +1 -0
  759. package/dist/utils/ablation-store.d.ts +123 -0
  760. package/dist/utils/ablation-store.d.ts.map +1 -0
  761. package/dist/utils/ablation-store.js +749 -0
  762. package/dist/utils/ablation-store.js.map +1 -0
  763. package/dist/utils/agent-refs.d.ts +31 -0
  764. package/dist/utils/agent-refs.d.ts.map +1 -0
  765. package/dist/utils/agent-refs.js +63 -0
  766. package/dist/utils/agent-refs.js.map +1 -0
  767. package/dist/utils/atomic-write.d.ts +12 -0
  768. package/dist/utils/atomic-write.d.ts.map +1 -0
  769. package/dist/utils/atomic-write.js +30 -0
  770. package/dist/utils/atomic-write.js.map +1 -0
  771. package/dist/utils/compare-versions.d.ts +12 -0
  772. package/dist/utils/compare-versions.d.ts.map +1 -0
  773. package/dist/utils/compare-versions.js +86 -0
  774. package/dist/utils/compare-versions.js.map +1 -0
  775. package/dist/utils/cwd-guard.d.ts +38 -0
  776. package/dist/utils/cwd-guard.d.ts.map +1 -0
  777. package/dist/utils/cwd-guard.js +62 -0
  778. package/dist/utils/cwd-guard.js.map +1 -0
  779. package/dist/utils/file-writer.d.ts +36 -0
  780. package/dist/utils/file-writer.d.ts.map +1 -0
  781. package/dist/utils/file-writer.js +204 -0
  782. package/dist/utils/file-writer.js.map +1 -0
  783. package/dist/utils/managed-paths.d.ts +5 -0
  784. package/dist/utils/managed-paths.d.ts.map +1 -0
  785. package/dist/utils/managed-paths.js +51 -0
  786. package/dist/utils/managed-paths.js.map +1 -0
  787. package/dist/utils/managed-removal.d.ts +74 -0
  788. package/dist/utils/managed-removal.d.ts.map +1 -0
  789. package/dist/utils/managed-removal.js +270 -0
  790. package/dist/utils/managed-removal.js.map +1 -0
  791. package/dist/utils/manifest-prune.d.ts +61 -0
  792. package/dist/utils/manifest-prune.d.ts.map +1 -0
  793. package/dist/utils/manifest-prune.js +135 -0
  794. package/dist/utils/manifest-prune.js.map +1 -0
  795. package/dist/utils/posix.d.ts +13 -0
  796. package/dist/utils/posix.d.ts.map +1 -0
  797. package/dist/utils/posix.js +15 -0
  798. package/dist/utils/posix.js.map +1 -0
  799. package/dist/utils/project-detector.d.ts +46 -0
  800. package/dist/utils/project-detector.d.ts.map +1 -0
  801. package/dist/utils/project-detector.js +666 -0
  802. package/dist/utils/project-detector.js.map +1 -0
  803. package/dist/utils/proxy.d.ts +25 -0
  804. package/dist/utils/proxy.d.ts.map +1 -0
  805. package/dist/utils/proxy.js +60 -0
  806. package/dist/utils/proxy.js.map +1 -0
  807. package/dist/utils/registry-config.d.ts +7 -0
  808. package/dist/utils/registry-config.d.ts.map +1 -0
  809. package/dist/utils/registry-config.js +172 -0
  810. package/dist/utils/registry-config.js.map +1 -0
  811. package/dist/utils/task-json.d.ts +13 -0
  812. package/dist/utils/task-json.d.ts.map +1 -0
  813. package/dist/utils/task-json.js +12 -0
  814. package/dist/utils/task-json.js.map +1 -0
  815. package/dist/utils/template-fetcher.d.ts +161 -0
  816. package/dist/utils/template-fetcher.d.ts.map +1 -0
  817. package/dist/utils/template-fetcher.js +994 -0
  818. package/dist/utils/template-fetcher.js.map +1 -0
  819. package/dist/utils/template-hash.d.ts +127 -0
  820. package/dist/utils/template-hash.d.ts.map +1 -0
  821. package/dist/utils/template-hash.js +335 -0
  822. package/dist/utils/template-hash.js.map +1 -0
  823. package/dist/utils/uninstall-scrubbers.d.ts +66 -0
  824. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
  825. package/dist/utils/uninstall-scrubbers.js +417 -0
  826. package/dist/utils/uninstall-scrubbers.js.map +1 -0
  827. package/dist/utils/workflow-resolver.d.ts +86 -0
  828. package/dist/utils/workflow-resolver.d.ts.map +1 -0
  829. package/dist/utils/workflow-resolver.js +265 -0
  830. package/dist/utils/workflow-resolver.js.map +1 -0
  831. package/package.json +89 -0
@@ -0,0 +1,1874 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Task CRUD operations.
4
+
5
+ Provides:
6
+ ensure_tasks_dir - Ensure tasks directory exists
7
+ cmd_create - Create a new task
8
+ cmd_rename - Rename a task and every reference to it
9
+ cmd_archive - Archive completed task
10
+ cmd_set_branch - Set git branch for task
11
+ cmd_set_base_branch - Set PR target branch
12
+ cmd_set_scope - Set scope for PR title
13
+ cmd_set_meta - Set/overwrite a task metadata key
14
+ cmd_add_subtask - Link child task to parent
15
+ cmd_remove_subtask - Unlink child task from parent
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import argparse
21
+ import re
22
+ import sys
23
+ from dataclasses import dataclass, field
24
+ from datetime import datetime
25
+ from pathlib import Path
26
+
27
+ from .config import (
28
+ get_codex_dispatch_mode,
29
+ get_packages,
30
+ get_session_auto_commit,
31
+ is_monorepo,
32
+ resolve_package,
33
+ validate_package,
34
+ )
35
+ from .git import (
36
+ INDEX_LOCK_RETRY_ATTEMPTS,
37
+ branch_exists_locally,
38
+ has_git_remote,
39
+ index_lock_path,
40
+ resolve_default_branch,
41
+ run_git,
42
+ run_git_retry_index_lock,
43
+ stderr_indicates_index_lock,
44
+ )
45
+ from .io import describe_json_read_failure, read_json_checked, write_json
46
+ from .log import Colors, colored
47
+ from .paths import (
48
+ DEVELOPER_HINT,
49
+ DIR_ARCHIVE,
50
+ DIR_TASKS,
51
+ DIR_WORKFLOW,
52
+ FILE_TASK_JSON,
53
+ generate_task_date_prefix,
54
+ get_developer,
55
+ get_repo_root,
56
+ get_tasks_dir,
57
+ )
58
+ from .safe_commit import (
59
+ print_gitignore_warning,
60
+ safe_archive_paths_to_add,
61
+ safe_git_add,
62
+ )
63
+ from .task_utils import (
64
+ archive_destination_for,
65
+ archive_task_complete,
66
+ find_task_by_name,
67
+ is_within_tasks_dir,
68
+ resolve_task_dir,
69
+ run_task_hooks,
70
+ )
71
+
72
+
73
+ # =============================================================================
74
+ # Helper Functions
75
+ # =============================================================================
76
+
77
+ # Characters stripped before deciding whether a title / description is empty.
78
+ # This is deliberately the *union* of Python's str.strip() default set and
79
+ # ECMAScript's String.trim() set, because the two disagree: Python strips
80
+ # U+0085 (NEL) and U+001C-U+001F, which JS keeps; JS strips U+FEFF (BOM),
81
+ # which Python keeps. Stripping the union means anything create accepts is
82
+ # still non-empty after either side trims it, so a record cannot pass here and
83
+ # then be refused as empty by a JS pre-archive validator.
84
+ BLANK_CHARS = (
85
+ "\t\n\v\f\r" # U+0009-U+000D
86
+ "\x1c\x1d\x1e\x1f" # U+001C-U+001F separators (Python only)
87
+ " " # U+0020 space
88
+ "\x85" # U+0085 next line (Python only)
89
+ "\xa0" # U+00A0 no-break space
90
+ "\u1680" # ogham space mark
91
+ "\u2000\u2001\u2002\u2003\u2004\u2005" # en/em quad, en/em/three/four-per-em
92
+ "\u2006\u2007\u2008\u2009\u200a" # six-per-em .. hair space
93
+ "\u2028\u2029" # line / paragraph separator
94
+ "\u202f\u205f\u3000" # narrow nbsp, math space, ideographic space
95
+ "\ufeff" # zero-width no-break space / BOM (JS only)
96
+ )
97
+
98
+
99
+ def strip_blank(value: str | None) -> str:
100
+ """Return ``value`` with :data:`BLANK_CHARS` trimmed from both ends."""
101
+ return (value or "").strip(BLANK_CHARS)
102
+
103
+
104
+ def _slugify(title: str) -> str:
105
+ """Convert title to slug (only works with ASCII)."""
106
+ result = title.lower()
107
+ result = re.sub(r"[^a-z0-9]", "-", result)
108
+ result = re.sub(r"-+", "-", result)
109
+ result = result.strip("-")
110
+ return result
111
+
112
+
113
+ def ensure_tasks_dir(repo_root: Path) -> Path:
114
+ """Ensure tasks directory exists."""
115
+ tasks_dir = get_tasks_dir(repo_root)
116
+ archive_dir = tasks_dir / "archive"
117
+
118
+ if not tasks_dir.exists():
119
+ tasks_dir.mkdir(parents=True)
120
+ print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
121
+
122
+ if not archive_dir.exists():
123
+ archive_dir.mkdir(parents=True)
124
+
125
+ return tasks_dir
126
+
127
+
128
+ def _find_archived_task_by_dir_name(tasks_dir: Path, dir_name: str) -> Path | None:
129
+ """Find an archived task directory with the exact active-task dir name."""
130
+ archive_dir = tasks_dir / DIR_ARCHIVE
131
+ if not archive_dir.is_dir():
132
+ return None
133
+
134
+ for month_dir in sorted(archive_dir.iterdir()):
135
+ if not month_dir.is_dir():
136
+ continue
137
+ candidate = month_dir / dir_name
138
+ if candidate.is_dir():
139
+ return candidate
140
+
141
+ return None
142
+
143
+
144
+ def _repo_relative_path(path: Path, repo_root: Path) -> str:
145
+ """Format a path relative to the repo root when possible."""
146
+ try:
147
+ return path.relative_to(repo_root).as_posix()
148
+ except ValueError:
149
+ return str(path)
150
+
151
+
152
+ def _report_read_failure(path: Path, reason: str | None) -> None:
153
+ """Print why a JSON file could not be loaded, and what to do about it.
154
+
155
+ These commands are about to rewrite the file they just failed to read, so
156
+ this message is the last warning the user gets. Exiting non-zero with no
157
+ output at all leaves a parse error, a permissions error and an empty file
158
+ indistinguishable.
159
+ """
160
+ problem, hint = describe_json_read_failure(path, reason)
161
+ print(colored(f"Error: {problem}", Colors.RED), file=sys.stderr)
162
+ print(hint, file=sys.stderr)
163
+
164
+
165
+ def _ensure_children_list(data: dict) -> list:
166
+ """The task's `children` as a list, repaired in place if it is not one.
167
+
168
+ `.get(key, default)` returns the default only when the key is *absent*. A
169
+ task.json carrying `"children": null` — older format, or hand-edited —
170
+ yields None, and every caller below goes on to use the result as a list.
171
+ In `cmd_create` that raise lands *after* the new task.json is written,
172
+ leaving a task on disk its parent does not reference.
173
+
174
+ The repair is written into `data` rather than only returned, because the
175
+ unlink path removes a name it may not find and would otherwise persist the
176
+ malformed value untouched — no crash, but the next caller inherits it.
177
+
178
+ Anything that is not a list is discarded rather than coerced: a string
179
+ would iterate per character and a dict per key, each producing a plausible
180
+ child set that is not one.
181
+ """
182
+ children = data.get("children")
183
+ if not isinstance(children, list):
184
+ children = []
185
+ data["children"] = children
186
+ return children
187
+
188
+
189
+ def _report_write_failure(path: Path) -> None:
190
+ """Print that a JSON write failed. Writes are atomic, so nothing changed."""
191
+ print(colored(f"Error: Failed to write {path}", Colors.RED), file=sys.stderr)
192
+ print(
193
+ "The existing file is unchanged (writes are atomic). "
194
+ "Check permissions and free disk space, then retry.",
195
+ file=sys.stderr,
196
+ )
197
+
198
+
199
+ # =============================================================================
200
+ # Sub-agent platform detection + JSONL context files
201
+ # =============================================================================
202
+
203
+ # Config directories of platforms that consume implement.jsonl / check.jsonl.
204
+ # Keep in sync with src/types/ai-tools.ts AI_TOOLS entries — these are the
205
+ # platforms listed in workflow.md's "agent-capable" Skill Routing block.
206
+ # Codex is checked separately because explicit inline mode does not consume
207
+ # JSONL. Kilo / Antigravity / Devin are NOT in this list either: they load
208
+ # specs through skills instead of JSONL.
209
+ _SUBAGENT_CONFIG_DIRS: tuple[str, ...] = (
210
+ ".claude",
211
+ ".cursor",
212
+ ".kiro",
213
+ ".gemini",
214
+ ".opencode",
215
+ ".qoder",
216
+ ".codebuddy",
217
+ ".factory", # Factory Droid
218
+ ".github/copilot",
219
+ ".pi", # Pi Agent
220
+ ".trae", # Trae IDE
221
+ ".omp", # Oh My Pi
222
+ ".zcode", # ZCode
223
+ ".grok", # Grok Build
224
+ ".kimi-code", # Kimi Code
225
+ )
226
+ _CODEX_CONFIG_DIR = ".codex"
227
+
228
+
229
+ def _has_subagent_platform(repo_root: Path) -> bool:
230
+ """Return True if any sub-agent-capable platform is configured.
231
+
232
+ Detected by probing well-known config directories at the repo root. Codex
233
+ counts by default through ``codex.dispatch_mode: auto`` (including the
234
+ legacy ``sub-agent`` alias); explicit inline mode loads context through
235
+ skills, not JSONL.
236
+ """
237
+ for config_dir in _SUBAGENT_CONFIG_DIRS:
238
+ if (repo_root / config_dir).is_dir():
239
+ return True
240
+ if (repo_root / _CODEX_CONFIG_DIR).is_dir():
241
+ return get_codex_dispatch_mode(repo_root) == "auto"
242
+ return False
243
+
244
+
245
+ def _parse_meta_pairs(pairs: list[str] | None) -> dict[str, str] | None:
246
+ """Parse repeatable ``--meta key=value`` pairs into a dict.
247
+
248
+ Returns ``None`` (after printing an error naming the bad value) on the
249
+ first malformed pair: missing ``=`` or an empty key. Values are stored
250
+ as-is (strings, no nesting, no type coercion).
251
+ """
252
+ meta: dict[str, str] = {}
253
+ for pair in pairs or []:
254
+ key, sep, value = pair.partition("=")
255
+ if not sep or not key:
256
+ print(
257
+ colored(f"Error: malformed --meta value '{pair}' (expected key=value)", Colors.RED),
258
+ file=sys.stderr,
259
+ )
260
+ return None
261
+ meta[key] = value
262
+ return meta
263
+
264
+
265
+ def _default_prd_content(title: str, description: str | None = None) -> str:
266
+ """Return the default PRD skeleton created with every task."""
267
+ goal = (description or "").strip() or "TBD."
268
+ heading = title.strip() or "Untitled task"
269
+ return f"""# {heading}
270
+
271
+ ## Goal
272
+
273
+ {goal}
274
+
275
+ ## Requirements
276
+
277
+ - TBD
278
+
279
+ ## Acceptance Criteria
280
+
281
+ - [ ] TBD
282
+
283
+ ## Notes
284
+
285
+ - Keep `prd.md` focused on requirements, constraints, and acceptance criteria.
286
+ - Lightweight tasks can remain PRD-only.
287
+ - For complex tasks, add `design.md` for technical design and `implement.md` for execution planning before `task.py start`.
288
+ """
289
+
290
+
291
+ # =============================================================================
292
+ # Command: create
293
+ # =============================================================================
294
+
295
+ def cmd_create(args: argparse.Namespace) -> int:
296
+ """Create a new task."""
297
+ repo_root = get_repo_root()
298
+
299
+ # Title and description are checked first, before ensure_tasks_dir and any
300
+ # other write, so a rejected create leaves the tree untouched. Both are
301
+ # required non-empty by pre-archive validation; catching that here costs
302
+ # one retyped command instead of blocking a PR hours later.
303
+ if not strip_blank(args.title):
304
+ print(colored("Error: title is required", Colors.RED), file=sys.stderr)
305
+ print(
306
+ 'Pass a non-empty <title> (whitespace only does not count): a task '
307
+ "with an empty title is refused at archive.",
308
+ file=sys.stderr,
309
+ )
310
+ return 1
311
+
312
+ description = strip_blank(getattr(args, "description", None))
313
+ if not description:
314
+ print(colored("Error: --description is required", Colors.RED), file=sys.stderr)
315
+ print(
316
+ 'Pass --description "<what this task delivers>" (whitespace only does '
317
+ "not count): a task with an empty description is refused at archive.",
318
+ file=sys.stderr,
319
+ )
320
+ return 1
321
+
322
+ # Validate --meta (CLI source: fail-fast, before any directory is created)
323
+ meta = _parse_meta_pairs(getattr(args, "meta", None))
324
+ if meta is None:
325
+ return 1
326
+
327
+ # Validate --package (CLI source: fail-fast)
328
+ package: str | None = getattr(args, "package", None)
329
+ if not is_monorepo(repo_root):
330
+ # Single-repo: ignore --package, no package prefix
331
+ if package:
332
+ print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
333
+ package = None
334
+ elif package:
335
+ if not validate_package(package, repo_root):
336
+ packages = get_packages(repo_root)
337
+ available = ", ".join(sorted(packages.keys())) if packages else "(none)"
338
+ print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr)
339
+ return 1
340
+ else:
341
+ # Inferred: default_package → None (no task.json yet for create)
342
+ package = resolve_package(repo_root=repo_root)
343
+
344
+ # Default assignee to current developer
345
+ assignee = args.assignee
346
+ if not assignee:
347
+ assignee = get_developer(repo_root)
348
+ if not assignee:
349
+ print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
350
+ print(DEVELOPER_HINT, file=sys.stderr)
351
+ return 1
352
+
353
+ ensure_tasks_dir(repo_root)
354
+
355
+ # Get current developer as creator
356
+ creator = get_developer(repo_root) or assignee
357
+
358
+ # Generate slug if not provided. A title-derived slug is sanitized by
359
+ # _slugify; an explicit --slug is not, so reject the characters that would
360
+ # let it escape the tasks directory once joined into the dir name.
361
+ slug = args.slug or _slugify(args.title)
362
+ if not slug:
363
+ print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
364
+ return 1
365
+
366
+ if args.slug and ("/" in slug or "\\" in slug or ".." in slug):
367
+ print(
368
+ colored(
369
+ f"Error: --slug must be a plain name without path separators or '..': {slug}",
370
+ Colors.RED,
371
+ ),
372
+ file=sys.stderr,
373
+ )
374
+ return 1
375
+
376
+ # Create task directory with MM-DD-slug format
377
+ tasks_dir = get_tasks_dir(repo_root)
378
+ date_prefix = generate_task_date_prefix()
379
+
380
+ # Guard against date-prefixed --slug (e.g. a full task dir name pasted in),
381
+ # which would otherwise produce MM-DD-MM-DD-slug (issue #377). Only an
382
+ # explicit --slug is guarded; title-derived slugs are left untouched.
383
+ if args.slug:
384
+ m = re.match(r"^(\d{2})-(\d{2})-(.+)$", slug)
385
+ if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
386
+ slug_prefix = f"{m.group(1)}-{m.group(2)}"
387
+ if slug_prefix == date_prefix:
388
+ slug = m.group(3)
389
+ print(
390
+ colored(
391
+ f'warning: --slug should not include the MM-DD prefix; normalized to "{slug}"',
392
+ Colors.YELLOW,
393
+ ),
394
+ file=sys.stderr,
395
+ )
396
+ else:
397
+ print(
398
+ colored(
399
+ f"Error: --slug starts with a date prefix ({slug_prefix}-), but task.py create always uses today's date ({date_prefix}).",
400
+ Colors.RED,
401
+ ),
402
+ file=sys.stderr,
403
+ )
404
+ print(f"Pass only the slug body, e.g. --slug {m.group(3)}", file=sys.stderr)
405
+ return 1
406
+
407
+ # Resolve --parent before anything is created. An explicit --parent that
408
+ # cannot be linked is a failed request, not a warning: creating the task
409
+ # anyway leaves a half-specified relationship that reads as success.
410
+ parent_dir: Path | None = None
411
+ parent_data: dict | None = None
412
+ if args.parent:
413
+ parent_dir = resolve_task_dir(args.parent, repo_root)
414
+ if parent_dir is None:
415
+ print(colored(f"Error: Parent task not resolved: {args.parent}", Colors.RED), file=sys.stderr)
416
+ print("No task was created. Pass an existing task directory to --parent.", file=sys.stderr)
417
+ return 1
418
+ parent_json_path = parent_dir / FILE_TASK_JSON
419
+ if not parent_json_path.is_file():
420
+ print(colored(f"Error: Parent task.json not found: {args.parent}", Colors.RED), file=sys.stderr)
421
+ print("No task was created. Pass an existing task directory to --parent.", file=sys.stderr)
422
+ return 1
423
+ parent_data, parent_reason = read_json_checked(parent_json_path)
424
+ if parent_data is None:
425
+ _report_read_failure(parent_json_path, parent_reason)
426
+ print("No task was created. Fix the parent task.json, then retry.", file=sys.stderr)
427
+ return 1
428
+
429
+ dir_name = f"{date_prefix}-{slug}"
430
+ task_dir = tasks_dir / dir_name
431
+ task_json_path = task_dir / FILE_TASK_JSON
432
+
433
+ archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, dir_name)
434
+ if archived_task_dir:
435
+ print(colored(f"Error: Task already archived: {dir_name}", Colors.RED), file=sys.stderr)
436
+ print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
437
+ print("Use a new slug if you intend to create a new task.", file=sys.stderr)
438
+ return 1
439
+
440
+ # Reusing a slug on the same day is an ordinary accident, and continuing
441
+ # would rewrite the existing task.json below — resetting status, children,
442
+ # parent, branch and meta, which nothing else can reconstruct. Fail like
443
+ # the archived-name collision above unless the caller asks for it.
444
+ if task_dir.exists():
445
+ if not getattr(args, "force", False):
446
+ print(colored(f"Error: Task already exists: {dir_name}", Colors.RED), file=sys.stderr)
447
+ print(f"Existing task at: {_repo_relative_path(task_dir, repo_root)}", file=sys.stderr)
448
+ print(
449
+ "Use a different --slug, or pass --force to overwrite its task.json.",
450
+ file=sys.stderr,
451
+ )
452
+ return 1
453
+ print(
454
+ colored(
455
+ f"Warning: --force: overwriting task.json in existing task: {dir_name}",
456
+ Colors.YELLOW,
457
+ ),
458
+ file=sys.stderr,
459
+ )
460
+ created_dir = False
461
+ else:
462
+ task_dir.mkdir(parents=True)
463
+ created_dir = True
464
+
465
+ today = datetime.now().strftime("%Y-%m-%d")
466
+
467
+ # Record the PR target branch. Prefer the repo's actual default branch
468
+ # (origin/HEAD) so creating a task from a feature branch doesn't
469
+ # mis-stamp that feature branch as the PR target (#399 item 1). Falls
470
+ # back to the checked-out branch when the default can't be resolved
471
+ # (no remote configured, offline, etc.) — the pre-existing behavior.
472
+ # --base-branch lets the caller override both when neither is correct.
473
+ _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
474
+ current_branch = branch_out.strip() or "main"
475
+ explicit_base_branch: str | None = getattr(args, "base_branch", None)
476
+ if explicit_base_branch:
477
+ base_branch = explicit_base_branch
478
+ else:
479
+ resolved_base_branch = resolve_default_branch(repo_root)
480
+ if resolved_base_branch:
481
+ base_branch = resolved_base_branch
482
+ else:
483
+ base_branch = current_branch
484
+ print(
485
+ colored(
486
+ f"warning: could not resolve the repository's default branch "
487
+ f"(no remote configured, offline, etc.); stamping base_branch as "
488
+ f"the checked-out branch '{base_branch}'. Pass --base-branch to override.",
489
+ Colors.YELLOW,
490
+ ),
491
+ file=sys.stderr,
492
+ )
493
+
494
+ task_data = {
495
+ "id": slug,
496
+ "name": slug,
497
+ "title": args.title,
498
+ "description": description,
499
+ "status": "planning",
500
+ "dev_type": None,
501
+ "scope": None,
502
+ "package": package,
503
+ "priority": args.priority,
504
+ "creator": creator,
505
+ "assignee": assignee,
506
+ "createdAt": today,
507
+ "completedAt": None,
508
+ "branch": None,
509
+ "base_branch": base_branch,
510
+ "worktree_path": None,
511
+ "commit": None,
512
+ "pr_url": None,
513
+ "subtasks": [],
514
+ "children": [],
515
+ "parent": None,
516
+ "relatedFiles": [],
517
+ "notes": "",
518
+ "meta": meta,
519
+ }
520
+
521
+ # A directory without task.json is not a task: `list` hides it and every
522
+ # lifecycle command refuses it. Fail here rather than printing "Created
523
+ # task" and emitting the path for script chaining.
524
+ if not write_json(task_json_path, task_data):
525
+ _report_write_failure(task_json_path)
526
+ print(colored(f"No task was created: {dir_name}", Colors.RED), file=sys.stderr)
527
+ if created_dir:
528
+ try:
529
+ task_dir.rmdir()
530
+ except OSError:
531
+ print(
532
+ f"Leftover empty directory: {_repo_relative_path(task_dir, repo_root)}",
533
+ file=sys.stderr,
534
+ )
535
+ return 1
536
+
537
+ prd_path = task_dir / "prd.md"
538
+ if not prd_path.exists():
539
+ prd_path.write_text(
540
+ _default_prd_content(args.title, description),
541
+ encoding="utf-8",
542
+ )
543
+
544
+ # Create empty implement.jsonl / check.jsonl for sub-agent-capable
545
+ # platforms. They stay empty until the agent curates real entries during
546
+ # planning — a placeholder row would read as unresolved scaffolding to
547
+ # `task.py validate` and to PR preflight, so the curation instructions go
548
+ # to the console below instead of into the files. Agent-less platforms
549
+ # (Kilo / Antigravity / Devin) skip this — they load specs via the
550
+ # trellis-before-dev skill instead of JSONL.
551
+ created_jsonl = False
552
+ if _has_subagent_platform(repo_root):
553
+ for jsonl_name in ("implement.jsonl", "check.jsonl"):
554
+ jsonl_path = task_dir / jsonl_name
555
+ if not jsonl_path.exists():
556
+ jsonl_path.write_text("", encoding="utf-8")
557
+ created_jsonl = True
558
+
559
+ # Establish the bidirectional link. Both sides were validated above, so a
560
+ # failure here is a write failure, not a bad argument.
561
+ if parent_dir is not None and parent_data is not None:
562
+ parent_json_path = parent_dir / FILE_TASK_JSON
563
+
564
+ # Add child to parent's children list
565
+ parent_children = _ensure_children_list(parent_data)
566
+ if dir_name not in parent_children:
567
+ parent_children.append(dir_name)
568
+ parent_data["children"] = parent_children
569
+ if not write_json(parent_json_path, parent_data):
570
+ _report_write_failure(parent_json_path)
571
+ print(
572
+ f"The task exists at {_repo_relative_path(task_dir, repo_root)} but is NOT "
573
+ f"linked to {parent_dir.name}. Re-link with: task.py add-subtask "
574
+ f"{parent_dir.name} {dir_name}",
575
+ file=sys.stderr,
576
+ )
577
+ return 1
578
+
579
+ # Set parent in child's task.json
580
+ task_data["parent"] = parent_dir.name
581
+ if not write_json(task_json_path, task_data):
582
+ _report_write_failure(task_json_path)
583
+ print(
584
+ f"Link is half-written: {parent_dir.name} now lists '{dir_name}' as a child, "
585
+ f"but the new task does not record its parent.",
586
+ file=sys.stderr,
587
+ )
588
+ return 1
589
+
590
+ print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
591
+
592
+ # Auto-activate the new task so the per-turn breadcrumb fires planning
593
+ # state. Best-effort: gracefully degrade if no session identity (CLI run
594
+ # outside an AI session) — the task is still created, the user can run
595
+ # task.py start later. Pointer is session-scoped so this never affects
596
+ # other AI sessions.
597
+ if getattr(args, "no_start", False):
598
+ print(
599
+ colored(
600
+ "Skipped session activation (--no-start); run task.py start when ready.",
601
+ Colors.YELLOW,
602
+ ),
603
+ file=sys.stderr,
604
+ )
605
+ else:
606
+ try:
607
+ from .active_task import resolve_context_key, set_active_task
608
+ except Exception as exc:
609
+ print(
610
+ colored(f"Warning: session activation unavailable (import failed: {exc})", Colors.YELLOW),
611
+ file=sys.stderr,
612
+ )
613
+ else:
614
+ try:
615
+ context_key = resolve_context_key()
616
+ except Exception as exc:
617
+ print(
618
+ colored(f"Warning: session activation failed (context resolution: {exc})", Colors.YELLOW),
619
+ file=sys.stderr,
620
+ )
621
+ else:
622
+ # No session identity is the normal CLI-outside-an-AI-session
623
+ # case (see comment above) — stay silent, not a failure.
624
+ if context_key:
625
+ try:
626
+ rel_dir = task_dir.relative_to(repo_root).as_posix()
627
+ except ValueError:
628
+ rel_dir = str(task_dir)
629
+ try:
630
+ active = set_active_task(rel_dir, repo_root)
631
+ except Exception as exc:
632
+ print(
633
+ colored(f"Warning: session activation failed (pointer persistence: {exc})", Colors.YELLOW),
634
+ file=sys.stderr,
635
+ )
636
+ else:
637
+ if active:
638
+ print(
639
+ colored(f"Activated task for this session: {active.task_path}", Colors.GREEN),
640
+ file=sys.stderr,
641
+ )
642
+ print(f"Source: {active.source}", file=sys.stderr)
643
+ else:
644
+ print(
645
+ colored("Warning: session activation failed (no pointer returned)", Colors.YELLOW),
646
+ file=sys.stderr,
647
+ )
648
+
649
+ print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
650
+ print("", file=sys.stderr)
651
+ print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
652
+ print(" - Fill prd.md with requirements and acceptance criteria", file=sys.stderr)
653
+ print(" - Lightweight task: PRD-only is valid", file=sys.stderr)
654
+ print(" - Complex task: add design.md and implement.md before task.py start", file=sys.stderr)
655
+ if created_jsonl:
656
+ print(
657
+ " - Curate implement.jsonl / check.jsonl (created empty) as spec/research "
658
+ "manifests before task.py start when sub-agents need context:",
659
+ file=sys.stderr,
660
+ )
661
+ print(
662
+ ' one JSON object per line — {"file": "<path>", "reason": "<why>"}; '
663
+ "spec/research docs only, no code paths",
664
+ file=sys.stderr,
665
+ )
666
+ print(
667
+ " list available specs: python3 .trellis/scripts/get_context.py --mode packages",
668
+ file=sys.stderr,
669
+ )
670
+ print(" - Use /trellis:continue or phase context to decide the next step", file=sys.stderr)
671
+ print("", file=sys.stderr)
672
+
673
+ # Output relative path for script chaining
674
+ print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
675
+
676
+ run_task_hooks("after_create", task_json_path, repo_root)
677
+ return 0
678
+
679
+
680
+ # =============================================================================
681
+ # Command: rename
682
+ # =============================================================================
683
+
684
+ # task.json fields that carry the task's own slug. The directory name carries
685
+ # it too, prefixed with the creation date; everything else that names the task
686
+ # (parent / children / subtasks in *other* tasks, jsonl paths) stores the full
687
+ # directory name instead.
688
+ RENAME_IDENTITY_FIELDS: tuple[str, ...] = ("id", "name")
689
+
690
+ _JSONL_NAMES: tuple[str, ...] = ("implement.jsonl", "check.jsonl")
691
+
692
+
693
+ @dataclass
694
+ class _RenamePlan:
695
+ """Every change a rename would make, computed before anything is written.
696
+
697
+ ``--dry-run`` and the real run render their output from this one structure,
698
+ so the printed change set cannot drift from the applied one.
699
+ """
700
+
701
+ task_dir: Path
702
+ new_dir: Path
703
+ old_name: str
704
+ new_name: str
705
+ old_rel: str
706
+ new_rel: str
707
+ task_json_path: Path
708
+ task_data: dict
709
+ # (field, current value, new value) for each identity field that changes.
710
+ identity: list[tuple[str, object, str]] = field(default_factory=list)
711
+ # (jsonl path, changed line numbers, full new file text)
712
+ jsonl: list[tuple[Path, list[int], str]] = field(default_factory=list)
713
+ # (other task's task.json, rewritten data, labels of the changed refs)
714
+ backrefs: list[tuple[Path, dict, list[str]]] = field(default_factory=list)
715
+ # (file, line number) of old-name mentions elsewhere under .trellis/
716
+ reported: list[tuple[Path, int]] = field(default_factory=list)
717
+ # Task dirs whose task.json could not be read, so back-refs in them are
718
+ # neither rewritten nor ruled out.
719
+ unreadable: list[Path] = field(default_factory=list)
720
+
721
+
722
+ def _split_date_prefix(dir_name: str) -> tuple[str, str]:
723
+ """Split ``MM-DD-slug`` into ``("MM-DD", "slug")``.
724
+
725
+ Returns ``("", dir_name)`` when there is no plausible date prefix, so a
726
+ hand-made directory name without one is renamed to the bare slug.
727
+ """
728
+ m = re.match(r"^(\d{2})-(\d{2})-(.+)$", dir_name)
729
+ if m and 1 <= int(m.group(1)) <= 12 and 1 <= int(m.group(2)) <= 31:
730
+ return f"{m.group(1)}-{m.group(2)}", m.group(3)
731
+ return "", dir_name
732
+
733
+
734
+ def _validate_rename_slug(slug: str, date_prefix: str) -> str | None:
735
+ """Return the slug body to rename to, or None after reporting the refusal.
736
+
737
+ Mirrors ``create``'s ``--slug`` handling: no path separators or ``..`` (the
738
+ slug is joined into a directory name), and a pasted-in date prefix is
739
+ normalized away rather than doubled — but a rename keeps the task's
740
+ *original* creation date, not today's.
741
+ """
742
+ if not slug:
743
+ print(colored("Error: new slug is required", Colors.RED), file=sys.stderr)
744
+ return None
745
+
746
+ if "/" in slug or "\\" in slug or ".." in slug:
747
+ print(
748
+ colored(
749
+ f"Error: <new-slug> must be a plain name without path separators or '..': {slug}",
750
+ Colors.RED,
751
+ ),
752
+ file=sys.stderr,
753
+ )
754
+ return None
755
+
756
+ slug_prefix, body = _split_date_prefix(slug)
757
+ if not slug_prefix:
758
+ return slug
759
+ if slug_prefix == date_prefix:
760
+ print(
761
+ colored(
762
+ f'warning: <new-slug> should not include the MM-DD prefix; normalized to "{body}"',
763
+ Colors.YELLOW,
764
+ ),
765
+ file=sys.stderr,
766
+ )
767
+ return body
768
+ print(
769
+ colored(
770
+ f"Error: <new-slug> starts with a date prefix ({slug_prefix}-), but rename keeps "
771
+ f"the task's own creation date ({date_prefix}).",
772
+ Colors.RED,
773
+ ),
774
+ file=sys.stderr,
775
+ )
776
+ print(f"Pass only the slug body, e.g. {body}", file=sys.stderr)
777
+ return None
778
+
779
+
780
+ def _plan_jsonl_rewrites(
781
+ task_dir: Path, old_rel: str, new_rel: str
782
+ ) -> list[tuple[Path, list[int], str]]:
783
+ """Plan rewrites of context entries pointing under the old task directory.
784
+
785
+ Matching on ``<old path>/`` rather than the bare path keeps a sibling task
786
+ whose name merely starts with this one's out of the rewrite.
787
+ """
788
+ rewrites: list[tuple[Path, list[int], str]] = []
789
+ needle = f"{old_rel}/"
790
+ replacement = f"{new_rel}/"
791
+
792
+ for jsonl_name in _JSONL_NAMES:
793
+ jsonl_path = task_dir / jsonl_name
794
+ if not jsonl_path.is_file():
795
+ continue
796
+ try:
797
+ lines = jsonl_path.read_text(encoding="utf-8").splitlines(keepends=True)
798
+ except (OSError, UnicodeDecodeError):
799
+ continue
800
+
801
+ changed: list[int] = []
802
+ for index, line in enumerate(lines):
803
+ if needle in line:
804
+ lines[index] = line.replace(needle, replacement)
805
+ changed.append(index + 1)
806
+ if changed:
807
+ rewrites.append((jsonl_path, changed, "".join(lines)))
808
+
809
+ return rewrites
810
+
811
+
812
+ def _plan_backrefs(
813
+ tasks_dir: Path, task_dir: Path, old_name: str, new_name: str
814
+ ) -> tuple[list[tuple[Path, dict, list[str]]], list[Path]]:
815
+ """Plan back-reference rewrites in the other active tasks.
816
+
817
+ Returns ``(changes, unreadable)``. ``subtasks`` is the legacy spelling of
818
+ ``children`` and is still carried in older task.json files, so both lists
819
+ are rewritten.
820
+ """
821
+ changes: list[tuple[Path, dict, list[str]]] = []
822
+ unreadable: list[Path] = []
823
+
824
+ for candidate in sorted(tasks_dir.iterdir()):
825
+ if not candidate.is_dir() or candidate.name == DIR_ARCHIVE:
826
+ continue
827
+ if candidate == task_dir:
828
+ continue
829
+ json_path = candidate / FILE_TASK_JSON
830
+ if not json_path.is_file():
831
+ continue
832
+
833
+ data, _reason = read_json_checked(json_path)
834
+ if data is None:
835
+ unreadable.append(json_path)
836
+ continue
837
+
838
+ labels: list[str] = []
839
+ if data.get("parent") == old_name:
840
+ data["parent"] = new_name
841
+ labels.append("parent")
842
+ for list_field in ("children", "subtasks"):
843
+ values = data.get(list_field)
844
+ if not isinstance(values, list):
845
+ continue
846
+ for index, value in enumerate(values):
847
+ if value == old_name:
848
+ values[index] = new_name
849
+ labels.append(f"{list_field}[{index}]")
850
+
851
+ if labels:
852
+ changes.append((json_path, data, labels))
853
+
854
+ return changes, unreadable
855
+
856
+
857
+ def _plan_reported_refs(
858
+ repo_root: Path, task_dir: Path, rewritten: set[Path], old_name: str
859
+ ) -> list[tuple[Path, int]]:
860
+ """Find remaining mentions of the old task name elsewhere under .trellis/.
861
+
862
+ These are reported, never rewritten: journal entries and workflow prose
863
+ cite tasks in free text, and a blind substitution there is how a rename
864
+ turns into a diff nobody asked for. The boundary-anchored pattern keeps a
865
+ longer task name that merely contains this one out.
866
+
867
+ Runtime session pointers are excluded because they are not prose and they
868
+ *are* rewritten — see ``repoint_task_in_sessions``. Listing them here as
869
+ "not rewritten" would be a false statement about the one file whose
870
+ staleness actually breaks the next command.
871
+ """
872
+ from .active_task import _runtime_sessions_dir
873
+
874
+ pattern = re.compile(
875
+ r"(?<![0-9A-Za-z_-])" + re.escape(old_name) + r"(?![0-9A-Za-z_-])"
876
+ )
877
+ hits: list[tuple[Path, int]] = []
878
+ trellis_dir = repo_root / DIR_WORKFLOW
879
+ sessions_dir = _runtime_sessions_dir(repo_root)
880
+ if not trellis_dir.is_dir():
881
+ return hits
882
+
883
+ for path in sorted(trellis_dir.rglob("*")):
884
+ if not path.is_file() or path in rewritten:
885
+ continue
886
+ if path == task_dir or task_dir in path.parents:
887
+ continue
888
+ if path.parent == sessions_dir:
889
+ continue
890
+ if any(
891
+ part == "__pycache__" or part.startswith(".backup-")
892
+ for part in path.relative_to(trellis_dir).parts[:-1]
893
+ ):
894
+ continue
895
+ try:
896
+ text = path.read_text(encoding="utf-8")
897
+ except (OSError, UnicodeDecodeError):
898
+ continue
899
+ for lineno, line in enumerate(text.splitlines(), 1):
900
+ if pattern.search(line):
901
+ hits.append((path, lineno))
902
+
903
+ return hits
904
+
905
+
906
+ def _render_rename_plan(plan: _RenamePlan, repo_root: Path) -> list[str]:
907
+ """Render the change set. Identical for --dry-run and the real run."""
908
+ lines = [
909
+ f"rename: {plan.old_name} -> {plan.new_name}",
910
+ f" dir: {plan.old_rel} -> {plan.new_rel}",
911
+ ]
912
+ for field_name, old_value, new_value in plan.identity:
913
+ lines.append(f" task.json: {field_name}: {old_value} -> {new_value}")
914
+ for json_path, _data, labels in plan.backrefs:
915
+ rel = _repo_relative_path(json_path, repo_root)
916
+ for label in labels:
917
+ lines.append(
918
+ f" backref: {rel}: {label}: {plan.old_name} -> {plan.new_name}"
919
+ )
920
+ for jsonl_path, linenos, _text in plan.jsonl:
921
+ rel = _repo_relative_path(jsonl_path, repo_root)
922
+ for lineno in linenos:
923
+ lines.append(
924
+ f" jsonl: {rel}:{lineno}: {plan.old_rel}/ -> {plan.new_rel}/"
925
+ )
926
+ for path, lineno in plan.reported:
927
+ lines.append(
928
+ f" reported (not rewritten): {_repo_relative_path(path, repo_root)}:{lineno}"
929
+ )
930
+ lines.append(
931
+ f" sessions: any active-task pointer at {plan.old_rel} is repointed to {plan.new_rel}"
932
+ )
933
+ return lines
934
+
935
+
936
+ def _rename_interrupted(plan: _RenamePlan) -> None:
937
+ """Explain how to finish a rename that stopped part-way through."""
938
+ print(
939
+ f"The task is still at {plan.old_rel} and was NOT moved. Every step up to "
940
+ f"this one is idempotent: fix the write failure, then run the same rename "
941
+ f"again to finish it.",
942
+ file=sys.stderr,
943
+ )
944
+
945
+
946
+ def _apply_rename(plan: _RenamePlan, repo_root: Path) -> int:
947
+ """Write the planned change set.
948
+
949
+ The directory move goes last on purpose. Everything before it is an
950
+ in-place edit that re-running the identical command recomputes as
951
+ already-done, so an interruption leaves a tree that the same command
952
+ finishes; a move-first ordering would instead leave the old name
953
+ unresolvable and the remaining edits to be made by hand.
954
+ """
955
+ if plan.identity:
956
+ data = dict(plan.task_data)
957
+ for field_name, _old_value, new_value in plan.identity:
958
+ data[field_name] = new_value
959
+ if not write_json(plan.task_json_path, data):
960
+ _report_write_failure(plan.task_json_path)
961
+ print("Nothing was renamed.", file=sys.stderr)
962
+ return 1
963
+
964
+ for jsonl_path, _linenos, text in plan.jsonl:
965
+ try:
966
+ jsonl_path.write_text(text, encoding="utf-8")
967
+ except OSError as exc:
968
+ print(
969
+ colored(f"Error: Failed to write {jsonl_path}: {exc}", Colors.RED),
970
+ file=sys.stderr,
971
+ )
972
+ _rename_interrupted(plan)
973
+ return 1
974
+
975
+ for json_path, data, _labels in plan.backrefs:
976
+ if not write_json(json_path, data):
977
+ _report_write_failure(json_path)
978
+ _rename_interrupted(plan)
979
+ return 1
980
+
981
+ try:
982
+ plan.task_dir.rename(plan.new_dir)
983
+ except OSError as exc:
984
+ print(
985
+ colored(f"Error: Failed to move {plan.old_rel} -> {plan.new_rel}: {exc}", Colors.RED),
986
+ file=sys.stderr,
987
+ )
988
+ _rename_interrupted(plan)
989
+ return 1
990
+
991
+ # Last, and after the move: a session pointing at the old path would now
992
+ # resolve to a missing directory, so `current` would report the task stale
993
+ # and the context hook would inject nothing until the user ran `start`
994
+ # again. Archive clears these pointers because the task is leaving; rename
995
+ # moves them, because the task is still the one being worked on.
996
+ from .active_task import repoint_task_in_sessions
997
+
998
+ repoint_task_in_sessions(str(plan.task_dir), str(plan.new_dir), repo_root)
999
+
1000
+ return 0
1001
+
1002
+
1003
+ def cmd_rename(args: argparse.Namespace) -> int:
1004
+ """Rename a task and every reference to it."""
1005
+ repo_root = get_repo_root()
1006
+ tasks_dir = get_tasks_dir(repo_root)
1007
+
1008
+ task_dir = resolve_task_dir(args.name, repo_root)
1009
+ if task_dir is None or not task_dir.is_dir():
1010
+ if task_dir is not None:
1011
+ print(colored(f"Error: Task not found: {args.name}", Colors.RED), file=sys.stderr)
1012
+ return 1
1013
+
1014
+ # Narrower than resolve_task_dir's containment: an archived task has left
1015
+ # the active set, so its back-references are no longer maintained here.
1016
+ if not is_within_tasks_dir(task_dir, repo_root):
1017
+ print(
1018
+ colored(
1019
+ f"Error: refusing to rename '{args.name}': "
1020
+ f"{task_dir} is not an active task under {tasks_dir}",
1021
+ Colors.RED,
1022
+ ),
1023
+ file=sys.stderr,
1024
+ )
1025
+ return 1
1026
+
1027
+ old_name = task_dir.name
1028
+ date_prefix, _old_slug = _split_date_prefix(old_name)
1029
+
1030
+ slug = _validate_rename_slug(args.new_slug, date_prefix)
1031
+ if slug is None:
1032
+ return 1
1033
+
1034
+ new_name = f"{date_prefix}-{slug}" if date_prefix else slug
1035
+ if new_name == old_name:
1036
+ print(
1037
+ colored(f"Error: '{new_name}' is the task's current name", Colors.RED),
1038
+ file=sys.stderr,
1039
+ )
1040
+ return 1
1041
+
1042
+ new_dir = task_dir.parent / new_name
1043
+ if new_dir.exists():
1044
+ print(
1045
+ colored(f"Error: Task already exists: {new_name}", Colors.RED),
1046
+ file=sys.stderr,
1047
+ )
1048
+ print(f"Existing task at: {_repo_relative_path(new_dir, repo_root)}", file=sys.stderr)
1049
+ print("Pick a different slug.", file=sys.stderr)
1050
+ return 1
1051
+
1052
+ archived_task_dir = _find_archived_task_by_dir_name(tasks_dir, new_name)
1053
+ if archived_task_dir:
1054
+ print(
1055
+ colored(f"Error: Task already archived: {new_name}", Colors.RED),
1056
+ file=sys.stderr,
1057
+ )
1058
+ print(f"Archived at: {_repo_relative_path(archived_task_dir, repo_root)}", file=sys.stderr)
1059
+ print("Pick a different slug.", file=sys.stderr)
1060
+ return 1
1061
+
1062
+ task_json_path = task_dir / FILE_TASK_JSON
1063
+ if not task_json_path.is_file():
1064
+ print(
1065
+ colored(f"Error: task.json not found at {task_dir}", Colors.RED),
1066
+ file=sys.stderr,
1067
+ )
1068
+ return 1
1069
+
1070
+ task_data, reason = read_json_checked(task_json_path)
1071
+ if task_data is None:
1072
+ _report_read_failure(task_json_path, reason)
1073
+ print("Nothing was renamed.", file=sys.stderr)
1074
+ return 1
1075
+
1076
+ plan = _RenamePlan(
1077
+ task_dir=task_dir,
1078
+ new_dir=new_dir,
1079
+ old_name=old_name,
1080
+ new_name=new_name,
1081
+ old_rel=_repo_relative_path(task_dir, repo_root),
1082
+ new_rel=_repo_relative_path(new_dir, repo_root),
1083
+ task_json_path=task_json_path,
1084
+ task_data=task_data,
1085
+ )
1086
+ plan.identity = [
1087
+ (name, task_data.get(name), slug)
1088
+ for name in RENAME_IDENTITY_FIELDS
1089
+ if task_data.get(name) != slug
1090
+ ]
1091
+ plan.jsonl = _plan_jsonl_rewrites(task_dir, plan.old_rel, plan.new_rel)
1092
+ plan.backrefs, plan.unreadable = _plan_backrefs(
1093
+ tasks_dir, task_dir, old_name, new_name
1094
+ )
1095
+ rewritten = {path for path, _linenos, _text in plan.jsonl}
1096
+ rewritten.update(path for path, _data, _labels in plan.backrefs)
1097
+ plan.reported = _plan_reported_refs(repo_root, task_dir, rewritten, old_name)
1098
+
1099
+ for line in _render_rename_plan(plan, repo_root):
1100
+ print(line)
1101
+
1102
+ for json_path in plan.unreadable:
1103
+ print(
1104
+ colored(
1105
+ f"Warning: {_repo_relative_path(json_path, repo_root)} could not be read; "
1106
+ "any back-reference in it is left as-is.",
1107
+ Colors.YELLOW,
1108
+ ),
1109
+ file=sys.stderr,
1110
+ )
1111
+
1112
+ if getattr(args, "dry_run", False):
1113
+ print(colored("Dry run: nothing was written.", Colors.YELLOW), file=sys.stderr)
1114
+ return 0
1115
+
1116
+ rc = _apply_rename(plan, repo_root)
1117
+ if rc != 0:
1118
+ return rc
1119
+
1120
+ print(colored(f"Renamed: {old_name} -> {new_name}", Colors.GREEN), file=sys.stderr)
1121
+ if plan.reported:
1122
+ print(
1123
+ colored(
1124
+ f"{len(plan.reported)} reference(s) elsewhere under {DIR_WORKFLOW}/ "
1125
+ "still name the old task; they are listed above and were not rewritten.",
1126
+ Colors.YELLOW,
1127
+ ),
1128
+ file=sys.stderr,
1129
+ )
1130
+ return 0
1131
+
1132
+
1133
+ # =============================================================================
1134
+ # Command: archive
1135
+ # =============================================================================
1136
+
1137
+ def _task_branch_field(data: dict, key: str) -> str:
1138
+ """Read a branch field as a trimmed string ("" when unset or not a string)."""
1139
+ value = data.get(key)
1140
+ return strip_blank(value) if isinstance(value, str) else ""
1141
+
1142
+
1143
+ def _validate_branch_metadata(
1144
+ data: dict,
1145
+ task_name: str,
1146
+ repo_root: Path,
1147
+ skip: bool,
1148
+ ) -> bool:
1149
+ """Check branch metadata before the task leaves the active tree.
1150
+
1151
+ Returns False when archiving must stop. Archive is the last gate that sees
1152
+ a task, so metadata nobody can reconstruct afterwards is refused here
1153
+ rather than repaired by hand later (#399 follow-up).
1154
+
1155
+ "PR-backed" is deliberately pragmatic: a task carrying a base_branch in a
1156
+ repo that has a remote was created expecting a PR, so a missing `branch`
1157
+ means the metadata was never recorded — not that the work had no branch.
1158
+ Local-only repos and tasks without a base_branch are left alone.
1159
+
1160
+ A recorded branch that no longer exists locally stays a warning: after a
1161
+ merge the feature branch is normally deleted, and refusing to archive then
1162
+ would be backwards.
1163
+ """
1164
+ branch = _task_branch_field(data, "branch")
1165
+ base_branch = _task_branch_field(data, "base_branch")
1166
+ task_py = f"python3 {DIR_WORKFLOW}/scripts/task.py"
1167
+
1168
+ if branch and not branch_exists_locally(branch, repo_root):
1169
+ print(
1170
+ colored(
1171
+ f"Warning: recorded branch '{branch}' no longer exists locally "
1172
+ "(likely merged and deleted).",
1173
+ Colors.YELLOW,
1174
+ ),
1175
+ file=sys.stderr,
1176
+ )
1177
+
1178
+ if skip:
1179
+ return True
1180
+
1181
+ if branch and base_branch and branch == base_branch:
1182
+ print(
1183
+ colored(
1184
+ f"Error: refusing to archive '{task_name}': branch and base_branch "
1185
+ f"are both '{branch}'. A PR cannot target its own branch, so this "
1186
+ "metadata cannot describe the work that was merged.",
1187
+ Colors.RED,
1188
+ ),
1189
+ file=sys.stderr,
1190
+ )
1191
+ print("Repair whichever field is wrong:", file=sys.stderr)
1192
+ print(f" {task_py} set-branch {task_name} <feature-branch>", file=sys.stderr)
1193
+ print(f" {task_py} set-base-branch {task_name} <target-branch>", file=sys.stderr)
1194
+ print(
1195
+ f" {task_py} archive {task_name} --skip-branch-validation"
1196
+ " # only if this task was never PR-backed",
1197
+ file=sys.stderr,
1198
+ )
1199
+ return False
1200
+
1201
+ if not branch and base_branch and has_git_remote(repo_root):
1202
+ print(
1203
+ colored(
1204
+ f"Error: refusing to archive '{task_name}': no branch is recorded, "
1205
+ f"but the task targets base_branch '{base_branch}' in a repo with a "
1206
+ "remote — the branch it was built on was never written down.",
1207
+ Colors.RED,
1208
+ ),
1209
+ file=sys.stderr,
1210
+ )
1211
+ print("Repair with:", file=sys.stderr)
1212
+ print(f" {task_py} set-branch {task_name} <branch>", file=sys.stderr)
1213
+ print(
1214
+ f" {task_py} archive {task_name} --skip-branch-validation"
1215
+ " # only if the work landed without a branch of its own",
1216
+ file=sys.stderr,
1217
+ )
1218
+ return False
1219
+
1220
+ return True
1221
+
1222
+
1223
+ def cmd_archive(args: argparse.Namespace) -> int:
1224
+ """Archive completed task."""
1225
+ repo_root = get_repo_root()
1226
+ task_name = args.name
1227
+
1228
+ if not task_name:
1229
+ print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
1230
+ return 1
1231
+
1232
+ tasks_dir = get_tasks_dir(repo_root)
1233
+
1234
+ # Resolve task directory (supports task name, relative path, or absolute path)
1235
+ task_dir = resolve_task_dir(task_name, repo_root)
1236
+
1237
+ if task_dir is None or not task_dir.is_dir():
1238
+ if task_dir is None:
1239
+ # resolve_task_dir already reported why; keep the archive-specific
1240
+ # refusal so it reads the same as the is_within_tasks_dir guard.
1241
+ print(colored(
1242
+ f"Error: refusing to archive '{task_name}': "
1243
+ f"it does not resolve to a task under {tasks_dir}",
1244
+ Colors.RED), file=sys.stderr)
1245
+ else:
1246
+ print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
1247
+ print("Active tasks:", file=sys.stderr)
1248
+ # Import lazily to avoid circular dependency
1249
+ from .tasks import iter_active_tasks
1250
+ for t in iter_active_tasks(tasks_dir):
1251
+ print(f" - {t.dir_name}/", file=sys.stderr)
1252
+ return 1
1253
+
1254
+ # Refuse to archive anything that isn't a real task directly under
1255
+ # .trellis/tasks/. resolve_task_dir keeps the target inside the tasks dir;
1256
+ # this narrows it further to a direct child, so an already-archived task
1257
+ # (.trellis/tasks/archive/<month>/<task>) is not archived a second time.
1258
+ if not is_within_tasks_dir(task_dir, repo_root):
1259
+ print(colored(
1260
+ f"Error: refusing to archive '{task_name}': "
1261
+ f"{task_dir} is not a task under {tasks_dir}",
1262
+ Colors.RED), file=sys.stderr)
1263
+ return 1
1264
+
1265
+ # Check the destination before anything below mutates task state. The
1266
+ # mover refuses a collision too, but by then this command has already
1267
+ # marked the task completed, re-parented its children and cleared the
1268
+ # sessions pointing at it — all of which would have to be undone by hand.
1269
+ archive_dest_check = archive_destination_for(task_dir)
1270
+ if archive_dest_check.exists():
1271
+ print(colored(
1272
+ f"Error: refusing to archive '{task_name}': "
1273
+ f"archive destination already exists: "
1274
+ f"{_repo_relative_path(archive_dest_check, repo_root)}",
1275
+ Colors.RED), file=sys.stderr)
1276
+ print(f"Task remains at: {_repo_relative_path(task_dir, repo_root)}", file=sys.stderr)
1277
+ print("Move or rename the existing archived task, then retry.", file=sys.stderr)
1278
+ return 1
1279
+
1280
+ dir_name = task_dir.name
1281
+ task_json_path = task_dir / FILE_TASK_JSON
1282
+
1283
+ # Update status before archiving
1284
+ today = datetime.now().strftime("%Y-%m-%d")
1285
+ # Names of child task dirs whose task.json gets modified below; passed
1286
+ # into safe_archive_paths_to_add so they're staged in this commit.
1287
+ modified_children: list[str] = []
1288
+ if task_json_path.is_file():
1289
+ data, read_reason = read_json_checked(task_json_path)
1290
+ if data is None:
1291
+ # Archiving is still the right outcome for a task whose task.json
1292
+ # is broken — but say so, or the missing "completed" status looks
1293
+ # like the archive silently did half its job.
1294
+ problem, _ = describe_json_read_failure(task_json_path, read_reason)
1295
+ print(
1296
+ colored(
1297
+ f"Warning: {problem}; archiving without updating status/children.",
1298
+ Colors.YELLOW,
1299
+ ),
1300
+ file=sys.stderr,
1301
+ )
1302
+ else:
1303
+ # Before any mutation: branch metadata is unrecoverable once the
1304
+ # task leaves the active tree. Stale branches only warn.
1305
+ if not _validate_branch_metadata(
1306
+ data,
1307
+ task_name,
1308
+ repo_root,
1309
+ getattr(args, "skip_branch_validation", False),
1310
+ ):
1311
+ print(
1312
+ f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged.",
1313
+ file=sys.stderr,
1314
+ )
1315
+ return 1
1316
+
1317
+ data["status"] = "completed"
1318
+ data["completedAt"] = today
1319
+ if not write_json(task_json_path, data):
1320
+ _report_write_failure(task_json_path)
1321
+ print(
1322
+ f"Not archived: {_repo_relative_path(task_dir, repo_root)} is unchanged. "
1323
+ "Archiving a task still marked in progress would hide it from `list` "
1324
+ "with the wrong status.",
1325
+ file=sys.stderr,
1326
+ )
1327
+ return 1
1328
+
1329
+ # Handle subtask relationships on archive.
1330
+ # Keep this task in its parent's children list so progress
1331
+ # counters (children_progress) stay consistent — children
1332
+ # missing from the active set are treated as completed.
1333
+ task_children = data.get("children", [])
1334
+
1335
+ # If this is a parent, clear parent field in all children
1336
+ if task_children:
1337
+ for child_name in task_children:
1338
+ child_dir_path = find_task_by_name(child_name, tasks_dir)
1339
+ if child_dir_path:
1340
+ child_json = child_dir_path / FILE_TASK_JSON
1341
+ if child_json.is_file():
1342
+ child_data, child_reason = read_json_checked(child_json)
1343
+ if child_data is None:
1344
+ problem, _ = describe_json_read_failure(child_json, child_reason)
1345
+ print(
1346
+ colored(
1347
+ f"Warning: {problem}; child '{child_dir_path.name}' "
1348
+ "keeps its parent reference.",
1349
+ Colors.YELLOW,
1350
+ ),
1351
+ file=sys.stderr,
1352
+ )
1353
+ continue
1354
+ child_data["parent"] = None
1355
+ if not write_json(child_json, child_data):
1356
+ # Stop before the move: a child pointing at a
1357
+ # parent that has left .trellis/tasks/ is a
1358
+ # dangling reference nothing repairs later.
1359
+ # Retrying is safe — every step so far is
1360
+ # idempotent.
1361
+ _report_write_failure(child_json)
1362
+ print(
1363
+ f"Not archived: {_repo_relative_path(task_dir, repo_root)} is "
1364
+ f"marked completed but stays in place because child "
1365
+ f"'{child_dir_path.name}' could not be unlinked. "
1366
+ "Fix the child, then run archive again.",
1367
+ file=sys.stderr,
1368
+ )
1369
+ return 1
1370
+ modified_children.append(child_dir_path.name)
1371
+
1372
+ # Clear any session that still points at this task before the path moves.
1373
+ from .active_task import clear_task_from_sessions
1374
+ clear_task_from_sessions(str(task_dir), repo_root)
1375
+
1376
+ # Archive
1377
+ result = archive_task_complete(task_dir, repo_root)
1378
+ if "archived_to" in result:
1379
+ archive_dest = Path(result["archived_to"])
1380
+ year_month = archive_dest.parent.name
1381
+ print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
1382
+
1383
+ # Auto-commit unless --no-commit
1384
+ if not getattr(args, "no_commit", False):
1385
+ if not _auto_commit_archive(dir_name, repo_root, modified_children):
1386
+ print(
1387
+ colored(
1388
+ "Archive moved on disk, but git auto-commit did not complete. "
1389
+ "Resolve `git status` before continuing.",
1390
+ Colors.RED,
1391
+ ),
1392
+ file=sys.stderr,
1393
+ )
1394
+ return 1
1395
+
1396
+ # Return the archive path
1397
+ print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
1398
+
1399
+ # Run hooks with the archived path
1400
+ archived_json = archive_dest / FILE_TASK_JSON
1401
+ run_task_hooks("after_archive", archived_json, repo_root)
1402
+ return 0
1403
+
1404
+ return 1
1405
+
1406
+
1407
+ def _auto_commit_archive(
1408
+ task_name: str,
1409
+ repo_root: Path,
1410
+ modified_children: list[str] | None = None,
1411
+ ) -> bool:
1412
+ """Stage Trellis-owned task paths and commit after archive.
1413
+
1414
+ Scoped narrowly to the archived task's source + destination paths
1415
+ plus any child task dirs whose ``task.json`` was edited (parent →
1416
+ children relationship update). Dirty changes in OTHER active task
1417
+ dirs are NOT bundled into the archive commit.
1418
+
1419
+ If ``.gitignore`` blocks the paths, we warn + skip — we do NOT
1420
+ retry with ``git add -f``. The warning explicitly forbids
1421
+ ``git add -f .trellis/`` (which would fan out to caches/backups)
1422
+ and points users at ``session_auto_commit: false``.
1423
+
1424
+ Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when
1425
+ set to ``false``, this function returns immediately without
1426
+ touching git (the archive directory move on disk is unaffected).
1427
+ """
1428
+ if not get_session_auto_commit(repo_root):
1429
+ print(
1430
+ "[OK] session_auto_commit: false — skipping git stage/commit.",
1431
+ file=sys.stderr,
1432
+ )
1433
+ return True
1434
+
1435
+ source_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}/{task_name}"
1436
+ rc, tracked_out, _ = run_git(
1437
+ ["ls-files", "--", source_rel],
1438
+ cwd=repo_root,
1439
+ )
1440
+ source_was_tracked = rc == 0 and bool(tracked_out.strip())
1441
+
1442
+ paths = safe_archive_paths_to_add(
1443
+ repo_root, task_name=task_name, modified_children=modified_children
1444
+ )
1445
+ if not paths:
1446
+ print("[OK] No task changes to commit.", file=sys.stderr)
1447
+ return True
1448
+
1449
+ success, _, err = safe_git_add(paths, repo_root, retry_on_index_lock=True)
1450
+ if not success:
1451
+ if err and "ignored by" in err.lower():
1452
+ print_gitignore_warning(paths)
1453
+ elif stderr_indicates_index_lock(err):
1454
+ _print_index_lock_warning(
1455
+ "git add", task_name, repo_root, [*paths, source_rel]
1456
+ )
1457
+ else:
1458
+ print(
1459
+ f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
1460
+ file=sys.stderr,
1461
+ )
1462
+ return not source_was_tracked
1463
+
1464
+ # Belt-and-suspenders for the phantom-delete bug: `safe_git_add` uses
1465
+ # `git add` (no -A) which only stages additions/modifications. The
1466
+ # source task directory was moved away by `shutil.move`, so its files
1467
+ # need an explicit `git rm --cached` to stage the deletions in this
1468
+ # same commit — otherwise they sit as uncommitted "phantom deletes"
1469
+ # against HEAD until something later picks them up.
1470
+ #
1471
+ # `--ignore-unmatch` makes this a no-op when the task was never tracked
1472
+ # (e.g. archiving a task that lived only in working tree).
1473
+ rc, _, err = run_git_retry_index_lock(
1474
+ ["rm", "-r", "--cached", "--ignore-unmatch", "--", source_rel],
1475
+ cwd=repo_root,
1476
+ )
1477
+ if rc != 0 and stderr_indicates_index_lock(err):
1478
+ # Committing now would record the archived copy without the
1479
+ # source-side deletes — a half-archived tree in history.
1480
+ _print_index_lock_warning(
1481
+ "git rm --cached", task_name, repo_root, [*paths, source_rel]
1482
+ )
1483
+ return not source_was_tracked
1484
+
1485
+ rc, _, _ = run_git(
1486
+ ["diff", "--cached", "--quiet", "--", *paths, source_rel],
1487
+ cwd=repo_root,
1488
+ )
1489
+ if rc == 0:
1490
+ print("[OK] No task changes to commit.", file=sys.stderr)
1491
+ return True
1492
+
1493
+ commit_msg = f"chore(task): archive {task_name}"
1494
+ # Commit with an explicit pathspec: a bare `git commit` would sweep any
1495
+ # unrelated entries the developer had staged before archiving into the
1496
+ # chore commit (#579). `source_rel` is included so the source-side
1497
+ # deletions staged above land in the same commit.
1498
+ rc, _, err = run_git_retry_index_lock(
1499
+ ["commit", "-m", commit_msg, "--", *paths, source_rel], cwd=repo_root
1500
+ )
1501
+ if rc == 0:
1502
+ print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
1503
+ return True
1504
+ elif stderr_indicates_index_lock(err):
1505
+ _print_index_lock_warning(
1506
+ "git commit", task_name, repo_root, [*paths, source_rel]
1507
+ )
1508
+ return not source_was_tracked
1509
+ else:
1510
+ print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
1511
+ return not source_was_tracked
1512
+
1513
+
1514
+ def _print_index_lock_warning(
1515
+ action: str, task_name: str, repo_root: Path, paths: list[str]
1516
+ ) -> None:
1517
+ """Report an archive auto-commit that gave up on a held index.lock.
1518
+
1519
+ The move itself already succeeded, so the state is consistent — the task
1520
+ lives in archive/ and its changes are staged-or-not but never half of
1521
+ both in a commit. Only the commit is outstanding, which the user (or an
1522
+ agent reading the log) has to finish by hand.
1523
+
1524
+ ``paths`` are the paths the auto-commit would have staged, so the manual
1525
+ command keeps the same narrow scope — a blanket ``git add -A -- .trellis/``
1526
+ would sweep dirty changes from other active tasks into the archive commit.
1527
+ """
1528
+ lock = index_lock_path(repo_root)
1529
+ print(
1530
+ f"[WARN] {action} gave up after {INDEX_LOCK_RETRY_ATTEMPTS} attempts: "
1531
+ f"another process is holding {lock}",
1532
+ file=sys.stderr,
1533
+ )
1534
+ print(
1535
+ "[WARN] The task was moved into archive/ on disk; only the commit is pending.",
1536
+ file=sys.stderr,
1537
+ )
1538
+ print(
1539
+ "[WARN] Close whatever holds the lock (an IDE git integration, a status",
1540
+ file=sys.stderr,
1541
+ )
1542
+ print(
1543
+ f"[WARN] daemon, another session), or delete {lock} if it is stale, then",
1544
+ file=sys.stderr,
1545
+ )
1546
+ print(
1547
+ f'[WARN] commit manually: git add -A -- {" ".join(paths)} && '
1548
+ f'git commit -m "chore(task): archive {task_name}"',
1549
+ file=sys.stderr,
1550
+ )
1551
+
1552
+
1553
+ # =============================================================================
1554
+ # Command: add-subtask
1555
+ # =============================================================================
1556
+
1557
+ def cmd_add_subtask(args: argparse.Namespace) -> int:
1558
+ """Link a child task to a parent task."""
1559
+ repo_root = get_repo_root()
1560
+
1561
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
1562
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
1563
+ if parent_dir is None or child_dir is None:
1564
+ return 1
1565
+
1566
+ if not parent_dir:
1567
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
1568
+ return 1
1569
+
1570
+ if not child_dir:
1571
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
1572
+ return 1
1573
+
1574
+ parent_json_path = parent_dir / FILE_TASK_JSON
1575
+ child_json_path = child_dir / FILE_TASK_JSON
1576
+
1577
+ if not parent_json_path.is_file():
1578
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
1579
+ return 1
1580
+
1581
+ if not child_json_path.is_file():
1582
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
1583
+ return 1
1584
+
1585
+ parent_data, parent_reason = read_json_checked(parent_json_path)
1586
+ if parent_data is None:
1587
+ _report_read_failure(parent_json_path, parent_reason)
1588
+ return 1
1589
+ child_data, child_reason = read_json_checked(child_json_path)
1590
+ if child_data is None:
1591
+ _report_read_failure(child_json_path, child_reason)
1592
+ return 1
1593
+
1594
+ # Check if child already has a parent
1595
+ existing_parent = child_data.get("parent")
1596
+ if existing_parent:
1597
+ print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
1598
+ return 1
1599
+
1600
+ # Add child to parent's children list
1601
+ parent_children = _ensure_children_list(parent_data)
1602
+ child_dir_name = child_dir.name
1603
+ if child_dir_name not in parent_children:
1604
+ parent_children.append(child_dir_name)
1605
+ parent_data["children"] = parent_children
1606
+
1607
+ # Set parent in child's task.json
1608
+ child_data["parent"] = parent_dir.name
1609
+
1610
+ # Write both. A link is two files; if the second write fails silently the
1611
+ # two sides disagree with no error, so check each and name the side that
1612
+ # did land so the user knows what to undo.
1613
+ if not write_json(parent_json_path, parent_data):
1614
+ print(colored(f"Error: Failed to write parent task.json: {parent_json_path}", Colors.RED), file=sys.stderr)
1615
+ print("No link was written.", file=sys.stderr)
1616
+ return 1
1617
+ if not write_json(child_json_path, child_data):
1618
+ print(colored(f"Error: Failed to write child task.json: {child_json_path}", Colors.RED), file=sys.stderr)
1619
+ print(
1620
+ f"Link is half-written: {parent_json_path} now lists "
1621
+ f"'{child_dir.name}' as a child, but the child does not record the parent.",
1622
+ file=sys.stderr,
1623
+ )
1624
+ return 1
1625
+
1626
+ print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
1627
+ return 0
1628
+
1629
+
1630
+ # =============================================================================
1631
+ # Command: remove-subtask
1632
+ # =============================================================================
1633
+
1634
+ def cmd_remove_subtask(args: argparse.Namespace) -> int:
1635
+ """Unlink a child task from a parent task."""
1636
+ repo_root = get_repo_root()
1637
+
1638
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
1639
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
1640
+ if parent_dir is None or child_dir is None:
1641
+ return 1
1642
+
1643
+ if not parent_dir:
1644
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
1645
+ return 1
1646
+
1647
+ if not child_dir:
1648
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
1649
+ return 1
1650
+
1651
+ parent_json_path = parent_dir / FILE_TASK_JSON
1652
+ child_json_path = child_dir / FILE_TASK_JSON
1653
+
1654
+ if not parent_json_path.is_file():
1655
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
1656
+ return 1
1657
+
1658
+ if not child_json_path.is_file():
1659
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
1660
+ return 1
1661
+
1662
+ parent_data, parent_reason = read_json_checked(parent_json_path)
1663
+ if parent_data is None:
1664
+ _report_read_failure(parent_json_path, parent_reason)
1665
+ return 1
1666
+ child_data, child_reason = read_json_checked(child_json_path)
1667
+ if child_data is None:
1668
+ _report_read_failure(child_json_path, child_reason)
1669
+ return 1
1670
+
1671
+ # Remove child from parent's children list
1672
+ parent_children = _ensure_children_list(parent_data)
1673
+ child_dir_name = child_dir.name
1674
+ if child_dir_name in parent_children:
1675
+ parent_children.remove(child_dir_name)
1676
+ parent_data["children"] = parent_children
1677
+
1678
+ # Clear parent in child's task.json
1679
+ child_data["parent"] = None
1680
+
1681
+ # Write both — see cmd_add_subtask: an unchecked second write leaves the
1682
+ # two sides disagreeing with no error.
1683
+ if not write_json(parent_json_path, parent_data):
1684
+ print(colored(f"Error: Failed to write parent task.json: {parent_json_path}", Colors.RED), file=sys.stderr)
1685
+ print("Nothing was unlinked.", file=sys.stderr)
1686
+ return 1
1687
+ if not write_json(child_json_path, child_data):
1688
+ print(colored(f"Error: Failed to write child task.json: {child_json_path}", Colors.RED), file=sys.stderr)
1689
+ print(
1690
+ f"Unlink is half-written: {parent_json_path} no longer lists "
1691
+ f"'{child_dir.name}', but the child still records the parent.",
1692
+ file=sys.stderr,
1693
+ )
1694
+ return 1
1695
+
1696
+ print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
1697
+ return 0
1698
+
1699
+
1700
+ # =============================================================================
1701
+ # Command: set-branch
1702
+ # =============================================================================
1703
+
1704
+ def cmd_set_branch(args: argparse.Namespace) -> int:
1705
+ """Set git branch for task."""
1706
+ repo_root = get_repo_root()
1707
+ target_dir = resolve_task_dir(args.dir, repo_root)
1708
+ if target_dir is None:
1709
+ return 1
1710
+ branch = args.branch
1711
+
1712
+ if not branch:
1713
+ print(colored("Error: Missing arguments", Colors.RED))
1714
+ print("Usage: python3 task.py set-branch <task-dir> <branch-name>")
1715
+ return 1
1716
+
1717
+ if not target_dir:
1718
+ # target_dir is None here, so it must not appear in the message. This
1719
+ # is also the branch a ref pointing outside the repo lands in.
1720
+ print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
1721
+ return 1
1722
+
1723
+ task_json = target_dir / FILE_TASK_JSON
1724
+ if not task_json.is_file():
1725
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1726
+ return 1
1727
+
1728
+ data, reason = read_json_checked(task_json)
1729
+ if data is None:
1730
+ _report_read_failure(task_json, reason)
1731
+ return 1
1732
+
1733
+ data["branch"] = branch
1734
+ if not write_json(task_json, data):
1735
+ _report_write_failure(task_json)
1736
+ return 1
1737
+
1738
+ print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
1739
+ return 0
1740
+
1741
+
1742
+ # =============================================================================
1743
+ # Command: set-base-branch
1744
+ # =============================================================================
1745
+
1746
+ def cmd_set_base_branch(args: argparse.Namespace) -> int:
1747
+ """Set the base branch (PR target) for task."""
1748
+ repo_root = get_repo_root()
1749
+ target_dir = resolve_task_dir(args.dir, repo_root)
1750
+ if target_dir is None:
1751
+ return 1
1752
+ base_branch = args.base_branch
1753
+
1754
+ if not base_branch:
1755
+ print(colored("Error: Missing arguments", Colors.RED))
1756
+ print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>")
1757
+ print("Example: python3 task.py set-base-branch <dir> develop")
1758
+ print()
1759
+ print("This sets the target branch for PR (the branch your feature will merge into).")
1760
+ return 1
1761
+
1762
+ if not target_dir:
1763
+ # target_dir is None here, so it must not appear in the message. This
1764
+ # is also the branch a ref pointing outside the repo lands in.
1765
+ print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
1766
+ return 1
1767
+
1768
+ task_json = target_dir / FILE_TASK_JSON
1769
+ if not task_json.is_file():
1770
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1771
+ return 1
1772
+
1773
+ data, reason = read_json_checked(task_json)
1774
+ if data is None:
1775
+ _report_read_failure(task_json, reason)
1776
+ return 1
1777
+
1778
+ data["base_branch"] = base_branch
1779
+ if not write_json(task_json, data):
1780
+ _report_write_failure(task_json)
1781
+ return 1
1782
+
1783
+ print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
1784
+ print(f" PR will target: {base_branch}")
1785
+ return 0
1786
+
1787
+
1788
+ # =============================================================================
1789
+ # Command: set-scope
1790
+ # =============================================================================
1791
+
1792
+ def cmd_set_scope(args: argparse.Namespace) -> int:
1793
+ """Set scope for PR title."""
1794
+ repo_root = get_repo_root()
1795
+ target_dir = resolve_task_dir(args.dir, repo_root)
1796
+ if target_dir is None:
1797
+ return 1
1798
+ scope = args.scope
1799
+
1800
+ if not scope:
1801
+ print(colored("Error: Missing arguments", Colors.RED))
1802
+ print("Usage: python3 task.py set-scope <task-dir> <scope>")
1803
+ return 1
1804
+
1805
+ if not target_dir:
1806
+ # target_dir is None here, so it must not appear in the message. This
1807
+ # is also the branch a ref pointing outside the repo lands in.
1808
+ print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
1809
+ return 1
1810
+
1811
+ task_json = target_dir / FILE_TASK_JSON
1812
+ if not task_json.is_file():
1813
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1814
+ return 1
1815
+
1816
+ data, reason = read_json_checked(task_json)
1817
+ if data is None:
1818
+ _report_read_failure(task_json, reason)
1819
+ return 1
1820
+
1821
+ data["scope"] = scope
1822
+ if not write_json(task_json, data):
1823
+ _report_write_failure(task_json)
1824
+ return 1
1825
+
1826
+ print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
1827
+ return 0
1828
+
1829
+
1830
+ # =============================================================================
1831
+ # Command: set-meta
1832
+ # =============================================================================
1833
+
1834
+ def cmd_set_meta(args: argparse.Namespace) -> int:
1835
+ """Set/overwrite one metadata key on an existing task."""
1836
+ repo_root = get_repo_root()
1837
+ target_dir = resolve_task_dir(args.dir, repo_root)
1838
+ if target_dir is None:
1839
+ return 1
1840
+ key = args.key
1841
+ value = args.value
1842
+
1843
+ if not key:
1844
+ print(colored("Error: Missing arguments", Colors.RED))
1845
+ print("Usage: python3 task.py set-meta <task-dir> <key> <value>")
1846
+ return 1
1847
+
1848
+ if not target_dir:
1849
+ # target_dir is None here, so it must not appear in the message. This
1850
+ # is also the branch a ref pointing outside the repo lands in.
1851
+ print(colored(f"Error: Task not found: {args.dir}", Colors.RED))
1852
+ return 1
1853
+
1854
+ task_json = target_dir / FILE_TASK_JSON
1855
+ if not task_json.is_file():
1856
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
1857
+ return 1
1858
+
1859
+ data, reason = read_json_checked(task_json)
1860
+ if data is None:
1861
+ _report_read_failure(task_json, reason)
1862
+ return 1
1863
+
1864
+ meta = data.get("meta")
1865
+ if not isinstance(meta, dict):
1866
+ meta = {}
1867
+ meta[key] = value
1868
+ data["meta"] = meta
1869
+ if not write_json(task_json, data):
1870
+ _report_write_failure(task_json)
1871
+ return 1
1872
+
1873
+ print(colored(f"✓ Meta set: {key} = {value}", Colors.GREEN))
1874
+ return 0