@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
@@ -16,21 +16,24 @@ from __future__ import annotations
16
16
  import argparse
17
17
  import errno
18
18
  import hashlib
19
+ import importlib
19
20
  import json
20
21
  import os
21
22
  import re
23
+ import shlex
22
24
  import subprocess
23
25
  import sys
24
26
  import tempfile
25
27
  import time
26
28
  from datetime import datetime, timezone
27
29
  from pathlib import Path
28
- from typing import BinaryIO, Callable, Dict, List, Optional
30
+ from typing import BinaryIO, Callable, Dict, List, Optional, Tuple
29
31
 
30
32
  try:
31
33
  from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
32
34
  pipeline_state, review_findings, review_panel, workflow_run, task_context, model_policy)
33
35
  from scripts import check_handoffs as contracts
36
+ from scripts.pipeline_git import is_bound_branch
34
37
  except ImportError: # bundled copy inside a skill's scripts directory
35
38
  import _common
36
39
  import archive_milestone
@@ -44,6 +47,7 @@ except ImportError: # bundled copy inside a skill's scripts directory
44
47
  import review_panel
45
48
  import workflow_run
46
49
  import model_policy
50
+ from pipeline_git import is_bound_branch
47
51
 
48
52
  if os.name == "nt":
49
53
  import msvcrt
@@ -71,12 +75,99 @@ class DriverStop(RuntimeError):
71
75
  STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
72
76
  contracts.HandoffError, review_findings.ReviewFindingsError,
73
77
  pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
78
+ _RUNTIME_ACTIVATION_ORDER = (
79
+ "_common",
80
+ "roadmap",
81
+ "review_panel",
82
+ "loop_run",
83
+ "pipeline_git",
84
+ "worktree_paths",
85
+ "build_recovery",
86
+ "isolation",
87
+ "pipeline_state",
88
+ "state_checkpoint",
89
+ "check_task_briefs",
90
+ "build_state",
91
+ "check_handoffs",
92
+ "task_context",
93
+ # discussion_validate and archive_milestone import each other: the second
94
+ # reload binds the reloaded ArchiveError that STOP_ERRORS catches.
95
+ "discussion_validate",
96
+ "archive_milestone",
97
+ "discussion_validate",
98
+ "review_findings",
99
+ "model_policy",
100
+ )
74
101
  RECOVERY_BLOCKED = "recovery blocked"
102
+ LEFTOVER_ISOLATE = "task {} is landed; remove its leftover isolate: {}"
75
103
  # The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
76
104
  # the same stops from review, panel, or skeptics stay with the parent.
77
105
  BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
78
106
 
79
107
 
108
+ def _bundled_helpers() -> bool:
109
+ return "scripts.isolation" not in sys.modules
110
+
111
+
112
+ def _refresh_stop_errors() -> None:
113
+ global STOP_ERRORS
114
+ STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
115
+ contracts.HandoffError, review_findings.ReviewFindingsError,
116
+ pipeline_state.PipelineStateError, archive_milestone.ArchiveError,
117
+ model_policy.PolicyError)
118
+
119
+
120
+ def _reload_pinned_modules(runtime_root: Path) -> None:
121
+ global isolation, build_state, task_context, contracts, pipeline_state
122
+ global archive_milestone, review_findings, model_policy
123
+ if _bundled_helpers():
124
+ path = str(runtime_root)
125
+ if path not in sys.path:
126
+ sys.path.insert(0, path)
127
+ for name in _RUNTIME_ACTIVATION_ORDER:
128
+ module = sys.modules.get(name)
129
+ if module is not None:
130
+ importlib.reload(module)
131
+ isolation = sys.modules["isolation"]
132
+ build_state = sys.modules["build_state"]
133
+ task_context = sys.modules["task_context"]
134
+ contracts = sys.modules["check_handoffs"]
135
+ pipeline_state = sys.modules["pipeline_state"]
136
+ archive_milestone = sys.modules["archive_milestone"]
137
+ review_findings = sys.modules["review_findings"]
138
+ model_policy = sys.modules["model_policy"]
139
+ else:
140
+ import scripts
141
+ path = str(runtime_root)
142
+ if path not in scripts.__path__:
143
+ scripts.__path__ = [path, *[entry for entry in scripts.__path__ if entry != path]]
144
+ for name in _RUNTIME_ACTIVATION_ORDER:
145
+ module = sys.modules.get(f"scripts.{name}")
146
+ if module is not None:
147
+ importlib.reload(module)
148
+ isolation = scripts.isolation
149
+ build_state = scripts.build_state
150
+ task_context = scripts.task_context
151
+ contracts = scripts.check_handoffs
152
+ pipeline_state = scripts.pipeline_state
153
+ archive_milestone = scripts.archive_milestone
154
+ review_findings = scripts.review_findings
155
+ model_policy = scripts.model_policy
156
+ _refresh_stop_errors()
157
+
158
+
159
+ def _activate_pinned_runtime(repo: Path) -> None:
160
+ if not os.path.lexists(repo / ".gsd-path/runtime.json"):
161
+ return
162
+ resolved = subprocess.run(
163
+ [sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
164
+ "--repo", str(repo), "--runtime-path"],
165
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
166
+ if resolved.returncode:
167
+ raise DriverStop(resolved.stderr.strip() or "pinned runtime resolution failed")
168
+ _reload_pinned_modules(Path(resolved.stdout.strip()))
169
+
170
+
80
171
  def now() -> str:
81
172
  return datetime.now(timezone.utc).isoformat(timespec="seconds")
82
173
 
@@ -107,6 +198,169 @@ def attempts_used(root: Path, task_id: str) -> int:
107
198
  if load_state(path).get("origin") == "dispatch")
108
199
 
109
200
 
201
+ def dispatch_primary(worktree: Path) -> Path:
202
+ """The bound-branch primary that owns dispatch records for a task checkout."""
203
+ worktree = worktree.resolve()
204
+ if is_bound_branch(isolation.require_attached(worktree)):
205
+ return worktree
206
+ matches = [
207
+ path.resolve()
208
+ for path, branch in isolation._registered_worktrees(worktree).items()
209
+ if branch and is_bound_branch(branch.removeprefix("refs/heads/"))
210
+ ]
211
+ if len(matches) != 1:
212
+ raise isolation.IsolationError(
213
+ "dispatch records require exactly one primary worktree for the bound branch")
214
+ return matches[0]
215
+
216
+
217
+ def should_open_native_shell(root: Path, task_id: str, base: str) -> bool:
218
+ """Whether activate-task should append a native dispatch attempt record."""
219
+ task_dir = root / task_id
220
+ if not task_dir.is_dir():
221
+ return True
222
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
223
+ key=lambda path: attempt_number(path.parent))
224
+ if not attempts:
225
+ return True
226
+ latest = read_attempt(attempts[-1])
227
+ if latest.get("outcome") is None and not latest.get("command"):
228
+ return False
229
+ if latest.get("outcome") == "blocked" and str(latest.get("base")) == base:
230
+ return False
231
+ return True
232
+
233
+
234
+ def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str, task_file: str,
235
+ task_branch: Optional[str], mode: str, wave: Optional[int] = None,
236
+ title: Optional[str] = None, files: Optional[List[str]] = None,
237
+ sidecar: Optional[object] = None, member: Optional[str] = None,
238
+ member_base: Optional[str] = None, contract_file: Optional[str] = None) -> None:
239
+ """Open a dispatch attempt record for a host-spawned coder before it runs."""
240
+ root = records_root(primary)
241
+ root.mkdir(parents=True, exist_ok=True)
242
+ record: Dict[str, object] = {
243
+ "task_id": task_id,
244
+ "base": base,
245
+ "worktree": worktree,
246
+ "task_file": task_file,
247
+ "task_branch": task_branch,
248
+ "mode": mode,
249
+ "origin": "dispatch",
250
+ "outcome": None,
251
+ "dispatched_at": now(),
252
+ "native": True,
253
+ }
254
+ if wave is not None:
255
+ record["wave"] = wave
256
+ if title is not None:
257
+ record["title"] = title
258
+ if files is not None:
259
+ record["files"] = files
260
+ if sidecar is not None:
261
+ record["sidecar"] = sidecar
262
+ if member is not None:
263
+ record["member"] = member
264
+ if member_base is not None:
265
+ record["member_base"] = member_base
266
+ if contract_file is not None:
267
+ record["contract_file"] = contract_file
268
+ task_dir = root / task_id
269
+ if not should_open_native_shell(root, task_id, base):
270
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
271
+ key=lambda path: attempt_number(path.parent))
272
+ if not attempts:
273
+ return
274
+ state_path = attempts[-1]
275
+ state = load_state(state_path)
276
+ if state.get("outcome") is not None or state.get("command"):
277
+ return
278
+ attempt = state.get("attempt")
279
+ state.update(record)
280
+ if attempt is not None:
281
+ state["attempt"] = attempt
282
+ state["outcome"] = None
283
+ for key in ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit",
284
+ "answer", "command", "usage_recorded"):
285
+ state.pop(key, None)
286
+ save_state(state_path, state)
287
+ exit_path = state_path.with_name("exit.json")
288
+ if exit_path.is_file():
289
+ exit_path.unlink()
290
+ return
291
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
292
+ attempt_dir = task_dir / f"attempt-{attempt}"
293
+ attempt_dir.mkdir(parents=True)
294
+ record["attempt"] = attempt
295
+ save_state(attempt_dir / "state.json", record)
296
+
297
+
298
+ def record_native_activation(worktree: Path, activation: Dict[str, object]) -> None:
299
+ """Record a native task activation in the milestone dispatch ledger."""
300
+ primary = dispatch_primary(worktree)
301
+ task_file = str(activation["task_file"])
302
+ task_path = worktree / task_file
303
+ fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
304
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
305
+ wave = None
306
+ if fields and fields.get("wave") is not None:
307
+ wave = int(fields["wave"])
308
+ open_native_shell(
309
+ primary,
310
+ task_id=task_id,
311
+ base=str(activation["base"]),
312
+ worktree=str(activation["worktree"]),
313
+ task_file=task_file,
314
+ task_branch=activation.get("task_branch"),
315
+ mode="parallel" if activation.get("task_branch") else "serial",
316
+ wave=wave,
317
+ title=str(fields.get("title")) if fields else None,
318
+ files=list(fields.get("files") or []) if fields else None,
319
+ )
320
+
321
+
322
+ def record_native_member_activation(coordinator: Path, activation: Dict[str, object]) -> None:
323
+ """Record a native member-task activation in the coordinator dispatch ledger."""
324
+ task_file = str(activation["task_file"])
325
+ sidecar = Path(str(activation["worktree"]))
326
+ copy = Path(str(activation["copy"]))
327
+ fields, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8"))
328
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
329
+ wave = None
330
+ if fields and fields.get("wave") is not None:
331
+ wave = int(fields["wave"])
332
+ open_native_shell(
333
+ coordinator,
334
+ task_id=task_id,
335
+ base=str(activation["base"]),
336
+ worktree=str(sidecar),
337
+ task_file=copy.relative_to(sidecar).as_posix(),
338
+ task_branch=str(activation["task_branch"]),
339
+ mode="member",
340
+ wave=wave,
341
+ title=str(fields.get("title")) if fields else None,
342
+ files=list(fields.get("files") or []) if fields else None,
343
+ member=str(activation["member"]),
344
+ member_base=str(activation["member_base"]),
345
+ contract_file=task_file,
346
+ )
347
+
348
+
349
+ def reusable_shell_attempt(root: Path, state: Dict[str, object]) -> Optional[Path]:
350
+ """A native activation shell the driver can attach its child wrapper to."""
351
+ task_dir = root / str(state["task_id"])
352
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
353
+ key=lambda path: attempt_number(path.parent))
354
+ if not attempts:
355
+ return None
356
+ prior = read_attempt(attempts[-1])
357
+ if prior.get("outcome") is not None or prior.get("command") or prior.get("pid"):
358
+ return None
359
+ if str(prior.get("base")) != str(state.get("base")):
360
+ return None
361
+ return attempts[-1].parent
362
+
363
+
110
364
  def acquire_lock(primary: Path) -> BinaryIO:
111
365
  root = records_root(primary)
112
366
  root.mkdir(parents=True, exist_ok=True)
@@ -307,7 +561,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
307
561
  raise DriverStop('cannot redispatch while the previous child is active')
308
562
  if prior.get('model_selection'):
309
563
  state['model_selection'] = prior['model_selection']
310
- else:
564
+ elif prior.get('command'):
311
565
  state['command'] = prior['command']
312
566
  reassignment = task_dir / 'model-reassignment.json'
313
567
  if reassignment.exists():
@@ -323,13 +577,23 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
323
577
  state = retain_selection(root, state)
324
578
  task_dir = root / str(state["task_id"])
325
579
  argv, selection = selected_command(state, options, command or options.child_command)
326
- attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
327
- attempt_dir = task_dir / f"attempt-{attempt}"
328
- attempt_dir.mkdir(parents=True)
580
+ attempt_dir = reusable_shell_attempt(root, state)
581
+ if attempt_dir is None:
582
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
583
+ attempt_dir = task_dir / f"attempt-{attempt}"
584
+ attempt_dir.mkdir(parents=True)
585
+ else:
586
+ attempt = attempt_number(attempt_dir)
329
587
  stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
330
588
  "usage_recorded")
331
- state = {key: value for key, value in state.items()
332
- if not key.startswith("_") and key not in stale}
589
+ incoming = {key: value for key, value in state.items()
590
+ if not key.startswith("_") and key not in stale}
591
+ if (attempt_dir / "state.json").is_file():
592
+ preserved = {key: value for key, value in read_attempt(attempt_dir / "state.json").items()
593
+ if not key.startswith("_") and key not in stale}
594
+ state = {**preserved, **incoming}
595
+ else:
596
+ state = incoming
333
597
  if selection is not None:
334
598
  state['model_selection'] = selection
335
599
  state.update({"attempt": attempt, "command": argv,
@@ -384,6 +648,15 @@ def run_verify(command: str, cwd: Path) -> Dict[str, object]:
384
648
  return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
385
649
 
386
650
 
651
+ def landing_verify(command: str, regressions: List[Dict[str, object]],
652
+ cwd: Path) -> Tuple[Dict[str, object], List[Dict[str, object]]]:
653
+ """The task's own Verify, then every regression Verify when the own one passes."""
654
+ execution = run_verify(command, cwd)
655
+ if execution["exit_code"]:
656
+ return execution, []
657
+ return execution, [{**item, **run_verify(str(item["command"]), cwd)} for item in regressions]
658
+
659
+
387
660
  def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
388
661
  """A finish-able state derived from the task frontmatter alone, for coders dispatched by hand."""
389
662
  tasks_dir = primary / ".project/tasks"
@@ -437,6 +710,22 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
437
710
 
438
711
  def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
439
712
  """Verify in the isolate, land, record the ledger, retire. Returns the landing receipt."""
713
+ landing = land_task(primary, state)
714
+ try:
715
+ if state["mode"] == "member":
716
+ isolation.retire_member_task(primary, str(state["member"]), str(state["task_id"]))
717
+ else:
718
+ isolation.retire(primary, Path(str(state["worktree"])), state.get("task_branch") or None, False)
719
+ except isolation.IsolationError as error:
720
+ # The landing is proven, so a failed removal is a warning; Round.recover checks the isolate again.
721
+ landing["cleanup_warning"] = (f"{error}; the task is landed, but its isolate was not removed. The next "
722
+ "round or finish checks the isolate again and stops if the isolate is "
723
+ "still there")
724
+ return landing
725
+
726
+
727
+ def land_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
728
+ """Verify in the isolate, land, record the ledger. Returns the landing receipt."""
440
729
  worktree = Path(str(state["worktree"]))
441
730
  task_id = str(state["task_id"])
442
731
  task_file = str(state["task_file"])
@@ -445,39 +734,59 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
445
734
  command = _common.task_verify_command(task_path.read_text(encoding="utf-8"))
446
735
  if not command:
447
736
  raise DriverStop("task has no Verify command", task=task_id)
737
+ # A later task can break an earlier task that shares a file; that task's Verify also decides this landing.
738
+ regressions = build_state.regression_verifies(primary, task_id, command)
739
+ # A plan-approved `supersedes` retires an earlier Verify; the receipt and the Log show each one.
740
+ retired = build_state.retired_verifies(primary, task_id)
741
+ superseded = {"retired": retired} if retired else {}
448
742
  if state["mode"] == "serial":
449
- # ponytail: sidecar reproduction is git plumbing that belongs in isolation.py as the
450
- # inverse of clean_verify; lift it there when a second caller appears.
451
743
  sidecar = state["sidecar"]
452
- sidecar_path = Path(str(sidecar["worktree"]))
744
+ ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
745
+ sidecar_path = Path(str(ensured["worktree"]))
453
746
  tree = snapshot_tree(primary)
454
- isolation.git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
455
- if isolation.git_output(sidecar_path, "write-tree") != tree:
456
- raise DriverStop("sidecar reproduction differs from the primary task diff", task=task_id)
457
- execution = run_verify(command, sidecar_path)
747
+ try:
748
+ isolation.reproduce_verify_sidecar(sidecar_path, tree)
749
+ except isolation.IsolationError as error:
750
+ raise DriverStop(str(error), task=task_id) from error
751
+ execution, reruns = landing_verify(command, regressions, sidecar_path)
458
752
  location = f"sidecar {sidecar['branch']}"
459
753
  isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
460
754
  isolation.retire(primary, sidecar_path, str(sidecar["branch"]), False)
461
755
  else:
462
- execution = run_verify(command, worktree)
756
+ execution, reruns = landing_verify(command, regressions, worktree)
463
757
  location = f"isolate {state['task_branch']}"
464
758
  passed = execution["exit_code"] == 0
759
+ broken = [item for item in reruns if item["exit_code"]]
465
760
  execution.update({"worktree": str(worktree), "location": location})
466
761
  evidence_dir = (Path(str(state["_path"])).parent if state.get("_path")
467
762
  else records_root(primary) / task_id / f"manual-{now().replace(':', '')}")
468
763
  evidence_dir.mkdir(parents=True, exist_ok=True)
469
764
  _common.atomic_write(evidence_dir / "verify.json",
470
- json.dumps(execution, indent=2, sort_keys=True) + "\n")
765
+ json.dumps({**execution, "regressions": reruns, **superseded},
766
+ indent=2, sort_keys=True) + "\n")
471
767
  execution["evidence"] = str(evidence_dir / "verify.json")
472
- output = "\n".join(f" {line}" for line in tail(execution["stdout"] + execution["stderr"]).splitlines())
473
- append_log(task_path, f"- {now()[:10]} — orchestrator Verify ({location}): "
474
- f"{'pass' if passed else 'fail'}, exit {execution['exit_code']}"
475
- + (f"; output tail:\n ```\n{output}\n ```" if output else ""))
476
- if not passed:
768
+ # The Verify of a superseding task runs in the place of the retired Verify: the label names both.
769
+ for label, run in [("Verify", execution),
770
+ *((f"regression Verify of {', '.join(item['tasks'])}"
771
+ + (f" (supersedes {', '.join(item['supersedes'])})" if item.get("supersedes") else ""),
772
+ item) for item in reruns)]:
773
+ output = "\n".join(f" {line}" for line in tail(run["stdout"] + run["stderr"]).splitlines())
774
+ append_log(task_path, f"- {now()[:10]} — orchestrator {label} ({location}): "
775
+ f"{'fail' if run['exit_code'] else 'pass'}, exit {run['exit_code']}"
776
+ + (f"; output tail:\n ```\n{output}\n ```" if output else ""))
777
+ for item in retired:
778
+ part = f": part of {item['part']}" if item.get("part") else ""
779
+ append_log(task_path, f"- {now()[:10]} — orchestrator regression Verify of {item['task']}{part} retired: "
780
+ f"superseded by {item['superseded_by']}")
781
+ if not passed or broken:
477
782
  if state["mode"] == "parallel":
478
783
  isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
479
784
  elif state["mode"] == "member":
480
785
  isolation.deactivate_member_task(primary, str(state["member"]), task_id)
786
+ if passed:
787
+ names = ", ".join(name for item in broken for name in item["tasks"])
788
+ raise DriverStop(f"regression Verify of {names} failed in the isolate", task=task_id,
789
+ execution=execution, regressions=broken)
481
790
  raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
482
791
  if state["mode"] == "member":
483
792
  member = str(state["member"])
@@ -488,9 +797,9 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
488
797
  ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
489
798
  if ledger:
490
799
  build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
491
- isolation.retire_member_task(primary, member, task_id)
492
800
  return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
493
- "landing": landing, "verify": execution, "ledger": ledger}
801
+ "landing": landing, "verify": execution, "regressions": reruns, **superseded,
802
+ "ledger": ledger}
494
803
  landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
495
804
  list(state["files"]))
496
805
  commit = str(landed["commit"])
@@ -498,9 +807,8 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
498
807
  ledger = isolation.git_output(primary, "rev-parse", f"{commit}^") == base
499
808
  if ledger:
500
809
  build_state.verify_record(str(primary), command, commit, "pass", execution=execution)
501
- isolation.retire(primary, worktree, state.get("task_branch") or None, False)
502
810
  return {"task": task_id, "commit": commit, "mode": landed["mode"], "verify": execution,
503
- "ledger": ledger}
811
+ "regressions": reruns, **superseded, "ledger": ledger}
504
812
 
505
813
 
506
814
  # --- shared by round and review --------------------------------------------
@@ -516,6 +824,11 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
516
824
  report = isolation.recover(primary, Path(project_dir) / "tasks")
517
825
  receipt["steps"].append({"script": "isolation.py recover", "result": report})
518
826
  if report["verdict"] == "block":
827
+ blocked = [task for task in report["tasks"] if task["verdict"] == "block"]
828
+ if blocked and all(task.get("leftover") for task in blocked):
829
+ # Every blocked task is landed: the stop names only the isolates to remove, and the build stays active.
830
+ raise DriverStop("; ".join(LEFTOVER_ISOLATE.format(task["task_id"], task["leftover"])
831
+ for task in blocked), recover=blocked)
519
832
  raise DriverStop(RECOVERY_BLOCKED, recover=report)
520
833
  return report
521
834
 
@@ -651,7 +964,7 @@ class Round:
651
964
  self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
652
965
  "result": result})
653
966
  if result["state"] == "landed":
654
- isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
967
+ self.retire_member(str(result["member"]), journal.stem, str(result["landing"]))
655
968
  retired_members.add(journal.stem)
656
969
  report = recover_report(self.primary, self.project_dir, self.receipt)
657
970
  for task in report["tasks"]:
@@ -659,26 +972,76 @@ class Round:
659
972
  verdict = task["verdict"]
660
973
  if verdict in ("recovered", "attested"):
661
974
  self.proven[str(task.get("task_id"))] = str(task.get("commit"))
975
+ if (verdict == "reconcile" and task.get("status") == "in-progress"
976
+ and task.get("task_branch") is None):
977
+ task_id = str(task.get("task_id"))
978
+ verify_name = f"task-{task_id.lower()}-verify"
979
+ verify_path = isolation.sidecar_root(self.primary, "verify", verify_name)
980
+ verify_branch = isolation.verify_branch_name(verify_name)
981
+ diagnose = shlex.join([
982
+ sys.executable,
983
+ "-B",
984
+ str(Path(__file__).with_name("pipeline_diagnose.py")),
985
+ "diagnose",
986
+ "--repo",
987
+ str(self.primary),
988
+ ])
989
+ raise DriverStop(
990
+ f"task {task_id} needs recovery: {task.get('reason')}; inspect Verify "
991
+ f"sidecar {verify_path} ({verify_branch}) before resetting it to pending; "
992
+ f"inventory it with `{diagnose}`",
993
+ recover=task,
994
+ )
662
995
  if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
663
- isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
996
+ self.retire_member(str(task["member"]), str(task["task_id"]), str(task["landing"]))
664
997
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
665
998
  isolation.retire(self.primary, Path(str(worktree["path"])),
666
999
  str(task["task_branch"]), False)
667
- elif (verdict == "recovered" and worktree is None and task.get("task_branch")) or (
668
- verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry"))):
669
- # ponytail: interrupted retirements and retries stay with the parent's documented
1000
+ elif verdict == "recovered" and worktree is None and task.get("task_branch"):
1001
+ # The isolate is gone and the landing is proven: retire proves the branch, then deletes it.
1002
+ isolation.retire(self.primary, None, str(task["task_branch"]), True, str(task["commit"]))
1003
+ elif verdict == "resume" and (task.get("landing_retry") or task.get("dispatch_retry")):
1004
+ # ponytail: interrupted retries stay with the parent's documented
670
1005
  # procedure; failed/blocked reconciliation surfaces through ready's typed error.
671
1006
  raise DriverStop(f"task {task.get('task_id')} needs recovery: {verdict}", recover=task)
672
1007
 
1008
+ def retire_member(self, member: str, task_id: str, landing: str) -> None:
1009
+ """Retire a landed member task's sidecar; a proven leftover of it is a stop that names only its path."""
1010
+ checkout, project, _ = isolation._member_context(self.primary, member)
1011
+ sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}")
1012
+ branch = f"refs/heads/{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}"
1013
+ tip = isolation.run_git(checkout, "rev-parse", "--verify", "--quiet", branch)
1014
+ commits = [landing, *tip.stdout.split()]
1015
+ try:
1016
+ isolation.retire_member_task(self.primary, member, task_id)
1017
+ except isolation.RemovalError as error:
1018
+ # Missing files are a removal that stopped part way; every other state keeps the refusal.
1019
+ if not (isolation.only_missing_files(sidecar)
1020
+ or isolation.holds_only_landed_files(checkout, sidecar, commits)):
1021
+ raise
1022
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar), cleanup_error=str(error))
1023
+ if isolation.holds_only_landed_files(checkout, sidecar, commits):
1024
+ raise DriverStop(LEFTOVER_ISOLATE.format(task_id, sidecar))
1025
+
673
1026
  # settle exited children ---------------------------------------------
674
1027
 
675
1028
  def isolate_live(self, state: Dict[str, object]) -> bool:
676
- """True while the isolate still holds the task as in-progress; the parent's recovery clears it."""
1029
+ """Keep blocked attempts reportable until the corresponding task is reset."""
677
1030
  task_path = Path(str(state["worktree"])) / str(state["task_file"])
678
1031
  if not task_path.is_file():
679
1032
  return False
680
1033
  fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
681
- return fields is not None and fields.get("status") == "in-progress"
1034
+ if fields is None:
1035
+ return False
1036
+ if fields.get("status") == "in-progress":
1037
+ return True
1038
+ return (
1039
+ state.get("mode") == "serial"
1040
+ and fields.get("status") == "blocked"
1041
+ and fields.get("id") == state.get("task_id")
1042
+ and fields.get("base") == state.get("base")
1043
+ and fields.get("task_branch") == "null"
1044
+ )
682
1045
 
683
1046
  def settle(self) -> bool:
684
1047
  """Classify every exited child. Returns True when a landing or redispatch changed the round."""
@@ -717,8 +1080,15 @@ class Round:
717
1080
  elif state.get("finished_at") is None:
718
1081
  if child_running(state):
719
1082
  self.receipt["in_flight"].append(self.summary(state))
1083
+ elif (state.get("native") and not state.get("command") and not state.get("pid")
1084
+ and self.isolate_live(state)):
1085
+ continue
720
1086
  else:
721
- self.fail(state, ORPHANED)
1087
+ state.update(read_attempt(Path(str(state["_path"]))))
1088
+ if state.get("finished_at") is not None:
1089
+ changed = self.classify(state) or changed
1090
+ else:
1091
+ self.fail(state, ORPHANED)
722
1092
  else:
723
1093
  changed = self.classify(state) or changed
724
1094
  return changed
@@ -729,6 +1099,35 @@ class Round:
729
1099
  update_state(state, outcome="blocked", reason=reason)
730
1100
  self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
731
1101
 
1102
+ def blocked_result(self, state: Dict[str, object], **diagnostics: object) -> bool:
1103
+ """Record an explicit serial block and retire only its untouched Verify sidecar."""
1104
+ reason = "child returned blocked"
1105
+ details: Dict[str, object] = dict(diagnostics)
1106
+ if state.get("mode") == "serial":
1107
+ try:
1108
+ task = isolation.block_serial_task(
1109
+ self.primary,
1110
+ str(state["task_file"]),
1111
+ str(state["task_id"]),
1112
+ str(state["base"]),
1113
+ )
1114
+ except STOP_ERRORS as error:
1115
+ self.fail(state, f"{reason}; could not record blocked task: {error}")
1116
+ return False
1117
+ details["task"] = task
1118
+ sidecar = state.get("sidecar")
1119
+ if isinstance(sidecar, dict):
1120
+ worktree = Path(str(sidecar["worktree"]))
1121
+ branch = str(sidecar["branch"])
1122
+ try:
1123
+ details["verify_cleanup"] = isolation.retire_unused_verify(
1124
+ self.primary, worktree, str(state["base"]), branch
1125
+ )
1126
+ except STOP_ERRORS as error:
1127
+ reason += f"; Verify sidecar retained at {worktree}: {error}"
1128
+ self.fail(state, reason, **details)
1129
+ return False
1130
+
732
1131
  def classify(self, state: Dict[str, object]) -> bool:
733
1132
  attempt_dir = Path(str(state["_path"])).parent
734
1133
  if self.budgeted and not state.get("usage_recorded"):
@@ -756,6 +1155,10 @@ class Round:
756
1155
  update_state(state, outcome="question", question=unanswered[0], answered=False)
757
1156
  self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
758
1157
  "log_delta": delta.strip()})
1158
+ elif verdict == "blocked" and named == str(state["task_id"]).lower():
1159
+ return self.blocked_result(
1160
+ state, stdout_tail=tail(message), log_delta=delta.strip()
1161
+ )
759
1162
  elif verdict != "ready" or named != str(state["task_id"]).lower():
760
1163
  self.fail(state, "child returned blocked" if verdict == "blocked"
761
1164
  else "child returned no RESULT line", stdout_tail=tail(message),
@@ -790,8 +1193,14 @@ class Round:
790
1193
  # dispatch -------------------------------------------------------------
791
1194
 
792
1195
  def in_flight_states(self) -> List[Dict[str, object]]:
793
- return [state for state in latest_states(self.root)
794
- if state.get("outcome") is None and state.get("wave") == self.receipt["wave"]]
1196
+ states = []
1197
+ for state in latest_states(self.root):
1198
+ if state.get("outcome") is not None or state.get("wave") != self.receipt["wave"]:
1199
+ continue
1200
+ if state.get("native") and not state.get("command") and not state.get("pid"):
1201
+ continue
1202
+ states.append(state)
1203
+ return states
795
1204
 
796
1205
  def checkpoint_bookkeeping(self) -> None:
797
1206
  open_ids = {state["task_id"] for state in latest_states(self.root)
@@ -815,6 +1224,24 @@ class Round:
815
1224
  ready = build_state.ready(str(self.primary), self.project_dir)
816
1225
  self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
817
1226
  wave = ready["current_wave"]
1227
+ blocked_review = ready.get("blocked_review")
1228
+ if blocked_review:
1229
+ waiting_waves = blocked_review["waiting_waves"]
1230
+ if self.receipt["wave"] in waiting_waves:
1231
+ raise DriverStop(
1232
+ f"wave {self.receipt['wave']} has unfinished tasks but wave "
1233
+ f"{blocked_review['wave']} review cycle {blocked_review['cycle']} "
1234
+ "is blocked; it must pass before dispatch")
1235
+ self.checkpoint_bookkeeping()
1236
+ return not in_flight
1237
+ repair_for_wave = ready.get("repair_for_wave")
1238
+ if repair_for_wave is not None and self.receipt["wave"] != wave:
1239
+ if self.receipt["wave"] > repair_for_wave:
1240
+ raise DriverStop(
1241
+ f"wave {self.receipt['wave']} is waiting on blocked wave {repair_for_wave}; "
1242
+ f"dispatch repair wave {wave} first")
1243
+ self.checkpoint_bookkeeping()
1244
+ return not in_flight
818
1245
  if wave is not None and wave < self.receipt["wave"]:
819
1246
  raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
820
1247
  self.checkpoint_bookkeeping()
@@ -881,7 +1308,7 @@ class Round:
881
1308
  continue
882
1309
  prepared = self.runner("prepare-task", head, task["id"], round_size)
883
1310
  isolate = prepared[0]["result"]
884
- sidecar = prepared[1]["result"] if len(prepared) > 1 else None
1311
+ sidecar = prepared[-1]["result"] if len(prepared) > 1 else None # the isolate-verify step is last
885
1312
  worktree = Path(str(isolate["worktree"]))
886
1313
  isolation.activate_task(worktree, head, task["id"], f"build_{task['id'].lower()}",
887
1314
  task["task_file"], isolate["task_branch"])
@@ -1657,6 +2084,23 @@ def next_task_id(tasks_dir: Path) -> str:
1657
2084
  return f"T{max(numbers, default=0) + 1:03d}"
1658
2085
 
1659
2086
 
2087
+ def replaced_review_base(primary: Path, project_dir: str, wave: int, cycle: int) -> Optional[str]:
2088
+ """The recovery base when this cycle's review replaces the one a build recovery set aside, else None."""
2089
+ recovery = pipeline_state._build_recovery().context(primary)
2090
+ if not recovery:
2091
+ return None
2092
+ relative = f"{project_dir}/review/wave-{wave}.cycle{cycle}.md"
2093
+ before = isolation.run_git(primary, "show", f"{recovery['base']}:{relative}")
2094
+ current = primary / relative
2095
+ # Plan approval restores the review of an unchanged wave before its checkpoint, so no commit
2096
+ # since the base touches that file. A set-aside review is deleted by that checkpoint.
2097
+ rewritten = isolation.run_git(primary, "log", "-1", "--format=%H", f"{recovery['base']}..HEAD", "--", relative)
2098
+ if (not rewritten.stdout.strip() and before.returncode == 0 and current.is_file()
2099
+ and current.read_text(encoding="utf-8") == before.stdout):
2100
+ return None # the restored review keeps its repairs
2101
+ return recovery["base"]
2102
+
2103
+
1660
2104
  def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1661
2105
  """Step 7: one fix task per findings batch, carrying failed criteria and observations verbatim."""
1662
2106
  receipt: Dict[str, object] = {"status": None, "wave": options.wave, "cycle": options.cycle,
@@ -1665,14 +2109,32 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1665
2109
  project = primary / options.project_dir
1666
2110
  tasks_dir = project / "tasks"
1667
2111
  repairs = []
2112
+ pending = []
1668
2113
  for path in sorted(tasks_dir.glob("*-fix-wave-*-cycle-*.md")):
1669
2114
  text = path.read_text(encoding="utf-8")
1670
2115
  fields, _ = isolation.task_frontmatter(text)
1671
2116
  repairs.append({"task": fields["id"], "path": path.relative_to(primary).as_posix(),
1672
- "wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or "")})
2117
+ "wave": int(fields["wave"]), "locators": re.findall(r"(?m)^### (.+)$", _common.section_body(text, "Review findings") or ""),
2118
+ **({"repo": fields["repo"]} if fields.get("repo") else {})})
2119
+ if fields["status"] != "done":
2120
+ pending.append(repairs[-1])
1673
2121
  suffix = f"-fix-wave-{options.wave}-cycle-{options.cycle}.md"
1674
- existing = [item for item in repairs if item["path"].endswith(suffix)]
1675
- carried = {locator for item in repairs if item not in existing for locator in item["locators"]}
2122
+ # A build recovery restarts a changed wave at cycle 1, so the repairs and the PLAN heading
2123
+ # of the review it set aside carry the same names. They belong to that review, not this one.
2124
+ replaced = replaced_review_base(primary, options.project_dir, options.wave, options.cycle)
2125
+
2126
+ def of_replaced_review(relative: str) -> bool:
2127
+ if not replaced or isolation.run_git(primary, "show", f"{replaced}:{relative}").returncode != 0:
2128
+ return False
2129
+ return not isolation.run_git(primary, "log", "--diff-filter=AD", "--format=%H",
2130
+ f"{replaced}..HEAD", "--", relative).stdout.strip()
2131
+
2132
+ stale = [item for item in repairs if item["path"].endswith(suffix) and of_replaced_review(item["path"])]
2133
+ existing = [item for item in repairs if item["path"].endswith(suffix) and item not in stale]
2134
+ # A landed repair no longer carries its finding: a repeat proves that fix failed and needs the next one.
2135
+ # Two repos can carry the same locator, so a repair carries a finding only in its own repo.
2136
+ carried = {(item.get("repo"), locator) for item in pending if not item["path"].endswith(suffix)
2137
+ for locator in item["locators"]}
1676
2138
  findings = review_findings.compute(primary, options.project_dir, options.wave, options.cycle)
1677
2139
  receipt["findings"] = findings
1678
2140
  escalation = [key for key in ("structural_blockers", "skeptic_groups", "cap_reached", "all_refuted")
@@ -1686,15 +2148,17 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1686
2148
  texts = contracts._task_texts(primary, options.project_dir)
1687
2149
  groups = {group["locator"]: group for group in findings["groups"]}
1688
2150
  today = now()[:10]
1689
- # Same-wave file overlap is forbidden, so repairs land in a newly appended wave.
2151
+ # Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
1690
2152
  plan_path = project / "plan/PLAN.md"
1691
2153
  plan_text = plan_path.read_text(encoding="utf-8")
1692
2154
  depths, waves = contracts._plan_waves(plan_text)
1693
2155
  repair_waves = {item["wave"] for item in existing}
1694
2156
  if len(repair_waves) > 1:
1695
2157
  raise DriverStop("repair tasks for this cycle disagree on their wave")
1696
- repair_heading = re.search(
2158
+ stale_waves = {item["wave"] for item in stale}
2159
+ repair_heading = next((heading for heading in re.finditer(
1697
2160
  rf"(?m)^## Wave (\d+) — fix wave {options.wave} cycle {options.cycle} review findings$", plan_text)
2161
+ if int(heading.group(1)) not in stale_waves), None)
1698
2162
  if repair_waves:
1699
2163
  fix_wave = next(iter(repair_waves))
1700
2164
  elif repair_heading:
@@ -1705,9 +2169,10 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1705
2169
  raise DriverStop("repair tasks disagree with the PLAN repair wave")
1706
2170
  receipt["existing"] = existing
1707
2171
  for batch in findings["fix_batches"]:
1708
- if any(set(batch["locators"]) <= set(item["locators"]) for item in existing):
2172
+ repo = batch.get("repo") # a member name, or None for the coordinator
2173
+ if any(item.get("repo") == repo and set(batch["locators"]) <= set(item["locators"]) for item in existing):
1709
2174
  continue
1710
- if all(locator in carried for locator in batch["locators"]):
2175
+ if all((repo, locator) in carried for locator in batch["locators"]):
1711
2176
  receipt["carried"].append(batch)
1712
2177
  continue
1713
2178
  batch_groups = [groups[locator] for locator in batch["locators"]]
@@ -1715,9 +2180,15 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1715
2180
  for source in sources:
1716
2181
  if source not in texts:
1717
2182
  raise DriverStop(f"fix batch names unknown task {source}")
2183
+ # One task changes one repo: the fix depends on, and runs the Verify of, the source tasks of its repo.
2184
+ sources = [source for source in sources if isolation.task_frontmatter(texts[source])[0].get("repo") == repo]
2185
+ # A repeat builds on the landed repairs of its finding: repair-evidence links them by this dependency.
2186
+ deps = sources + [item["task"] for item in repairs
2187
+ if item not in pending and item.get("repo") == repo
2188
+ and f"-fix-wave-{options.wave}-cycle-" in item["path"] and not item["path"].endswith(suffix)
2189
+ and set(item["locators"]) & set(batch["locators"])]
1718
2190
  task_id = next_task_id(tasks_dir)
1719
- verify = "set -e\n" + "\n".join( # each source Verify in its own subshell
1720
- f"(\n{command}\n)" for command in dict.fromkeys(_common.task_verify_command(texts[source]) for source in sources))
2191
+ verify = _common.fix_verify(_common.task_verify_command(texts[source]) for source in sources)
1721
2192
  heavy = any(re.search(r"(?m)^Heavy:\s*yes", texts[source]) for source in sources)
1722
2193
  criteria = "\n".join(f"{index}. {criterion}" for index, criterion in
1723
2194
  enumerate(dict.fromkeys(group["criterion"] for group in batch_groups), 1))
@@ -1725,18 +2196,19 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1725
2196
  f"### {group['locator']}\nCriterion: {group['criterion']}\n"
1726
2197
  + "\n".join(f"- {item['text']}" for item in group["observations"]) for group in batch_groups)
1727
2198
  files = "\n".join(f" - {path}" for path in batch["files"])
2199
+ member = f"repo: {repo}\n" if repo else "" # the task template puts `repo:` before `files:`
1728
2200
  title = f"Fix wave {options.wave} cycle {options.cycle} review findings in {', '.join(sources)}"
1729
2201
  text = f"""---
1730
2202
  id: {task_id}
1731
2203
  title: {title}
1732
2204
  wave: {fix_wave}
1733
- deps: [{', '.join(sources)}]
2205
+ deps: [{', '.join(deps)}]
1734
2206
  status: pending
1735
2207
  agent: null
1736
2208
  base: null
1737
2209
  worktree: null
1738
2210
  task_branch: null
1739
- files:
2211
+ {member}files:
1740
2212
  {files}
1741
2213
  ---
1742
2214
 
@@ -1785,8 +2257,8 @@ Heavy: {'yes' if heavy else 'no'}
1785
2257
  path = tasks_dir / f"{task_id}-fix-wave-{options.wave}-cycle-{options.cycle}.md"
1786
2258
  _common.atomic_write(path, text)
1787
2259
  receipt["created"].append({"task": task_id, "path": path.relative_to(primary).as_posix(),
1788
- "wave": fix_wave, "deps": sources, "files": batch["files"],
1789
- "locators": batch["locators"]})
2260
+ "wave": fix_wave, "deps": deps, "files": batch["files"],
2261
+ "locators": batch["locators"], **({"repo": repo} if repo else {})})
1790
2262
  rows = []
1791
2263
  for item in [*existing, *receipt["created"]]:
1792
2264
  fields, _ = isolation.task_frontmatter((primary / item["path"]).read_text(encoding="utf-8"))
@@ -1820,7 +2292,8 @@ Heavy: {'yes' if heavy else 'no'}
1820
2292
  receipt["plan_wave"] = fix_wave
1821
2293
  lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
1822
2294
  "--repo", str(primary), "--base", isolation.current_sha(primary),
1823
- "--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, encoding="utf-8", errors="replace")
2295
+ "--tasks-dir", f"{options.project_dir}/tasks",
2296
+ "--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
1824
2297
  receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
1825
2298
  "stdout": lint.stdout, "stderr": lint.stderr})
1826
2299
  if lint.returncode:
@@ -1935,7 +2408,11 @@ def positive_int(value: str) -> int:
1935
2408
 
1936
2409
 
1937
2410
  def default_resource(name: str) -> Optional[Path]:
1938
- candidate = Path(__file__).resolve().parent.parent / name
2411
+ script_root = Path(__file__).resolve().parent.parent
2412
+ candidate = script_root / name
2413
+ if candidate.is_file():
2414
+ return candidate
2415
+ candidate = script_root / "skills" / "gsd-path" / name
1939
2416
  return candidate if candidate.is_file() else None
1940
2417
 
1941
2418
 
@@ -2026,6 +2503,7 @@ def main(argv=None) -> int:
2026
2503
  if arguments.action == "_child":
2027
2504
  return child_main(arguments.state)
2028
2505
  primary = arguments.repo.resolve()
2506
+ _activate_pinned_runtime(primary)
2029
2507
  lock = None
2030
2508
  try:
2031
2509
  if arguments.action != "status":
@@ -2065,6 +2543,10 @@ def main(argv=None) -> int:
2065
2543
  if native and native["base"] != records[0].get("base"):
2066
2544
  records = []
2067
2545
  else:
2546
+ if (native and records[0]["outcome"] == "blocked" and records[0].get("native")
2547
+ and not records[0].get("command")):
2548
+ # A failed finish leaves a host-dispatched task active at its base: finish may run again.
2549
+ update_state(records[0], outcome=None, reason=None)
2068
2550
  native = None
2069
2551
  if arguments.task_id in current.proven:
2070
2552
  commit = current.proven[arguments.task_id]
@@ -2078,8 +2560,22 @@ def main(argv=None) -> int:
2078
2560
  raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
2079
2561
  "use round")
2080
2562
  if record.get("finished_at") is None:
2081
- raise DriverStop(f"task {arguments.task_id} still has a running child")
2082
- current.classify(record)
2563
+ if child_running(record):
2564
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2565
+ if record.get("command"):
2566
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2567
+ live = state_from_task(primary, arguments.task_id)
2568
+ merged = dict(record, **live)
2569
+ merged["_path"] = record["_path"]
2570
+ try:
2571
+ landing = finish_task(primary, merged)
2572
+ except STOP_ERRORS as error:
2573
+ update_state(record, outcome="blocked", reason=str(error))
2574
+ raise
2575
+ update_state(record, outcome="landed", commit=landing["commit"])
2576
+ current.receipt["landed"].append(landing)
2577
+ else:
2578
+ current.classify(record)
2083
2579
  else: # a coder dispatched by hand: derive the isolate from the task frontmatter
2084
2580
  current.receipt["landed"].append(
2085
2581
  finish_task(primary, native or state_from_task(primary, arguments.task_id)))