@pennixrv/trellis 0.6.17

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 (831) hide show
  1. package/LICENSE +235 -0
  2. package/README.md +168 -0
  3. package/bin/trellis.js +3 -0
  4. package/dist/cli/index.d.ts +3 -0
  5. package/dist/cli/index.d.ts.map +1 -0
  6. package/dist/cli/index.js +299 -0
  7. package/dist/cli/index.js.map +1 -0
  8. package/dist/commands/ablate.d.ts +14 -0
  9. package/dist/commands/ablate.d.ts.map +1 -0
  10. package/dist/commands/ablate.js +307 -0
  11. package/dist/commands/ablate.js.map +1 -0
  12. package/dist/commands/channel/adapters/claude.d.ts +53 -0
  13. package/dist/commands/channel/adapters/claude.d.ts.map +1 -0
  14. package/dist/commands/channel/adapters/claude.js +225 -0
  15. package/dist/commands/channel/adapters/claude.js.map +1 -0
  16. package/dist/commands/channel/adapters/codex.d.ts +93 -0
  17. package/dist/commands/channel/adapters/codex.d.ts.map +1 -0
  18. package/dist/commands/channel/adapters/codex.js +573 -0
  19. package/dist/commands/channel/adapters/codex.js.map +1 -0
  20. package/dist/commands/channel/adapters/index.d.ts +94 -0
  21. package/dist/commands/channel/adapters/index.d.ts.map +1 -0
  22. package/dist/commands/channel/adapters/index.js +116 -0
  23. package/dist/commands/channel/adapters/index.js.map +1 -0
  24. package/dist/commands/channel/adapters/types.d.ts +33 -0
  25. package/dist/commands/channel/adapters/types.d.ts.map +1 -0
  26. package/dist/commands/channel/adapters/types.js +2 -0
  27. package/dist/commands/channel/adapters/types.js.map +1 -0
  28. package/dist/commands/channel/agent-loader.d.ts +32 -0
  29. package/dist/commands/channel/agent-loader.d.ts.map +1 -0
  30. package/dist/commands/channel/agent-loader.js +158 -0
  31. package/dist/commands/channel/agent-loader.js.map +1 -0
  32. package/dist/commands/channel/context-loader.d.ts +26 -0
  33. package/dist/commands/channel/context-loader.d.ts.map +1 -0
  34. package/dist/commands/channel/context-loader.js +311 -0
  35. package/dist/commands/channel/context-loader.js.map +1 -0
  36. package/dist/commands/channel/context-trust.d.ts +31 -0
  37. package/dist/commands/channel/context-trust.d.ts.map +1 -0
  38. package/dist/commands/channel/context-trust.js +139 -0
  39. package/dist/commands/channel/context-trust.js.map +1 -0
  40. package/dist/commands/channel/context.d.ts +16 -0
  41. package/dist/commands/channel/context.d.ts.map +1 -0
  42. package/dist/commands/channel/context.js +83 -0
  43. package/dist/commands/channel/context.js.map +1 -0
  44. package/dist/commands/channel/create.d.ts +27 -0
  45. package/dist/commands/channel/create.d.ts.map +1 -0
  46. package/dist/commands/channel/create.js +39 -0
  47. package/dist/commands/channel/create.js.map +1 -0
  48. package/dist/commands/channel/dev-parse-trace.d.ts +14 -0
  49. package/dist/commands/channel/dev-parse-trace.d.ts.map +1 -0
  50. package/dist/commands/channel/dev-parse-trace.js +70 -0
  51. package/dist/commands/channel/dev-parse-trace.js.map +1 -0
  52. package/dist/commands/channel/guard.d.ts +150 -0
  53. package/dist/commands/channel/guard.d.ts.map +1 -0
  54. package/dist/commands/channel/guard.js +479 -0
  55. package/dist/commands/channel/guard.js.map +1 -0
  56. package/dist/commands/channel/index.d.ts +3 -0
  57. package/dist/commands/channel/index.d.ts.map +1 -0
  58. package/dist/commands/channel/index.js +535 -0
  59. package/dist/commands/channel/index.js.map +1 -0
  60. package/dist/commands/channel/interrupt.d.ts +10 -0
  61. package/dist/commands/channel/interrupt.d.ts.map +1 -0
  62. package/dist/commands/channel/interrupt.js +22 -0
  63. package/dist/commands/channel/interrupt.js.map +1 -0
  64. package/dist/commands/channel/kill.d.ts +7 -0
  65. package/dist/commands/channel/kill.d.ts.map +1 -0
  66. package/dist/commands/channel/kill.js +130 -0
  67. package/dist/commands/channel/kill.js.map +1 -0
  68. package/dist/commands/channel/list.d.ts +17 -0
  69. package/dist/commands/channel/list.d.ts.map +1 -0
  70. package/dist/commands/channel/list.js +233 -0
  71. package/dist/commands/channel/list.js.map +1 -0
  72. package/dist/commands/channel/messages.d.ts +15 -0
  73. package/dist/commands/channel/messages.d.ts.map +1 -0
  74. package/dist/commands/channel/messages.js +245 -0
  75. package/dist/commands/channel/messages.js.map +1 -0
  76. package/dist/commands/channel/rm.d.ts +27 -0
  77. package/dist/commands/channel/rm.d.ts.map +1 -0
  78. package/dist/commands/channel/rm.js +216 -0
  79. package/dist/commands/channel/rm.js.map +1 -0
  80. package/dist/commands/channel/run.d.ts +30 -0
  81. package/dist/commands/channel/run.d.ts.map +1 -0
  82. package/dist/commands/channel/run.js +134 -0
  83. package/dist/commands/channel/run.js.map +1 -0
  84. package/dist/commands/channel/send.d.ts +11 -0
  85. package/dist/commands/channel/send.d.ts.map +1 -0
  86. package/dist/commands/channel/send.js +24 -0
  87. package/dist/commands/channel/send.js.map +1 -0
  88. package/dist/commands/channel/spawn.d.ts +43 -0
  89. package/dist/commands/channel/spawn.d.ts.map +1 -0
  90. package/dist/commands/channel/spawn.js +247 -0
  91. package/dist/commands/channel/spawn.js.map +1 -0
  92. package/dist/commands/channel/store/events.d.ts +22 -0
  93. package/dist/commands/channel/store/events.d.ts.map +1 -0
  94. package/dist/commands/channel/store/events.js +43 -0
  95. package/dist/commands/channel/store/events.js.map +1 -0
  96. package/dist/commands/channel/store/filter.d.ts +3 -0
  97. package/dist/commands/channel/store/filter.d.ts.map +1 -0
  98. package/dist/commands/channel/store/filter.js +2 -0
  99. package/dist/commands/channel/store/filter.js.map +1 -0
  100. package/dist/commands/channel/store/lock.d.ts +23 -0
  101. package/dist/commands/channel/store/lock.d.ts.map +1 -0
  102. package/dist/commands/channel/store/lock.js +99 -0
  103. package/dist/commands/channel/store/lock.js.map +1 -0
  104. package/dist/commands/channel/store/paths.d.ts +77 -0
  105. package/dist/commands/channel/store/paths.d.ts.map +1 -0
  106. package/dist/commands/channel/store/paths.js +272 -0
  107. package/dist/commands/channel/store/paths.js.map +1 -0
  108. package/dist/commands/channel/store/schema.d.ts +27 -0
  109. package/dist/commands/channel/store/schema.d.ts.map +1 -0
  110. package/dist/commands/channel/store/schema.js +34 -0
  111. package/dist/commands/channel/store/schema.js.map +1 -0
  112. package/dist/commands/channel/store/thread-state.d.ts +5 -0
  113. package/dist/commands/channel/store/thread-state.d.ts.map +1 -0
  114. package/dist/commands/channel/store/thread-state.js +16 -0
  115. package/dist/commands/channel/store/thread-state.js.map +1 -0
  116. package/dist/commands/channel/store/watch.d.ts +19 -0
  117. package/dist/commands/channel/store/watch.d.ts.map +1 -0
  118. package/dist/commands/channel/store/watch.js +153 -0
  119. package/dist/commands/channel/store/watch.js.map +1 -0
  120. package/dist/commands/channel/supervisor/idle.d.ts +45 -0
  121. package/dist/commands/channel/supervisor/idle.d.ts.map +1 -0
  122. package/dist/commands/channel/supervisor/idle.js +70 -0
  123. package/dist/commands/channel/supervisor/idle.js.map +1 -0
  124. package/dist/commands/channel/supervisor/inbox.d.ts +30 -0
  125. package/dist/commands/channel/supervisor/inbox.d.ts.map +1 -0
  126. package/dist/commands/channel/supervisor/inbox.js +160 -0
  127. package/dist/commands/channel/supervisor/inbox.js.map +1 -0
  128. package/dist/commands/channel/supervisor/shutdown.d.ts +68 -0
  129. package/dist/commands/channel/supervisor/shutdown.d.ts.map +1 -0
  130. package/dist/commands/channel/supervisor/shutdown.js +146 -0
  131. package/dist/commands/channel/supervisor/shutdown.js.map +1 -0
  132. package/dist/commands/channel/supervisor/stdout.d.ts +71 -0
  133. package/dist/commands/channel/supervisor/stdout.d.ts.map +1 -0
  134. package/dist/commands/channel/supervisor/stdout.js +229 -0
  135. package/dist/commands/channel/supervisor/stdout.js.map +1 -0
  136. package/dist/commands/channel/supervisor/turns.d.ts +31 -0
  137. package/dist/commands/channel/supervisor/turns.d.ts.map +1 -0
  138. package/dist/commands/channel/supervisor/turns.js +45 -0
  139. package/dist/commands/channel/supervisor/turns.js.map +1 -0
  140. package/dist/commands/channel/supervisor/warning.d.ts +48 -0
  141. package/dist/commands/channel/supervisor/warning.d.ts.map +1 -0
  142. package/dist/commands/channel/supervisor/warning.js +77 -0
  143. package/dist/commands/channel/supervisor/warning.js.map +1 -0
  144. package/dist/commands/channel/supervisor.d.ts +91 -0
  145. package/dist/commands/channel/supervisor.d.ts.map +1 -0
  146. package/dist/commands/channel/supervisor.js +463 -0
  147. package/dist/commands/channel/supervisor.js.map +1 -0
  148. package/dist/commands/channel/text-body.d.ts +13 -0
  149. package/dist/commands/channel/text-body.d.ts.map +1 -0
  150. package/dist/commands/channel/text-body.js +47 -0
  151. package/dist/commands/channel/text-body.js.map +1 -0
  152. package/dist/commands/channel/threads.d.ts +39 -0
  153. package/dist/commands/channel/threads.d.ts.map +1 -0
  154. package/dist/commands/channel/threads.js +106 -0
  155. package/dist/commands/channel/threads.js.map +1 -0
  156. package/dist/commands/channel/title.d.ts +12 -0
  157. package/dist/commands/channel/title.d.ts.map +1 -0
  158. package/dist/commands/channel/title.js +24 -0
  159. package/dist/commands/channel/title.js.map +1 -0
  160. package/dist/commands/channel/wait.d.ts +17 -0
  161. package/dist/commands/channel/wait.d.ts.map +1 -0
  162. package/dist/commands/channel/wait.js +81 -0
  163. package/dist/commands/channel/wait.js.map +1 -0
  164. package/dist/commands/init.d.ts +71 -0
  165. package/dist/commands/init.d.ts.map +1 -0
  166. package/dist/commands/init.js +1603 -0
  167. package/dist/commands/init.js.map +1 -0
  168. package/dist/commands/mem.d.ts +30 -0
  169. package/dist/commands/mem.d.ts.map +1 -0
  170. package/dist/commands/mem.js +423 -0
  171. package/dist/commands/mem.js.map +1 -0
  172. package/dist/commands/uninstall.d.ts +36 -0
  173. package/dist/commands/uninstall.d.ts.map +1 -0
  174. package/dist/commands/uninstall.js +205 -0
  175. package/dist/commands/uninstall.js.map +1 -0
  176. package/dist/commands/update.d.ts +104 -0
  177. package/dist/commands/update.d.ts.map +1 -0
  178. package/dist/commands/update.js +2163 -0
  179. package/dist/commands/update.js.map +1 -0
  180. package/dist/commands/upgrade.d.ts +28 -0
  181. package/dist/commands/upgrade.d.ts.map +1 -0
  182. package/dist/commands/upgrade.js +84 -0
  183. package/dist/commands/upgrade.js.map +1 -0
  184. package/dist/commands/workflow.d.ts +35 -0
  185. package/dist/commands/workflow.d.ts.map +1 -0
  186. package/dist/commands/workflow.js +232 -0
  187. package/dist/commands/workflow.js.map +1 -0
  188. package/dist/configurators/antigravity.d.ts +7 -0
  189. package/dist/configurators/antigravity.d.ts.map +1 -0
  190. package/dist/configurators/antigravity.js +11 -0
  191. package/dist/configurators/antigravity.js.map +1 -0
  192. package/dist/configurators/claude.d.ts +23 -0
  193. package/dist/configurators/claude.d.ts.map +1 -0
  194. package/dist/configurators/claude.js +123 -0
  195. package/dist/configurators/claude.js.map +1 -0
  196. package/dist/configurators/codebuddy.d.ts +10 -0
  197. package/dist/configurators/codebuddy.d.ts.map +1 -0
  198. package/dist/configurators/codebuddy.js +24 -0
  199. package/dist/configurators/codebuddy.js.map +1 -0
  200. package/dist/configurators/codex.d.ts +57 -0
  201. package/dist/configurators/codex.d.ts.map +1 -0
  202. package/dist/configurators/codex.js +170 -0
  203. package/dist/configurators/codex.js.map +1 -0
  204. package/dist/configurators/copilot.d.ts +14 -0
  205. package/dist/configurators/copilot.d.ts.map +1 -0
  206. package/dist/configurators/copilot.js +46 -0
  207. package/dist/configurators/copilot.js.map +1 -0
  208. package/dist/configurators/cursor.d.ts +10 -0
  209. package/dist/configurators/cursor.d.ts.map +1 -0
  210. package/dist/configurators/cursor.js +23 -0
  211. package/dist/configurators/cursor.js.map +1 -0
  212. package/dist/configurators/devin.d.ts +8 -0
  213. package/dist/configurators/devin.d.ts.map +1 -0
  214. package/dist/configurators/devin.js +12 -0
  215. package/dist/configurators/devin.js.map +1 -0
  216. package/dist/configurators/droid.d.ts +10 -0
  217. package/dist/configurators/droid.d.ts.map +1 -0
  218. package/dist/configurators/droid.js +24 -0
  219. package/dist/configurators/droid.js.map +1 -0
  220. package/dist/configurators/dsh.d.ts +29 -0
  221. package/dist/configurators/dsh.d.ts.map +1 -0
  222. package/dist/configurators/dsh.js +64 -0
  223. package/dist/configurators/dsh.js.map +1 -0
  224. package/dist/configurators/gemini.d.ts +18 -0
  225. package/dist/configurators/gemini.d.ts.map +1 -0
  226. package/dist/configurators/gemini.js +39 -0
  227. package/dist/configurators/gemini.js.map +1 -0
  228. package/dist/configurators/grok.d.ts +17 -0
  229. package/dist/configurators/grok.d.ts.map +1 -0
  230. package/dist/configurators/grok.js +38 -0
  231. package/dist/configurators/grok.js.map +1 -0
  232. package/dist/configurators/index.d.ts +68 -0
  233. package/dist/configurators/index.d.ts.map +1 -0
  234. package/dist/configurators/index.js +191 -0
  235. package/dist/configurators/index.js.map +1 -0
  236. package/dist/configurators/kilo.d.ts +7 -0
  237. package/dist/configurators/kilo.d.ts.map +1 -0
  238. package/dist/configurators/kilo.js +11 -0
  239. package/dist/configurators/kilo.js.map +1 -0
  240. package/dist/configurators/kimi.d.ts +26 -0
  241. package/dist/configurators/kimi.d.ts.map +1 -0
  242. package/dist/configurators/kimi.js +74 -0
  243. package/dist/configurators/kimi.js.map +1 -0
  244. package/dist/configurators/kiro.d.ts +11 -0
  245. package/dist/configurators/kiro.d.ts.map +1 -0
  246. package/dist/configurators/kiro.js +30 -0
  247. package/dist/configurators/kiro.js.map +1 -0
  248. package/dist/configurators/omp.d.ts +6 -0
  249. package/dist/configurators/omp.d.ts.map +1 -0
  250. package/dist/configurators/omp.js +28 -0
  251. package/dist/configurators/omp.js.map +1 -0
  252. package/dist/configurators/opencode.d.ts +5 -0
  253. package/dist/configurators/opencode.d.ts.map +1 -0
  254. package/dist/configurators/opencode.js +80 -0
  255. package/dist/configurators/opencode.js.map +1 -0
  256. package/dist/configurators/pi.d.ts +5 -0
  257. package/dist/configurators/pi.d.ts.map +1 -0
  258. package/dist/configurators/pi.js +39 -0
  259. package/dist/configurators/pi.js.map +1 -0
  260. package/dist/configurators/qoder.d.ts +12 -0
  261. package/dist/configurators/qoder.d.ts.map +1 -0
  262. package/dist/configurators/qoder.js +30 -0
  263. package/dist/configurators/qoder.js.map +1 -0
  264. package/dist/configurators/reasonix.d.ts +17 -0
  265. package/dist/configurators/reasonix.d.ts.map +1 -0
  266. package/dist/configurators/reasonix.js +37 -0
  267. package/dist/configurators/reasonix.js.map +1 -0
  268. package/dist/configurators/shared.d.ts +187 -0
  269. package/dist/configurators/shared.d.ts.map +1 -0
  270. package/dist/configurators/shared.js +589 -0
  271. package/dist/configurators/shared.js.map +1 -0
  272. package/dist/configurators/snow.d.ts +25 -0
  273. package/dist/configurators/snow.d.ts.map +1 -0
  274. package/dist/configurators/snow.js +69 -0
  275. package/dist/configurators/snow.js.map +1 -0
  276. package/dist/configurators/trae.d.ts +16 -0
  277. package/dist/configurators/trae.d.ts.map +1 -0
  278. package/dist/configurators/trae.js +34 -0
  279. package/dist/configurators/trae.js.map +1 -0
  280. package/dist/configurators/workflow.d.ts +51 -0
  281. package/dist/configurators/workflow.d.ts.map +1 -0
  282. package/dist/configurators/workflow.js +210 -0
  283. package/dist/configurators/workflow.js.map +1 -0
  284. package/dist/configurators/zcode.d.ts +25 -0
  285. package/dist/configurators/zcode.d.ts.map +1 -0
  286. package/dist/configurators/zcode.js +67 -0
  287. package/dist/configurators/zcode.js.map +1 -0
  288. package/dist/constants/paths.d.ts +74 -0
  289. package/dist/constants/paths.d.ts.map +1 -0
  290. package/dist/constants/paths.js +83 -0
  291. package/dist/constants/paths.js.map +1 -0
  292. package/dist/constants/version.d.ts +9 -0
  293. package/dist/constants/version.d.ts.map +1 -0
  294. package/dist/constants/version.js +15 -0
  295. package/dist/constants/version.js.map +1 -0
  296. package/dist/index.d.ts +9 -0
  297. package/dist/index.d.ts.map +1 -0
  298. package/dist/index.js +9 -0
  299. package/dist/index.js.map +1 -0
  300. package/dist/migrations/index.d.ts +62 -0
  301. package/dist/migrations/index.d.ts.map +1 -0
  302. package/dist/migrations/index.js +187 -0
  303. package/dist/migrations/index.js.map +1 -0
  304. package/dist/migrations/manifests/0.1.9.json +30 -0
  305. package/dist/migrations/manifests/0.2.0.json +49 -0
  306. package/dist/migrations/manifests/0.2.12.json +9 -0
  307. package/dist/migrations/manifests/0.2.13.json +9 -0
  308. package/dist/migrations/manifests/0.2.14.json +175 -0
  309. package/dist/migrations/manifests/0.2.15.json +33 -0
  310. package/dist/migrations/manifests/0.3.0-beta.0.json +297 -0
  311. package/dist/migrations/manifests/0.3.0-beta.1.json +9 -0
  312. package/dist/migrations/manifests/0.3.0-beta.10.json +9 -0
  313. package/dist/migrations/manifests/0.3.0-beta.11.json +9 -0
  314. package/dist/migrations/manifests/0.3.0-beta.12.json +9 -0
  315. package/dist/migrations/manifests/0.3.0-beta.13.json +9 -0
  316. package/dist/migrations/manifests/0.3.0-beta.14.json +9 -0
  317. package/dist/migrations/manifests/0.3.0-beta.15.json +9 -0
  318. package/dist/migrations/manifests/0.3.0-beta.16.json +9 -0
  319. package/dist/migrations/manifests/0.3.0-beta.2.json +9 -0
  320. package/dist/migrations/manifests/0.3.0-beta.3.json +9 -0
  321. package/dist/migrations/manifests/0.3.0-beta.4.json +9 -0
  322. package/dist/migrations/manifests/0.3.0-beta.5.json +9 -0
  323. package/dist/migrations/manifests/0.3.0-beta.6.json +9 -0
  324. package/dist/migrations/manifests/0.3.0-beta.7.json +11 -0
  325. package/dist/migrations/manifests/0.3.0-beta.8.json +9 -0
  326. package/dist/migrations/manifests/0.3.0-beta.9.json +9 -0
  327. package/dist/migrations/manifests/0.3.0-rc.0.json +9 -0
  328. package/dist/migrations/manifests/0.3.0-rc.1.json +9 -0
  329. package/dist/migrations/manifests/0.3.0-rc.2.json +9 -0
  330. package/dist/migrations/manifests/0.3.0-rc.3.json +9 -0
  331. package/dist/migrations/manifests/0.3.0-rc.4.json +9 -0
  332. package/dist/migrations/manifests/0.3.0-rc.5.json +9 -0
  333. package/dist/migrations/manifests/0.3.0-rc.6.json +9 -0
  334. package/dist/migrations/manifests/0.3.0.json +11 -0
  335. package/dist/migrations/manifests/0.3.1.json +9 -0
  336. package/dist/migrations/manifests/0.3.10.json +9 -0
  337. package/dist/migrations/manifests/0.3.2.json +9 -0
  338. package/dist/migrations/manifests/0.3.3.json +9 -0
  339. package/dist/migrations/manifests/0.3.4.json +21 -0
  340. package/dist/migrations/manifests/0.3.5.json +9 -0
  341. package/dist/migrations/manifests/0.3.6.json +9 -0
  342. package/dist/migrations/manifests/0.3.7.json +9 -0
  343. package/dist/migrations/manifests/0.3.8.json +9 -0
  344. package/dist/migrations/manifests/0.3.9.json +9 -0
  345. package/dist/migrations/manifests/0.4.0-beta.1.json +228 -0
  346. package/dist/migrations/manifests/0.4.0-beta.10.json +9 -0
  347. package/dist/migrations/manifests/0.4.0-beta.2.json +9 -0
  348. package/dist/migrations/manifests/0.4.0-beta.3.json +9 -0
  349. package/dist/migrations/manifests/0.4.0-beta.4.json +9 -0
  350. package/dist/migrations/manifests/0.4.0-beta.5.json +9 -0
  351. package/dist/migrations/manifests/0.4.0-beta.6.json +9 -0
  352. package/dist/migrations/manifests/0.4.0-beta.7.json +9 -0
  353. package/dist/migrations/manifests/0.4.0-beta.8.json +34 -0
  354. package/dist/migrations/manifests/0.4.0-beta.9.json +9 -0
  355. package/dist/migrations/manifests/0.4.0-rc.0.json +9 -0
  356. package/dist/migrations/manifests/0.4.0-rc.1.json +9 -0
  357. package/dist/migrations/manifests/0.4.0.json +9 -0
  358. package/dist/migrations/manifests/0.5.0-beta.0.json +1646 -0
  359. package/dist/migrations/manifests/0.5.0-beta.1.json +9 -0
  360. package/dist/migrations/manifests/0.5.0-beta.10.json +9 -0
  361. package/dist/migrations/manifests/0.5.0-beta.11.json +9 -0
  362. package/dist/migrations/manifests/0.5.0-beta.12.json +9 -0
  363. package/dist/migrations/manifests/0.5.0-beta.13.json +9 -0
  364. package/dist/migrations/manifests/0.5.0-beta.14.json +9 -0
  365. package/dist/migrations/manifests/0.5.0-beta.15.json +116 -0
  366. package/dist/migrations/manifests/0.5.0-beta.16.json +9 -0
  367. package/dist/migrations/manifests/0.5.0-beta.17.json +9 -0
  368. package/dist/migrations/manifests/0.5.0-beta.18.json +9 -0
  369. package/dist/migrations/manifests/0.5.0-beta.19.json +9 -0
  370. package/dist/migrations/manifests/0.5.0-beta.2.json +9 -0
  371. package/dist/migrations/manifests/0.5.0-beta.3.json +9 -0
  372. package/dist/migrations/manifests/0.5.0-beta.4.json +9 -0
  373. package/dist/migrations/manifests/0.5.0-beta.5.json +222 -0
  374. package/dist/migrations/manifests/0.5.0-beta.6.json +9 -0
  375. package/dist/migrations/manifests/0.5.0-beta.7.json +9 -0
  376. package/dist/migrations/manifests/0.5.0-beta.8.json +9 -0
  377. package/dist/migrations/manifests/0.5.0-beta.9.json +48 -0
  378. package/dist/migrations/manifests/0.5.0-rc.0.json +9 -0
  379. package/dist/migrations/manifests/0.5.0-rc.1.json +9 -0
  380. package/dist/migrations/manifests/0.5.0-rc.2.json +9 -0
  381. package/dist/migrations/manifests/0.5.0-rc.3.json +9 -0
  382. package/dist/migrations/manifests/0.5.0-rc.4.json +9 -0
  383. package/dist/migrations/manifests/0.5.0-rc.5.json +9 -0
  384. package/dist/migrations/manifests/0.5.0-rc.6.json +9 -0
  385. package/dist/migrations/manifests/0.5.0-rc.7.json +9 -0
  386. package/dist/migrations/manifests/0.5.0.json +9 -0
  387. package/dist/migrations/manifests/0.5.1.json +9 -0
  388. package/dist/migrations/manifests/0.5.10.json +9 -0
  389. package/dist/migrations/manifests/0.5.11.json +16 -0
  390. package/dist/migrations/manifests/0.5.12.json +9 -0
  391. package/dist/migrations/manifests/0.5.13.json +9 -0
  392. package/dist/migrations/manifests/0.5.14.json +9 -0
  393. package/dist/migrations/manifests/0.5.15.json +9 -0
  394. package/dist/migrations/manifests/0.5.16.json +9 -0
  395. package/dist/migrations/manifests/0.5.17.json +9 -0
  396. package/dist/migrations/manifests/0.5.18.json +9 -0
  397. package/dist/migrations/manifests/0.5.19.json +9 -0
  398. package/dist/migrations/manifests/0.5.2.json +9 -0
  399. package/dist/migrations/manifests/0.5.3.json +9 -0
  400. package/dist/migrations/manifests/0.5.4.json +9 -0
  401. package/dist/migrations/manifests/0.5.5.json +9 -0
  402. package/dist/migrations/manifests/0.5.6.json +9 -0
  403. package/dist/migrations/manifests/0.5.7.json +16 -0
  404. package/dist/migrations/manifests/0.5.8.json +9 -0
  405. package/dist/migrations/manifests/0.5.9.json +9 -0
  406. package/dist/migrations/manifests/0.6.0-beta.0.json +16 -0
  407. package/dist/migrations/manifests/0.6.0-beta.1.json +9 -0
  408. package/dist/migrations/manifests/0.6.0-beta.10.json +9 -0
  409. package/dist/migrations/manifests/0.6.0-beta.11.json +9 -0
  410. package/dist/migrations/manifests/0.6.0-beta.12.json +9 -0
  411. package/dist/migrations/manifests/0.6.0-beta.13.json +9 -0
  412. package/dist/migrations/manifests/0.6.0-beta.14.json +9 -0
  413. package/dist/migrations/manifests/0.6.0-beta.15.json +9 -0
  414. package/dist/migrations/manifests/0.6.0-beta.16.json +9 -0
  415. package/dist/migrations/manifests/0.6.0-beta.17.json +9 -0
  416. package/dist/migrations/manifests/0.6.0-beta.18.json +16 -0
  417. package/dist/migrations/manifests/0.6.0-beta.19.json +9 -0
  418. package/dist/migrations/manifests/0.6.0-beta.2.json +9 -0
  419. package/dist/migrations/manifests/0.6.0-beta.20.json +9 -0
  420. package/dist/migrations/manifests/0.6.0-beta.21.json +9 -0
  421. package/dist/migrations/manifests/0.6.0-beta.22.json +9 -0
  422. package/dist/migrations/manifests/0.6.0-beta.23.json +88 -0
  423. package/dist/migrations/manifests/0.6.0-beta.3.json +9 -0
  424. package/dist/migrations/manifests/0.6.0-beta.4.json +9 -0
  425. package/dist/migrations/manifests/0.6.0-beta.5.json +9 -0
  426. package/dist/migrations/manifests/0.6.0-beta.6.json +16 -0
  427. package/dist/migrations/manifests/0.6.0-beta.7.json +9 -0
  428. package/dist/migrations/manifests/0.6.0-beta.8.json +9 -0
  429. package/dist/migrations/manifests/0.6.0-beta.9.json +9 -0
  430. package/dist/migrations/manifests/0.6.0-rc.0.json +9 -0
  431. package/dist/migrations/manifests/0.6.0.json +1 -0
  432. package/dist/migrations/manifests/0.6.1.json +9 -0
  433. package/dist/migrations/manifests/0.6.10.json +9 -0
  434. package/dist/migrations/manifests/0.6.11.json +9 -0
  435. package/dist/migrations/manifests/0.6.12.json +9 -0
  436. package/dist/migrations/manifests/0.6.13.json +9 -0
  437. package/dist/migrations/manifests/0.6.14.json +9 -0
  438. package/dist/migrations/manifests/0.6.15.json +9 -0
  439. package/dist/migrations/manifests/0.6.16.json +9 -0
  440. package/dist/migrations/manifests/0.6.17.json +9 -0
  441. package/dist/migrations/manifests/0.6.2.json +9 -0
  442. package/dist/migrations/manifests/0.6.3.json +24 -0
  443. package/dist/migrations/manifests/0.6.4.json +9 -0
  444. package/dist/migrations/manifests/0.6.5.json +9 -0
  445. package/dist/migrations/manifests/0.6.6.json +17 -0
  446. package/dist/migrations/manifests/0.6.7.json +9 -0
  447. package/dist/migrations/manifests/0.6.8.json +17 -0
  448. package/dist/migrations/manifests/0.6.9.json +9 -0
  449. package/dist/migrations/manifests/0.7.0-beta.0.json +9 -0
  450. package/dist/migrations/manifests/0.7.0-beta.1.json +9 -0
  451. package/dist/migrations/manifests/0.7.0-beta.2.json +9 -0
  452. package/dist/migrations/manifests/0.7.0-beta.3.json +9 -0
  453. package/dist/templates/claude/agents/trellis-check.md +115 -0
  454. package/dist/templates/claude/agents/trellis-implement.md +110 -0
  455. package/dist/templates/claude/agents/trellis-research.md +137 -0
  456. package/dist/templates/claude/hooks/statusline.py +332 -0
  457. package/dist/templates/claude/index.d.ts +31 -0
  458. package/dist/templates/claude/index.d.ts.map +1 -0
  459. package/dist/templates/claude/index.js +57 -0
  460. package/dist/templates/claude/index.js.map +1 -0
  461. package/dist/templates/claude/settings.json +73 -0
  462. package/dist/templates/codebuddy/agents/trellis-check.md +115 -0
  463. package/dist/templates/codebuddy/agents/trellis-implement.md +110 -0
  464. package/dist/templates/codebuddy/agents/trellis-research.md +137 -0
  465. package/dist/templates/codebuddy/index.d.ts +15 -0
  466. package/dist/templates/codebuddy/index.d.ts.map +1 -0
  467. package/dist/templates/codebuddy/index.js +15 -0
  468. package/dist/templates/codebuddy/index.js.map +1 -0
  469. package/dist/templates/codebuddy/settings.json +69 -0
  470. package/dist/templates/codex/agents/trellis-check.toml +56 -0
  471. package/dist/templates/codex/agents/trellis-implement.toml +37 -0
  472. package/dist/templates/codex/agents/trellis-research.toml +78 -0
  473. package/dist/templates/codex/config.toml +39 -0
  474. package/dist/templates/codex/hooks/session-start.py +552 -0
  475. package/dist/templates/codex/hooks.json +27 -0
  476. package/dist/templates/codex/index.d.ts +35 -0
  477. package/dist/templates/codex/index.d.ts.map +1 -0
  478. package/dist/templates/codex/index.js +67 -0
  479. package/dist/templates/codex/index.js.map +1 -0
  480. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +67 -0
  481. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +480 -0
  482. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +233 -0
  483. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +226 -0
  484. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +276 -0
  485. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +128 -0
  486. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +85 -0
  487. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/add-project-local-conventions.md +83 -0
  488. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +56 -0
  489. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-context-loading.md +84 -0
  490. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-hooks.md +57 -0
  491. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +123 -0
  492. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +83 -0
  493. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +90 -0
  494. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +65 -0
  495. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +55 -0
  496. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +159 -0
  497. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/context-injection.md +68 -0
  498. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +80 -0
  499. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +69 -0
  500. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/overview.md +51 -0
  501. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +102 -0
  502. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +130 -0
  503. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +75 -0
  504. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workspace-memory.md +71 -0
  505. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/agents.md +83 -0
  506. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/hooks-and-settings.md +72 -0
  507. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/overview.md +59 -0
  508. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/platform-map.md +112 -0
  509. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +86 -0
  510. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +81 -0
  511. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +65 -0
  512. package/dist/templates/common/bundled-skills/trellis-session-insight/references/triggering-patterns.md +93 -0
  513. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/SKILL.md +41 -0
  514. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/mcp-setup.md +90 -0
  515. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/repository-analysis.md +59 -0
  516. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-task-planning.md +61 -0
  517. package/dist/templates/common/bundled-skills/trellis-spec-bootstrap/references/spec-writing.md +70 -0
  518. package/dist/templates/common/commands/continue.md +56 -0
  519. package/dist/templates/common/commands/finish-work.md +66 -0
  520. package/dist/templates/common/commands/start.md +59 -0
  521. package/dist/templates/common/index.d.ts +48 -0
  522. package/dist/templates/common/index.d.ts.map +1 -0
  523. package/dist/templates/common/index.js +104 -0
  524. package/dist/templates/common/index.js.map +1 -0
  525. package/dist/templates/common/skills/before-dev.md +46 -0
  526. package/dist/templates/common/skills/brainstorm.md +195 -0
  527. package/dist/templates/common/skills/break-loop.md +184 -0
  528. package/dist/templates/common/skills/check.md +106 -0
  529. package/dist/templates/common/skills/update-spec.md +351 -0
  530. package/dist/templates/copilot/copilot-instructions.md +11 -0
  531. package/dist/templates/copilot/hooks/session-start.py +548 -0
  532. package/dist/templates/copilot/hooks.json +19 -0
  533. package/dist/templates/copilot/index.d.ts +28 -0
  534. package/dist/templates/copilot/index.d.ts.map +1 -0
  535. package/dist/templates/copilot/index.js +61 -0
  536. package/dist/templates/copilot/index.js.map +1 -0
  537. package/dist/templates/copilot/prompts/before-dev.prompt.md +39 -0
  538. package/dist/templates/copilot/prompts/brainstorm.prompt.md +156 -0
  539. package/dist/templates/copilot/prompts/break-loop.prompt.md +129 -0
  540. package/dist/templates/copilot/prompts/check-cross-layer.prompt.md +170 -0
  541. package/dist/templates/copilot/prompts/check.prompt.md +110 -0
  542. package/dist/templates/copilot/prompts/create-command.prompt.md +116 -0
  543. package/dist/templates/copilot/prompts/finish-work.prompt.md +99 -0
  544. package/dist/templates/copilot/prompts/integrate-skill.prompt.md +223 -0
  545. package/dist/templates/copilot/prompts/onboard.prompt.md +362 -0
  546. package/dist/templates/copilot/prompts/parallel.prompt.md +204 -0
  547. package/dist/templates/copilot/prompts/record-session.prompt.md +66 -0
  548. package/dist/templates/copilot/prompts/start.prompt.md +63 -0
  549. package/dist/templates/copilot/prompts/update-spec.prompt.md +358 -0
  550. package/dist/templates/cursor/agents/trellis-check.md +114 -0
  551. package/dist/templates/cursor/agents/trellis-implement.md +109 -0
  552. package/dist/templates/cursor/agents/trellis-research.md +136 -0
  553. package/dist/templates/cursor/hooks.json +24 -0
  554. package/dist/templates/cursor/index.d.ts +13 -0
  555. package/dist/templates/cursor/index.d.ts.map +1 -0
  556. package/dist/templates/cursor/index.js +13 -0
  557. package/dist/templates/cursor/index.js.map +1 -0
  558. package/dist/templates/droid/droids/trellis-check.md +107 -0
  559. package/dist/templates/droid/droids/trellis-implement.md +102 -0
  560. package/dist/templates/droid/droids/trellis-research.md +137 -0
  561. package/dist/templates/droid/index.d.ts +15 -0
  562. package/dist/templates/droid/index.d.ts.map +1 -0
  563. package/dist/templates/droid/index.js +15 -0
  564. package/dist/templates/droid/index.js.map +1 -0
  565. package/dist/templates/droid/settings.json +69 -0
  566. package/dist/templates/dsh/DSH.md +56 -0
  567. package/dist/templates/dsh/index.d.ts +21 -0
  568. package/dist/templates/dsh/index.d.ts.map +1 -0
  569. package/dist/templates/dsh/index.js +25 -0
  570. package/dist/templates/dsh/index.js.map +1 -0
  571. package/dist/templates/extract.d.ts +40 -0
  572. package/dist/templates/extract.d.ts.map +1 -0
  573. package/dist/templates/extract.js +106 -0
  574. package/dist/templates/extract.js.map +1 -0
  575. package/dist/templates/gemini/agents/trellis-check.md +107 -0
  576. package/dist/templates/gemini/agents/trellis-implement.md +102 -0
  577. package/dist/templates/gemini/agents/trellis-research.md +136 -0
  578. package/dist/templates/gemini/index.d.ts +13 -0
  579. package/dist/templates/gemini/index.d.ts.map +1 -0
  580. package/dist/templates/gemini/index.js +13 -0
  581. package/dist/templates/gemini/index.js.map +1 -0
  582. package/dist/templates/gemini/settings.json +40 -0
  583. package/dist/templates/grok/agents/trellis-check.md +105 -0
  584. package/dist/templates/grok/agents/trellis-implement.md +116 -0
  585. package/dist/templates/grok/agents/trellis-research.md +146 -0
  586. package/dist/templates/grok/index.d.ts +16 -0
  587. package/dist/templates/grok/index.d.ts.map +1 -0
  588. package/dist/templates/grok/index.js +19 -0
  589. package/dist/templates/grok/index.js.map +1 -0
  590. package/dist/templates/kimi/agents/trellis-check.md +103 -0
  591. package/dist/templates/kimi/agents/trellis-implement.md +115 -0
  592. package/dist/templates/kimi/agents/trellis-research.md +144 -0
  593. package/dist/templates/kimi/index.d.ts +20 -0
  594. package/dist/templates/kimi/index.d.ts.map +1 -0
  595. package/dist/templates/kimi/index.js +23 -0
  596. package/dist/templates/kimi/index.js.map +1 -0
  597. package/dist/templates/kiro/agents/trellis-check.json +26 -0
  598. package/dist/templates/kiro/agents/trellis-implement.json +26 -0
  599. package/dist/templates/kiro/agents/trellis-research.json +30 -0
  600. package/dist/templates/kiro/agents/trellis.json +34 -0
  601. package/dist/templates/kiro/hooks/trellis-workflow-state.kiro.hook +14 -0
  602. package/dist/templates/kiro/index.d.ts +32 -0
  603. package/dist/templates/kiro/index.d.ts.map +1 -0
  604. package/dist/templates/kiro/index.js +28 -0
  605. package/dist/templates/kiro/index.js.map +1 -0
  606. package/dist/templates/markdown/agents.md +21 -0
  607. package/dist/templates/markdown/gitignore.txt +15 -0
  608. package/dist/templates/markdown/index.d.ts +27 -0
  609. package/dist/templates/markdown/index.d.ts.map +1 -0
  610. package/dist/templates/markdown/index.js +52 -0
  611. package/dist/templates/markdown/index.js.map +1 -0
  612. package/dist/templates/markdown/spec/backend/database-guidelines.md.txt +51 -0
  613. package/dist/templates/markdown/spec/backend/directory-structure.md.txt +54 -0
  614. package/dist/templates/markdown/spec/backend/error-handling.md.txt +51 -0
  615. package/dist/templates/markdown/spec/backend/index.md.txt +38 -0
  616. package/dist/templates/markdown/spec/backend/logging-guidelines.md.txt +51 -0
  617. package/dist/templates/markdown/spec/backend/quality-guidelines.md.txt +51 -0
  618. package/dist/templates/markdown/spec/frontend/component-guidelines.md.txt +59 -0
  619. package/dist/templates/markdown/spec/frontend/directory-structure.md.txt +54 -0
  620. package/dist/templates/markdown/spec/frontend/hook-guidelines.md.txt +51 -0
  621. package/dist/templates/markdown/spec/frontend/index.md.txt +39 -0
  622. package/dist/templates/markdown/spec/frontend/quality-guidelines.md.txt +51 -0
  623. package/dist/templates/markdown/spec/frontend/state-management.md.txt +51 -0
  624. package/dist/templates/markdown/spec/frontend/type-safety.md.txt +51 -0
  625. package/dist/templates/markdown/spec/guides/code-reuse-thinking-guide.md.txt +223 -0
  626. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +327 -0
  627. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +633 -0
  628. package/dist/templates/markdown/spec/guides/index.md.txt +97 -0
  629. package/dist/templates/markdown/workspace-index.md +125 -0
  630. package/dist/templates/markdown/worktree.yaml.txt +58 -0
  631. package/dist/templates/omp/agents/trellis-check.md +41 -0
  632. package/dist/templates/omp/agents/trellis-implement.md +45 -0
  633. package/dist/templates/omp/agents/trellis-research.md +30 -0
  634. package/dist/templates/omp/extensions/trellis/index.ts.txt +1105 -0
  635. package/dist/templates/omp/index.d.ts +4 -0
  636. package/dist/templates/omp/index.d.ts.map +1 -0
  637. package/dist/templates/omp/index.js +9 -0
  638. package/dist/templates/omp/index.js.map +1 -0
  639. package/dist/templates/opencode/agents/trellis-check.md +122 -0
  640. package/dist/templates/opencode/agents/trellis-implement.md +118 -0
  641. package/dist/templates/opencode/agents/trellis-research.md +145 -0
  642. package/dist/templates/opencode/lib/context-visibility.js +148 -0
  643. package/dist/templates/opencode/lib/session-utils.js +527 -0
  644. package/dist/templates/opencode/lib/trellis-context.js +627 -0
  645. package/dist/templates/opencode/package.json +5 -0
  646. package/dist/templates/opencode/plugins/inject-subagent-context.js +569 -0
  647. package/dist/templates/opencode/plugins/inject-workflow-state.js +240 -0
  648. package/dist/templates/opencode/plugins/session-start.js +65 -0
  649. package/dist/templates/pi/agents/trellis-check.md +37 -0
  650. package/dist/templates/pi/agents/trellis-implement.md +42 -0
  651. package/dist/templates/pi/agents/trellis-research.md +25 -0
  652. package/dist/templates/pi/extensions/trellis/index.ts.txt +2027 -0
  653. package/dist/templates/pi/index.d.ts +5 -0
  654. package/dist/templates/pi/index.d.ts.map +1 -0
  655. package/dist/templates/pi/index.js +12 -0
  656. package/dist/templates/pi/index.js.map +1 -0
  657. package/dist/templates/pi/settings.json +9 -0
  658. package/dist/templates/qoder/agents/trellis-check.md +108 -0
  659. package/dist/templates/qoder/agents/trellis-implement.md +103 -0
  660. package/dist/templates/qoder/agents/trellis-research.md +137 -0
  661. package/dist/templates/qoder/index.d.ts +15 -0
  662. package/dist/templates/qoder/index.d.ts.map +1 -0
  663. package/dist/templates/qoder/index.js +15 -0
  664. package/dist/templates/qoder/index.js.map +1 -0
  665. package/dist/templates/qoder/settings.json +59 -0
  666. package/dist/templates/reasonix/agents/trellis-check.md +36 -0
  667. package/dist/templates/reasonix/agents/trellis-implement.md +41 -0
  668. package/dist/templates/reasonix/index.d.ts +13 -0
  669. package/dist/templates/reasonix/index.d.ts.map +1 -0
  670. package/dist/templates/reasonix/index.js +16 -0
  671. package/dist/templates/reasonix/index.js.map +1 -0
  672. package/dist/templates/shared-hooks/index.d.ts +77 -0
  673. package/dist/templates/shared-hooks/index.d.ts.map +1 -0
  674. package/dist/templates/shared-hooks/index.js +140 -0
  675. package/dist/templates/shared-hooks/index.js.map +1 -0
  676. package/dist/templates/shared-hooks/inject-shell-session-context.py +292 -0
  677. package/dist/templates/shared-hooks/inject-subagent-context.py +1212 -0
  678. package/dist/templates/shared-hooks/inject-workflow-state.py +488 -0
  679. package/dist/templates/shared-hooks/session-start.py +949 -0
  680. package/dist/templates/snow/SNOW.md +86 -0
  681. package/dist/templates/snow/agents/trellis-check.md +135 -0
  682. package/dist/templates/snow/agents/trellis-implement.md +153 -0
  683. package/dist/templates/snow/agents/trellis-research.md +114 -0
  684. package/dist/templates/snow/hooks/beforeSubAgentStart.json +15 -0
  685. package/dist/templates/snow/hooks/onSessionStart.json +15 -0
  686. package/dist/templates/snow/hooks/onUserMessage.json +15 -0
  687. package/dist/templates/snow/hooks/write-trellis-context.py +644 -0
  688. package/dist/templates/snow/index.d.ts +26 -0
  689. package/dist/templates/snow/index.d.ts.map +1 -0
  690. package/dist/templates/snow/index.js +36 -0
  691. package/dist/templates/snow/index.js.map +1 -0
  692. package/dist/templates/template-utils.d.ts +26 -0
  693. package/dist/templates/template-utils.d.ts.map +1 -0
  694. package/dist/templates/template-utils.js +64 -0
  695. package/dist/templates/template-utils.js.map +1 -0
  696. package/dist/templates/trae/agents/trellis-check.md +108 -0
  697. package/dist/templates/trae/agents/trellis-implement.md +103 -0
  698. package/dist/templates/trae/agents/trellis-research.md +137 -0
  699. package/dist/templates/trae/hooks.json +39 -0
  700. package/dist/templates/trae/index.d.ts +15 -0
  701. package/dist/templates/trae/index.d.ts.map +1 -0
  702. package/dist/templates/trae/index.js +15 -0
  703. package/dist/templates/trae/index.js.map +1 -0
  704. package/dist/templates/trellis/agents/check.md +70 -0
  705. package/dist/templates/trellis/agents/implement.md +71 -0
  706. package/dist/templates/trellis/config.yaml +158 -0
  707. package/dist/templates/trellis/gitattributes.txt +9 -0
  708. package/dist/templates/trellis/gitignore.txt +32 -0
  709. package/dist/templates/trellis/index.d.ts +67 -0
  710. package/dist/templates/trellis/index.d.ts.map +1 -0
  711. package/dist/templates/trellis/index.js +121 -0
  712. package/dist/templates/trellis/index.js.map +1 -0
  713. package/dist/templates/trellis/scripts/__init__.py +5 -0
  714. package/dist/templates/trellis/scripts/add_session.py +1545 -0
  715. package/dist/templates/trellis/scripts/common/__init__.py +94 -0
  716. package/dist/templates/trellis/scripts/common/active_task.py +857 -0
  717. package/dist/templates/trellis/scripts/common/cli_adapter.py +950 -0
  718. package/dist/templates/trellis/scripts/common/config.py +482 -0
  719. package/dist/templates/trellis/scripts/common/developer.py +192 -0
  720. package/dist/templates/trellis/scripts/common/git.py +203 -0
  721. package/dist/templates/trellis/scripts/common/git_context.py +106 -0
  722. package/dist/templates/trellis/scripts/common/io.py +160 -0
  723. package/dist/templates/trellis/scripts/common/log.py +45 -0
  724. package/dist/templates/trellis/scripts/common/packages_context.py +238 -0
  725. package/dist/templates/trellis/scripts/common/paths.py +543 -0
  726. package/dist/templates/trellis/scripts/common/safe_commit.py +322 -0
  727. package/dist/templates/trellis/scripts/common/session_context.py +892 -0
  728. package/dist/templates/trellis/scripts/common/task_context.py +470 -0
  729. package/dist/templates/trellis/scripts/common/task_queue.py +188 -0
  730. package/dist/templates/trellis/scripts/common/task_store.py +1874 -0
  731. package/dist/templates/trellis/scripts/common/task_utils.py +498 -0
  732. package/dist/templates/trellis/scripts/common/tasks.py +122 -0
  733. package/dist/templates/trellis/scripts/common/trellis_config.py +239 -0
  734. package/dist/templates/trellis/scripts/common/types.py +110 -0
  735. package/dist/templates/trellis/scripts/common/workflow_phase.py +242 -0
  736. package/dist/templates/trellis/scripts/get_context.py +16 -0
  737. package/dist/templates/trellis/scripts/get_developer.py +26 -0
  738. package/dist/templates/trellis/scripts/hooks/linear_sync.py +243 -0
  739. package/dist/templates/trellis/scripts/init_developer.py +51 -0
  740. package/dist/templates/trellis/scripts/task.py +801 -0
  741. package/dist/templates/trellis/tasks/.gitkeep +0 -0
  742. package/dist/templates/trellis/workflow.md +721 -0
  743. package/dist/templates/zcode/agents/trellis-check.md +109 -0
  744. package/dist/templates/zcode/agents/trellis-implement.md +111 -0
  745. package/dist/templates/zcode/agents/trellis-research.md +133 -0
  746. package/dist/templates/zcode/config.json +52 -0
  747. package/dist/templates/zcode/index.d.ts +25 -0
  748. package/dist/templates/zcode/index.d.ts.map +1 -0
  749. package/dist/templates/zcode/index.js +29 -0
  750. package/dist/templates/zcode/index.js.map +1 -0
  751. package/dist/types/ai-tools.d.ts +105 -0
  752. package/dist/types/ai-tools.d.ts.map +1 -0
  753. package/dist/types/ai-tools.js +502 -0
  754. package/dist/types/ai-tools.js.map +1 -0
  755. package/dist/types/migration.d.ts +125 -0
  756. package/dist/types/migration.d.ts.map +1 -0
  757. package/dist/types/migration.js +8 -0
  758. package/dist/types/migration.js.map +1 -0
  759. package/dist/utils/ablation-store.d.ts +123 -0
  760. package/dist/utils/ablation-store.d.ts.map +1 -0
  761. package/dist/utils/ablation-store.js +749 -0
  762. package/dist/utils/ablation-store.js.map +1 -0
  763. package/dist/utils/agent-refs.d.ts +31 -0
  764. package/dist/utils/agent-refs.d.ts.map +1 -0
  765. package/dist/utils/agent-refs.js +63 -0
  766. package/dist/utils/agent-refs.js.map +1 -0
  767. package/dist/utils/atomic-write.d.ts +12 -0
  768. package/dist/utils/atomic-write.d.ts.map +1 -0
  769. package/dist/utils/atomic-write.js +30 -0
  770. package/dist/utils/atomic-write.js.map +1 -0
  771. package/dist/utils/compare-versions.d.ts +12 -0
  772. package/dist/utils/compare-versions.d.ts.map +1 -0
  773. package/dist/utils/compare-versions.js +86 -0
  774. package/dist/utils/compare-versions.js.map +1 -0
  775. package/dist/utils/cwd-guard.d.ts +38 -0
  776. package/dist/utils/cwd-guard.d.ts.map +1 -0
  777. package/dist/utils/cwd-guard.js +62 -0
  778. package/dist/utils/cwd-guard.js.map +1 -0
  779. package/dist/utils/file-writer.d.ts +36 -0
  780. package/dist/utils/file-writer.d.ts.map +1 -0
  781. package/dist/utils/file-writer.js +204 -0
  782. package/dist/utils/file-writer.js.map +1 -0
  783. package/dist/utils/managed-paths.d.ts +5 -0
  784. package/dist/utils/managed-paths.d.ts.map +1 -0
  785. package/dist/utils/managed-paths.js +51 -0
  786. package/dist/utils/managed-paths.js.map +1 -0
  787. package/dist/utils/managed-removal.d.ts +74 -0
  788. package/dist/utils/managed-removal.d.ts.map +1 -0
  789. package/dist/utils/managed-removal.js +270 -0
  790. package/dist/utils/managed-removal.js.map +1 -0
  791. package/dist/utils/manifest-prune.d.ts +61 -0
  792. package/dist/utils/manifest-prune.d.ts.map +1 -0
  793. package/dist/utils/manifest-prune.js +135 -0
  794. package/dist/utils/manifest-prune.js.map +1 -0
  795. package/dist/utils/posix.d.ts +13 -0
  796. package/dist/utils/posix.d.ts.map +1 -0
  797. package/dist/utils/posix.js +15 -0
  798. package/dist/utils/posix.js.map +1 -0
  799. package/dist/utils/project-detector.d.ts +46 -0
  800. package/dist/utils/project-detector.d.ts.map +1 -0
  801. package/dist/utils/project-detector.js +666 -0
  802. package/dist/utils/project-detector.js.map +1 -0
  803. package/dist/utils/proxy.d.ts +25 -0
  804. package/dist/utils/proxy.d.ts.map +1 -0
  805. package/dist/utils/proxy.js +60 -0
  806. package/dist/utils/proxy.js.map +1 -0
  807. package/dist/utils/registry-config.d.ts +7 -0
  808. package/dist/utils/registry-config.d.ts.map +1 -0
  809. package/dist/utils/registry-config.js +172 -0
  810. package/dist/utils/registry-config.js.map +1 -0
  811. package/dist/utils/task-json.d.ts +13 -0
  812. package/dist/utils/task-json.d.ts.map +1 -0
  813. package/dist/utils/task-json.js +12 -0
  814. package/dist/utils/task-json.js.map +1 -0
  815. package/dist/utils/template-fetcher.d.ts +161 -0
  816. package/dist/utils/template-fetcher.d.ts.map +1 -0
  817. package/dist/utils/template-fetcher.js +994 -0
  818. package/dist/utils/template-fetcher.js.map +1 -0
  819. package/dist/utils/template-hash.d.ts +127 -0
  820. package/dist/utils/template-hash.d.ts.map +1 -0
  821. package/dist/utils/template-hash.js +335 -0
  822. package/dist/utils/template-hash.js.map +1 -0
  823. package/dist/utils/uninstall-scrubbers.d.ts +66 -0
  824. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -0
  825. package/dist/utils/uninstall-scrubbers.js +417 -0
  826. package/dist/utils/uninstall-scrubbers.js.map +1 -0
  827. package/dist/utils/workflow-resolver.d.ts +86 -0
  828. package/dist/utils/workflow-resolver.d.ts.map +1 -0
  829. package/dist/utils/workflow-resolver.js +265 -0
  830. package/dist/utils/workflow-resolver.js.map +1 -0
  831. package/package.json +89 -0
@@ -0,0 +1,1545 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Add a new session to journal file and update index.md.
5
+
6
+ Usage:
7
+ python3 add_session.py --title "Title" --commit "hash" --summary "Summary" [--package cli]
8
+ python3 add_session.py --title "Title" --branch "feat/my-branch"
9
+
10
+ # Pipe detailed content via stdin (use --stdin to opt in):
11
+ cat << 'EOF' | python3 add_session.py --stdin --title "Title" --summary "Summary"
12
+ <session content here>
13
+ EOF
14
+
15
+ # Structured content (repeatable; a section with no bullets is omitted):
16
+ python3 add_session.py --title "Title" --change "Did X" --test "Ran Y" --next-step "Do Z"
17
+
18
+ # Commit evidence that cannot be resolved from the local object database
19
+ # (amended, not yet fetched) needs an explicit subject, one per OID:
20
+ python3 add_session.py --title "Title" --commit "abc1234" \
21
+ --commit-subject "abc1234=fix(cli): stop truncating the manifest"
22
+
23
+ Commit evidence:
24
+ Every --commit token is a bounded hex OID (7-40 chars). Each one is
25
+ resolved to its real subject from the local object database BEFORE
26
+ anything is written. An OID that cannot be resolved fails the command
27
+ before the journal or index is touched — no placeholder prose is ever
28
+ recorded. Pass "-" (the default) for a planning session with no commits.
29
+
30
+ Retry convergence:
31
+ The journal append, the index row and the optional auto-commit are one
32
+ resumable operation. Every entry carries a fingerprint marker derived from
33
+ its own inputs, so re-running an interrupted command repairs the pending
34
+ record instead of appending a second one, and a failed auto-commit exits
35
+ non-zero with the checkpoint to resume from. A record that is already
36
+ committed is never reused: an identical later request is a new session
37
+ unless an explicit --idempotency-key says otherwise.
38
+
39
+ Branch resolution order:
40
+ 1. --branch CLI arg (explicit)
41
+ 2. task.json branch field (from active task, if still exists)
42
+ 3. git branch --show-current (auto-detect)
43
+ 4. None (omitted gracefully)
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import argparse
49
+ import hashlib
50
+ import json
51
+ import re
52
+ import sys
53
+ from datetime import datetime
54
+ from pathlib import Path
55
+
56
+ from common.paths import (
57
+ DIR_TASKS,
58
+ DIR_WORKFLOW,
59
+ FILE_JOURNAL_PREFIX,
60
+ get_repo_root,
61
+ get_current_task,
62
+ get_developer,
63
+ get_workspace_dir,
64
+ )
65
+ from common.developer import ensure_developer
66
+ from common.git import branch_exists_locally, run_git
67
+ from common.io import write_text_atomic
68
+ from common.log import Colors, colored
69
+ from common.safe_commit import (
70
+ print_gitignore_warning,
71
+ safe_git_add,
72
+ safe_trellis_paths_to_add,
73
+ )
74
+ from common.tasks import load_task
75
+ from common.types import TaskInfo
76
+ from common.config import (
77
+ get_packages,
78
+ get_session_auto_commit,
79
+ get_session_commit_message,
80
+ get_max_journal_lines,
81
+ is_monorepo,
82
+ resolve_package,
83
+ validate_package,
84
+ )
85
+
86
+
87
+ # Bounded, argv-safe input shapes. Anything outside them is rejected during
88
+ # preflight, before a single byte is written.
89
+ COMMIT_TOKEN_RE = re.compile(r"^[0-9a-fA-F]{7,40}$")
90
+ IDEMPOTENCY_KEY_RE = re.compile(r"^[A-Za-z0-9._-]{1,64}$")
91
+ MAX_SUBJECT_LEN = 500
92
+
93
+ # Machine-readable record identity. An HTML comment renders as nothing, so the
94
+ # human evidence in the entry is unchanged by its presence.
95
+ MARKER_PREFIX = "<!-- trellis-session:"
96
+ # Bumped when the fingerprint inputs change. Untagged markers are v1, whose
97
+ # fingerprint mixed in the calendar date; see compute_record_fingerprint.
98
+ MARKER_VERSION = 2
99
+ LEGACY_MARKER_RE = re.compile(r"^<!-- trellis-session: fp=([0-9a-f]{16}) -->$")
100
+ ENTRY_DATE_RE = re.compile(r"^\*\*Date\*\*: (\d{4}-\d{2}-\d{2})\s*$")
101
+ SESSION_HEADING_RE = re.compile(r"^## Session (\d+):", re.MULTILINE)
102
+
103
+ # Recording states, in the order the operation walks them.
104
+ STATE_ABSENT = "absent"
105
+ STATE_JOURNAL_RECORDED = "journal-recorded"
106
+ STATE_INDEX_RECORDED = "index-recorded"
107
+ STATE_COMMITTED = "committed"
108
+
109
+ # Auto-commit outcomes.
110
+ COMMIT_DONE = "committed"
111
+ COMMIT_SKIPPED = "skipped"
112
+ COMMIT_BLOCKED = "blocked"
113
+ COMMIT_FAILED = "failed"
114
+
115
+ # Git probes against other refs are best-effort; never let one hang a session.
116
+ GIT_PROBE_TIMEOUT = 15.0
117
+ # Upper bound on the refs folded into session numbering. A repo with hundreds
118
+ # of stale branches must not turn session recording into a tree walk.
119
+ MAX_CONVERGENCE_REFS = 100
120
+
121
+
122
+ # =============================================================================
123
+ # Helper Functions
124
+ # =============================================================================
125
+
126
+ def get_latest_journal_info(dev_dir: Path) -> tuple[Path | None, int, int]:
127
+ """Get latest journal file info.
128
+
129
+ Returns:
130
+ Tuple of (file_path, file_number, line_count).
131
+ """
132
+ latest_file: Path | None = None
133
+ latest_num = -1
134
+
135
+ for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
136
+ if not f.is_file():
137
+ continue
138
+
139
+ match = re.search(r"(\d+)$", f.stem)
140
+ if match:
141
+ num = int(match.group(1))
142
+ if num > latest_num:
143
+ latest_num = num
144
+ latest_file = f
145
+
146
+ if latest_file:
147
+ lines = len(latest_file.read_text(encoding="utf-8").splitlines())
148
+ return latest_file, latest_num, lines
149
+
150
+ return None, 0, 0
151
+
152
+
153
+ def get_current_session(index_file: Path) -> int:
154
+ """Get current session number from index.md."""
155
+ if not index_file.is_file():
156
+ return 0
157
+
158
+ content = index_file.read_text(encoding="utf-8")
159
+ return _max_session_in_index(content)
160
+
161
+
162
+ def _max_session_in_index(content: str) -> int:
163
+ """Read the `Total Sessions` counter out of an index.md body."""
164
+ for line in content.splitlines():
165
+ if "Total Sessions" in line:
166
+ match = re.search(r":\s*(\d+)", line)
167
+ if match:
168
+ return int(match.group(1))
169
+ return 0
170
+
171
+
172
+ def _max_session_in_journal(content: str) -> int:
173
+ """Highest `## Session N:` heading in a journal body (0 when none)."""
174
+ numbers = [int(m.group(1)) for m in SESSION_HEADING_RE.finditer(content)]
175
+ return max(numbers) if numbers else 0
176
+
177
+
178
+ def _extract_journal_num(filename: str) -> int:
179
+ """Extract journal number from filename for sorting."""
180
+ match = re.search(r"(\d+)", filename)
181
+ return int(match.group(1)) if match else 0
182
+
183
+
184
+ def count_journal_files(dev_dir: Path, active_num: int) -> str:
185
+ """Count journal files and return table rows."""
186
+ active_file = f"{FILE_JOURNAL_PREFIX}{active_num}.md"
187
+ result_lines = []
188
+
189
+ files = sorted(
190
+ [f for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md") if f.is_file()],
191
+ key=lambda f: _extract_journal_num(f.stem),
192
+ reverse=True
193
+ )
194
+
195
+ for f in files:
196
+ filename = f.name
197
+ lines = len(f.read_text(encoding="utf-8").splitlines())
198
+ status = "Active" if filename == active_file else "Archived"
199
+ result_lines.append(f"| `{filename}` | ~{lines} | {status} |")
200
+
201
+ return "\n".join(result_lines)
202
+
203
+
204
+ def get_current_git_branch(repo_root: Path) -> str | None:
205
+ """Return the current checkout branch, or None for detached/non-git states."""
206
+ rc, branch_out, _ = run_git(["branch", "--show-current"], cwd=repo_root)
207
+ if rc != 0:
208
+ return None
209
+ detected = branch_out.strip()
210
+ return detected or None
211
+
212
+
213
+ def branch_ref_exists(repo_root: Path, branch: str) -> bool:
214
+ """Return True when branch exists locally or as the local origin ref."""
215
+ for ref in (f"refs/heads/{branch}", f"refs/remotes/origin/{branch}"):
216
+ rc, _, _ = run_git(["show-ref", "--verify", "--quiet", ref], cwd=repo_root)
217
+ if rc == 0:
218
+ return True
219
+ return False
220
+
221
+
222
+ def resolve_session_branch(
223
+ repo_root: Path,
224
+ cli_branch: str | None,
225
+ task_data: TaskInfo | None,
226
+ ) -> str | None:
227
+ """Resolve journal branch without trusting stale task.json branch fields."""
228
+ if cli_branch:
229
+ return cli_branch
230
+
231
+ current_branch = get_current_git_branch(repo_root)
232
+ raw_task_branch = task_data.raw.get("branch") if task_data else None
233
+ task_branch = raw_task_branch.strip() if isinstance(raw_task_branch, str) else ""
234
+ if not task_branch:
235
+ return current_branch
236
+
237
+ if branch_ref_exists(repo_root, task_branch):
238
+ return task_branch
239
+
240
+ if current_branch:
241
+ print(
242
+ f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; using current branch '{current_branch}'.",
243
+ file=sys.stderr,
244
+ )
245
+ return current_branch
246
+
247
+ print(
248
+ f"Warning: task.json branch '{task_branch}' no longer exists locally or as origin/{task_branch}; omitting branch.",
249
+ file=sys.stderr,
250
+ )
251
+ return None
252
+
253
+
254
+ def is_git_worktree(repo_root: Path) -> bool:
255
+ """Return True when repo_root is a linked worktree (not the main working tree).
256
+
257
+ Standard test: `git rev-parse --git-dir` (per-worktree) differs from
258
+ `git rev-parse --git-common-dir` (shared across all worktrees) once both
259
+ are resolved to absolute paths. In the main working tree these are the
260
+ same directory.
261
+ """
262
+ rc_dir, git_dir, _ = run_git(["rev-parse", "--git-dir"], cwd=repo_root)
263
+ rc_common, git_common_dir, _ = run_git(
264
+ ["rev-parse", "--git-common-dir"], cwd=repo_root
265
+ )
266
+ if rc_dir != 0 or rc_common != 0:
267
+ return False
268
+
269
+ git_dir_path = (repo_root / git_dir.strip()).resolve()
270
+ git_common_dir_path = (repo_root / git_common_dir.strip()).resolve()
271
+ return git_dir_path != git_common_dir_path
272
+
273
+
274
+ def warn_if_parallel_worktree(repo_root: Path) -> None:
275
+ """Non-blocking note: index.md conflicts across parallel worktrees/branches
276
+ are expected and safe. Only fires when running in a linked git worktree
277
+ (not the main tree) with `session_auto_commit` enabled (#415 quick-fix tier).
278
+ """
279
+ if not get_session_auto_commit(repo_root):
280
+ return
281
+ if not is_git_worktree(repo_root):
282
+ return
283
+ print(
284
+ colored(
285
+ "[NOTE] Running in a git worktree with session_auto_commit enabled: "
286
+ "journal-*.md files auto-merge via .gitattributes, but index.md "
287
+ "conflicts across parallel worktrees/branches are expected and safe "
288
+ "to resolve by picking either side (task state lives in task.json, "
289
+ "not index.md). See .trellis/spec/cli/backend/directory-structure.md "
290
+ '("Workspace Journal Merge Behavior").',
291
+ Colors.YELLOW,
292
+ ),
293
+ file=sys.stderr,
294
+ )
295
+
296
+
297
+ def create_new_journal_file(
298
+ dev_dir: Path, num: int, developer: str, today: str, max_lines: int = 2000,
299
+ ) -> Path | None:
300
+ """Create a new journal file. Returns None when the write fails."""
301
+ prev_num = num - 1
302
+ new_file = dev_dir / f"{FILE_JOURNAL_PREFIX}{num}.md"
303
+
304
+ content = f"""# Journal - {developer} (Part {num})
305
+
306
+ > Continuation from `{FILE_JOURNAL_PREFIX}{prev_num}.md` (archived at ~{max_lines} lines)
307
+ > Started: {today}
308
+
309
+ ---
310
+
311
+ """
312
+ if not write_text_atomic(new_file, content):
313
+ return None
314
+ return new_file
315
+
316
+
317
+ # =============================================================================
318
+ # Commit evidence (R1) — accurate subjects or nothing
319
+ # =============================================================================
320
+
321
+ def parse_commit_tokens(commit: str) -> tuple[list[str], str | None]:
322
+ """Split --commit into bounded hex OIDs. Returns (tokens, error)."""
323
+ raw = (commit or "").strip()
324
+ if not raw or raw == "-":
325
+ return [], None
326
+
327
+ tokens: list[str] = []
328
+ for part in raw.split(","):
329
+ token = part.strip()
330
+ if not token:
331
+ continue
332
+ if not COMMIT_TOKEN_RE.match(token):
333
+ return [], (
334
+ f"invalid --commit token '{token}': expected a 7-40 character hex "
335
+ "commit OID, or '-' for a planning session with no commits"
336
+ )
337
+ token = token.lower()
338
+ if token not in tokens:
339
+ tokens.append(token)
340
+ return tokens, None
341
+
342
+
343
+ def parse_subject_overrides(values: list[str] | None) -> tuple[dict[str, str], str | None]:
344
+ """Parse `--commit-subject <oid>=<subject>` into a one-to-one mapping."""
345
+ overrides: dict[str, str] = {}
346
+ for raw in values or []:
347
+ if "=" not in raw:
348
+ return {}, (
349
+ f"invalid --commit-subject '{raw}': expected <oid>=<subject>"
350
+ )
351
+ oid_part, subject = raw.split("=", 1)
352
+ oid = oid_part.strip().lower()
353
+ subject = subject.strip()
354
+ if not COMMIT_TOKEN_RE.match(oid):
355
+ return {}, (
356
+ f"invalid --commit-subject key '{oid_part.strip()}': expected a "
357
+ "7-40 character hex commit OID"
358
+ )
359
+ if not subject:
360
+ return {}, f"invalid --commit-subject for '{oid}': subject is empty"
361
+ if len(subject) > MAX_SUBJECT_LEN:
362
+ return {}, (
363
+ f"invalid --commit-subject for '{oid}': subject exceeds "
364
+ f"{MAX_SUBJECT_LEN} characters"
365
+ )
366
+ if oid in overrides:
367
+ return {}, f"duplicate --commit-subject mapping for '{oid}'"
368
+ overrides[oid] = subject
369
+ return overrides, None
370
+
371
+
372
+ def resolve_commit_subject(repo_root: Path, oid: str) -> str | None:
373
+ """Resolve one OID to its subject from the local object database.
374
+
375
+ Passed as argv (never interpolated into a shell) and peeled with
376
+ ``^{commit}`` so a hex-looking ref name cannot resolve to a tree or tag.
377
+ Returns None when the object is missing, ambiguous, or has no subject.
378
+ """
379
+ rc, out, _ = run_git(
380
+ ["show", "-s", "--format=%s", f"{oid}^{{commit}}", "--"], cwd=repo_root
381
+ )
382
+ if rc != 0:
383
+ return None
384
+ for line in out.splitlines():
385
+ subject = line.strip()
386
+ if subject:
387
+ return subject[:MAX_SUBJECT_LEN]
388
+ return None
389
+
390
+
391
+ def build_commit_evidence(
392
+ repo_root: Path,
393
+ tokens: list[str],
394
+ overrides: dict[str, str],
395
+ ) -> tuple[list[tuple[str, str]], str | None]:
396
+ """Pair every commit OID with an accurate subject, or fail.
397
+
398
+ There is no placeholder path: an OID that neither resolves locally nor
399
+ carries an explicit mapping stops the command before any mutation.
400
+ """
401
+ evidence: list[tuple[str, str]] = []
402
+ unresolved: list[str] = []
403
+
404
+ for oid in tokens:
405
+ if oid in overrides:
406
+ evidence.append((oid, overrides[oid]))
407
+ continue
408
+ subject = resolve_commit_subject(repo_root, oid)
409
+ if subject is None:
410
+ unresolved.append(oid)
411
+ continue
412
+ evidence.append((oid, subject))
413
+
414
+ if unresolved:
415
+ return [], (
416
+ "cannot resolve commit evidence for: "
417
+ + ", ".join(unresolved)
418
+ + ". Fetch the objects, correct the OID, or pass the exact subject "
419
+ "with --commit-subject <oid>=<subject>. Nothing was written."
420
+ )
421
+
422
+ unused = sorted(oid for oid in overrides if oid not in tokens)
423
+ if unused:
424
+ return [], (
425
+ "--commit-subject supplied for OIDs that are not in --commit: "
426
+ + ", ".join(unused)
427
+ + ". The mapping must be one-to-one. Nothing was written."
428
+ )
429
+
430
+ return evidence, None
431
+
432
+
433
+ def escape_markdown_cell(text: str) -> str:
434
+ """Make a commit subject safe inside a Markdown table cell."""
435
+ collapsed = " ".join(text.split())
436
+ return collapsed.replace("\\", "\\\\").replace("|", "\\|")
437
+
438
+
439
+ # =============================================================================
440
+ # Record identity (R2)
441
+ # =============================================================================
442
+
443
+ def _normalize_text(value: str | None) -> str:
444
+ return " ".join((value or "").split())
445
+
446
+
447
+ def _fingerprint_payload(
448
+ developer: str,
449
+ title: str,
450
+ summary: str,
451
+ package: str | None,
452
+ branch: str | None,
453
+ evidence: list[tuple[str, str]],
454
+ changes: list[str] | None,
455
+ extra_content: str | None,
456
+ tests: list[str] | None,
457
+ next_steps: list[str] | None,
458
+ idempotency_key: str | None,
459
+ ) -> dict:
460
+ """Normalized semantic inputs of one record, shared by both schemes."""
461
+ return {
462
+ "developer": developer,
463
+ "title": _normalize_text(title),
464
+ "summary": _normalize_text(summary),
465
+ "package": package or "",
466
+ "branch": branch or "",
467
+ "commits": [[oid, _normalize_text(subject)] for oid, subject in evidence],
468
+ "changes": [_normalize_text(c) for c in changes or []],
469
+ "extra": _normalize_text(extra_content),
470
+ "tests": [_normalize_text(t) for t in tests or []],
471
+ "next_steps": [_normalize_text(n) for n in next_steps or []],
472
+ "idempotency_key": idempotency_key or "",
473
+ }
474
+
475
+
476
+ def _hash_payload(payload: dict) -> str:
477
+ encoded = json.dumps(payload, sort_keys=True, ensure_ascii=False).encode("utf-8")
478
+ return hashlib.sha256(encoded).hexdigest()[:16]
479
+
480
+
481
+ def compute_record_fingerprint(payload: dict) -> str:
482
+ """Bounded fingerprint over the normalized semantic inputs of one record.
483
+
484
+ This is the retry key while the record is still pending in the worktree —
485
+ not a global dedupe key. Two genuinely separate sessions with identical
486
+ prose differ only if the caller says so, which is why an already-committed
487
+ match is never adopted (see classify_record).
488
+
489
+ Deliberately date-free (v2). v1 mixed in the calendar date, which made a
490
+ record unfindable by its own retry across a midnight rollover: the journal
491
+ and index were already written, the commit had failed, and the recomputed
492
+ fingerprint no longer matched the marker sitting in the journal, so the
493
+ retry appended a second entry for one session.
494
+
495
+ Dropping the date cannot over-collapse two same-day sessions, because the
496
+ date was equal for both of them anyway. Across days it only ever separated
497
+ records with byte-identical prose, commits, branch and package — and a
498
+ *committed* record of that shape is already refused for reuse by
499
+ cmd_add_session, which steps to the next `generation` of the payload
500
+ rather than reusing the committed marker. What is left
501
+ is exactly the question this key should answer: is there an uncommitted
502
+ record in this worktree matching these inputs?
503
+ """
504
+ return _hash_payload(payload)
505
+
506
+
507
+ def compute_legacy_fingerprint(payload: dict, date: str) -> str:
508
+ """The v1 fingerprint, for resolving markers written before the change.
509
+
510
+ Lookup only — nothing writes this scheme any more.
511
+ """
512
+ return _hash_payload({**payload, "date": date})
513
+
514
+
515
+ def render_marker(fingerprint: str) -> str:
516
+ """Machine-readable record marker; renders as nothing in Markdown."""
517
+ return f"{MARKER_PREFIX} v={MARKER_VERSION} fp={fingerprint} -->"
518
+
519
+
520
+ def render_legacy_marker(fingerprint: str) -> str:
521
+ """The v1 marker form: no version tag. Read, never written."""
522
+ return f"{MARKER_PREFIX} fp={fingerprint} -->"
523
+
524
+
525
+ # =============================================================================
526
+ # Session numbering (collision-proof across concurrent branches)
527
+ # =============================================================================
528
+
529
+ def _repo_relative(repo_root: Path, path: Path) -> str | None:
530
+ try:
531
+ return path.resolve().relative_to(repo_root.resolve()).as_posix()
532
+ except (OSError, ValueError):
533
+ return None
534
+
535
+
536
+ def _default_branch_local(repo_root: Path) -> str | None:
537
+ """Resolve the default branch without ever touching the network.
538
+
539
+ `resolve_default_branch()` falls back to `git remote show origin`, which
540
+ can block on a fetch. Session recording is a hot path, so it stops at the
541
+ local symbolic ref and the two conventional names.
542
+ """
543
+ rc, out, _ = run_git(
544
+ ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"],
545
+ cwd=repo_root,
546
+ timeout=GIT_PROBE_TIMEOUT,
547
+ )
548
+ if rc == 0 and out.strip():
549
+ return out.strip().rsplit("/", 1)[-1]
550
+ for candidate in ("main", "master"):
551
+ if branch_exists_locally(candidate, repo_root):
552
+ return candidate
553
+ return None
554
+
555
+
556
+ def _convergence_refs(repo_root: Path) -> list[str]:
557
+ """Refs whose recorded workspace state must be folded into the counter.
558
+
559
+ Ordered by how much they matter, then capped: HEAD and the default branch
560
+ first (the merge target every branch eventually meets), then every other
561
+ local head — a parallel worktree's branch lives there and is the case that
562
+ actually collided — then remote-tracking refs.
563
+ """
564
+ refs: list[str] = []
565
+
566
+ def add(ref: str) -> None:
567
+ if ref not in refs:
568
+ refs.append(ref)
569
+
570
+ rc, _, _ = run_git(["rev-parse", "--verify", "--quiet", "HEAD"], cwd=repo_root)
571
+ if rc == 0:
572
+ add("HEAD")
573
+
574
+ default = _default_branch_local(repo_root)
575
+ if default:
576
+ for ref in (f"refs/heads/{default}", f"refs/remotes/origin/{default}"):
577
+ rc, _, _ = run_git(
578
+ ["show-ref", "--verify", "--quiet", ref], cwd=repo_root
579
+ )
580
+ if rc == 0:
581
+ add(ref)
582
+
583
+ for pattern in ("refs/heads/", "refs/remotes/"):
584
+ rc, out, _ = run_git(
585
+ ["for-each-ref", "--format=%(refname)", pattern],
586
+ cwd=repo_root,
587
+ timeout=GIT_PROBE_TIMEOUT,
588
+ )
589
+ if rc != 0:
590
+ continue
591
+ for line in out.splitlines():
592
+ ref = line.strip()
593
+ # A symbolic ref like refs/remotes/origin/HEAD is a duplicate of
594
+ # the branch it points at and git grep rejects some of them.
595
+ if ref and not ref.endswith("/HEAD"):
596
+ add(ref)
597
+
598
+ return refs[:MAX_CONVERGENCE_REFS]
599
+
600
+
601
+ def _max_session_across_refs(repo_root: Path, refs: list[str], dev_rel: str) -> int:
602
+ """Highest session number recorded under `dev_rel` in any of `refs`.
603
+
604
+ One `git grep` over every ref rather than an ls-tree + show per ref: the
605
+ number of branches in a real repo is unbounded, the per-ref cost is not.
606
+ Exit status 1 (no match) is a normal outcome, not an error.
607
+ """
608
+ if not refs:
609
+ return 0
610
+
611
+ rc, out, _ = run_git(
612
+ [
613
+ "grep",
614
+ "--no-color",
615
+ "-h",
616
+ "-E",
617
+ "-e",
618
+ r"^## Session [0-9]+:",
619
+ "-e",
620
+ r"Total Sessions",
621
+ ]
622
+ + refs
623
+ + ["--", dev_rel],
624
+ cwd=repo_root,
625
+ timeout=GIT_PROBE_TIMEOUT,
626
+ )
627
+ if rc > 1 or not out:
628
+ return 0
629
+
630
+ # `git grep` prefixes each hit with `<rev>:<path>:`, so the numbers are
631
+ # matched anywhere in the line rather than anchored.
632
+ numbers = [int(m.group(1)) for m in re.finditer(r"## Session (\d+):", out)]
633
+ numbers += [
634
+ int(m.group(1)) for m in re.finditer(r"Total Sessions[^\d]{0,8}(\d+)", out)
635
+ ]
636
+ return max(numbers) if numbers else 0
637
+
638
+
639
+ def max_local_session(dev_dir: Path, index_file: Path) -> int:
640
+ """Highest session number visible in the working tree."""
641
+ highest = get_current_session(index_file)
642
+ for f in dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md"):
643
+ if not f.is_file():
644
+ continue
645
+ try:
646
+ content = f.read_text(encoding="utf-8")
647
+ except OSError:
648
+ continue
649
+ highest = max(highest, _max_session_in_journal(content))
650
+ return highest
651
+
652
+
653
+ def resolve_next_session(repo_root: Path, dev_dir: Path, index_file: Path) -> int:
654
+ """Next session number, converged across the local tree and other branches.
655
+
656
+ Deriving the number from the working tree alone lets two branches that
657
+ each record before merging claim the same number (observed twice on
658
+ 2026-08-06). Taking the union with every recorded ref — the default
659
+ branch, and the other local heads a parallel worktree records on — makes
660
+ the number monotonic across concurrent branches; `journal-*.md`
661
+ merge=union then keeps both entries when the branches meet.
662
+ """
663
+ highest = max_local_session(dev_dir, index_file)
664
+
665
+ dev_rel = _repo_relative(repo_root, dev_dir)
666
+ if dev_rel:
667
+ highest = max(
668
+ highest,
669
+ _max_session_across_refs(
670
+ repo_root, _convergence_refs(repo_root), dev_rel
671
+ ),
672
+ )
673
+
674
+ return highest + 1
675
+
676
+
677
+ # =============================================================================
678
+ # Pending-record classifier (R3/R4)
679
+ # =============================================================================
680
+
681
+ def find_marker_entries(dev_dir: Path, marker: str) -> list[tuple[Path, int | None]]:
682
+ """Locate every journal entry carrying `marker`.
683
+
684
+ Returns (journal_file, session_number); the number is None when the marker
685
+ is not directly under a `## Session N:` heading, which is malformed
686
+ pending evidence and must never be guessed into completion.
687
+ """
688
+ hits: list[tuple[Path, int | None]] = []
689
+ for f in sorted(dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
690
+ if not f.is_file():
691
+ continue
692
+ try:
693
+ lines = f.read_text(encoding="utf-8").splitlines()
694
+ except OSError:
695
+ continue
696
+ for i, line in enumerate(lines):
697
+ if line.strip() != marker:
698
+ continue
699
+ session_num: int | None = None
700
+ for j in range(i - 1, max(-1, i - 4), -1):
701
+ match = SESSION_HEADING_RE.match(lines[j])
702
+ if match:
703
+ session_num = int(match.group(1))
704
+ break
705
+ hits.append((f, session_num))
706
+ return hits
707
+
708
+
709
+ def content_at_head(repo_root: Path, path: Path) -> str | None:
710
+ """File content as committed at HEAD, or None when it isn't there.
711
+
712
+ The `./` prefix makes the path cwd-relative. `HEAD:<path>` alone is
713
+ relative to the *git* toplevel, which is not always `repo_root` — the
714
+ Trellis root is the nearest directory holding `.trellis/`, which can sit
715
+ below the git root. Without it, every lookup in that layout fails and a
716
+ committed record looks pending.
717
+ """
718
+ rel = _repo_relative(repo_root, path)
719
+ if not rel:
720
+ return None
721
+ rc, out, _ = run_git(["show", f"HEAD:./{rel}"], cwd=repo_root)
722
+ return out if rc == 0 else None
723
+
724
+
725
+ def index_has_session_row(index_file: Path, session_num: int) -> bool:
726
+ """Whether the session-history block already holds this session's row."""
727
+ if not index_file.is_file():
728
+ return False
729
+ try:
730
+ lines = index_file.read_text(encoding="utf-8").splitlines()
731
+ except OSError:
732
+ return False
733
+
734
+ row_re = re.compile(r"^\|\s*%d\s*\|" % session_num)
735
+ in_history = False
736
+ for line in lines:
737
+ if "@@@auto:session-history" in line:
738
+ in_history = True
739
+ continue
740
+ if "@@@/auto:session-history" in line:
741
+ in_history = False
742
+ continue
743
+ if in_history and row_re.match(line):
744
+ return True
745
+ return False
746
+
747
+
748
+ def _entry_date_at(lines: list[str], marker_index: int) -> str | None:
749
+ """The `**Date**:` value belonging to the entry whose marker is at `marker_index`.
750
+
751
+ `generate_session_content` renders it two lines below the marker. Scanning
752
+ a short window rather than a fixed offset keeps this working if blank-line
753
+ spacing around the header ever changes, and stops well before it could
754
+ reach the next entry.
755
+ """
756
+ for line in lines[marker_index + 1:marker_index + 6]:
757
+ match = ENTRY_DATE_RE.match(line.strip())
758
+ if match:
759
+ return match.group(1)
760
+ return None
761
+
762
+
763
+ def resolve_effective_marker(
764
+ dev_dir: Path,
765
+ payload: dict,
766
+ marker: str,
767
+ ) -> tuple[str, str | None]:
768
+ """The marker this record actually carries, across both schemes.
769
+
770
+ Returns (marker, error). The v2 marker wins whenever an entry carries it,
771
+ which is every record written since the change and costs one scan.
772
+
773
+ Otherwise this looks for a pending record written under v1. Those markers
774
+ are only a hash, but the entry renders its own `**Date**:` line right
775
+ below, so the date that produced the v1 fingerprint is recoverable from
776
+ the entry itself: recompute with that date and compare against what is
777
+ actually written there. That is exact, and unlike a +/-1 day window it does
778
+ not quietly fail a retry resumed after a weekend.
779
+
780
+ Returning the v1 marker leaves the entry untouched — it is an in-flight
781
+ record about to be committed, and rewriting its marker mid-repair would
782
+ move it to a state the machine does not model.
783
+ """
784
+ if find_marker_entries(dev_dir, marker):
785
+ return marker, None
786
+
787
+ matches: set[str] = set()
788
+ for journal in sorted(dev_dir.glob(f"{FILE_JOURNAL_PREFIX}*.md")):
789
+ if not journal.is_file():
790
+ continue
791
+ try:
792
+ lines = journal.read_text(encoding="utf-8").splitlines()
793
+ except OSError:
794
+ continue
795
+ for i, line in enumerate(lines):
796
+ legacy = LEGACY_MARKER_RE.match(line.strip())
797
+ if not legacy:
798
+ continue
799
+ date = _entry_date_at(lines, i)
800
+ if date is None:
801
+ continue
802
+ if compute_legacy_fingerprint(payload, date) == legacy.group(1):
803
+ matches.add(line.strip())
804
+
805
+ if len(matches) > 1:
806
+ return "", (
807
+ f"found {len(matches)} pending journal entries matching this record "
808
+ "under the pre-versioning marker scheme. Refusing to guess which one "
809
+ "to resume — remove the duplicate entry or pass --idempotency-key to "
810
+ "record a new session."
811
+ )
812
+ if matches:
813
+ return matches.pop(), None
814
+ return marker, None
815
+
816
+
817
+ def classify_record(
818
+ repo_root: Path,
819
+ dev_dir: Path,
820
+ index_file: Path,
821
+ marker: str,
822
+ ) -> tuple[str, Path | None, int | None, str | None]:
823
+ """Classify the current state of this exact record.
824
+
825
+ Returns (state, journal_file, session_num, error). Only a unique, exact,
826
+ still-uncommitted match is ever adopted; anything ambiguous, malformed or
827
+ partially written comes back as an error so the caller fails safely.
828
+ """
829
+ hits = find_marker_entries(dev_dir, marker)
830
+
831
+ if not hits:
832
+ return STATE_ABSENT, None, None, None
833
+
834
+ if len(hits) > 1:
835
+ files = ", ".join(sorted({p.name for p, _ in hits}))
836
+ return "", None, None, (
837
+ f"found {len(hits)} journal entries carrying this record's marker "
838
+ f"({files}). Refusing to guess which one to resume — remove the "
839
+ "duplicate entry or pass --idempotency-key to record a new session."
840
+ )
841
+
842
+ journal_file, session_num = hits[0]
843
+ if session_num is None:
844
+ return "", None, None, (
845
+ f"{journal_file.name} carries this record's marker without a "
846
+ "'## Session N:' heading above it. The pending record is malformed; "
847
+ "repair it by hand before retrying."
848
+ )
849
+
850
+ head_content = content_at_head(repo_root, journal_file)
851
+ if head_content is not None and marker in head_content:
852
+ return STATE_COMMITTED, journal_file, session_num, None
853
+
854
+ if index_has_session_row(index_file, session_num):
855
+ return STATE_INDEX_RECORDED, journal_file, session_num, None
856
+
857
+ recorded_total = get_current_session(index_file)
858
+ if recorded_total > session_num:
859
+ return "", None, None, (
860
+ f"journal entry for session {session_num} has no index row, but "
861
+ f"index.md already counts {recorded_total} sessions. Repairing the "
862
+ "row would rewrite a later record — resolve index.md by hand."
863
+ )
864
+
865
+ return STATE_JOURNAL_RECORDED, journal_file, session_num, None
866
+
867
+
868
+ # =============================================================================
869
+ # Rendering
870
+ # =============================================================================
871
+
872
+ def _render_bullet_section(header: str, items: list[str], bullet_prefix: str = "- ") -> str:
873
+ """Render a Markdown section as bullets, or "" when there is no content.
874
+
875
+ A section with zero provided values is omitted entirely from the
876
+ rendered entry rather than falling back to a placeholder string.
877
+ """
878
+ if not items:
879
+ return ""
880
+ bullets = "\n".join(f"{bullet_prefix}{item}" for item in items)
881
+ return f"\n\n### {header}\n\n{bullets}"
882
+
883
+
884
+ def _render_main_changes(changes: list[str], extra_content: str | None) -> str:
885
+ """Render the Main Changes section from --change bullets or freeform content."""
886
+ if changes:
887
+ return _render_bullet_section("Main Changes", changes)
888
+ if extra_content:
889
+ return f"\n\n### Main Changes\n\n{extra_content}"
890
+ return ""
891
+
892
+
893
+ def generate_session_content(
894
+ session_num: int,
895
+ title: str,
896
+ evidence: list[tuple[str, str]],
897
+ summary: str,
898
+ today: str,
899
+ marker: str,
900
+ package: str | None = None,
901
+ branch: str | None = None,
902
+ changes: list[str] | None = None,
903
+ extra_content: str | None = None,
904
+ tests: list[str] | None = None,
905
+ next_steps: list[str] | None = None,
906
+ ) -> str:
907
+ """Generate session content."""
908
+ if evidence:
909
+ commit_table = """| Hash | Message |
910
+ |------|---------|"""
911
+ for oid, subject in evidence:
912
+ commit_table += f"\n| `{oid}` | {escape_markdown_cell(subject)} |"
913
+ else:
914
+ commit_table = "(No commits - planning session)"
915
+
916
+ package_line = f"\n**Package**: {package}" if package else ""
917
+ branch_line = f"\n**Branch**: `{branch}`" if branch else ""
918
+
919
+ main_changes_section = _render_main_changes(changes or [], extra_content)
920
+ testing_section = _render_bullet_section("Testing", tests or [], bullet_prefix="- [OK] ")
921
+ next_steps_section = _render_bullet_section("Next Steps", next_steps or [])
922
+
923
+ return f"""
924
+
925
+ ## Session {session_num}: {title}
926
+ {marker}
927
+
928
+ **Date**: {today}
929
+ **Task**: {title}{package_line}{branch_line}
930
+
931
+ ### Summary
932
+
933
+ {summary}{main_changes_section}
934
+
935
+ ### Git Commits
936
+
937
+ {commit_table}{testing_section}
938
+
939
+ ### Status
940
+
941
+ [OK] **Completed**{next_steps_section}
942
+ """
943
+
944
+
945
+ def format_commit_display(evidence: list[tuple[str, str]]) -> str:
946
+ """Index-table rendering of the commit OIDs."""
947
+ if not evidence:
948
+ return "-"
949
+ return ", ".join(f"`{oid}`" for oid, _ in evidence)
950
+
951
+
952
+ def update_index(
953
+ index_file: Path,
954
+ dev_dir: Path,
955
+ title: str,
956
+ evidence: list[tuple[str, str]],
957
+ new_session: int,
958
+ active_file: str,
959
+ today: str,
960
+ branch: str | None = None,
961
+ ) -> bool:
962
+ """Update index.md with new session info."""
963
+ commit_display = format_commit_display(evidence)
964
+
965
+ # Get file number from active_file name
966
+ match = re.search(r"(\d+)", active_file)
967
+ active_num = int(match.group(1)) if match else 0
968
+ files_table = count_journal_files(dev_dir, active_num)
969
+
970
+ print(f"Updating index.md for session {new_session}...")
971
+ print(f" Title: {title}")
972
+ print(f" Commit: {commit_display}")
973
+ print(f" Active File: {active_file}")
974
+ print()
975
+
976
+ content = index_file.read_text(encoding="utf-8")
977
+
978
+ if "@@@auto:current-status" not in content:
979
+ print("Error: Markers not found in index.md. Please ensure markers exist.", file=sys.stderr)
980
+ return False
981
+
982
+ # Process sections
983
+ lines = content.splitlines()
984
+ new_lines = []
985
+
986
+ in_current_status = False
987
+ in_active_documents = False
988
+ in_session_history = False
989
+ header_written = False
990
+
991
+ for line in lines:
992
+ if "@@@auto:current-status" in line:
993
+ new_lines.append(line)
994
+ in_current_status = True
995
+ new_lines.append(f"- **Active File**: `{active_file}`")
996
+ new_lines.append(f"- **Total Sessions**: {new_session}")
997
+ new_lines.append(f"- **Last Active**: {today}")
998
+ continue
999
+
1000
+ if "@@@/auto:current-status" in line:
1001
+ in_current_status = False
1002
+ new_lines.append(line)
1003
+ continue
1004
+
1005
+ if "@@@auto:active-documents" in line:
1006
+ new_lines.append(line)
1007
+ in_active_documents = True
1008
+ new_lines.append("| File | Lines | Status |")
1009
+ new_lines.append("|------|-------|--------|")
1010
+ new_lines.append(files_table)
1011
+ continue
1012
+
1013
+ if "@@@/auto:active-documents" in line:
1014
+ in_active_documents = False
1015
+ new_lines.append(line)
1016
+ continue
1017
+
1018
+ if "@@@auto:session-history" in line:
1019
+ new_lines.append(line)
1020
+ in_session_history = True
1021
+ header_written = False
1022
+ continue
1023
+
1024
+ if "@@@/auto:session-history" in line:
1025
+ in_session_history = False
1026
+ new_lines.append(line)
1027
+ continue
1028
+
1029
+ if in_current_status:
1030
+ continue
1031
+
1032
+ if in_active_documents:
1033
+ continue
1034
+
1035
+ if in_session_history:
1036
+ # Migrate old 4/6-column headers to 5-column Branch-only history.
1037
+ if re.match(
1038
+ r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*Base Branch\s*\|\s*$",
1039
+ line,
1040
+ ):
1041
+ new_lines.append("| # | Date | Title | Commits | Branch |")
1042
+ continue
1043
+ if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*Branch\s*\|\s*$", line):
1044
+ new_lines.append("| # | Date | Title | Commits | Branch |")
1045
+ continue
1046
+ if re.match(r"^\|\s*#\s*\|\s*Date\s*\|\s*Title\s*\|\s*Commits\s*\|\s*$", line):
1047
+ new_lines.append("| # | Date | Title | Commits | Branch |")
1048
+ continue
1049
+ if re.match(r"^\|[-| ]+\|\s*$", line) and not header_written:
1050
+ new_lines.append("|---|------|-------|---------|--------|")
1051
+ new_lines.append(f"| {new_session} | {today} | {title} | {commit_display} | `{branch or '-'}` |")
1052
+ header_written = True
1053
+ continue
1054
+ new_lines.append(line)
1055
+ continue
1056
+
1057
+ new_lines.append(line)
1058
+
1059
+ if not write_text_atomic(index_file, "\n".join(new_lines)):
1060
+ print(f"Error: failed to write {index_file}", file=sys.stderr)
1061
+ return False
1062
+ print("[OK] Updated index.md successfully!")
1063
+ return True
1064
+
1065
+
1066
+ # =============================================================================
1067
+ # Main Function
1068
+ # =============================================================================
1069
+
1070
+ def _auto_commit_workspace(repo_root: Path) -> str:
1071
+ """Stage Trellis-owned workspace + current-task paths and commit.
1072
+
1073
+ Path scope is restricted to specific products: the current developer's
1074
+ journal files + index.md, and ONLY the current task directory (resolved
1075
+ via ``get_current_task``). We never `git add` the whole `.trellis/` tree
1076
+ or iterate over all active task dirs (#303: parallel-window dirty task
1077
+ dirs must not be bundled into the session auto-commit). If `.gitignore`
1078
+ blocks the specific paths we warn + skip — never retry with ``-f``.
1079
+
1080
+ Honors ``session_auto_commit`` in ``.trellis/config.yaml``: when set to
1081
+ ``false``, this function returns immediately without touching git
1082
+ (journal/index files are still written to disk by the caller).
1083
+
1084
+ Returns one of ``COMMIT_DONE`` / ``COMMIT_SKIPPED`` / ``COMMIT_BLOCKED`` /
1085
+ ``COMMIT_FAILED``. ``COMMIT_BLOCKED`` is the gitignored-`.trellis/` case:
1086
+ the user has told git to stay out of this tree, so it is a configured skip
1087
+ like ``session_auto_commit: false``, not a failure to retry.
1088
+ """
1089
+ if not get_session_auto_commit(repo_root):
1090
+ print(
1091
+ "[OK] session_auto_commit: false — skipping git stage/commit.",
1092
+ file=sys.stderr,
1093
+ )
1094
+ return COMMIT_SKIPPED
1095
+
1096
+ # Not a git repository at all: like the gitignored case, this is an
1097
+ # environment the user configured, not a transient git error — a retry
1098
+ # can never succeed, so it must not be COMMIT_FAILED's exit-1 loop.
1099
+ rc, _, _ = run_git(["rev-parse", "--is-inside-work-tree"], cwd=repo_root)
1100
+ if rc != 0:
1101
+ print(
1102
+ "[WARN] Not a git repository — journal/index written, "
1103
+ "skipping auto-commit.",
1104
+ file=sys.stderr,
1105
+ )
1106
+ return COMMIT_BLOCKED
1107
+
1108
+ commit_msg = get_session_commit_message(repo_root)
1109
+ # Resolve the current task so staging is scoped to its dir only. The ref
1110
+ # is ``.trellis/tasks/<name>`` (or under archive/) — pass the bare name.
1111
+ current = get_current_task(repo_root)
1112
+ if current:
1113
+ task_name = Path(current).name
1114
+ paths = safe_trellis_paths_to_add(repo_root, task_name=task_name)
1115
+ else:
1116
+ # Current task unknown (0 or >=2 parallel sessions — exactly the
1117
+ # parallel-window case #303 is about). Do NOT fall back to the wide
1118
+ # `tasks_dir.iterdir()` scan; that would re-leak other tasks' dirty
1119
+ # dirs into the session commit. Stage only the developer's journal/
1120
+ # index and skip every task dir.
1121
+ paths = [
1122
+ p
1123
+ for p in safe_trellis_paths_to_add(repo_root, task_name=None)
1124
+ if not p.startswith(f"{DIR_WORKFLOW}/{DIR_TASKS}/")
1125
+ ]
1126
+ if not paths:
1127
+ print("[OK] No workspace changes to commit.", file=sys.stderr)
1128
+ return COMMIT_SKIPPED
1129
+
1130
+ success, _, err = safe_git_add(paths, repo_root)
1131
+ if not success:
1132
+ if err and "ignored by" in err.lower():
1133
+ print_gitignore_warning(paths)
1134
+ return COMMIT_BLOCKED
1135
+ print(
1136
+ f"[WARN] git add failed: {err.strip() if err else 'unknown error'}",
1137
+ file=sys.stderr,
1138
+ )
1139
+ return COMMIT_FAILED
1140
+
1141
+ # Check if there are staged changes for the paths we just staged.
1142
+ rc, _, _ = run_git(
1143
+ ["diff", "--cached", "--quiet", "--", *paths], cwd=repo_root
1144
+ )
1145
+ if rc == 0:
1146
+ print("[OK] No workspace changes to commit.", file=sys.stderr)
1147
+ return COMMIT_SKIPPED
1148
+
1149
+ # Commit with an explicit pathspec: a bare `git commit` would sweep any
1150
+ # unrelated entries the developer had staged before this script ran into
1151
+ # the chore commit (#579). The pathspec keeps their staged work untouched.
1152
+ rc, _, commit_err = run_git(
1153
+ ["commit", "-m", commit_msg, "--", *paths], cwd=repo_root
1154
+ )
1155
+ if rc == 0:
1156
+ print(f"[OK] Auto-committed: {commit_msg}", file=sys.stderr)
1157
+ return COMMIT_DONE
1158
+
1159
+ print(
1160
+ f"[WARN] Auto-commit failed: {commit_err.strip()}",
1161
+ file=sys.stderr,
1162
+ )
1163
+ return COMMIT_FAILED
1164
+
1165
+
1166
+ def _print_commit_checkpoint(session_num: int, journal_name: str) -> None:
1167
+ """Actionable checkpoint for a failed auto-commit (R5)."""
1168
+ print("", file=sys.stderr)
1169
+ print(
1170
+ f"[BLOCKED] Checkpoint: session {session_num} is recorded in "
1171
+ f"{journal_name} and index.md, but the auto-commit did not complete.",
1172
+ file=sys.stderr,
1173
+ )
1174
+ print(
1175
+ "[BLOCKED] The pending record was left exactly as written — nothing was "
1176
+ "rolled back.",
1177
+ file=sys.stderr,
1178
+ )
1179
+ print(
1180
+ "[BLOCKED] Fix the git error above, then re-run the identical "
1181
+ "add_session.py command: it resumes at the commit step and will not add "
1182
+ "a second session.",
1183
+ file=sys.stderr,
1184
+ )
1185
+
1186
+
1187
+ def add_session(
1188
+ title: str,
1189
+ commit: str = "-",
1190
+ summary: str = "Session summary was not supplied.",
1191
+ changes: list[str] | None = None,
1192
+ extra_content: str | None = None,
1193
+ tests: list[str] | None = None,
1194
+ next_steps: list[str] | None = None,
1195
+ auto_commit: bool = True,
1196
+ package: str | None = None,
1197
+ branch: str | None = None,
1198
+ commit_subjects: list[str] | None = None,
1199
+ idempotency_key: str | None = None,
1200
+ ) -> int:
1201
+ """Add a new session, resuming an interrupted one instead of duplicating it."""
1202
+ repo_root = get_repo_root()
1203
+ warn_if_parallel_worktree(repo_root)
1204
+ ensure_developer(repo_root)
1205
+
1206
+ developer = get_developer(repo_root)
1207
+ if not developer:
1208
+ print("Error: Developer not initialized", file=sys.stderr)
1209
+ return 1
1210
+
1211
+ dev_dir = get_workspace_dir(repo_root)
1212
+ if not dev_dir:
1213
+ print("Error: Workspace directory not found", file=sys.stderr)
1214
+ return 1
1215
+
1216
+ # -------------------------------------------------------------------
1217
+ # Preflight — no writes happen until every one of these succeeds.
1218
+ # -------------------------------------------------------------------
1219
+ if idempotency_key is not None and not IDEMPOTENCY_KEY_RE.match(idempotency_key):
1220
+ print(
1221
+ f"Error: invalid --idempotency-key '{idempotency_key}': expected 1-64 "
1222
+ "characters from [A-Za-z0-9._-]",
1223
+ file=sys.stderr,
1224
+ )
1225
+ return 1
1226
+
1227
+ tokens, token_error = parse_commit_tokens(commit)
1228
+ if token_error:
1229
+ print(f"Error: {token_error}", file=sys.stderr)
1230
+ return 1
1231
+
1232
+ overrides, override_error = parse_subject_overrides(commit_subjects)
1233
+ if override_error:
1234
+ print(f"Error: {override_error}", file=sys.stderr)
1235
+ return 1
1236
+
1237
+ evidence, evidence_error = build_commit_evidence(repo_root, tokens, overrides)
1238
+ if evidence_error:
1239
+ print(f"Error: {evidence_error}", file=sys.stderr)
1240
+ return 1
1241
+
1242
+ max_lines = get_max_journal_lines(repo_root)
1243
+ index_file = dev_dir / "index.md"
1244
+ today = datetime.now().strftime("%Y-%m-%d")
1245
+
1246
+ payload = _fingerprint_payload(
1247
+ developer, title, summary, package, branch, evidence,
1248
+ changes, extra_content, tests, next_steps, idempotency_key,
1249
+ )
1250
+ marker = render_marker(compute_record_fingerprint(payload))
1251
+
1252
+ # `today` is no longer a fingerprint input; a record has to be findable by
1253
+ # its own retry after a date rollover. It still dates the rendered entry.
1254
+ marker, resolve_error = resolve_effective_marker(dev_dir, payload, marker)
1255
+ if resolve_error:
1256
+ print(f"Error: {resolve_error}", file=sys.stderr)
1257
+ return 1
1258
+
1259
+ state, matched_file, matched_num, classify_error = classify_record(
1260
+ repo_root, dev_dir, index_file, marker
1261
+ )
1262
+ if classify_error:
1263
+ print(f"Error: {classify_error}", file=sys.stderr)
1264
+ return 1
1265
+
1266
+ if state == STATE_COMMITTED and idempotency_key:
1267
+ print(
1268
+ f"[OK] Session {matched_num} with idempotency key "
1269
+ f"'{idempotency_key}' is already recorded and committed in "
1270
+ f"{matched_file.name if matched_file else 'the journal'}; "
1271
+ "nothing to do.",
1272
+ file=sys.stderr,
1273
+ )
1274
+ return 0
1275
+
1276
+ # A committed record is finished, so an identical later request is a
1277
+ # legitimately new session. It must not reuse the committed marker: two
1278
+ # entries carrying one marker make every later run ambiguous, and
1279
+ # classify_record refuses to guess between them. Step to the next
1280
+ # generation of this record instead. The marker stays a pure function of
1281
+ # (payload, generation), so a retry of the new entry recomputes the same
1282
+ # marker and still resumes; generation 0 omits the field entirely, leaving
1283
+ # first-time markers byte-identical to what this scheme already writes.
1284
+ generation = 0
1285
+ while state == STATE_COMMITTED:
1286
+ generation += 1
1287
+ marker = render_marker(
1288
+ compute_record_fingerprint({**payload, "generation": generation})
1289
+ )
1290
+ state, matched_file, matched_num, classify_error = classify_record(
1291
+ repo_root, dev_dir, index_file, marker
1292
+ )
1293
+ if classify_error:
1294
+ print(f"Error: {classify_error}", file=sys.stderr)
1295
+ return 1
1296
+
1297
+ print("========================================", file=sys.stderr)
1298
+ print("ADD SESSION", file=sys.stderr)
1299
+ print("========================================", file=sys.stderr)
1300
+ print("", file=sys.stderr)
1301
+
1302
+ # -------------------------------------------------------------------
1303
+ # Absent → journal-recorded
1304
+ # -------------------------------------------------------------------
1305
+ target_file: Path | None
1306
+ target_num: int
1307
+ new_session: int
1308
+
1309
+ if state == STATE_ABSENT:
1310
+ journal_file, current_num, current_lines = get_latest_journal_info(dev_dir)
1311
+ new_session = resolve_next_session(repo_root, dev_dir, index_file)
1312
+
1313
+ session_content = generate_session_content(
1314
+ new_session, title, evidence, summary, today, marker, package, branch,
1315
+ changes=changes, extra_content=extra_content, tests=tests,
1316
+ next_steps=next_steps,
1317
+ )
1318
+ content_lines = len(session_content.splitlines())
1319
+
1320
+ print(f"Session: {new_session}", file=sys.stderr)
1321
+ print(f"Title: {title}", file=sys.stderr)
1322
+ print(f"Commit: {format_commit_display(evidence)}", file=sys.stderr)
1323
+ print("", file=sys.stderr)
1324
+ print(f"Current journal file: {FILE_JOURNAL_PREFIX}{current_num}.md", file=sys.stderr)
1325
+ print(f"Current lines: {current_lines}", file=sys.stderr)
1326
+ print(f"New content lines: {content_lines}", file=sys.stderr)
1327
+ print(f"Total after append: {current_lines + content_lines}", file=sys.stderr)
1328
+ print("", file=sys.stderr)
1329
+
1330
+ target_file = journal_file
1331
+ target_num = current_num
1332
+
1333
+ if current_lines + content_lines > max_lines:
1334
+ target_num = current_num + 1
1335
+ print(f"[!] Exceeds {max_lines} lines, creating {FILE_JOURNAL_PREFIX}{target_num}.md", file=sys.stderr)
1336
+ target_file = create_new_journal_file(dev_dir, target_num, developer, today, max_lines)
1337
+ if target_file is None:
1338
+ print(
1339
+ f"Error: failed to create {FILE_JOURNAL_PREFIX}{target_num}.md; "
1340
+ "nothing was recorded.",
1341
+ file=sys.stderr,
1342
+ )
1343
+ return 1
1344
+ print(f"Created: {target_file}", file=sys.stderr)
1345
+
1346
+ if target_file is None:
1347
+ print(
1348
+ "Error: no journal file to append to. Run init_developer.py first.",
1349
+ file=sys.stderr,
1350
+ )
1351
+ return 1
1352
+
1353
+ try:
1354
+ existing = target_file.read_text(encoding="utf-8") if target_file.is_file() else ""
1355
+ except OSError as exc:
1356
+ print(f"Error: cannot read {target_file}: {exc}", file=sys.stderr)
1357
+ return 1
1358
+
1359
+ if not write_text_atomic(target_file, existing + session_content):
1360
+ print(
1361
+ f"Error: failed to append the session to {target_file.name}; "
1362
+ "the previous journal content is intact and nothing was recorded.",
1363
+ file=sys.stderr,
1364
+ )
1365
+ return 1
1366
+ print(f"[OK] Appended session to {target_file.name}", file=sys.stderr)
1367
+ state = STATE_JOURNAL_RECORDED
1368
+ else:
1369
+ if matched_file is None or matched_num is None:
1370
+ print(
1371
+ f"Error: resumed state '{state}' without a matching journal entry.",
1372
+ file=sys.stderr,
1373
+ )
1374
+ return 1
1375
+ target_file = matched_file
1376
+ new_session = matched_num
1377
+ target_num = _extract_journal_num(target_file.stem)
1378
+ print(
1379
+ f"[RESUME] Session {new_session} is already in {target_file.name} and "
1380
+ f"uncommitted; continuing from '{state}' instead of appending again.",
1381
+ file=sys.stderr,
1382
+ )
1383
+ print("", file=sys.stderr)
1384
+
1385
+ print("", file=sys.stderr)
1386
+
1387
+ # -------------------------------------------------------------------
1388
+ # Journal-recorded → index-recorded
1389
+ # -------------------------------------------------------------------
1390
+ active_file = f"{FILE_JOURNAL_PREFIX}{target_num}.md"
1391
+ if state == STATE_JOURNAL_RECORDED:
1392
+ if not update_index(
1393
+ index_file,
1394
+ dev_dir,
1395
+ title,
1396
+ evidence,
1397
+ new_session,
1398
+ active_file,
1399
+ today,
1400
+ branch,
1401
+ ):
1402
+ print(
1403
+ f"[BLOCKED] Checkpoint: session {new_session} is in "
1404
+ f"{active_file} but index.md was not updated. Fix index.md, then "
1405
+ "re-run the identical command to repair the row without adding a "
1406
+ "second session.",
1407
+ file=sys.stderr,
1408
+ )
1409
+ return 1
1410
+ state = STATE_INDEX_RECORDED
1411
+ else:
1412
+ print(f"[OK] index.md already records session {new_session}.", file=sys.stderr)
1413
+
1414
+ print("", file=sys.stderr)
1415
+ print("========================================", file=sys.stderr)
1416
+ print(f"[OK] Session {new_session} added successfully!", file=sys.stderr)
1417
+ print("========================================", file=sys.stderr)
1418
+ print("", file=sys.stderr)
1419
+ print("Files updated:", file=sys.stderr)
1420
+ print(f" - {target_file.name if target_file else 'journal'}", file=sys.stderr)
1421
+ print(" - index.md", file=sys.stderr)
1422
+
1423
+ # -------------------------------------------------------------------
1424
+ # Index-recorded → committed
1425
+ # -------------------------------------------------------------------
1426
+ if not auto_commit:
1427
+ return 0
1428
+
1429
+ print("", file=sys.stderr)
1430
+ outcome = _auto_commit_workspace(repo_root)
1431
+
1432
+ if outcome == COMMIT_FAILED:
1433
+ _print_commit_checkpoint(
1434
+ new_session, target_file.name if target_file else "the journal"
1435
+ )
1436
+ return 1
1437
+
1438
+ if outcome == COMMIT_DONE and target_file is not None:
1439
+ committed = content_at_head(repo_root, target_file)
1440
+ if committed is None or marker not in committed:
1441
+ print(
1442
+ f"[WARN] Auto-commit reported success but {target_file.name} at "
1443
+ "HEAD does not contain this session's record.",
1444
+ file=sys.stderr,
1445
+ )
1446
+ _print_commit_checkpoint(new_session, target_file.name)
1447
+ return 1
1448
+
1449
+ return 0
1450
+
1451
+
1452
+ # =============================================================================
1453
+ # Main Entry
1454
+ # =============================================================================
1455
+
1456
+ def main() -> int:
1457
+ """CLI entry point."""
1458
+ parser = argparse.ArgumentParser(
1459
+ description="Add a new session to journal file and update index.md"
1460
+ )
1461
+ parser.add_argument("--title", required=True, help="Session title")
1462
+ parser.add_argument(
1463
+ "--commit",
1464
+ default="-",
1465
+ help=(
1466
+ "Comma-separated commit OIDs (7-40 hex chars each). Each one is "
1467
+ "resolved to its real subject before anything is written; an "
1468
+ "unresolvable OID fails the command. Use '-' for a planning session."
1469
+ ),
1470
+ )
1471
+ parser.add_argument(
1472
+ "--commit-subject",
1473
+ action="append",
1474
+ metavar="OID=SUBJECT",
1475
+ help=(
1476
+ "Explicit subject for a commit that cannot be resolved locally "
1477
+ "(repeatable). The mapping must be one-to-one with --commit."
1478
+ ),
1479
+ )
1480
+ parser.add_argument("--summary", default="Session summary was not supplied.", help="Brief summary")
1481
+ parser.add_argument("--content-file", help="Path to file with detailed content")
1482
+ parser.add_argument("--package", help="Package name tag (e.g., cli, docs-site)")
1483
+ parser.add_argument("--branch", help="Branch name (auto-detected if omitted)")
1484
+ parser.add_argument("--change", action="append", help="Main Changes bullet (repeatable)")
1485
+ parser.add_argument("--test", action="append", help="Testing bullet (repeatable)")
1486
+ parser.add_argument("--next-step", action="append", help="Next Steps bullet (repeatable)")
1487
+ parser.add_argument(
1488
+ "--idempotency-key",
1489
+ help=(
1490
+ "Caller-supplied retry key ([A-Za-z0-9._-], 1-64 chars). Makes an "
1491
+ "already-committed identical record a no-op instead of a new session."
1492
+ ),
1493
+ )
1494
+ parser.add_argument("--no-commit", action="store_true",
1495
+ help="Skip auto-commit of workspace changes")
1496
+ parser.add_argument("--stdin", action="store_true",
1497
+ help="Read extra content from stdin (explicit opt-in)")
1498
+
1499
+ args = parser.parse_args()
1500
+
1501
+ extra_content: str | None = None
1502
+ if args.content_file:
1503
+ content_path = Path(args.content_file)
1504
+ if content_path.is_file():
1505
+ extra_content = content_path.read_text(encoding="utf-8")
1506
+ elif args.stdin:
1507
+ extra_content = sys.stdin.read()
1508
+
1509
+ # Load active task once — shared by package and branch resolution
1510
+ repo_root = get_repo_root()
1511
+ current = get_current_task(repo_root)
1512
+ task_data = load_task(repo_root / current) if current else None
1513
+
1514
+ package = args.package
1515
+ if package:
1516
+ # CLI source: fail-fast in monorepo, ignore in single-repo
1517
+ if not is_monorepo(repo_root):
1518
+ print("Warning: --package ignored in single-repo project", file=sys.stderr)
1519
+ package = None
1520
+ elif not validate_package(package, repo_root):
1521
+ packages = get_packages(repo_root)
1522
+ available = ", ".join(sorted(packages.keys())) if packages else "(none)"
1523
+ print(f"Error: unknown package '{package}'. Available: {available}", file=sys.stderr)
1524
+ return 1
1525
+ else:
1526
+ # Inferred: active task's task.json.package → default_package → None
1527
+ task_package = task_data.package if task_data else None
1528
+ package = resolve_package(task_package, repo_root)
1529
+
1530
+ branch = resolve_session_branch(repo_root, args.branch, task_data)
1531
+
1532
+ return add_session(
1533
+ args.title, args.commit, args.summary,
1534
+ changes=args.change, extra_content=extra_content, tests=args.test,
1535
+ next_steps=args.next_step,
1536
+ auto_commit=not args.no_commit,
1537
+ package=package,
1538
+ branch=branch,
1539
+ commit_subjects=args.commit_subject,
1540
+ idempotency_key=args.idempotency_key,
1541
+ )
1542
+
1543
+
1544
+ if __name__ == "__main__":
1545
+ sys.exit(main())