@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
@@ -17,12 +17,15 @@ from datetime import date
17
17
  from pathlib import Path, PurePosixPath
18
18
  from typing import Mapping, Optional, Sequence
19
19
 
20
- try:
21
- from isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
22
- except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
23
- if error.name != "isolation":
24
- raise
25
- from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, require_commit, require_full_sha, PROJECT_ENTRIES
20
+ if __package__:
21
+ from .isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
22
+ else:
23
+ try:
24
+ from isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
25
+ except ModuleNotFoundError as error: # pragma: no cover - package imports used by tests
26
+ if error.name != "isolation":
27
+ raise
28
+ from scripts.isolation import IsolationError, checkpoint as isolation_checkpoint, PROJECT_ENTRIES
26
29
 
27
30
 
28
31
  if __package__:
@@ -66,6 +69,11 @@ def _sha256(content: str) -> str:
66
69
  return hashlib.sha256(content.encode("utf-8")).hexdigest()
67
70
 
68
71
 
72
+ def _is_junk_file(path: Path) -> bool:
73
+ # Drift digests skip OS junk by name: Finder rewrites it whatever its ignore status.
74
+ return path.name in pipeline_state._common.OS_JUNK_NAMES and path.is_file() and not path.is_symlink()
75
+
76
+
69
77
  def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
70
78
  if path.is_symlink() or not path.is_dir():
71
79
  raise PipelineStateError(f"promotion path must be a real directory: {path}")
@@ -74,7 +82,7 @@ def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
74
82
  if child.is_symlink():
75
83
  raise PipelineStateError(f"promotion path contains a symlink: {child}")
76
84
  relative = child.relative_to(path).as_posix()
77
- if PurePosixPath(relative).parts[0] in excluded:
85
+ if PurePosixPath(relative).parts[0] in excluded or _is_junk_file(child):
78
86
  continue
79
87
  if child.is_dir():
80
88
  digest.update(f"d\0{relative}\0".encode())
@@ -87,15 +95,15 @@ def _tree_digest(path: Path, excluded: Sequence[str] = ()) -> str:
87
95
 
88
96
 
89
97
  def _checkpoint_artifact_digest(project: Path, mutable_paths: set[str]) -> str:
90
- """Hash every approval artifact except the helper-owned metadata files."""
98
+ """Hash approval artifacts except helper-owned metadata and OS junk."""
91
99
  digest = hashlib.sha256()
92
100
  for child in sorted(project.rglob("*")):
93
101
  if child.is_symlink():
94
102
  raise PipelineStateError(f"checkpoint path contains a symlink: {child}")
95
103
  relative = child.relative_to(project).as_posix()
96
- # Unsupported entries (OS junk) are refused by the checkpoint itself; leaving them
97
- # out lets a refused approval resume after the owner removes them.
98
- if relative in mutable_paths or relative.split("/", 1)[0] not in PROJECT_ENTRIES:
104
+ # Finder can rewrite OS junk during approval; it must not cause artifact drift.
105
+ if (relative in mutable_paths or relative.split("/", 1)[0] not in PROJECT_ENTRIES
106
+ or _is_junk_file(child)):
99
107
  continue
100
108
  if child.is_dir():
101
109
  digest.update(f"d\0{relative}\0".encode())
@@ -333,11 +341,7 @@ def _converge_checkpoint_file(path: Path, before: str, after: str, label: str) -
333
341
 
334
342
  def _unlink_checkpoint_journal(path: Path) -> None:
335
343
  path.unlink()
336
- descriptor = os.open(path.parent, os.O_RDONLY)
337
- try:
338
- os.fsync(descriptor)
339
- finally:
340
- os.close(descriptor)
344
+ pipeline_state._common.fsync_directory(path.parent)
341
345
 
342
346
 
343
347
  def _resume_checkpoint_locked(
@@ -439,41 +443,85 @@ def _validate_plan_briefs(repo: Path, kind: str, project_dir: str) -> None:
439
443
  if head.returncode != 0:
440
444
  # Pre-Git approval defers base-dependent checks to build.
441
445
  return
446
+ if __package__:
447
+ from .check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
448
+ else:
449
+ try:
450
+ from check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
451
+ except ModuleNotFoundError as error: # pragma: no cover - package imports
452
+ if error.name != "check_task_briefs":
453
+ raise
454
+ from scripts.check_task_briefs import BriefError, _member_bases, plan_validation_bases, validate_task_briefs
455
+ if __package__:
456
+ from . import check_handoffs
457
+ else:
458
+ try:
459
+ import check_handoffs
460
+ except ModuleNotFoundError as error:
461
+ if error.name != "check_handoffs":
462
+ raise
463
+ from scripts import check_handoffs
442
464
  try:
443
- from check_task_briefs import BriefError, validate_task_briefs
444
- except ModuleNotFoundError as error: # pragma: no cover - package imports
445
- if error.name != "check_task_briefs":
446
- raise
447
- from scripts.check_task_briefs import BriefError, validate_task_briefs
448
- try:
449
- import check_handoffs
450
- except ModuleNotFoundError as error:
451
- if error.name != "check_handoffs":
452
- raise
453
- from scripts import check_handoffs
454
- try:
455
- tasks, dependency_files = check_handoffs.plan_brief_inputs(repo, project_dir)
456
- landed_bases = {}
457
- for task_id, text in tasks.items():
458
- if check_handoffs._task_scalar(text, task_id, "status") != "done":
459
- continue
460
- agent = check_handoffs._task_scalar(text, task_id, "agent")
461
- if agent in {"", "null"}:
462
- raise BriefError(f"{task_id} landed task has no recorded agent")
463
- recorded_base = check_handoffs._task_scalar(text, task_id, "base")
464
- try:
465
- landed_bases[task_id] = require_commit(repo, require_full_sha(recorded_base))
466
- except IsolationError as error:
467
- raise BriefError(f"{task_id} landed task has invalid historical base: {error}") from error
468
- dependency_files[task_id] = set()
465
+ member_base = _member_bases(repo)
466
+ tasks, dependency_files, landed_bases = plan_validation_bases(
467
+ repo, project_dir, member_base, initial=True
468
+ )
469
469
  validate_task_briefs(
470
470
  repo, head.stdout.strip(), f"{project_dir}/tasks",
471
471
  dependency_files=dependency_files, landed_bases=landed_bases,
472
+ member_base=member_base,
472
473
  )
474
+ if project_dir == ".project/next":
475
+ # Promotion checks lookahead member tasks from the bases their briefs were checked at.
476
+ named = {
477
+ str(check_handoffs._strict_frontmatter(text, task_id).get("repo"))
478
+ for task_id, text in tasks.items()
479
+ if check_handoffs._strict_frontmatter(text, task_id).get("repo")
480
+ and check_handoffs._task_scalar(text, task_id, "status") != "done"
481
+ }
482
+ if named:
483
+ bases = {name: member_base(name)[1] for name in sorted(named)}
484
+ milestone = pipeline_state.load_state(repo, project_dir)[0].milestone
485
+ path = _lookahead_member_bases_path(repo, str(milestone))
486
+ path.parent.mkdir(parents=True, exist_ok=True)
487
+ pipeline_state._common.atomic_write(path, json.dumps(
488
+ {"schema": LOOKAHEAD_MEMBER_BASES_SCHEMA, "milestone": milestone, "bases": bases},
489
+ indent=2, sort_keys=True) + "\n")
473
490
  except (BriefError, check_handoffs.HandoffError) as error:
474
491
  raise PipelineStateError(f"task brief validation failed: {error}") from error
475
492
 
476
493
 
494
+ LOOKAHEAD_MEMBER_BASES_SCHEMA = "gsd-path/lookahead-member-bases/v1"
495
+
496
+
497
+ def _lookahead_member_bases_path(repo: Path, milestone: str) -> Path:
498
+ common = pipeline_state._run_git(repo, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
499
+ return Path(common) / "gsd-path" / "lookahead-member-bases" / f"{milestone}.json"
500
+
501
+
502
+ def _lookahead_member_bases(repo: Path, milestone: str) -> Optional[dict]:
503
+ path = _lookahead_member_bases_path(repo, milestone)
504
+ if not path.is_file():
505
+ return None
506
+ value = json.loads(path.read_text(encoding="utf-8"))
507
+ if value.get("schema") != LOOKAHEAD_MEMBER_BASES_SCHEMA or value.get("milestone") != milestone:
508
+ return None
509
+ return value.get("bases")
510
+
511
+
512
+ def _shipped_member_merges(repo: Path, landing: str) -> dict:
513
+ """Member merges named by the ship commit the integration landing merged."""
514
+ if __package__:
515
+ from .pipeline_git import ship_member_rows
516
+ else:
517
+ from pipeline_git import ship_member_rows
518
+ ship = pipeline_state._run_git(repo, "rev-parse", "--verify", "--quiet", f"{landing}^2", check=False)
519
+ if ship.returncode != 0:
520
+ return {}
521
+ message = pipeline_state._run_git(repo, "show", "-s", "--format=%B", ship.stdout.strip()).stdout
522
+ return {row["name"]: row["merge"] for row in ship_member_rows(message)}
523
+
524
+
477
525
  def checkpoint_approval(
478
526
  repo: Path,
479
527
  kind: str,
@@ -1027,6 +1075,46 @@ def _classify_plan_drift(
1027
1075
  }
1028
1076
  approved = _approved_task_contracts(repo, checkpoint)
1029
1077
  current = _task_contracts_at(repo, revision)
1078
+ if __package__:
1079
+ from . import check_handoffs
1080
+ else:
1081
+ import check_handoffs
1082
+ # Member task paths live in member repos: compare each member from the base its brief
1083
+ # was checked at to the member merge the shipped milestone made (both fixed commits).
1084
+ task_repos = {
1085
+ task_id: str(check_handoffs._strict_frontmatter(contract.text, contract.path).get("repo") or "")
1086
+ for task_id, contract in approved.items()
1087
+ }
1088
+ active_member_tasks = {
1089
+ task_id for task_id, name in task_repos.items()
1090
+ if name and check_handoffs._task_scalar(approved[task_id].text, task_id, "status") != "done"
1091
+ }
1092
+ active_members = {task_repos[task_id] for task_id in active_member_tasks}
1093
+ member_changes: dict[str, list] = {}
1094
+ if active_members:
1095
+ bases = _lookahead_member_bases(repo, str(state.milestone)) or {}
1096
+ if any(name not in bases for name in active_members):
1097
+ return {
1098
+ "class": "unverifiable",
1099
+ "checkpoint": checkpoint,
1100
+ "task_ids": [],
1101
+ "changed_paths": [],
1102
+ "contract_paths": [],
1103
+ "member_tasks": True,
1104
+ "reason": "member bases were not recorded at plan approval",
1105
+ }
1106
+ if __package__:
1107
+ from . import members
1108
+ else:
1109
+ import members
1110
+ checkouts = {item["name"]: Path(item["checkout"]) for item in members.read_members(repo)}
1111
+ merges = _shipped_member_merges(repo, landing or revision)
1112
+ for name in active_members:
1113
+ member_changes[name] = [
1114
+ item for item in pipeline_state._run_git(
1115
+ checkouts[name], "diff", "--no-renames", "--name-only", "-z", bases[name], merges[name]
1116
+ ).stdout.split("\0") if item
1117
+ ] if name in merges else []
1030
1118
 
1031
1119
  contract_paths: set[str] = set()
1032
1120
  flagged_ids: set[str] = set()
@@ -1050,9 +1138,12 @@ def _classify_plan_drift(
1050
1138
 
1051
1139
  product_paths: set[str] = set()
1052
1140
  for task_id, contract in approved.items():
1141
+ member = task_repos.get(task_id)
1142
+ if member and task_id not in active_member_tasks:
1143
+ continue
1053
1144
  matches = sorted(
1054
- changed_path
1055
- for changed_path in changed
1145
+ (f"{member}:{changed_path}" if member else changed_path)
1146
+ for changed_path in (member_changes[member] if member else changed)
1056
1147
  if any(
1057
1148
  changed_path == path
1058
1149
  or changed_path.startswith(f"{path.rstrip('/')}/")
@@ -82,6 +82,8 @@ def _promotion_event(
82
82
  if isinstance(task_ids, list) and task_ids:
83
83
  return f"{prefix}; plan drift flagged tasks {', '.join(task_ids)}"
84
84
  return f"{prefix}; plan contract changed after approval"
85
+ if drift_class == "unverifiable" and drift.get("member_tasks"):
86
+ return f"{prefix}; plan drift unverifiable because member bases were not recorded at plan approval"
85
87
  if drift_class == "unverifiable":
86
88
  return f"{prefix}; plan drift unverifiable because approval checkpoint is missing"
87
89
  return prefix
@@ -5,27 +5,45 @@ import argparse
5
5
  import hashlib
6
6
  import json
7
7
  import os
8
+ import re
8
9
  import subprocess
9
10
  import sys
10
11
  import tempfile
11
12
  from pathlib import Path
12
13
 
13
14
  try:
14
- from scripts import _common
15
+ from scripts import _common, isolation
15
16
  except ImportError: # bundled copy inside a skill's scripts directory
16
17
  import _common
18
+ import isolation
17
19
 
18
20
 
19
21
  class StepFailed(RuntimeError):
20
22
  pass
21
23
 
24
+
25
+ def _table_rows(body):
26
+ """Cells of a Markdown table's data rows (header and separator skipped)."""
27
+ lines = [line.strip() for line in (body or "").splitlines() if line.strip().startswith("|")]
28
+ return [[cell.strip() for cell in re.split(r"(?<!\\)\|", line.strip("|"))] for line in lines[2:]]
29
+
22
30
  INSPECTION_SPECS = (("inspect_codebase", "codebase-mapper", "codebase", "evidence-codebase.md"),
23
31
  ("inspect_docs", "docs-auditor", "docs-audit", "DOCS-AUDIT.md"))
24
32
 
25
33
 
34
+ def _task_member(repo: Path, project_dir: str, task_id: str):
35
+ """The `repo:` member a task names, or None for a coordinator task."""
36
+ for path in sorted((repo / project_dir / "tasks").glob(f"{task_id}-*.md")):
37
+ fields, error = isolation.task_frontmatter(path.read_text(encoding="utf-8"))
38
+ if error:
39
+ raise StepFailed(error)
40
+ return fields.get("repo")
41
+ return None
42
+
43
+
26
44
  def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str = None,
27
45
  task_id: str = None, round_size: int = None, inspection: Path = None,
28
- mapper_reviewed: bool = False) -> dict:
46
+ mapper_reviewed: bool = False, kind: str = None) -> dict:
29
47
  steps = []
30
48
  outputs = {}
31
49
  scripts = Path(__file__).resolve().parent
@@ -33,7 +51,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
33
51
 
34
52
  def step(script, *arguments, raw=False):
35
53
  command = [sys.executable, "-B", str(helpers / script), *arguments]
36
- completed = subprocess.run(command, cwd=repo, capture_output=True, text=True)
54
+ completed = subprocess.run(command, cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
37
55
  receipt = {"script": script, "command": command, "exit_code": completed.returncode,
38
56
  "stdout": completed.stdout, "stderr": completed.stderr}
39
57
  steps.append(receipt)
@@ -50,12 +68,29 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
50
68
  return receipt["result"]
51
69
 
52
70
  common = ["--repo", str(repo), "--project-dir", project_dir]
71
+ next_step = "$gsd-path-forensics"
72
+
73
+ def gate_plan():
74
+ if step("discussion_records.py", "pending", "--repo", str(repo))["pending"]:
75
+ raise StepFailed("pending discussion requires its owner disposition")
76
+ step("check_handoffs.py", "plan", *common)
77
+ head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"],
78
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
79
+ step("check_task_briefs.py", "--repo", str(repo), "--base", head,
80
+ "--tasks-dir", f"{project_dir}/tasks", "--project-dir", project_dir)
81
+ panel = ["validate-plan", "--plan", str(repo / project_dir / "plan/PLAN.md"),
82
+ "--intent", str(repo / project_dir / "intent/INTENT.md")]
83
+ charter = repo / ".project/CHARTER.md"
84
+ if charter.exists():
85
+ panel += ["--charter", str(charter)]
86
+ return step("review_panel.py", *panel)
87
+
53
88
  try:
54
89
  if os.path.lexists(repo / ".gsd-path/runtime.json"):
55
90
  resolved = subprocess.run(
56
91
  [sys.executable, "-B", str(repo / ".gsd-path/status_runtime.py"),
57
92
  "--repo", str(repo), "--runtime-path"],
58
- cwd=repo, capture_output=True, text=True)
93
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace")
59
94
  if resolved.returncode:
60
95
  sys.stderr.write(resolved.stderr)
61
96
  raise StepFailed(resolved.stderr.strip())
@@ -71,7 +106,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
71
106
  (repo / project_dir / "research" / spec[3]).is_symlink() for spec in specs):
72
107
  raise StepFailed("prior inspection evidence requires the re-inspection contract")
73
108
  branch = subprocess.run(["git", "branch", "--show-current"], cwd=repo,
74
- capture_output=True, text=True, check=True).stdout.strip()
109
+ capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
75
110
  if branch != state["branch"]:
76
111
  raise StepFailed("current branch differs from the recorded inspection branch")
77
112
  pending = step("discussion_records.py", "pending", "--repo", str(repo))
@@ -87,7 +122,7 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
87
122
  raise StepFailed(f"missing inspection resource: {path}")
88
123
  staging = Path(tempfile.mkdtemp(prefix="gsd-path-inspect-"))
89
124
  inventory_file = staging / "inventory.txt"
90
- inventory_file.write_text(inventory, encoding="utf-8")
125
+ inventory_file.write_bytes(inventory.encode("utf-8"))
91
126
  assignments = []
92
127
  for task_name, role, template, filename in specs:
93
128
  isolated = step("isolation.py", "isolate-verify", "--repo", str(repo),
@@ -100,13 +135,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
100
135
  f"Template: {template_path}\nRepository and command cwd: {isolated['worktree']}\n"
101
136
  f"Recorded baseline and Audited HEAD: {isolated['base']}\n"
102
137
  f"Write only: {Path(isolated['worktree']) / output}\n"
138
+ f"Write header Repo root: {repo}\n"
103
139
  "Use only this supplied sidecar for project commands; do not create worktrees.\n"
104
140
  "The parent owns gates, collection, retirement, and phase state.\n"
105
141
  "Exclude .git, node_modules, .project/archive, and installed GSD Path and path skill bundles.\n")
106
142
  if task_name == "inspect_docs":
107
143
  brief += ("Alignment mode: false. No prior audit or changed set.\n"
108
144
  "Frozen inventory follows; use it exactly, without rediscovery:\n" + inventory)
109
- brief_file.write_text(brief, encoding="utf-8")
145
+ brief_file.write_bytes(brief.encode("utf-8"))
110
146
  assignments.append({**isolated, "task_name": task_name, "role": str(role_path),
111
147
  "template": str(template_path), "output": output,
112
148
  "brief_file": str(brief_file)})
@@ -136,16 +172,16 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
136
172
  if state["phase"] != "inspect" or state["status"] != "active":
137
173
  raise StepFailed("inspection completion requires inspect/active")
138
174
  branch = subprocess.run(["git", "branch", "--show-current"], cwd=repo,
139
- capture_output=True, text=True, check=True).stdout.strip()
175
+ capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
140
176
  if state["branch"] != branch:
141
177
  raise StepFailed("current branch differs from the recorded inspection branch")
142
178
  pending = step("discussion_records.py", "pending", "--repo", str(repo))
143
179
  if pending["pending"]:
144
180
  raise StepFailed("pending discussion requires its owner disposition")
145
181
  docs = assignments["inspect_docs"]
146
- step("check_docs_audit.py", "--repo", docs["worktree"], "--audit", docs["output"],
147
- "--inventory", str(inventory_file))
148
- if f"Audited HEAD: {expected_head}" not in (Path(docs["worktree"]) / docs["output"]).read_text().splitlines():
182
+ step("check_docs_audit.py", "--repo", docs["worktree"], "--primary", str(repo),
183
+ "--audit", docs["output"], "--inventory", str(inventory_file))
184
+ if f"Audited HEAD: {expected_head}" not in (Path(docs["worktree"]) / docs["output"]).read_text(encoding="utf-8").splitlines():
149
185
  raise StepFailed("audit does not name the supplied inspection baseline")
150
186
  for task_name, _, _, _ in INSPECTION_SPECS:
151
187
  assignment = assignments[task_name]
@@ -161,10 +197,14 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
161
197
  "--set-phase", "inspect", "--set-status", "done", "--event", "inspection artifacts passed")
162
198
  elif action == "lint-round":
163
199
  head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"],
164
- cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
200
+ cwd=repo, capture_output=True, encoding="utf-8", errors="replace", check=True).stdout.strip()
165
201
  step("check_task_briefs.py", "--repo", str(repo), "--base", head,
166
- "--tasks-dir", f"{project_dir}/tasks")
202
+ "--tasks-dir", f"{project_dir}/tasks", "--project-dir", project_dir)
167
203
  step("check_handoffs.py", "plan", *common)
204
+ elif action == "prepare-task" and _task_member(repo, project_dir, task_id):
205
+ # A member task works in a sidecar of its member; its Verify runs there at finish.
206
+ step("isolation.py", "isolate-member-task", "--repo", str(repo),
207
+ "--member", _task_member(repo, project_dir, task_id), "--task-id", task_id)
168
208
  elif action == "prepare-task":
169
209
  task = step("isolation.py", "isolate-task", "--repo", str(repo),
170
210
  "--base", expected_head, "--task-id", task_id,
@@ -186,33 +226,67 @@ def run_workflow(repo: Path, action: str, project_dir: str, expected_head: str =
186
226
  raise StepFailed("pending discussion requires its owner disposition")
187
227
  step("build_state.py", "verify-landed", *common, "--head", expected_head)
188
228
  step("lean_verification.py", "--repo", str(repo), "--expected-head", expected_head)
189
- else:
190
- pending = step("discussion_records.py", "pending", "--repo", str(repo))
191
- if pending["pending"]:
229
+ elif action == "preauthorize":
230
+ # A block here is not a failure: the gate asks its owner as usual.
231
+ next_step = "ask the owner at this gate"
232
+ if project_dir != ".project" or kind not in {"intent", "plan"}:
233
+ raise StepFailed("pre-approval covers only intent or plan on the active track")
234
+ routed = step("pipeline_state.py", "route", *common)
235
+ if routed["route"].get("action") != "run-phase" or "recovery" in routed:
236
+ raise StepFailed("pre-approval needs an ordinary phase route")
237
+ if step("discussion_records.py", "pending", "--repo", str(repo))["pending"]:
192
238
  raise StepFailed("pending discussion requires its owner disposition")
193
- step("check_handoffs.py", "plan", *common)
194
- head = subprocess.run(["git", "rev-parse", "--verify", "HEAD"],
195
- cwd=repo, capture_output=True, text=True, check=True).stdout.strip()
196
- step("check_task_briefs.py", "--repo", str(repo), "--base", head,
197
- "--tasks-dir", f"{project_dir}/tasks")
198
- panel = ["validate-plan", "--plan", str(repo / project_dir / "plan/PLAN.md"),
199
- "--intent", str(repo / project_dir / "intent/INTENT.md")]
200
- charter = repo / ".project/CHARTER.md"
201
- if charter.exists():
202
- panel += ["--charter", str(charter)]
203
- step("review_panel.py", *panel)
239
+ track = repo / project_dir
240
+ intent = (track / "intent/INTENT.md").read_text(encoding="utf-8")
241
+ lane = re.search(r"(?m)^Lane:\s*(.+)$", intent)
242
+ if not lane or lane.group(1).split("<!--", 1)[0].strip().strip("`") != "quick":
243
+ raise StepFailed("pre-approval covers only the quick lane")
244
+ intent = re.sub(r"(?s)<!--.*?-->", "", intent)
245
+ if re.search(r"(?m)^\s*-\s*\[(RESEARCH|NEEDS-USER)\]", _common.section_body(intent, "Open questions") or ""):
246
+ raise StepFailed("INTENT.md has open RESEARCH or NEEDS-USER questions")
247
+ audit_path = track / "research/DOCS-AUDIT.md"
248
+ if audit_path.exists():
249
+ audit = audit_path.read_text(encoding="utf-8")
250
+ queue = _table_rows(_common.section_body(audit, "Remediation queue"))
251
+ ruled = set()
252
+ for row in _table_rows(_common.section_body(audit, "User rulings")):
253
+ if len(row) != 4 or row[1] not in {"fix-code", "fix-doc", "accept-drift"}:
254
+ continue
255
+ words = row[2].strip().strip('`"').strip()
256
+ if words and words.casefold() != "none" and not re.fullmatch(r"<[^>]*>", words):
257
+ ruled.add(row[0])
258
+ if any(len(row) == 6 and row[4] == "NEEDS-USER" and row[0] not in ruled for row in queue):
259
+ raise StepFailed("DOCS-AUDIT.md has NEEDS-USER rows without a user ruling")
260
+ if kind == "intent":
261
+ # Malformed or incomplete spec-probe tables are not a pre-approvable draft.
262
+ step("check_handoffs.py", "intent", *common)
263
+ if kind == "plan":
264
+ if gate_plan()["mode"] != "off":
265
+ raise StepFailed("pre-approval requires review_panel off")
266
+ plan = re.sub(r"(?s)<!--.*?-->", "", (track / "plan/PLAN.md").read_text(encoding="utf-8"))
267
+ config = _common.section_body(plan, "Config") or ""
268
+ skeptics = re.search(r"(?m)^[ \t]*-[ \t]*finding_skeptics:[ \t]*(\S+)", config)
269
+ if skeptics and skeptics.group(1) != "off":
270
+ raise StepFailed("pre-approval requires finding_skeptics off")
271
+ # Quick-lane limit from project policy (gsd-path-plan Quick mode): one wave, at most two tasks.
272
+ if (len(re.findall(r"(?m)^## Wave \d+ — ", plan)) != 1
273
+ or len(list((track / "tasks").glob("T[0-9][0-9][0-9]-*.md"))) > 2):
274
+ raise StepFailed("pre-approval requires one wave and at most two tasks")
275
+ step("pipeline_state.py", "pre-approve", "--repo", str(repo), "--use", kind)
276
+ else:
277
+ gate_plan()
204
278
  if action == "approve-plan":
205
279
  step("pipeline_state.py", "approve", *common, "--kind", "plan",
206
280
  "--expected-head", expected_head)
207
281
  except (StepFailed, OSError, subprocess.CalledProcessError, ValueError, KeyError, TypeError) as error:
208
282
  return {"status": "blocked", "reason": str(error), "steps": steps,
209
- "next": "$gsd-path-forensics"}
283
+ "next": next_step}
210
284
  return {"status": "complete", "steps": steps, **outputs}
211
285
 
212
286
 
213
287
  def main(argv=None):
214
288
  parser = argparse.ArgumentParser(description=__doc__)
215
- parser.add_argument("action", choices=("route", "gate-plan", "approve-plan", "build-evidence", "lint-round", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect"))
289
+ parser.add_argument("action", choices=("route", "gate-plan", "approve-plan", "build-evidence", "lint-round", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect", "preauthorize"))
216
290
  parser.add_argument("--repo", type=Path, required=True)
217
291
  parser.add_argument("--project-dir", choices=(".project", ".project/next"), default=".project")
218
292
  parser.add_argument("--expected-head")
@@ -220,15 +294,18 @@ def main(argv=None):
220
294
  parser.add_argument("--round-size", type=int)
221
295
  parser.add_argument("--inspection", type=Path)
222
296
  parser.add_argument("--mapper-reviewed", action="store_true")
297
+ parser.add_argument("--kind", choices=("intent", "plan"))
223
298
  arguments = parser.parse_args(argv)
224
299
  if arguments.action in {"approve-plan", "build-evidence", "prepare-task", "prepare-final", "prepare-inspect", "finish-inspect"} and not arguments.expected_head:
225
300
  parser.error(f"{arguments.action} requires --expected-head")
226
301
  if arguments.action == "prepare-task" and (not arguments.task_id or arguments.round_size is None):
227
302
  parser.error("prepare-task requires --task-id and --round-size")
303
+ if arguments.action == "preauthorize" and not arguments.kind:
304
+ parser.error("preauthorize requires --kind")
228
305
  result = run_workflow(arguments.repo.resolve(), arguments.action,
229
306
  arguments.project_dir, arguments.expected_head,
230
307
  arguments.task_id, arguments.round_size, arguments.inspection,
231
- arguments.mapper_reviewed)
308
+ arguments.mapper_reviewed, arguments.kind)
232
309
  print(json.dumps(result, sort_keys=True))
233
310
  return 0 if result["status"] == "complete" else 1
234
311
 
@@ -13,10 +13,34 @@ import os
13
13
  import tempfile
14
14
  from pathlib import Path
15
15
 
16
- try:
17
- import _common
18
- except ImportError:
19
- from scripts import _common
16
+ if __package__:
17
+ from . import _common
18
+ else:
19
+ try:
20
+ import _common
21
+ except ImportError:
22
+ from scripts import _common
23
+
24
+
25
+ def _worktree_list_porcelain(primary: Path) -> tuple[str, bool]:
26
+ try:
27
+ if __package__:
28
+ from . import pipeline_git as pg
29
+ else:
30
+ from scripts import pipeline_git as pg
31
+ except ImportError:
32
+ try:
33
+ import pipeline_git as pg
34
+ except ImportError:
35
+ pg = None
36
+ if pg is not None:
37
+ return pg.worktree_list_porcelain(primary)
38
+ try:
39
+ output = _git(primary, "worktree", "list", "--porcelain", "-z")
40
+ return output, True
41
+ except ValueError:
42
+ output = _git(primary, "worktree", "list", "--porcelain")
43
+ return output, False
20
44
 
21
45
 
22
46
  def _git(primary: Path, *args: str) -> str:
@@ -27,7 +51,10 @@ def _git(primary: Path, *args: str) -> str:
27
51
 
28
52
 
29
53
  def _identity(path: Path) -> str:
30
- return hashlib.sha256(os.fsencode(path)).hexdigest()
54
+ digest = hashlib.sha256(os.fsencode(path)).hexdigest()
55
+ # Two identities nest in every managed path; full digests would use about
56
+ # 130 of Windows' 260-character MAX_PATH before any repository content.
57
+ return digest[:16] if os.name == "nt" else digest
31
58
 
32
59
 
33
60
  def _workspace(primary: Path, pin: bool) -> Path:
@@ -65,7 +92,7 @@ def _workspace(primary: Path, pin: bool) -> Path:
65
92
  receipt.parent.mkdir(parents=True, exist_ok=True)
66
93
  fd, temporary = tempfile.mkstemp(dir=receipt.parent)
67
94
  try:
68
- with os.fdopen(fd, "w", encoding="utf-8") as handle:
95
+ with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
69
96
  json.dump({"primary": str(primary), "root": str(workspace)}, handle)
70
97
  try:
71
98
  os.link(temporary, receipt)
@@ -76,17 +103,52 @@ def _workspace(primary: Path, pin: bool) -> Path:
76
103
  return workspace
77
104
 
78
105
 
106
+ def release_workspace(primary: Path) -> bool:
107
+ """Forget the pinned placement receipt once no linked worktrees remain.
108
+
109
+ Lets GSD_PATH_WORKTREE_ROOT (or a new default) apply again at the next
110
+ milestone's first allocation. Returns True when a receipt was removed;
111
+ any remaining linked worktree (including one only still registered) keeps
112
+ the pin.
113
+ """
114
+ primary = primary.resolve()
115
+ common = Path(_git(primary, "rev-parse", "--git-common-dir"))
116
+ common = (primary / common).resolve()
117
+ receipt = common / "gsd-path" / "workspaces" / f"{_identity(primary)}.json"
118
+ if not receipt.is_file():
119
+ return False
120
+ output, nul_separated = _worktree_list_porcelain(primary)
121
+ records = output.split("\0\0") if nul_separated else [
122
+ block for block in output.strip().split("\n\n") if block.strip()
123
+ ]
124
+ for record in records:
125
+ fields = record.split("\0") if nul_separated else record.splitlines()
126
+ for field in fields:
127
+ if field.startswith("worktree ") and Path(field[len("worktree "):]).resolve() != primary:
128
+ return False
129
+ receipt.unlink()
130
+ return True
131
+
132
+
79
133
  def worktree_path(primary: Path, kind: str, name: str, *, pin: bool = False) -> Path:
80
134
  primary = primary.resolve()
81
- if kind not in {"task", "verify", "integrate"} or not name or Path(name).name != name or name in {".", ".."}:
135
+ if kind not in {"task", "verify", "integrate", "bound"} or not name or Path(name).name != name or name in {".", ".."}:
82
136
  raise ValueError("invalid managed worktree kind or name")
137
+ if kind == "bound":
138
+ return _workspace(primary, pin) / kind / name
83
139
  legacy = (primary.parent / f".{primary.name}-gsd-path-integrate-{name}"
84
140
  if kind == "integrate" else primary.parent / f"{primary.name}.gsd-path" / kind / name)
85
141
  branch = f"refs/heads/gsd-path-{kind}/{name}"
86
- records = _git(primary, "worktree", "list", "--porcelain", "-z").split("\0\0")
142
+ output, nul_separated = _worktree_list_porcelain(primary)
143
+ if nul_separated:
144
+ records = output.split("\0\0")
145
+ else:
146
+ records = [block for block in output.strip().split("\n\n") if block.strip()]
87
147
  for record in records:
88
- fields = record.split("\0")
89
- if f"worktree {legacy}" in fields and f"branch {branch}" in fields:
148
+ fields = record.split("\0") if nul_separated else record.splitlines()
149
+ # Compare as paths: git prints forward slashes on Windows.
150
+ worktrees = [Path(field[len("worktree "):]) for field in fields if field.startswith("worktree ")]
151
+ if legacy in worktrees and f"branch {branch}" in fields:
90
152
  return legacy
91
153
  # Keep collision checks and missing-directory recovery at the old location.
92
154
  if os.path.lexists(legacy):