@mudssky/trellis 0.6.16-mudssky.5

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