@mindfoldhq/trellis 0.6.0-beta.2 → 0.6.0-beta.20

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 (330) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts +1 -1
  3. package/dist/cli/index.d.ts.map +1 -1
  4. package/dist/cli/index.js +58 -2
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/commands/channel/adapters/claude.d.ts +29 -0
  7. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  8. package/dist/commands/channel/adapters/claude.js +203 -0
  9. package/dist/commands/channel/adapters/claude.js.map +1 -0
  10. package/dist/commands/channel/adapters/codex.d.ts +85 -0
  11. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  12. package/dist/commands/channel/adapters/codex.js +505 -0
  13. package/dist/commands/channel/adapters/codex.js.map +1 -0
  14. package/dist/commands/channel/adapters/index.d.ts +84 -0
  15. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  16. package/dist/commands/channel/adapters/index.js +115 -0
  17. package/dist/commands/channel/adapters/index.js.map +1 -0
  18. package/dist/commands/channel/adapters/types.d.ts +33 -0
  19. package/dist/commands/channel/adapters/types.d.ts.map +1 -0
  20. package/dist/commands/channel/adapters/types.js +2 -0
  21. package/dist/commands/channel/adapters/types.js.map +1 -0
  22. package/dist/commands/channel/agent-loader.d.ts +32 -0
  23. package/dist/commands/channel/agent-loader.d.ts.map +1 -0
  24. package/dist/commands/channel/agent-loader.js +154 -0
  25. package/dist/commands/channel/agent-loader.js.map +1 -0
  26. package/dist/commands/channel/context-loader.d.ts +26 -0
  27. package/dist/commands/channel/context-loader.d.ts.map +1 -0
  28. package/dist/commands/channel/context-loader.js +290 -0
  29. package/dist/commands/channel/context-loader.js.map +1 -0
  30. package/dist/commands/channel/context.d.ts +16 -0
  31. package/dist/commands/channel/context.d.ts.map +1 -0
  32. package/dist/commands/channel/context.js +83 -0
  33. package/dist/commands/channel/context.js.map +1 -0
  34. package/dist/commands/channel/create.d.ts +27 -0
  35. package/dist/commands/channel/create.d.ts.map +1 -0
  36. package/dist/commands/channel/create.js +39 -0
  37. package/dist/commands/channel/create.js.map +1 -0
  38. package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
  39. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
  40. package/dist/commands/channel/dev-parse-trace.js +70 -0
  41. package/dist/commands/channel/dev-parse-trace.js.map +1 -0
  42. package/dist/commands/channel/guard.d.ts +150 -0
  43. package/dist/commands/channel/guard.d.ts.map +1 -0
  44. package/dist/commands/channel/guard.js +474 -0
  45. package/dist/commands/channel/guard.js.map +1 -0
  46. package/dist/commands/channel/index.d.ts +3 -0
  47. package/dist/commands/channel/index.d.ts.map +1 -0
  48. package/dist/commands/channel/index.js +531 -0
  49. package/dist/commands/channel/index.js.map +1 -0
  50. package/dist/commands/channel/interrupt.d.ts +10 -0
  51. package/dist/commands/channel/interrupt.d.ts.map +1 -0
  52. package/dist/commands/channel/interrupt.js +22 -0
  53. package/dist/commands/channel/interrupt.js.map +1 -0
  54. package/dist/commands/channel/kill.d.ts +7 -0
  55. package/dist/commands/channel/kill.d.ts.map +1 -0
  56. package/dist/commands/channel/kill.js +121 -0
  57. package/dist/commands/channel/kill.js.map +1 -0
  58. package/dist/commands/channel/list.d.ts +17 -0
  59. package/dist/commands/channel/list.d.ts.map +1 -0
  60. package/dist/commands/channel/list.js +233 -0
  61. package/dist/commands/channel/list.js.map +1 -0
  62. package/dist/commands/channel/messages.d.ts +15 -0
  63. package/dist/commands/channel/messages.d.ts.map +1 -0
  64. package/dist/commands/channel/messages.js +245 -0
  65. package/dist/commands/channel/messages.js.map +1 -0
  66. package/dist/commands/channel/rm.d.ts +27 -0
  67. package/dist/commands/channel/rm.d.ts.map +1 -0
  68. package/dist/commands/channel/rm.js +216 -0
  69. package/dist/commands/channel/rm.js.map +1 -0
  70. package/dist/commands/channel/run.d.ts +30 -0
  71. package/dist/commands/channel/run.d.ts.map +1 -0
  72. package/dist/commands/channel/run.js +130 -0
  73. package/dist/commands/channel/run.js.map +1 -0
  74. package/dist/commands/channel/send.d.ts +11 -0
  75. package/dist/commands/channel/send.d.ts.map +1 -0
  76. package/dist/commands/channel/send.js +24 -0
  77. package/dist/commands/channel/send.js.map +1 -0
  78. package/dist/commands/channel/spawn.d.ts +40 -0
  79. package/dist/commands/channel/spawn.d.ts.map +1 -0
  80. package/dist/commands/channel/spawn.js +244 -0
  81. package/dist/commands/channel/spawn.js.map +1 -0
  82. package/dist/commands/channel/store/events.d.ts +39 -0
  83. package/dist/commands/channel/store/events.d.ts.map +1 -0
  84. package/dist/commands/channel/store/events.js +87 -0
  85. package/dist/commands/channel/store/events.js.map +1 -0
  86. package/dist/commands/channel/store/filter.d.ts +3 -0
  87. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  88. package/dist/commands/channel/store/filter.js +2 -0
  89. package/dist/commands/channel/store/filter.js.map +1 -0
  90. package/dist/commands/channel/store/lock.d.ts +23 -0
  91. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  92. package/dist/commands/channel/store/lock.js +99 -0
  93. package/dist/commands/channel/store/lock.js.map +1 -0
  94. package/dist/commands/channel/store/paths.d.ts +63 -0
  95. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  96. package/dist/commands/channel/store/paths.js +246 -0
  97. package/dist/commands/channel/store/paths.js.map +1 -0
  98. package/dist/commands/channel/store/schema.d.ts +27 -0
  99. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  100. package/dist/commands/channel/store/schema.js +34 -0
  101. package/dist/commands/channel/store/schema.js.map +1 -0
  102. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  103. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  104. package/dist/commands/channel/store/thread-state.js +16 -0
  105. package/dist/commands/channel/store/thread-state.js.map +1 -0
  106. package/dist/commands/channel/store/watch.d.ts +19 -0
  107. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  108. package/dist/commands/channel/store/watch.js +146 -0
  109. package/dist/commands/channel/store/watch.js.map +1 -0
  110. package/dist/commands/channel/supervisor/idle.d.ts +46 -0
  111. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
  112. package/dist/commands/channel/supervisor/idle.js +72 -0
  113. package/dist/commands/channel/supervisor/idle.js.map +1 -0
  114. package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
  115. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  116. package/dist/commands/channel/supervisor/inbox.js +160 -0
  117. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  118. package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
  119. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  120. package/dist/commands/channel/supervisor/shutdown.js +146 -0
  121. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  122. package/dist/commands/channel/supervisor/stdout.d.ts +51 -0
  123. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  124. package/dist/commands/channel/supervisor/stdout.js +121 -0
  125. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  126. package/dist/commands/channel/supervisor/turns.d.ts +31 -0
  127. package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
  128. package/dist/commands/channel/supervisor/turns.js +45 -0
  129. package/dist/commands/channel/supervisor/turns.js.map +1 -0
  130. package/dist/commands/channel/supervisor/warning.d.ts +48 -0
  131. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
  132. package/dist/commands/channel/supervisor/warning.js +77 -0
  133. package/dist/commands/channel/supervisor/warning.js.map +1 -0
  134. package/dist/commands/channel/supervisor.d.ts +59 -0
  135. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  136. package/dist/commands/channel/supervisor.js +344 -0
  137. package/dist/commands/channel/supervisor.js.map +1 -0
  138. package/dist/commands/channel/text-body.d.ts +13 -0
  139. package/dist/commands/channel/text-body.d.ts.map +1 -0
  140. package/dist/commands/channel/text-body.js +47 -0
  141. package/dist/commands/channel/text-body.js.map +1 -0
  142. package/dist/commands/channel/threads.d.ts +39 -0
  143. package/dist/commands/channel/threads.d.ts.map +1 -0
  144. package/dist/commands/channel/threads.js +106 -0
  145. package/dist/commands/channel/threads.js.map +1 -0
  146. package/dist/commands/channel/title.d.ts +12 -0
  147. package/dist/commands/channel/title.d.ts.map +1 -0
  148. package/dist/commands/channel/title.js +24 -0
  149. package/dist/commands/channel/title.js.map +1 -0
  150. package/dist/commands/channel/wait.d.ts +17 -0
  151. package/dist/commands/channel/wait.d.ts.map +1 -0
  152. package/dist/commands/channel/wait.js +75 -0
  153. package/dist/commands/channel/wait.js.map +1 -0
  154. package/dist/commands/init.d.ts +2 -0
  155. package/dist/commands/init.d.ts.map +1 -1
  156. package/dist/commands/init.js +97 -42
  157. package/dist/commands/init.js.map +1 -1
  158. package/dist/commands/mem.d.ts +13 -117
  159. package/dist/commands/mem.d.ts.map +1 -1
  160. package/dist/commands/mem.js +168 -1074
  161. package/dist/commands/mem.js.map +1 -1
  162. package/dist/commands/uninstall.d.ts.map +1 -1
  163. package/dist/commands/uninstall.js +28 -2
  164. package/dist/commands/uninstall.js.map +1 -1
  165. package/dist/commands/update.d.ts.map +1 -1
  166. package/dist/commands/update.js +31 -111
  167. package/dist/commands/update.js.map +1 -1
  168. package/dist/commands/upgrade.d.ts +28 -0
  169. package/dist/commands/upgrade.d.ts.map +1 -0
  170. package/dist/commands/upgrade.js +84 -0
  171. package/dist/commands/upgrade.js.map +1 -0
  172. package/dist/commands/workflow.d.ts +35 -0
  173. package/dist/commands/workflow.d.ts.map +1 -0
  174. package/dist/commands/workflow.js +219 -0
  175. package/dist/commands/workflow.js.map +1 -0
  176. package/dist/configurators/claude.d.ts.map +1 -1
  177. package/dist/configurators/claude.js +1 -0
  178. package/dist/configurators/claude.js.map +1 -1
  179. package/dist/configurators/codex.d.ts.map +1 -1
  180. package/dist/configurators/codex.js +5 -3
  181. package/dist/configurators/codex.js.map +1 -1
  182. package/dist/configurators/shared.js +4 -4
  183. package/dist/configurators/shared.js.map +1 -1
  184. package/dist/configurators/workflow.d.ts +8 -0
  185. package/dist/configurators/workflow.d.ts.map +1 -1
  186. package/dist/configurators/workflow.js +3 -2
  187. package/dist/configurators/workflow.js.map +1 -1
  188. package/dist/migrations/manifests/0.5.10.json +9 -0
  189. package/dist/migrations/manifests/0.5.11.json +16 -0
  190. package/dist/migrations/manifests/0.5.12.json +9 -0
  191. package/dist/migrations/manifests/0.5.13.json +9 -0
  192. package/dist/migrations/manifests/0.5.14.json +9 -0
  193. package/dist/migrations/manifests/0.5.15.json +9 -0
  194. package/dist/migrations/manifests/0.5.16.json +9 -0
  195. package/dist/migrations/manifests/0.5.17.json +9 -0
  196. package/dist/migrations/manifests/0.5.18.json +9 -0
  197. package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
  198. package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
  199. package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
  200. package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
  201. package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
  202. package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
  203. package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
  204. package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
  205. package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
  206. package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
  207. package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
  208. package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
  209. package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
  210. package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
  211. package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
  212. package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
  213. package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
  214. package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
  215. package/dist/templates/claude/agents/trellis-check.md +13 -7
  216. package/dist/templates/claude/agents/trellis-implement.md +8 -7
  217. package/dist/templates/claude/settings.json +4 -4
  218. package/dist/templates/codebuddy/agents/trellis-check.md +13 -7
  219. package/dist/templates/codebuddy/agents/trellis-implement.md +8 -7
  220. package/dist/templates/codebuddy/settings.json +4 -4
  221. package/dist/templates/codex/agents/trellis-check.toml +4 -4
  222. package/dist/templates/codex/agents/trellis-implement.toml +4 -4
  223. package/dist/templates/codex/config.toml +5 -3
  224. package/dist/templates/codex/hooks/session-start.py +205 -119
  225. package/dist/templates/codex/hooks.json +2 -2
  226. package/dist/templates/codex/skills/before-dev/SKILL.md +12 -6
  227. package/dist/templates/codex/skills/brainstorm/SKILL.md +69 -457
  228. package/dist/templates/codex/skills/check/SKILL.md +86 -18
  229. package/dist/templates/codex/skills/start/SKILL.md +33 -323
  230. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +7 -4
  231. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
  232. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +3 -2
  233. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +5 -5
  234. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
  235. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +35 -6
  236. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +5 -4
  237. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/SKILL.md +41 -0
  238. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/mcp-setup.md +90 -0
  239. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/repository-analysis.md +59 -0
  240. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-task-planning.md +61 -0
  241. package/dist/templates/common/bundled-skills/trellis-spec-bootstarp/references/spec-writing.md +70 -0
  242. package/dist/templates/common/commands/continue.md +6 -5
  243. package/dist/templates/common/commands/start.md +9 -6
  244. package/dist/templates/common/skills/before-dev.md +12 -6
  245. package/dist/templates/common/skills/brainstorm.md +68 -504
  246. package/dist/templates/common/skills/check.md +7 -1
  247. package/dist/templates/copilot/hooks/session-start.py +219 -101
  248. package/dist/templates/copilot/hooks.json +2 -2
  249. package/dist/templates/copilot/prompts/before-dev.prompt.md +12 -6
  250. package/dist/templates/copilot/prompts/brainstorm.prompt.md +69 -457
  251. package/dist/templates/copilot/prompts/check.prompt.md +86 -18
  252. package/dist/templates/copilot/prompts/parallel.prompt.md +16 -8
  253. package/dist/templates/copilot/prompts/start.prompt.md +33 -367
  254. package/dist/templates/cursor/agents/trellis-check.md +13 -7
  255. package/dist/templates/cursor/agents/trellis-implement.md +8 -7
  256. package/dist/templates/cursor/hooks.json +1 -7
  257. package/dist/templates/droid/droids/trellis-check.md +13 -7
  258. package/dist/templates/droid/droids/trellis-implement.md +8 -7
  259. package/dist/templates/droid/settings.json +4 -4
  260. package/dist/templates/gemini/agents/trellis-check.md +11 -5
  261. package/dist/templates/gemini/agents/trellis-implement.md +7 -6
  262. package/dist/templates/gemini/settings.json +2 -2
  263. package/dist/templates/kiro/agents/trellis-check.json +1 -1
  264. package/dist/templates/kiro/agents/trellis-implement.json +1 -1
  265. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +127 -9
  266. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +171 -6
  267. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +333 -43
  268. package/dist/templates/markdown/spec/guides/index.md.txt +18 -0
  269. package/dist/templates/opencode/agents/trellis-check.md +13 -7
  270. package/dist/templates/opencode/agents/trellis-implement.md +9 -8
  271. package/dist/templates/opencode/lib/session-utils.js +212 -123
  272. package/dist/templates/opencode/lib/trellis-context.js +73 -11
  273. package/dist/templates/opencode/plugins/inject-subagent-context.js +131 -29
  274. package/dist/templates/opencode/plugins/inject-workflow-state.js +9 -5
  275. package/dist/templates/opencode/plugins/session-start.js +9 -1
  276. package/dist/templates/pi/agents/trellis-check.md +5 -4
  277. package/dist/templates/pi/agents/trellis-implement.md +5 -4
  278. package/dist/templates/pi/extensions/trellis/index.ts.txt +1357 -754
  279. package/dist/templates/qoder/agents/trellis-check.md +11 -5
  280. package/dist/templates/qoder/agents/trellis-implement.md +7 -6
  281. package/dist/templates/qoder/settings.json +4 -4
  282. package/dist/templates/shared-hooks/index.d.ts.map +1 -1
  283. package/dist/templates/shared-hooks/index.js +0 -1
  284. package/dist/templates/shared-hooks/index.js.map +1 -1
  285. package/dist/templates/shared-hooks/inject-subagent-context.py +36 -14
  286. package/dist/templates/shared-hooks/inject-workflow-state.py +40 -42
  287. package/dist/templates/shared-hooks/session-start.py +222 -171
  288. package/dist/templates/trellis/config.yaml +38 -0
  289. package/dist/templates/trellis/index.d.ts +1 -0
  290. package/dist/templates/trellis/index.d.ts.map +1 -1
  291. package/dist/templates/trellis/index.js +2 -0
  292. package/dist/templates/trellis/index.js.map +1 -1
  293. package/dist/templates/trellis/scripts/add_session.py +50 -24
  294. package/dist/templates/trellis/scripts/common/config.py +57 -1
  295. package/dist/templates/trellis/scripts/common/safe_commit.py +285 -0
  296. package/dist/templates/trellis/scripts/common/session_context.py +384 -137
  297. package/dist/templates/trellis/scripts/common/task_context.py +3 -3
  298. package/dist/templates/trellis/scripts/common/task_store.py +161 -15
  299. package/dist/templates/trellis/scripts/common/workflow_phase.py +7 -10
  300. package/dist/templates/trellis/scripts/task.py +3 -3
  301. package/dist/templates/trellis/workflow.md +119 -98
  302. package/dist/utils/cwd-guard.d.ts +38 -0
  303. package/dist/utils/cwd-guard.d.ts.map +1 -0
  304. package/dist/utils/cwd-guard.js +62 -0
  305. package/dist/utils/cwd-guard.js.map +1 -0
  306. package/dist/utils/file-writer.d.ts +13 -0
  307. package/dist/utils/file-writer.d.ts.map +1 -1
  308. package/dist/utils/file-writer.js +59 -1
  309. package/dist/utils/file-writer.js.map +1 -1
  310. package/dist/utils/manifest-prune.d.ts +61 -0
  311. package/dist/utils/manifest-prune.d.ts.map +1 -0
  312. package/dist/utils/manifest-prune.js +136 -0
  313. package/dist/utils/manifest-prune.js.map +1 -0
  314. package/dist/utils/task-json.d.ts +9 -42
  315. package/dist/utils/task-json.d.ts.map +1 -1
  316. package/dist/utils/task-json.js +8 -45
  317. package/dist/utils/task-json.js.map +1 -1
  318. package/dist/utils/template-hash.d.ts +32 -6
  319. package/dist/utils/template-hash.d.ts.map +1 -1
  320. package/dist/utils/template-hash.js +53 -31
  321. package/dist/utils/template-hash.js.map +1 -1
  322. package/dist/utils/uninstall-scrubbers.d.ts +1 -0
  323. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  324. package/dist/utils/uninstall-scrubbers.js +21 -0
  325. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  326. package/dist/utils/workflow-resolver.d.ts +86 -0
  327. package/dist/utils/workflow-resolver.d.ts.map +1 -0
  328. package/dist/utils/workflow-resolver.js +265 -0
  329. package/dist/utils/workflow-resolver.js.map +1 -0
  330. package/package.json +9 -8
@@ -14,8 +14,12 @@ Provides:
14
14
  from __future__ import annotations
15
15
 
16
16
  import json
17
+ import os
18
+ import re
19
+ import subprocess
17
20
  from pathlib import Path
18
21
 
22
+ from .active_task import resolve_context_key
19
23
  from .config import get_git_packages
20
24
  from .git import run_git
21
25
  from .packages_context import get_packages_section
@@ -40,10 +44,146 @@ from .paths import (
40
44
  # Helpers
41
45
  # =============================================================================
42
46
 
43
- def _collect_package_git_info(repo_root: Path) -> list[dict]:
44
- """Collect git status and recent commits for packages with independent git repos.
47
+ _PACKAGE_NAME = "@mindfoldhq/trellis"
48
+ _UPDATE_CHECK_TIMEOUT_SECONDS = 1.0
49
+ _VERSION_RE = re.compile(
50
+ r"^\s*(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z.-]+))?\s*$"
51
+ )
52
+ _VERSION_TOKEN_RE = re.compile(r"\b\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?\b")
53
+ _POLYREPO_IGNORED_DIRS = {
54
+ "node_modules",
55
+ "target",
56
+ "dist",
57
+ "build",
58
+ "out",
59
+ "bin",
60
+ "obj",
61
+ "vendor",
62
+ "coverage",
63
+ "tmp",
64
+ "__pycache__",
65
+ }
66
+ _POLYREPO_SCAN_MAX_DEPTH = 2
67
+
68
+
69
+ def _is_git_worktree(path: Path) -> bool:
70
+ """Return True when path is inside a Git worktree."""
71
+ rc, out, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=path)
72
+ return rc == 0 and out.strip().lower() == "true"
73
+
74
+
75
+ def _parse_recent_commits(log_output: str) -> list[dict]:
76
+ """Parse `git log --oneline` output into structured commit entries."""
77
+ commits = []
78
+ for line in log_output.splitlines():
79
+ if not line.strip():
80
+ continue
81
+ parts = line.split(" ", 1)
82
+ if len(parts) >= 2:
83
+ commits.append({"hash": parts[0], "message": parts[1]})
84
+ elif len(parts) == 1:
85
+ commits.append({"hash": parts[0], "message": ""})
86
+ return commits
87
+
88
+
89
+ def _collect_git_repo_info(name: str, rel_path: str, repo_dir: Path) -> dict | None:
90
+ """Collect Git status for one known repository directory."""
91
+ if not (repo_dir / ".git").exists():
92
+ return None
93
+
94
+ _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_dir)
95
+ branch = branch_out.strip() or "unknown"
96
+
97
+ _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_dir)
98
+ changes = len([l for l in status_out.splitlines() if l.strip()])
99
+
100
+ _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_dir)
101
+
102
+ return {
103
+ "name": name,
104
+ "path": rel_path,
105
+ "branch": branch,
106
+ "isClean": changes == 0,
107
+ "uncommittedChanges": changes,
108
+ "recentCommits": _parse_recent_commits(log_out),
109
+ }
110
+
111
+
112
+ def _collect_root_git_info(repo_root: Path) -> dict:
113
+ """Collect root Git info without pretending a non-Git root is clean."""
114
+ if not _is_git_worktree(repo_root):
115
+ return {
116
+ "isRepo": False,
117
+ "branch": "",
118
+ "isClean": False,
119
+ "uncommittedChanges": 0,
120
+ "recentCommits": [],
121
+ }
122
+
123
+ _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
124
+ branch = branch_out.strip() or "unknown"
125
+
126
+ _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
127
+ status_lines = [line for line in status_out.splitlines() if line.strip()]
128
+
129
+ _, short_out, _ = run_git(["status", "--short"], cwd=repo_root)
130
+
131
+ _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
132
+
133
+ return {
134
+ "isRepo": True,
135
+ "branch": branch,
136
+ "isClean": len(status_lines) == 0,
137
+ "uncommittedChanges": len(status_lines),
138
+ "statusShort": short_out.splitlines(),
139
+ "recentCommits": _parse_recent_commits(log_out),
140
+ }
141
+
142
+
143
+ def _discover_child_git_repos(repo_root: Path) -> list[tuple[str, str]]:
144
+ """Discover child Git repositories using the init-time polyrepo heuristic."""
145
+ found: list[str] = []
146
+
147
+ def is_candidate_dir(path: Path) -> bool:
148
+ name = path.name
149
+ return not name.startswith(".") and name not in _POLYREPO_IGNORED_DIRS
45
150
 
46
- Only packages marked with ``git: true`` in config.yaml are included.
151
+ def scan(rel_dir: Path, depth: int) -> None:
152
+ if depth >= _POLYREPO_SCAN_MAX_DEPTH:
153
+ return
154
+ abs_dir = repo_root / rel_dir
155
+ try:
156
+ children = sorted(abs_dir.iterdir(), key=lambda p: p.name)
157
+ except OSError:
158
+ return
159
+
160
+ for child in children:
161
+ if not child.is_dir() or not is_candidate_dir(child):
162
+ continue
163
+
164
+ child_rel = (
165
+ rel_dir / child.name if rel_dir != Path(".") else Path(child.name)
166
+ )
167
+ if (child / ".git").exists():
168
+ found.append(child_rel.as_posix())
169
+ continue
170
+ scan(child_rel, depth + 1)
171
+
172
+ scan(Path("."), 0)
173
+ if len(found) < 2:
174
+ return []
175
+ return [(path.replace("/", "_"), path) for path in sorted(found)]
176
+
177
+
178
+ def _collect_package_git_info(
179
+ repo_root: Path,
180
+ discover_unconfigured: bool = False,
181
+ ) -> list[dict]:
182
+ """Collect Git status for independent package repositories.
183
+
184
+ Packages marked with ``git: true`` in config.yaml are authoritative.
185
+ When the Trellis root is not a Git repo and no configured package repos are
186
+ available, optionally fall back to the bounded polyrepo child scan.
47
187
 
48
188
  Returns:
49
189
  List of dicts with keys: name, path, branch, isClean,
@@ -51,41 +191,56 @@ def _collect_package_git_info(repo_root: Path) -> list[dict]:
51
191
  Empty list if no git-repo packages are configured.
52
192
  """
53
193
  git_pkgs = get_git_packages(repo_root)
54
- if not git_pkgs:
55
- return []
56
-
57
194
  result = []
58
195
  for pkg_name, pkg_path in git_pkgs.items():
59
196
  pkg_dir = repo_root / pkg_path
60
- if not (pkg_dir / ".git").exists():
61
- continue
197
+ info = _collect_git_repo_info(pkg_name, pkg_path, pkg_dir)
198
+ if info is not None:
199
+ result.append(info)
62
200
 
63
- _, branch_out, _ = run_git(["branch", "--show-current"], cwd=pkg_dir)
64
- branch = branch_out.strip() or "unknown"
65
-
66
- _, status_out, _ = run_git(["status", "--porcelain"], cwd=pkg_dir)
67
- changes = len([l for l in status_out.splitlines() if l.strip()])
68
-
69
- _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=pkg_dir)
70
- commits = []
71
- for line in log_out.splitlines():
72
- if line.strip():
73
- parts = line.split(" ", 1)
74
- if len(parts) >= 2:
75
- commits.append({"hash": parts[0], "message": parts[1]})
76
- elif len(parts) == 1:
77
- commits.append({"hash": parts[0], "message": ""})
78
-
79
- result.append({
80
- "name": pkg_name,
81
- "path": pkg_path,
82
- "branch": branch,
83
- "isClean": changes == 0,
84
- "uncommittedChanges": changes,
85
- "recentCommits": commits,
86
- })
201
+ if result or not discover_unconfigured:
202
+ return result
87
203
 
88
- return result
204
+ discovered = []
205
+ for pkg_name, pkg_path in _discover_child_git_repos(repo_root):
206
+ info = _collect_git_repo_info(pkg_name, pkg_path, repo_root / pkg_path)
207
+ if info is not None:
208
+ discovered.append(info)
209
+ return discovered
210
+
211
+
212
+ def _append_root_git_context(lines: list[str], root_git_info: dict) -> None:
213
+ """Append root Git status without misleading non-Git roots."""
214
+ lines.append("## GIT STATUS")
215
+ if not root_git_info["isRepo"]:
216
+ lines.append("Root is not a Git repository.")
217
+ lines.append("Run Git commands from the package repository paths listed below.")
218
+ else:
219
+ lines.append(f"Branch: {root_git_info['branch']}")
220
+ if root_git_info["isClean"]:
221
+ lines.append("Working directory: Clean")
222
+ else:
223
+ lines.append(
224
+ f"Working directory: {root_git_info['uncommittedChanges']} "
225
+ "uncommitted change(s)"
226
+ )
227
+ lines.append("")
228
+ lines.append("Changes:")
229
+ for line in root_git_info.get("statusShort", [])[:10]:
230
+ lines.append(line)
231
+ lines.append("")
232
+
233
+ lines.append("## RECENT COMMITS")
234
+ if not root_git_info["isRepo"]:
235
+ lines.append(
236
+ "Root has no Git commit history because it is not a Git repository."
237
+ )
238
+ elif root_git_info["recentCommits"]:
239
+ for commit in root_git_info["recentCommits"]:
240
+ lines.append(f"{commit['hash']} {commit['message']}")
241
+ else:
242
+ lines.append("(no commits)")
243
+ lines.append("")
89
244
 
90
245
 
91
246
  def _append_package_git_context(lines: list[str], package_git_info: list[dict]) -> None:
@@ -109,6 +264,158 @@ def _append_package_git_context(lines: list[str], package_git_info: list[dict])
109
264
  lines.append("")
110
265
 
111
266
 
267
+ def _read_project_version(repo_root: Path) -> str | None:
268
+ try:
269
+ version = (repo_root / DIR_WORKFLOW / ".version").read_text(
270
+ encoding="utf-8"
271
+ ).strip()
272
+ except OSError:
273
+ return None
274
+ return version or None
275
+
276
+
277
+ def _fetch_trellis_version_output() -> str | None:
278
+ try:
279
+ result = subprocess.run(
280
+ ["trellis", "--version"],
281
+ capture_output=True,
282
+ text=True,
283
+ encoding="utf-8",
284
+ errors="replace",
285
+ timeout=_UPDATE_CHECK_TIMEOUT_SECONDS,
286
+ )
287
+ except (OSError, subprocess.SubprocessError, TimeoutError):
288
+ return None
289
+
290
+ if result.returncode != 0:
291
+ return None
292
+ output = f"{result.stdout}\n{result.stderr}".strip()
293
+ return output or None
294
+
295
+
296
+ def _extract_available_update_version(output: str) -> str | None:
297
+ update_match = re.search(
298
+ r"Trellis update available:\s*"
299
+ r"(?P<current>\S+)\s*(?:→|->)\s*(?P<latest>\S+)",
300
+ output,
301
+ )
302
+ if update_match:
303
+ return update_match.group("latest").strip()
304
+ candidates = _VERSION_TOKEN_RE.findall(output)
305
+ return candidates[-1] if candidates else None
306
+
307
+
308
+ def _resolve_available_update_version() -> str | None:
309
+ output = _fetch_trellis_version_output()
310
+ if not output:
311
+ return None
312
+ return _extract_available_update_version(output)
313
+
314
+
315
+ def _parse_version(version: str) -> tuple[tuple[int, int, int], tuple[str, ...] | None] | None:
316
+ match = _VERSION_RE.match(version)
317
+ if not match:
318
+ return None
319
+ major, minor, patch, prerelease = match.groups()
320
+ numbers = (int(major), int(minor or "0"), int(patch or "0"))
321
+ prerelease_parts = tuple(prerelease.split(".")) if prerelease else None
322
+ return numbers, prerelease_parts
323
+
324
+
325
+ def _compare_prerelease(
326
+ left: tuple[str, ...] | None,
327
+ right: tuple[str, ...] | None,
328
+ ) -> int:
329
+ if left is None and right is None:
330
+ return 0
331
+ if left is None:
332
+ return 1
333
+ if right is None:
334
+ return -1
335
+
336
+ for left_part, right_part in zip(left, right):
337
+ if left_part == right_part:
338
+ continue
339
+ left_numeric = left_part.isdigit()
340
+ right_numeric = right_part.isdigit()
341
+ if left_numeric and right_numeric:
342
+ left_int = int(left_part)
343
+ right_int = int(right_part)
344
+ return (left_int > right_int) - (left_int < right_int)
345
+ if left_numeric:
346
+ return -1
347
+ if right_numeric:
348
+ return 1
349
+ return (left_part > right_part) - (left_part < right_part)
350
+
351
+ return (len(left) > len(right)) - (len(left) < len(right))
352
+
353
+
354
+ def _compare_versions(left: str, right: str) -> int | None:
355
+ parsed_left = _parse_version(left)
356
+ parsed_right = _parse_version(right)
357
+ if parsed_left is None or parsed_right is None:
358
+ return None
359
+
360
+ left_numbers, left_prerelease = parsed_left
361
+ right_numbers, right_prerelease = parsed_right
362
+ if left_numbers != right_numbers:
363
+ return (left_numbers > right_numbers) - (left_numbers < right_numbers)
364
+ return _compare_prerelease(left_prerelease, right_prerelease)
365
+
366
+
367
+ def _update_marker_path(repo_root: Path) -> Path:
368
+ context_key = resolve_context_key()
369
+ if not context_key:
370
+ terminal_key = os.environ.get("TERM_SESSION_ID", "").strip()
371
+ context_key = terminal_key or f"ppid-{os.getppid()}"
372
+ safe_key = re.sub(r"[^A-Za-z0-9._-]+", "_", context_key).strip("._-")
373
+ if not safe_key:
374
+ safe_key = "session"
375
+ return (
376
+ repo_root
377
+ / DIR_WORKFLOW
378
+ / ".runtime"
379
+ / f"update-check-{safe_key[:160]}.marker"
380
+ )
381
+
382
+
383
+ def _mark_update_check_attempted(repo_root: Path) -> bool:
384
+ marker_path = _update_marker_path(repo_root)
385
+ if marker_path.exists():
386
+ return False
387
+ try:
388
+ marker_path.parent.mkdir(parents=True, exist_ok=True)
389
+ marker_path.write_text("checked\n", encoding="utf-8")
390
+ except OSError:
391
+ pass
392
+ return True
393
+
394
+
395
+ def _get_update_hint(repo_root: Path) -> str | None:
396
+ marker_path = _update_marker_path(repo_root)
397
+ if marker_path.exists():
398
+ return None
399
+
400
+ current_version = _read_project_version(repo_root)
401
+ if not current_version:
402
+ return None
403
+
404
+ latest_version = _resolve_available_update_version()
405
+ if not latest_version:
406
+ return None
407
+
408
+ _mark_update_check_attempted(repo_root)
409
+ comparison = _compare_versions(current_version, latest_version)
410
+ if comparison is None or comparison >= 0:
411
+ return None
412
+
413
+ return (
414
+ f"Trellis update available: {current_version} -> {latest_version}, "
415
+ "run trellis upgrade"
416
+ )
417
+
418
+
112
419
  # =============================================================================
113
420
  # JSON Output
114
421
  # =============================================================================
@@ -137,24 +444,7 @@ def get_context_json(repo_root: Path | None = None) -> dict:
137
444
  f"{DIR_WORKFLOW}/{DIR_WORKSPACE}/{developer}/{journal_file.name}"
138
445
  )
139
446
 
140
- # Git info
141
- _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
142
- branch = branch_out.strip() or "unknown"
143
-
144
- _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
145
- git_status_count = len([line for line in status_out.splitlines() if line.strip()])
146
- is_clean = git_status_count == 0
147
-
148
- # Recent commits
149
- _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
150
- commits = []
151
- for line in log_out.splitlines():
152
- if line.strip():
153
- parts = line.split(" ", 1)
154
- if len(parts) >= 2:
155
- commits.append({"hash": parts[0], "message": parts[1]})
156
- elif len(parts) == 1:
157
- commits.append({"hash": parts[0], "message": ""})
447
+ root_git_info = _collect_root_git_info(repo_root)
158
448
 
159
449
  # Tasks
160
450
  tasks = [
@@ -169,15 +459,19 @@ def get_context_json(repo_root: Path | None = None) -> dict:
169
459
  ]
170
460
 
171
461
  # Package git repos (independent sub-repositories)
172
- pkg_git_info = _collect_package_git_info(repo_root)
462
+ pkg_git_info = _collect_package_git_info(
463
+ repo_root,
464
+ discover_unconfigured=not root_git_info["isRepo"],
465
+ )
173
466
 
174
467
  result = {
175
468
  "developer": developer or "",
176
469
  "git": {
177
- "branch": branch,
178
- "isClean": is_clean,
179
- "uncommittedChanges": git_status_count,
180
- "recentCommits": commits,
470
+ "isRepo": root_git_info["isRepo"],
471
+ "branch": root_git_info["branch"],
472
+ "isClean": root_git_info["isClean"],
473
+ "uncommittedChanges": root_git_info["uncommittedChanges"],
474
+ "recentCommits": root_git_info["recentCommits"],
181
475
  },
182
476
  "tasks": {
183
477
  "active": tasks,
@@ -241,39 +535,17 @@ def get_context_text(repo_root: Path | None = None) -> str:
241
535
  lines.append(f"Name: {developer}")
242
536
  lines.append("")
243
537
 
244
- # Git status
245
- lines.append("## GIT STATUS")
246
- _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
247
- branch = branch_out.strip() or "unknown"
248
- lines.append(f"Branch: {branch}")
249
-
250
- _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
251
- status_lines = [line for line in status_out.splitlines() if line.strip()]
252
- status_count = len(status_lines)
253
-
254
- if status_count == 0:
255
- lines.append("Working directory: Clean")
256
- else:
257
- lines.append(f"Working directory: {status_count} uncommitted change(s)")
258
- lines.append("")
259
- lines.append("Changes:")
260
- _, short_out, _ = run_git(["status", "--short"], cwd=repo_root)
261
- for line in short_out.splitlines()[:10]:
262
- lines.append(line)
263
- lines.append("")
264
-
265
- # Recent commits
266
- lines.append("## RECENT COMMITS")
267
- _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
268
- if log_out.strip():
269
- for line in log_out.splitlines():
270
- lines.append(line)
271
- else:
272
- lines.append("(no commits)")
273
- lines.append("")
538
+ root_git_info = _collect_root_git_info(repo_root)
539
+ _append_root_git_context(lines, root_git_info)
274
540
 
275
541
  # Package git repos — independent sub-repositories
276
- _append_package_git_context(lines, _collect_package_git_info(repo_root))
542
+ _append_package_git_context(
543
+ lines,
544
+ _collect_package_git_info(
545
+ repo_root,
546
+ discover_unconfigured=not root_git_info["isRepo"],
547
+ ),
548
+ )
277
549
 
278
550
  # Current task
279
551
  lines.append("## CURRENT TASK")
@@ -393,20 +665,7 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
393
665
  developer = get_developer(repo_root)
394
666
  tasks_dir = get_tasks_dir(repo_root)
395
667
 
396
- # Git info
397
- _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
398
- branch = branch_out.strip() or "unknown"
399
-
400
- _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
401
- git_status_count = len([line for line in status_out.splitlines() if line.strip()])
402
-
403
- _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
404
- commits = []
405
- for line in log_out.splitlines():
406
- if line.strip():
407
- parts = line.split(" ", 1)
408
- if len(parts) >= 2:
409
- commits.append({"hash": parts[0], "message": parts[1]})
668
+ root_git_info = _collect_root_git_info(repo_root)
410
669
 
411
670
  # My tasks (single pass — collect statuses and filter by assignee)
412
671
  all_tasks_list = list(iter_active_tasks(tasks_dir))
@@ -446,15 +705,19 @@ def get_context_record_json(repo_root: Path | None = None) -> dict:
446
705
  }
447
706
 
448
707
  # Package git repos
449
- pkg_git_info = _collect_package_git_info(repo_root)
708
+ pkg_git_info = _collect_package_git_info(
709
+ repo_root,
710
+ discover_unconfigured=not root_git_info["isRepo"],
711
+ )
450
712
 
451
713
  result = {
452
714
  "developer": developer or "",
453
715
  "git": {
454
- "branch": branch,
455
- "isClean": git_status_count == 0,
456
- "uncommittedChanges": git_status_count,
457
- "recentCommits": commits,
716
+ "isRepo": root_git_info["isRepo"],
717
+ "branch": root_git_info["branch"],
718
+ "isClean": root_git_info["isClean"],
719
+ "uncommittedChanges": root_git_info["uncommittedChanges"],
720
+ "recentCommits": root_git_info["recentCommits"],
458
721
  },
459
722
  "myTasks": my_tasks,
460
723
  "currentTask": current_task_info,
@@ -509,39 +772,17 @@ def get_context_text_record(repo_root: Path | None = None) -> str:
509
772
  lines.append("(no active tasks assigned to you)")
510
773
  lines.append("")
511
774
 
512
- # GIT STATUS
513
- lines.append("## GIT STATUS")
514
- _, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
515
- branch = branch_out.strip() or "unknown"
516
- lines.append(f"Branch: {branch}")
517
-
518
- _, status_out, _ = run_git(["status", "--porcelain"], cwd=repo_root)
519
- status_lines = [line for line in status_out.splitlines() if line.strip()]
520
- status_count = len(status_lines)
521
-
522
- if status_count == 0:
523
- lines.append("Working directory: Clean")
524
- else:
525
- lines.append(f"Working directory: {status_count} uncommitted change(s)")
526
- lines.append("")
527
- lines.append("Changes:")
528
- _, short_out, _ = run_git(["status", "--short"], cwd=repo_root)
529
- for line in short_out.splitlines()[:10]:
530
- lines.append(line)
531
- lines.append("")
532
-
533
- # RECENT COMMITS
534
- lines.append("## RECENT COMMITS")
535
- _, log_out, _ = run_git(["log", "--oneline", "-5"], cwd=repo_root)
536
- if log_out.strip():
537
- for line in log_out.splitlines():
538
- lines.append(line)
539
- else:
540
- lines.append("(no commits)")
541
- lines.append("")
775
+ root_git_info = _collect_root_git_info(repo_root)
776
+ _append_root_git_context(lines, root_git_info)
542
777
 
543
778
  # Package git repos — independent sub-repositories
544
- _append_package_git_context(lines, _collect_package_git_info(repo_root))
779
+ _append_package_git_context(
780
+ lines,
781
+ _collect_package_git_info(
782
+ repo_root,
783
+ discover_unconfigured=not root_git_info["isRepo"],
784
+ ),
785
+ )
545
786
 
546
787
  # CURRENT TASK
547
788
  lines.append("## CURRENT TASK")
@@ -571,4 +812,10 @@ def output_text(repo_root: Path | None = None) -> None:
571
812
  Args:
572
813
  repo_root: Repository root path. Defaults to auto-detected.
573
814
  """
815
+ if repo_root is None:
816
+ repo_root = get_repo_root()
817
+ update_hint = _get_update_hint(repo_root)
818
+ if update_hint:
819
+ print(update_hint)
820
+ print("")
574
821
  print(get_context_text(repo_root))
@@ -10,9 +10,9 @@ Provides:
10
10
  Note:
11
11
  ``cmd_init_context`` was removed in v0.5.0-beta.12. JSONL context files
12
12
  are now seeded at ``task.py create`` time with a self-describing
13
- ``_example`` line; the AI agent curates real entries during Phase 1.3 of
14
- the workflow. See ``.trellis/workflow.md`` Phase 1.3 for the current
15
- instructions.
13
+ ``_example`` line; the AI agent curates real entries during planning when
14
+ the task needs sub-agent/spec context. See ``.trellis/workflow.md`` for the
15
+ current planning artifact contract.
16
16
  """
17
17
 
18
18
  from __future__ import annotations