@chinhae/atw-cli 0.1.0

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