@pennixrv/trellis 0.7.0-beta.9 → 0.7.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 (483) 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 +48 -89
  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.json +9 -0
  254. package/dist/templates/claude/hooks/statusline.py +0 -7
  255. package/dist/templates/codex/hooks/session-start.py +31 -38
  256. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/SKILL.md +10 -0
  257. package/dist/templates/common/bundled-skills/trellis-decision-space-construction-and-merging/workflow.md +40 -0
  258. package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/SKILL.md +10 -0
  259. package/dist/templates/common/bundled-skills/trellis-delivery-review-and-closeout/workflow.md +55 -0
  260. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/SKILL.md +10 -0
  261. package/dist/templates/common/bundled-skills/trellis-goal-exploration-and-requirements/workflow.md +33 -0
  262. package/dist/templates/common/bundled-skills/trellis-meta/SKILL.md +25 -25
  263. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-agents.md +1 -1
  264. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-skills-or-commands.md +13 -19
  265. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-spec-structure.md +1 -1
  266. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-task-lifecycle.md +27 -75
  267. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/change-workflow.md +17 -56
  268. package/dist/templates/common/bundled-skills/trellis-meta/references/customize-local/overview.md +1 -1
  269. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/bundled-skills.md +14 -11
  270. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/generated-files.md +0 -33
  271. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-teamwork.md +32 -0
  272. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/spec-system.md +1 -1
  273. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/task-system.md +25 -111
  274. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/workflow.md +19 -60
  275. package/dist/templates/common/bundled-skills/trellis-meta/references/platform-files/skills-and-commands.md +1 -1
  276. package/dist/templates/common/bundled-skills/trellis-session-insight/SKILL.md +1 -1
  277. package/dist/templates/common/bundled-skills/trellis-session-insight/references/cli-quick-reference.md +1 -1
  278. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/SKILL.md +10 -0
  279. package/dist/templates/common/bundled-skills/trellis-solution-design-and-convergence/workflow.md +36 -0
  280. package/dist/templates/common/bundled-skills/trellis-task-implementation/SKILL.md +10 -0
  281. package/dist/templates/common/bundled-skills/trellis-task-implementation/workflow.md +29 -0
  282. package/dist/templates/common/bundled-skills/trellis-teamwork/SKILL.md +73 -0
  283. package/dist/templates/common/bundled-skills/trellis-teamwork/references/coordination-modes.md +29 -0
  284. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-configuration-matrix.md +31 -0
  285. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teammate-processes.md +37 -0
  286. package/dist/templates/common/bundled-skills/trellis-teamwork/references/teamwork-plan-construction.md +78 -0
  287. package/dist/templates/common/commands/continue.md +9 -49
  288. package/dist/templates/common/commands/finish-work.md +10 -58
  289. package/dist/templates/common/commands/start.md +19 -49
  290. package/dist/templates/common/skills/brainstorm.md +11 -199
  291. package/dist/templates/copilot/hooks/session-start.py +30 -26
  292. package/dist/templates/copilot/prompts/brainstorm.prompt.md +7 -160
  293. package/dist/templates/copilot/prompts/finish-work.prompt.md +11 -88
  294. package/dist/templates/copilot/prompts/record-session.prompt.md +4 -12
  295. package/dist/templates/copilot/prompts/start.prompt.md +12 -10
  296. package/dist/templates/dsh/DSH.md +2 -2
  297. package/dist/templates/kiro/agents/trellis.json +2 -2
  298. package/dist/templates/markdown/agents.md +14 -3
  299. package/dist/templates/markdown/spec/guides/cross-layer-thinking-guide.md.txt +9 -8
  300. package/dist/templates/markdown/spec/guides/cross-platform-thinking-guide.md.txt +1 -1
  301. package/dist/templates/markdown/workspace-index.md +0 -1
  302. package/dist/templates/omp/extensions/trellis/index.ts.txt +28 -17
  303. package/dist/templates/opencode/lib/session-utils.js +20 -25
  304. package/dist/templates/opencode/lib/trellis-context.js +4 -76
  305. package/dist/templates/opencode/plugins/inject-subagent-context.js +5 -4
  306. package/dist/templates/opencode/plugins/inject-workflow-state.js +14 -17
  307. package/dist/templates/shared-hooks/inject-shell-session-context.py +3 -3
  308. package/dist/templates/shared-hooks/inject-subagent-context.py +445 -67
  309. package/dist/templates/shared-hooks/inject-workflow-state.py +44 -36
  310. package/dist/templates/shared-hooks/session-start.py +21 -33
  311. package/dist/templates/trellis/agents/delegated-worktree-primary-agent.md +18 -0
  312. package/dist/templates/trellis/agents/teammate-profiles.json +8 -0
  313. package/dist/templates/trellis/agents/teammate.md +63 -0
  314. package/dist/templates/trellis/config.yaml +29 -38
  315. package/dist/templates/trellis/gitignore.txt +1 -0
  316. package/dist/templates/trellis/index.d.ts +8 -12
  317. package/dist/templates/trellis/index.d.ts.map +1 -1
  318. package/dist/templates/trellis/index.js +14 -25
  319. package/dist/templates/trellis/index.js.map +1 -1
  320. package/dist/templates/trellis/scripts/add_session.py +1 -1
  321. package/dist/templates/trellis/scripts/common/__init__.py +0 -1
  322. package/dist/templates/trellis/scripts/common/active_task.py +136 -100
  323. package/dist/templates/trellis/scripts/common/config.py +7 -9
  324. package/dist/templates/trellis/scripts/common/git_context.py +1 -1
  325. package/dist/templates/trellis/scripts/common/io.py +19 -0
  326. package/dist/templates/trellis/scripts/common/paths.py +0 -1
  327. package/dist/templates/trellis/scripts/common/safe_commit.py +11 -90
  328. package/dist/templates/trellis/scripts/common/session_context.py +1 -8
  329. package/dist/templates/trellis/scripts/common/spec_match.py +1 -1
  330. package/dist/templates/trellis/scripts/common/task_context.py +152 -59
  331. package/dist/templates/trellis/scripts/common/task_continuity.py +563 -0
  332. package/dist/templates/trellis/scripts/common/task_store.py +70 -548
  333. package/dist/templates/trellis/scripts/common/task_utils.py +4 -98
  334. package/dist/templates/trellis/scripts/common/task_workflow.py +1096 -0
  335. package/dist/templates/trellis/scripts/common/tasks.py +5 -8
  336. package/dist/templates/trellis/scripts/common/workflow_phase.py +8 -7
  337. package/dist/templates/trellis/scripts/hooks/linear_sync.py +3 -3
  338. package/dist/templates/trellis/scripts/task.py +114 -377
  339. package/dist/templates/trellis/workflow.md +111 -706
  340. package/dist/utils/agent-refs.d.ts +5 -5
  341. package/dist/utils/agent-refs.js +6 -6
  342. package/dist/utils/file-writer.d.ts +3 -2
  343. package/dist/utils/file-writer.d.ts.map +1 -1
  344. package/dist/utils/file-writer.js +3 -2
  345. package/dist/utils/file-writer.js.map +1 -1
  346. package/dist/utils/managed-paths.d.ts +0 -7
  347. package/dist/utils/managed-paths.d.ts.map +1 -1
  348. package/dist/utils/managed-paths.js +0 -40
  349. package/dist/utils/managed-paths.js.map +1 -1
  350. package/dist/utils/manifest-prune.d.ts.map +1 -1
  351. package/dist/utils/manifest-prune.js +2 -11
  352. package/dist/utils/manifest-prune.js.map +1 -1
  353. package/dist/utils/uninstall-scrubbers.d.ts.map +1 -1
  354. package/dist/utils/uninstall-scrubbers.js +3 -3
  355. package/dist/utils/uninstall-scrubbers.js.map +1 -1
  356. package/dist/utils/workflow-resolver.d.ts +10 -16
  357. package/dist/utils/workflow-resolver.d.ts.map +1 -1
  358. package/dist/utils/workflow-resolver.js +25 -36
  359. package/dist/utils/workflow-resolver.js.map +1 -1
  360. package/package.json +2 -2
  361. package/dist/commands/channel/adapters/claude.d.ts.map +0 -1
  362. package/dist/commands/channel/adapters/claude.js.map +0 -1
  363. package/dist/commands/channel/adapters/codex.d.ts.map +0 -1
  364. package/dist/commands/channel/adapters/codex.js.map +0 -1
  365. package/dist/commands/channel/adapters/index.d.ts.map +0 -1
  366. package/dist/commands/channel/adapters/index.js.map +0 -1
  367. package/dist/commands/channel/adapters/types.d.ts.map +0 -1
  368. package/dist/commands/channel/adapters/types.js.map +0 -1
  369. package/dist/commands/channel/agent-loader.d.ts.map +0 -1
  370. package/dist/commands/channel/agent-loader.js.map +0 -1
  371. package/dist/commands/channel/context-loader.d.ts.map +0 -1
  372. package/dist/commands/channel/context-loader.js.map +0 -1
  373. package/dist/commands/channel/context-trust.d.ts.map +0 -1
  374. package/dist/commands/channel/context-trust.js.map +0 -1
  375. package/dist/commands/channel/context.d.ts +0 -16
  376. package/dist/commands/channel/context.d.ts.map +0 -1
  377. package/dist/commands/channel/context.js.map +0 -1
  378. package/dist/commands/channel/create.d.ts +0 -29
  379. package/dist/commands/channel/create.d.ts.map +0 -1
  380. package/dist/commands/channel/create.js +0 -47
  381. package/dist/commands/channel/create.js.map +0 -1
  382. package/dist/commands/channel/dev-parse-trace.d.ts.map +0 -1
  383. package/dist/commands/channel/dev-parse-trace.js.map +0 -1
  384. package/dist/commands/channel/guard.d.ts.map +0 -1
  385. package/dist/commands/channel/guard.js.map +0 -1
  386. package/dist/commands/channel/index.d.ts +0 -3
  387. package/dist/commands/channel/index.d.ts.map +0 -1
  388. package/dist/commands/channel/index.js.map +0 -1
  389. package/dist/commands/channel/interrupt.d.ts.map +0 -1
  390. package/dist/commands/channel/interrupt.js +0 -22
  391. package/dist/commands/channel/interrupt.js.map +0 -1
  392. package/dist/commands/channel/kill.d.ts +0 -7
  393. package/dist/commands/channel/kill.d.ts.map +0 -1
  394. package/dist/commands/channel/kill.js.map +0 -1
  395. package/dist/commands/channel/list.d.ts +0 -18
  396. package/dist/commands/channel/list.d.ts.map +0 -1
  397. package/dist/commands/channel/list.js.map +0 -1
  398. package/dist/commands/channel/messages.d.ts.map +0 -1
  399. package/dist/commands/channel/messages.js.map +0 -1
  400. package/dist/commands/channel/rm.d.ts +0 -27
  401. package/dist/commands/channel/rm.d.ts.map +0 -1
  402. package/dist/commands/channel/rm.js.map +0 -1
  403. package/dist/commands/channel/run.d.ts +0 -32
  404. package/dist/commands/channel/run.js +0 -141
  405. package/dist/commands/channel/run.js.map +0 -1
  406. package/dist/commands/channel/send.d.ts.map +0 -1
  407. package/dist/commands/channel/send.js.map +0 -1
  408. package/dist/commands/channel/spawn.d.ts.map +0 -1
  409. package/dist/commands/channel/spawn.js.map +0 -1
  410. package/dist/commands/channel/store/events.d.ts +0 -22
  411. package/dist/commands/channel/store/events.d.ts.map +0 -1
  412. package/dist/commands/channel/store/events.js.map +0 -1
  413. package/dist/commands/channel/store/filter.d.ts +0 -3
  414. package/dist/commands/channel/store/filter.d.ts.map +0 -1
  415. package/dist/commands/channel/store/filter.js +0 -2
  416. package/dist/commands/channel/store/filter.js.map +0 -1
  417. package/dist/commands/channel/store/lock.d.ts.map +0 -1
  418. package/dist/commands/channel/store/lock.js.map +0 -1
  419. package/dist/commands/channel/store/paths.d.ts.map +0 -1
  420. package/dist/commands/channel/store/paths.js.map +0 -1
  421. package/dist/commands/channel/store/schema.d.ts +0 -27
  422. package/dist/commands/channel/store/schema.d.ts.map +0 -1
  423. package/dist/commands/channel/store/schema.js +0 -34
  424. package/dist/commands/channel/store/schema.js.map +0 -1
  425. package/dist/commands/channel/store/thread-state.d.ts.map +0 -1
  426. package/dist/commands/channel/store/thread-state.js.map +0 -1
  427. package/dist/commands/channel/store/watch.d.ts +0 -19
  428. package/dist/commands/channel/store/watch.d.ts.map +0 -1
  429. package/dist/commands/channel/store/watch.js.map +0 -1
  430. package/dist/commands/channel/supervisor/idle.d.ts.map +0 -1
  431. package/dist/commands/channel/supervisor/idle.js.map +0 -1
  432. package/dist/commands/channel/supervisor/inbox.d.ts.map +0 -1
  433. package/dist/commands/channel/supervisor/inbox.js.map +0 -1
  434. package/dist/commands/channel/supervisor/shutdown.d.ts.map +0 -1
  435. package/dist/commands/channel/supervisor/shutdown.js.map +0 -1
  436. package/dist/commands/channel/supervisor/stdout.d.ts.map +0 -1
  437. package/dist/commands/channel/supervisor/stdout.js.map +0 -1
  438. package/dist/commands/channel/supervisor/turns.d.ts.map +0 -1
  439. package/dist/commands/channel/supervisor/turns.js.map +0 -1
  440. package/dist/commands/channel/supervisor/warning.d.ts.map +0 -1
  441. package/dist/commands/channel/supervisor/warning.js.map +0 -1
  442. package/dist/commands/channel/supervisor.d.ts.map +0 -1
  443. package/dist/commands/channel/supervisor.js.map +0 -1
  444. package/dist/commands/channel/text-body.d.ts +0 -13
  445. package/dist/commands/channel/text-body.d.ts.map +0 -1
  446. package/dist/commands/channel/text-body.js.map +0 -1
  447. package/dist/commands/channel/threads.d.ts +0 -39
  448. package/dist/commands/channel/threads.d.ts.map +0 -1
  449. package/dist/commands/channel/threads.js.map +0 -1
  450. package/dist/commands/channel/title.d.ts +0 -12
  451. package/dist/commands/channel/title.d.ts.map +0 -1
  452. package/dist/commands/channel/title.js +0 -24
  453. package/dist/commands/channel/title.js.map +0 -1
  454. package/dist/commands/channel/wait.d.ts.map +0 -1
  455. package/dist/commands/channel/wait.js.map +0 -1
  456. package/dist/commands/channel/workers.d.ts +0 -9
  457. package/dist/commands/channel/workers.d.ts.map +0 -1
  458. package/dist/commands/channel/workers.js +0 -31
  459. package/dist/commands/channel/workers.js.map +0 -1
  460. package/dist/templates/common/bundled-skills/trellis-channel/SKILL.md +0 -69
  461. package/dist/templates/common/bundled-skills/trellis-channel/references/command-reference.md +0 -549
  462. package/dist/templates/common/bundled-skills/trellis-channel/references/forum.md +0 -233
  463. package/dist/templates/common/bundled-skills/trellis-channel/references/progress-debugging.md +0 -237
  464. package/dist/templates/common/bundled-skills/trellis-channel/references/subnode-work.md +0 -222
  465. package/dist/templates/common/bundled-skills/trellis-channel/references/workers.md +0 -295
  466. package/dist/templates/common/bundled-skills/trellis-channel/references/workflows.md +0 -131
  467. package/dist/templates/common/bundled-skills/trellis-meta/references/local-architecture/multi-agent-channel.md +0 -70
  468. package/dist/templates/common/bundled-skills/trellis-research-record/SKILL.md +0 -14
  469. package/dist/templates/copilot/prompts/parallel.prompt.md +0 -204
  470. package/dist/templates/trellis/agents/check.md +0 -70
  471. package/dist/templates/trellis/agents/implement.md +0 -71
  472. package/dist/templates/trellis/agents/subnode.env +0 -4
  473. package/dist/templates/trellis/agents/subnode.md +0 -76
  474. package/dist/templates/trellis/scripts/common/context_projection.py +0 -508
  475. package/dist/templates/trellis/scripts/common/continuation_record.py +0 -450
  476. package/dist/templates/trellis/scripts/common/ownership_record.py +0 -439
  477. package/dist/templates/trellis/scripts/subnode_artifact.py +0 -735
  478. package/dist/templates/trellis/scripts/workspace_note.py +0 -126
  479. /package/dist/commands/{channel → teamwork}/adapters/types.js +0 -0
  480. /package/dist/commands/{channel → teamwork}/supervisor/idle.d.ts +0 -0
  481. /package/dist/commands/{channel → teamwork}/supervisor/idle.js +0 -0
  482. /package/dist/commands/{channel → teamwork}/supervisor/turns.d.ts +0 -0
  483. /package/dist/commands/{channel → teamwork}/supervisor/turns.js +0 -0
@@ -0,0 +1,563 @@
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 set(record) != required 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
+ return record
304
+
305
+
306
+ def _current_projection_digest(root: Path, task_dir: Path, data: dict, task_id: str) -> str:
307
+ return _digest(_projection(root, task_dir, data, task_id))
308
+
309
+
310
+ def transfer_task_ownership(args: argparse.Namespace) -> int:
311
+ root = get_repo_root()
312
+ operation = args.transfer_action
313
+ try:
314
+ task_dir = resolve_task_dir(args.task_path, root)
315
+ if task_dir is None or not is_within_tasks_dir(task_dir, root):
316
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task path is unavailable or unsafe")
317
+ task_path = task_dir.relative_to(root).as_posix()
318
+ data, reason = read_json_checked(task_dir / FILE_TASK_JSON)
319
+ if data is None:
320
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", f"task record cannot be read: {reason}")
321
+ task_id = data.get("id") or data.get("name")
322
+ if not isinstance(task_id, str) or task_id != args.task_id:
323
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "task identity does not match the requested task")
324
+ if operation != "status" and data.get("status") in {"completed", "cancelled"}:
325
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "terminal tasks cannot change ownership")
326
+ path = _ownership_path(root, task_path)
327
+ with _record_lock(path):
328
+ record = _load_owner(path)
329
+ if operation == "status":
330
+ if record and (record["package_id"] != args.package_id or record["package_digest"] != args.package_digest):
331
+ raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
332
+ result = record or {"status": "absent", "generation": 0, "task_id": task_id}
333
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
334
+ return 0
335
+ if not args.explicit_user_request:
336
+ raise ContinuityError("TASK_TRANSFER_NOT_AUTHORIZED", "ownership changes require an explicit user request")
337
+ if args.expected_generation is None or args.expected_generation < 0:
338
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "a non-negative expected generation is required")
339
+ if operation == "quiesce":
340
+ if not PACKAGE_ID_RE.fullmatch(args.package_id) or not PACKAGE_DIGEST_RE.fullmatch(args.package_digest):
341
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "session package identity or SHA-256 digest is invalid")
342
+ context_key, current_path = _direct_pointer(root, task_path)
343
+ if current_path is None:
344
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session has no current task")
345
+ continuity_digest = _current_projection_digest(root, task_dir, data, task_id)
346
+ if args.expected_continuity_digest is not None:
347
+ if not PACKAGE_DIGEST_RE.fullmatch(args.expected_continuity_digest):
348
+ raise ContinuityError("TASK_TRANSFER_INVALID_INPUT", "expected continuity digest must be a SHA-256 digest")
349
+ if continuity_digest != args.expected_continuity_digest:
350
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "current task projection differs from the package snapshot")
351
+ if record is not None:
352
+ 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:
353
+ if record["continuity_digest"] != continuity_digest:
354
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
355
+ result = record
356
+ print(json.dumps(result, ensure_ascii=False, sort_keys=True))
357
+ return 0
358
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "task already has an ownership record")
359
+ if args.expected_generation != 0:
360
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", "initial ownership generation must be 0")
361
+ record = {
362
+ "schema_version": 1, "task_id": task_id, "task_path": task_path,
363
+ "package_id": args.package_id, "package_digest": args.package_digest,
364
+ "generation": 1, "status": "quiescing", "source_session_id": context_key,
365
+ "target_session_id": None, "continuity_digest": continuity_digest,
366
+ "route": _route_snapshot(data),
367
+ }
368
+ else:
369
+ if record is None:
370
+ raise ContinuityError("TASK_TRANSFER_ABSENT", "ownership transfer has not started")
371
+ if args.package_id != record["package_id"] or args.package_digest != record["package_digest"]:
372
+ raise ContinuityError("TASK_TRANSFER_STALE_PACKAGE", "session package identity or digest changed")
373
+ if args.expected_generation != record["generation"]:
374
+ raise ContinuityError("TASK_TRANSFER_STALE_GENERATION", f"expected generation {args.expected_generation}, current generation is {record['generation']}")
375
+ if operation != "release" and _current_projection_digest(root, task_dir, data, task_id) != record["continuity_digest"]:
376
+ raise ContinuityError("TASK_TRANSFER_STALE_CONTINUITY", "task continuity changed during ownership transfer")
377
+ if operation != "release" and _route_snapshot(data) != record["route"]:
378
+ raise ContinuityError("TASK_TRANSFER_STALE_ROUTE", "task phase, route, input, or implementation authorization changed")
379
+ context_key, current_path = _direct_pointer(root)
380
+ if operation in {"seal", "retire"}:
381
+ if context_key != record["source_session_id"] or current_path not in {record["task_path"], None}:
382
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "source session no longer owns the current task")
383
+ elif operation in {"claim", "consume"}:
384
+ if context_key == record["source_session_id"]:
385
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "target must be a different direct session")
386
+ if operation == "release":
387
+ if record["status"] not in {"quiescing", "sealed", "recovery_required"} or record["target_session_id"] is not None:
388
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "ownership can be released only before target claim")
389
+ if context_key != record["source_session_id"] or current_path not in {None, record["task_path"]}:
390
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the source session can release this transfer")
391
+ if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
392
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer could not be restored")
393
+ active = resolve_active_task(root)
394
+ if active.task_path != record["task_path"] or active.context_key != context_key:
395
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source task pointer restoration could not be verified")
396
+ path.unlink(missing_ok=True)
397
+ print(json.dumps({"status": "released", "generation": record["generation"], "task_id": task_id}, ensure_ascii=False, sort_keys=True))
398
+ return 0
399
+ if operation == "seal" and record["status"] == "quiescing":
400
+ record.update({"status": "sealed", "generation": record["generation"] + 1})
401
+ elif operation == "retire" and record["status"] in {"sealed", "retiring", "recovery_required"}:
402
+ if record["status"] == "sealed":
403
+ record.update({"status": "retiring", "generation": record["generation"] + 1})
404
+ if not write_json(path, record):
405
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record source retirement")
406
+ if current_path is not None and not compare_and_set_active_task(root, context_key, record["task_path"], None):
407
+ record["status"] = "recovery_required"
408
+ write_json(path, record)
409
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer changed during retirement")
410
+ if resolve_active_task(root).task_path is not None:
411
+ record["status"] = "recovery_required"
412
+ write_json(path, record)
413
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "source pointer remains after retirement")
414
+ record["status"] = "ready"
415
+ elif operation == "claim" and record["status"] in {"ready", "claiming", "recovery_required", "claimed"}:
416
+ if record["status"] == "recovery_required" and record["target_session_id"] != context_key:
417
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the recorded target session can recover a claim")
418
+ if current_path not in {None, record["task_path"]}:
419
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "target session already has a current task")
420
+ if record["status"] == "ready":
421
+ record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
422
+ if not write_json(path, record):
423
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target claim")
424
+ elif record["status"] == "claimed":
425
+ previous_target = record["target_session_id"]
426
+ if previous_target == context_key:
427
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", "the current target must consume ownership instead of claiming it again")
428
+ if previous_target is not None and not clear_active_task_if_matches(root, previous_target, record["task_path"]):
429
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "previous target pointer could not be cleared")
430
+ record.update({"status": "claiming", "generation": record["generation"] + 1, "target_session_id": context_key})
431
+ if not write_json(path, record):
432
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not record target takeover")
433
+ if current_path is None and not compare_and_set_active_task(root, context_key, None, record["task_path"]):
434
+ record["status"] = "recovery_required"
435
+ write_json(path, record)
436
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer could not be claimed")
437
+ active = resolve_active_task(root)
438
+ if active.task_path != record["task_path"] or active.context_key != context_key:
439
+ record["status"] = "recovery_required"
440
+ write_json(path, record)
441
+ raise ContinuityError("TASK_TRANSFER_RECOVERY_REQUIRED", "target pointer verification failed")
442
+ record["status"] = "claimed"
443
+ elif operation == "consume" and record["status"] == "claimed":
444
+ if context_key != record["target_session_id"] or current_path != record["task_path"]:
445
+ raise ContinuityError("TASK_TRANSFER_WITHHELD", "only the direct target session can consume the task")
446
+ record.update({"status": "consumed", "generation": record["generation"] + 1})
447
+ else:
448
+ raise ContinuityError("TASK_TRANSFER_INVALID_TRANSITION", f"cannot apply {operation} while ownership is {record['status']}")
449
+ if not write_json(path, record):
450
+ raise ContinuityError("TASK_TRANSFER_WRITE_FAILED", "could not atomically update ownership record")
451
+ print(json.dumps(record, ensure_ascii=False, sort_keys=True))
452
+ return 0
453
+ except ContinuityError as error:
454
+ print(json.dumps({"status": "withheld", "code": error.code, "reason": str(error)}, ensure_ascii=False))
455
+ return 1
456
+
457
+
458
+ def add_continuity_commands(subparsers) -> None:
459
+ continuity = subparsers.add_parser("continuity", help="Read or capture current task continuity")
460
+ continuity_sub = continuity.add_subparsers(dest="continuity_action", required=True)
461
+ continuity_sub.add_parser("show", help="Recompute current continuity status")
462
+ capture = continuity_sub.add_parser("capture", help="Capture the current task projection with compare-and-set")
463
+ capture.add_argument("--expected-source-digest", required=True, help="Current captured digest, or absent")
464
+
465
+ transfer = subparsers.add_parser("transfer", help="Transfer task ownership between direct sessions")
466
+ transfer.add_argument("transfer_action", choices=("status", "quiesce", "seal", "retire", "claim", "consume", "release"))
467
+ transfer.add_argument("--task-id", required=True)
468
+ transfer.add_argument("--task-path", required=True)
469
+ transfer.add_argument("--package-id", required=True)
470
+ transfer.add_argument("--package-digest", required=True)
471
+ transfer.add_argument("--expected-continuity-digest")
472
+ transfer.add_argument("--expected-generation", type=int)
473
+ transfer.add_argument("--explicit-user-request", action="store_true")
474
+
475
+
476
+ def clear_task_runtime_records(repo_root: Path, task_path: str) -> bool:
477
+ """Remove only this task's current continuity and ownership projections."""
478
+ for path in (_continuity_path(repo_root, task_path), _ownership_path(repo_root, task_path)):
479
+ if not path.exists():
480
+ continue
481
+ with _record_lock(path):
482
+ try:
483
+ path.unlink(missing_ok=True)
484
+ except OSError:
485
+ return False
486
+ return True
487
+
488
+
489
+ @contextlib.contextmanager
490
+ def active_task_change_guard(repo_root: Path, context_key: str, task_path: str, *, clearing: bool):
491
+ """Serialize pointer writes with ownership changes for the same task."""
492
+ directory = resolve_task_dir(task_path, repo_root)
493
+ if directory is None:
494
+ yield False
495
+ return
496
+ task_path = directory.relative_to(repo_root).as_posix()
497
+ path = _ownership_path(repo_root, task_path)
498
+ with _record_lock(path):
499
+ try:
500
+ record = _load_owner(path)
501
+ except ContinuityError:
502
+ yield False
503
+ return
504
+ if record is None:
505
+ yield True
506
+ return
507
+ data, _ = read_json_checked(directory / FILE_TASK_JSON)
508
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
509
+ if task_id != record["task_id"] or record["task_path"] != task_path:
510
+ yield False
511
+ return
512
+ if clearing:
513
+ allowed = record["status"] == "consumed" and record["target_session_id"] == context_key
514
+ elif record["status"] == "ready":
515
+ allowed = context_key != record["source_session_id"] and task_path == record["task_path"]
516
+ elif record["status"] == "consumed":
517
+ allowed = context_key == record["target_session_id"] and task_path == record["task_path"]
518
+ else:
519
+ allowed = record["status"] in {"claiming", "claimed"} and context_key == record["target_session_id"] and task_path == record["task_path"]
520
+ yield allowed
521
+
522
+
523
+ def assert_task_mutation_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
524
+ """Reject task writes until ownership is consumed or the source releases it."""
525
+ with task_mutation_guard(repo_root, task_dir, data) as allowed:
526
+ if not allowed:
527
+ raise TaskWorkflowError("TASK_TRANSFER_ACTIVE", "task changes are fenced until the target session consumes ownership or the source releases the transfer")
528
+
529
+
530
+ def assert_task_identity_change_allowed(repo_root: Path, task_dir: Path, data: dict) -> None:
531
+ """Keep an active ownership record addressable while the task ID is fixed."""
532
+ task_path = task_dir.relative_to(repo_root).as_posix()
533
+ path = _ownership_path(repo_root, task_path)
534
+ with _record_lock(path):
535
+ try:
536
+ record = _load_owner(path)
537
+ except ContinuityError as error:
538
+ raise TaskWorkflowError(error.code, str(error)) from error
539
+ if record is not None:
540
+ raise TaskWorkflowError("TASK_TRANSFER_TASK_ID_LOCKED", "task ID cannot change while its ownership record is active")
541
+
542
+
543
+ @contextlib.contextmanager
544
+ def task_mutation_guard(repo_root: Path, task_dir: Path, data: dict):
545
+ """Hold the ownership lock across a single task mutation."""
546
+ task_path = task_dir.relative_to(repo_root).as_posix()
547
+ path = _ownership_path(repo_root, task_path)
548
+ with _record_lock(path):
549
+ try:
550
+ record = _load_owner(path)
551
+ except ContinuityError:
552
+ yield False
553
+ return
554
+ if record is None:
555
+ yield True
556
+ return
557
+ task_id = (data.get("id") or data.get("name")) if isinstance(data, dict) else None
558
+ if task_id != record["task_id"] or record["task_path"] != task_path:
559
+ yield False
560
+ return
561
+ active = resolve_active_task(repo_root)
562
+ allowed = record["status"] == "consumed" and active.context_key == record["target_session_id"] and active.task_path == task_path
563
+ yield allowed