@opengsd/gsd-path 1.4.0 → 1.5.0

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 (347) hide show
  1. package/CHANGELOG.md +325 -344
  2. package/DOCS.md +12 -2
  3. package/FULL.md +3 -2
  4. package/HOOKS.md +95 -8
  5. package/QUICK.md +1 -1
  6. package/README.md +78 -110
  7. package/RUNTIME.md +1 -1
  8. package/UPDATE.md +4 -3
  9. package/WORKFLOW.md +22 -1
  10. package/package.json +5 -2
  11. package/platforms/shared-agents/dispatch.md +13 -0
  12. package/scripts/_common.py +149 -1
  13. package/scripts/archive_milestone.py +14 -13
  14. package/scripts/build_recovery.py +96 -31
  15. package/scripts/build_state.py +79 -3
  16. package/scripts/check_docs_audit.py +77 -4
  17. package/scripts/check_handoffs.py +546 -30
  18. package/scripts/check_task_briefs.py +279 -23
  19. package/scripts/detect_project.py +243 -18
  20. package/scripts/discussion_validate.py +86 -29
  21. package/scripts/dispatch_driver.py +392 -22
  22. package/scripts/git_guard.py +3 -3
  23. package/scripts/guard_hook.py +1416 -165
  24. package/scripts/install.mjs +23 -8
  25. package/scripts/install.py +49 -9
  26. package/scripts/integration.py +146 -1
  27. package/scripts/isolation.py +392 -60
  28. package/scripts/loop_run.py +16 -4
  29. package/scripts/members.py +11 -9
  30. package/scripts/path_config.py +3 -3
  31. package/scripts/pipeline_git.py +63 -12
  32. package/scripts/pipeline_state.py +111 -33
  33. package/scripts/pipeline_undo.py +101 -8
  34. package/scripts/runtime_store.py +20 -2
  35. package/scripts/skill-resources.json +9 -2
  36. package/scripts/state_checkpoint.py +9 -27
  37. package/scripts/status_runtime.py +22 -4
  38. package/scripts/task_context.py +12 -3
  39. package/scripts/wizard.mjs +1 -0
  40. package/scripts/workflow_run.py +5 -2
  41. package/scripts/worktree_paths.py +54 -2
  42. package/skills/gsd-path/DEFINE.md +28 -3
  43. package/skills/gsd-path/PLAN.md +11 -3
  44. package/skills/gsd-path/RESEARCH.md +11 -0
  45. package/skills/gsd-path/SHIP.md +6 -1
  46. package/skills/gsd-path/references/build-native.md +16 -1
  47. package/skills/gsd-path/references/dispatch.md +13 -0
  48. package/skills/gsd-path/references/docs-auditor.md +5 -0
  49. package/skills/gsd-path/references/planner.md +14 -4
  50. package/skills/gsd-path/references/reviewer-final.md +20 -2
  51. package/skills/gsd-path/references/reviewer-wave.md +15 -4
  52. package/skills/gsd-path/references/reviewer.md +3 -1
  53. package/skills/gsd-path/references/spec-probes.md +150 -0
  54. package/skills/gsd-path/scripts/_common.py +149 -1
  55. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  56. package/skills/gsd-path/scripts/build_recovery.py +96 -31
  57. package/skills/gsd-path/scripts/build_state.py +79 -3
  58. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  59. package/skills/gsd-path/scripts/check_handoffs.py +546 -30
  60. package/skills/gsd-path/scripts/check_task_briefs.py +279 -23
  61. package/skills/gsd-path/scripts/detect_project.py +243 -18
  62. package/skills/gsd-path/scripts/discussion_validate.py +86 -29
  63. package/skills/gsd-path/scripts/dispatch_driver.py +392 -22
  64. package/skills/gsd-path/scripts/git_guard.py +3 -3
  65. package/skills/gsd-path/scripts/integration.py +146 -1
  66. package/skills/gsd-path/scripts/isolation.py +392 -60
  67. package/skills/gsd-path/scripts/loop_run.py +16 -4
  68. package/skills/gsd-path/scripts/members.py +11 -9
  69. package/skills/gsd-path/scripts/path_config.py +3 -3
  70. package/skills/gsd-path/scripts/pipeline_git.py +63 -12
  71. package/skills/gsd-path/scripts/pipeline_state.py +111 -33
  72. package/skills/gsd-path/scripts/state_checkpoint.py +9 -27
  73. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  74. package/skills/gsd-path/scripts/task_context.py +12 -3
  75. package/skills/gsd-path/scripts/workflow_run.py +5 -2
  76. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  77. package/skills/gsd-path/templates/docs-audit.md +5 -1
  78. package/skills/gsd-path/templates/final-review.md +11 -2
  79. package/skills/gsd-path/templates/intent.md +30 -0
  80. package/skills/gsd-path/templates/patch-findings.md +3 -3
  81. package/skills/gsd-path/templates/plan.md +17 -2
  82. package/skills/gsd-path/templates/research-handoff.md +14 -3
  83. package/skills/gsd-path/templates/state.md +1 -1
  84. package/skills/gsd-path/templates/wave-review.md +16 -6
  85. package/skills/gsd-path-build/references/build-native.md +16 -1
  86. package/skills/gsd-path-build/references/dispatch.md +13 -0
  87. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  88. package/skills/gsd-path-build/references/reviewer-wave.md +15 -4
  89. package/skills/gsd-path-build/references/reviewer.md +3 -1
  90. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  91. package/skills/gsd-path-build/scripts/_common.py +149 -1
  92. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  93. package/skills/gsd-path-build/scripts/build_recovery.py +96 -31
  94. package/skills/gsd-path-build/scripts/build_state.py +79 -3
  95. package/skills/gsd-path-build/scripts/check_handoffs.py +546 -30
  96. package/skills/gsd-path-build/scripts/check_task_briefs.py +279 -23
  97. package/skills/gsd-path-build/scripts/detect_project.py +243 -18
  98. package/skills/gsd-path-build/scripts/discussion_validate.py +86 -29
  99. package/skills/gsd-path-build/scripts/dispatch_driver.py +392 -22
  100. package/skills/gsd-path-build/scripts/integration.py +146 -1
  101. package/skills/gsd-path-build/scripts/isolation.py +392 -60
  102. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  103. package/skills/gsd-path-build/scripts/members.py +11 -9
  104. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  105. package/skills/gsd-path-build/scripts/pipeline_git.py +63 -12
  106. package/skills/gsd-path-build/scripts/pipeline_state.py +111 -33
  107. package/skills/gsd-path-build/scripts/state_checkpoint.py +9 -27
  108. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  109. package/skills/gsd-path-build/scripts/workflow_run.py +5 -2
  110. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  111. package/skills/gsd-path-build/templates/wave-review.md +16 -6
  112. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  113. package/skills/gsd-path-decide/scripts/_common.py +149 -1
  114. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  115. package/skills/gsd-path-decide/scripts/build_recovery.py +96 -31
  116. package/skills/gsd-path-decide/scripts/check_handoffs.py +546 -30
  117. package/skills/gsd-path-decide/scripts/check_task_briefs.py +279 -23
  118. package/skills/gsd-path-decide/scripts/detect_project.py +243 -18
  119. package/skills/gsd-path-decide/scripts/discussion_validate.py +86 -29
  120. package/skills/gsd-path-decide/scripts/integration.py +146 -1
  121. package/skills/gsd-path-decide/scripts/isolation.py +392 -60
  122. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  123. package/skills/gsd-path-decide/scripts/members.py +11 -9
  124. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  125. package/skills/gsd-path-decide/scripts/pipeline_git.py +63 -12
  126. package/skills/gsd-path-decide/scripts/pipeline_state.py +111 -33
  127. package/skills/gsd-path-decide/scripts/state_checkpoint.py +9 -27
  128. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  129. package/skills/gsd-path-define/SKILL.md +28 -3
  130. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  131. package/skills/gsd-path-define/scripts/_common.py +149 -1
  132. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  133. package/skills/gsd-path-define/scripts/build_recovery.py +96 -31
  134. package/skills/gsd-path-define/scripts/check_handoffs.py +546 -30
  135. package/skills/gsd-path-define/scripts/check_task_briefs.py +279 -23
  136. package/skills/gsd-path-define/scripts/detect_project.py +243 -18
  137. package/skills/gsd-path-define/scripts/discussion_validate.py +86 -29
  138. package/skills/gsd-path-define/scripts/integration.py +146 -1
  139. package/skills/gsd-path-define/scripts/isolation.py +392 -60
  140. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  141. package/skills/gsd-path-define/scripts/members.py +11 -9
  142. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  143. package/skills/gsd-path-define/scripts/pipeline_git.py +63 -12
  144. package/skills/gsd-path-define/scripts/pipeline_state.py +111 -33
  145. package/skills/gsd-path-define/scripts/state_checkpoint.py +9 -27
  146. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  147. package/skills/gsd-path-define/templates/intent.md +30 -0
  148. package/skills/gsd-path-define/templates/state.md +1 -1
  149. package/skills/gsd-path-discuss/scripts/_common.py +149 -1
  150. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  151. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  152. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  153. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  154. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  155. package/skills/gsd-path-docs-audit/scripts/_common.py +149 -1
  156. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  157. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +96 -31
  158. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  159. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +546 -30
  160. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +279 -23
  161. package/skills/gsd-path-docs-audit/scripts/detect_project.py +243 -18
  162. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +86 -29
  163. package/skills/gsd-path-docs-audit/scripts/integration.py +146 -1
  164. package/skills/gsd-path-docs-audit/scripts/isolation.py +392 -60
  165. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  166. package/skills/gsd-path-docs-audit/scripts/members.py +11 -9
  167. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  168. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +63 -12
  169. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +111 -33
  170. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +9 -27
  171. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  172. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  173. package/skills/gsd-path-forensics/scripts/_common.py +149 -1
  174. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  175. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  176. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +101 -8
  177. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  178. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  179. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  180. package/skills/gsd-path-inspect/scripts/_common.py +149 -1
  181. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  182. package/skills/gsd-path-inspect/scripts/build_recovery.py +96 -31
  183. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  184. package/skills/gsd-path-inspect/scripts/check_handoffs.py +546 -30
  185. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +279 -23
  186. package/skills/gsd-path-inspect/scripts/detect_project.py +243 -18
  187. package/skills/gsd-path-inspect/scripts/discussion_validate.py +86 -29
  188. package/skills/gsd-path-inspect/scripts/integration.py +146 -1
  189. package/skills/gsd-path-inspect/scripts/isolation.py +392 -60
  190. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  191. package/skills/gsd-path-inspect/scripts/members.py +11 -9
  192. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  193. package/skills/gsd-path-inspect/scripts/pipeline_git.py +63 -12
  194. package/skills/gsd-path-inspect/scripts/pipeline_state.py +111 -33
  195. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +9 -27
  196. package/skills/gsd-path-inspect/scripts/workflow_run.py +5 -2
  197. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  198. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  199. package/skills/gsd-path-inspect/templates/state.md +1 -1
  200. package/skills/gsd-path-loop/scripts/_common.py +149 -1
  201. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  203. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  204. package/skills/gsd-path-migrate/scripts/_common.py +149 -1
  205. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  206. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  207. package/skills/gsd-path-plan/SKILL.md +11 -3
  208. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  209. package/skills/gsd-path-plan/references/planner.md +14 -4
  210. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  211. package/skills/gsd-path-plan/scripts/_common.py +149 -1
  212. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  213. package/skills/gsd-path-plan/scripts/build_recovery.py +96 -31
  214. package/skills/gsd-path-plan/scripts/check_handoffs.py +546 -30
  215. package/skills/gsd-path-plan/scripts/check_task_briefs.py +279 -23
  216. package/skills/gsd-path-plan/scripts/detect_project.py +243 -18
  217. package/skills/gsd-path-plan/scripts/discussion_validate.py +86 -29
  218. package/skills/gsd-path-plan/scripts/integration.py +146 -1
  219. package/skills/gsd-path-plan/scripts/isolation.py +392 -60
  220. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-plan/scripts/members.py +11 -9
  222. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  223. package/skills/gsd-path-plan/scripts/pipeline_git.py +63 -12
  224. package/skills/gsd-path-plan/scripts/pipeline_state.py +111 -33
  225. package/skills/gsd-path-plan/scripts/state_checkpoint.py +9 -27
  226. package/skills/gsd-path-plan/scripts/workflow_run.py +5 -2
  227. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  228. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  229. package/skills/gsd-path-plan/templates/plan.md +17 -2
  230. package/skills/gsd-path-research/SKILL.md +11 -0
  231. package/skills/gsd-path-research/references/dispatch.md +13 -0
  232. package/skills/gsd-path-research/scripts/_common.py +149 -1
  233. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  234. package/skills/gsd-path-research/scripts/build_recovery.py +96 -31
  235. package/skills/gsd-path-research/scripts/check_handoffs.py +546 -30
  236. package/skills/gsd-path-research/scripts/check_task_briefs.py +279 -23
  237. package/skills/gsd-path-research/scripts/detect_project.py +243 -18
  238. package/skills/gsd-path-research/scripts/discussion_validate.py +86 -29
  239. package/skills/gsd-path-research/scripts/integration.py +146 -1
  240. package/skills/gsd-path-research/scripts/isolation.py +392 -60
  241. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  242. package/skills/gsd-path-research/scripts/members.py +11 -9
  243. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  244. package/skills/gsd-path-research/scripts/pipeline_git.py +63 -12
  245. package/skills/gsd-path-research/scripts/pipeline_state.py +111 -33
  246. package/skills/gsd-path-research/scripts/state_checkpoint.py +9 -27
  247. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  248. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  249. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  250. package/skills/gsd-path-roadmap/scripts/_common.py +149 -1
  251. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  252. package/skills/gsd-path-roadmap/scripts/build_recovery.py +96 -31
  253. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +546 -30
  254. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +279 -23
  255. package/skills/gsd-path-roadmap/scripts/detect_project.py +243 -18
  256. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +86 -29
  257. package/skills/gsd-path-roadmap/scripts/integration.py +146 -1
  258. package/skills/gsd-path-roadmap/scripts/isolation.py +392 -60
  259. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  260. package/skills/gsd-path-roadmap/scripts/members.py +11 -9
  261. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  262. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +63 -12
  263. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +111 -33
  264. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +9 -27
  265. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  266. package/skills/gsd-path-ship/SKILL.md +6 -1
  267. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  268. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  269. package/skills/gsd-path-ship/references/reviewer-wave.md +15 -4
  270. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  271. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  272. package/skills/gsd-path-ship/scripts/_common.py +149 -1
  273. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  274. package/skills/gsd-path-ship/scripts/build_recovery.py +96 -31
  275. package/skills/gsd-path-ship/scripts/build_state.py +79 -3
  276. package/skills/gsd-path-ship/scripts/check_handoffs.py +546 -30
  277. package/skills/gsd-path-ship/scripts/check_task_briefs.py +279 -23
  278. package/skills/gsd-path-ship/scripts/detect_project.py +243 -18
  279. package/skills/gsd-path-ship/scripts/discussion_validate.py +86 -29
  280. package/skills/gsd-path-ship/scripts/integration.py +146 -1
  281. package/skills/gsd-path-ship/scripts/isolation.py +392 -60
  282. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  283. package/skills/gsd-path-ship/scripts/members.py +11 -9
  284. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  285. package/skills/gsd-path-ship/scripts/pipeline_git.py +63 -12
  286. package/skills/gsd-path-ship/scripts/pipeline_state.py +111 -33
  287. package/skills/gsd-path-ship/scripts/state_checkpoint.py +9 -27
  288. package/skills/gsd-path-ship/scripts/workflow_run.py +5 -2
  289. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  290. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  291. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  292. package/skills/gsd-path-ship/templates/wave-review.md +16 -6
  293. package/skills/gsd-path-undo/scripts/_common.py +149 -1
  294. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  295. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  296. package/skills/gsd-path-undo/scripts/pipeline_undo.py +101 -8
  297. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  298. package/skills/path/DEFINE.md +28 -3
  299. package/skills/path/PLAN.md +11 -3
  300. package/skills/path/RESEARCH.md +11 -0
  301. package/skills/path/SHIP.md +6 -1
  302. package/skills/path/references/build-native.md +16 -1
  303. package/skills/path/references/dispatch.md +13 -0
  304. package/skills/path/references/docs-auditor.md +5 -0
  305. package/skills/path/references/planner.md +14 -4
  306. package/skills/path/references/reviewer-final.md +20 -2
  307. package/skills/path/references/reviewer-wave.md +15 -4
  308. package/skills/path/references/reviewer.md +3 -1
  309. package/skills/path/references/spec-probes.md +150 -0
  310. package/skills/path/scripts/_common.py +149 -1
  311. package/skills/path/scripts/archive_milestone.py +14 -13
  312. package/skills/path/scripts/build_recovery.py +96 -31
  313. package/skills/path/scripts/build_state.py +79 -3
  314. package/skills/path/scripts/check_docs_audit.py +77 -4
  315. package/skills/path/scripts/check_handoffs.py +546 -30
  316. package/skills/path/scripts/check_task_briefs.py +279 -23
  317. package/skills/path/scripts/detect_project.py +243 -18
  318. package/skills/path/scripts/discussion_validate.py +86 -29
  319. package/skills/path/scripts/dispatch_driver.py +392 -22
  320. package/skills/path/scripts/git_guard.py +3 -3
  321. package/skills/path/scripts/integration.py +146 -1
  322. package/skills/path/scripts/isolation.py +392 -60
  323. package/skills/path/scripts/loop_run.py +16 -4
  324. package/skills/path/scripts/members.py +11 -9
  325. package/skills/path/scripts/path_config.py +3 -3
  326. package/skills/path/scripts/pipeline_git.py +63 -12
  327. package/skills/path/scripts/pipeline_state.py +111 -33
  328. package/skills/path/scripts/state_checkpoint.py +9 -27
  329. package/skills/path/scripts/status_runtime.py +22 -4
  330. package/skills/path/scripts/task_context.py +12 -3
  331. package/skills/path/scripts/workflow_run.py +5 -2
  332. package/skills/path/scripts/worktree_paths.py +54 -2
  333. package/skills/path/templates/docs-audit.md +5 -1
  334. package/skills/path/templates/final-review.md +11 -2
  335. package/skills/path/templates/intent.md +30 -0
  336. package/skills/path/templates/patch-findings.md +3 -3
  337. package/skills/path/templates/plan.md +17 -2
  338. package/skills/path/templates/research-handoff.md +14 -3
  339. package/skills/path/templates/state.md +1 -1
  340. package/skills/path/templates/wave-review.md +16 -6
  341. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  342. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  343. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  344. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  345. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  346. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  347. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -15,6 +15,7 @@ import hashlib
15
15
  import io
16
16
  import json
17
17
  import shlex
18
+ import shutil
18
19
  import os
19
20
  import re
20
21
  import stat
@@ -27,11 +28,6 @@ import tempfile
27
28
  from pathlib import Path, PurePosixPath
28
29
  from typing import Dict, Iterator, Optional, Sequence, Set, Tuple
29
30
 
30
- if sys.platform == "win32":
31
- import msvcrt
32
- else:
33
- import fcntl
34
-
35
31
  if __package__:
36
32
  from .pipeline_git import (
37
33
  attest_commit_body,
@@ -176,6 +172,12 @@ def sidecar_root(primary: Path, kind: str, name: str, *, pin: bool = False) -> P
176
172
  raise IsolationError(str(error)) from error
177
173
 
178
174
 
175
+ def release_workspace_quietly(primary: Path) -> None:
176
+ """Best-effort placement hygiene after a sidecar retires; never blocks landing."""
177
+ with contextlib.suppress(ValueError, OSError):
178
+ worktree_paths.release_workspace(primary)
179
+
180
+
179
181
  def validate_task_id(task_id: str) -> str:
180
182
  if not TASK_ID_RE.fullmatch(task_id):
181
183
  raise IsolationError(f"invalid task id: {task_id}")
@@ -439,8 +441,9 @@ def collect_artifact_recoveries(primary: Path) -> list[Dict[str, object]]:
439
441
 
440
442
 
441
443
  def _git_file(repo: Path, revision: str, path: str) -> bytes:
444
+ git_path = PurePosixPath(path.replace("\\", "/")).as_posix()
442
445
  result = subprocess.run(
443
- ("git", "-C", str(repo), "show", f"{revision}:{path}"),
446
+ ("git", "-C", str(repo), "show", f"{revision}:{git_path}"),
444
447
  capture_output=True,
445
448
  check=False,
446
449
  )
@@ -481,6 +484,70 @@ def _same_directory(path: Path, expected: os.stat_result) -> bool:
481
484
  return stat.S_ISDIR(current.st_mode) and os.path.samestat(current, expected)
482
485
 
483
486
 
487
+ def _long_path(absolute: str) -> str:
488
+ if absolute.startswith("\\\\?\\"):
489
+ return absolute
490
+ if absolute.startswith("\\\\"):
491
+ return "\\\\?\\UNC\\" + absolute[2:]
492
+ return "\\\\?\\" + absolute
493
+
494
+
495
+ def _force_rmtree(path: Path) -> None:
496
+ """Delete a sidecar tree directly, tolerating read-only files and long paths."""
497
+ def _writable(function, target, _error):
498
+ os.chmod(target, stat.S_IWRITE)
499
+ function(target)
500
+
501
+ # The \\?\ prefix sidesteps Windows MAX_PATH on deep sidecar trees.
502
+ target = Path(_long_path(os.path.abspath(path))) if os.name == "nt" else path
503
+ try:
504
+ shutil.rmtree(target, onerror=_writable)
505
+ except OSError as error:
506
+ raise IsolationError(f"could not delete {path}: {error}") from error
507
+
508
+
509
+ def _remove_worktree(primary: Path, destination: Path, force: bool) -> None:
510
+ """Remove a sidecar worktree, tolerating populated submodules and locked trees.
511
+
512
+ git refuses to remove worktrees whose submodules were ever initialized, and
513
+ forced removal can still fail deleting deep trees (Windows "Directory not
514
+ empty"). A dirty sidecar keeps git's refusal unless `force`, so forced
515
+ retries only run for a provably clean tree. The direct-delete fallback runs
516
+ only for git's tree-deletion failures, so any other git error leaves the
517
+ sidecar intact for diagnosis.
518
+ """
519
+ tree_delete_failure = ("failed to delete", "directory not empty", "permission denied")
520
+
521
+ def failure(*flags: str) -> str:
522
+ result = run_git(primary, "worktree", "remove", *flags, str(destination))
523
+ return (result.stderr or result.stdout).strip() if result.returncode else ""
524
+
525
+ detail = failure("--force") if force else failure()
526
+ if not detail:
527
+ return
528
+ if not force and destination.is_dir():
529
+ status = run_git(
530
+ destination, "status", "--porcelain", "--untracked-files=all"
531
+ )
532
+ if status.returncode != 0 or status.stdout.strip():
533
+ raise IsolationError(detail or "git worktree remove failed")
534
+ run_git(destination, "submodule", "deinit", "--all", "--force")
535
+ detail = failure("--force")
536
+ if not detail:
537
+ return
538
+ if destination.is_dir() and any(
539
+ marker in detail.lower() for marker in tree_delete_failure
540
+ ):
541
+ _force_rmtree(destination)
542
+ detail = failure("--force")
543
+ if detail and not destination.is_dir():
544
+ pruned = run_git(primary, "worktree", "prune")
545
+ if pruned.returncode == 0:
546
+ detail = ""
547
+ if detail or destination.is_dir() or destination.resolve() in _registered_worktrees(primary):
548
+ raise IsolationError(detail or "git worktree remove failed")
549
+
550
+
484
551
  def _cleanup_reserved_worktree(
485
552
  primary: Path,
486
553
  branch: str,
@@ -758,7 +825,7 @@ def retire_member_task(
758
825
  raise IsolationError(f"member task branch {ref} has unlanded commits")
759
826
  destination = sidecar_root(checkout, "task", name)
760
827
  copy_rejected = None
761
- if force:
828
+ if force and task_file:
762
829
  rejected, copy_rejected = _rejected_member_attempt_error(coordinator, member, task_id, task_file, destination)
763
830
  if rejected:
764
831
  raise IsolationError(rejected)
@@ -766,9 +833,7 @@ def retire_member_task(
766
833
  if destination.resolve() in registered:
767
834
  if registered[destination.resolve()] != ref:
768
835
  raise IsolationError(f"member task sidecar is on another branch: {destination}")
769
- removed = run_git(checkout, "worktree", "remove", *(("--force",) if force else ()), str(destination))
770
- if removed.returncode != 0:
771
- raise IsolationError((removed.stderr or removed.stdout).strip() or "git worktree remove failed")
836
+ _remove_worktree(checkout, destination, force)
772
837
  authorization = run_git(checkout, "update-ref", "-d", member_task_authorization_ref(project, task_id))
773
838
  if authorization.returncode != 0:
774
839
  raise IsolationError((authorization.stderr or authorization.stdout).strip() or "git update-ref failed")
@@ -776,6 +841,7 @@ def retire_member_task(
776
841
  deleted = run_git(checkout, "update-ref", "-d", ref, tip.stdout.strip())
777
842
  if deleted.returncode != 0:
778
843
  raise IsolationError((deleted.stderr or deleted.stdout).strip() or "git update-ref failed")
844
+ release_workspace_quietly(checkout)
779
845
  return copy_rejected
780
846
 
781
847
 
@@ -936,9 +1002,11 @@ def activate_member_task(
936
1002
  created = run_git(checkout, "update-ref", ref, member_base, "0" * 40)
937
1003
  if created.returncode != 0 and run_git(checkout, "rev-parse", "--verify", "--quiet", ref).stdout.strip() != member_base:
938
1004
  raise IsolationError("could not record member task authorization")
939
- return {"agent": agent, "base": resolved_base, "copy": str(copy), "member": member,
1005
+ result = {"agent": agent, "base": resolved_base, "copy": str(copy), "member": member,
940
1006
  "member_base": member_base, "status": "in-progress", "task_branch": branch,
941
1007
  "task_file": task_file, "worktree": str(sidecar)}
1008
+ _write_native_member_dispatch_record(coordinator, result)
1009
+ return result
942
1010
 
943
1011
 
944
1012
  def retained_member_task(
@@ -1050,25 +1118,8 @@ def _member_journal_path(coordinator: Path, task_id: str) -> Path:
1050
1118
  @contextlib.contextmanager
1051
1119
  def _member_landing_lock(coordinator: Path) -> Iterator[None]:
1052
1120
  path = common_git_dir(coordinator).joinpath(*MEMBER_LANDING_DIR, ".lock")
1053
- path.parent.mkdir(parents=True, exist_ok=True)
1054
- with path.open("a+b") as handle:
1055
- if sys.platform == "win32":
1056
- handle.seek(0, os.SEEK_END)
1057
- if handle.tell() == 0:
1058
- handle.write(b"\0")
1059
- handle.flush()
1060
- handle.seek(0)
1061
- msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
1062
- else:
1063
- fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
1064
- try:
1065
- yield
1066
- finally:
1067
- if sys.platform == "win32":
1068
- handle.seek(0)
1069
- msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
1070
- else:
1071
- fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
1121
+ with _common.exclusive_lock(path):
1122
+ yield
1072
1123
 
1073
1124
 
1074
1125
  def _require_member_coordinator_branch(coordinator: Path) -> None:
@@ -1411,8 +1462,9 @@ def retire_member_verify(created: Dict[str, Dict[str, str]]) -> None:
1411
1462
  """Remove the member siblings of a coordinator verify sidecar; they hold no output."""
1412
1463
  for record in reversed(list(created.values())):
1413
1464
  checkout, destination, branch = _member_verify_sidecar(record)
1414
- git_output(checkout, "worktree", "remove", "--force", str(destination))
1465
+ _remove_worktree(checkout, destination, True)
1415
1466
  git_output(checkout, "branch", "-D", branch)
1467
+ release_workspace_quietly(checkout)
1416
1468
 
1417
1469
 
1418
1470
  def check_member_verify(created: Dict[str, Dict[str, str]]) -> None:
@@ -1444,6 +1496,89 @@ def clean_verify(primary: Path, worktree: Path, base: str, branch: str) -> Dict[
1444
1496
  return {"worktree": str(worktree), "branch": branch, "base": expected, "removed": removed}
1445
1497
 
1446
1498
 
1499
+ def ensure_verify_sidecar(primary: Path, base: str, branch: str) -> Dict[str, object]:
1500
+ """Return the named verify sidecar, recreating it when it is missing.
1501
+
1502
+ Finish needs the sidecar prepare-task created. A deleted sidecar must be
1503
+ rebuildable from the recorded base without a clean primary: the serial
1504
+ task's own product changes are exactly the tree finish reproduces next,
1505
+ so this must not reuse isolate_verify's clean-tree gate.
1506
+ """
1507
+ primary = require_directory(primary, "primary worktree")
1508
+ if worktree_root(primary) != primary:
1509
+ raise IsolationError(f"primary is not its Git root: {primary}")
1510
+ resolved_base = require_commit(primary, require_full_sha(base))
1511
+ if not branch.startswith(VERIFY_BRANCH_PREFIX):
1512
+ raise IsolationError("verify sidecar reproduction requires a verify branch")
1513
+ destination = _sidecar_path_for_branch(primary, branch)
1514
+ ref = f"refs/heads/{branch}"
1515
+ registered = _registered_worktrees(primary)
1516
+ if destination.is_symlink() or (destination.exists() and not destination.is_dir()):
1517
+ raise IsolationError(f"worktree path already exists: {destination}")
1518
+ if destination.is_dir():
1519
+ try:
1520
+ # A leftover empty directory (a half-deleted sidecar) clears;
1521
+ # anything non-empty must be the sidecar itself.
1522
+ destination.rmdir()
1523
+ except OSError:
1524
+ try:
1525
+ same = (
1526
+ worktree_root(destination) == destination
1527
+ and destination.resolve() in registered
1528
+ and registered[destination.resolve()] == ref
1529
+ and common_git_dir(destination) == common_git_dir(primary)
1530
+ )
1531
+ except IsolationError:
1532
+ same = False
1533
+ if not same:
1534
+ raise IsolationError(
1535
+ f"verify sidecar ownership changed: {destination}"
1536
+ ) from None
1537
+ if (
1538
+ require_attached(destination) != branch
1539
+ or current_sha(destination) != resolved_base
1540
+ ):
1541
+ raise IsolationError(f"verify sidecar HEAD changed: {destination}")
1542
+ return {
1543
+ "base": resolved_base,
1544
+ "branch": branch,
1545
+ "kind": "verify",
1546
+ "mode": "sidecar",
1547
+ "worktree": str(destination),
1548
+ }
1549
+ if destination.resolve() in registered:
1550
+ cleared = run_git(primary, "worktree", "remove", str(destination))
1551
+ if cleared.returncode != 0:
1552
+ run_git(primary, "worktree", "prune")
1553
+ if destination.resolve() in _registered_worktrees(primary):
1554
+ raise IsolationError(
1555
+ (cleared.stderr or cleared.stdout).strip()
1556
+ or "could not clear stale worktree registration"
1557
+ )
1558
+ existing = run_git(primary, "show-ref", "--verify", "--quiet", ref)
1559
+ if existing.returncode == 0:
1560
+ deleted = run_git(primary, "update-ref", "-d", ref)
1561
+ if deleted.returncode != 0:
1562
+ raise IsolationError(
1563
+ (deleted.stderr or deleted.stdout).strip() or "git update-ref failed"
1564
+ )
1565
+ create_named_worktree(primary, branch, destination, resolved_base)
1566
+ return {
1567
+ "base": resolved_base,
1568
+ "branch": branch,
1569
+ "kind": "verify",
1570
+ "mode": "sidecar",
1571
+ "worktree": str(destination),
1572
+ }
1573
+
1574
+
1575
+ def reproduce_verify_sidecar(sidecar_path: Path, tree: str) -> None:
1576
+ """Copy the primary's current task tree into the verify sidecar (clean_verify's inverse)."""
1577
+ git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
1578
+ if git_output(sidecar_path, "write-tree") != tree:
1579
+ raise IsolationError("sidecar reproduction differs from the primary task diff")
1580
+
1581
+
1447
1582
  def _split_paths(block: str) -> Set[str]:
1448
1583
  return {line.strip() for line in block.splitlines() if line.strip()}
1449
1584
 
@@ -1451,7 +1586,7 @@ def _split_paths(block: str) -> Set[str]:
1451
1586
  # Top-level .project entries the pipeline owns; ship checks filter other entries by Git visibility.
1452
1587
  PROJECT_ENTRIES = frozenset({
1453
1588
  "STATE.md", "LESSONS.md", "REPOSITORY.md", "MEMBERS.md", "CHARTER.md", "ROADMAP.md",
1454
- "SYNTHESIS.md",
1589
+ "SYNTHESIS.md", "config.json", "model-policy.json",
1455
1590
  "intent", "research", "plan", "tasks", "review", "build", "discuss", "archive", "next",
1456
1591
  })
1457
1592
 
@@ -1556,14 +1691,24 @@ def _commit_pending(
1556
1691
 
1557
1692
 
1558
1693
  def validate_allowed_changes(
1559
- repo: Path, base: str, pending: Set[str], allowed: Set[str]
1694
+ repo: Path,
1695
+ base: str,
1696
+ pending: Set[str],
1697
+ allowed: Set[str],
1698
+ tolerated: Set[str] = frozenset(),
1560
1699
  ) -> None:
1561
- if not pending:
1700
+ # Tolerated paths (primary bookkeeping) may stay uncommitted; the next
1701
+ # `.project` checkpoint commits them.
1702
+ landing = pending - tolerated
1703
+ if not landing:
1562
1704
  raise IsolationError("no changes to land")
1563
- since_base = committed_paths_since(repo, base) | pending
1705
+ since_base = committed_paths_since(repo, base) | landing
1564
1706
  unexpected = sorted(path for path in since_base if path not in allowed)
1565
1707
  if unexpected:
1566
- raise IsolationError("unexpected paths: " + ", ".join(unexpected))
1708
+ raise IsolationError(
1709
+ "unexpected paths: " + ", ".join(unexpected)
1710
+ + "; landing allows only the task file and its declared files"
1711
+ )
1567
1712
  def _clean_blob_oid(repo: Path, path: str, text: str, *, write: bool) -> str:
1568
1713
  arguments = ["hash-object"]
1569
1714
  if write:
@@ -1583,21 +1728,23 @@ def commit_allowed_changes(
1583
1728
  subject: str,
1584
1729
  allowed: Set[str],
1585
1730
  body: str,
1731
+ tolerated: Set[str] = frozenset(),
1586
1732
  ) -> tuple[str, str, Dict[str, Optional[tuple[str, str]]]]:
1587
1733
  require_attached(repo)
1588
1734
  pending = uncommitted_paths(repo)
1589
- validate_allowed_changes(repo, base, pending, allowed)
1735
+ validate_allowed_changes(repo, base, pending, allowed, tolerated)
1736
+ staging = pending - tolerated
1590
1737
  reset = run_git(repo, "reset", "-q", "HEAD")
1591
1738
  if reset.returncode != 0:
1592
1739
  raise IsolationError("could not clear the index before landing")
1593
- for path in sorted(pending):
1740
+ for path in sorted(staging):
1594
1741
  added = run_git(repo, "add", "-A", "--", path)
1595
1742
  if added.returncode != 0:
1596
1743
  raise IsolationError(f"could not stage {path}")
1597
1744
  staged = _split_paths(
1598
1745
  git_output(repo, "diff", "--cached", "--name-only", "--relative")
1599
1746
  )
1600
- if staged != pending:
1747
+ if staged != staging:
1601
1748
  raise IsolationError("staged paths do not match the uncommitted change set")
1602
1749
  verified_tree = git_output(repo, "write-tree")
1603
1750
  verified_delta = _tree_delta(repo, base, verified_tree)
@@ -1743,6 +1890,17 @@ def activate_task(
1743
1890
  if task_branch is None:
1744
1891
  if branch.startswith(TASK_BRANCH_PREFIX):
1745
1892
  raise IsolationError("parallel task activation requires --task-branch")
1893
+ live = [
1894
+ path
1895
+ for path, ref in _registered_worktrees(worktree).items()
1896
+ if ref == f"refs/heads/{task_branch_name(task_id)}"
1897
+ and path != worktree
1898
+ ]
1899
+ if live:
1900
+ raise IsolationError(
1901
+ "task has an active parallel worktree; serial activation in "
1902
+ f"another worktree is refused (activate {live[0]} with --task-branch)"
1903
+ )
1746
1904
  elif task_branch != task_branch_name(task_id) or branch != task_branch:
1747
1905
  raise IsolationError("task activation branch does not match the task worktree")
1748
1906
  normalized_task = relative_posix(task_file)
@@ -1773,7 +1931,7 @@ def activate_task(
1773
1931
  if existing.returncode != 0 or existing.stdout.strip() != resolved_base:
1774
1932
  _replace_regular_file(task_path, text.encode("utf-8"), mode)
1775
1933
  raise IsolationError("could not record task worktree authorization")
1776
- return {
1934
+ result = {
1777
1935
  "agent": agent,
1778
1936
  "base": resolved_base,
1779
1937
  "status": "in-progress",
@@ -1781,6 +1939,30 @@ def activate_task(
1781
1939
  "task_file": normalized_task,
1782
1940
  "worktree": str(worktree),
1783
1941
  }
1942
+ _write_native_dispatch_record(worktree, result)
1943
+ return result
1944
+
1945
+
1946
+ def _write_native_dispatch_record(worktree: Path, activation: Dict[str, object]) -> None:
1947
+ try:
1948
+ if __package__:
1949
+ from . import dispatch_driver
1950
+ else:
1951
+ import dispatch_driver
1952
+ except ImportError:
1953
+ return
1954
+ dispatch_driver.record_native_activation(worktree, activation)
1955
+
1956
+
1957
+ def _write_native_member_dispatch_record(coordinator: Path, activation: Dict[str, object]) -> None:
1958
+ try:
1959
+ if __package__:
1960
+ from . import dispatch_driver
1961
+ else:
1962
+ import dispatch_driver
1963
+ except ImportError:
1964
+ return
1965
+ dispatch_driver.record_native_member_activation(coordinator, activation)
1784
1966
 
1785
1967
 
1786
1968
  def deactivate_task(
@@ -1899,6 +2081,59 @@ def stamp_task_landed(task_path: Path, base: str) -> None:
1899
2081
  )
1900
2082
 
1901
2083
 
2084
+ def block_serial_task(
2085
+ primary: Path, task_file: str, task_id: str, base: str
2086
+ ) -> Dict[str, str]:
2087
+ """Record an explicit blocked result on its still-active serial task."""
2088
+ primary = require_directory(primary, "primary worktree")
2089
+ if worktree_root(primary) != primary:
2090
+ raise IsolationError(f"primary is not its Git root: {primary}")
2091
+ normalized_task = relative_posix(task_file)
2092
+ if not normalized_task.startswith(".project/tasks/"):
2093
+ raise IsolationError("serial task file must stay under .project/tasks/")
2094
+ task_id = validate_task_id(task_id)
2095
+ expected_base = require_commit(primary, require_full_sha(base))
2096
+ task_path = _real_file(primary, normalized_task, "serial task file")
2097
+ text, mode = _read_task_text(task_path)
2098
+ fields, error = task_frontmatter(text)
2099
+ if error or fields is None:
2100
+ raise IsolationError(error or "serial task frontmatter is unreadable")
2101
+ if fields.get("id") != task_id:
2102
+ raise IsolationError("serial task id changed before blocked result")
2103
+ if fields.get("status") not in {"in-progress", "blocked"}:
2104
+ raise IsolationError("serial task is no longer active")
2105
+ if fields.get("base") != expected_base:
2106
+ raise IsolationError("serial task base changed before blocked result")
2107
+ if fields.get("task_branch") != "null":
2108
+ raise IsolationError("serial task records a task branch")
2109
+ recorded_worktree = fields.get("worktree")
2110
+ if not isinstance(recorded_worktree, str) or Path(recorded_worktree).resolve() != primary:
2111
+ raise IsolationError("serial task worktree changed before blocked result")
2112
+
2113
+ if fields.get("status") == "blocked":
2114
+ return {"task_id": task_id, "task_file": normalized_task,
2115
+ "base": expected_base, "status": "blocked"}
2116
+
2117
+ head, body = split_frontmatter(text)
2118
+ replaced = False
2119
+ lines = []
2120
+ for line in head:
2121
+ if _frontmatter_key(line) == "status":
2122
+ if replaced:
2123
+ raise IsolationError("serial task frontmatter repeats status")
2124
+ replaced = True
2125
+ lines.append("status: blocked")
2126
+ else:
2127
+ lines.append(line)
2128
+ if not replaced:
2129
+ raise IsolationError("serial task frontmatter is missing status")
2130
+ _replace_regular_file(
2131
+ task_path, ("---\n" + "\n".join(lines) + "\n---\n" + body).encode("utf-8"), mode
2132
+ )
2133
+ return {"task_id": task_id, "task_file": normalized_task,
2134
+ "base": expected_base, "status": "blocked"}
2135
+
2136
+
1902
2137
  def _restore_landing_state(
1903
2138
  worktree: Path, task_path: Path, task_bytes: bytes, task_mode: int, index_tree: str
1904
2139
  ) -> None:
@@ -1933,7 +2168,12 @@ def _restore_rejected_commit(
1933
2168
 
1934
2169
 
1935
2170
  def _stamp_and_commit(
1936
- worktree: Path, base: str, subject: str, task_file: str, allowed: Set[str]
2171
+ worktree: Path,
2172
+ base: str,
2173
+ subject: str,
2174
+ task_file: str,
2175
+ allowed: Set[str],
2176
+ tolerated: Set[str] = frozenset(),
1937
2177
  ) -> tuple[str, Dict[str, Optional[tuple[str, str]]]]:
1938
2178
  task_path = worktree / task_file
1939
2179
  task_text, task_mode = _read_task_text(task_path)
@@ -1954,14 +2194,14 @@ def _stamp_and_commit(
1954
2194
  pending = uncommitted_paths(worktree)
1955
2195
  if stamped != task_bytes:
1956
2196
  pending.add(task_file)
1957
- validate_allowed_changes(worktree, base, pending, allowed)
2197
+ validate_allowed_changes(worktree, base, pending, allowed, tolerated)
1958
2198
  original_head = current_sha(worktree)
1959
2199
  index_tree = git_output(worktree, "write-tree")
1960
2200
  try:
1961
2201
  _replace_regular_file(task_path, stamped, task_mode)
1962
- body = task_commit_body(task_file, uncommitted_paths(worktree), base)
2202
+ body = task_commit_body(task_file, uncommitted_paths(worktree) - tolerated, base)
1963
2203
  commit, verified_tree, verified_delta = commit_allowed_changes(
1964
- worktree, base, subject, allowed, body
2204
+ worktree, base, subject, allowed, body, tolerated
1965
2205
  )
1966
2206
  proof_error = _landing_commit_proof_error(
1967
2207
  worktree,
@@ -2039,6 +2279,7 @@ def land(
2039
2279
  subject,
2040
2280
  relative_posix(task_file),
2041
2281
  allowed,
2282
+ BOOKKEEPING_PATHS,
2042
2283
  )
2043
2284
  return {
2044
2285
  "bound_branch": bound,
@@ -2333,7 +2574,7 @@ def _landing_retry_error(
2333
2574
  if task_file not in pending:
2334
2575
  return "interrupted landing does not retain the task change"
2335
2576
  try:
2336
- validate_allowed_changes(repo, base, pending, contract_paths)
2577
+ validate_allowed_changes(repo, base, pending, contract_paths, BOOKKEEPING_PATHS)
2337
2578
  except IsolationError as validation_error:
2338
2579
  return str(validation_error)
2339
2580
  return None
@@ -2556,6 +2797,40 @@ def _resume_task_error(
2556
2797
  return _retained_task_contract_error(current_text, isolate_text)
2557
2798
 
2558
2799
 
2800
+ def _serial_bookkeeping_error(primary: Path, base: str, head: str) -> Optional[str]:
2801
+ """Prove that serial task history advanced only through linear .project commits."""
2802
+ if base == head:
2803
+ return None
2804
+ if run_git(primary, "merge-base", "--is-ancestor", base, head).returncode != 0:
2805
+ return "recorded base is not an ancestor of the current branch"
2806
+ commits = git_output(
2807
+ primary, "rev-list", "--first-parent", "--reverse", f"{base}..{head}"
2808
+ ).splitlines()
2809
+ previous = base
2810
+ for commit in commits:
2811
+ parents = git_output(primary, "rev-list", "--parents", "-n", "1", commit).split()
2812
+ if len(parents) != 2 or parents[1] != previous:
2813
+ return "history contains an unproven merge or commit sequence"
2814
+ paths = git_output(
2815
+ primary,
2816
+ "diff-tree",
2817
+ "--no-commit-id",
2818
+ "--name-only",
2819
+ "-r",
2820
+ "--no-renames",
2821
+ previous,
2822
+ commit,
2823
+ ).splitlines()
2824
+ if not paths:
2825
+ return "history contains an empty commit"
2826
+ if any(path != ".project" and not path.startswith(".project/") for path in paths):
2827
+ return "history contains changes outside .project"
2828
+ previous = commit
2829
+ if previous != head:
2830
+ return "history does not prove a first-parent path from the recorded base"
2831
+ return None
2832
+
2833
+
2559
2834
  MEMBER_RECORD_RE = re.compile(
2560
2835
  r"Task: (?P<task>\S+)\nBase: (?P<base>[0-9a-f]{40})\n"
2561
2836
  r"Member: (?P<member>\S+) (?P<landing>[0-9a-f]{40}) (?P<member_base>[0-9a-f]{40})"
@@ -2845,8 +3120,12 @@ def _recover_task(
2845
3120
  )
2846
3121
  except IsolationError as error:
2847
3122
  return result("block", reason=f"{status} task has invalid base: {error}")
2848
- if head != base:
2849
- return result("block", reason=f"{status} serial task advanced past its base")
3123
+ advance_error = _serial_bookkeeping_error(primary, base, head)
3124
+ if advance_error:
3125
+ return result(
3126
+ "block",
3127
+ reason=f"{status} serial task advanced past its base: {advance_error}",
3128
+ )
2850
3129
  primary_worktree, primary_error = _worktree_report(primary, primary, bound)
2851
3130
  if primary_error:
2852
3131
  return result("block", reason=primary_error)
@@ -2860,6 +3139,15 @@ def _recover_task(
2860
3139
  str(primary),
2861
3140
  "null",
2862
3141
  )
3142
+ if head != base and resume_error is None:
3143
+ return result(
3144
+ "reconcile",
3145
+ base=base,
3146
+ reason=(
3147
+ "serial task advanced through .project bookkeeping; reconcile the "
3148
+ "attempt and any retained Verify sidecar before resetting it to pending"
3149
+ ),
3150
+ )
2863
3151
  if resume_error:
2864
3152
  return result("block", reason=resume_error)
2865
3153
  return result("resume", base=base)
@@ -2888,8 +3176,12 @@ def _recover_task(
2888
3176
  if serial_retained:
2889
3177
  if str(fields.get("task_branch", "")) != "null":
2890
3178
  return result("block", reason=f"{status} serial task records a task branch")
2891
- if run_git(primary, "merge-base", "--is-ancestor", base, head).returncode != 0:
2892
- return result("block", reason=f"{status} serial task base is not an ancestor")
3179
+ advance_error = _serial_bookkeeping_error(primary, base, head)
3180
+ if advance_error:
3181
+ return result(
3182
+ "block",
3183
+ reason=f"{status} serial task advanced past its base: {advance_error}",
3184
+ )
2893
3185
  primary_worktree, primary_error = _worktree_report(primary, primary, bound)
2894
3186
  if primary_error:
2895
3187
  return result("block", reason=primary_error)
@@ -4321,6 +4613,7 @@ def retire(
4321
4613
  or f"could not delete {branch}"
4322
4614
  )
4323
4615
  branch_retired = True
4616
+ release_workspace_quietly(primary)
4324
4617
  return {
4325
4618
  "bound_branch": bound,
4326
4619
  "branch": branch,
@@ -4354,22 +4647,13 @@ def retire(
4354
4647
  raise IsolationError(
4355
4648
  "worktree is dirty; pass --force only from the retry-retirement path"
4356
4649
  )
4357
- remove = run_git(
4358
- primary,
4359
- "worktree",
4360
- "remove",
4361
- *(("--force",) if force else ()),
4362
- str(resolved_worktree),
4363
- )
4364
- if remove.returncode != 0:
4365
- raise IsolationError(
4366
- (remove.stderr or remove.stdout).strip() or "git worktree remove failed"
4367
- )
4650
+ _remove_worktree(primary, resolved_worktree, force)
4368
4651
  deleted = run_git(primary, "branch", "-D", retire_branch)
4369
4652
  if deleted.returncode != 0:
4370
4653
  raise IsolationError(
4371
4654
  (deleted.stderr or deleted.stdout).strip() or f"could not delete {retire_branch}"
4372
4655
  )
4656
+ release_workspace_quietly(primary)
4373
4657
  return {
4374
4658
  "bound_branch": bound,
4375
4659
  "branch": retire_branch,
@@ -4379,6 +4663,54 @@ def retire(
4379
4663
  }
4380
4664
 
4381
4665
 
4666
+ def retire_unused_verify(
4667
+ primary: Path, worktree: Path, base: str, branch: str
4668
+ ) -> Dict[str, object]:
4669
+ """Retire a Verify sidecar only when it is provably untouched since creation."""
4670
+ primary = require_directory(primary, "primary worktree")
4671
+ if worktree_root(primary) != primary:
4672
+ raise IsolationError(f"primary is not its Git root: {primary}")
4673
+ if not branch.startswith(VERIFY_BRANCH_PREFIX):
4674
+ raise IsolationError("unused Verify retirement requires a verify branch")
4675
+ expected = _sidecar_path_for_branch(primary, branch)
4676
+ resolved_worktree = worktree.resolve()
4677
+ if resolved_worktree != expected:
4678
+ raise IsolationError("unused Verify path differs from its branch")
4679
+ expected_base = require_commit(primary, require_full_sha(base))
4680
+
4681
+ if os.path.lexists(worktree):
4682
+ if worktree.is_symlink() or not worktree.is_dir() or worktree.resolve() != worktree:
4683
+ raise IsolationError(f"unused Verify path is not an owned worktree: {worktree}")
4684
+ if worktree_root(worktree) != worktree:
4685
+ raise IsolationError(f"unused Verify path is not its Git root: {worktree}")
4686
+ if common_git_dir(primary) != common_git_dir(worktree):
4687
+ raise IsolationError("unused Verify sidecar belongs to another repository")
4688
+ if require_attached(worktree) != branch:
4689
+ raise IsolationError("unused Verify sidecar branch changed")
4690
+ if current_sha(worktree) != expected_base:
4691
+ raise IsolationError("unused Verify sidecar HEAD changed; preserving it")
4692
+ dirty = git_output(
4693
+ worktree,
4694
+ "status",
4695
+ "--porcelain",
4696
+ "--untracked-files=all",
4697
+ "--ignored=matching",
4698
+ )
4699
+ if dirty:
4700
+ raise IsolationError("unused Verify sidecar has uncommitted files; preserving it")
4701
+ else:
4702
+ branch_head = run_git(
4703
+ primary, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"
4704
+ )
4705
+ if branch_head.returncode == 0 and branch_head.stdout.strip() != expected_base:
4706
+ raise IsolationError("unused Verify branch HEAD changed; preserving it")
4707
+ for registered_path, registered_branch in _registered_worktrees(primary).items():
4708
+ if registered_branch == f"refs/heads/{branch}" and registered_path != expected:
4709
+ raise IsolationError("unused Verify branch is checked out at another path")
4710
+
4711
+ return retire(primary, expected, branch, False)
4712
+
4713
+
4382
4714
  def parser() -> argparse.ArgumentParser:
4383
4715
  argument_parser = argparse.ArgumentParser(description=__doc__)
4384
4716
  subparsers = argument_parser.add_subparsers(dest="command", required=True)