@opengsd/gsd-path 1.3.1 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (481) hide show
  1. package/AGENTS.md +2 -207
  2. package/CHANGELOG.md +331 -69
  3. package/DOCS.md +25 -9
  4. package/FULL.md +12 -5
  5. package/GUIDE.md +1 -1
  6. package/HOOKS.md +104 -14
  7. package/QUICK.md +5 -2
  8. package/README.md +131 -111
  9. package/RUNTIME.md +2 -2
  10. package/UPDATE.md +61 -11
  11. package/WORKFLOW.md +44 -7
  12. package/package.json +13 -9
  13. package/platforms/shared-agents/dispatch.md +13 -0
  14. package/scripts/_common.py +619 -18
  15. package/scripts/archive_milestone.py +160 -86
  16. package/scripts/bootstrap_repository.py +8 -5
  17. package/scripts/build_recovery.py +100 -32
  18. package/scripts/build_state.py +143 -24
  19. package/scripts/check_docs_audit.py +91 -7
  20. package/scripts/check_handoffs.py +662 -50
  21. package/scripts/check_task_briefs.py +338 -30
  22. package/scripts/check_update.py +2 -2
  23. package/scripts/core_hook_gate.py +10 -1
  24. package/scripts/core_hook_settings.py +11 -4
  25. package/scripts/detect_project.py +587 -32
  26. package/scripts/discussion_records.py +19 -13
  27. package/scripts/discussion_validate.py +152 -50
  28. package/scripts/dispatch_driver.py +501 -46
  29. package/scripts/git_guard.py +118 -31
  30. package/scripts/guard_hook.py +1567 -194
  31. package/scripts/install.mjs +77 -46
  32. package/scripts/install.py +611 -50
  33. package/scripts/integration.py +580 -25
  34. package/scripts/isolation.py +1438 -57
  35. package/scripts/lean_verification.py +99 -19
  36. package/scripts/loop_run.py +31 -29
  37. package/scripts/members.py +706 -0
  38. package/scripts/migrate_core.py +2 -2
  39. package/scripts/path_config.py +5 -5
  40. package/scripts/pipeline_diagnose.py +82 -31
  41. package/scripts/pipeline_git.py +124 -34
  42. package/scripts/pipeline_state.py +287 -87
  43. package/scripts/pipeline_undo.py +218 -30
  44. package/scripts/promote_lookahead.py +1 -1
  45. package/scripts/review_panel.py +1 -1
  46. package/scripts/runtime_store.py +55 -11
  47. package/scripts/skill-resources.json +37 -5
  48. package/scripts/state_checkpoint.py +135 -44
  49. package/scripts/state_promote.py +2 -0
  50. package/scripts/status_runtime.py +58 -10
  51. package/scripts/sync_skill_resources.py +8 -7
  52. package/scripts/task_context.py +12 -3
  53. package/scripts/token_budget.py +12 -8
  54. package/scripts/wizard.mjs +1 -0
  55. package/scripts/workflow_run.py +107 -30
  56. package/scripts/worktree_paths.py +72 -10
  57. package/skills/gsd-path/BUILD.md +7 -1
  58. package/skills/gsd-path/DECIDE.md +11 -4
  59. package/skills/gsd-path/DEFINE.md +36 -4
  60. package/skills/gsd-path/DOCS-AUDIT.md +6 -4
  61. package/skills/gsd-path/INSPECT.md +5 -4
  62. package/skills/gsd-path/PLAN.md +18 -4
  63. package/skills/gsd-path/RESEARCH.md +13 -1
  64. package/skills/gsd-path/ROADMAP.md +2 -1
  65. package/skills/gsd-path/SHIP.md +33 -11
  66. package/skills/gsd-path/SKILL.md +45 -1
  67. package/skills/gsd-path/references/build-native.md +63 -15
  68. package/skills/gsd-path/references/codebase-mapper.md +2 -1
  69. package/skills/gsd-path/references/dispatch.md +13 -0
  70. package/skills/gsd-path/references/docs-auditor.md +7 -1
  71. package/skills/gsd-path/references/operating-rules.md +214 -0
  72. package/skills/gsd-path/references/planner.md +22 -4
  73. package/skills/gsd-path/references/reviewer-final.md +20 -2
  74. package/skills/gsd-path/references/reviewer-wave.md +18 -5
  75. package/skills/gsd-path/references/reviewer.md +3 -1
  76. package/skills/gsd-path/references/spec-probes.md +150 -0
  77. package/skills/gsd-path/scripts/_common.py +619 -18
  78. package/skills/gsd-path/scripts/archive_milestone.py +160 -86
  79. package/skills/gsd-path/scripts/bootstrap_repository.py +8 -5
  80. package/skills/gsd-path/scripts/build_recovery.py +100 -32
  81. package/skills/gsd-path/scripts/build_state.py +143 -24
  82. package/skills/gsd-path/scripts/check_docs_audit.py +91 -7
  83. package/skills/gsd-path/scripts/check_handoffs.py +662 -50
  84. package/skills/gsd-path/scripts/check_task_briefs.py +338 -30
  85. package/skills/gsd-path/scripts/check_update.py +2 -2
  86. package/skills/gsd-path/scripts/detect_project.py +587 -32
  87. package/skills/gsd-path/scripts/discussion_records.py +19 -13
  88. package/skills/gsd-path/scripts/discussion_validate.py +152 -50
  89. package/skills/gsd-path/scripts/dispatch_driver.py +501 -46
  90. package/skills/gsd-path/scripts/git_guard.py +118 -31
  91. package/skills/gsd-path/scripts/integration.py +580 -25
  92. package/skills/gsd-path/scripts/isolation.py +1438 -57
  93. package/skills/gsd-path/scripts/lean_verification.py +99 -19
  94. package/skills/gsd-path/scripts/loop_run.py +31 -29
  95. package/skills/gsd-path/scripts/members.py +706 -0
  96. package/skills/gsd-path/scripts/path_config.py +5 -5
  97. package/skills/gsd-path/scripts/pipeline_git.py +124 -34
  98. package/skills/gsd-path/scripts/pipeline_state.py +287 -87
  99. package/skills/gsd-path/scripts/promote_lookahead.py +1 -1
  100. package/skills/gsd-path/scripts/review_panel.py +1 -1
  101. package/skills/gsd-path/scripts/state_checkpoint.py +135 -44
  102. package/skills/gsd-path/scripts/state_promote.py +2 -0
  103. package/skills/gsd-path/scripts/status_runtime.py +58 -10
  104. package/skills/gsd-path/scripts/task_context.py +12 -3
  105. package/skills/gsd-path/scripts/token_budget.py +12 -8
  106. package/skills/gsd-path/scripts/workflow_run.py +107 -30
  107. package/skills/gsd-path/scripts/worktree_paths.py +72 -10
  108. package/skills/gsd-path/templates/codebase.md +1 -1
  109. package/skills/gsd-path/templates/docs-audit.md +6 -2
  110. package/skills/gsd-path/templates/final-review.md +16 -4
  111. package/skills/gsd-path/templates/gap-review.md +2 -1
  112. package/skills/gsd-path/templates/intent.md +30 -0
  113. package/skills/gsd-path/templates/patch-findings.md +3 -3
  114. package/skills/gsd-path/templates/plan.md +17 -2
  115. package/skills/gsd-path/templates/research-handoff.md +14 -3
  116. package/skills/gsd-path/templates/state.md +1 -1
  117. package/skills/gsd-path/templates/task.md +1 -0
  118. package/skills/gsd-path/templates/wave-review.md +16 -4
  119. package/skills/gsd-path-build/SKILL.md +7 -1
  120. package/skills/gsd-path-build/references/build-native.md +63 -15
  121. package/skills/gsd-path-build/references/dispatch.md +13 -0
  122. package/skills/gsd-path-build/references/operating-rules.md +214 -0
  123. package/skills/gsd-path-build/references/reviewer-final.md +20 -2
  124. package/skills/gsd-path-build/references/reviewer-wave.md +18 -5
  125. package/skills/gsd-path-build/references/reviewer.md +3 -1
  126. package/skills/gsd-path-build/references/spec-probes.md +150 -0
  127. package/skills/gsd-path-build/scripts/_common.py +619 -18
  128. package/skills/gsd-path-build/scripts/archive_milestone.py +160 -86
  129. package/skills/gsd-path-build/scripts/build_recovery.py +100 -32
  130. package/skills/gsd-path-build/scripts/build_state.py +143 -24
  131. package/skills/gsd-path-build/scripts/check_handoffs.py +662 -50
  132. package/skills/gsd-path-build/scripts/check_task_briefs.py +338 -30
  133. package/skills/gsd-path-build/scripts/detect_project.py +587 -32
  134. package/skills/gsd-path-build/scripts/discussion_records.py +19 -13
  135. package/skills/gsd-path-build/scripts/discussion_validate.py +152 -50
  136. package/skills/gsd-path-build/scripts/dispatch_driver.py +501 -46
  137. package/skills/gsd-path-build/scripts/integration.py +580 -25
  138. package/skills/gsd-path-build/scripts/isolation.py +1438 -57
  139. package/skills/gsd-path-build/scripts/loop_run.py +31 -29
  140. package/skills/gsd-path-build/scripts/members.py +706 -0
  141. package/skills/gsd-path-build/scripts/path_config.py +5 -5
  142. package/skills/gsd-path-build/scripts/pipeline_git.py +124 -34
  143. package/skills/gsd-path-build/scripts/pipeline_state.py +287 -87
  144. package/skills/gsd-path-build/scripts/promote_lookahead.py +1 -1
  145. package/skills/gsd-path-build/scripts/review_panel.py +1 -1
  146. package/skills/gsd-path-build/scripts/state_checkpoint.py +135 -44
  147. package/skills/gsd-path-build/scripts/state_promote.py +2 -0
  148. package/skills/gsd-path-build/scripts/task_context.py +12 -3
  149. package/skills/gsd-path-build/scripts/token_budget.py +12 -8
  150. package/skills/gsd-path-build/scripts/workflow_run.py +107 -30
  151. package/skills/gsd-path-build/scripts/worktree_paths.py +72 -10
  152. package/skills/gsd-path-build/templates/task.md +1 -0
  153. package/skills/gsd-path-build/templates/wave-review.md +16 -4
  154. package/skills/gsd-path-decide/SKILL.md +11 -4
  155. package/skills/gsd-path-decide/references/dispatch.md +13 -0
  156. package/skills/gsd-path-decide/references/operating-rules.md +214 -0
  157. package/skills/gsd-path-decide/scripts/_common.py +619 -18
  158. package/skills/gsd-path-decide/scripts/archive_milestone.py +160 -86
  159. package/skills/gsd-path-decide/scripts/build_recovery.py +100 -32
  160. package/skills/gsd-path-decide/scripts/check_handoffs.py +662 -50
  161. package/skills/gsd-path-decide/scripts/check_task_briefs.py +338 -30
  162. package/skills/gsd-path-decide/scripts/detect_project.py +587 -32
  163. package/skills/gsd-path-decide/scripts/discussion_records.py +19 -13
  164. package/skills/gsd-path-decide/scripts/discussion_validate.py +152 -50
  165. package/skills/gsd-path-decide/scripts/integration.py +580 -25
  166. package/skills/gsd-path-decide/scripts/isolation.py +1438 -57
  167. package/skills/gsd-path-decide/scripts/loop_run.py +31 -29
  168. package/skills/gsd-path-decide/scripts/members.py +706 -0
  169. package/skills/gsd-path-decide/scripts/path_config.py +5 -5
  170. package/skills/gsd-path-decide/scripts/pipeline_git.py +124 -34
  171. package/skills/gsd-path-decide/scripts/pipeline_state.py +287 -87
  172. package/skills/gsd-path-decide/scripts/promote_lookahead.py +1 -1
  173. package/skills/gsd-path-decide/scripts/review_panel.py +1 -1
  174. package/skills/gsd-path-decide/scripts/state_checkpoint.py +135 -44
  175. package/skills/gsd-path-decide/scripts/state_promote.py +2 -0
  176. package/skills/gsd-path-decide/scripts/worktree_paths.py +72 -10
  177. package/skills/gsd-path-define/SKILL.md +36 -4
  178. package/skills/gsd-path-define/references/operating-rules.md +214 -0
  179. package/skills/gsd-path-define/references/spec-probes.md +150 -0
  180. package/skills/gsd-path-define/scripts/_common.py +619 -18
  181. package/skills/gsd-path-define/scripts/archive_milestone.py +160 -86
  182. package/skills/gsd-path-define/scripts/build_recovery.py +100 -32
  183. package/skills/gsd-path-define/scripts/check_handoffs.py +662 -50
  184. package/skills/gsd-path-define/scripts/check_task_briefs.py +338 -30
  185. package/skills/gsd-path-define/scripts/detect_project.py +587 -32
  186. package/skills/gsd-path-define/scripts/discussion_records.py +19 -13
  187. package/skills/gsd-path-define/scripts/discussion_validate.py +152 -50
  188. package/skills/gsd-path-define/scripts/integration.py +580 -25
  189. package/skills/gsd-path-define/scripts/isolation.py +1438 -57
  190. package/skills/gsd-path-define/scripts/loop_run.py +31 -29
  191. package/skills/gsd-path-define/scripts/members.py +706 -0
  192. package/skills/gsd-path-define/scripts/path_config.py +5 -5
  193. package/skills/gsd-path-define/scripts/pipeline_git.py +124 -34
  194. package/skills/gsd-path-define/scripts/pipeline_state.py +287 -87
  195. package/skills/gsd-path-define/scripts/promote_lookahead.py +1 -1
  196. package/skills/gsd-path-define/scripts/review_panel.py +1 -1
  197. package/skills/gsd-path-define/scripts/state_checkpoint.py +135 -44
  198. package/skills/gsd-path-define/scripts/state_promote.py +2 -0
  199. package/skills/gsd-path-define/scripts/worktree_paths.py +72 -10
  200. package/skills/gsd-path-define/templates/intent.md +30 -0
  201. package/skills/gsd-path-define/templates/state.md +1 -1
  202. package/skills/gsd-path-discuss/SKILL.md +2 -1
  203. package/skills/gsd-path-discuss/references/operating-rules.md +214 -0
  204. package/skills/gsd-path-discuss/scripts/_common.py +619 -18
  205. package/skills/gsd-path-discuss/scripts/discussion_records.py +19 -13
  206. package/skills/gsd-path-discuss/scripts/loop_run.py +31 -29
  207. package/skills/gsd-path-discuss/scripts/path_config.py +5 -5
  208. package/skills/gsd-path-discuss/scripts/review_panel.py +1 -1
  209. package/skills/gsd-path-discuss/scripts/worktree_paths.py +72 -10
  210. package/skills/gsd-path-docs-audit/SKILL.md +6 -4
  211. package/skills/gsd-path-docs-audit/references/dispatch.md +13 -0
  212. package/skills/gsd-path-docs-audit/references/docs-auditor.md +7 -1
  213. package/skills/gsd-path-docs-audit/references/operating-rules.md +214 -0
  214. package/skills/gsd-path-docs-audit/scripts/_common.py +619 -18
  215. package/skills/gsd-path-docs-audit/scripts/archive_milestone.py +160 -86
  216. package/skills/gsd-path-docs-audit/scripts/build_recovery.py +100 -32
  217. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +91 -7
  218. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +662 -50
  219. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +338 -30
  220. package/skills/gsd-path-docs-audit/scripts/detect_project.py +587 -32
  221. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +19 -13
  222. package/skills/gsd-path-docs-audit/scripts/discussion_validate.py +152 -50
  223. package/skills/gsd-path-docs-audit/scripts/integration.py +580 -25
  224. package/skills/gsd-path-docs-audit/scripts/isolation.py +1438 -57
  225. package/skills/gsd-path-docs-audit/scripts/loop_run.py +31 -29
  226. package/skills/gsd-path-docs-audit/scripts/members.py +706 -0
  227. package/skills/gsd-path-docs-audit/scripts/path_config.py +5 -5
  228. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +124 -34
  229. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +287 -87
  230. package/skills/gsd-path-docs-audit/scripts/promote_lookahead.py +1 -1
  231. package/skills/gsd-path-docs-audit/scripts/review_panel.py +1 -1
  232. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +135 -44
  233. package/skills/gsd-path-docs-audit/scripts/state_promote.py +2 -0
  234. package/skills/gsd-path-docs-audit/scripts/worktree_paths.py +72 -10
  235. package/skills/gsd-path-docs-audit/templates/docs-audit.md +6 -2
  236. package/skills/gsd-path-forensics/SKILL.md +2 -1
  237. package/skills/gsd-path-forensics/references/operating-rules.md +214 -0
  238. package/skills/gsd-path-forensics/scripts/_common.py +619 -18
  239. package/skills/gsd-path-forensics/scripts/discussion_records.py +19 -13
  240. package/skills/gsd-path-forensics/scripts/loop_run.py +31 -29
  241. package/skills/gsd-path-forensics/scripts/path_config.py +5 -5
  242. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +82 -31
  243. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +218 -30
  244. package/skills/gsd-path-forensics/scripts/review_panel.py +1 -1
  245. package/skills/gsd-path-forensics/scripts/worktree_paths.py +72 -10
  246. package/skills/gsd-path-inspect/SKILL.md +5 -4
  247. package/skills/gsd-path-inspect/references/codebase-mapper.md +2 -1
  248. package/skills/gsd-path-inspect/references/dispatch.md +13 -0
  249. package/skills/gsd-path-inspect/references/docs-auditor.md +7 -1
  250. package/skills/gsd-path-inspect/references/operating-rules.md +214 -0
  251. package/skills/gsd-path-inspect/scripts/_common.py +619 -18
  252. package/skills/gsd-path-inspect/scripts/archive_milestone.py +160 -86
  253. package/skills/gsd-path-inspect/scripts/build_recovery.py +100 -32
  254. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +91 -7
  255. package/skills/gsd-path-inspect/scripts/check_handoffs.py +662 -50
  256. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +338 -30
  257. package/skills/gsd-path-inspect/scripts/detect_project.py +587 -32
  258. package/skills/gsd-path-inspect/scripts/discussion_records.py +19 -13
  259. package/skills/gsd-path-inspect/scripts/discussion_validate.py +152 -50
  260. package/skills/gsd-path-inspect/scripts/integration.py +580 -25
  261. package/skills/gsd-path-inspect/scripts/isolation.py +1438 -57
  262. package/skills/gsd-path-inspect/scripts/loop_run.py +31 -29
  263. package/skills/gsd-path-inspect/scripts/members.py +706 -0
  264. package/skills/gsd-path-inspect/scripts/path_config.py +5 -5
  265. package/skills/gsd-path-inspect/scripts/pipeline_git.py +124 -34
  266. package/skills/gsd-path-inspect/scripts/pipeline_state.py +287 -87
  267. package/skills/gsd-path-inspect/scripts/promote_lookahead.py +1 -1
  268. package/skills/gsd-path-inspect/scripts/review_panel.py +1 -1
  269. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +135 -44
  270. package/skills/gsd-path-inspect/scripts/state_promote.py +2 -0
  271. package/skills/gsd-path-inspect/scripts/workflow_run.py +107 -30
  272. package/skills/gsd-path-inspect/scripts/worktree_paths.py +72 -10
  273. package/skills/gsd-path-inspect/templates/codebase.md +1 -1
  274. package/skills/gsd-path-inspect/templates/docs-audit.md +6 -2
  275. package/skills/gsd-path-inspect/templates/state.md +1 -1
  276. package/skills/gsd-path-loop/SKILL.md +2 -1
  277. package/skills/gsd-path-loop/references/operating-rules.md +214 -0
  278. package/skills/gsd-path-loop/scripts/_common.py +619 -18
  279. package/skills/gsd-path-loop/scripts/discussion_records.py +19 -13
  280. package/skills/gsd-path-loop/scripts/loop_run.py +31 -29
  281. package/skills/gsd-path-loop/scripts/path_config.py +5 -5
  282. package/skills/gsd-path-loop/scripts/review_panel.py +1 -1
  283. package/skills/gsd-path-loop/scripts/worktree_paths.py +72 -10
  284. package/skills/gsd-path-migrate/SKILL.md +2 -1
  285. package/skills/gsd-path-migrate/references/operating-rules.md +214 -0
  286. package/skills/gsd-path-migrate/scripts/_common.py +619 -18
  287. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +10 -1
  288. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +11 -4
  289. package/skills/gsd-path-migrate/scripts/discussion_records.py +19 -13
  290. package/skills/gsd-path-migrate/scripts/loop_run.py +31 -29
  291. package/skills/gsd-path-migrate/scripts/migrate_core.py +2 -2
  292. package/skills/gsd-path-migrate/scripts/path_config.py +5 -5
  293. package/skills/gsd-path-migrate/scripts/review_panel.py +1 -1
  294. package/skills/gsd-path-plan/SKILL.md +18 -4
  295. package/skills/gsd-path-plan/references/dispatch.md +13 -0
  296. package/skills/gsd-path-plan/references/operating-rules.md +214 -0
  297. package/skills/gsd-path-plan/references/planner.md +22 -4
  298. package/skills/gsd-path-plan/references/spec-probes.md +150 -0
  299. package/skills/gsd-path-plan/scripts/_common.py +619 -18
  300. package/skills/gsd-path-plan/scripts/archive_milestone.py +160 -86
  301. package/skills/gsd-path-plan/scripts/build_recovery.py +100 -32
  302. package/skills/gsd-path-plan/scripts/check_handoffs.py +662 -50
  303. package/skills/gsd-path-plan/scripts/check_task_briefs.py +338 -30
  304. package/skills/gsd-path-plan/scripts/detect_project.py +587 -32
  305. package/skills/gsd-path-plan/scripts/discussion_records.py +19 -13
  306. package/skills/gsd-path-plan/scripts/discussion_validate.py +152 -50
  307. package/skills/gsd-path-plan/scripts/integration.py +580 -25
  308. package/skills/gsd-path-plan/scripts/isolation.py +1438 -57
  309. package/skills/gsd-path-plan/scripts/loop_run.py +31 -29
  310. package/skills/gsd-path-plan/scripts/members.py +706 -0
  311. package/skills/gsd-path-plan/scripts/path_config.py +5 -5
  312. package/skills/gsd-path-plan/scripts/pipeline_git.py +124 -34
  313. package/skills/gsd-path-plan/scripts/pipeline_state.py +287 -87
  314. package/skills/gsd-path-plan/scripts/promote_lookahead.py +1 -1
  315. package/skills/gsd-path-plan/scripts/review_panel.py +1 -1
  316. package/skills/gsd-path-plan/scripts/state_checkpoint.py +135 -44
  317. package/skills/gsd-path-plan/scripts/state_promote.py +2 -0
  318. package/skills/gsd-path-plan/scripts/workflow_run.py +107 -30
  319. package/skills/gsd-path-plan/scripts/worktree_paths.py +72 -10
  320. package/skills/gsd-path-plan/templates/patch-findings.md +3 -3
  321. package/skills/gsd-path-plan/templates/plan.md +17 -2
  322. package/skills/gsd-path-plan/templates/task.md +1 -0
  323. package/skills/gsd-path-research/SKILL.md +13 -1
  324. package/skills/gsd-path-research/references/dispatch.md +13 -0
  325. package/skills/gsd-path-research/references/operating-rules.md +214 -0
  326. package/skills/gsd-path-research/scripts/_common.py +619 -18
  327. package/skills/gsd-path-research/scripts/archive_milestone.py +160 -86
  328. package/skills/gsd-path-research/scripts/build_recovery.py +100 -32
  329. package/skills/gsd-path-research/scripts/check_handoffs.py +662 -50
  330. package/skills/gsd-path-research/scripts/check_task_briefs.py +338 -30
  331. package/skills/gsd-path-research/scripts/detect_project.py +587 -32
  332. package/skills/gsd-path-research/scripts/discussion_records.py +19 -13
  333. package/skills/gsd-path-research/scripts/discussion_validate.py +152 -50
  334. package/skills/gsd-path-research/scripts/integration.py +580 -25
  335. package/skills/gsd-path-research/scripts/isolation.py +1438 -57
  336. package/skills/gsd-path-research/scripts/loop_run.py +31 -29
  337. package/skills/gsd-path-research/scripts/members.py +706 -0
  338. package/skills/gsd-path-research/scripts/path_config.py +5 -5
  339. package/skills/gsd-path-research/scripts/pipeline_git.py +124 -34
  340. package/skills/gsd-path-research/scripts/pipeline_state.py +287 -87
  341. package/skills/gsd-path-research/scripts/promote_lookahead.py +1 -1
  342. package/skills/gsd-path-research/scripts/review_panel.py +1 -1
  343. package/skills/gsd-path-research/scripts/state_checkpoint.py +135 -44
  344. package/skills/gsd-path-research/scripts/state_promote.py +2 -0
  345. package/skills/gsd-path-research/scripts/worktree_paths.py +72 -10
  346. package/skills/gsd-path-research/templates/research-handoff.md +14 -3
  347. package/skills/gsd-path-roadmap/SKILL.md +2 -1
  348. package/skills/gsd-path-roadmap/references/dispatch.md +13 -0
  349. package/skills/gsd-path-roadmap/references/operating-rules.md +214 -0
  350. package/skills/gsd-path-roadmap/scripts/_common.py +619 -18
  351. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +160 -86
  352. package/skills/gsd-path-roadmap/scripts/build_recovery.py +100 -32
  353. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +662 -50
  354. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +338 -30
  355. package/skills/gsd-path-roadmap/scripts/detect_project.py +587 -32
  356. package/skills/gsd-path-roadmap/scripts/discussion_records.py +19 -13
  357. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +152 -50
  358. package/skills/gsd-path-roadmap/scripts/integration.py +580 -25
  359. package/skills/gsd-path-roadmap/scripts/isolation.py +1438 -57
  360. package/skills/gsd-path-roadmap/scripts/loop_run.py +31 -29
  361. package/skills/gsd-path-roadmap/scripts/members.py +706 -0
  362. package/skills/gsd-path-roadmap/scripts/path_config.py +5 -5
  363. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +124 -34
  364. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +287 -87
  365. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +1 -1
  366. package/skills/gsd-path-roadmap/scripts/review_panel.py +1 -1
  367. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +135 -44
  368. package/skills/gsd-path-roadmap/scripts/state_promote.py +2 -0
  369. package/skills/gsd-path-roadmap/scripts/worktree_paths.py +72 -10
  370. package/skills/gsd-path-ship/SKILL.md +33 -11
  371. package/skills/gsd-path-ship/references/dispatch.md +13 -0
  372. package/skills/gsd-path-ship/references/operating-rules.md +214 -0
  373. package/skills/gsd-path-ship/references/reviewer-final.md +20 -2
  374. package/skills/gsd-path-ship/references/reviewer-wave.md +18 -5
  375. package/skills/gsd-path-ship/references/reviewer.md +3 -1
  376. package/skills/gsd-path-ship/references/spec-probes.md +150 -0
  377. package/skills/gsd-path-ship/scripts/_common.py +619 -18
  378. package/skills/gsd-path-ship/scripts/archive_milestone.py +160 -86
  379. package/skills/gsd-path-ship/scripts/build_recovery.py +100 -32
  380. package/skills/gsd-path-ship/scripts/build_state.py +143 -24
  381. package/skills/gsd-path-ship/scripts/check_handoffs.py +662 -50
  382. package/skills/gsd-path-ship/scripts/check_task_briefs.py +338 -30
  383. package/skills/gsd-path-ship/scripts/detect_project.py +587 -32
  384. package/skills/gsd-path-ship/scripts/discussion_records.py +19 -13
  385. package/skills/gsd-path-ship/scripts/discussion_validate.py +152 -50
  386. package/skills/gsd-path-ship/scripts/integration.py +580 -25
  387. package/skills/gsd-path-ship/scripts/isolation.py +1438 -57
  388. package/skills/gsd-path-ship/scripts/lean_verification.py +99 -19
  389. package/skills/gsd-path-ship/scripts/loop_run.py +31 -29
  390. package/skills/gsd-path-ship/scripts/members.py +706 -0
  391. package/skills/gsd-path-ship/scripts/path_config.py +5 -5
  392. package/skills/gsd-path-ship/scripts/pipeline_git.py +124 -34
  393. package/skills/gsd-path-ship/scripts/pipeline_state.py +287 -87
  394. package/skills/gsd-path-ship/scripts/promote_lookahead.py +1 -1
  395. package/skills/gsd-path-ship/scripts/review_panel.py +1 -1
  396. package/skills/gsd-path-ship/scripts/state_checkpoint.py +135 -44
  397. package/skills/gsd-path-ship/scripts/state_promote.py +2 -0
  398. package/skills/gsd-path-ship/scripts/workflow_run.py +107 -30
  399. package/skills/gsd-path-ship/scripts/worktree_paths.py +72 -10
  400. package/skills/gsd-path-ship/templates/final-review.md +16 -4
  401. package/skills/gsd-path-ship/templates/gap-review.md +2 -1
  402. package/skills/gsd-path-ship/templates/patch-findings.md +3 -3
  403. package/skills/gsd-path-ship/templates/wave-review.md +16 -4
  404. package/skills/gsd-path-undo/SKILL.md +2 -1
  405. package/skills/gsd-path-undo/references/operating-rules.md +214 -0
  406. package/skills/gsd-path-undo/scripts/_common.py +619 -18
  407. package/skills/gsd-path-undo/scripts/discussion_records.py +19 -13
  408. package/skills/gsd-path-undo/scripts/loop_run.py +31 -29
  409. package/skills/gsd-path-undo/scripts/path_config.py +5 -5
  410. package/skills/gsd-path-undo/scripts/pipeline_undo.py +218 -30
  411. package/skills/gsd-path-undo/scripts/review_panel.py +1 -1
  412. package/skills/gsd-path-undo/scripts/worktree_paths.py +72 -10
  413. package/skills/path/BUILD.md +7 -1
  414. package/skills/path/DECIDE.md +11 -4
  415. package/skills/path/DEFINE.md +36 -4
  416. package/skills/path/DOCS-AUDIT.md +6 -4
  417. package/skills/path/INSPECT.md +5 -4
  418. package/skills/path/PLAN.md +18 -4
  419. package/skills/path/RESEARCH.md +13 -1
  420. package/skills/path/ROADMAP.md +2 -1
  421. package/skills/path/SHIP.md +33 -11
  422. package/skills/path/SKILL.md +45 -1
  423. package/skills/path/references/build-native.md +63 -15
  424. package/skills/path/references/codebase-mapper.md +2 -1
  425. package/skills/path/references/dispatch.md +13 -0
  426. package/skills/path/references/docs-auditor.md +7 -1
  427. package/skills/path/references/operating-rules.md +214 -0
  428. package/skills/path/references/planner.md +22 -4
  429. package/skills/path/references/reviewer-final.md +20 -2
  430. package/skills/path/references/reviewer-wave.md +18 -5
  431. package/skills/path/references/reviewer.md +3 -1
  432. package/skills/path/references/spec-probes.md +150 -0
  433. package/skills/path/scripts/_common.py +619 -18
  434. package/skills/path/scripts/archive_milestone.py +160 -86
  435. package/skills/path/scripts/bootstrap_repository.py +8 -5
  436. package/skills/path/scripts/build_recovery.py +100 -32
  437. package/skills/path/scripts/build_state.py +143 -24
  438. package/skills/path/scripts/check_docs_audit.py +91 -7
  439. package/skills/path/scripts/check_handoffs.py +662 -50
  440. package/skills/path/scripts/check_task_briefs.py +338 -30
  441. package/skills/path/scripts/check_update.py +2 -2
  442. package/skills/path/scripts/detect_project.py +587 -32
  443. package/skills/path/scripts/discussion_records.py +19 -13
  444. package/skills/path/scripts/discussion_validate.py +152 -50
  445. package/skills/path/scripts/dispatch_driver.py +501 -46
  446. package/skills/path/scripts/git_guard.py +118 -31
  447. package/skills/path/scripts/integration.py +580 -25
  448. package/skills/path/scripts/isolation.py +1438 -57
  449. package/skills/path/scripts/lean_verification.py +99 -19
  450. package/skills/path/scripts/loop_run.py +31 -29
  451. package/skills/path/scripts/members.py +706 -0
  452. package/skills/path/scripts/path_config.py +5 -5
  453. package/skills/path/scripts/pipeline_git.py +124 -34
  454. package/skills/path/scripts/pipeline_state.py +287 -87
  455. package/skills/path/scripts/promote_lookahead.py +1 -1
  456. package/skills/path/scripts/review_panel.py +1 -1
  457. package/skills/path/scripts/state_checkpoint.py +135 -44
  458. package/skills/path/scripts/state_promote.py +2 -0
  459. package/skills/path/scripts/status_runtime.py +58 -10
  460. package/skills/path/scripts/task_context.py +12 -3
  461. package/skills/path/scripts/token_budget.py +12 -8
  462. package/skills/path/scripts/workflow_run.py +107 -30
  463. package/skills/path/scripts/worktree_paths.py +72 -10
  464. package/skills/path/templates/codebase.md +1 -1
  465. package/skills/path/templates/docs-audit.md +6 -2
  466. package/skills/path/templates/final-review.md +16 -4
  467. package/skills/path/templates/gap-review.md +2 -1
  468. package/skills/path/templates/intent.md +30 -0
  469. package/skills/path/templates/patch-findings.md +3 -3
  470. package/skills/path/templates/plan.md +17 -2
  471. package/skills/path/templates/research-handoff.md +14 -3
  472. package/skills/path/templates/state.md +1 -1
  473. package/skills/path/templates/task.md +1 -0
  474. package/skills/path/templates/wave-review.md +16 -4
  475. package/skills/gsd-path/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  476. package/skills/gsd-path/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  477. package/skills/gsd-path-build/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  478. package/skills/gsd-path-build/scripts/__pycache__/check_handoffs.cpython-312.pyc +0 -0
  479. package/skills/gsd-path-inspect/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  480. package/skills/gsd-path-plan/scripts/__pycache__/_common.cpython-312.pyc +0 -0
  481. package/skills/gsd-path-ship/scripts/__pycache__/_common.cpython-312.pyc +0 -0
@@ -16,11 +16,11 @@ 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
22
23
  import shlex
23
- import signal
24
24
  import subprocess
25
25
  import sys
26
26
  import tempfile
@@ -33,6 +33,7 @@ try:
33
33
  from scripts import (_common, archive_milestone, build_state, discussion_validate, isolation,
34
34
  pipeline_state, review_findings, review_panel, workflow_run, task_context, model_policy)
35
35
  from scripts import check_handoffs as contracts
36
+ from scripts.pipeline_git import is_bound_branch
36
37
  except ImportError: # bundled copy inside a skill's scripts directory
37
38
  import _common
38
39
  import archive_milestone
@@ -46,6 +47,7 @@ except ImportError: # bundled copy inside a skill's scripts directory
46
47
  import review_panel
47
48
  import workflow_run
48
49
  import model_policy
50
+ from pipeline_git import is_bound_branch
49
51
 
50
52
  if os.name == "nt":
51
53
  import msvcrt
@@ -73,12 +75,91 @@ class DriverStop(RuntimeError):
73
75
  STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
74
76
  contracts.HandoffError, review_findings.ReviewFindingsError,
75
77
  pipeline_state.PipelineStateError, archive_milestone.ArchiveError, model_policy.PolicyError)
78
+ _RUNTIME_ACTIVATION_ORDER = (
79
+ "_common",
80
+ "pipeline_git",
81
+ "worktree_paths",
82
+ "build_recovery",
83
+ "isolation",
84
+ "pipeline_state",
85
+ "state_checkpoint",
86
+ "check_task_briefs",
87
+ "build_state",
88
+ "check_handoffs",
89
+ "task_context",
90
+ "archive_milestone",
91
+ "review_findings",
92
+ "model_policy",
93
+ )
76
94
  RECOVERY_BLOCKED = "recovery blocked"
77
95
  # The stops the build contract answers with build/blocked when the round raises them (steps 1 and 2);
78
96
  # the same stops from review, panel, or skeptics stay with the parent.
79
97
  BLOCKING_STOPS = {"dependency-deadlock": "dependency deadlock", RECOVERY_BLOCKED: "blocked recovery"}
80
98
 
81
99
 
100
+ def _bundled_helpers() -> bool:
101
+ return "scripts.isolation" not in sys.modules
102
+
103
+
104
+ def _refresh_stop_errors() -> None:
105
+ global STOP_ERRORS
106
+ STOP_ERRORS = (DriverStop, isolation.IsolationError, build_state.BuildStateError,
107
+ contracts.HandoffError, review_findings.ReviewFindingsError,
108
+ pipeline_state.PipelineStateError, archive_milestone.ArchiveError,
109
+ model_policy.PolicyError)
110
+
111
+
112
+ def _reload_pinned_modules(runtime_root: Path) -> None:
113
+ global isolation, build_state, task_context, contracts, pipeline_state
114
+ global archive_milestone, review_findings, model_policy
115
+ if _bundled_helpers():
116
+ path = str(runtime_root)
117
+ if path not in sys.path:
118
+ sys.path.insert(0, path)
119
+ for name in _RUNTIME_ACTIVATION_ORDER:
120
+ module = sys.modules.get(name)
121
+ if module is not None:
122
+ importlib.reload(module)
123
+ isolation = sys.modules["isolation"]
124
+ build_state = sys.modules["build_state"]
125
+ task_context = sys.modules["task_context"]
126
+ contracts = sys.modules["check_handoffs"]
127
+ pipeline_state = sys.modules["pipeline_state"]
128
+ archive_milestone = sys.modules["archive_milestone"]
129
+ review_findings = sys.modules["review_findings"]
130
+ model_policy = sys.modules["model_policy"]
131
+ else:
132
+ import scripts
133
+ path = str(runtime_root)
134
+ if path not in scripts.__path__:
135
+ scripts.__path__ = [path, *[entry for entry in scripts.__path__ if entry != path]]
136
+ for name in _RUNTIME_ACTIVATION_ORDER:
137
+ module = sys.modules.get(f"scripts.{name}")
138
+ if module is not None:
139
+ importlib.reload(module)
140
+ isolation = scripts.isolation
141
+ build_state = scripts.build_state
142
+ task_context = scripts.task_context
143
+ contracts = scripts.check_handoffs
144
+ pipeline_state = scripts.pipeline_state
145
+ archive_milestone = scripts.archive_milestone
146
+ review_findings = scripts.review_findings
147
+ model_policy = scripts.model_policy
148
+ _refresh_stop_errors()
149
+
150
+
151
+ def _activate_pinned_runtime(repo: Path) -> None:
152
+ if not os.path.lexists(repo / ".gsd-path/runtime.json"):
153
+ return
154
+ resolved = subprocess.run(
155
+ [sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
156
+ "--repo", str(repo), "--runtime-path"],
157
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
158
+ if resolved.returncode:
159
+ raise DriverStop(resolved.stderr.strip() or "pinned runtime resolution failed")
160
+ _reload_pinned_modules(Path(resolved.stdout.strip()))
161
+
162
+
82
163
  def now() -> str:
83
164
  return datetime.now(timezone.utc).isoformat(timespec="seconds")
84
165
 
@@ -109,6 +190,169 @@ def attempts_used(root: Path, task_id: str) -> int:
109
190
  if load_state(path).get("origin") == "dispatch")
110
191
 
111
192
 
193
+ def dispatch_primary(worktree: Path) -> Path:
194
+ """The bound-branch primary that owns dispatch records for a task checkout."""
195
+ worktree = worktree.resolve()
196
+ if is_bound_branch(isolation.require_attached(worktree)):
197
+ return worktree
198
+ matches = [
199
+ path.resolve()
200
+ for path, branch in isolation._registered_worktrees(worktree).items()
201
+ if branch and is_bound_branch(branch.removeprefix("refs/heads/"))
202
+ ]
203
+ if len(matches) != 1:
204
+ raise isolation.IsolationError(
205
+ "dispatch records require exactly one primary worktree for the bound branch")
206
+ return matches[0]
207
+
208
+
209
+ def should_open_native_shell(root: Path, task_id: str, base: str) -> bool:
210
+ """Whether activate-task should append a native dispatch attempt record."""
211
+ task_dir = root / task_id
212
+ if not task_dir.is_dir():
213
+ return True
214
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
215
+ key=lambda path: attempt_number(path.parent))
216
+ if not attempts:
217
+ return True
218
+ latest = read_attempt(attempts[-1])
219
+ if latest.get("outcome") is None and not latest.get("command"):
220
+ return False
221
+ if latest.get("outcome") == "blocked" and str(latest.get("base")) == base:
222
+ return False
223
+ return True
224
+
225
+
226
+ def open_native_shell(primary: Path, *, task_id: str, base: str, worktree: str, task_file: str,
227
+ task_branch: Optional[str], mode: str, wave: Optional[int] = None,
228
+ title: Optional[str] = None, files: Optional[List[str]] = None,
229
+ sidecar: Optional[object] = None, member: Optional[str] = None,
230
+ member_base: Optional[str] = None, contract_file: Optional[str] = None) -> None:
231
+ """Open a dispatch attempt record for a host-spawned coder before it runs."""
232
+ root = records_root(primary)
233
+ root.mkdir(parents=True, exist_ok=True)
234
+ record: Dict[str, object] = {
235
+ "task_id": task_id,
236
+ "base": base,
237
+ "worktree": worktree,
238
+ "task_file": task_file,
239
+ "task_branch": task_branch,
240
+ "mode": mode,
241
+ "origin": "dispatch",
242
+ "outcome": None,
243
+ "dispatched_at": now(),
244
+ "native": True,
245
+ }
246
+ if wave is not None:
247
+ record["wave"] = wave
248
+ if title is not None:
249
+ record["title"] = title
250
+ if files is not None:
251
+ record["files"] = files
252
+ if sidecar is not None:
253
+ record["sidecar"] = sidecar
254
+ if member is not None:
255
+ record["member"] = member
256
+ if member_base is not None:
257
+ record["member_base"] = member_base
258
+ if contract_file is not None:
259
+ record["contract_file"] = contract_file
260
+ task_dir = root / task_id
261
+ if not should_open_native_shell(root, task_id, base):
262
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
263
+ key=lambda path: attempt_number(path.parent))
264
+ if not attempts:
265
+ return
266
+ state_path = attempts[-1]
267
+ state = load_state(state_path)
268
+ if state.get("outcome") is not None or state.get("command"):
269
+ return
270
+ attempt = state.get("attempt")
271
+ state.update(record)
272
+ if attempt is not None:
273
+ state["attempt"] = attempt
274
+ state["outcome"] = None
275
+ for key in ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit",
276
+ "answer", "command", "usage_recorded"):
277
+ state.pop(key, None)
278
+ save_state(state_path, state)
279
+ exit_path = state_path.with_name("exit.json")
280
+ if exit_path.is_file():
281
+ exit_path.unlink()
282
+ return
283
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
284
+ attempt_dir = task_dir / f"attempt-{attempt}"
285
+ attempt_dir.mkdir(parents=True)
286
+ record["attempt"] = attempt
287
+ save_state(attempt_dir / "state.json", record)
288
+
289
+
290
+ def record_native_activation(worktree: Path, activation: Dict[str, object]) -> None:
291
+ """Record a native task activation in the milestone dispatch ledger."""
292
+ primary = dispatch_primary(worktree)
293
+ task_file = str(activation["task_file"])
294
+ task_path = worktree / task_file
295
+ fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
296
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
297
+ wave = None
298
+ if fields and fields.get("wave") is not None:
299
+ wave = int(fields["wave"])
300
+ open_native_shell(
301
+ primary,
302
+ task_id=task_id,
303
+ base=str(activation["base"]),
304
+ worktree=str(activation["worktree"]),
305
+ task_file=task_file,
306
+ task_branch=activation.get("task_branch"),
307
+ mode="parallel" if activation.get("task_branch") else "serial",
308
+ wave=wave,
309
+ title=str(fields.get("title")) if fields else None,
310
+ files=list(fields.get("files") or []) if fields else None,
311
+ )
312
+
313
+
314
+ def record_native_member_activation(coordinator: Path, activation: Dict[str, object]) -> None:
315
+ """Record a native member-task activation in the coordinator dispatch ledger."""
316
+ task_file = str(activation["task_file"])
317
+ sidecar = Path(str(activation["worktree"]))
318
+ copy = Path(str(activation["copy"]))
319
+ fields, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8"))
320
+ task_id = str(activation.get("task_id") or (fields or {}).get("id"))
321
+ wave = None
322
+ if fields and fields.get("wave") is not None:
323
+ wave = int(fields["wave"])
324
+ open_native_shell(
325
+ coordinator,
326
+ task_id=task_id,
327
+ base=str(activation["base"]),
328
+ worktree=str(sidecar),
329
+ task_file=copy.relative_to(sidecar).as_posix(),
330
+ task_branch=str(activation["task_branch"]),
331
+ mode="member",
332
+ wave=wave,
333
+ title=str(fields.get("title")) if fields else None,
334
+ files=list(fields.get("files") or []) if fields else None,
335
+ member=str(activation["member"]),
336
+ member_base=str(activation["member_base"]),
337
+ contract_file=task_file,
338
+ )
339
+
340
+
341
+ def reusable_shell_attempt(root: Path, state: Dict[str, object]) -> Optional[Path]:
342
+ """A native activation shell the driver can attach its child wrapper to."""
343
+ task_dir = root / str(state["task_id"])
344
+ attempts = sorted(task_dir.glob("attempt-*/state.json"),
345
+ key=lambda path: attempt_number(path.parent))
346
+ if not attempts:
347
+ return None
348
+ prior = read_attempt(attempts[-1])
349
+ if prior.get("outcome") is not None or prior.get("command") or prior.get("pid"):
350
+ return None
351
+ if str(prior.get("base")) != str(state.get("base")):
352
+ return None
353
+ return attempts[-1].parent
354
+
355
+
112
356
  def acquire_lock(primary: Path) -> BinaryIO:
113
357
  root = records_root(primary)
114
358
  root.mkdir(parents=True, exist_ok=True)
@@ -170,12 +414,7 @@ def child_running(state: Dict[str, object]) -> bool:
170
414
  return state.get("finished_at") is None and bool(state.get("pid")) and process_alive(int(state["pid"]))
171
415
 
172
416
 
173
- def process_alive(pid: int) -> bool:
174
- try:
175
- os.kill(pid, 0)
176
- except ProcessLookupError:
177
- return False
178
- return True
417
+ process_alive = _common.process_alive
179
418
 
180
419
 
181
420
  # --- child process ---------------------------------------------------------
@@ -208,13 +447,13 @@ def child_main(state_path: Path) -> int:
208
447
  attempt_dir = state_path.parent
209
448
  with open(attempt_dir / "brief.md", "rb") as brief, \
210
449
  open(attempt_dir / "stdout", "wb") as out, open(attempt_dir / "stderr", "wb") as err:
211
- process = subprocess.Popen(state["command"], cwd=state["worktree"], stdin=brief,
212
- stdout=out, stderr=err, start_new_session=True)
450
+ process = _common.popen_group(_common.resolve_argv(state["command"]), cwd=state["worktree"],
451
+ stdin=brief, stdout=out, stderr=err)
213
452
  try:
214
453
  exit_code: Optional[int] = process.wait(timeout=state.get("child_timeout"))
215
454
  timed_out = False
216
455
  except subprocess.TimeoutExpired:
217
- os.killpg(process.pid, signal.SIGKILL)
456
+ _common.kill_tree(process)
218
457
  process.wait()
219
458
  exit_code, timed_out = None, True
220
459
  save_state(attempt_dir / "exit.json",
@@ -224,6 +463,7 @@ def child_main(state_path: Path) -> int:
224
463
 
225
464
  def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path) -> str:
226
465
  worktree = Path(str(state["worktree"]))
466
+ project_root = Path(str(state.get("project_root") or worktree))
227
467
  task_id = state["task_id"]
228
468
  agents = worktree / "AGENTS.md"
229
469
  workflow = worktree / "WORKFLOW.md"
@@ -234,12 +474,14 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
234
474
  f"Work only in this worktree root: {worktree}",
235
475
  f"Task file: {worktree / str(state['task_file'])}",
236
476
  f"Task template: {task_template}",
237
- f"INTENT.md: {worktree / '.project/intent/INTENT.md'}",
477
+ f"INTENT.md: {project_root / '.project/intent/INTENT.md'}",
238
478
  f"AGENTS.md: {agents if agents.exists() else 'absent'}",
239
479
  f"WORKFLOW.md: {workflow if workflow.exists() else 'absent'}",
240
480
  f"Recorded base: {state['base']}",
241
481
  f"Dispatch mode: {state['mode']} ("
242
482
  + ("the primary worktree on the bound branch" if state["mode"] == "serial"
483
+ else f"an isolated worktree of member {state['member']}; never touch the member team's "
484
+ "checkout or the coordinator" if state["mode"] == "member"
243
485
  else "an isolated task worktree; never touch the primary") + ").",
244
486
  "Next consumer: the build orchestrator reruns Verify in an isolated checkout and lands "
245
487
  "the task. Gate: Verify passes and only the task's declared files plus its Log changed.",
@@ -248,7 +490,7 @@ def brief_text(state: Dict[str, object], role_brief: Path, task_template: Path)
248
490
  if state.get("answered"):
249
491
  lines.append(f"The task Log now records an `{ANSWER_MARK}` to your earlier question; "
250
492
  "continue from it.")
251
- lines.append(task_context.render(worktree, worktree / str(state['task_file'])))
493
+ lines.append(task_context.render(project_root, worktree / str(state['task_file'])))
252
494
  return "\n".join(lines) + "\n"
253
495
 
254
496
 
@@ -282,7 +524,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
282
524
  if caps_path is None:
283
525
  if overrides or previous or model_policy.policy_path(primary, project_dir).exists():
284
526
  raise model_policy.PolicyError('model policy requires --model-capabilities for this dispatch')
285
- return shlex.split(command), None
527
+ return _common.split_command(command), None
286
528
  caps = model_policy.read_json(caps_path)
287
529
  action = getattr(options, 'action', 'round')
288
530
  role = {'round': 'coder', 'review': 'reviewer', 'panel': 'review_panel',
@@ -297,7 +539,7 @@ def selected_command(state: Dict[str, object], options: argparse.Namespace, comm
297
539
  dict(overrides, model=state['slug']), previous)
298
540
  model_policy.validate_panel(selection, state['family'], state.get('excluded_families', []))
299
541
  state['slug'] = selection['selected']['model']
300
- return model_policy.command_args(shlex.split(command), selection), selection
542
+ return model_policy.command_args(_common.split_command(command), selection), selection
301
543
 
302
544
 
303
545
  def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
@@ -311,7 +553,7 @@ def retain_selection(root: Path, state: Dict[str, object]) -> Dict[str, object]:
311
553
  raise DriverStop('cannot redispatch while the previous child is active')
312
554
  if prior.get('model_selection'):
313
555
  state['model_selection'] = prior['model_selection']
314
- else:
556
+ elif prior.get('command'):
315
557
  state['command'] = prior['command']
316
558
  reassignment = task_dir / 'model-reassignment.json'
317
559
  if reassignment.exists():
@@ -327,25 +569,35 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
327
569
  state = retain_selection(root, state)
328
570
  task_dir = root / str(state["task_id"])
329
571
  argv, selection = selected_command(state, options, command or options.child_command)
330
- attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
331
- attempt_dir = task_dir / f"attempt-{attempt}"
332
- attempt_dir.mkdir(parents=True)
572
+ attempt_dir = reusable_shell_attempt(root, state)
573
+ if attempt_dir is None:
574
+ attempt = max((attempt_number(path) for path in task_dir.glob("attempt-*")), default=0) + 1
575
+ attempt_dir = task_dir / f"attempt-{attempt}"
576
+ attempt_dir.mkdir(parents=True)
577
+ else:
578
+ attempt = attempt_number(attempt_dir)
333
579
  stale = ("pid", "finished_at", "exit_code", "timed_out", "reason", "question", "commit", "answer",
334
580
  "usage_recorded")
335
- state = {key: value for key, value in state.items()
336
- if not key.startswith("_") and key not in stale}
581
+ incoming = {key: value for key, value in state.items()
582
+ if not key.startswith("_") and key not in stale}
583
+ if (attempt_dir / "state.json").is_file():
584
+ preserved = {key: value for key, value in read_attempt(attempt_dir / "state.json").items()
585
+ if not key.startswith("_") and key not in stale}
586
+ state = {**preserved, **incoming}
587
+ else:
588
+ state = incoming
337
589
  if selection is not None:
338
590
  state['model_selection'] = selection
339
591
  state.update({"attempt": attempt, "command": argv,
340
592
  "child_timeout": options.child_timeout, "outcome": None,
341
593
  "dispatched_at": now(), "origin": "question" if state.get("answered") else "dispatch"})
342
594
  state_path = attempt_dir / "state.json"
343
- (attempt_dir / "brief.md").write_text(brief(state), encoding="utf-8")
595
+ (attempt_dir / "brief.md").write_bytes(brief(state).encode("utf-8"))
344
596
  save_state(state_path, state)
345
- wrapper = subprocess.Popen(
597
+ wrapper = _common.popen_detached(
346
598
  [sys.executable, "-B", str(Path(__file__).resolve()), "_child", "--state", str(state_path)],
347
599
  cwd=state["worktree"], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
348
- stderr=subprocess.DEVNULL, start_new_session=True,
600
+ stderr=subprocess.DEVNULL,
349
601
  )
350
602
  state["_path"] = str(state_path)
351
603
  update_state(state, pid=wrapper.pid)
@@ -356,8 +608,11 @@ def spawn(root: Path, state: Dict[str, object], options: argparse.Namespace,
356
608
 
357
609
 
358
610
  def log_delta(primary: Path, state: Dict[str, object]) -> str:
359
- base_text = isolation.git_text(primary, "show", f"{state['base']}:{state['task_file']}")
611
+ contract_file = state.get("contract_file") or state["task_file"]
612
+ base_text = isolation.git_text(primary, "show", f"{state['base']}:{contract_file}")
360
613
  text = (Path(str(state["worktree"])) / str(state["task_file"])).read_text(encoding="utf-8")
614
+ if state["mode"] == "member":
615
+ return isolation.member_log_delta(base_text, text)
361
616
  return isolation.task_log_delta(base_text, text)
362
617
 
363
618
 
@@ -374,14 +629,14 @@ def snapshot_tree(primary: Path) -> str:
374
629
  env = dict(os.environ, GIT_INDEX_FILE=str(Path(temporary) / "index"))
375
630
  for arguments in (("read-tree", "HEAD"), ("add", "-A"), ("write-tree",)):
376
631
  completed = subprocess.run(("git", "-C", str(primary), *arguments), env=env,
377
- text=True, capture_output=True)
632
+ encoding="utf-8", errors="replace", capture_output=True)
378
633
  if completed.returncode:
379
634
  raise DriverStop(f"git {' '.join(arguments)} failed: {completed.stderr.strip()}")
380
635
  return completed.stdout.strip()
381
636
 
382
637
 
383
638
  def run_verify(command: str, cwd: Path) -> Dict[str, object]:
384
- completed = _common.run_command("bash", "-c", command, cwd=cwd)
639
+ completed = _common.run_command(*_common.bash_argv(command), cwd=cwd)
385
640
  return {"exit_code": completed.returncode, "stdout": completed.stdout, "stderr": completed.stderr}
386
641
 
387
642
 
@@ -394,6 +649,30 @@ def state_from_task(primary: Path, task_id: str) -> Dict[str, object]:
394
649
  break
395
650
  else:
396
651
  raise DriverStop(f"task {task_id} has no task file")
652
+ member = fields.get("repo")
653
+ if member:
654
+ # A member activation lives in the live copy inside its member sidecar.
655
+ try:
656
+ checkout, project, _ = isolation._member_context(primary, str(member))
657
+ except isolation.IsolationError as error:
658
+ raise DriverStop(str(error), task=task_id) from error
659
+ task_file = task_path.relative_to(primary).as_posix()
660
+ sidecar = isolation.sidecar_root(checkout, "task", f"{project}-{task_id}").resolve()
661
+ copy = isolation.member_task_copy(sidecar, task_file)
662
+ live, _ = isolation.task_frontmatter(copy.read_text(encoding="utf-8")) if copy.is_file() else ({}, None)
663
+ if not live or live.get("status") != "in-progress" or not live.get("base") or not live.get("member_base"):
664
+ raise DriverStop(f"member task {task_id} has no activated live copy in {sidecar}", task=task_id)
665
+ return {"task_id": task_id, "title": str(live.get("title")), "task_file": copy.relative_to(sidecar).as_posix(),
666
+ "contract_file": task_file, "files": list(live.get("files") or []), "base": str(live["base"]),
667
+ "worktree": str(sidecar), "task_branch": f"{isolation.TASK_BRANCH_PREFIX}{project}-{task_id}",
668
+ "mode": "member", "member": str(member), "member_base": str(live["member_base"]),
669
+ "project_root": str(primary), "sidecar": None}
670
+ if fields.get("status") != "in-progress" or not fields.get("base"):
671
+ # A parallel activation lives only in its isolate; the primary copy stays pending.
672
+ isolate = isolation.sidecar_root(primary, "task", task_id).resolve()
673
+ if isolation.authorized_task_worktree(isolate, isolation.require_bound(primary)):
674
+ fields, _ = isolation.task_frontmatter(
675
+ (isolate / task_path.relative_to(primary)).read_text(encoding="utf-8"))
397
676
  if fields.get("status") != "in-progress" or not fields.get("base"):
398
677
  raise DriverStop(f"task {task_id} is not an in-progress task with a recorded base")
399
678
  def value(key: str) -> Optional[str]:
@@ -423,14 +702,14 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
423
702
  if not command:
424
703
  raise DriverStop("task has no Verify command", task=task_id)
425
704
  if state["mode"] == "serial":
426
- # ponytail: sidecar reproduction is git plumbing that belongs in isolation.py as the
427
- # inverse of clean_verify; lift it there when a second caller appears.
428
705
  sidecar = state["sidecar"]
429
- sidecar_path = Path(str(sidecar["worktree"]))
706
+ ensured = isolation.ensure_verify_sidecar(primary, base, str(sidecar["branch"]))
707
+ sidecar_path = Path(str(ensured["worktree"]))
430
708
  tree = snapshot_tree(primary)
431
- isolation.git_output(sidecar_path, "read-tree", "-u", "--reset", tree)
432
- if isolation.git_output(sidecar_path, "write-tree") != tree:
433
- raise DriverStop("sidecar reproduction differs from the primary task diff", task=task_id)
709
+ try:
710
+ isolation.reproduce_verify_sidecar(sidecar_path, tree)
711
+ except isolation.IsolationError as error:
712
+ raise DriverStop(str(error), task=task_id) from error
434
713
  execution = run_verify(command, sidecar_path)
435
714
  location = f"sidecar {sidecar['branch']}"
436
715
  isolation.clean_verify(primary, sidecar_path, base, str(sidecar["branch"]))
@@ -453,7 +732,21 @@ def finish_task(primary: Path, state: Dict[str, object]) -> Dict[str, object]:
453
732
  if not passed:
454
733
  if state["mode"] == "parallel":
455
734
  isolation.deactivate_task(worktree, task_id, str(state["task_branch"]))
735
+ elif state["mode"] == "member":
736
+ isolation.deactivate_member_task(primary, str(state["member"]), task_id)
456
737
  raise DriverStop("Verify failed in the isolate", task=task_id, execution=execution)
738
+ if state["mode"] == "member":
739
+ member = str(state["member"])
740
+ landed = isolation.land_member(primary, member, task_id, str(state["title"]),
741
+ str(state["contract_file"]), base, str(state["member_base"]))
742
+ landing = str(landed["landing"])
743
+ # Only a landing whose parent is the member base proves the verified member tree.
744
+ ledger = isolation.git_output(worktree, "rev-parse", f"{landing}^") == str(state["member_base"])
745
+ if ledger:
746
+ build_state.verify_record(str(primary), command, landing, "pass", execution=execution, member=member)
747
+ isolation.retire_member_task(primary, member, task_id)
748
+ return {"task": task_id, "commit": str(landed["commit"]), "mode": "member",
749
+ "landing": landing, "verify": execution, "ledger": ledger}
457
750
  landed = isolation.land(primary, worktree, base, task_id, str(state["title"]), task_file,
458
751
  list(state["files"]))
459
752
  commit = str(landed["commit"])
@@ -485,7 +778,8 @@ def recover_report(primary: Path, project_dir: str, receipt: Dict[str, object])
485
778
 
486
779
  def checkpoint_project(primary: Path, receipt: Dict[str, object], subject: str, body: str) -> Optional[str]:
487
780
  """Commit .project bookkeeping through the canonical checkpoint; None when nothing is dirty."""
488
- if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)):
781
+ if not any(path.startswith(".project/") for path in isolation.uncommitted_paths(primary)) \
782
+ and not isolation.ignored_legacy_ledger(primary):
489
783
  return None
490
784
  result = isolation.checkpoint(primary, isolation.current_sha(primary), subject, body, [".project"])
491
785
  receipt["steps"].append({"script": "isolation.py checkpoint", "result": result})
@@ -582,7 +876,7 @@ class Round:
582
876
  """Run the bundled token_budget.py against this milestone's ledger; blocked is a stop."""
583
877
  command = [sys.executable, "-B", str(Path(__file__).resolve().parent / "token_budget.py"),
584
878
  action, "--ledger", str(budget_ledger(self.primary)), *arguments]
585
- completed = subprocess.run(command, cwd=self.primary, capture_output=True, text=True)
879
+ completed = subprocess.run(command, cwd=self.primary, capture_output=True, encoding="utf-8", errors="replace")
586
880
  result = json.loads(completed.stdout) if completed.stdout.strip() else {}
587
881
  self.receipt["steps"].append({"script": f"token_budget.py {action}", "command": command,
588
882
  "exit_code": completed.returncode, "result": result,
@@ -605,12 +899,44 @@ class Round:
605
899
  # recovery -------------------------------------------------------------
606
900
 
607
901
  def recover(self) -> None:
902
+ # A journaled member landing is safe to finish; it never lands the same work twice.
903
+ journals = isolation.common_git_dir(self.primary).joinpath(*isolation.MEMBER_LANDING_DIR)
904
+ retired_members = set()
905
+ for journal in sorted(journals.glob("*.json")) if journals.is_dir() else []:
906
+ result = isolation.recover_member_landing(self.primary, journal.stem)
907
+ self.receipt["steps"].append({"script": "isolation.recover_member_landing", "task": journal.stem,
908
+ "result": result})
909
+ if result["state"] == "landed":
910
+ isolation.retire_member_task(self.primary, str(result["member"]), journal.stem)
911
+ retired_members.add(journal.stem)
608
912
  report = recover_report(self.primary, self.project_dir, self.receipt)
609
913
  for task in report["tasks"]:
610
914
  worktree = task.get("worktree")
611
915
  verdict = task["verdict"]
612
916
  if verdict in ("recovered", "attested"):
613
917
  self.proven[str(task.get("task_id"))] = str(task.get("commit"))
918
+ if (verdict == "reconcile" and task.get("status") == "in-progress"
919
+ and task.get("task_branch") is None):
920
+ task_id = str(task.get("task_id"))
921
+ verify_name = f"task-{task_id.lower()}-verify"
922
+ verify_path = isolation.sidecar_root(self.primary, "verify", verify_name)
923
+ verify_branch = isolation.verify_branch_name(verify_name)
924
+ diagnose = shlex.join([
925
+ sys.executable,
926
+ "-B",
927
+ str(Path(__file__).with_name("pipeline_diagnose.py")),
928
+ "diagnose",
929
+ "--repo",
930
+ str(self.primary),
931
+ ])
932
+ raise DriverStop(
933
+ f"task {task_id} needs recovery: {task.get('reason')}; inspect Verify "
934
+ f"sidecar {verify_path} ({verify_branch}) before resetting it to pending; "
935
+ f"inventory it with `{diagnose}`",
936
+ recover=task,
937
+ )
938
+ if verdict == "recovered" and task.get("member") and task.get("task_id") not in retired_members:
939
+ isolation.retire_member_task(self.primary, str(task["member"]), str(task["task_id"]))
614
940
  if verdict == "recovered" and worktree and worktree["clean"] and task.get("task_branch"):
615
941
  isolation.retire(self.primary, Path(str(worktree["path"])),
616
942
  str(task["task_branch"]), False)
@@ -623,12 +949,22 @@ class Round:
623
949
  # settle exited children ---------------------------------------------
624
950
 
625
951
  def isolate_live(self, state: Dict[str, object]) -> bool:
626
- """True while the isolate still holds the task as in-progress; the parent's recovery clears it."""
952
+ """Keep blocked attempts reportable until the corresponding task is reset."""
627
953
  task_path = Path(str(state["worktree"])) / str(state["task_file"])
628
954
  if not task_path.is_file():
629
955
  return False
630
956
  fields, _ = isolation.task_frontmatter(task_path.read_text(encoding="utf-8"))
631
- return fields is not None and fields.get("status") == "in-progress"
957
+ if fields is None:
958
+ return False
959
+ if fields.get("status") == "in-progress":
960
+ return True
961
+ return (
962
+ state.get("mode") == "serial"
963
+ and fields.get("status") == "blocked"
964
+ and fields.get("id") == state.get("task_id")
965
+ and fields.get("base") == state.get("base")
966
+ and fields.get("task_branch") == "null"
967
+ )
632
968
 
633
969
  def settle(self) -> bool:
634
970
  """Classify every exited child. Returns True when a landing or redispatch changed the round."""
@@ -667,8 +1003,15 @@ class Round:
667
1003
  elif state.get("finished_at") is None:
668
1004
  if child_running(state):
669
1005
  self.receipt["in_flight"].append(self.summary(state))
1006
+ elif (state.get("native") and not state.get("command") and not state.get("pid")
1007
+ and self.isolate_live(state)):
1008
+ continue
670
1009
  else:
671
- self.fail(state, ORPHANED)
1010
+ state.update(read_attempt(Path(str(state["_path"]))))
1011
+ if state.get("finished_at") is not None:
1012
+ changed = self.classify(state) or changed
1013
+ else:
1014
+ self.fail(state, ORPHANED)
672
1015
  else:
673
1016
  changed = self.classify(state) or changed
674
1017
  return changed
@@ -679,6 +1022,35 @@ class Round:
679
1022
  update_state(state, outcome="blocked", reason=reason)
680
1023
  self.receipt["blocked"].append({**self.summary(state), "reason": reason, **details})
681
1024
 
1025
+ def blocked_result(self, state: Dict[str, object], **diagnostics: object) -> bool:
1026
+ """Record an explicit serial block and retire only its untouched Verify sidecar."""
1027
+ reason = "child returned blocked"
1028
+ details: Dict[str, object] = dict(diagnostics)
1029
+ if state.get("mode") == "serial":
1030
+ try:
1031
+ task = isolation.block_serial_task(
1032
+ self.primary,
1033
+ str(state["task_file"]),
1034
+ str(state["task_id"]),
1035
+ str(state["base"]),
1036
+ )
1037
+ except STOP_ERRORS as error:
1038
+ self.fail(state, f"{reason}; could not record blocked task: {error}")
1039
+ return False
1040
+ details["task"] = task
1041
+ sidecar = state.get("sidecar")
1042
+ if isinstance(sidecar, dict):
1043
+ worktree = Path(str(sidecar["worktree"]))
1044
+ branch = str(sidecar["branch"])
1045
+ try:
1046
+ details["verify_cleanup"] = isolation.retire_unused_verify(
1047
+ self.primary, worktree, str(state["base"]), branch
1048
+ )
1049
+ except STOP_ERRORS as error:
1050
+ reason += f"; Verify sidecar retained at {worktree}: {error}"
1051
+ self.fail(state, reason, **details)
1052
+ return False
1053
+
682
1054
  def classify(self, state: Dict[str, object]) -> bool:
683
1055
  attempt_dir = Path(str(state["_path"])).parent
684
1056
  if self.budgeted and not state.get("usage_recorded"):
@@ -706,6 +1078,10 @@ class Round:
706
1078
  update_state(state, outcome="question", question=unanswered[0], answered=False)
707
1079
  self.receipt["questions"].append({**self.summary(state), "question": unanswered[0],
708
1080
  "log_delta": delta.strip()})
1081
+ elif verdict == "blocked" and named == str(state["task_id"]).lower():
1082
+ return self.blocked_result(
1083
+ state, stdout_tail=tail(message), log_delta=delta.strip()
1084
+ )
709
1085
  elif verdict != "ready" or named != str(state["task_id"]).lower():
710
1086
  self.fail(state, "child returned blocked" if verdict == "blocked"
711
1087
  else "child returned no RESULT line", stdout_tail=tail(message),
@@ -730,6 +1106,8 @@ class Round:
730
1106
  if state.get("wave") != self.receipt["wave"]:
731
1107
  raise DriverStop("task is outside the requested wave", task=state["task_id"],
732
1108
  wave=state.get("wave"), requested_wave=self.receipt["wave"])
1109
+ if state.get("mode") == "member":
1110
+ isolation.ensure_member_hooks(self.primary, str(state["member"]), str(state["task_id"]))
733
1111
  fresh = spawn(self.root, state, self.options,
734
1112
  lambda fresh: brief_text(fresh, self.options.role_brief, self.options.task_template))
735
1113
  self.receipt["dispatched"].append(self.summary(fresh))
@@ -738,8 +1116,14 @@ class Round:
738
1116
  # dispatch -------------------------------------------------------------
739
1117
 
740
1118
  def in_flight_states(self) -> List[Dict[str, object]]:
741
- return [state for state in latest_states(self.root)
742
- if state.get("outcome") is None and state.get("wave") == self.receipt["wave"]]
1119
+ states = []
1120
+ for state in latest_states(self.root):
1121
+ if state.get("outcome") is not None or state.get("wave") != self.receipt["wave"]:
1122
+ continue
1123
+ if state.get("native") and not state.get("command") and not state.get("pid"):
1124
+ continue
1125
+ states.append(state)
1126
+ return states
743
1127
 
744
1128
  def checkpoint_bookkeeping(self) -> None:
745
1129
  open_ids = {state["task_id"] for state in latest_states(self.root)
@@ -763,6 +1147,24 @@ class Round:
763
1147
  ready = build_state.ready(str(self.primary), self.project_dir)
764
1148
  self.receipt["steps"].append({"script": "build_state.py ready", "result": ready})
765
1149
  wave = ready["current_wave"]
1150
+ blocked_review = ready.get("blocked_review")
1151
+ if blocked_review:
1152
+ waiting_waves = blocked_review["waiting_waves"]
1153
+ if self.receipt["wave"] in waiting_waves:
1154
+ raise DriverStop(
1155
+ f"wave {self.receipt['wave']} has unfinished tasks but wave "
1156
+ f"{blocked_review['wave']} review cycle {blocked_review['cycle']} "
1157
+ "is blocked; it must pass before dispatch")
1158
+ self.checkpoint_bookkeeping()
1159
+ return not in_flight
1160
+ repair_for_wave = ready.get("repair_for_wave")
1161
+ if repair_for_wave is not None and self.receipt["wave"] != wave:
1162
+ if self.receipt["wave"] > repair_for_wave:
1163
+ raise DriverStop(
1164
+ f"wave {self.receipt['wave']} is waiting on blocked wave {repair_for_wave}; "
1165
+ f"dispatch repair wave {wave} first")
1166
+ self.checkpoint_bookkeeping()
1167
+ return not in_flight
766
1168
  if wave is not None and wave < self.receipt["wave"]:
767
1169
  raise DriverStop(f"wave {wave} is still unfinished; run round --wave {wave} first")
768
1170
  self.checkpoint_bookkeeping()
@@ -805,6 +1207,28 @@ class Round:
805
1207
  self.runner("lint-round", head)
806
1208
  round_size = len(selected) + len(in_flight)
807
1209
  for task in selected:
1210
+ if task.get("repo"):
1211
+ member = str(task["repo"])
1212
+ retained = isolation.retained_member_task(self.primary, member, task["id"],
1213
+ task["task_file"], head)
1214
+ if retained and retained["state"] == "unused":
1215
+ isolation.retire_member_task(self.primary, member, task["id"])
1216
+ retained = None
1217
+ if retained:
1218
+ isolate, active = retained["isolate"], retained["active"]
1219
+ else:
1220
+ isolate = isolation.isolate_member_task(self.primary, member, task["id"])
1221
+ active = isolation.activate_member_task(self.primary, member, task["id"],
1222
+ f"build_{task['id'].lower()}", task["task_file"], head)
1223
+ sidecar_path = Path(str(active["worktree"]))
1224
+ self.launch({"task_id": task["id"], "title": task["title"],
1225
+ "task_file": Path(str(active["copy"])).relative_to(sidecar_path).as_posix(),
1226
+ "contract_file": task["task_file"], "files": task["files"], "wave": task["wave"],
1227
+ "verify_heavy": task["verify_heavy"], "base": head, "worktree": str(sidecar_path),
1228
+ "task_branch": isolate["task_branch"], "mode": "member", "member": member,
1229
+ "member_base": isolate["member_base"], "project_root": str(self.primary),
1230
+ "sidecar": None, "answered": False, "model_selection": task['model_selection']})
1231
+ continue
808
1232
  prepared = self.runner("prepare-task", head, task["id"], round_size)
809
1233
  isolate = prepared[0]["result"]
810
1234
  sidecar = prepared[1]["result"] if len(prepared) > 1 else None
@@ -1178,7 +1602,7 @@ class Review:
1178
1602
  def helper_json(receipt: Dict[str, object], script: str, *arguments: str, cwd: Path) -> Dict[str, object]:
1179
1603
  """Run a bundled helper and return its JSON stdout; a non-zero exit is a typed stop."""
1180
1604
  command = [sys.executable, "-B", str(Path(__file__).resolve().parent / script), *arguments]
1181
- completed = subprocess.run(command, cwd=cwd, capture_output=True, text=True)
1605
+ completed = subprocess.run(command, cwd=cwd, capture_output=True, encoding="utf-8", errors="replace")
1182
1606
  step = {"script": script, "command": command, "exit_code": completed.returncode,
1183
1607
  "stdout": completed.stdout, "stderr": completed.stderr}
1184
1608
  receipt["steps"].append(step)
@@ -1612,7 +2036,7 @@ def fix_tasks(primary: Path, options: argparse.Namespace) -> Dict[str, object]:
1612
2036
  texts = contracts._task_texts(primary, options.project_dir)
1613
2037
  groups = {group["locator"]: group for group in findings["groups"]}
1614
2038
  today = now()[:10]
1615
- # Same-wave file overlap is forbidden, so repairs land in a newly appended wave.
2039
+ # Unordered same-wave file overlap is rejected, so repairs land in a newly appended wave.
1616
2040
  plan_path = project / "plan/PLAN.md"
1617
2041
  plan_text = plan_path.read_text(encoding="utf-8")
1618
2042
  depths, waves = contracts._plan_waves(plan_text)
@@ -1746,7 +2170,8 @@ Heavy: {'yes' if heavy else 'no'}
1746
2170
  receipt["plan_wave"] = fix_wave
1747
2171
  lint = subprocess.run([sys.executable, "-B", str(Path(__file__).resolve().parent / "check_task_briefs.py"),
1748
2172
  "--repo", str(primary), "--base", isolation.current_sha(primary),
1749
- "--tasks-dir", f"{options.project_dir}/tasks"], capture_output=True, text=True)
2173
+ "--tasks-dir", f"{options.project_dir}/tasks",
2174
+ "--project-dir", options.project_dir], capture_output=True, encoding="utf-8", errors="replace")
1750
2175
  receipt["steps"].append({"script": "check_task_briefs.py", "exit_code": lint.returncode,
1751
2176
  "stdout": lint.stdout, "stderr": lint.stderr})
1752
2177
  if lint.returncode:
@@ -1861,7 +2286,11 @@ def positive_int(value: str) -> int:
1861
2286
 
1862
2287
 
1863
2288
  def default_resource(name: str) -> Optional[Path]:
1864
- candidate = Path(__file__).resolve().parent.parent / name
2289
+ script_root = Path(__file__).resolve().parent.parent
2290
+ candidate = script_root / name
2291
+ if candidate.is_file():
2292
+ return candidate
2293
+ candidate = script_root / "skills" / "gsd-path" / name
1865
2294
  return candidate if candidate.is_file() else None
1866
2295
 
1867
2296
 
@@ -1952,6 +2381,7 @@ def main(argv=None) -> int:
1952
2381
  if arguments.action == "_child":
1953
2382
  return child_main(arguments.state)
1954
2383
  primary = arguments.repo.resolve()
2384
+ _activate_pinned_runtime(primary)
1955
2385
  lock = None
1956
2386
  try:
1957
2387
  if arguments.action != "status":
@@ -1981,6 +2411,17 @@ def main(argv=None) -> int:
1981
2411
  current.recover()
1982
2412
  records = [state for state in latest_states(current.root)
1983
2413
  if state["task_id"] == arguments.task_id]
2414
+ native = None
2415
+ if records and records[0].get("outcome") is not None:
2416
+ # A settled driver record at an older base is superseded by a native retry.
2417
+ try:
2418
+ native = state_from_task(primary, arguments.task_id)
2419
+ except DriverStop:
2420
+ pass
2421
+ if native and native["base"] != records[0].get("base"):
2422
+ records = []
2423
+ else:
2424
+ native = None
1984
2425
  if arguments.task_id in current.proven:
1985
2426
  commit = current.proven[arguments.task_id]
1986
2427
  if records and records[0].get("outcome") is None:
@@ -1993,11 +2434,25 @@ def main(argv=None) -> int:
1993
2434
  raise DriverStop(f"task {arguments.task_id} dispatch record is {record['outcome']}; "
1994
2435
  "use round")
1995
2436
  if record.get("finished_at") is None:
1996
- raise DriverStop(f"task {arguments.task_id} still has a running child")
1997
- current.classify(record)
2437
+ if child_running(record):
2438
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2439
+ if record.get("command"):
2440
+ raise DriverStop(f"task {arguments.task_id} still has a running child")
2441
+ live = state_from_task(primary, arguments.task_id)
2442
+ merged = dict(record, **live)
2443
+ merged["_path"] = record["_path"]
2444
+ try:
2445
+ landing = finish_task(primary, merged)
2446
+ except STOP_ERRORS as error:
2447
+ update_state(record, outcome="blocked", reason=str(error))
2448
+ raise
2449
+ update_state(record, outcome="landed", commit=landing["commit"])
2450
+ current.receipt["landed"].append(landing)
2451
+ else:
2452
+ current.classify(record)
1998
2453
  else: # a coder dispatched by hand: derive the isolate from the task frontmatter
1999
2454
  current.receipt["landed"].append(
2000
- finish_task(primary, state_from_task(primary, arguments.task_id)))
2455
+ finish_task(primary, native or state_from_task(primary, arguments.task_id)))
2001
2456
  result = {**current.receipt, "status": "landed" if current.receipt["landed"] else "blocked"}
2002
2457
  elif arguments.action == "answer":
2003
2458
  result = answer(primary, arguments.task_id, arguments.answer)