@mindfoldhq/trellis 0.3.10 → 0.4.0-beta.10

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 (304) hide show
  1. package/README.md +19 -5
  2. package/dist/cli/index.js +5 -0
  3. package/dist/cli/index.js.map +1 -1
  4. package/dist/commands/init.d.ts +4 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +240 -43
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/update.d.ts.map +1 -1
  9. package/dist/commands/update.js +206 -47
  10. package/dist/commands/update.js.map +1 -1
  11. package/dist/configurators/codebuddy.d.ts +11 -0
  12. package/dist/configurators/codebuddy.d.ts.map +1 -0
  13. package/dist/configurators/codebuddy.js +58 -0
  14. package/dist/configurators/codebuddy.js.map +1 -0
  15. package/dist/configurators/codex.d.ts +7 -4
  16. package/dist/configurators/codex.d.ts.map +1 -1
  17. package/dist/configurators/codex.js +40 -10
  18. package/dist/configurators/codex.js.map +1 -1
  19. package/dist/configurators/copilot.d.ts +9 -0
  20. package/dist/configurators/copilot.d.ts.map +1 -0
  21. package/dist/configurators/copilot.js +34 -0
  22. package/dist/configurators/copilot.js.map +1 -0
  23. package/dist/configurators/index.d.ts +11 -1
  24. package/dist/configurators/index.d.ts.map +1 -1
  25. package/dist/configurators/index.js +72 -4
  26. package/dist/configurators/index.js.map +1 -1
  27. package/dist/configurators/opencode.d.ts +1 -1
  28. package/dist/configurators/opencode.js +1 -1
  29. package/dist/configurators/windsurf.d.ts +8 -0
  30. package/dist/configurators/windsurf.d.ts.map +1 -0
  31. package/dist/configurators/windsurf.js +18 -0
  32. package/dist/configurators/windsurf.js.map +1 -0
  33. package/dist/configurators/workflow.d.ts +6 -2
  34. package/dist/configurators/workflow.d.ts.map +1 -1
  35. package/dist/configurators/workflow.js +90 -58
  36. package/dist/configurators/workflow.js.map +1 -1
  37. package/dist/migrations/index.d.ts +1 -0
  38. package/dist/migrations/index.d.ts.map +1 -1
  39. package/dist/migrations/index.js +2 -0
  40. package/dist/migrations/index.js.map +1 -1
  41. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  42. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  43. package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
  44. package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
  45. package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
  46. package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
  47. package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
  48. package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
  49. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  50. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  51. package/dist/templates/claude/agents/dispatch.md +1 -2
  52. package/dist/templates/claude/agents/implement.md +2 -3
  53. package/dist/templates/claude/commands/trellis/before-dev.md +29 -0
  54. package/dist/templates/claude/commands/trellis/check.md +25 -0
  55. package/dist/templates/claude/commands/trellis/create-command.md +2 -2
  56. package/dist/templates/claude/commands/trellis/onboard.md +13 -13
  57. package/dist/templates/claude/commands/trellis/parallel.md +1 -2
  58. package/dist/templates/claude/commands/trellis/record-session.md +3 -2
  59. package/dist/templates/claude/commands/trellis/start.md +8 -4
  60. package/dist/templates/claude/hooks/inject-subagent-context.py +29 -14
  61. package/dist/templates/claude/hooks/ralph-loop.py +18 -10
  62. package/dist/templates/claude/hooks/session-start.py +201 -9
  63. package/dist/templates/claude/hooks/statusline.py +211 -0
  64. package/dist/templates/claude/settings.json +4 -0
  65. package/dist/templates/codebuddy/commands/trellis/before-dev.md +29 -0
  66. package/dist/templates/codebuddy/commands/trellis/brainstorm.md +487 -0
  67. package/dist/templates/codebuddy/commands/trellis/break-loop.md +107 -0
  68. package/dist/templates/codebuddy/commands/trellis/check-cross-layer.md +153 -0
  69. package/dist/templates/codebuddy/commands/trellis/check.md +25 -0
  70. package/dist/templates/codebuddy/commands/trellis/create-command.md +154 -0
  71. package/dist/templates/codebuddy/commands/trellis/finish-work.md +143 -0
  72. package/dist/templates/codebuddy/commands/trellis/integrate-skill.md +219 -0
  73. package/dist/templates/codebuddy/commands/trellis/onboard.md +358 -0
  74. package/dist/templates/codebuddy/commands/trellis/record-session.md +61 -0
  75. package/dist/templates/codebuddy/commands/trellis/start.md +373 -0
  76. package/dist/templates/codebuddy/commands/trellis/update-spec.md +354 -0
  77. package/dist/templates/codebuddy/index.d.ts +25 -0
  78. package/dist/templates/codebuddy/index.d.ts.map +1 -0
  79. package/dist/templates/codebuddy/index.js +45 -0
  80. package/dist/templates/codebuddy/index.js.map +1 -0
  81. package/dist/templates/codex/agents/check.toml +23 -0
  82. package/dist/templates/codex/agents/implement.toml +19 -0
  83. package/dist/templates/codex/agents/research.toml +26 -0
  84. package/dist/templates/codex/codex-skills/parallel/SKILL.md +194 -0
  85. package/dist/templates/codex/config.toml +5 -0
  86. package/dist/templates/codex/hooks/session-start.py +228 -0
  87. package/dist/templates/codex/hooks.json +16 -0
  88. package/dist/templates/codex/index.d.ts +27 -5
  89. package/dist/templates/codex/index.d.ts.map +1 -1
  90. package/dist/templates/codex/index.js +60 -8
  91. package/dist/templates/codex/index.js.map +1 -1
  92. package/dist/templates/codex/skills/before-dev/SKILL.md +34 -0
  93. package/dist/templates/codex/skills/brainstorm/SKILL.md +1 -1
  94. package/dist/templates/codex/skills/break-loop/SKILL.md +1 -1
  95. package/dist/templates/codex/skills/check/SKILL.md +30 -0
  96. package/dist/templates/codex/skills/check-cross-layer/SKILL.md +1 -1
  97. package/dist/templates/codex/skills/create-command/SKILL.md +3 -3
  98. package/dist/templates/codex/skills/finish-work/SKILL.md +1 -1
  99. package/dist/templates/codex/skills/improve-ut/SKILL.md +69 -0
  100. package/dist/templates/codex/skills/integrate-skill/SKILL.md +1 -1
  101. package/dist/templates/codex/skills/onboard/SKILL.md +12 -12
  102. package/dist/templates/codex/skills/record-session/SKILL.md +4 -3
  103. package/dist/templates/codex/skills/start/SKILL.md +9 -4
  104. package/dist/templates/codex/skills/update-spec/SKILL.md +1 -1
  105. package/dist/templates/copilot/hooks/session-start.py +218 -0
  106. package/dist/templates/copilot/hooks.json +11 -0
  107. package/dist/templates/copilot/index.d.ts +23 -0
  108. package/dist/templates/copilot/index.d.ts.map +1 -0
  109. package/dist/templates/copilot/index.js +54 -0
  110. package/dist/templates/copilot/index.js.map +1 -0
  111. package/dist/templates/copilot/prompts/before-dev.prompt.md +33 -0
  112. package/dist/templates/copilot/prompts/brainstorm.prompt.md +491 -0
  113. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  114. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +157 -0
  115. package/dist/templates/copilot/prompts/check.prompt.md +29 -0
  116. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  117. package/dist/templates/copilot/prompts/finish-work.prompt.md +157 -0
  118. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  119. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  120. package/dist/templates/copilot/prompts/parallel.prompt.md +196 -0
  121. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  122. package/dist/templates/copilot/prompts/start.prompt.md +397 -0
  123. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  124. package/dist/templates/cursor/commands/trellis-before-dev.md +29 -0
  125. package/dist/templates/cursor/commands/trellis-check.md +25 -0
  126. package/dist/templates/cursor/commands/trellis-create-command.md +2 -2
  127. package/dist/templates/cursor/commands/trellis-onboard.md +13 -13
  128. package/dist/templates/cursor/commands/trellis-record-session.md +3 -2
  129. package/dist/templates/cursor/commands/trellis-start.md +7 -16
  130. package/dist/templates/extract.d.ts +36 -0
  131. package/dist/templates/extract.d.ts.map +1 -1
  132. package/dist/templates/extract.js +64 -0
  133. package/dist/templates/extract.js.map +1 -1
  134. package/dist/templates/gemini/commands/trellis/before-dev.toml +33 -0
  135. package/dist/templates/gemini/commands/trellis/check.toml +29 -0
  136. package/dist/templates/gemini/commands/trellis/create-command.toml +2 -2
  137. package/dist/templates/gemini/commands/trellis/onboard.toml +2 -2
  138. package/dist/templates/gemini/commands/trellis/record-session.toml +3 -2
  139. package/dist/templates/gemini/commands/trellis/start.toml +9 -4
  140. package/dist/templates/iflow/agents/dispatch.md +1 -2
  141. package/dist/templates/iflow/agents/implement.md +2 -3
  142. package/dist/templates/iflow/commands/trellis/before-dev.md +29 -0
  143. package/dist/templates/iflow/commands/trellis/check.md +25 -0
  144. package/dist/templates/iflow/commands/trellis/create-command.md +2 -2
  145. package/dist/templates/iflow/commands/trellis/onboard.md +13 -13
  146. package/dist/templates/iflow/commands/trellis/parallel.md +1 -2
  147. package/dist/templates/iflow/commands/trellis/record-session.md +3 -2
  148. package/dist/templates/iflow/commands/trellis/start.md +8 -4
  149. package/dist/templates/iflow/hooks/inject-subagent-context.py +29 -14
  150. package/dist/templates/iflow/hooks/ralph-loop.py +8 -1
  151. package/dist/templates/iflow/hooks/session-start.py +187 -8
  152. package/dist/templates/kilo/workflows/before-dev.md +29 -0
  153. package/dist/templates/kilo/workflows/check.md +25 -0
  154. package/dist/templates/kilo/workflows/create-command.md +2 -2
  155. package/dist/templates/kilo/workflows/onboard.md +13 -13
  156. package/dist/templates/kilo/workflows/parallel.md +1 -2
  157. package/dist/templates/kilo/workflows/record-session.md +3 -2
  158. package/dist/templates/kilo/workflows/start.md +8 -3
  159. package/dist/templates/kiro/skills/before-dev/SKILL.md +34 -0
  160. package/dist/templates/kiro/skills/brainstorm/SKILL.md +1 -1
  161. package/dist/templates/kiro/skills/break-loop/SKILL.md +1 -1
  162. package/dist/templates/kiro/skills/check/SKILL.md +30 -0
  163. package/dist/templates/kiro/skills/check-cross-layer/SKILL.md +1 -1
  164. package/dist/templates/kiro/skills/create-command/SKILL.md +3 -3
  165. package/dist/templates/kiro/skills/finish-work/SKILL.md +1 -1
  166. package/dist/templates/kiro/skills/integrate-skill/SKILL.md +1 -1
  167. package/dist/templates/kiro/skills/onboard/SKILL.md +12 -12
  168. package/dist/templates/kiro/skills/record-session/SKILL.md +4 -3
  169. package/dist/templates/kiro/skills/start/SKILL.md +9 -4
  170. package/dist/templates/kiro/skills/update-spec/SKILL.md +1 -1
  171. package/dist/templates/markdown/agents.md +4 -0
  172. package/dist/templates/markdown/spec/backend/directory-structure.md +1 -1
  173. package/dist/templates/markdown/spec/backend/script-conventions.md +93 -0
  174. package/dist/templates/markdown/workspace-index.md +2 -0
  175. package/dist/templates/opencode/agents/dispatch.md +21 -21
  176. package/dist/templates/opencode/agents/implement.md +2 -2
  177. package/dist/templates/opencode/agents/research.md +1 -2
  178. package/dist/templates/opencode/commands/trellis/before-dev.md +29 -0
  179. package/dist/templates/opencode/commands/trellis/check.md +25 -0
  180. package/dist/templates/opencode/commands/trellis/create-command.md +2 -2
  181. package/dist/templates/opencode/commands/trellis/onboard.md +13 -13
  182. package/dist/templates/opencode/commands/trellis/parallel.md +1 -2
  183. package/dist/templates/opencode/commands/trellis/record-session.md +3 -2
  184. package/dist/templates/opencode/commands/trellis/start.md +8 -3
  185. package/dist/templates/opencode/lib/trellis-context.js +42 -2
  186. package/dist/templates/opencode/{plugin → plugins}/inject-subagent-context.js +45 -18
  187. package/dist/templates/opencode/{plugin → plugins}/session-start.js +156 -28
  188. package/dist/templates/qoder/skills/before-dev/SKILL.md +34 -0
  189. package/dist/templates/qoder/skills/brainstorm/SKILL.md +1 -1
  190. package/dist/templates/qoder/skills/break-loop/SKILL.md +1 -1
  191. package/dist/templates/qoder/skills/check/SKILL.md +30 -0
  192. package/dist/templates/qoder/skills/check-cross-layer/SKILL.md +1 -1
  193. package/dist/templates/qoder/skills/create-command/SKILL.md +3 -3
  194. package/dist/templates/qoder/skills/finish-work/SKILL.md +1 -1
  195. package/dist/templates/qoder/skills/integrate-skill/SKILL.md +1 -1
  196. package/dist/templates/qoder/skills/onboard/SKILL.md +14 -14
  197. package/dist/templates/qoder/skills/record-session/SKILL.md +4 -3
  198. package/dist/templates/qoder/skills/start/SKILL.md +9 -4
  199. package/dist/templates/qoder/skills/update-spec/SKILL.md +1 -1
  200. package/dist/templates/trellis/config.yaml +20 -0
  201. package/dist/templates/trellis/index.d.ts +11 -0
  202. package/dist/templates/trellis/index.d.ts.map +1 -1
  203. package/dist/templates/trellis/index.js +22 -0
  204. package/dist/templates/trellis/index.js.map +1 -1
  205. package/dist/templates/trellis/scripts/add_session.py +111 -13
  206. package/dist/templates/trellis/scripts/common/__init__.py +2 -0
  207. package/dist/templates/trellis/scripts/common/cli_adapter.py +164 -64
  208. package/dist/templates/trellis/scripts/common/config.py +192 -0
  209. package/dist/templates/trellis/scripts/common/developer.py +2 -2
  210. package/dist/templates/trellis/scripts/common/git.py +31 -0
  211. package/dist/templates/trellis/scripts/common/git_context.py +23 -586
  212. package/dist/templates/trellis/scripts/common/io.py +37 -0
  213. package/dist/templates/trellis/scripts/common/log.py +45 -0
  214. package/dist/templates/trellis/scripts/common/packages_context.py +238 -0
  215. package/dist/templates/trellis/scripts/common/paths.py +103 -6
  216. package/dist/templates/trellis/scripts/common/phase.py +50 -49
  217. package/dist/templates/trellis/scripts/common/registry.py +41 -72
  218. package/dist/templates/trellis/scripts/common/session_context.py +562 -0
  219. package/dist/templates/trellis/scripts/common/task_context.py +410 -0
  220. package/dist/templates/trellis/scripts/common/task_queue.py +27 -98
  221. package/dist/templates/trellis/scripts/common/task_store.py +536 -0
  222. package/dist/templates/trellis/scripts/common/task_utils.py +106 -10
  223. package/dist/templates/trellis/scripts/common/tasks.py +109 -0
  224. package/dist/templates/trellis/scripts/common/types.py +112 -0
  225. package/dist/templates/trellis/scripts/create_bootstrap.py +32 -27
  226. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  227. package/dist/templates/trellis/scripts/multi_agent/_bootstrap.py +17 -0
  228. package/dist/templates/trellis/scripts/multi_agent/cleanup.py +43 -48
  229. package/dist/templates/trellis/scripts/multi_agent/create_pr.py +336 -45
  230. package/dist/templates/trellis/scripts/multi_agent/plan.py +9 -32
  231. package/dist/templates/trellis/scripts/multi_agent/start.py +142 -68
  232. package/dist/templates/trellis/scripts/multi_agent/status.py +12 -753
  233. package/dist/templates/trellis/scripts/multi_agent/status_display.py +542 -0
  234. package/dist/templates/trellis/scripts/multi_agent/status_monitor.py +225 -0
  235. package/dist/templates/trellis/scripts/task.py +51 -976
  236. package/dist/templates/trellis/scripts-shell-archive/create-bootstrap.sh +1 -1
  237. package/dist/templates/trellis/workflow.md +38 -38
  238. package/dist/templates/windsurf/index.d.ts +21 -0
  239. package/dist/templates/windsurf/index.d.ts.map +1 -0
  240. package/dist/templates/windsurf/index.js +44 -0
  241. package/dist/templates/windsurf/index.js.map +1 -0
  242. package/dist/templates/windsurf/workflows/trellis-before-dev.md +31 -0
  243. package/dist/templates/windsurf/workflows/trellis-brainstorm.md +491 -0
  244. package/dist/templates/windsurf/workflows/trellis-break-loop.md +111 -0
  245. package/dist/templates/windsurf/workflows/trellis-check-cross-layer.md +157 -0
  246. package/dist/templates/windsurf/workflows/trellis-check.md +27 -0
  247. package/dist/templates/windsurf/workflows/trellis-create-command.md +154 -0
  248. package/dist/templates/windsurf/workflows/trellis-finish-work.md +147 -0
  249. package/dist/templates/windsurf/workflows/trellis-integrate-skill.md +220 -0
  250. package/dist/templates/windsurf/workflows/trellis-onboard.md +362 -0
  251. package/dist/templates/windsurf/workflows/trellis-record-session.md +66 -0
  252. package/dist/templates/windsurf/workflows/trellis-start.md +373 -0
  253. package/dist/templates/windsurf/workflows/trellis-update-spec.md +358 -0
  254. package/dist/types/ai-tools.d.ts +15 -3
  255. package/dist/types/ai-tools.d.ts.map +1 -1
  256. package/dist/types/ai-tools.js +42 -2
  257. package/dist/types/ai-tools.js.map +1 -1
  258. package/dist/types/migration.d.ts +3 -1
  259. package/dist/types/migration.d.ts.map +1 -1
  260. package/dist/utils/project-detector.d.ts +28 -0
  261. package/dist/utils/project-detector.d.ts.map +1 -1
  262. package/dist/utils/project-detector.js +371 -0
  263. package/dist/utils/project-detector.js.map +1 -1
  264. package/dist/utils/template-fetcher.d.ts +19 -6
  265. package/dist/utils/template-fetcher.d.ts.map +1 -1
  266. package/dist/utils/template-fetcher.js +99 -17
  267. package/dist/utils/template-fetcher.js.map +1 -1
  268. package/package.json +1 -1
  269. package/dist/templates/claude/commands/trellis/before-backend-dev.md +0 -13
  270. package/dist/templates/claude/commands/trellis/before-frontend-dev.md +0 -13
  271. package/dist/templates/claude/commands/trellis/check-backend.md +0 -13
  272. package/dist/templates/claude/commands/trellis/check-frontend.md +0 -13
  273. package/dist/templates/codex/skills/before-backend-dev/SKILL.md +0 -18
  274. package/dist/templates/codex/skills/before-frontend-dev/SKILL.md +0 -18
  275. package/dist/templates/codex/skills/check-backend/SKILL.md +0 -18
  276. package/dist/templates/codex/skills/check-frontend/SKILL.md +0 -18
  277. package/dist/templates/cursor/commands/trellis-before-backend-dev.md +0 -13
  278. package/dist/templates/cursor/commands/trellis-before-frontend-dev.md +0 -13
  279. package/dist/templates/cursor/commands/trellis-check-backend.md +0 -13
  280. package/dist/templates/cursor/commands/trellis-check-frontend.md +0 -13
  281. package/dist/templates/gemini/commands/trellis/before-backend-dev.toml +0 -17
  282. package/dist/templates/gemini/commands/trellis/before-frontend-dev.toml +0 -17
  283. package/dist/templates/gemini/commands/trellis/check-backend.toml +0 -17
  284. package/dist/templates/gemini/commands/trellis/check-frontend.toml +0 -17
  285. package/dist/templates/iflow/commands/trellis/before-backend-dev.md +0 -13
  286. package/dist/templates/iflow/commands/trellis/before-frontend-dev.md +0 -13
  287. package/dist/templates/iflow/commands/trellis/check-backend.md +0 -13
  288. package/dist/templates/iflow/commands/trellis/check-frontend.md +0 -13
  289. package/dist/templates/kilo/workflows/before-backend-dev.md +0 -13
  290. package/dist/templates/kilo/workflows/before-frontend-dev.md +0 -13
  291. package/dist/templates/kilo/workflows/check-backend.md +0 -13
  292. package/dist/templates/kilo/workflows/check-frontend.md +0 -13
  293. package/dist/templates/kiro/skills/before-backend-dev/SKILL.md +0 -18
  294. package/dist/templates/kiro/skills/before-frontend-dev/SKILL.md +0 -18
  295. package/dist/templates/kiro/skills/check-backend/SKILL.md +0 -18
  296. package/dist/templates/kiro/skills/check-frontend/SKILL.md +0 -18
  297. package/dist/templates/opencode/commands/trellis/before-backend-dev.md +0 -13
  298. package/dist/templates/opencode/commands/trellis/before-frontend-dev.md +0 -13
  299. package/dist/templates/opencode/commands/trellis/check-backend.md +0 -13
  300. package/dist/templates/opencode/commands/trellis/check-frontend.md +0 -13
  301. package/dist/templates/qoder/skills/before-backend-dev/SKILL.md +0 -18
  302. package/dist/templates/qoder/skills/before-frontend-dev/SKILL.md +0 -18
  303. package/dist/templates/qoder/skills/check-backend/SKILL.md +0 -18
  304. package/dist/templates/qoder/skills/check-frontend/SKILL.md +0 -18
@@ -0,0 +1,536 @@
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_archive - Archive completed task
9
+ cmd_set_branch - Set git branch for task
10
+ cmd_set_base_branch - Set PR target branch
11
+ cmd_set_scope - Set scope for PR title
12
+ cmd_add_subtask - Link child task to parent
13
+ cmd_remove_subtask - Unlink child task from parent
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import re
20
+ import sys
21
+ from datetime import datetime
22
+ from pathlib import Path
23
+
24
+ from .config import (
25
+ get_packages,
26
+ is_monorepo,
27
+ resolve_package,
28
+ validate_package,
29
+ )
30
+ from .git import run_git
31
+ from .io import read_json, write_json
32
+ from .log import Colors, colored
33
+ from .paths import (
34
+ DIR_ARCHIVE,
35
+ DIR_TASKS,
36
+ DIR_WORKFLOW,
37
+ FILE_TASK_JSON,
38
+ clear_current_task,
39
+ generate_task_date_prefix,
40
+ get_current_task,
41
+ get_developer,
42
+ get_repo_root,
43
+ get_tasks_dir,
44
+ )
45
+ from .task_utils import (
46
+ archive_task_complete,
47
+ find_task_by_name,
48
+ resolve_task_dir,
49
+ run_task_hooks,
50
+ )
51
+
52
+
53
+ # =============================================================================
54
+ # Helper Functions
55
+ # =============================================================================
56
+
57
+ def _slugify(title: str) -> str:
58
+ """Convert title to slug (only works with ASCII)."""
59
+ result = title.lower()
60
+ result = re.sub(r"[^a-z0-9]", "-", result)
61
+ result = re.sub(r"-+", "-", result)
62
+ result = result.strip("-")
63
+ return result
64
+
65
+
66
+ def ensure_tasks_dir(repo_root: Path) -> Path:
67
+ """Ensure tasks directory exists."""
68
+ tasks_dir = get_tasks_dir(repo_root)
69
+ archive_dir = tasks_dir / "archive"
70
+
71
+ if not tasks_dir.exists():
72
+ tasks_dir.mkdir(parents=True)
73
+ print(colored(f"Created tasks directory: {tasks_dir}", Colors.GREEN), file=sys.stderr)
74
+
75
+ if not archive_dir.exists():
76
+ archive_dir.mkdir(parents=True)
77
+
78
+ return tasks_dir
79
+
80
+
81
+ # =============================================================================
82
+ # Command: create
83
+ # =============================================================================
84
+
85
+ def cmd_create(args: argparse.Namespace) -> int:
86
+ """Create a new task."""
87
+ repo_root = get_repo_root()
88
+
89
+ if not args.title:
90
+ print(colored("Error: title is required", Colors.RED), file=sys.stderr)
91
+ return 1
92
+
93
+ # Validate --package (CLI source: fail-fast)
94
+ package: str | None = getattr(args, "package", None)
95
+ if not is_monorepo(repo_root):
96
+ # Single-repo: ignore --package, no package prefix
97
+ if package:
98
+ print(colored(f"Warning: --package ignored in single-repo project", Colors.YELLOW), file=sys.stderr)
99
+ package = None
100
+ elif package:
101
+ if not validate_package(package, repo_root):
102
+ packages = get_packages(repo_root)
103
+ available = ", ".join(sorted(packages.keys())) if packages else "(none)"
104
+ print(colored(f"Error: unknown package '{package}'. Available: {available}", Colors.RED), file=sys.stderr)
105
+ return 1
106
+ else:
107
+ # Inferred: default_package → None (no task.json yet for create)
108
+ package = resolve_package(repo_root=repo_root)
109
+
110
+ # Default assignee to current developer
111
+ assignee = args.assignee
112
+ if not assignee:
113
+ assignee = get_developer(repo_root)
114
+ if not assignee:
115
+ print(colored("Error: No developer set. Run init_developer.py first or use --assignee", Colors.RED), file=sys.stderr)
116
+ return 1
117
+
118
+ ensure_tasks_dir(repo_root)
119
+
120
+ # Get current developer as creator
121
+ creator = get_developer(repo_root) or assignee
122
+
123
+ # Generate slug if not provided
124
+ slug = args.slug or _slugify(args.title)
125
+ if not slug:
126
+ print(colored("Error: could not generate slug from title", Colors.RED), file=sys.stderr)
127
+ return 1
128
+
129
+ # Create task directory with MM-DD-slug format
130
+ tasks_dir = get_tasks_dir(repo_root)
131
+ date_prefix = generate_task_date_prefix()
132
+ dir_name = f"{date_prefix}-{slug}"
133
+ task_dir = tasks_dir / dir_name
134
+ task_json_path = task_dir / FILE_TASK_JSON
135
+
136
+ if task_dir.exists():
137
+ print(colored(f"Warning: Task directory already exists: {dir_name}", Colors.YELLOW), file=sys.stderr)
138
+ else:
139
+ task_dir.mkdir(parents=True)
140
+
141
+ today = datetime.now().strftime("%Y-%m-%d")
142
+
143
+ # Record current branch as base_branch (PR target)
144
+ _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
145
+ current_branch = branch_out.strip() or "main"
146
+
147
+ task_data = {
148
+ "id": slug,
149
+ "name": slug,
150
+ "title": args.title,
151
+ "description": args.description or "",
152
+ "status": "planning",
153
+ "dev_type": None,
154
+ "scope": None,
155
+ "package": package,
156
+ "priority": args.priority,
157
+ "creator": creator,
158
+ "assignee": assignee,
159
+ "createdAt": today,
160
+ "completedAt": None,
161
+ "branch": None,
162
+ "base_branch": current_branch,
163
+ "worktree_path": None,
164
+ "current_phase": 0,
165
+ "next_action": [
166
+ {"phase": 1, "action": "brainstorm"},
167
+ {"phase": 2, "action": "research"},
168
+ {"phase": 3, "action": "implement"},
169
+ {"phase": 4, "action": "check"},
170
+ {"phase": 5, "action": "update-spec"},
171
+ {"phase": 6, "action": "record-session"},
172
+ ],
173
+ "commit": None,
174
+ "pr_url": None,
175
+ "subtasks": [],
176
+ "children": [],
177
+ "parent": None,
178
+ "relatedFiles": [],
179
+ "notes": "",
180
+ "meta": {},
181
+ }
182
+
183
+ write_json(task_json_path, task_data)
184
+
185
+ # Handle --parent: establish bidirectional link
186
+ if args.parent:
187
+ parent_dir = resolve_task_dir(args.parent, repo_root)
188
+ parent_json_path = parent_dir / FILE_TASK_JSON
189
+ if not parent_json_path.is_file():
190
+ print(colored(f"Warning: Parent task.json not found: {args.parent}", Colors.YELLOW), file=sys.stderr)
191
+ else:
192
+ parent_data = read_json(parent_json_path)
193
+ if parent_data:
194
+ # Add child to parent's children list
195
+ parent_children = parent_data.get("children", [])
196
+ if dir_name not in parent_children:
197
+ parent_children.append(dir_name)
198
+ parent_data["children"] = parent_children
199
+ write_json(parent_json_path, parent_data)
200
+
201
+ # Set parent in child's task.json
202
+ task_data["parent"] = parent_dir.name
203
+ write_json(task_json_path, task_data)
204
+
205
+ print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
206
+
207
+ print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
208
+ print("", file=sys.stderr)
209
+ print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
210
+ print(" 1. Create prd.md with requirements", file=sys.stderr)
211
+ print(" 2. Run: python3 task.py init-context <dir> <dev_type>", file=sys.stderr)
212
+ print(" 3. Run: python3 task.py start <dir>", file=sys.stderr)
213
+ print("", file=sys.stderr)
214
+
215
+ # Output relative path for script chaining
216
+ print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{dir_name}")
217
+
218
+ run_task_hooks("after_create", task_json_path, repo_root)
219
+ return 0
220
+
221
+
222
+ # =============================================================================
223
+ # Command: archive
224
+ # =============================================================================
225
+
226
+ def cmd_archive(args: argparse.Namespace) -> int:
227
+ """Archive completed task."""
228
+ repo_root = get_repo_root()
229
+ task_name = args.name
230
+
231
+ if not task_name:
232
+ print(colored("Error: Task name is required", Colors.RED), file=sys.stderr)
233
+ return 1
234
+
235
+ tasks_dir = get_tasks_dir(repo_root)
236
+
237
+ # Find task directory
238
+ task_dir = find_task_by_name(task_name, tasks_dir)
239
+
240
+ if not task_dir or not task_dir.is_dir():
241
+ print(colored(f"Error: Task not found: {task_name}", Colors.RED), file=sys.stderr)
242
+ print("Active tasks:", file=sys.stderr)
243
+ # Import lazily to avoid circular dependency
244
+ from .tasks import iter_active_tasks
245
+ for t in iter_active_tasks(tasks_dir):
246
+ print(f" - {t.dir_name}/", file=sys.stderr)
247
+ return 1
248
+
249
+ dir_name = task_dir.name
250
+ task_json_path = task_dir / FILE_TASK_JSON
251
+
252
+ # Update status before archiving
253
+ today = datetime.now().strftime("%Y-%m-%d")
254
+ if task_json_path.is_file():
255
+ data = read_json(task_json_path)
256
+ if data:
257
+ data["status"] = "completed"
258
+ data["completedAt"] = today
259
+ write_json(task_json_path, data)
260
+
261
+ # Handle subtask relationships on archive
262
+ task_parent = data.get("parent")
263
+ task_children = data.get("children", [])
264
+
265
+ # If this is a child, remove from parent's children list
266
+ if task_parent:
267
+ parent_dir = find_task_by_name(task_parent, tasks_dir)
268
+ if parent_dir:
269
+ parent_json = parent_dir / FILE_TASK_JSON
270
+ if parent_json.is_file():
271
+ parent_data = read_json(parent_json)
272
+ if parent_data:
273
+ parent_children = parent_data.get("children", [])
274
+ if dir_name in parent_children:
275
+ parent_children.remove(dir_name)
276
+ parent_data["children"] = parent_children
277
+ write_json(parent_json, parent_data)
278
+
279
+ # If this is a parent, clear parent field in all children
280
+ if task_children:
281
+ for child_name in task_children:
282
+ child_dir_path = find_task_by_name(child_name, tasks_dir)
283
+ if child_dir_path:
284
+ child_json = child_dir_path / FILE_TASK_JSON
285
+ if child_json.is_file():
286
+ child_data = read_json(child_json)
287
+ if child_data:
288
+ child_data["parent"] = None
289
+ write_json(child_json, child_data)
290
+
291
+ # Clear if current task
292
+ current = get_current_task(repo_root)
293
+ if current and dir_name in current:
294
+ clear_current_task(repo_root)
295
+
296
+ # Archive
297
+ result = archive_task_complete(task_dir, repo_root)
298
+ if "archived_to" in result:
299
+ archive_dest = Path(result["archived_to"])
300
+ year_month = archive_dest.parent.name
301
+ print(colored(f"Archived: {dir_name} -> archive/{year_month}/", Colors.GREEN), file=sys.stderr)
302
+
303
+ # Auto-commit unless --no-commit
304
+ if not getattr(args, "no_commit", False):
305
+ _auto_commit_archive(dir_name, repo_root)
306
+
307
+ # Return the archive path
308
+ print(f"{DIR_WORKFLOW}/{DIR_TASKS}/{DIR_ARCHIVE}/{year_month}/{dir_name}")
309
+
310
+ # Run hooks with the archived path
311
+ archived_json = archive_dest / FILE_TASK_JSON
312
+ run_task_hooks("after_archive", archived_json, repo_root)
313
+ return 0
314
+
315
+ return 1
316
+
317
+
318
+ def _auto_commit_archive(task_name: str, repo_root: Path) -> None:
319
+ """Stage .trellis/tasks/ changes and commit after archive."""
320
+ tasks_rel = f"{DIR_WORKFLOW}/{DIR_TASKS}"
321
+ run_git(["add", "-A", tasks_rel], cwd=repo_root)
322
+
323
+ # Check if there are staged changes
324
+ rc, _, _ = run_git(
325
+ ["diff", "--cached", "--quiet", "--", tasks_rel], cwd=repo_root
326
+ )
327
+ if rc == 0:
328
+ print("[OK] No task changes to commit.", file=sys.stderr)
329
+ return
330
+
331
+ commit_msg = f"chore(task): archive {task_name}"
332
+ rc, _, err = run_git(["commit", "-m", commit_msg], cwd=repo_root)
333
+ if rc == 0:
334
+ print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
335
+ else:
336
+ print(f"[WARN] Auto-commit failed: {err.strip()}", file=sys.stderr)
337
+
338
+
339
+ # =============================================================================
340
+ # Command: add-subtask
341
+ # =============================================================================
342
+
343
+ def cmd_add_subtask(args: argparse.Namespace) -> int:
344
+ """Link a child task to a parent task."""
345
+ repo_root = get_repo_root()
346
+
347
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
348
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
349
+
350
+ parent_json_path = parent_dir / FILE_TASK_JSON
351
+ child_json_path = child_dir / FILE_TASK_JSON
352
+
353
+ if not parent_json_path.is_file():
354
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
355
+ return 1
356
+
357
+ if not child_json_path.is_file():
358
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
359
+ return 1
360
+
361
+ parent_data = read_json(parent_json_path)
362
+ child_data = read_json(child_json_path)
363
+
364
+ if not parent_data or not child_data:
365
+ print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
366
+ return 1
367
+
368
+ # Check if child already has a parent
369
+ existing_parent = child_data.get("parent")
370
+ if existing_parent:
371
+ print(colored(f"Error: Child task already has a parent: {existing_parent}", Colors.RED), file=sys.stderr)
372
+ return 1
373
+
374
+ # Add child to parent's children list
375
+ parent_children = parent_data.get("children", [])
376
+ child_dir_name = child_dir.name
377
+ if child_dir_name not in parent_children:
378
+ parent_children.append(child_dir_name)
379
+ parent_data["children"] = parent_children
380
+
381
+ # Set parent in child's task.json
382
+ child_data["parent"] = parent_dir.name
383
+
384
+ # Write both
385
+ write_json(parent_json_path, parent_data)
386
+ write_json(child_json_path, child_data)
387
+
388
+ print(colored(f"Linked: {child_dir.name} -> {parent_dir.name}", Colors.GREEN), file=sys.stderr)
389
+ return 0
390
+
391
+
392
+ # =============================================================================
393
+ # Command: remove-subtask
394
+ # =============================================================================
395
+
396
+ def cmd_remove_subtask(args: argparse.Namespace) -> int:
397
+ """Unlink a child task from a parent task."""
398
+ repo_root = get_repo_root()
399
+
400
+ parent_dir = resolve_task_dir(args.parent_dir, repo_root)
401
+ child_dir = resolve_task_dir(args.child_dir, repo_root)
402
+
403
+ parent_json_path = parent_dir / FILE_TASK_JSON
404
+ child_json_path = child_dir / FILE_TASK_JSON
405
+
406
+ if not parent_json_path.is_file():
407
+ print(colored(f"Error: Parent task.json not found: {args.parent_dir}", Colors.RED), file=sys.stderr)
408
+ return 1
409
+
410
+ if not child_json_path.is_file():
411
+ print(colored(f"Error: Child task.json not found: {args.child_dir}", Colors.RED), file=sys.stderr)
412
+ return 1
413
+
414
+ parent_data = read_json(parent_json_path)
415
+ child_data = read_json(child_json_path)
416
+
417
+ if not parent_data or not child_data:
418
+ print(colored("Error: Failed to read task.json", Colors.RED), file=sys.stderr)
419
+ return 1
420
+
421
+ # Remove child from parent's children list
422
+ parent_children = parent_data.get("children", [])
423
+ child_dir_name = child_dir.name
424
+ if child_dir_name in parent_children:
425
+ parent_children.remove(child_dir_name)
426
+ parent_data["children"] = parent_children
427
+
428
+ # Clear parent in child's task.json
429
+ child_data["parent"] = None
430
+
431
+ # Write both
432
+ write_json(parent_json_path, parent_data)
433
+ write_json(child_json_path, child_data)
434
+
435
+ print(colored(f"Unlinked: {child_dir.name} from {parent_dir.name}", Colors.GREEN), file=sys.stderr)
436
+ return 0
437
+
438
+
439
+ # =============================================================================
440
+ # Command: set-branch
441
+ # =============================================================================
442
+
443
+ def cmd_set_branch(args: argparse.Namespace) -> int:
444
+ """Set git branch for task."""
445
+ repo_root = get_repo_root()
446
+ target_dir = resolve_task_dir(args.dir, repo_root)
447
+ branch = args.branch
448
+
449
+ if not branch:
450
+ print(colored("Error: Missing arguments", Colors.RED))
451
+ print("Usage: python3 task.py set-branch <task-dir> <branch-name>")
452
+ return 1
453
+
454
+ task_json = target_dir / FILE_TASK_JSON
455
+ if not task_json.is_file():
456
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
457
+ return 1
458
+
459
+ data = read_json(task_json)
460
+ if not data:
461
+ return 1
462
+
463
+ data["branch"] = branch
464
+ write_json(task_json, data)
465
+
466
+ print(colored(f"✓ Branch set to: {branch}", Colors.GREEN))
467
+ print()
468
+ print(colored("Now you can start the multi-agent pipeline:", Colors.BLUE))
469
+ print(f" python3 ./.trellis/scripts/multi_agent/start.py {args.dir}")
470
+ return 0
471
+
472
+
473
+ # =============================================================================
474
+ # Command: set-base-branch
475
+ # =============================================================================
476
+
477
+ def cmd_set_base_branch(args: argparse.Namespace) -> int:
478
+ """Set the base branch (PR target) for task."""
479
+ repo_root = get_repo_root()
480
+ target_dir = resolve_task_dir(args.dir, repo_root)
481
+ base_branch = args.base_branch
482
+
483
+ if not base_branch:
484
+ print(colored("Error: Missing arguments", Colors.RED))
485
+ print("Usage: python3 task.py set-base-branch <task-dir> <base-branch>")
486
+ print("Example: python3 task.py set-base-branch <dir> develop")
487
+ print()
488
+ print("This sets the target branch for PR (the branch your feature will merge into).")
489
+ return 1
490
+
491
+ task_json = target_dir / FILE_TASK_JSON
492
+ if not task_json.is_file():
493
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
494
+ return 1
495
+
496
+ data = read_json(task_json)
497
+ if not data:
498
+ return 1
499
+
500
+ data["base_branch"] = base_branch
501
+ write_json(task_json, data)
502
+
503
+ print(colored(f"✓ Base branch set to: {base_branch}", Colors.GREEN))
504
+ print(f" PR will target: {base_branch}")
505
+ return 0
506
+
507
+
508
+ # =============================================================================
509
+ # Command: set-scope
510
+ # =============================================================================
511
+
512
+ def cmd_set_scope(args: argparse.Namespace) -> int:
513
+ """Set scope for PR title."""
514
+ repo_root = get_repo_root()
515
+ target_dir = resolve_task_dir(args.dir, repo_root)
516
+ scope = args.scope
517
+
518
+ if not scope:
519
+ print(colored("Error: Missing arguments", Colors.RED))
520
+ print("Usage: python3 task.py set-scope <task-dir> <scope>")
521
+ return 1
522
+
523
+ task_json = target_dir / FILE_TASK_JSON
524
+ if not task_json.is_file():
525
+ print(colored(f"Error: task.json not found at {target_dir}", Colors.RED))
526
+ return 1
527
+
528
+ data = read_json(task_json)
529
+ if not data:
530
+ return 1
531
+
532
+ data["scope"] = scope
533
+ write_json(task_json, data)
534
+
535
+ print(colored(f"✓ Scope set to: {scope}", Colors.GREEN))
536
+ return 0
@@ -3,9 +3,11 @@
3
3
  Task utility functions.
4
4
 
5
5
  Provides:
6
- is_safe_task_path - Validate task path is safe to operate on
7
- find_task_by_name - Find task directory by name
8
- archive_task_dir - Archive task to monthly directory
6
+ is_safe_task_path - Validate task path is safe to operate on
7
+ find_task_by_name - Find task directory by name
8
+ resolve_task_dir - Resolve task directory from name, relative, or absolute path
9
+ archive_task_dir - Archive task to monthly directory
10
+ run_task_hooks - Run lifecycle hooks for task events
9
11
  """
10
12
 
11
13
  from __future__ import annotations
@@ -15,7 +17,7 @@ import sys
15
17
  from datetime import datetime
16
18
  from pathlib import Path
17
19
 
18
- from .paths import get_repo_root
20
+ from .paths import get_repo_root, get_tasks_dir
19
21
 
20
22
 
21
23
  # =============================================================================
@@ -35,23 +37,25 @@ def is_safe_task_path(task_path: str, repo_root: Path | None = None) -> bool:
35
37
  if repo_root is None:
36
38
  repo_root = get_repo_root()
37
39
 
40
+ normalized = task_path.replace("\\", "/")
41
+
38
42
  # Check empty or null
39
- if not task_path or task_path == "null":
43
+ if not normalized or normalized == "null":
40
44
  print("Error: empty or null task path", file=sys.stderr)
41
45
  return False
42
46
 
43
47
  # Reject absolute paths
44
- if task_path.startswith("/"):
48
+ if Path(task_path).is_absolute():
45
49
  print(f"Error: absolute path not allowed: {task_path}", file=sys.stderr)
46
50
  return False
47
51
 
48
52
  # Reject ".", "..", paths starting with "./" or "../", or containing ".."
49
- if task_path in (".", "..") or task_path.startswith("./") or task_path.startswith("../") or ".." in task_path:
53
+ if normalized in (".", "..") or normalized.startswith("./") or normalized.startswith("../") or ".." in normalized:
50
54
  print(f"Error: path traversal not allowed: {task_path}", file=sys.stderr)
51
55
  return False
52
56
 
53
57
  # Final check: ensure resolved path is not the repo root
54
- abs_path = repo_root / task_path
58
+ abs_path = repo_root / Path(normalized)
55
59
  if abs_path.exists():
56
60
  try:
57
61
  resolved = abs_path.resolve()
@@ -163,13 +167,105 @@ def archive_task_complete(
163
167
  return {}
164
168
 
165
169
 
170
+ # =============================================================================
171
+ # Task Directory Resolution
172
+ # =============================================================================
173
+
174
+ def resolve_task_dir(target_dir: str, repo_root: Path) -> Path:
175
+ """Resolve task directory to absolute path.
176
+
177
+ Supports:
178
+ - Absolute path: /path/to/task
179
+ - Relative path: .trellis/tasks/01-31-my-task
180
+ - Task name: my-task (uses find_task_by_name for lookup)
181
+
182
+ Args:
183
+ target_dir: Task directory specification.
184
+ repo_root: Repository root path.
185
+
186
+ Returns:
187
+ Resolved absolute path.
188
+ """
189
+ if not target_dir:
190
+ return Path()
191
+
192
+ normalized = target_dir.replace("\\", "/")
193
+ while normalized.startswith("./"):
194
+ normalized = normalized[2:]
195
+
196
+ # Absolute path
197
+ if Path(target_dir).is_absolute():
198
+ return Path(target_dir)
199
+
200
+ # Relative path (contains path separator or starts with .trellis)
201
+ if "/" in normalized or normalized.startswith(".trellis"):
202
+ return repo_root / Path(normalized)
203
+
204
+ # Task name - try to find in tasks directory
205
+ tasks_dir = get_tasks_dir(repo_root)
206
+ found = find_task_by_name(target_dir, tasks_dir)
207
+ if found:
208
+ return found
209
+
210
+ # Fallback to treating as relative path
211
+ return repo_root / Path(normalized)
212
+
213
+
214
+ # =============================================================================
215
+ # Lifecycle Hooks
216
+ # =============================================================================
217
+
218
+ def run_task_hooks(event: str, task_json_path: Path, repo_root: Path) -> None:
219
+ """Run lifecycle hooks for a task event.
220
+
221
+ Args:
222
+ event: Event name (e.g. "after_create").
223
+ task_json_path: Absolute path to the task's task.json.
224
+ repo_root: Repository root for cwd and config lookup.
225
+ """
226
+ import os
227
+ import subprocess
228
+
229
+ from .config import get_hooks
230
+ from .log import Colors, colored
231
+
232
+ commands = get_hooks(event, repo_root)
233
+ if not commands:
234
+ return
235
+
236
+ env = {**os.environ, "TASK_JSON_PATH": str(task_json_path)}
237
+
238
+ for cmd in commands:
239
+ try:
240
+ result = subprocess.run(
241
+ cmd,
242
+ shell=True,
243
+ cwd=repo_root,
244
+ env=env,
245
+ capture_output=True,
246
+ text=True,
247
+ encoding="utf-8",
248
+ errors="replace",
249
+ )
250
+ if result.returncode != 0:
251
+ print(
252
+ colored(f"[WARN] Hook failed ({event}): {cmd}", Colors.YELLOW),
253
+ file=sys.stderr,
254
+ )
255
+ if result.stderr.strip():
256
+ print(f" {result.stderr.strip()}", file=sys.stderr)
257
+ except Exception as e:
258
+ print(
259
+ colored(f"[WARN] Hook error ({event}): {cmd} — {e}", Colors.YELLOW),
260
+ file=sys.stderr,
261
+ )
262
+
263
+
166
264
  # =============================================================================
167
265
  # Main Entry (for testing)
168
266
  # =============================================================================
169
267
 
170
268
  if __name__ == "__main__":
171
- from .paths import get_tasks_dir
172
-
173
269
  repo = get_repo_root()
174
270
  tasks = get_tasks_dir(repo)
175
271