@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,37 @@
1
+ """
2
+ JSON file I/O utilities.
3
+
4
+ Provides read_json and write_json as the single source of truth
5
+ for JSON file operations across all Trellis scripts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from pathlib import Path
12
+
13
+
14
+ def read_json(path: Path) -> dict | None:
15
+ """Read and parse a JSON file.
16
+
17
+ Returns None if the file doesn't exist, is invalid JSON, or can't be read.
18
+ """
19
+ try:
20
+ return json.loads(path.read_text(encoding="utf-8"))
21
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
22
+ return None
23
+
24
+
25
+ def write_json(path: Path, data: dict) -> bool:
26
+ """Write dict to JSON file with pretty formatting.
27
+
28
+ Returns True on success, False on error.
29
+ """
30
+ try:
31
+ path.write_text(
32
+ json.dumps(data, indent=2, ensure_ascii=False),
33
+ encoding="utf-8",
34
+ )
35
+ return True
36
+ except (OSError, IOError):
37
+ return False
@@ -0,0 +1,45 @@
1
+ """
2
+ Terminal output utilities: colors and structured logging.
3
+
4
+ Single source of truth for Colors and log_* functions
5
+ used across all Trellis scripts.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+
11
+ class Colors:
12
+ """ANSI color codes for terminal output."""
13
+
14
+ RED = "\033[0;31m"
15
+ GREEN = "\033[0;32m"
16
+ YELLOW = "\033[1;33m"
17
+ BLUE = "\033[0;34m"
18
+ CYAN = "\033[0;36m"
19
+ DIM = "\033[2m"
20
+ NC = "\033[0m" # No Color / Reset
21
+
22
+
23
+ def colored(text: str, color: str) -> str:
24
+ """Apply ANSI color to text."""
25
+ return f"{color}{text}{Colors.NC}"
26
+
27
+
28
+ def log_info(msg: str) -> None:
29
+ """Print info-level message with [INFO] prefix."""
30
+ print(f"{Colors.BLUE}[INFO]{Colors.NC} {msg}")
31
+
32
+
33
+ def log_success(msg: str) -> None:
34
+ """Print success message with [SUCCESS] prefix."""
35
+ print(f"{Colors.GREEN}[SUCCESS]{Colors.NC} {msg}")
36
+
37
+
38
+ def log_warn(msg: str) -> None:
39
+ """Print warning message with [WARN] prefix."""
40
+ print(f"{Colors.YELLOW}[WARN]{Colors.NC} {msg}")
41
+
42
+
43
+ def log_error(msg: str) -> None:
44
+ """Print error message with [ERROR] prefix."""
45
+ print(f"{Colors.RED}[ERROR]{Colors.NC} {msg}")
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Package discovery and context output.
4
+
5
+ Provides:
6
+ get_packages_info - Get structured package info
7
+ get_packages_section - Build PACKAGES text section
8
+ get_context_packages_text - Full packages text output (--mode packages)
9
+ get_context_packages_json - Full packages JSON output (--mode packages --json)
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from pathlib import Path
15
+
16
+ from .config import _is_true_config_value, get_default_package, get_packages, get_spec_scope
17
+ from .paths import (
18
+ DIR_SPEC,
19
+ DIR_WORKFLOW,
20
+ get_current_task,
21
+ get_repo_root,
22
+ )
23
+ from .tasks import load_task
24
+
25
+
26
+ # =============================================================================
27
+ # Internal Helpers
28
+ # =============================================================================
29
+
30
+ def _scan_spec_layers(spec_dir: Path, package: str | None = None) -> list[str]:
31
+ """Scan spec directory for available layers (subdirectories).
32
+
33
+ For monorepo: scans spec/<package>/
34
+ For single-repo: scans spec/
35
+ """
36
+ target = spec_dir / package if package else spec_dir
37
+ if not target.is_dir():
38
+ return []
39
+ return sorted(
40
+ d.name for d in target.iterdir() if d.is_dir() and d.name != "guides"
41
+ )
42
+
43
+
44
+ def _get_active_task_package(repo_root: Path) -> str | None:
45
+ """Get the package field from the active task's task.json."""
46
+ current = get_current_task(repo_root)
47
+ if not current:
48
+ return None
49
+ ct = load_task(repo_root / current)
50
+ return ct.package if ct and ct.package else None
51
+
52
+
53
+ def _resolve_scope_set(
54
+ packages: dict,
55
+ spec_scope,
56
+ task_pkg: str | None,
57
+ default_pkg: str | None,
58
+ ) -> set | None:
59
+ """Resolve spec_scope to a set of allowed package names, or None for full scan."""
60
+ if not packages:
61
+ return None
62
+
63
+ if spec_scope is None:
64
+ return None
65
+
66
+ if isinstance(spec_scope, str) and spec_scope == "active_task":
67
+ if task_pkg and task_pkg in packages:
68
+ return {task_pkg}
69
+ if default_pkg and default_pkg in packages:
70
+ return {default_pkg}
71
+ return None
72
+
73
+ if isinstance(spec_scope, list):
74
+ valid = {e for e in spec_scope if e in packages}
75
+ if valid:
76
+ return valid
77
+ # All invalid: fallback
78
+ if task_pkg and task_pkg in packages:
79
+ return {task_pkg}
80
+ if default_pkg and default_pkg in packages:
81
+ return {default_pkg}
82
+ return None
83
+
84
+ return None
85
+
86
+
87
+ # =============================================================================
88
+ # Public Functions
89
+ # =============================================================================
90
+
91
+ def get_packages_info(repo_root: Path) -> list[dict]:
92
+ """Get structured package info for monorepo projects.
93
+
94
+ Returns list of dicts with keys: name, path, type, default, specLayers,
95
+ isSubmodule, isGitRepo.
96
+ Returns empty list for single-repo projects.
97
+ """
98
+ packages = get_packages(repo_root)
99
+ if not packages:
100
+ return []
101
+
102
+ default_pkg = get_default_package(repo_root)
103
+ spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
104
+ result = []
105
+
106
+ for pkg_name, pkg_config in packages.items():
107
+ pkg_path = pkg_config.get("path", pkg_name) if isinstance(pkg_config, dict) else str(pkg_config)
108
+ pkg_type = pkg_config.get("type", "local") if isinstance(pkg_config, dict) else "local"
109
+ pkg_git = pkg_config.get("git", False) if isinstance(pkg_config, dict) else False
110
+ layers = _scan_spec_layers(spec_dir, pkg_name)
111
+
112
+ result.append({
113
+ "name": pkg_name,
114
+ "path": pkg_path,
115
+ "type": pkg_type,
116
+ "default": pkg_name == default_pkg,
117
+ "specLayers": layers,
118
+ "isSubmodule": pkg_type == "submodule",
119
+ "isGitRepo": _is_true_config_value(pkg_git),
120
+ })
121
+
122
+ return result
123
+
124
+
125
+ def get_packages_section(repo_root: Path) -> str:
126
+ """Build the PACKAGES section for text output."""
127
+ spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
128
+ pkg_info = get_packages_info(repo_root)
129
+
130
+ lines: list[str] = []
131
+ lines.append("## PACKAGES")
132
+
133
+ if not pkg_info:
134
+ lines.append("(single-repo mode)")
135
+ layers = _scan_spec_layers(spec_dir)
136
+ if layers:
137
+ lines.append(f"Spec layers: {', '.join(layers)}")
138
+ return "\n".join(lines)
139
+
140
+ default_pkg = get_default_package(repo_root)
141
+
142
+ for pkg in pkg_info:
143
+ layers_str = f" [{', '.join(pkg['specLayers'])}]" if pkg["specLayers"] else ""
144
+ submodule_tag = " (submodule)" if pkg["isSubmodule"] else ""
145
+ git_repo_tag = " (git repo)" if pkg["isGitRepo"] else ""
146
+ default_tag = " *" if pkg["default"] else ""
147
+ lines.append(
148
+ f"- {pkg['name']:<16} {pkg['path']:<20}{layers_str}{submodule_tag}{git_repo_tag}{default_tag}"
149
+ )
150
+
151
+ if default_pkg:
152
+ lines.append(f"Default package: {default_pkg}")
153
+
154
+ return "\n".join(lines)
155
+
156
+
157
+ def get_context_packages_text(repo_root: Path | None = None) -> str:
158
+ """Get packages context as formatted text (for --mode packages)."""
159
+ if repo_root is None:
160
+ repo_root = get_repo_root()
161
+
162
+ pkg_info = get_packages_info(repo_root)
163
+ lines: list[str] = []
164
+
165
+ if not pkg_info:
166
+ spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
167
+ lines.append("Single-repo project (no packages configured)")
168
+ lines.append("")
169
+ layers = _scan_spec_layers(spec_dir)
170
+ if layers:
171
+ lines.append(f"Spec layers: {', '.join(layers)}")
172
+ return "\n".join(lines)
173
+
174
+ # Resolve scope for annotations
175
+ packages_dict = get_packages(repo_root) or {}
176
+ default_pkg = get_default_package(repo_root)
177
+ spec_scope = get_spec_scope(repo_root)
178
+ task_pkg = _get_active_task_package(repo_root)
179
+ scope_set = _resolve_scope_set(packages_dict, spec_scope, task_pkg, default_pkg)
180
+
181
+ lines.append("## PACKAGES")
182
+ lines.append("")
183
+ for pkg in pkg_info:
184
+ default_tag = " (default)" if pkg["default"] else ""
185
+ type_tag = f" [{pkg['type']}]" if pkg["type"] != "local" else ""
186
+ git_tag = " [git repo]" if pkg["isGitRepo"] else ""
187
+
188
+ # Scope annotation
189
+ scope_tag = ""
190
+ if scope_set is not None and pkg["name"] not in scope_set:
191
+ scope_tag = " (out of scope)"
192
+
193
+ lines.append(f"### {pkg['name']}{default_tag}{type_tag}{git_tag}{scope_tag}")
194
+ lines.append(f"Path: {pkg['path']}")
195
+ if pkg["specLayers"]:
196
+ lines.append(f"Spec layers: {', '.join(pkg['specLayers'])}")
197
+ for layer in pkg["specLayers"]:
198
+ lines.append(f" - .trellis/spec/{pkg['name']}/{layer}/index.md")
199
+ else:
200
+ lines.append("Spec: not configured")
201
+ lines.append("")
202
+
203
+ # Also show shared guides
204
+ guides_dir = repo_root / DIR_WORKFLOW / DIR_SPEC / "guides"
205
+ if guides_dir.is_dir():
206
+ lines.append("### Shared Guides (always included)")
207
+ lines.append("Path: .trellis/spec/guides/index.md")
208
+ lines.append("")
209
+
210
+ return "\n".join(lines)
211
+
212
+
213
+ def get_context_packages_json(repo_root: Path | None = None) -> dict:
214
+ """Get packages context as a dictionary (for --mode packages --json)."""
215
+ if repo_root is None:
216
+ repo_root = get_repo_root()
217
+
218
+ pkg_info = get_packages_info(repo_root)
219
+
220
+ if not pkg_info:
221
+ spec_dir = repo_root / DIR_WORKFLOW / DIR_SPEC
222
+ layers = _scan_spec_layers(spec_dir)
223
+ return {
224
+ "mode": "single-repo",
225
+ "specLayers": layers,
226
+ }
227
+
228
+ default_pkg = get_default_package(repo_root)
229
+ spec_scope = get_spec_scope(repo_root)
230
+ task_pkg = _get_active_task_package(repo_root)
231
+
232
+ return {
233
+ "mode": "monorepo",
234
+ "packages": pkg_info,
235
+ "defaultPackage": default_pkg,
236
+ "specScope": spec_scope,
237
+ "activeTaskPackage": task_pkg,
238
+ }
@@ -221,6 +221,50 @@ def _get_current_task_file(repo_root: Path | None = None) -> Path:
221
221
  return repo_root / DIR_WORKFLOW / FILE_CURRENT_TASK
222
222
 
223
223
 
224
+ def normalize_task_ref(task_ref: str) -> str:
225
+ """Normalize a task ref for stable storage in .current-task.
226
+
227
+ Stored refs should prefer repo-relative POSIX paths like
228
+ `.trellis/tasks/03-27-my-task`, even on Windows. Absolute paths are preserved
229
+ unless they can later be converted back to repo-relative form by callers.
230
+ """
231
+ normalized = task_ref.strip()
232
+ if not normalized:
233
+ return ""
234
+
235
+ path_obj = Path(normalized)
236
+ if path_obj.is_absolute():
237
+ return str(path_obj)
238
+
239
+ normalized = normalized.replace("\\", "/")
240
+ while normalized.startswith("./"):
241
+ normalized = normalized[2:]
242
+
243
+ if normalized.startswith(f"{DIR_TASKS}/"):
244
+ return f"{DIR_WORKFLOW}/{normalized}"
245
+
246
+ return normalized
247
+
248
+
249
+ def resolve_task_ref(task_ref: str, repo_root: Path | None = None) -> Path | None:
250
+ """Resolve a task ref from .current-task to an absolute task directory path."""
251
+ if repo_root is None:
252
+ repo_root = get_repo_root()
253
+
254
+ normalized = normalize_task_ref(task_ref)
255
+ if not normalized:
256
+ return None
257
+
258
+ path_obj = Path(normalized)
259
+ if path_obj.is_absolute():
260
+ return path_obj
261
+
262
+ if normalized.startswith(f"{DIR_WORKFLOW}/"):
263
+ return repo_root / path_obj
264
+
265
+ return repo_root / DIR_WORKFLOW / DIR_TASKS / path_obj
266
+
267
+
224
268
  def get_current_task(repo_root: Path | None = None) -> str | None:
225
269
  """Get current task directory path (relative to repo_root).
226
270
 
@@ -236,7 +280,8 @@ def get_current_task(repo_root: Path | None = None) -> str | None:
236
280
  return None
237
281
 
238
282
  try:
239
- return current_file.read_text(encoding="utf-8").strip()
283
+ content = current_file.read_text(encoding="utf-8").strip()
284
+ return normalize_task_ref(content) if content else None
240
285
  except (OSError, IOError):
241
286
  return None
242
287
 
@@ -255,7 +300,7 @@ def get_current_task_abs(repo_root: Path | None = None) -> Path | None:
255
300
 
256
301
  relative = get_current_task(repo_root)
257
302
  if relative:
258
- return repo_root / relative
303
+ return resolve_task_ref(relative, repo_root)
259
304
  return None
260
305
 
261
306
 
@@ -272,18 +317,24 @@ def set_current_task(task_path: str, repo_root: Path | None = None) -> bool:
272
317
  if repo_root is None:
273
318
  repo_root = get_repo_root()
274
319
 
275
- if not task_path:
320
+ normalized = normalize_task_ref(task_path)
321
+ if not normalized:
276
322
  return False
277
323
 
278
324
  # Verify task directory exists
279
- full_path = repo_root / task_path
280
- if not full_path.is_dir():
325
+ full_path = resolve_task_ref(normalized, repo_root)
326
+ if full_path is None or not full_path.is_dir():
281
327
  return False
282
328
 
329
+ try:
330
+ normalized = full_path.relative_to(repo_root).as_posix()
331
+ except ValueError:
332
+ normalized = str(full_path)
333
+
283
334
  current_file = _get_current_task_file(repo_root)
284
335
 
285
336
  try:
286
- current_file.write_text(task_path, encoding="utf-8")
337
+ current_file.write_text(normalized, encoding="utf-8")
287
338
  return True
288
339
  except (OSError, IOError):
289
340
  return False
@@ -333,6 +384,52 @@ def generate_task_date_prefix() -> str:
333
384
  return datetime.now().strftime("%m-%d")
334
385
 
335
386
 
387
+ # =============================================================================
388
+ # Monorepo / Package Paths
389
+ # =============================================================================
390
+
391
+
392
+ def get_spec_dir(package: str | None = None, repo_root: Path | None = None) -> Path:
393
+ """Get the spec directory path.
394
+
395
+ Single-repo: .trellis/spec
396
+ Monorepo with package: .trellis/spec/<package>
397
+
398
+ Uses lazy import to avoid circular dependency with config.py.
399
+ """
400
+ if repo_root is None:
401
+ repo_root = get_repo_root()
402
+
403
+ from .config import get_spec_base
404
+
405
+ base = get_spec_base(package, repo_root)
406
+ return repo_root / DIR_WORKFLOW / base
407
+
408
+
409
+ def get_package_path(package: str, repo_root: Path | None = None) -> Path | None:
410
+ """Get a package's source directory absolute path from config.
411
+
412
+ Returns:
413
+ Absolute path to the package directory, or None if not found.
414
+ """
415
+ if repo_root is None:
416
+ repo_root = get_repo_root()
417
+
418
+ from .config import get_packages
419
+
420
+ packages = get_packages(repo_root)
421
+ if not packages or package not in packages:
422
+ return None
423
+
424
+ info = packages[package]
425
+ if isinstance(info, dict):
426
+ rel_path = info.get("path", package)
427
+ else:
428
+ rel_path = str(info)
429
+
430
+ return repo_root / rel_path
431
+
432
+
336
433
  # =============================================================================
337
434
  # Main Entry (for testing)
338
435
  # =============================================================================
@@ -19,25 +19,39 @@ Provides:
19
19
 
20
20
  from __future__ import annotations
21
21
 
22
- import json
23
22
  from pathlib import Path
24
23
 
24
+ from .io import read_json, write_json
25
25
 
26
- def _read_json_file(path: Path) -> dict | None:
27
- """Read and parse a JSON file."""
28
- try:
29
- return json.loads(path.read_text(encoding="utf-8"))
30
- except (FileNotFoundError, json.JSONDecodeError, OSError):
31
- return None
32
26
 
27
+ # =============================================================================
28
+ # Internal Helpers (operate on pre-loaded data dict)
29
+ # =============================================================================
30
+
31
+ def _total_phases(data: dict) -> int:
32
+ """Get total phases from pre-loaded data."""
33
+ next_action = data.get("next_action", [])
34
+ return len(next_action) if isinstance(next_action, list) else 0
33
35
 
34
- def _write_json_file(path: Path, data: dict) -> bool:
35
- """Write dict to JSON file."""
36
- try:
37
- path.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
38
- return True
39
- except (OSError, IOError):
40
- return False
36
+
37
+ def _phase_action(data: dict, phase: int) -> str:
38
+ """Get action name for a phase from pre-loaded data."""
39
+ next_action = data.get("next_action", [])
40
+ if isinstance(next_action, list):
41
+ for item in next_action:
42
+ if isinstance(item, dict) and item.get("phase") == phase:
43
+ return item.get("action", "unknown")
44
+ return "unknown"
45
+
46
+
47
+ def _phase_for_action(data: dict, action: str) -> int:
48
+ """Get phase number for an action name from pre-loaded data."""
49
+ next_action = data.get("next_action", [])
50
+ if isinstance(next_action, list):
51
+ for item in next_action:
52
+ if isinstance(item, dict) and item.get("action") == action:
53
+ return item.get("phase", 0)
54
+ return 0
41
55
 
42
56
 
43
57
  # =============================================================================
@@ -53,7 +67,7 @@ def get_current_phase(task_json: Path) -> int:
53
67
  Returns:
54
68
  Current phase number, or 0 if not found.
55
69
  """
56
- data = _read_json_file(task_json)
70
+ data = read_json(task_json)
57
71
  if not data:
58
72
  return 0
59
73
  return data.get("current_phase", 0) or 0
@@ -68,14 +82,10 @@ def get_total_phases(task_json: Path) -> int:
68
82
  Returns:
69
83
  Total phase count, or 0 if not found.
70
84
  """
71
- data = _read_json_file(task_json)
85
+ data = read_json(task_json)
72
86
  if not data:
73
87
  return 0
74
-
75
- next_action = data.get("next_action", [])
76
- if isinstance(next_action, list):
77
- return len(next_action)
78
- return 0
88
+ return _total_phases(data)
79
89
 
80
90
 
81
91
  def get_phase_action(task_json: Path, phase: int) -> str:
@@ -88,16 +98,10 @@ def get_phase_action(task_json: Path, phase: int) -> str:
88
98
  Returns:
89
99
  Action name, or "unknown" if not found.
90
100
  """
91
- data = _read_json_file(task_json)
101
+ data = read_json(task_json)
92
102
  if not data:
93
103
  return "unknown"
94
-
95
- next_action = data.get("next_action", [])
96
- if isinstance(next_action, list):
97
- for item in next_action:
98
- if isinstance(item, dict) and item.get("phase") == phase:
99
- return item.get("action", "unknown")
100
- return "unknown"
104
+ return _phase_action(data, phase)
101
105
 
102
106
 
103
107
  def get_phase_info(task_json: Path) -> str:
@@ -109,18 +113,18 @@ def get_phase_info(task_json: Path) -> str:
109
113
  Returns:
110
114
  Formatted string like "1/4 (implement)".
111
115
  """
112
- data = _read_json_file(task_json)
116
+ data = read_json(task_json)
113
117
  if not data:
114
118
  return "N/A"
115
119
 
116
120
  current_phase = data.get("current_phase", 0) or 0
117
- total_phases = get_total_phases(task_json)
118
- action_name = get_phase_action(task_json, current_phase)
121
+ total = _total_phases(data)
122
+ action_name = _phase_action(data, current_phase)
119
123
 
120
124
  if current_phase == 0 or current_phase is None:
121
- return f"0/{total_phases} (pending)"
125
+ return f"0/{total} (pending)"
122
126
  else:
123
- return f"{current_phase}/{total_phases} ({action_name})"
127
+ return f"{current_phase}/{total} ({action_name})"
124
128
 
125
129
 
126
130
  def set_phase(task_json: Path, phase: int) -> bool:
@@ -133,12 +137,12 @@ def set_phase(task_json: Path, phase: int) -> bool:
133
137
  Returns:
134
138
  True on success, False on error.
135
139
  """
136
- data = _read_json_file(task_json)
140
+ data = read_json(task_json)
137
141
  if not data:
138
142
  return False
139
143
 
140
144
  data["current_phase"] = phase
141
- return _write_json_file(task_json, data)
145
+ return write_json(task_json, data)
142
146
 
143
147
 
144
148
  def advance_phase(task_json: Path) -> bool:
@@ -150,19 +154,19 @@ def advance_phase(task_json: Path) -> bool:
150
154
  Returns:
151
155
  True on success, False on error or at final phase.
152
156
  """
153
- data = _read_json_file(task_json)
157
+ data = read_json(task_json)
154
158
  if not data:
155
159
  return False
156
160
 
157
161
  current = data.get("current_phase", 0) or 0
158
- total = get_total_phases(task_json)
162
+ total = _total_phases(data)
159
163
  next_phase = current + 1
160
164
 
161
165
  if next_phase > total:
162
166
  return False # Already at final phase
163
167
 
164
168
  data["current_phase"] = next_phase
165
- return _write_json_file(task_json, data)
169
+ return write_json(task_json, data)
166
170
 
167
171
 
168
172
  def get_phase_for_action(task_json: Path, action: str) -> int:
@@ -175,16 +179,10 @@ def get_phase_for_action(task_json: Path, action: str) -> int:
175
179
  Returns:
176
180
  Phase number, or 0 if not found.
177
181
  """
178
- data = _read_json_file(task_json)
182
+ data = read_json(task_json)
179
183
  if not data:
180
184
  return 0
181
-
182
- next_action = data.get("next_action", [])
183
- if isinstance(next_action, list):
184
- for item in next_action:
185
- if isinstance(item, dict) and item.get("action") == action:
186
- return item.get("phase", 0)
187
- return 0
185
+ return _phase_for_action(data, action)
188
186
 
189
187
 
190
188
  def map_subagent_to_action(subagent_type: str) -> str:
@@ -231,8 +229,11 @@ def is_current_action(task_json: Path, action: str) -> bool:
231
229
  Returns:
232
230
  True if current phase matches the action.
233
231
  """
234
- current = get_current_phase(task_json)
235
- action_phase = get_phase_for_action(task_json, action)
232
+ data = read_json(task_json)
233
+ if not data:
234
+ return False
235
+ current = data.get("current_phase", 0) or 0
236
+ action_phase = _phase_for_action(data, action)
236
237
  return current == action_phase
237
238
 
238
239