@opengsd/gsd-path 1.4.0 → 1.6.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 (476) hide show
  1. package/CHANGELOG.md +460 -341
  2. package/DOCS.md +16 -5
  3. package/FULL.md +67 -6
  4. package/HOOKS.md +158 -10
  5. package/MIGRATE.md +12 -0
  6. package/QUICK.md +4 -2
  7. package/README.md +136 -116
  8. package/RUNTIME.md +24 -6
  9. package/UPDATE.md +39 -3
  10. package/WORKFLOW.md +80 -4
  11. package/package.json +5 -2
  12. package/platforms/shared-agents/dispatch.md +13 -0
  13. package/scripts/_common.py +256 -2
  14. package/scripts/archive_milestone.py +14 -13
  15. package/scripts/build_recovery.py +155 -58
  16. package/scripts/build_state.py +274 -8
  17. package/scripts/check_docs_audit.py +77 -4
  18. package/scripts/check_handoffs.py +635 -57
  19. package/scripts/check_task_briefs.py +293 -32
  20. package/scripts/detect_project.py +260 -20
  21. package/scripts/discussion_records.py +9 -1
  22. package/scripts/discussion_validate.py +93 -35
  23. package/scripts/dispatch_driver.py +549 -53
  24. package/scripts/git_guard.py +27 -8
  25. package/scripts/guard_hook.py +2630 -210
  26. package/scripts/install.mjs +99 -11
  27. package/scripts/install.py +231 -46
  28. package/scripts/integration.py +244 -65
  29. package/scripts/isolation.py +539 -70
  30. package/scripts/loop_run.py +16 -4
  31. package/scripts/members.py +77 -32
  32. package/scripts/migrate_core.py +2 -0
  33. package/scripts/model_policy.py +5 -1
  34. package/scripts/path_config.py +3 -3
  35. package/scripts/pipeline_diagnose.py +17 -2
  36. package/scripts/pipeline_git.py +131 -42
  37. package/scripts/pipeline_state.py +139 -39
  38. package/scripts/pipeline_undo.py +118 -16
  39. package/scripts/promote_lookahead.py +3 -3
  40. package/scripts/review_findings.py +180 -44
  41. package/scripts/roadmap.py +9 -1
  42. package/scripts/runtime_store.py +21 -2
  43. package/scripts/skill-resources.json +9 -2
  44. package/scripts/state_checkpoint.py +39 -36
  45. package/scripts/state_promote.py +15 -4
  46. package/scripts/status_runtime.py +22 -4
  47. package/scripts/task_context.py +14 -3
  48. package/scripts/wizard.mjs +1 -0
  49. package/scripts/workflow_run.py +14 -4
  50. package/scripts/worktree_paths.py +54 -2
  51. package/skills/gsd-path/DEFINE.md +34 -5
  52. package/skills/gsd-path/PLAN.md +14 -3
  53. package/skills/gsd-path/RESEARCH.md +11 -0
  54. package/skills/gsd-path/ROADMAP.md +16 -6
  55. package/skills/gsd-path/SHIP.md +32 -5
  56. package/skills/gsd-path/SKILL.md +52 -24
  57. package/skills/gsd-path/references/build-native.md +112 -10
  58. package/skills/gsd-path/references/build-recovery.md +13 -0
  59. package/skills/gsd-path/references/coder.md +7 -3
  60. package/skills/gsd-path/references/dispatch.md +13 -0
  61. package/skills/gsd-path/references/docs-auditor.md +5 -0
  62. package/skills/gsd-path/references/operating-rules.md +11 -3
  63. package/skills/gsd-path/references/planner.md +26 -4
  64. package/skills/gsd-path/references/reviewer-final.md +20 -2
  65. package/skills/gsd-path/references/reviewer-wave.md +56 -12
  66. package/skills/gsd-path/references/reviewer.md +3 -1
  67. package/skills/gsd-path/references/runtime-selection.md +9 -0
  68. package/skills/gsd-path/references/spec-probes.md +150 -0
  69. package/skills/gsd-path/scripts/_common.py +256 -2
  70. package/skills/gsd-path/scripts/archive_milestone.py +14 -13
  71. package/skills/gsd-path/scripts/build_recovery.py +155 -58
  72. package/skills/gsd-path/scripts/build_state.py +274 -8
  73. package/skills/gsd-path/scripts/check_docs_audit.py +77 -4
  74. package/skills/gsd-path/scripts/check_handoffs.py +635 -57
  75. package/skills/gsd-path/scripts/check_task_briefs.py +293 -32
  76. package/skills/gsd-path/scripts/detect_project.py +260 -20
  77. package/skills/gsd-path/scripts/discussion_records.py +9 -1
  78. package/skills/gsd-path/scripts/discussion_validate.py +93 -35
  79. package/skills/gsd-path/scripts/dispatch_driver.py +549 -53
  80. package/skills/gsd-path/scripts/git_guard.py +27 -8
  81. package/skills/gsd-path/scripts/integration.py +244 -65
  82. package/skills/gsd-path/scripts/isolation.py +539 -70
  83. package/skills/gsd-path/scripts/loop_run.py +16 -4
  84. package/skills/gsd-path/scripts/members.py +77 -32
  85. package/skills/gsd-path/scripts/model_policy.py +5 -1
  86. package/skills/gsd-path/scripts/path_config.py +3 -3
  87. package/skills/gsd-path/scripts/pipeline_git.py +131 -42
  88. package/skills/gsd-path/scripts/pipeline_state.py +139 -39
  89. package/skills/gsd-path/scripts/promote_lookahead.py +3 -3
  90. package/skills/gsd-path/scripts/review_findings.py +180 -44
  91. package/skills/gsd-path/scripts/roadmap.py +9 -1
  92. package/skills/gsd-path/scripts/state_checkpoint.py +39 -36
  93. package/skills/gsd-path/scripts/state_promote.py +15 -4
  94. package/skills/gsd-path/scripts/status_runtime.py +22 -4
  95. package/skills/gsd-path/scripts/task_context.py +14 -3
  96. package/skills/gsd-path/scripts/workflow_run.py +14 -4
  97. package/skills/gsd-path/scripts/worktree_paths.py +54 -2
  98. package/skills/gsd-path/templates/docs-audit.md +5 -1
  99. package/skills/gsd-path/templates/final-review.md +11 -2
  100. package/skills/gsd-path/templates/intent.md +30 -0
  101. package/skills/gsd-path/templates/patch-findings.md +3 -3
  102. package/skills/gsd-path/templates/plan.md +17 -2
  103. package/skills/gsd-path/templates/research-handoff.md +14 -3
  104. package/skills/gsd-path/templates/state.md +2 -1
  105. package/skills/gsd-path/templates/task.md +1 -0
  106. package/skills/gsd-path/templates/wave-review.md +28 -9
  107. package/skills/gsd-path-build/references/build-native.md +112 -10
  108. package/skills/gsd-path-build/references/build-recovery.md +13 -0
  109. package/skills/gsd-path-build/references/coder.md +7 -3
  110. package/skills/gsd-path-build/references/dispatch.md +13 -0
  111. package/skills/gsd-path-build/references/operating-rules.md +11 -3
  112. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  113. package/skills/gsd-path-build/references/reviewer-wave.md +56 -12
  114. package/skills/gsd-path-build/references/reviewer.md +3 -1
  115. package/skills/gsd-path-build/references/runtime-selection.md +9 -0
  116. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  117. package/skills/gsd-path-build/scripts/_common.py +256 -2
  118. package/skills/gsd-path-build/scripts/archive_milestone.py +14 -13
  119. package/skills/gsd-path-build/scripts/build_recovery.py +155 -58
  120. package/skills/gsd-path-build/scripts/build_state.py +274 -8
  121. package/skills/gsd-path-build/scripts/check_handoffs.py +635 -57
  122. package/skills/gsd-path-build/scripts/check_task_briefs.py +293 -32
  123. package/skills/gsd-path-build/scripts/detect_project.py +260 -20
  124. package/skills/gsd-path-build/scripts/discussion_records.py +9 -1
  125. package/skills/gsd-path-build/scripts/discussion_validate.py +93 -35
  126. package/skills/gsd-path-build/scripts/dispatch_driver.py +549 -53
  127. package/skills/gsd-path-build/scripts/integration.py +244 -65
  128. package/skills/gsd-path-build/scripts/isolation.py +539 -70
  129. package/skills/gsd-path-build/scripts/loop_run.py +16 -4
  130. package/skills/gsd-path-build/scripts/members.py +77 -32
  131. package/skills/gsd-path-build/scripts/model_policy.py +5 -1
  132. package/skills/gsd-path-build/scripts/path_config.py +3 -3
  133. package/skills/gsd-path-build/scripts/pipeline_git.py +131 -42
  134. package/skills/gsd-path-build/scripts/pipeline_state.py +139 -39
  135. package/skills/gsd-path-build/scripts/promote_lookahead.py +3 -3
  136. package/skills/gsd-path-build/scripts/review_findings.py +180 -44
  137. package/skills/gsd-path-build/scripts/roadmap.py +9 -1
  138. package/skills/gsd-path-build/scripts/state_checkpoint.py +39 -36
  139. package/skills/gsd-path-build/scripts/state_promote.py +15 -4
  140. package/skills/gsd-path-build/scripts/task_context.py +14 -3
  141. package/skills/gsd-path-build/scripts/workflow_run.py +14 -4
  142. package/skills/gsd-path-build/scripts/worktree_paths.py +54 -2
  143. package/skills/gsd-path-build/templates/task.md +1 -0
  144. package/skills/gsd-path-build/templates/wave-review.md +28 -9
  145. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  146. package/skills/gsd-path-decide/references/operating-rules.md +11 -3
  147. package/skills/gsd-path-decide/references/runtime-selection.md +9 -0
  148. package/skills/gsd-path-decide/scripts/_common.py +256 -2
  149. package/skills/gsd-path-decide/scripts/archive_milestone.py +14 -13
  150. package/skills/gsd-path-decide/scripts/build_recovery.py +155 -58
  151. package/skills/gsd-path-decide/scripts/check_handoffs.py +635 -57
  152. package/skills/gsd-path-decide/scripts/check_task_briefs.py +293 -32
  153. package/skills/gsd-path-decide/scripts/detect_project.py +260 -20
  154. package/skills/gsd-path-decide/scripts/discussion_records.py +9 -1
  155. package/skills/gsd-path-decide/scripts/discussion_validate.py +93 -35
  156. package/skills/gsd-path-decide/scripts/integration.py +244 -65
  157. package/skills/gsd-path-decide/scripts/isolation.py +539 -70
  158. package/skills/gsd-path-decide/scripts/loop_run.py +16 -4
  159. package/skills/gsd-path-decide/scripts/members.py +77 -32
  160. package/skills/gsd-path-decide/scripts/model_policy.py +5 -1
  161. package/skills/gsd-path-decide/scripts/path_config.py +3 -3
  162. package/skills/gsd-path-decide/scripts/pipeline_git.py +131 -42
  163. package/skills/gsd-path-decide/scripts/pipeline_state.py +139 -39
  164. package/skills/gsd-path-decide/scripts/promote_lookahead.py +3 -3
  165. package/skills/gsd-path-decide/scripts/roadmap.py +9 -1
  166. package/skills/gsd-path-decide/scripts/state_checkpoint.py +39 -36
  167. package/skills/gsd-path-decide/scripts/state_promote.py +15 -4
  168. package/skills/gsd-path-decide/scripts/worktree_paths.py +54 -2
  169. package/skills/gsd-path-define/SKILL.md +34 -5
  170. package/skills/gsd-path-define/references/build-recovery.md +13 -0
  171. package/skills/gsd-path-define/references/operating-rules.md +11 -3
  172. package/skills/gsd-path-define/references/runtime-selection.md +9 -0
  173. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  174. package/skills/gsd-path-define/scripts/_common.py +256 -2
  175. package/skills/gsd-path-define/scripts/archive_milestone.py +14 -13
  176. package/skills/gsd-path-define/scripts/build_recovery.py +155 -58
  177. package/skills/gsd-path-define/scripts/check_handoffs.py +635 -57
  178. package/skills/gsd-path-define/scripts/check_task_briefs.py +293 -32
  179. package/skills/gsd-path-define/scripts/detect_project.py +260 -20
  180. package/skills/gsd-path-define/scripts/discussion_records.py +9 -1
  181. package/skills/gsd-path-define/scripts/discussion_validate.py +93 -35
  182. package/skills/gsd-path-define/scripts/integration.py +244 -65
  183. package/skills/gsd-path-define/scripts/isolation.py +539 -70
  184. package/skills/gsd-path-define/scripts/loop_run.py +16 -4
  185. package/skills/gsd-path-define/scripts/members.py +77 -32
  186. package/skills/gsd-path-define/scripts/model_policy.py +5 -1
  187. package/skills/gsd-path-define/scripts/path_config.py +3 -3
  188. package/skills/gsd-path-define/scripts/pipeline_git.py +131 -42
  189. package/skills/gsd-path-define/scripts/pipeline_state.py +139 -39
  190. package/skills/gsd-path-define/scripts/promote_lookahead.py +3 -3
  191. package/skills/gsd-path-define/scripts/roadmap.py +9 -1
  192. package/skills/gsd-path-define/scripts/state_checkpoint.py +39 -36
  193. package/skills/gsd-path-define/scripts/state_promote.py +15 -4
  194. package/skills/gsd-path-define/scripts/worktree_paths.py +54 -2
  195. package/skills/gsd-path-define/templates/intent.md +30 -0
  196. package/skills/gsd-path-define/templates/state.md +2 -1
  197. package/skills/gsd-path-discuss/references/operating-rules.md +11 -3
  198. package/skills/gsd-path-discuss/references/runtime-selection.md +9 -0
  199. package/skills/gsd-path-discuss/scripts/_common.py +256 -2
  200. package/skills/gsd-path-discuss/scripts/discussion_records.py +9 -1
  201. package/skills/gsd-path-discuss/scripts/loop_run.py +16 -4
  202. package/skills/gsd-path-discuss/scripts/model_policy.py +5 -1
  203. package/skills/gsd-path-discuss/scripts/path_config.py +3 -3
  204. package/skills/gsd-path-discuss/scripts/worktree_paths.py +54 -2
  205. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  206. package/skills/gsd-path-docs-audit/references/docs-auditor.md +5 -0
  207. package/skills/gsd-path-docs-audit/references/operating-rules.md +11 -3
  208. package/skills/gsd-path-docs-audit/references/runtime-selection.md +9 -0
  209. package/skills/gsd-path-docs-audit/scripts/_common.py +256 -2
  210. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +14 -13
  211. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +155 -58
  212. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +77 -4
  213. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +635 -57
  214. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +293 -32
  215. package/skills/gsd-path-docs-audit/scripts/detect_project.py +260 -20
  216. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +9 -1
  217. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +93 -35
  218. package/skills/gsd-path-docs-audit/scripts/integration.py +244 -65
  219. package/skills/gsd-path-docs-audit/scripts/isolation.py +539 -70
  220. package/skills/gsd-path-docs-audit/scripts/loop_run.py +16 -4
  221. package/skills/gsd-path-docs-audit/scripts/members.py +77 -32
  222. package/skills/gsd-path-docs-audit/scripts/model_policy.py +5 -1
  223. package/skills/gsd-path-docs-audit/scripts/path_config.py +3 -3
  224. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +131 -42
  225. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +139 -39
  226. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +3 -3
  227. package/skills/gsd-path-docs-audit/scripts/roadmap.py +9 -1
  228. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +39 -36
  229. package/skills/gsd-path-docs-audit/scripts/state_promote.py +15 -4
  230. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +54 -2
  231. package/skills/gsd-path-docs-audit/templates/docs-audit.md +5 -1
  232. package/skills/gsd-path-forensics/references/operating-rules.md +11 -3
  233. package/skills/gsd-path-forensics/references/runtime-selection.md +9 -0
  234. package/skills/gsd-path-forensics/scripts/_common.py +256 -2
  235. package/skills/gsd-path-forensics/scripts/discussion_records.py +9 -1
  236. package/skills/gsd-path-forensics/scripts/loop_run.py +16 -4
  237. package/skills/gsd-path-forensics/scripts/model_policy.py +5 -1
  238. package/skills/gsd-path-forensics/scripts/path_config.py +3 -3
  239. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +17 -2
  240. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +118 -16
  241. package/skills/gsd-path-forensics/scripts/worktree_paths.py +54 -2
  242. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  243. package/skills/gsd-path-inspect/references/docs-auditor.md +5 -0
  244. package/skills/gsd-path-inspect/references/operating-rules.md +11 -3
  245. package/skills/gsd-path-inspect/references/runtime-selection.md +9 -0
  246. package/skills/gsd-path-inspect/scripts/_common.py +256 -2
  247. package/skills/gsd-path-inspect/scripts/archive_milestone.py +14 -13
  248. package/skills/gsd-path-inspect/scripts/build_recovery.py +155 -58
  249. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +77 -4
  250. package/skills/gsd-path-inspect/scripts/check_handoffs.py +635 -57
  251. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +293 -32
  252. package/skills/gsd-path-inspect/scripts/detect_project.py +260 -20
  253. package/skills/gsd-path-inspect/scripts/discussion_records.py +9 -1
  254. package/skills/gsd-path-inspect/scripts/discussion_validate.py +93 -35
  255. package/skills/gsd-path-inspect/scripts/integration.py +244 -65
  256. package/skills/gsd-path-inspect/scripts/isolation.py +539 -70
  257. package/skills/gsd-path-inspect/scripts/loop_run.py +16 -4
  258. package/skills/gsd-path-inspect/scripts/members.py +77 -32
  259. package/skills/gsd-path-inspect/scripts/model_policy.py +5 -1
  260. package/skills/gsd-path-inspect/scripts/path_config.py +3 -3
  261. package/skills/gsd-path-inspect/scripts/pipeline_git.py +131 -42
  262. package/skills/gsd-path-inspect/scripts/pipeline_state.py +139 -39
  263. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +3 -3
  264. package/skills/gsd-path-inspect/scripts/roadmap.py +9 -1
  265. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +39 -36
  266. package/skills/gsd-path-inspect/scripts/state_promote.py +15 -4
  267. package/skills/gsd-path-inspect/scripts/workflow_run.py +14 -4
  268. package/skills/gsd-path-inspect/scripts/worktree_paths.py +54 -2
  269. package/skills/gsd-path-inspect/templates/docs-audit.md +5 -1
  270. package/skills/gsd-path-inspect/templates/state.md +2 -1
  271. package/skills/gsd-path-loop/references/operating-rules.md +11 -3
  272. package/skills/gsd-path-loop/references/runtime-selection.md +9 -0
  273. package/skills/gsd-path-loop/scripts/_common.py +256 -2
  274. package/skills/gsd-path-loop/scripts/discussion_records.py +9 -1
  275. package/skills/gsd-path-loop/scripts/loop_run.py +16 -4
  276. package/skills/gsd-path-loop/scripts/model_policy.py +5 -1
  277. package/skills/gsd-path-loop/scripts/path_config.py +3 -3
  278. package/skills/gsd-path-loop/scripts/worktree_paths.py +54 -2
  279. package/skills/gsd-path-migrate/SKILL.md +8 -0
  280. package/skills/gsd-path-migrate/references/operating-rules.md +11 -3
  281. package/skills/gsd-path-migrate/references/runtime-selection.md +9 -0
  282. package/skills/gsd-path-migrate/scripts/_common.py +256 -2
  283. package/skills/gsd-path-migrate/scripts/discussion_records.py +9 -1
  284. package/skills/gsd-path-migrate/scripts/loop_run.py +16 -4
  285. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -0
  286. package/skills/gsd-path-migrate/scripts/model_policy.py +5 -1
  287. package/skills/gsd-path-migrate/scripts/path_config.py +3 -3
  288. package/skills/gsd-path-plan/SKILL.md +14 -3
  289. package/skills/gsd-path-plan/references/build-recovery.md +13 -0
  290. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  291. package/skills/gsd-path-plan/references/operating-rules.md +11 -3
  292. package/skills/gsd-path-plan/references/planner.md +26 -4
  293. package/skills/gsd-path-plan/references/runtime-selection.md +9 -0
  294. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  295. package/skills/gsd-path-plan/scripts/_common.py +256 -2
  296. package/skills/gsd-path-plan/scripts/archive_milestone.py +14 -13
  297. package/skills/gsd-path-plan/scripts/build_recovery.py +155 -58
  298. package/skills/gsd-path-plan/scripts/check_handoffs.py +635 -57
  299. package/skills/gsd-path-plan/scripts/check_task_briefs.py +293 -32
  300. package/skills/gsd-path-plan/scripts/detect_project.py +260 -20
  301. package/skills/gsd-path-plan/scripts/discussion_records.py +9 -1
  302. package/skills/gsd-path-plan/scripts/discussion_validate.py +93 -35
  303. package/skills/gsd-path-plan/scripts/integration.py +244 -65
  304. package/skills/gsd-path-plan/scripts/isolation.py +539 -70
  305. package/skills/gsd-path-plan/scripts/loop_run.py +16 -4
  306. package/skills/gsd-path-plan/scripts/members.py +77 -32
  307. package/skills/gsd-path-plan/scripts/model_policy.py +5 -1
  308. package/skills/gsd-path-plan/scripts/path_config.py +3 -3
  309. package/skills/gsd-path-plan/scripts/pipeline_git.py +131 -42
  310. package/skills/gsd-path-plan/scripts/pipeline_state.py +139 -39
  311. package/skills/gsd-path-plan/scripts/promote_lookahead.py +3 -3
  312. package/skills/gsd-path-plan/scripts/roadmap.py +9 -1
  313. package/skills/gsd-path-plan/scripts/state_checkpoint.py +39 -36
  314. package/skills/gsd-path-plan/scripts/state_promote.py +15 -4
  315. package/skills/gsd-path-plan/scripts/workflow_run.py +14 -4
  316. package/skills/gsd-path-plan/scripts/worktree_paths.py +54 -2
  317. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  318. package/skills/gsd-path-plan/templates/plan.md +17 -2
  319. package/skills/gsd-path-plan/templates/task.md +1 -0
  320. package/skills/gsd-path-research/SKILL.md +11 -0
  321. package/skills/gsd-path-research/references/dispatch.md +13 -0
  322. package/skills/gsd-path-research/references/operating-rules.md +11 -3
  323. package/skills/gsd-path-research/references/runtime-selection.md +9 -0
  324. package/skills/gsd-path-research/scripts/_common.py +256 -2
  325. package/skills/gsd-path-research/scripts/archive_milestone.py +14 -13
  326. package/skills/gsd-path-research/scripts/build_recovery.py +155 -58
  327. package/skills/gsd-path-research/scripts/check_handoffs.py +635 -57
  328. package/skills/gsd-path-research/scripts/check_task_briefs.py +293 -32
  329. package/skills/gsd-path-research/scripts/detect_project.py +260 -20
  330. package/skills/gsd-path-research/scripts/discussion_records.py +9 -1
  331. package/skills/gsd-path-research/scripts/discussion_validate.py +93 -35
  332. package/skills/gsd-path-research/scripts/integration.py +244 -65
  333. package/skills/gsd-path-research/scripts/isolation.py +539 -70
  334. package/skills/gsd-path-research/scripts/loop_run.py +16 -4
  335. package/skills/gsd-path-research/scripts/members.py +77 -32
  336. package/skills/gsd-path-research/scripts/model_policy.py +5 -1
  337. package/skills/gsd-path-research/scripts/path_config.py +3 -3
  338. package/skills/gsd-path-research/scripts/pipeline_git.py +131 -42
  339. package/skills/gsd-path-research/scripts/pipeline_state.py +139 -39
  340. package/skills/gsd-path-research/scripts/promote_lookahead.py +3 -3
  341. package/skills/gsd-path-research/scripts/roadmap.py +9 -1
  342. package/skills/gsd-path-research/scripts/state_checkpoint.py +39 -36
  343. package/skills/gsd-path-research/scripts/state_promote.py +15 -4
  344. package/skills/gsd-path-research/scripts/worktree_paths.py +54 -2
  345. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  346. package/skills/gsd-path-roadmap/SKILL.md +16 -6
  347. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  348. package/skills/gsd-path-roadmap/references/operating-rules.md +11 -3
  349. package/skills/gsd-path-roadmap/references/runtime-selection.md +9 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +256 -2
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +14 -13
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +155 -58
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +635 -57
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +293 -32
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +260 -20
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +9 -1
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +93 -35
  358. package/skills/gsd-path-roadmap/scripts/integration.py +244 -65
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +539 -70
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +16 -4
  361. package/skills/gsd-path-roadmap/scripts/members.py +77 -32
  362. package/skills/gsd-path-roadmap/scripts/model_policy.py +5 -1
  363. package/skills/gsd-path-roadmap/scripts/path_config.py +3 -3
  364. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +131 -42
  365. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +139 -39
  366. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +3 -3
  367. package/skills/gsd-path-roadmap/scripts/roadmap.py +9 -1
  368. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +39 -36
  369. package/skills/gsd-path-roadmap/scripts/state_promote.py +15 -4
  370. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +54 -2
  371. package/skills/gsd-path-ship/SKILL.md +32 -5
  372. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  373. package/skills/gsd-path-ship/references/operating-rules.md +11 -3
  374. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  375. package/skills/gsd-path-ship/references/reviewer-wave.md +56 -12
  376. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  377. package/skills/gsd-path-ship/references/runtime-selection.md +9 -0
  378. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  379. package/skills/gsd-path-ship/scripts/_common.py +256 -2
  380. package/skills/gsd-path-ship/scripts/archive_milestone.py +14 -13
  381. package/skills/gsd-path-ship/scripts/build_recovery.py +155 -58
  382. package/skills/gsd-path-ship/scripts/build_state.py +274 -8
  383. package/skills/gsd-path-ship/scripts/check_handoffs.py +635 -57
  384. package/skills/gsd-path-ship/scripts/check_task_briefs.py +293 -32
  385. package/skills/gsd-path-ship/scripts/detect_project.py +260 -20
  386. package/skills/gsd-path-ship/scripts/discussion_records.py +9 -1
  387. package/skills/gsd-path-ship/scripts/discussion_validate.py +93 -35
  388. package/skills/gsd-path-ship/scripts/integration.py +244 -65
  389. package/skills/gsd-path-ship/scripts/isolation.py +539 -70
  390. package/skills/gsd-path-ship/scripts/loop_run.py +16 -4
  391. package/skills/gsd-path-ship/scripts/members.py +77 -32
  392. package/skills/gsd-path-ship/scripts/model_policy.py +5 -1
  393. package/skills/gsd-path-ship/scripts/path_config.py +3 -3
  394. package/skills/gsd-path-ship/scripts/pipeline_git.py +131 -42
  395. package/skills/gsd-path-ship/scripts/pipeline_state.py +139 -39
  396. package/skills/gsd-path-ship/scripts/promote_lookahead.py +3 -3
  397. package/skills/gsd-path-ship/scripts/roadmap.py +9 -1
  398. package/skills/gsd-path-ship/scripts/state_checkpoint.py +39 -36
  399. package/skills/gsd-path-ship/scripts/state_promote.py +15 -4
  400. package/skills/gsd-path-ship/scripts/workflow_run.py +14 -4
  401. package/skills/gsd-path-ship/scripts/worktree_paths.py +54 -2
  402. package/skills/gsd-path-ship/templates/final-review.md +11 -2
  403. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  404. package/skills/gsd-path-ship/templates/wave-review.md +28 -9
  405. package/skills/gsd-path-undo/references/operating-rules.md +11 -3
  406. package/skills/gsd-path-undo/references/runtime-selection.md +9 -0
  407. package/skills/gsd-path-undo/scripts/_common.py +256 -2
  408. package/skills/gsd-path-undo/scripts/discussion_records.py +9 -1
  409. package/skills/gsd-path-undo/scripts/loop_run.py +16 -4
  410. package/skills/gsd-path-undo/scripts/model_policy.py +5 -1
  411. package/skills/gsd-path-undo/scripts/path_config.py +3 -3
  412. package/skills/gsd-path-undo/scripts/pipeline_undo.py +118 -16
  413. package/skills/gsd-path-undo/scripts/worktree_paths.py +54 -2
  414. package/skills/path/DEFINE.md +34 -5
  415. package/skills/path/PLAN.md +14 -3
  416. package/skills/path/RESEARCH.md +11 -0
  417. package/skills/path/ROADMAP.md +16 -6
  418. package/skills/path/SHIP.md +32 -5
  419. package/skills/path/SKILL.md +52 -24
  420. package/skills/path/references/build-native.md +112 -10
  421. package/skills/path/references/build-recovery.md +13 -0
  422. package/skills/path/references/coder.md +7 -3
  423. package/skills/path/references/dispatch.md +13 -0
  424. package/skills/path/references/docs-auditor.md +5 -0
  425. package/skills/path/references/operating-rules.md +11 -3
  426. package/skills/path/references/planner.md +26 -4
  427. package/skills/path/references/reviewer-final.md +20 -2
  428. package/skills/path/references/reviewer-wave.md +56 -12
  429. package/skills/path/references/reviewer.md +3 -1
  430. package/skills/path/references/runtime-selection.md +9 -0
  431. package/skills/path/references/spec-probes.md +150 -0
  432. package/skills/path/scripts/_common.py +256 -2
  433. package/skills/path/scripts/archive_milestone.py +14 -13
  434. package/skills/path/scripts/build_recovery.py +155 -58
  435. package/skills/path/scripts/build_state.py +274 -8
  436. package/skills/path/scripts/check_docs_audit.py +77 -4
  437. package/skills/path/scripts/check_handoffs.py +635 -57
  438. package/skills/path/scripts/check_task_briefs.py +293 -32
  439. package/skills/path/scripts/detect_project.py +260 -20
  440. package/skills/path/scripts/discussion_records.py +9 -1
  441. package/skills/path/scripts/discussion_validate.py +93 -35
  442. package/skills/path/scripts/dispatch_driver.py +549 -53
  443. package/skills/path/scripts/git_guard.py +27 -8
  444. package/skills/path/scripts/integration.py +244 -65
  445. package/skills/path/scripts/isolation.py +539 -70
  446. package/skills/path/scripts/loop_run.py +16 -4
  447. package/skills/path/scripts/members.py +77 -32
  448. package/skills/path/scripts/model_policy.py +5 -1
  449. package/skills/path/scripts/path_config.py +3 -3
  450. package/skills/path/scripts/pipeline_git.py +131 -42
  451. package/skills/path/scripts/pipeline_state.py +139 -39
  452. package/skills/path/scripts/promote_lookahead.py +3 -3
  453. package/skills/path/scripts/review_findings.py +180 -44
  454. package/skills/path/scripts/roadmap.py +9 -1
  455. package/skills/path/scripts/state_checkpoint.py +39 -36
  456. package/skills/path/scripts/state_promote.py +15 -4
  457. package/skills/path/scripts/status_runtime.py +22 -4
  458. package/skills/path/scripts/task_context.py +14 -3
  459. package/skills/path/scripts/workflow_run.py +14 -4
  460. package/skills/path/scripts/worktree_paths.py +54 -2
  461. package/skills/path/templates/docs-audit.md +5 -1
  462. package/skills/path/templates/final-review.md +11 -2
  463. package/skills/path/templates/intent.md +30 -0
  464. package/skills/path/templates/patch-findings.md +3 -3
  465. package/skills/path/templates/plan.md +17 -2
  466. package/skills/path/templates/research-handoff.md +14 -3
  467. package/skills/path/templates/state.md +2 -1
  468. package/skills/path/templates/task.md +1 -0
  469. package/skills/path/templates/wave-review.md +28 -9
  470. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  471. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  472. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  473. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  474. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  475. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -11,10 +11,12 @@ from __future__ import annotations
11
11
 
12
12
  import argparse
13
13
  import contextlib
14
+ import functools
14
15
  import hashlib
15
16
  import io
16
17
  import json
17
18
  import shlex
19
+ import shutil
18
20
  import os
19
21
  import re
20
22
  import stat
@@ -27,11 +29,6 @@ import tempfile
27
29
  from pathlib import Path, PurePosixPath
28
30
  from typing import Dict, Iterator, Optional, Sequence, Set, Tuple
29
31
 
30
- if sys.platform == "win32":
31
- import msvcrt
32
- else:
33
- import fcntl
34
-
35
32
  if __package__:
36
33
  from .pipeline_git import (
37
34
  attest_commit_body,
@@ -107,6 +104,10 @@ class IsolationError(RuntimeError):
107
104
  """Raised when isolation, landing, or retirement cannot proceed."""
108
105
 
109
106
 
107
+ class RemovalError(IsolationError):
108
+ """Raised when the removal of a sidecar worktree fails."""
109
+
110
+
110
111
  run_git = _common.run_git
111
112
 
112
113
 
@@ -150,7 +151,10 @@ def require_commit(repo: Path, value: str) -> str:
150
151
 
151
152
  def require_full_sha(value: str) -> str:
152
153
  if not re.fullmatch(r"[0-9a-f]{40}", value):
153
- raise IsolationError(f"base must be a full 40-character SHA: {value}")
154
+ raise IsolationError(
155
+ f"base must be a full 40-character SHA: {value}; "
156
+ "resolve a ref or a short SHA with git rev-parse first"
157
+ )
154
158
  return value
155
159
 
156
160
 
@@ -176,6 +180,12 @@ def sidecar_root(primary: Path, kind: str, name: str, *, pin: bool = False) -> P
176
180
  raise IsolationError(str(error)) from error
177
181
 
178
182
 
183
+ def release_workspace_quietly(primary: Path) -> None:
184
+ """Best-effort placement hygiene after a sidecar retires; never blocks landing."""
185
+ with contextlib.suppress(ValueError, OSError):
186
+ worktree_paths.release_workspace(primary)
187
+
188
+
179
189
  def validate_task_id(task_id: str) -> str:
180
190
  if not TASK_ID_RE.fullmatch(task_id):
181
191
  raise IsolationError(f"invalid task id: {task_id}")
@@ -439,8 +449,9 @@ def collect_artifact_recoveries(primary: Path) -> list[Dict[str, object]]:
439
449
 
440
450
 
441
451
  def _git_file(repo: Path, revision: str, path: str) -> bytes:
452
+ git_path = PurePosixPath(path.replace("\\", "/")).as_posix()
442
453
  result = subprocess.run(
443
- ("git", "-C", str(repo), "show", f"{revision}:{path}"),
454
+ ("git", "-C", str(repo), "show", f"{revision}:{git_path}"),
444
455
  capture_output=True,
445
456
  check=False,
446
457
  )
@@ -481,6 +492,70 @@ def _same_directory(path: Path, expected: os.stat_result) -> bool:
481
492
  return stat.S_ISDIR(current.st_mode) and os.path.samestat(current, expected)
482
493
 
483
494
 
495
+ def _long_path(absolute: str) -> str:
496
+ if absolute.startswith("\\\\?\\"):
497
+ return absolute
498
+ if absolute.startswith("\\\\"):
499
+ return "\\\\?\\UNC\\" + absolute[2:]
500
+ return "\\\\?\\" + absolute
501
+
502
+
503
+ def _force_rmtree(path: Path) -> None:
504
+ """Delete a sidecar tree directly, tolerating read-only files and long paths."""
505
+ def _writable(function, target, _error):
506
+ os.chmod(target, stat.S_IWRITE)
507
+ function(target)
508
+
509
+ # The \\?\ prefix sidesteps Windows MAX_PATH on deep sidecar trees.
510
+ target = Path(_long_path(os.path.abspath(path))) if os.name == "nt" else path
511
+ try:
512
+ shutil.rmtree(target, onerror=_writable)
513
+ except OSError as error:
514
+ raise IsolationError(f"could not delete {path}: {error}") from error
515
+
516
+
517
+ def _remove_worktree(primary: Path, destination: Path, force: bool) -> None:
518
+ """Remove a sidecar worktree, tolerating populated submodules and locked trees.
519
+
520
+ git refuses to remove worktrees whose submodules were ever initialized, and
521
+ forced removal can still fail deleting deep trees (Windows "Directory not
522
+ empty"). A dirty sidecar keeps git's refusal unless `force`, so forced
523
+ retries only run for a provably clean tree. The direct-delete fallback runs
524
+ only for git's tree-deletion failures, so any other git error leaves the
525
+ sidecar intact for diagnosis.
526
+ """
527
+ tree_delete_failure = ("failed to delete", "directory not empty", "permission denied")
528
+
529
+ def failure(*flags: str) -> str:
530
+ result = run_git(primary, "worktree", "remove", *flags, str(destination))
531
+ return (result.stderr or result.stdout).strip() if result.returncode else ""
532
+
533
+ detail = failure("--force") if force else failure()
534
+ if not detail:
535
+ return
536
+ if not force and destination.is_dir():
537
+ status = run_git(
538
+ destination, "status", "--porcelain", "--untracked-files=all"
539
+ )
540
+ if status.returncode != 0 or status.stdout.strip():
541
+ raise IsolationError(detail or "git worktree remove failed")
542
+ run_git(destination, "submodule", "deinit", "--all", "--force")
543
+ detail = failure("--force")
544
+ if not detail:
545
+ return
546
+ if destination.is_dir() and any(
547
+ marker in detail.lower() for marker in tree_delete_failure
548
+ ):
549
+ _force_rmtree(destination)
550
+ detail = failure("--force")
551
+ if detail and not destination.is_dir():
552
+ pruned = run_git(primary, "worktree", "prune")
553
+ if pruned.returncode == 0:
554
+ detail = ""
555
+ if detail or destination.is_dir() or destination.resolve() in _registered_worktrees(primary):
556
+ raise IsolationError(detail or "git worktree remove failed")
557
+
558
+
484
559
  def _cleanup_reserved_worktree(
485
560
  primary: Path,
486
561
  branch: str,
@@ -647,6 +722,18 @@ def member_checkout(coordinator: Path, member: str) -> Tuple[Path, str]:
647
722
  return checkout, str(role["project"])
648
723
 
649
724
 
725
+ def member_default_branch(coordinator: Path, member: str) -> str:
726
+ """The default branch that MEMBERS.md records for a member; a row without it means main."""
727
+ try:
728
+ import members
729
+ except ImportError: # pragma: no cover - package import used by tests
730
+ from scripts import members
731
+ try:
732
+ return members.member_default_branch(require_directory(coordinator, "coordinator"), member)
733
+ except members.MembersError as error:
734
+ raise IsolationError(str(error)) from error
735
+
736
+
650
737
  def member_bound_checkout(coordinator: Path, member: str) -> Dict[str, object]:
651
738
  """Path's own checkout of a locked member's bound branch, outside the team's checkout."""
652
739
  checkout, _, entry = _member_context(coordinator, member)
@@ -758,7 +845,7 @@ def retire_member_task(
758
845
  raise IsolationError(f"member task branch {ref} has unlanded commits")
759
846
  destination = sidecar_root(checkout, "task", name)
760
847
  copy_rejected = None
761
- if force:
848
+ if force and task_file:
762
849
  rejected, copy_rejected = _rejected_member_attempt_error(coordinator, member, task_id, task_file, destination)
763
850
  if rejected:
764
851
  raise IsolationError(rejected)
@@ -766,9 +853,10 @@ def retire_member_task(
766
853
  if destination.resolve() in registered:
767
854
  if registered[destination.resolve()] != ref:
768
855
  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")
856
+ try:
857
+ _remove_worktree(checkout, destination, force)
858
+ except IsolationError as error:
859
+ raise RemovalError(str(error)) from error
772
860
  authorization = run_git(checkout, "update-ref", "-d", member_task_authorization_ref(project, task_id))
773
861
  if authorization.returncode != 0:
774
862
  raise IsolationError((authorization.stderr or authorization.stdout).strip() or "git update-ref failed")
@@ -776,6 +864,7 @@ def retire_member_task(
776
864
  deleted = run_git(checkout, "update-ref", "-d", ref, tip.stdout.strip())
777
865
  if deleted.returncode != 0:
778
866
  raise IsolationError((deleted.stderr or deleted.stdout).strip() or "git update-ref failed")
867
+ release_workspace_quietly(checkout)
779
868
  return copy_rejected
780
869
 
781
870
 
@@ -936,9 +1025,11 @@ def activate_member_task(
936
1025
  created = run_git(checkout, "update-ref", ref, member_base, "0" * 40)
937
1026
  if created.returncode != 0 and run_git(checkout, "rev-parse", "--verify", "--quiet", ref).stdout.strip() != member_base:
938
1027
  raise IsolationError("could not record member task authorization")
939
- return {"agent": agent, "base": resolved_base, "copy": str(copy), "member": member,
1028
+ result = {"agent": agent, "base": resolved_base, "copy": str(copy), "member": member,
940
1029
  "member_base": member_base, "status": "in-progress", "task_branch": branch,
941
1030
  "task_file": task_file, "worktree": str(sidecar)}
1031
+ _write_native_member_dispatch_record(coordinator, result)
1032
+ return result
942
1033
 
943
1034
 
944
1035
  def retained_member_task(
@@ -1050,25 +1141,8 @@ def _member_journal_path(coordinator: Path, task_id: str) -> Path:
1050
1141
  @contextlib.contextmanager
1051
1142
  def _member_landing_lock(coordinator: Path) -> Iterator[None]:
1052
1143
  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)
1144
+ with _common.exclusive_lock(path):
1145
+ yield
1072
1146
 
1073
1147
 
1074
1148
  def _require_member_coordinator_branch(coordinator: Path) -> None:
@@ -1126,7 +1200,7 @@ def _prove_member_commit(
1126
1200
  parent, commit).splitlines())
1127
1201
  if not changed:
1128
1202
  return "commit changes nothing"
1129
- undeclared = sorted(changed - allowed)
1203
+ undeclared = undeclared_paths(repo, parent, changed, allowed)
1130
1204
  if undeclared:
1131
1205
  return "undeclared paths: " + ", ".join(undeclared)
1132
1206
  if git_output(repo, "log", "-1", "--format=%s", commit) != subject:
@@ -1142,7 +1216,7 @@ def _commit_member_source(sidecar: Path, journal: Dict[str, object], allowed: Se
1142
1216
  if pending:
1143
1217
  if current_sha(sidecar) != member_base:
1144
1218
  raise IsolationError("dirty member source HEAD must equal the member base")
1145
- undeclared = sorted(pending - allowed)
1219
+ undeclared = undeclared_paths(sidecar, member_base, pending, allowed)
1146
1220
  if undeclared:
1147
1221
  raise IsolationError("undeclared paths: " + ", ".join(undeclared))
1148
1222
  git_output(sidecar, "add", "-A", "--", *sorted(pending))
@@ -1411,8 +1485,9 @@ def retire_member_verify(created: Dict[str, Dict[str, str]]) -> None:
1411
1485
  """Remove the member siblings of a coordinator verify sidecar; they hold no output."""
1412
1486
  for record in reversed(list(created.values())):
1413
1487
  checkout, destination, branch = _member_verify_sidecar(record)
1414
- git_output(checkout, "worktree", "remove", "--force", str(destination))
1488
+ _remove_worktree(checkout, destination, True)
1415
1489
  git_output(checkout, "branch", "-D", branch)
1490
+ release_workspace_quietly(checkout)
1416
1491
 
1417
1492
 
1418
1493
  def check_member_verify(created: Dict[str, Dict[str, str]]) -> None:
@@ -1444,6 +1519,89 @@ def clean_verify(primary: Path, worktree: Path, base: str, branch: str) -> Dict[
1444
1519
  return {"worktree": str(worktree), "branch": branch, "base": expected, "removed": removed}
1445
1520
 
1446
1521
 
1522
+ def ensure_verify_sidecar(primary: Path, base: str, branch: str) -> Dict[str, object]:
1523
+ """Return the named verify sidecar, recreating it when it is missing.
1524
+
1525
+ Finish needs the sidecar prepare-task created. A deleted sidecar must be
1526
+ rebuildable from the recorded base without a clean primary: the serial
1527
+ task's own product changes are exactly the tree finish reproduces next,
1528
+ so this must not reuse isolate_verify's clean-tree gate.
1529
+ """
1530
+ primary = require_directory(primary, "primary worktree")
1531
+ if worktree_root(primary) != primary:
1532
+ raise IsolationError(f"primary is not its Git root: {primary}")
1533
+ resolved_base = require_commit(primary, require_full_sha(base))
1534
+ if not branch.startswith(VERIFY_BRANCH_PREFIX):
1535
+ raise IsolationError("verify sidecar reproduction requires a verify branch")
1536
+ destination = _sidecar_path_for_branch(primary, branch)
1537
+ ref = f"refs/heads/{branch}"
1538
+ registered = _registered_worktrees(primary)
1539
+ if destination.is_symlink() or (destination.exists() and not destination.is_dir()):
1540
+ raise IsolationError(f"worktree path already exists: {destination}")
1541
+ if destination.is_dir():
1542
+ try:
1543
+ # A leftover empty directory (a half-deleted sidecar) clears;
1544
+ # anything non-empty must be the sidecar itself.
1545
+ destination.rmdir()
1546
+ except OSError:
1547
+ try:
1548
+ same = (
1549
+ worktree_root(destination) == destination
1550
+ and destination.resolve() in registered
1551
+ and registered[destination.resolve()] == ref
1552
+ and common_git_dir(destination) == common_git_dir(primary)
1553
+ )
1554
+ except IsolationError:
1555
+ same = False
1556
+ if not same:
1557
+ raise IsolationError(
1558
+ f"verify sidecar ownership changed: {destination}"
1559
+ ) from None
1560
+ if (
1561
+ require_attached(destination) != branch
1562
+ or current_sha(destination) != resolved_base
1563
+ ):
1564
+ raise IsolationError(f"verify sidecar HEAD changed: {destination}")
1565
+ return {
1566
+ "base": resolved_base,
1567
+ "branch": branch,
1568
+ "kind": "verify",
1569
+ "mode": "sidecar",
1570
+ "worktree": str(destination),
1571
+ }
1572
+ if destination.resolve() in registered:
1573
+ cleared = run_git(primary, "worktree", "remove", str(destination))
1574
+ if cleared.returncode != 0:
1575
+ run_git(primary, "worktree", "prune")
1576
+ if destination.resolve() in _registered_worktrees(primary):
1577
+ raise IsolationError(
1578
+ (cleared.stderr or cleared.stdout).strip()
1579
+ or "could not clear stale worktree registration"
1580
+ )
1581
+ existing = run_git(primary, "show-ref", "--verify", "--quiet", ref)
1582
+ if existing.returncode == 0:
1583
+ deleted = run_git(primary, "update-ref", "-d", ref)
1584
+ if deleted.returncode != 0:
1585
+ raise IsolationError(
1586
+ (deleted.stderr or deleted.stdout).strip() or "git update-ref failed"
1587
+ )
1588
+ create_named_worktree(primary, branch, destination, resolved_base)
1589
+ return {
1590
+ "base": resolved_base,
1591
+ "branch": branch,
1592
+ "kind": "verify",
1593
+ "mode": "sidecar",
1594
+ "worktree": str(destination),
1595
+ }
1596
+
1597
+
1598
+ def reproduce_verify_sidecar(sidecar_path: Path, tree: str) -> None:
1599
+ """Copy the primary's current task tree into the verify sidecar (clean_verify's inverse)."""
1600
+ git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
1601
+ if git_output(sidecar_path, "write-tree") != tree:
1602
+ raise IsolationError("sidecar reproduction differs from the primary task diff")
1603
+
1604
+
1447
1605
  def _split_paths(block: str) -> Set[str]:
1448
1606
  return {line.strip() for line in block.splitlines() if line.strip()}
1449
1607
 
@@ -1451,7 +1609,7 @@ def _split_paths(block: str) -> Set[str]:
1451
1609
  # Top-level .project entries the pipeline owns; ship checks filter other entries by Git visibility.
1452
1610
  PROJECT_ENTRIES = frozenset({
1453
1611
  "STATE.md", "LESSONS.md", "REPOSITORY.md", "MEMBERS.md", "CHARTER.md", "ROADMAP.md",
1454
- "SYNTHESIS.md",
1612
+ "SYNTHESIS.md", "config.json", "model-policy.json",
1455
1613
  "intent", "research", "plan", "tasks", "review", "build", "discuss", "archive", "next",
1456
1614
  })
1457
1615
 
@@ -1536,7 +1694,7 @@ def _commit_pending(
1536
1694
  mode = "-f" if path in forced else "-A"
1537
1695
  added = run_git(repo, "add", mode, "--", path)
1538
1696
  if added.returncode != 0:
1539
- raise IsolationError(f"could not stage {path}")
1697
+ raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
1540
1698
  staged = _split_paths(
1541
1699
  git_output(
1542
1700
  repo,
@@ -1555,15 +1713,51 @@ def _commit_pending(
1555
1713
  return current_sha(repo)
1556
1714
 
1557
1715
 
1716
+ def undeclared_paths(
1717
+ repo: Path, base: str, paths: Set[str], declared: Set[str]
1718
+ ) -> list[str]:
1719
+ """Return the sorted paths that the declared entries do not cover.
1720
+
1721
+ An entry covers itself and, as a declared directory, the files below it.
1722
+ An entry that is a file at `base` covers only itself: the task-brief lint
1723
+ rejects a path below a base file.
1724
+ """
1725
+
1726
+ @functools.lru_cache(maxsize=None)
1727
+ def directory(entry: str) -> bool:
1728
+ kind = run_git(repo, "cat-file", "-t", f"{base}:{entry}")
1729
+ return kind.returncode != 0 or kind.stdout.strip() == "tree"
1730
+
1731
+ return sorted(
1732
+ path
1733
+ for path in paths
1734
+ if path not in declared
1735
+ and not any(
1736
+ str(parent) in declared and directory(str(parent))
1737
+ for parent in list(PurePosixPath(path).parents)[:-1]
1738
+ )
1739
+ )
1740
+
1741
+
1558
1742
  def validate_allowed_changes(
1559
- repo: Path, base: str, pending: Set[str], allowed: Set[str]
1743
+ repo: Path,
1744
+ base: str,
1745
+ pending: Set[str],
1746
+ allowed: Set[str],
1747
+ tolerated: Set[str] = frozenset(),
1560
1748
  ) -> None:
1561
- if not pending:
1749
+ # Tolerated paths (primary bookkeeping) may stay uncommitted; the next
1750
+ # `.project` checkpoint commits them.
1751
+ landing = pending - tolerated
1752
+ if not landing:
1562
1753
  raise IsolationError("no changes to land")
1563
- since_base = committed_paths_since(repo, base) | pending
1564
- unexpected = sorted(path for path in since_base if path not in allowed)
1754
+ since_base = committed_paths_since(repo, base) | landing
1755
+ unexpected = undeclared_paths(repo, base, since_base, allowed)
1565
1756
  if unexpected:
1566
- raise IsolationError("unexpected paths: " + ", ".join(unexpected))
1757
+ raise IsolationError(
1758
+ "unexpected paths: " + ", ".join(unexpected)
1759
+ + "; landing allows only the task file and its declared files"
1760
+ )
1567
1761
  def _clean_blob_oid(repo: Path, path: str, text: str, *, write: bool) -> str:
1568
1762
  arguments = ["hash-object"]
1569
1763
  if write:
@@ -1583,21 +1777,23 @@ def commit_allowed_changes(
1583
1777
  subject: str,
1584
1778
  allowed: Set[str],
1585
1779
  body: str,
1780
+ tolerated: Set[str] = frozenset(),
1586
1781
  ) -> tuple[str, str, Dict[str, Optional[tuple[str, str]]]]:
1587
1782
  require_attached(repo)
1588
1783
  pending = uncommitted_paths(repo)
1589
- validate_allowed_changes(repo, base, pending, allowed)
1784
+ validate_allowed_changes(repo, base, pending, allowed, tolerated)
1785
+ staging = pending - tolerated
1590
1786
  reset = run_git(repo, "reset", "-q", "HEAD")
1591
1787
  if reset.returncode != 0:
1592
1788
  raise IsolationError("could not clear the index before landing")
1593
- for path in sorted(pending):
1789
+ for path in sorted(staging):
1594
1790
  added = run_git(repo, "add", "-A", "--", path)
1595
1791
  if added.returncode != 0:
1596
- raise IsolationError(f"could not stage {path}")
1792
+ raise IsolationError(f"could not stage {path}: {added.stderr.strip()}")
1597
1793
  staged = _split_paths(
1598
1794
  git_output(repo, "diff", "--cached", "--name-only", "--relative")
1599
1795
  )
1600
- if staged != pending:
1796
+ if staged != staging:
1601
1797
  raise IsolationError("staged paths do not match the uncommitted change set")
1602
1798
  verified_tree = git_output(repo, "write-tree")
1603
1799
  verified_delta = _tree_delta(repo, base, verified_tree)
@@ -1743,6 +1939,17 @@ def activate_task(
1743
1939
  if task_branch is None:
1744
1940
  if branch.startswith(TASK_BRANCH_PREFIX):
1745
1941
  raise IsolationError("parallel task activation requires --task-branch")
1942
+ live = [
1943
+ path
1944
+ for path, ref in _registered_worktrees(worktree).items()
1945
+ if ref == f"refs/heads/{task_branch_name(task_id)}"
1946
+ and path != worktree
1947
+ ]
1948
+ if live:
1949
+ raise IsolationError(
1950
+ "task has an active parallel worktree; serial activation in "
1951
+ f"another worktree is refused (activate {live[0]} with --task-branch)"
1952
+ )
1746
1953
  elif task_branch != task_branch_name(task_id) or branch != task_branch:
1747
1954
  raise IsolationError("task activation branch does not match the task worktree")
1748
1955
  normalized_task = relative_posix(task_file)
@@ -1773,7 +1980,7 @@ def activate_task(
1773
1980
  if existing.returncode != 0 or existing.stdout.strip() != resolved_base:
1774
1981
  _replace_regular_file(task_path, text.encode("utf-8"), mode)
1775
1982
  raise IsolationError("could not record task worktree authorization")
1776
- return {
1983
+ result = {
1777
1984
  "agent": agent,
1778
1985
  "base": resolved_base,
1779
1986
  "status": "in-progress",
@@ -1781,6 +1988,30 @@ def activate_task(
1781
1988
  "task_file": normalized_task,
1782
1989
  "worktree": str(worktree),
1783
1990
  }
1991
+ _write_native_dispatch_record(worktree, result)
1992
+ return result
1993
+
1994
+
1995
+ def _write_native_dispatch_record(worktree: Path, activation: Dict[str, object]) -> None:
1996
+ try:
1997
+ if __package__:
1998
+ from . import dispatch_driver
1999
+ else:
2000
+ import dispatch_driver
2001
+ except ImportError:
2002
+ return
2003
+ dispatch_driver.record_native_activation(worktree, activation)
2004
+
2005
+
2006
+ def _write_native_member_dispatch_record(coordinator: Path, activation: Dict[str, object]) -> None:
2007
+ try:
2008
+ if __package__:
2009
+ from . import dispatch_driver
2010
+ else:
2011
+ import dispatch_driver
2012
+ except ImportError:
2013
+ return
2014
+ dispatch_driver.record_native_member_activation(coordinator, activation)
1784
2015
 
1785
2016
 
1786
2017
  def deactivate_task(
@@ -1899,6 +2130,59 @@ def stamp_task_landed(task_path: Path, base: str) -> None:
1899
2130
  )
1900
2131
 
1901
2132
 
2133
+ def block_serial_task(
2134
+ primary: Path, task_file: str, task_id: str, base: str
2135
+ ) -> Dict[str, str]:
2136
+ """Record an explicit blocked result on its still-active serial task."""
2137
+ primary = require_directory(primary, "primary worktree")
2138
+ if worktree_root(primary) != primary:
2139
+ raise IsolationError(f"primary is not its Git root: {primary}")
2140
+ normalized_task = relative_posix(task_file)
2141
+ if not normalized_task.startswith(".project/tasks/"):
2142
+ raise IsolationError("serial task file must stay under .project/tasks/")
2143
+ task_id = validate_task_id(task_id)
2144
+ expected_base = require_commit(primary, require_full_sha(base))
2145
+ task_path = _real_file(primary, normalized_task, "serial task file")
2146
+ text, mode = _read_task_text(task_path)
2147
+ fields, error = task_frontmatter(text)
2148
+ if error or fields is None:
2149
+ raise IsolationError(error or "serial task frontmatter is unreadable")
2150
+ if fields.get("id") != task_id:
2151
+ raise IsolationError("serial task id changed before blocked result")
2152
+ if fields.get("status") not in {"in-progress", "blocked"}:
2153
+ raise IsolationError("serial task is no longer active")
2154
+ if fields.get("base") != expected_base:
2155
+ raise IsolationError("serial task base changed before blocked result")
2156
+ if fields.get("task_branch") != "null":
2157
+ raise IsolationError("serial task records a task branch")
2158
+ recorded_worktree = fields.get("worktree")
2159
+ if not isinstance(recorded_worktree, str) or Path(recorded_worktree).resolve() != primary:
2160
+ raise IsolationError("serial task worktree changed before blocked result")
2161
+
2162
+ if fields.get("status") == "blocked":
2163
+ return {"task_id": task_id, "task_file": normalized_task,
2164
+ "base": expected_base, "status": "blocked"}
2165
+
2166
+ head, body = split_frontmatter(text)
2167
+ replaced = False
2168
+ lines = []
2169
+ for line in head:
2170
+ if _frontmatter_key(line) == "status":
2171
+ if replaced:
2172
+ raise IsolationError("serial task frontmatter repeats status")
2173
+ replaced = True
2174
+ lines.append("status: blocked")
2175
+ else:
2176
+ lines.append(line)
2177
+ if not replaced:
2178
+ raise IsolationError("serial task frontmatter is missing status")
2179
+ _replace_regular_file(
2180
+ task_path, ("---\n" + "\n".join(lines) + "\n---\n" + body).encode("utf-8"), mode
2181
+ )
2182
+ return {"task_id": task_id, "task_file": normalized_task,
2183
+ "base": expected_base, "status": "blocked"}
2184
+
2185
+
1902
2186
  def _restore_landing_state(
1903
2187
  worktree: Path, task_path: Path, task_bytes: bytes, task_mode: int, index_tree: str
1904
2188
  ) -> None:
@@ -1933,7 +2217,12 @@ def _restore_rejected_commit(
1933
2217
 
1934
2218
 
1935
2219
  def _stamp_and_commit(
1936
- worktree: Path, base: str, subject: str, task_file: str, allowed: Set[str]
2220
+ worktree: Path,
2221
+ base: str,
2222
+ subject: str,
2223
+ task_file: str,
2224
+ allowed: Set[str],
2225
+ tolerated: Set[str] = frozenset(),
1937
2226
  ) -> tuple[str, Dict[str, Optional[tuple[str, str]]]]:
1938
2227
  task_path = worktree / task_file
1939
2228
  task_text, task_mode = _read_task_text(task_path)
@@ -1954,14 +2243,14 @@ def _stamp_and_commit(
1954
2243
  pending = uncommitted_paths(worktree)
1955
2244
  if stamped != task_bytes:
1956
2245
  pending.add(task_file)
1957
- validate_allowed_changes(worktree, base, pending, allowed)
2246
+ validate_allowed_changes(worktree, base, pending, allowed, tolerated)
1958
2247
  original_head = current_sha(worktree)
1959
2248
  index_tree = git_output(worktree, "write-tree")
1960
2249
  try:
1961
2250
  _replace_regular_file(task_path, stamped, task_mode)
1962
- body = task_commit_body(task_file, uncommitted_paths(worktree), base)
2251
+ body = task_commit_body(task_file, uncommitted_paths(worktree) - tolerated, base)
1963
2252
  commit, verified_tree, verified_delta = commit_allowed_changes(
1964
- worktree, base, subject, allowed, body
2253
+ worktree, base, subject, allowed, body, tolerated
1965
2254
  )
1966
2255
  proof_error = _landing_commit_proof_error(
1967
2256
  worktree,
@@ -2039,6 +2328,7 @@ def land(
2039
2328
  subject,
2040
2329
  relative_posix(task_file),
2041
2330
  allowed,
2331
+ BOOKKEEPING_PATHS,
2042
2332
  )
2043
2333
  return {
2044
2334
  "bound_branch": bound,
@@ -2333,7 +2623,7 @@ def _landing_retry_error(
2333
2623
  if task_file not in pending:
2334
2624
  return "interrupted landing does not retain the task change"
2335
2625
  try:
2336
- validate_allowed_changes(repo, base, pending, contract_paths)
2626
+ validate_allowed_changes(repo, base, pending, contract_paths, BOOKKEEPING_PATHS)
2337
2627
  except IsolationError as validation_error:
2338
2628
  return str(validation_error)
2339
2629
  return None
@@ -2378,7 +2668,7 @@ def _prove_task_commit(
2378
2668
  )
2379
2669
  if task_file not in changed:
2380
2670
  return None, "commit does not touch the task file"
2381
- stray = sorted(changed - contract_paths)
2671
+ stray = undeclared_paths(repo, base, changed, contract_paths)
2382
2672
  if stray:
2383
2673
  return None, f"commit touches undeclared paths: {', '.join(stray)}"
2384
2674
  if landing_error:
@@ -2470,6 +2760,56 @@ def _task_branch_proof_error(
2470
2760
  return None
2471
2761
 
2472
2762
 
2763
+ def only_missing_files(worktree: Path) -> bool:
2764
+ """True when the only difference of a worktree from its HEAD is tracked files that are missing.
2765
+
2766
+ An untracked or a gitignored file is content that Git does not have, so it makes this False.
2767
+ """
2768
+ status = run_git(worktree, "status", "--porcelain", "--untracked-files=all", "--ignored=matching")
2769
+ lines = status.stdout.splitlines()
2770
+ return status.returncode == 0 and bool(lines) and all(line.startswith(" D") for line in lines)
2771
+
2772
+
2773
+ def holds_only_landed_files(repo: Path, path: Path, commits: Sequence[str]) -> bool:
2774
+ """True when `path` is a directory, not a usable worktree, that holds only files of `commits`.
2775
+
2776
+ Each file must be the same blob at the same path in one of the commits, so the removal of
2777
+ the directory loses nothing that Git does not have. A check that cannot be done is False.
2778
+ """
2779
+
2780
+ def unreadable(error: OSError) -> None:
2781
+ raise error
2782
+
2783
+ try:
2784
+ if path.is_symlink() or not path.is_dir() or path.resolve() != path:
2785
+ return False
2786
+ root = run_git(path, "rev-parse", "--show-toplevel")
2787
+ if root.returncode == 0 and Path(root.stdout.strip()).resolve() == path:
2788
+ return False
2789
+ files = []
2790
+ for folder, folders, names in os.walk(path, onerror=unreadable):
2791
+ if any(os.path.islink(os.path.join(folder, name)) for name in folders):
2792
+ return False
2793
+ files += [os.path.join(folder, name) for name in names]
2794
+ # What a removal left of the worktree link is not content.
2795
+ files = [name for name in files if name != str(path / ".git")]
2796
+ if not all(stat.S_ISREG(os.lstat(name).st_mode) for name in files):
2797
+ return False
2798
+ known = {
2799
+ entry.split(" ", 2)[2]
2800
+ for commit in commits
2801
+ for entry in git_text(repo, "ls-tree", "-r", "-z", commit).split("\0")
2802
+ if entry
2803
+ }
2804
+ hashed = run_git(repo, "hash-object", "--stdin-paths", input="".join(f"{name}\n" for name in files))
2805
+ except (OSError, IsolationError):
2806
+ return False
2807
+ hashes = hashed.stdout.split()
2808
+ return hashed.returncode == 0 and len(hashes) == len(files) and all(
2809
+ f"{sha}\t{Path(name).relative_to(path).as_posix()}" in known for sha, name in zip(hashes, files)
2810
+ )
2811
+
2812
+
2473
2813
  def _worktree_report(
2474
2814
  primary: Path, path: Path, expected_branch: str
2475
2815
  ) -> tuple[Optional[Dict[str, object]], Optional[str]]:
@@ -2556,6 +2896,40 @@ def _resume_task_error(
2556
2896
  return _retained_task_contract_error(current_text, isolate_text)
2557
2897
 
2558
2898
 
2899
+ def _serial_bookkeeping_error(primary: Path, base: str, head: str) -> Optional[str]:
2900
+ """Prove that serial task history advanced only through linear .project commits."""
2901
+ if base == head:
2902
+ return None
2903
+ if run_git(primary, "merge-base", "--is-ancestor", base, head).returncode != 0:
2904
+ return "recorded base is not an ancestor of the current branch"
2905
+ commits = git_output(
2906
+ primary, "rev-list", "--first-parent", "--reverse", f"{base}..{head}"
2907
+ ).splitlines()
2908
+ previous = base
2909
+ for commit in commits:
2910
+ parents = git_output(primary, "rev-list", "--parents", "-n", "1", commit).split()
2911
+ if len(parents) != 2 or parents[1] != previous:
2912
+ return "history contains an unproven merge or commit sequence"
2913
+ paths = git_output(
2914
+ primary,
2915
+ "diff-tree",
2916
+ "--no-commit-id",
2917
+ "--name-only",
2918
+ "-r",
2919
+ "--no-renames",
2920
+ previous,
2921
+ commit,
2922
+ ).splitlines()
2923
+ if not paths:
2924
+ return "history contains an empty commit"
2925
+ if any(path != ".project" and not path.startswith(".project/") for path in paths):
2926
+ return "history contains changes outside .project"
2927
+ previous = commit
2928
+ if previous != head:
2929
+ return "history does not prove a first-parent path from the recorded base"
2930
+ return None
2931
+
2932
+
2559
2933
  MEMBER_RECORD_RE = re.compile(
2560
2934
  r"Task: (?P<task>\S+)\nBase: (?P<base>[0-9a-f]{40})\n"
2561
2935
  r"Member: (?P<member>\S+) (?P<landing>[0-9a-f]{40}) (?P<member_base>[0-9a-f]{40})"
@@ -2669,6 +3043,7 @@ def _recover_task(
2669
3043
  head: str,
2670
3044
  bound: str,
2671
3045
  canonical_task_file: Optional[str] = None,
3046
+ inspect_isolate: bool = True,
2672
3047
  ) -> Dict[str, object]:
2673
3048
  task_file = canonical_task_file or relative_posix(str(path.relative_to(primary)))
2674
3049
  report: Dict[str, object] = {"task": task_file}
@@ -2676,6 +3051,11 @@ def _recover_task(
2676
3051
  def result(verdict: str, **extra: object) -> Dict[str, object]:
2677
3052
  return {**report, "verdict": verdict, **extra}
2678
3053
 
3054
+ def leftover(commit: object, base: object) -> Dict[str, object]:
3055
+ # The landing is proven, so this block names only what is left to remove.
3056
+ return result("block", reason=f"task landed at {commit}; remove its leftover isolate: {sidecar}",
3057
+ commit=commit, base=base, leftover=str(sidecar))
3058
+
2679
3059
  try:
2680
3060
  task_text, _ = _read_task_text(path)
2681
3061
  except IsolationError as error:
@@ -2702,14 +3082,22 @@ def _recover_task(
2702
3082
  return result(**_prove_member_task(primary, task_file, fields, subject, history))
2703
3083
  task_branch = task_branch_name(task_id)
2704
3084
  branch_exists = task_branch in branches
2705
- worktree, worktree_error = _worktree_report(
2706
- primary, sidecar_root(primary, "task", task_id), task_branch
3085
+ sidecar = sidecar_root(primary, "task", task_id)
3086
+ worktree, worktree_error = (
3087
+ _worktree_report(primary, sidecar, task_branch) if inspect_isolate else (None, None)
2707
3088
  )
2708
3089
  report["worktree"] = worktree
2709
3090
  report["task_branch"] = task_branch if branch_exists else None
2710
3091
  report["rejected"] = []
2711
3092
 
2712
3093
  if worktree_error:
3094
+ landed = _recover_task(
3095
+ primary, path, history, branches, head, bound, canonical_task_file, False
3096
+ ) if status == "done" else {}
3097
+ if landed.get("verdict") == "recovered" and holds_only_landed_files(
3098
+ primary, sidecar, [str(landed["commit"]), *([task_branch] if branch_exists else [])]
3099
+ ):
3100
+ return leftover(landed["commit"], landed["base"])
2713
3101
  return result("block", reason=worktree_error)
2714
3102
 
2715
3103
  valid_statuses = {"pending", "in-progress", "done", "failed", "blocked"}
@@ -2845,8 +3233,12 @@ def _recover_task(
2845
3233
  )
2846
3234
  except IsolationError as error:
2847
3235
  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")
3236
+ advance_error = _serial_bookkeeping_error(primary, base, head)
3237
+ if advance_error:
3238
+ return result(
3239
+ "block",
3240
+ reason=f"{status} serial task advanced past its base: {advance_error}",
3241
+ )
2850
3242
  primary_worktree, primary_error = _worktree_report(primary, primary, bound)
2851
3243
  if primary_error:
2852
3244
  return result("block", reason=primary_error)
@@ -2860,6 +3252,15 @@ def _recover_task(
2860
3252
  str(primary),
2861
3253
  "null",
2862
3254
  )
3255
+ if head != base and resume_error is None:
3256
+ return result(
3257
+ "reconcile",
3258
+ base=base,
3259
+ reason=(
3260
+ "serial task advanced through .project bookkeeping; reconcile the "
3261
+ "attempt and any retained Verify sidecar before resetting it to pending"
3262
+ ),
3263
+ )
2863
3264
  if resume_error:
2864
3265
  return result("block", reason=resume_error)
2865
3266
  return result("resume", base=base)
@@ -2888,8 +3289,12 @@ def _recover_task(
2888
3289
  if serial_retained:
2889
3290
  if str(fields.get("task_branch", "")) != "null":
2890
3291
  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")
3292
+ advance_error = _serial_bookkeeping_error(primary, base, head)
3293
+ if advance_error:
3294
+ return result(
3295
+ "block",
3296
+ reason=f"{status} serial task advanced past its base: {advance_error}",
3297
+ )
2893
3298
  primary_worktree, primary_error = _worktree_report(primary, primary, bound)
2894
3299
  if primary_error:
2895
3300
  return result("block", reason=primary_error)
@@ -2946,6 +3351,9 @@ def _recover_task(
2946
3351
  if branch_error:
2947
3352
  return result("block", reason=branch_error)
2948
3353
  if worktree is not None and not bool(worktree["clean"]):
3354
+ if only_missing_files(sidecar):
3355
+ # Only landed files are missing: a removal stopped part way, and nothing unknown is there.
3356
+ return leftover(commit, base)
2949
3357
  return result(
2950
3358
  "block",
2951
3359
  reason="proven task retains a dirty worktree",
@@ -3511,7 +3919,11 @@ def _attest_body_fields(body: str) -> tuple[Dict[str, str], list[str]]:
3511
3919
 
3512
3920
 
3513
3921
  def _attested_changes(repo: Path, base: str, head: str, declared: Set[str]) -> list[str]:
3514
- """Declared paths that changed between base and head, matched literally like land."""
3922
+ """Declared paths that changed between base and head, matched literally.
3923
+
3924
+ Land also accepts files below a declared directory. Attestation does not:
3925
+ a recorded attestation body must stay provable.
3926
+ """
3515
3927
  changed = set(git_output(repo, "diff", "--name-only", base, head).splitlines())
3516
3928
  return sorted(changed & declared)
3517
3929
 
@@ -4321,6 +4733,7 @@ def retire(
4321
4733
  or f"could not delete {branch}"
4322
4734
  )
4323
4735
  branch_retired = True
4736
+ release_workspace_quietly(primary)
4324
4737
  return {
4325
4738
  "bound_branch": bound,
4326
4739
  "branch": branch,
@@ -4354,22 +4767,13 @@ def retire(
4354
4767
  raise IsolationError(
4355
4768
  "worktree is dirty; pass --force only from the retry-retirement path"
4356
4769
  )
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
- )
4770
+ _remove_worktree(primary, resolved_worktree, force)
4368
4771
  deleted = run_git(primary, "branch", "-D", retire_branch)
4369
4772
  if deleted.returncode != 0:
4370
4773
  raise IsolationError(
4371
4774
  (deleted.stderr or deleted.stdout).strip() or f"could not delete {retire_branch}"
4372
4775
  )
4776
+ release_workspace_quietly(primary)
4373
4777
  return {
4374
4778
  "bound_branch": bound,
4375
4779
  "branch": retire_branch,
@@ -4379,6 +4783,71 @@ def retire(
4379
4783
  }
4380
4784
 
4381
4785
 
4786
+ def retire_unused_verify(
4787
+ primary: Path, worktree: Path, base: str, branch: str
4788
+ ) -> Dict[str, object]:
4789
+ """Retire a Verify sidecar only when it is provably untouched since creation."""
4790
+ primary = require_directory(primary, "primary worktree")
4791
+ if worktree_root(primary) != primary:
4792
+ raise IsolationError(f"primary is not its Git root: {primary}")
4793
+ if not branch.startswith(VERIFY_BRANCH_PREFIX):
4794
+ raise IsolationError("unused Verify retirement requires a verify branch")
4795
+ expected = _sidecar_path_for_branch(primary, branch)
4796
+ resolved_worktree = worktree.resolve()
4797
+ if resolved_worktree != expected:
4798
+ raise IsolationError("unused Verify path differs from its branch")
4799
+ expected_base = require_commit(primary, require_full_sha(base))
4800
+
4801
+ if os.path.lexists(worktree):
4802
+ if worktree.is_symlink() or not worktree.is_dir() or worktree.resolve() != worktree:
4803
+ raise IsolationError(f"unused Verify path is not an owned worktree: {worktree}")
4804
+ if worktree_root(worktree) != worktree:
4805
+ raise IsolationError(f"unused Verify path is not its Git root: {worktree}")
4806
+ if common_git_dir(primary) != common_git_dir(worktree):
4807
+ raise IsolationError("unused Verify sidecar belongs to another repository")
4808
+ if require_attached(worktree) != branch:
4809
+ raise IsolationError("unused Verify sidecar branch changed")
4810
+ if current_sha(worktree) != expected_base:
4811
+ raise IsolationError("unused Verify sidecar HEAD changed; preserving it")
4812
+ dirty = git_output(
4813
+ worktree,
4814
+ "status",
4815
+ "--porcelain",
4816
+ "--untracked-files=all",
4817
+ "--ignored=matching",
4818
+ )
4819
+ if dirty:
4820
+ raise IsolationError("unused Verify sidecar has uncommitted files; preserving it")
4821
+ else:
4822
+ branch_head = run_git(
4823
+ primary, "rev-parse", "--verify", "--quiet", f"refs/heads/{branch}"
4824
+ )
4825
+ if branch_head.returncode == 0 and branch_head.stdout.strip() != expected_base:
4826
+ raise IsolationError("unused Verify branch HEAD changed; preserving it")
4827
+ for registered_path, registered_branch in _registered_worktrees(primary).items():
4828
+ if registered_branch == f"refs/heads/{branch}" and registered_path != expected:
4829
+ raise IsolationError("unused Verify branch is checked out at another path")
4830
+
4831
+ return retire(primary, expected, branch, False)
4832
+
4833
+
4834
+ def retire_stale_verify(primary: Path, worktree: Path, branch: str) -> Dict[str, object]:
4835
+ """Retire the Verify sidecar of an earlier prepare-task only when it is proven untouched.
4836
+
4837
+ The reflog of its branch is the record of its creation base. It must hold only the
4838
+ creation, at a commit of the bound branch; `retire_unused_verify` then proves the checkout.
4839
+ """
4840
+ try:
4841
+ created = git_output(primary, "reflog", "show", "--format=%H", f"refs/heads/{branch}").split()
4842
+ if len(created) != 1:
4843
+ raise IsolationError("its branch moved after its creation, or has no record of its creation")
4844
+ if run_git(primary, "merge-base", "--is-ancestor", created[0], "HEAD").returncode != 0:
4845
+ raise IsolationError("its branch is not at a commit of the bound branch")
4846
+ return retire_unused_verify(primary, worktree, created[0], branch)
4847
+ except IsolationError as error:
4848
+ raise IsolationError(f"Verify sidecar {worktree} is kept: {error}") from error
4849
+
4850
+
4382
4851
  def parser() -> argparse.ArgumentParser:
4383
4852
  argument_parser = argparse.ArgumentParser(description=__doc__)
4384
4853
  subparsers = argument_parser.add_subparsers(dest="command", required=True)