@pennixrv/trellis 0.7.0-beta.9 → 0.7.1-beta.1

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 (488) hide show
  1. package/README.md +1 -1
  2. package/dist/cli/index.d.ts.map +1 -1
  3. package/dist/cli/index.js +14 -26
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/commands/ablate.js +1 -1
  6. package/dist/commands/ablate.js.map +1 -1
  7. package/dist/commands/init.d.ts.map +1 -1
  8. package/dist/commands/init.js +30 -56
  9. package/dist/commands/init.js.map +1 -1
  10. package/dist/commands/task-progress.d.ts +2 -0
  11. package/dist/commands/task-progress.d.ts.map +1 -1
  12. package/dist/commands/task-progress.js +37 -30
  13. package/dist/commands/task-progress.js.map +1 -1
  14. package/dist/commands/{channel → teamwork}/adapters/claude.d.ts +3 -3
  15. package/dist/commands/teamwork/adapters/claude.d.ts.map +1 -0
  16. package/dist/commands/{channel → teamwork}/adapters/claude.js +3 -3
  17. package/dist/commands/teamwork/adapters/claude.js.map +1 -0
  18. package/dist/commands/{channel → teamwork}/adapters/codex.d.ts +1 -15
  19. package/dist/commands/teamwork/adapters/codex.d.ts.map +1 -0
  20. package/dist/commands/{channel → teamwork}/adapters/codex.js +5 -42
  21. package/dist/commands/teamwork/adapters/codex.js.map +1 -0
  22. package/dist/commands/{channel → teamwork}/adapters/index.d.ts +6 -6
  23. package/dist/commands/teamwork/adapters/index.d.ts.map +1 -0
  24. package/dist/commands/{channel → teamwork}/adapters/index.js +16 -52
  25. package/dist/commands/teamwork/adapters/index.js.map +1 -0
  26. package/dist/commands/{channel → teamwork}/adapters/types.d.ts +2 -2
  27. package/dist/commands/teamwork/adapters/types.d.ts.map +1 -0
  28. package/dist/commands/teamwork/adapters/types.js.map +1 -0
  29. package/dist/commands/{channel → teamwork}/agent-loader.d.ts +2 -4
  30. package/dist/commands/teamwork/agent-loader.d.ts.map +1 -0
  31. package/dist/commands/{channel → teamwork}/agent-loader.js +4 -35
  32. package/dist/commands/teamwork/agent-loader.js.map +1 -0
  33. package/dist/commands/teamwork/artifact.d.ts +177 -0
  34. package/dist/commands/teamwork/artifact.d.ts.map +1 -0
  35. package/dist/commands/teamwork/artifact.js +908 -0
  36. package/dist/commands/teamwork/artifact.js.map +1 -0
  37. package/dist/commands/{channel → teamwork}/context-loader.d.ts +1 -1
  38. package/dist/commands/teamwork/context-loader.d.ts.map +1 -0
  39. package/dist/commands/{channel → teamwork}/context-loader.js +12 -12
  40. package/dist/commands/teamwork/context-loader.js.map +1 -0
  41. package/dist/commands/{channel → teamwork}/context-trust.d.ts +6 -6
  42. package/dist/commands/teamwork/context-trust.d.ts.map +1 -0
  43. package/dist/commands/{channel → teamwork}/context-trust.js +16 -16
  44. package/dist/commands/teamwork/context-trust.js.map +1 -0
  45. package/dist/commands/teamwork/context.d.ts +16 -0
  46. package/dist/commands/teamwork/context.d.ts.map +1 -0
  47. package/dist/commands/{channel → teamwork}/context.js +17 -17
  48. package/dist/commands/teamwork/context.js.map +1 -0
  49. package/dist/commands/teamwork/coordinator.d.ts +89 -0
  50. package/dist/commands/teamwork/coordinator.d.ts.map +1 -0
  51. package/dist/commands/teamwork/coordinator.js +1198 -0
  52. package/dist/commands/teamwork/coordinator.js.map +1 -0
  53. package/dist/commands/teamwork/create.d.ts +23 -0
  54. package/dist/commands/teamwork/create.d.ts.map +1 -0
  55. package/dist/commands/teamwork/create.js +39 -0
  56. package/dist/commands/teamwork/create.js.map +1 -0
  57. package/dist/commands/{channel → teamwork}/dev-parse-trace.d.ts +1 -1
  58. package/dist/commands/teamwork/dev-parse-trace.d.ts.map +1 -0
  59. package/dist/commands/{channel → teamwork}/dev-parse-trace.js +1 -1
  60. package/dist/commands/teamwork/dev-parse-trace.js.map +1 -0
  61. package/dist/commands/{channel → teamwork}/guard.d.ts +27 -46
  62. package/dist/commands/teamwork/guard.d.ts.map +1 -0
  63. package/dist/commands/{channel → teamwork}/guard.js +54 -156
  64. package/dist/commands/teamwork/guard.js.map +1 -0
  65. package/dist/commands/teamwork/index.d.ts +3 -0
  66. package/dist/commands/teamwork/index.d.ts.map +1 -0
  67. package/dist/commands/{channel → teamwork}/index.js +135 -165
  68. package/dist/commands/teamwork/index.js.map +1 -0
  69. package/dist/commands/{channel → teamwork}/interrupt.d.ts +1 -1
  70. package/dist/commands/teamwork/interrupt.d.ts.map +1 -0
  71. package/dist/commands/teamwork/interrupt.js +22 -0
  72. package/dist/commands/teamwork/interrupt.js.map +1 -0
  73. package/dist/commands/teamwork/kill.d.ts +7 -0
  74. package/dist/commands/teamwork/kill.d.ts.map +1 -0
  75. package/dist/commands/{channel → teamwork}/kill.js +21 -21
  76. package/dist/commands/teamwork/kill.js.map +1 -0
  77. package/dist/commands/teamwork/list.d.ts +17 -0
  78. package/dist/commands/teamwork/list.d.ts.map +1 -0
  79. package/dist/commands/{channel → teamwork}/list.js +18 -27
  80. package/dist/commands/teamwork/list.js.map +1 -0
  81. package/dist/commands/{channel → teamwork}/messages.d.ts +1 -1
  82. package/dist/commands/teamwork/messages.d.ts.map +1 -0
  83. package/dist/commands/{channel → teamwork}/messages.js +18 -18
  84. package/dist/commands/teamwork/messages.js.map +1 -0
  85. package/dist/commands/teamwork/profiles.d.ts +25 -0
  86. package/dist/commands/teamwork/profiles.d.ts.map +1 -0
  87. package/dist/commands/teamwork/profiles.js +125 -0
  88. package/dist/commands/teamwork/profiles.js.map +1 -0
  89. package/dist/commands/teamwork/rm.d.ts +27 -0
  90. package/dist/commands/teamwork/rm.d.ts.map +1 -0
  91. package/dist/commands/{channel → teamwork}/rm.js +21 -22
  92. package/dist/commands/teamwork/rm.js.map +1 -0
  93. package/dist/commands/teamwork/run.d.ts +29 -0
  94. package/dist/commands/{channel → teamwork}/run.d.ts.map +1 -1
  95. package/dist/commands/teamwork/run.js +129 -0
  96. package/dist/commands/teamwork/run.js.map +1 -0
  97. package/dist/commands/{channel → teamwork}/send.d.ts +1 -1
  98. package/dist/commands/teamwork/send.d.ts.map +1 -0
  99. package/dist/commands/{channel → teamwork}/send.js +7 -7
  100. package/dist/commands/teamwork/send.js.map +1 -0
  101. package/dist/commands/{channel → teamwork}/spawn.d.ts +8 -12
  102. package/dist/commands/teamwork/spawn.d.ts.map +1 -0
  103. package/dist/commands/{channel → teamwork}/spawn.js +51 -112
  104. package/dist/commands/teamwork/spawn.js.map +1 -0
  105. package/dist/commands/teamwork/store/events.d.ts +22 -0
  106. package/dist/commands/teamwork/store/events.d.ts.map +1 -0
  107. package/dist/commands/{channel → teamwork}/store/events.js +8 -8
  108. package/dist/commands/teamwork/store/events.js.map +1 -0
  109. package/dist/commands/teamwork/store/filter.d.ts +3 -0
  110. package/dist/commands/teamwork/store/filter.d.ts.map +1 -0
  111. package/dist/commands/teamwork/store/filter.js +2 -0
  112. package/dist/commands/teamwork/store/filter.js.map +1 -0
  113. package/dist/commands/{channel → teamwork}/store/lock.d.ts +1 -1
  114. package/dist/commands/teamwork/store/lock.d.ts.map +1 -0
  115. package/dist/commands/{channel → teamwork}/store/lock.js +2 -2
  116. package/dist/commands/teamwork/store/lock.js.map +1 -0
  117. package/dist/commands/{channel → teamwork}/store/paths.d.ts +18 -32
  118. package/dist/commands/teamwork/store/paths.d.ts.map +1 -0
  119. package/dist/commands/{channel → teamwork}/store/paths.js +42 -109
  120. package/dist/commands/teamwork/store/paths.js.map +1 -0
  121. package/dist/commands/teamwork/store/schema.d.ts +18 -0
  122. package/dist/commands/teamwork/store/schema.d.ts.map +1 -0
  123. package/dist/commands/teamwork/store/schema.js +23 -0
  124. package/dist/commands/teamwork/store/schema.js.map +1 -0
  125. package/dist/commands/{channel → teamwork}/store/thread-state.d.ts +3 -3
  126. package/dist/commands/teamwork/store/thread-state.d.ts.map +1 -0
  127. package/dist/commands/{channel → teamwork}/store/thread-state.js +1 -1
  128. package/dist/commands/teamwork/store/thread-state.js.map +1 -0
  129. package/dist/commands/teamwork/store/watch.d.ts +19 -0
  130. package/dist/commands/teamwork/store/watch.d.ts.map +1 -0
  131. package/dist/commands/{channel → teamwork}/store/watch.js +8 -8
  132. package/dist/commands/teamwork/store/watch.js.map +1 -0
  133. package/dist/commands/teamwork/supervisor/idle.d.ts.map +1 -0
  134. package/dist/commands/teamwork/supervisor/idle.js.map +1 -0
  135. package/dist/commands/{channel → teamwork}/supervisor/inbox.d.ts +2 -2
  136. package/dist/commands/teamwork/supervisor/inbox.d.ts.map +1 -0
  137. package/dist/commands/{channel → teamwork}/supervisor/inbox.js +14 -14
  138. package/dist/commands/teamwork/supervisor/inbox.js.map +1 -0
  139. package/dist/commands/{channel → teamwork}/supervisor/shutdown.d.ts +3 -6
  140. package/dist/commands/teamwork/supervisor/shutdown.d.ts.map +1 -0
  141. package/dist/commands/{channel → teamwork}/supervisor/shutdown.js +7 -17
  142. package/dist/commands/teamwork/supervisor/shutdown.js.map +1 -0
  143. package/dist/commands/{channel → teamwork}/supervisor/stdout.d.ts +3 -5
  144. package/dist/commands/teamwork/supervisor/stdout.d.ts.map +1 -0
  145. package/dist/commands/{channel → teamwork}/supervisor/stdout.js +12 -23
  146. package/dist/commands/teamwork/supervisor/stdout.js.map +1 -0
  147. package/dist/commands/teamwork/supervisor/turns.d.ts.map +1 -0
  148. package/dist/commands/teamwork/supervisor/turns.js.map +1 -0
  149. package/dist/commands/{channel → teamwork}/supervisor/warning.d.ts +5 -5
  150. package/dist/commands/teamwork/supervisor/warning.d.ts.map +1 -0
  151. package/dist/commands/{channel → teamwork}/supervisor/warning.js +6 -6
  152. package/dist/commands/teamwork/supervisor/warning.js.map +1 -0
  153. package/dist/commands/{channel → teamwork}/supervisor.d.ts +21 -21
  154. package/dist/commands/teamwork/supervisor.d.ts.map +1 -0
  155. package/dist/commands/{channel → teamwork}/supervisor.js +53 -63
  156. package/dist/commands/teamwork/supervisor.js.map +1 -0
  157. package/dist/commands/teamwork/teammate-adapters.d.ts +31 -0
  158. package/dist/commands/teamwork/teammate-adapters.d.ts.map +1 -0
  159. package/dist/commands/teamwork/teammate-adapters.js +96 -0
  160. package/dist/commands/teamwork/teammate-adapters.js.map +1 -0
  161. package/dist/commands/teamwork/teammate-config.d.ts +19 -0
  162. package/dist/commands/teamwork/teammate-config.d.ts.map +1 -0
  163. package/dist/commands/teamwork/teammate-config.js +97 -0
  164. package/dist/commands/teamwork/teammate-config.js.map +1 -0
  165. package/dist/commands/teamwork/teammate-launch.d.ts +58 -0
  166. package/dist/commands/teamwork/teammate-launch.d.ts.map +1 -0
  167. package/dist/commands/teamwork/teammate-launch.js +125 -0
  168. package/dist/commands/teamwork/teammate-launch.js.map +1 -0
  169. package/dist/commands/teamwork/text-body.d.ts +13 -0
  170. package/dist/commands/teamwork/text-body.d.ts.map +1 -0
  171. package/dist/commands/{channel → teamwork}/text-body.js +3 -3
  172. package/dist/commands/teamwork/text-body.js.map +1 -0
  173. package/dist/commands/teamwork/threads.d.ts +35 -0
  174. package/dist/commands/teamwork/threads.d.ts.map +1 -0
  175. package/dist/commands/{channel → teamwork}/threads.js +19 -19
  176. package/dist/commands/teamwork/threads.js.map +1 -0
  177. package/dist/commands/teamwork/title.d.ts +12 -0
  178. package/dist/commands/teamwork/title.d.ts.map +1 -0
  179. package/dist/commands/teamwork/title.js +24 -0
  180. package/dist/commands/teamwork/title.js.map +1 -0
  181. package/dist/commands/{channel → teamwork}/wait.d.ts +1 -5
  182. package/dist/commands/teamwork/wait.d.ts.map +1 -0
  183. package/dist/commands/{channel → teamwork}/wait.js +8 -21
  184. package/dist/commands/teamwork/wait.js.map +1 -0
  185. package/dist/commands/update.d.ts.map +1 -1
  186. package/dist/commands/update.js +93 -90
  187. package/dist/commands/update.js.map +1 -1
  188. package/dist/commands/workflow.d.ts +1 -1
  189. package/dist/commands/workflow.d.ts.map +1 -1
  190. package/dist/commands/workflow.js +9 -10
  191. package/dist/commands/workflow.js.map +1 -1
  192. package/dist/configurators/claude.d.ts +0 -17
  193. package/dist/configurators/claude.d.ts.map +1 -1
  194. package/dist/configurators/claude.js +2 -61
  195. package/dist/configurators/claude.js.map +1 -1
  196. package/dist/configurators/shared.d.ts.map +1 -1
  197. package/dist/configurators/shared.js +2 -2
  198. package/dist/configurators/shared.js.map +1 -1
  199. package/dist/configurators/snow.js +1 -1
  200. package/dist/configurators/snow.js.map +1 -1
  201. package/dist/configurators/workflow.d.ts +1 -1
  202. package/dist/configurators/workflow.js +5 -5
  203. package/dist/configurators/workflow.js.map +1 -1
  204. package/dist/constants/paths.d.ts +1 -1
  205. package/dist/constants/paths.d.ts.map +1 -1
  206. package/dist/constants/paths.js +1 -1
  207. package/dist/constants/paths.js.map +1 -1
  208. package/dist/migrations/manifests/0.7.0-beta.10.json +9 -0
  209. package/dist/migrations/manifests/0.7.0-beta.11.json +9 -0
  210. package/dist/migrations/manifests/0.7.0-beta.12.json +9 -0
  211. package/dist/migrations/manifests/0.7.0-beta.13.json +9 -0
  212. package/dist/migrations/manifests/0.7.0-beta.14.json +9 -0
  213. package/dist/migrations/manifests/0.7.0-beta.15.json +9 -0
  214. package/dist/migrations/manifests/0.7.0-beta.16.json +9 -0
  215. package/dist/migrations/manifests/0.7.0-beta.17.json +9 -0
  216. package/dist/migrations/manifests/0.7.0-beta.18.json +9 -0
  217. package/dist/migrations/manifests/0.7.0-beta.19.json +9 -0
  218. package/dist/migrations/manifests/0.7.0-beta.20.json +9 -0
  219. package/dist/migrations/manifests/0.7.0-beta.21.json +17 -0
  220. package/dist/migrations/manifests/0.7.0-beta.22.json +9 -0
  221. package/dist/migrations/manifests/0.7.0-beta.23.json +9 -0
  222. package/dist/migrations/manifests/0.7.0-beta.24.json +9 -0
  223. package/dist/migrations/manifests/0.7.0-beta.25.json +9 -0
  224. package/dist/migrations/manifests/0.7.0-beta.26.json +9 -0
  225. package/dist/migrations/manifests/0.7.0-beta.27.json +9 -0
  226. package/dist/migrations/manifests/0.7.0-beta.28.json +9 -0
  227. package/dist/migrations/manifests/0.7.0-beta.29.json +9 -0
  228. package/dist/migrations/manifests/0.7.0-beta.30.json +9 -0
  229. package/dist/migrations/manifests/0.7.0-beta.31.json +9 -0
  230. package/dist/migrations/manifests/0.7.0-beta.32.json +9 -0
  231. package/dist/migrations/manifests/0.7.0-beta.33.json +9 -0
  232. package/dist/migrations/manifests/0.7.0-beta.34.json +9 -0
  233. package/dist/migrations/manifests/0.7.0-beta.35.json +9 -0
  234. package/dist/migrations/manifests/0.7.0-beta.36.json +9 -0
  235. package/dist/migrations/manifests/0.7.0-beta.37.json +9 -0
  236. package/dist/migrations/manifests/0.7.0-beta.38.json +9 -0
  237. package/dist/migrations/manifests/0.7.0-beta.39.json +9 -0
  238. package/dist/migrations/manifests/0.7.0-beta.40.json +9 -0
  239. package/dist/migrations/manifests/0.7.0-beta.41.json +9 -0
  240. package/dist/migrations/manifests/0.7.0-beta.42.json +9 -0
  241. package/dist/migrations/manifests/0.7.0-beta.43.json +9 -0
  242. package/dist/migrations/manifests/0.7.0-beta.44.json +9 -0
  243. package/dist/migrations/manifests/0.7.0-beta.45.json +9 -0
  244. package/dist/migrations/manifests/0.7.0-beta.46.json +9 -0
  245. package/dist/migrations/manifests/0.7.0-beta.47.json +9 -0
  246. package/dist/migrations/manifests/0.7.0-beta.48.json +9 -0
  247. package/dist/migrations/manifests/0.7.0-beta.49.json +13 -0
  248. package/dist/migrations/manifests/0.7.0-beta.50.json +12 -0
  249. package/dist/migrations/manifests/0.7.0-beta.51.json +9 -0
  250. package/dist/migrations/manifests/0.7.0-beta.52.json +9 -0
  251. package/dist/migrations/manifests/0.7.0-beta.53.json +27 -0
  252. package/dist/migrations/manifests/0.7.0-beta.54.json +8 -0
  253. package/dist/migrations/manifests/0.7.1-beta.0.json +9 -0
  254. package/dist/migrations/manifests/0.7.1-beta.1.json +186 -0
  255. package/dist/migrations/manifests/0.7.1.json +9 -0
  256. package/dist/templates/claude/hooks/statusline.py +0 -7
  257. package/dist/templates/codex/hooks/session-start.py +31 -38
  258. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/SKILL.md +10 -0
  259. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +40 -0
  260. package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/SKILL.md +10 -0
  261. package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/workflow.md +55 -0
  262. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/SKILL.md +10 -0
  263. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +33 -0
  264. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +25 -25
  265. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
  266. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +13 -19
  267. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
  268. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +27 -75
  269. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +17 -56
  270. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  271. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +14 -11
  272. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -33
  273. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +32 -0
  274. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
  275. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +25 -111
  276. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +19 -60
  277. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
  278. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  279. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +1 -1
  280. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/SKILL.md +10 -0
  281. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +36 -0
  282. package/dist/templates/common/bundled-skills/trellis-task-implementation/SKILL.md +10 -0
  283. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +29 -0
  284. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +73 -0
  285. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +29 -0
  286. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +31 -0
  287. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +37 -0
  288. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +78 -0
  289. package/dist/templates/common/commands/continue.md +9 -49
  290. package/dist/templates/common/commands/finish-work.md +10 -58
  291. package/dist/templates/common/commands/start.md +19 -49
  292. package/dist/templates/common/skills/brainstorm.md +11 -199
  293. package/dist/templates/copilot/hooks/session-start.py +30 -26
  294. package/dist/templates/copilot/prompts/brainstorm.prompt.md +7 -160
  295. package/dist/templates/copilot/prompts/finish-work.prompt.md +11 -88
  296. package/dist/templates/copilot/prompts/record-session.prompt.md +4 -12
  297. package/dist/templates/copilot/prompts/start.prompt.md +12 -10
  298. package/dist/templates/dsh/DSH.md +2 -2
  299. package/dist/templates/extract.js +4 -0
  300. package/dist/templates/extract.js.map +1 -1
  301. package/dist/templates/kiro/agents/trellis.json +2 -2
  302. package/dist/templates/markdown/agents.md +14 -3
  303. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +9 -8
  304. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +1 -1
  305. package/dist/templates/markdown/workspace-index.md +0 -1
  306. package/dist/templates/omp/extensions/trellis/index.ts.txt +28 -17
  307. package/dist/templates/opencode/lib/session-utils.js +20 -25
  308. package/dist/templates/opencode/lib/trellis-context.js +4 -76
  309. package/dist/templates/opencode/plugins/inject-subagent-context.js +5 -4
  310. package/dist/templates/opencode/plugins/inject-workflow-state.js +14 -17
  311. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
  312. package/dist/templates/shared-hooks/inject-subagent-context.py +445 -67
  313. package/dist/templates/shared-hooks/inject-workflow-state.py +44 -36
  314. package/dist/templates/shared-hooks/session-start.py +21 -33
  315. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +18 -0
  316. package/dist/templates/trellis/agents/teammate-profiles.json +8 -0
  317. package/dist/templates/trellis/agents/teammate.md +63 -0
  318. package/dist/templates/trellis/config.yaml +29 -38
  319. package/dist/templates/trellis/gitignore.txt +1 -0
  320. package/dist/templates/trellis/index.d.ts +8 -12
  321. package/dist/templates/trellis/index.d.ts.map +1 -1
  322. package/dist/templates/trellis/index.js +14 -25
  323. package/dist/templates/trellis/index.js.map +1 -1
  324. package/dist/templates/trellis/scripts/add_session.py +1 -1
  325. package/dist/templates/trellis/scripts/common/__init__.py +0 -1
  326. package/dist/templates/trellis/scripts/common/active_task.py +136 -100
  327. package/dist/templates/trellis/scripts/common/config.py +7 -9
  328. package/dist/templates/trellis/scripts/common/git_context.py +1 -1
  329. package/dist/templates/trellis/scripts/common/io.py +19 -0
  330. package/dist/templates/trellis/scripts/common/paths.py +0 -1
  331. package/dist/templates/trellis/scripts/common/safe_commit.py +11 -90
  332. package/dist/templates/trellis/scripts/common/session_context.py +1 -8
  333. package/dist/templates/trellis/scripts/common/spec_match.py +1 -1
  334. package/dist/templates/trellis/scripts/common/task_context.py +152 -59
  335. package/dist/templates/trellis/scripts/common/task_continuity.py +598 -0
  336. package/dist/templates/trellis/scripts/common/task_store.py +70 -548
  337. package/dist/templates/trellis/scripts/common/task_utils.py +4 -98
  338. package/dist/templates/trellis/scripts/common/task_workflow.py +1096 -0
  339. package/dist/templates/trellis/scripts/common/tasks.py +5 -8
  340. package/dist/templates/trellis/scripts/common/workflow_phase.py +8 -7
  341. package/dist/templates/trellis/scripts/hooks/linear_sync.py +3 -3
  342. package/dist/templates/trellis/scripts/task.py +119 -377
  343. package/dist/templates/trellis/workflow.md +111 -706
  344. package/dist/utils/agent-refs.d.ts +5 -5
  345. package/dist/utils/agent-refs.js +6 -6
  346. package/dist/utils/file-writer.d.ts +3 -2
  347. package/dist/utils/file-writer.d.ts.map +1 -1
  348. package/dist/utils/file-writer.js +3 -2
  349. package/dist/utils/file-writer.js.map +1 -1
  350. package/dist/utils/managed-paths.d.ts +0 -7
  351. package/dist/utils/managed-paths.d.ts.map +1 -1
  352. package/dist/utils/managed-paths.js +0 -40
  353. package/dist/utils/managed-paths.js.map +1 -1
  354. package/dist/utils/manifest-prune.d.ts +2 -1
  355. package/dist/utils/manifest-prune.d.ts.map +1 -1
  356. package/dist/utils/manifest-prune.js +14 -16
  357. package/dist/utils/manifest-prune.js.map +1 -1
  358. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  359. package/dist/utils/uninstall-scrubbers.js +3 -3
  360. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  361. package/dist/utils/workflow-resolver.d.ts +10 -16
  362. package/dist/utils/workflow-resolver.d.ts.map +1 -1
  363. package/dist/utils/workflow-resolver.js +25 -36
  364. package/dist/utils/workflow-resolver.js.map +1 -1
  365. package/package.json +2 -2
  366. package/dist/commands/channel/adapters/claude.d.ts.map +0 -1
  367. package/dist/commands/channel/adapters/claude.js.map +0 -1
  368. package/dist/commands/channel/adapters/codex.d.ts.map +0 -1
  369. package/dist/commands/channel/adapters/codex.js.map +0 -1
  370. package/dist/commands/channel/adapters/index.d.ts.map +0 -1
  371. package/dist/commands/channel/adapters/index.js.map +0 -1
  372. package/dist/commands/channel/adapters/types.d.ts.map +0 -1
  373. package/dist/commands/channel/adapters/types.js.map +0 -1
  374. package/dist/commands/channel/agent-loader.d.ts.map +0 -1
  375. package/dist/commands/channel/agent-loader.js.map +0 -1
  376. package/dist/commands/channel/context-loader.d.ts.map +0 -1
  377. package/dist/commands/channel/context-loader.js.map +0 -1
  378. package/dist/commands/channel/context-trust.d.ts.map +0 -1
  379. package/dist/commands/channel/context-trust.js.map +0 -1
  380. package/dist/commands/channel/context.d.ts +0 -16
  381. package/dist/commands/channel/context.d.ts.map +0 -1
  382. package/dist/commands/channel/context.js.map +0 -1
  383. package/dist/commands/channel/create.d.ts +0 -29
  384. package/dist/commands/channel/create.d.ts.map +0 -1
  385. package/dist/commands/channel/create.js +0 -47
  386. package/dist/commands/channel/create.js.map +0 -1
  387. package/dist/commands/channel/dev-parse-trace.d.ts.map +0 -1
  388. package/dist/commands/channel/dev-parse-trace.js.map +0 -1
  389. package/dist/commands/channel/guard.d.ts.map +0 -1
  390. package/dist/commands/channel/guard.js.map +0 -1
  391. package/dist/commands/channel/index.d.ts +0 -3
  392. package/dist/commands/channel/index.d.ts.map +0 -1
  393. package/dist/commands/channel/index.js.map +0 -1
  394. package/dist/commands/channel/interrupt.d.ts.map +0 -1
  395. package/dist/commands/channel/interrupt.js +0 -22
  396. package/dist/commands/channel/interrupt.js.map +0 -1
  397. package/dist/commands/channel/kill.d.ts +0 -7
  398. package/dist/commands/channel/kill.d.ts.map +0 -1
  399. package/dist/commands/channel/kill.js.map +0 -1
  400. package/dist/commands/channel/list.d.ts +0 -18
  401. package/dist/commands/channel/list.d.ts.map +0 -1
  402. package/dist/commands/channel/list.js.map +0 -1
  403. package/dist/commands/channel/messages.d.ts.map +0 -1
  404. package/dist/commands/channel/messages.js.map +0 -1
  405. package/dist/commands/channel/rm.d.ts +0 -27
  406. package/dist/commands/channel/rm.d.ts.map +0 -1
  407. package/dist/commands/channel/rm.js.map +0 -1
  408. package/dist/commands/channel/run.d.ts +0 -32
  409. package/dist/commands/channel/run.js +0 -141
  410. package/dist/commands/channel/run.js.map +0 -1
  411. package/dist/commands/channel/send.d.ts.map +0 -1
  412. package/dist/commands/channel/send.js.map +0 -1
  413. package/dist/commands/channel/spawn.d.ts.map +0 -1
  414. package/dist/commands/channel/spawn.js.map +0 -1
  415. package/dist/commands/channel/store/events.d.ts +0 -22
  416. package/dist/commands/channel/store/events.d.ts.map +0 -1
  417. package/dist/commands/channel/store/events.js.map +0 -1
  418. package/dist/commands/channel/store/filter.d.ts +0 -3
  419. package/dist/commands/channel/store/filter.d.ts.map +0 -1
  420. package/dist/commands/channel/store/filter.js +0 -2
  421. package/dist/commands/channel/store/filter.js.map +0 -1
  422. package/dist/commands/channel/store/lock.d.ts.map +0 -1
  423. package/dist/commands/channel/store/lock.js.map +0 -1
  424. package/dist/commands/channel/store/paths.d.ts.map +0 -1
  425. package/dist/commands/channel/store/paths.js.map +0 -1
  426. package/dist/commands/channel/store/schema.d.ts +0 -27
  427. package/dist/commands/channel/store/schema.d.ts.map +0 -1
  428. package/dist/commands/channel/store/schema.js +0 -34
  429. package/dist/commands/channel/store/schema.js.map +0 -1
  430. package/dist/commands/channel/store/thread-state.d.ts.map +0 -1
  431. package/dist/commands/channel/store/thread-state.js.map +0 -1
  432. package/dist/commands/channel/store/watch.d.ts +0 -19
  433. package/dist/commands/channel/store/watch.d.ts.map +0 -1
  434. package/dist/commands/channel/store/watch.js.map +0 -1
  435. package/dist/commands/channel/supervisor/idle.d.ts.map +0 -1
  436. package/dist/commands/channel/supervisor/idle.js.map +0 -1
  437. package/dist/commands/channel/supervisor/inbox.d.ts.map +0 -1
  438. package/dist/commands/channel/supervisor/inbox.js.map +0 -1
  439. package/dist/commands/channel/supervisor/shutdown.d.ts.map +0 -1
  440. package/dist/commands/channel/supervisor/shutdown.js.map +0 -1
  441. package/dist/commands/channel/supervisor/stdout.d.ts.map +0 -1
  442. package/dist/commands/channel/supervisor/stdout.js.map +0 -1
  443. package/dist/commands/channel/supervisor/turns.d.ts.map +0 -1
  444. package/dist/commands/channel/supervisor/turns.js.map +0 -1
  445. package/dist/commands/channel/supervisor/warning.d.ts.map +0 -1
  446. package/dist/commands/channel/supervisor/warning.js.map +0 -1
  447. package/dist/commands/channel/supervisor.d.ts.map +0 -1
  448. package/dist/commands/channel/supervisor.js.map +0 -1
  449. package/dist/commands/channel/text-body.d.ts +0 -13
  450. package/dist/commands/channel/text-body.d.ts.map +0 -1
  451. package/dist/commands/channel/text-body.js.map +0 -1
  452. package/dist/commands/channel/threads.d.ts +0 -39
  453. package/dist/commands/channel/threads.d.ts.map +0 -1
  454. package/dist/commands/channel/threads.js.map +0 -1
  455. package/dist/commands/channel/title.d.ts +0 -12
  456. package/dist/commands/channel/title.d.ts.map +0 -1
  457. package/dist/commands/channel/title.js +0 -24
  458. package/dist/commands/channel/title.js.map +0 -1
  459. package/dist/commands/channel/wait.d.ts.map +0 -1
  460. package/dist/commands/channel/wait.js.map +0 -1
  461. package/dist/commands/channel/workers.d.ts +0 -9
  462. package/dist/commands/channel/workers.d.ts.map +0 -1
  463. package/dist/commands/channel/workers.js +0 -31
  464. package/dist/commands/channel/workers.js.map +0 -1
  465. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +0 -69
  466. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +0 -549
  467. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +0 -233
  468. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +0 -237
  469. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +0 -222
  470. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +0 -295
  471. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +0 -131
  472. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +0 -70
  473. package/dist/templates/common/bundled-skills/trellis-research-record/SKILL.md +0 -14
  474. package/dist/templates/copilot/prompts/parallel.prompt.md +0 -204
  475. package/dist/templates/trellis/agents/check.md +0 -70
  476. package/dist/templates/trellis/agents/implement.md +0 -71
  477. package/dist/templates/trellis/agents/subnode.env +0 -4
  478. package/dist/templates/trellis/agents/subnode.md +0 -76
  479. package/dist/templates/trellis/scripts/common/context_projection.py +0 -508
  480. package/dist/templates/trellis/scripts/common/continuation_record.py +0 -450
  481. package/dist/templates/trellis/scripts/common/ownership_record.py +0 -439
  482. package/dist/templates/trellis/scripts/subnode_artifact.py +0 -735
  483. package/dist/templates/trellis/scripts/workspace_note.py +0 -126
  484. /package/dist/commands/{channel → teamwork}/adapters/types.js +0 -0
  485. /package/dist/commands/{channel → teamwork}/supervisor/idle.d.ts +0 -0
  486. /package/dist/commands/{channel → teamwork}/supervisor/idle.js +0 -0
  487. /package/dist/commands/{channel → teamwork}/supervisor/turns.d.ts +0 -0
  488. /package/dist/commands/{channel → teamwork}/supervisor/turns.js +0 -0
@@ -0,0 +1,598 @@
1
+ """Current-task continuity snapshots and explicit session ownership transfer."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import contextlib
7
+ import hashlib
8
+ import json
9
+ import os
10
+ import re
11
+ from pathlib import Path
12
+
13
+ from .active_task import (
14
+ compare_and_set_active_task,
15
+ clear_active_task_if_matches,
16
+ resolve_active_task,
17
+ )
18
+ from .git import run_git
19
+ from .io import JSON_READ_MISSING, read_json_checked, write_json
20
+ from .paths import FILE_TASK_JSON, get_repo_root
21
+ from .task_utils import is_within_tasks_dir, resolve_task_dir
22
+ from .task_workflow import TaskWorkflowError, _artifact_digest, _state
23
+
24
+
25
+ PACKAGE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
26
+ PACKAGE_DIGEST_RE = re.compile(r"^[0-9a-f]{64}$")
27
+
28
+
29
+ class ContinuityError(Exception):
30
+ def __init__(self, code: str, message: str):
31
+ super().__init__(message)
32
+ self.code = code
33
+
34
+
35
+ def _key(task_path: str) -> str:
36
+ return hashlib.sha256(task_path.encode("utf-8")).hexdigest()
37
+
38
+
39
+ def _continuity_path(root: Path, task_path: str) -> Path:
40
+ return root / ".trellis" / ".runtime" / "continuity" / f"{_key(task_path)}.json"
41
+
42
+
43
+ def _ownership_path(root: Path, task_path: str) -> Path:
44
+ return root / ".trellis" / ".runtime" / "task-ownership" / f"{_key(task_path)}.json"
45
+
46
+
47
+ @contextlib.contextmanager
48
+ def _record_lock(path: Path):
49
+ lock_path = path.with_suffix(".lock")
50
+ lock_path.parent.mkdir(parents=True, exist_ok=True)
51
+ with lock_path.open("a+b") as handle:
52
+ if os.name == "nt":
53
+ import msvcrt
54
+
55
+ handle.seek(0)
56
+ if handle.read(1) == b"":
57
+ handle.write(b"\0")
58
+ handle.flush()
59
+ handle.seek(0)
60
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
61
+ try:
62
+ yield
63
+ finally:
64
+ handle.seek(0)
65
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
66
+ else:
67
+ import fcntl
68
+
69
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
70
+ try:
71
+ yield
72
+ finally:
73
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
74
+
75
+
76
+ def _task(root: Path, task_path: str | None = None) -> tuple[Path, dict, str, str]:
77
+ active = resolve_active_task(root)
78
+ if active.stale or active.source_type == "session-fallback":
79
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "current task is stale or not directly bound to this session")
80
+ if task_path is None:
81
+ if not active.task_path:
82
+ raise ContinuityError("TASK_CONTINUITY_ABSENT", "this direct session has no current task")
83
+ task_path = active.task_path
84
+ elif active.task_path != task_path:
85
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "requested task is not the direct current task")
86
+ directory = resolve_task_dir(task_path, root)
87
+ if directory is None or not is_within_tasks_dir(directory, root):
88
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task path is unavailable or unsafe")
89
+ data, reason = read_json_checked(directory / FILE_TASK_JSON)
90
+ if data is None:
91
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"task record cannot be read: {reason}")
92
+ task_id = data.get("id") or data.get("name")
93
+ if not isinstance(task_id, str) or not task_id.strip():
94
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task identity is missing")
95
+ if not active.context_key:
96
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "direct session identity is unavailable")
97
+ return directory, data, task_id, active.context_key
98
+
99
+
100
+ def _projection(root: Path, directory: Path, data: dict, task_id: str) -> dict:
101
+ meta = data.get("meta")
102
+ if not isinstance(meta, dict):
103
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "task metadata is invalid")
104
+ workflow = meta.get("task_workflow")
105
+ phases: list[dict] = []
106
+ evidence: list[str] = []
107
+ route_revision = input_revision = current_phase = None
108
+ if workflow is not None:
109
+ try:
110
+ state = _state(meta)
111
+ except TaskWorkflowError as error:
112
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", str(error)) from error
113
+ route_revision = state["task_route_revision"]
114
+ input_revision = state["input_revision"]
115
+ current_phase = state["current_phase_id"]
116
+ for phase_id in state["phase_ids"]:
117
+ record = state["phases"][phase_id]
118
+ refs = record.get("artifact_refs", [])
119
+ if not isinstance(refs, list) or not all(isinstance(ref, str) for ref in refs):
120
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"phase artifact references are invalid: {phase_id}")
121
+ try:
122
+ artifact_digest = _artifact_digest(directory, refs)
123
+ except TaskWorkflowError as error:
124
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", str(error)) from error
125
+ evidence_refs = record.get("evidence_refs", [])
126
+ if not isinstance(evidence_refs, list) or not all(isinstance(ref, str) for ref in evidence_refs):
127
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"phase evidence references are invalid: {phase_id}")
128
+ evidence.extend(evidence_refs)
129
+ acceptance = record.get("acceptance")
130
+ phases.append({
131
+ "id": phase_id,
132
+ "status": record.get("status"),
133
+ "input_revision": record.get("input_revision"),
134
+ "artifact_revision": record.get("artifact_revision"),
135
+ "artifact_digest": artifact_digest,
136
+ "result_revision": record.get("result_revision"),
137
+ "result_ref": record.get("result_ref"),
138
+ "outcome_kind": record.get("outcome_kind"),
139
+ "evidence_refs": record.get("evidence_refs", []),
140
+ "acceptance": acceptance,
141
+ })
142
+ rc, head, _ = run_git(["rev-parse", "HEAD"], cwd=root, timeout=5)
143
+ if rc:
144
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git HEAD is unavailable")
145
+ rc, branch, _ = run_git(["branch", "--show-current"], cwd=root, timeout=5)
146
+ if rc:
147
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git branch is unavailable")
148
+ rc, status, _ = run_git(["status", "--porcelain=v1", "--untracked-files=all"], cwd=root, timeout=10)
149
+ if rc:
150
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git working-tree status is unavailable")
151
+ status = "".join(
152
+ line for line in status.splitlines(keepends=True)
153
+ if ".trellis/session-handoffs/" not in line[3:]
154
+ )
155
+ rc, diff, _ = run_git(["diff", "--binary", "HEAD", "--", "."], cwd=root, timeout=20)
156
+ if rc:
157
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git working-tree diff is unavailable")
158
+ rc, untracked, _ = run_git(["ls-files", "--others", "--exclude-standard", "-z"], cwd=root, timeout=10)
159
+ if rc:
160
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", "Git untracked-file list is unavailable")
161
+ worktree = hashlib.sha256(diff.encode("utf-8", errors="surrogateescape"))
162
+ for raw_path in sorted(os.fsencode(path) for path in untracked.split("\0") if path):
163
+ relative = Path(os.fsdecode(raw_path))
164
+ if relative.parts[:2] == (".trellis", "session-handoffs"):
165
+ continue
166
+ candidate = root / relative
167
+ try:
168
+ candidate.resolve(strict=True).relative_to(root.resolve())
169
+ if candidate.is_symlink():
170
+ content = os.fsencode(os.readlink(candidate))
171
+ elif candidate.is_file():
172
+ content = candidate.read_bytes()
173
+ else:
174
+ raise OSError("not a regular file or symbolic link")
175
+ except (OSError, ValueError) as error:
176
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"untracked path is unavailable or unsafe: {relative}") from error
177
+ worktree.update(raw_path)
178
+ worktree.update(b"\0")
179
+ worktree.update(content)
180
+ worktree.update(b"\0")
181
+ evidence_digest = hashlib.sha256(
182
+ json.dumps(sorted(evidence), ensure_ascii=False, separators=(",", ":")).encode("utf-8")
183
+ ).hexdigest()
184
+ return {
185
+ "task": {"id": task_id, "path": directory.relative_to(root).as_posix(), "status": data.get("status")},
186
+ "phase": {"current_phase_id": current_phase, "task_route_revision": route_revision, "input_revision": input_revision, "phases": phases},
187
+ "evidence_digest": evidence_digest,
188
+ "git": {
189
+ "head": head.strip(),
190
+ "branch": branch.strip() or None,
191
+ "clean": not status,
192
+ "status_digest": hashlib.sha256(status.encode("utf-8")).hexdigest(),
193
+ "content_digest": worktree.hexdigest(),
194
+ },
195
+ }
196
+
197
+
198
+ def _digest(value: dict) -> str:
199
+ raw = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
200
+ return hashlib.sha256(raw.encode("utf-8")).hexdigest()
201
+
202
+
203
+ def _read_record(path: Path) -> tuple[dict | None, str | None]:
204
+ value, reason = read_json_checked(path)
205
+ if value is None and reason == JSON_READ_MISSING:
206
+ return None, None
207
+ if value is None:
208
+ raise ContinuityError("TASK_CONTINUITY_WITHHELD", f"runtime record cannot be read: {reason}")
209
+ return value, None
210
+
211
+
212
+ def continuity_show(args: argparse.Namespace) -> int:
213
+ root = get_repo_root()
214
+ try:
215
+ directory, data, task_id, _ = _task(root)
216
+ current = _projection(root, directory, data, task_id)
217
+ source_digest = _digest(current)
218
+ task_path = directory.relative_to(root).as_posix()
219
+ record, _ = _read_record(_continuity_path(root, task_path))
220
+ if record is None:
221
+ result = {"status": "absent", "source_digest": source_digest, "current": current}
222
+ elif record.get("schema_version") != 1 or not isinstance(record.get("generation"), int):
223
+ result = {"status": "withheld", "reason": "continuity record has an invalid shape"}
224
+ else:
225
+ result = {
226
+ "status": "ready" if record.get("source_digest") == source_digest else "stale",
227
+ "generation": record["generation"],
228
+ "source_digest": source_digest,
229
+ "captured_source_digest": record.get("source_digest"),
230
+ "current": current,
231
+ }
232
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
233
+ return 0
234
+ except ContinuityError as error:
235
+ status = "absent" if error.code == "TASK_CONTINUITY_ABSENT" else "withheld"
236
+ print(json.dumps({"status": status, "code": error.code, "reason": str(error)}, ensure_ascii=False))
237
+ return 0 if status == "absent" else 1
238
+
239
+
240
+ def continuity_capture(args: argparse.Namespace) -> int:
241
+ root = get_repo_root()
242
+ try:
243
+ directory, data, task_id, _ = _task(root)
244
+ current = _projection(root, directory, data, task_id)
245
+ source_digest = _digest(current)
246
+ task_path = directory.relative_to(root).as_posix()
247
+ path = _continuity_path(root, task_path)
248
+ with _record_lock(path):
249
+ record, _ = _read_record(path)
250
+ actual = record.get("source_digest") if record else "absent"
251
+ if actual != args.expected_source_digest:
252
+ raise ContinuityError("TASK_CONTINUITY_STALE", f"expected source digest {args.expected_source_digest}, current record is {actual}")
253
+ latest = _projection(root, directory, data, task_id)
254
+ if _digest(latest) != source_digest:
255
+ raise ContinuityError("TASK_CONTINUITY_STALE", "task or Git sources changed while capturing continuity")
256
+ generation = (record.get("generation", 0) + 1) if record else 1
257
+ saved = {"schema_version": 1, "generation": generation, "source_digest": source_digest, "current": current}
258
+ if not write_json(path, saved):
259
+ raise ContinuityError("TASK_CONTINUITY_WRITE_FAILED", "could not atomically write continuity record")
260
+ print(json.dumps({"status": "ready", "generation": generation, "source_digest": source_digest, "current": current}, ensure_ascii=False, sort_keys=True))
261
+ return 0
262
+ except ContinuityError as error:
263
+ print(json.dumps({"status": "stale" if error.code == "TASK_CONTINUITY_STALE" else "withheld", "code": error.code, "reason": str(error)}, ensure_ascii=False))
264
+ return 1
265
+
266
+
267
+ def _route_snapshot(data: dict) -> dict:
268
+ meta = data.get("meta")
269
+ if not isinstance(meta, dict):
270
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task metadata is invalid")
271
+ workflow = meta.get("task_workflow")
272
+ if workflow is None:
273
+ return {"current_phase_id": None, "task_route_revision": None, "input_revision": None, "implementation_authorization": None}
274
+ try:
275
+ state = _state(meta)
276
+ except TaskWorkflowError as error:
277
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", str(error)) from error
278
+ authorization = state.get("implementation_authorization")
279
+ return {
280
+ "current_phase_id": state["current_phase_id"],
281
+ "task_route_revision": state["task_route_revision"],
282
+ "input_revision": state["input_revision"],
283
+ "implementation_authorization": authorization,
284
+ }
285
+
286
+
287
+ def _direct_pointer(root: Path, task_path: str | None = None) -> tuple[str, str | None]:
288
+ active = resolve_active_task(root)
289
+ if not active.context_key or active.source_type == "session-fallback" or active.stale:
290
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "a direct, unambiguous session identity is required")
291
+ if task_path is not None and active.task_path != task_path:
292
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task is not current in this direct session")
293
+ return active.context_key, active.task_path
294
+
295
+
296
+ def _load_owner(path: Path) -> dict | None:
297
+ record, _ = _read_record(path)
298
+ if record is None:
299
+ return None
300
+ required = {"schema_version", "task_id", "task_path", "package_id", "package_digest", "generation", "status", "source_session_id", "target_session_id", "continuity_digest", "route"}
301
+ if not required.issubset(record) or set(record) - required - {"target_continuity_digest"} or record.get("schema_version") != 1 or not isinstance(record.get("generation"), int):
302
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "ownership record has an invalid shape")
303
+ if "target_continuity_digest" in record and (not isinstance(record["target_continuity_digest"], str) or not PACKAGE_DIGEST_RE.fullmatch(record["target_continuity_digest"])):
304
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "target continuity digest is invalid")
305
+ return record
306
+
307
+
308
+ def _current_projection_digest(root: Path, task_dir: Path, data: dict, task_id: str) -> str:
309
+ return _digest(_projection(root, task_dir, data, task_id))
310
+
311
+
312
+ def transfer_task_ownership(args: argparse.Namespace) -> int:
313
+ root = get_repo_root()
314
+ operation = args.transfer_action
315
+ try:
316
+ task_dir = resolve_task_dir(args.task_path, root)
317
+ if task_dir is None or not is_within_tasks_dir(task_dir, root):
318
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task path is unavailable or unsafe")
319
+ task_path = task_dir.relative_to(root).as_posix()
320
+ data, reason = read_json_checked(task_dir / FILE_TASK_JSON)
321
+ if data is None:
322
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", f"task record cannot be read: {reason}")
323
+ task_id = data.get("id") or data.get("name")
324
+ if not isinstance(task_id, str) or task_id != args.task_id:
325
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task identity does not match the requested task")
326
+ if operation != "status" and data.get("status") in {"completed", "cancelled"}:
327
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "terminal tasks cannot change ownership")
328
+ path = _ownership_path(root, task_path)
329
+ with _record_lock(path):
330
+ record = _load_owner(path)
331
+ if operation == "status":
332
+ if record and (record["package_id"] != args.package_id or record["package_digest"] != args.package_digest):
333
+ raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
334
+ result = record or {"status": "absent", "generation": 0, "task_id": task_id}
335
+ if args.include_current:
336
+ current = _projection(root, task_dir, data, task_id)
337
+ result = {**result, "current_continuity_digest": _digest(current), "current_projection": current}
338
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
339
+ return 0
340
+ if not args.explicit_user_request:
341
+ raise ContinuityError("TASK_TRANSFER_NOT_AUTHORIZED", "ownership changes require an explicit user request")
342
+ if args.expected_generation is None or args.expected_generation < 0:
343
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "a non-negative expected generation is required")
344
+ if operation == "quiesce":
345
+ if not PACKAGE_ID_RE.fullmatch(args.package_id) or not PACKAGE_DIGEST_RE.fullmatch(args.package_digest):
346
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "session package identity or SHA-256 digest is invalid")
347
+ context_key, current_path = _direct_pointer(root, task_path)
348
+ if current_path is None:
349
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session has no current task")
350
+ continuity_digest = _current_projection_digest(root, task_dir, data, task_id)
351
+ if args.expected_continuity_digest is not None:
352
+ if not PACKAGE_DIGEST_RE.fullmatch(args.expected_continuity_digest):
353
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "expected continuity digest must be a SHA-256 digest")
354
+ if continuity_digest != args.expected_continuity_digest:
355
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "current task projection differs from the package snapshot")
356
+ if record is not None:
357
+ if record["status"] == "quiescing" and record["generation"] == args.expected_generation + 1 and record["source_session_id"] == context_key and record["package_id"] == args.package_id and record["package_digest"] == args.package_digest:
358
+ if record["continuity_digest"] != continuity_digest:
359
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
360
+ result = record
361
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
362
+ return 0
363
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "task already has an ownership record")
364
+ if args.expected_generation != 0:
365
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "initial ownership generation must be 0")
366
+ record = {
367
+ "schema_version": 1, "task_id": task_id, "task_path": task_path,
368
+ "package_id": args.package_id, "package_digest": args.package_digest,
369
+ "generation": 1, "status": "quiescing", "source_session_id": context_key,
370
+ "target_session_id": None, "continuity_digest": continuity_digest,
371
+ "route": _route_snapshot(data),
372
+ }
373
+ else:
374
+ if record is None:
375
+ raise ContinuityError("TASK_TRANSFER_ABSENT", "ownership transfer has not started")
376
+ if args.package_id != record["package_id"] or args.package_digest != record["package_digest"]:
377
+ raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
378
+ if args.expected_generation != record["generation"]:
379
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", f"expected generation {args.expected_generation}, current generation is {record['generation']}")
380
+ current_digest = (
381
+ _current_projection_digest(root, task_dir, data, task_id)
382
+ if operation != "release" else record["continuity_digest"]
383
+ )
384
+ if args.expected_continuity_digest is not None:
385
+ if not PACKAGE_DIGEST_RE.fullmatch(args.expected_continuity_digest):
386
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "expected continuity digest must be a SHA-256 digest")
387
+ if args.expected_continuity_digest != current_digest:
388
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "current task projection differs from the explicitly reconciled snapshot")
389
+ # Recovery acknowledges a live target snapshot without rebasing the source package.
390
+ reconciled_claim = (
391
+ operation == "claim" and record["status"] == "sealed"
392
+ and args.expected_continuity_digest == current_digest
393
+ )
394
+ reconciled_target = (
395
+ operation == "reconcile" and record["status"] == "consumed"
396
+ and args.expected_continuity_digest == current_digest
397
+ )
398
+ if operation != "release" and current_digest != record.get("target_continuity_digest", record["continuity_digest"]) and not (reconciled_claim or reconciled_target):
399
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
400
+ if operation != "release" and _route_snapshot(data) != record["route"]:
401
+ raise ContinuityError("TASK_TRANSFER_STALE_ROUTE", "task phase, route, input, or implementation authorization changed")
402
+ context_key, current_path = _direct_pointer(root)
403
+ if operation in {"seal", "retire"}:
404
+ if context_key != record["source_session_id"] or current_path not in {record["task_path"], None}:
405
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session no longer owns the current task")
406
+ elif operation in {"claim", "consume", "reconcile"}:
407
+ if context_key == record["source_session_id"]:
408
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "target must be a different direct session")
409
+ if operation == "reconcile":
410
+ if not reconciled_target or context_key != record["target_session_id"] or current_path != record["task_path"]:
411
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the consumed target may reconcile its explicitly pinned projection")
412
+ elif operation == "release":
413
+ if record["status"] not in {"quiescing", "sealed", "recovery_required"} or record["target_session_id"] is not None:
414
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "ownership can be released only before target claim")
415
+ if context_key != record["source_session_id"] or current_path not in {None, record["task_path"]}:
416
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the source session can release this transfer")
417
+ if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
418
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer could not be restored")
419
+ active = resolve_active_task(root)
420
+ if active.task_path != record["task_path"] or active.context_key != context_key:
421
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer restoration could not be verified")
422
+ path.unlink(missing_ok=True)
423
+ print(json.dumps({"status": "released", "generation": record["generation"], "task_id": task_id}, ensure_ascii=False, sort_keys=True))
424
+ return 0
425
+ if operation == "reconcile":
426
+ record.update({"target_continuity_digest": current_digest, "generation": record["generation"] + 1})
427
+ elif operation == "seal" and record["status"] == "quiescing":
428
+ record.update({"status": "sealed", "generation": record["generation"] + 1})
429
+ elif operation == "retire" and record["status"] in {"sealed", "retiring", "recovery_required"}:
430
+ if record["status"] == "sealed":
431
+ record.update({"status": "retiring", "generation": record["generation"] + 1})
432
+ if not write_json(path, record):
433
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record source retirement")
434
+ if current_path is not None and not compare_and_set_active_task(root, context_key, record["task_path"], None):
435
+ record["status"] = "recovery_required"
436
+ write_json(path, record)
437
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer changed during retirement")
438
+ if resolve_active_task(root).task_path is not None:
439
+ record["status"] = "recovery_required"
440
+ write_json(path, record)
441
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer remains after retirement")
442
+ record["status"] = "ready"
443
+ elif operation == "claim" and (record["status"] in {"ready", "claiming", "recovery_required", "claimed"} or reconciled_claim):
444
+ if record["status"] in {"claiming", "recovery_required"} and record["target_session_id"] != context_key:
445
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the recorded target session can recover a claim")
446
+ if current_path not in {None, record["task_path"]}:
447
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "target session already has a current task")
448
+ if record["status"] in {"ready", "sealed"}:
449
+ record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
450
+ if reconciled_claim:
451
+ record["target_continuity_digest"] = current_digest
452
+ if not write_json(path, record):
453
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target claim")
454
+ elif record["status"] == "claimed":
455
+ previous_target = record["target_session_id"]
456
+ if previous_target == context_key:
457
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "the current target must consume ownership instead of claiming it again")
458
+ if previous_target is not None and not clear_active_task_if_matches(root, previous_target, record["task_path"]):
459
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "previous target pointer could not be cleared")
460
+ record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
461
+ if not write_json(path, record):
462
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target takeover")
463
+ if not clear_active_task_if_matches(root, record["source_session_id"], record["task_path"]):
464
+ record["status"] = "recovery_required"
465
+ write_json(path, record)
466
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer could not be retired during claim")
467
+ if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
468
+ record["status"] = "recovery_required"
469
+ write_json(path, record)
470
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer could not be claimed")
471
+ active = resolve_active_task(root)
472
+ if active.task_path != record["task_path"] or active.context_key != context_key:
473
+ record["status"] = "recovery_required"
474
+ write_json(path, record)
475
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer verification failed")
476
+ record["status"] = "claimed"
477
+ elif operation == "consume" and record["status"] == "claimed":
478
+ if context_key != record["target_session_id"] or current_path != record["task_path"]:
479
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the direct target session can consume the task")
480
+ record.update({"status": "consumed", "generation": record["generation"] + 1})
481
+ else:
482
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", f"cannot apply {operation} while ownership is {record['status']}")
483
+ if not write_json(path, record):
484
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not atomically update ownership record")
485
+ print(json.dumps(record, ensure_ascii=False, sort_keys=True))
486
+ return 0
487
+ except ContinuityError as error:
488
+ print(json.dumps({"status": "withheld", "code": error.code, "reason": str(error)}, ensure_ascii=False))
489
+ return 1
490
+
491
+
492
+ def add_continuity_commands(subparsers) -> None:
493
+ continuity = subparsers.add_parser("continuity", help="Read or capture current task continuity")
494
+ continuity_sub = continuity.add_subparsers(dest="continuity_action", required=True)
495
+ continuity_sub.add_parser("show", help="Recompute current continuity status")
496
+ capture = continuity_sub.add_parser("capture", help="Capture the current task projection with compare-and-set")
497
+ capture.add_argument("--expected-source-digest", required=True, help="Current captured digest, or absent")
498
+
499
+ transfer = subparsers.add_parser("transfer", help="Transfer task ownership between direct sessions")
500
+ transfer.add_argument("transfer_action", choices=("status", "quiesce", "seal", "retire", "claim", "consume", "release", "reconcile"))
501
+ transfer.add_argument("--task-id", required=True)
502
+ transfer.add_argument("--task-path", required=True)
503
+ transfer.add_argument("--package-id", required=True)
504
+ transfer.add_argument("--package-digest", required=True)
505
+ transfer.add_argument("--expected-continuity-digest", help="Expected live projection; required for sealed-source claim or consumed-target reconciliation")
506
+ transfer.add_argument("--expected-generation", type=int)
507
+ transfer.add_argument("--include-current", action="store_true", help="Include the live task projection and its digest in transfer status")
508
+ transfer.add_argument("--explicit-user-request", action="store_true")
509
+
510
+
511
+ def clear_task_runtime_records(repo_root: Path, task_path: str) -> bool:
512
+ """Remove only this task's current continuity and ownership projections."""
513
+ for path in (_continuity_path(repo_root, task_path), _ownership_path(repo_root, task_path)):
514
+ if not path.exists():
515
+ continue
516
+ with _record_lock(path):
517
+ try:
518
+ path.unlink(missing_ok=True)
519
+ except OSError:
520
+ return False
521
+ return True
522
+
523
+
524
+ @contextlib.contextmanager
525
+ def active_task_change_guard(repo_root: Path, context_key: str, task_path: str, *, clearing: bool):
526
+ """Serialize pointer writes with ownership changes for the same task."""
527
+ directory = resolve_task_dir(task_path, repo_root)
528
+ if directory is None:
529
+ yield False
530
+ return
531
+ task_path = directory.relative_to(repo_root).as_posix()
532
+ path = _ownership_path(repo_root, task_path)
533
+ with _record_lock(path):
534
+ try:
535
+ record = _load_owner(path)
536
+ except ContinuityError:
537
+ yield False
538
+ return
539
+ if record is None:
540
+ yield True
541
+ return
542
+ data, _ = read_json_checked(directory / FILE_TASK_JSON)
543
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
544
+ if task_id != record["task_id"] or record["task_path"] != task_path:
545
+ yield False
546
+ return
547
+ if clearing:
548
+ allowed = record["status"] == "consumed" and record["target_session_id"] == context_key
549
+ elif record["status"] == "ready":
550
+ allowed = context_key != record["source_session_id"] and task_path == record["task_path"]
551
+ elif record["status"] == "consumed":
552
+ allowed = context_key == record["target_session_id"] and task_path == record["task_path"]
553
+ else:
554
+ allowed = record["status"] in {"claiming", "claimed"} and context_key == record["target_session_id"] and task_path == record["task_path"]
555
+ yield allowed
556
+
557
+
558
+ def assert_task_mutation_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
559
+ """Reject task writes until ownership is consumed or the source releases it."""
560
+ with task_mutation_guard(repo_root, task_dir, data) as allowed:
561
+ if not allowed:
562
+ raise TaskWorkflowError("TASK_TRANSFER_ACTIVE", "task changes are fenced until the target session consumes ownership or the source releases the transfer")
563
+
564
+
565
+ def assert_task_identity_change_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
566
+ """Keep an active ownership record addressable while the task ID is fixed."""
567
+ task_path = task_dir.relative_to(repo_root).as_posix()
568
+ path = _ownership_path(repo_root, task_path)
569
+ with _record_lock(path):
570
+ try:
571
+ record = _load_owner(path)
572
+ except ContinuityError as error:
573
+ raise TaskWorkflowError(error.code, str(error)) from error
574
+ if record is not None:
575
+ raise TaskWorkflowError("TASK_TRANSFER_TASK_ID_LOCKED", "task ID cannot change while its ownership record is active")
576
+
577
+
578
+ @contextlib.contextmanager
579
+ def task_mutation_guard(repo_root: Path, task_dir: Path, data: dict):
580
+ """Hold the ownership lock across a single task mutation."""
581
+ task_path = task_dir.relative_to(repo_root).as_posix()
582
+ path = _ownership_path(repo_root, task_path)
583
+ with _record_lock(path):
584
+ try:
585
+ record = _load_owner(path)
586
+ except ContinuityError:
587
+ yield False
588
+ return
589
+ if record is None:
590
+ yield True
591
+ return
592
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
593
+ if task_id != record["task_id"] or record["task_path"] != task_path:
594
+ yield False
595
+ return
596
+ active = resolve_active_task(repo_root)
597
+ allowed = record["status"] == "consumed" and active.context_key == record["target_session_id"] and active.task_path == task_path
598
+ yield allowed