@opengsd/gsd-path 1.0.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 (398) hide show
  1. package/AGENTS.md +397 -0
  2. package/DOCS.md +341 -0
  3. package/FULL.md +475 -0
  4. package/GUIDE.md +23 -0
  5. package/HOOKS.md +273 -0
  6. package/LICENSE +21 -0
  7. package/MIGRATE.md +100 -0
  8. package/QUICK.md +146 -0
  9. package/README.md +280 -0
  10. package/RUNTIME.md +229 -0
  11. package/UPDATE.md +196 -0
  12. package/WORKFLOW.md +683 -0
  13. package/package.json +80 -0
  14. package/platforms/claude/dispatch.md +63 -0
  15. package/platforms/copilot/dispatch.md +61 -0
  16. package/platforms/cursor/agent.md +17 -0
  17. package/platforms/cursor/dispatch.md +63 -0
  18. package/platforms/grok/dispatch.md +61 -0
  19. package/platforms/kimi/dispatch.md +64 -0
  20. package/platforms/kiro/dispatch.md +58 -0
  21. package/platforms/opencode/dispatch.md +62 -0
  22. package/platforms/qwen/dispatch.md +60 -0
  23. package/platforms/shared-agents/dispatch.md +131 -0
  24. package/plugin.json +14 -0
  25. package/scripts/_common.py +180 -0
  26. package/scripts/archive_milestone.py +2265 -0
  27. package/scripts/bootstrap_repository.py +710 -0
  28. package/scripts/build_state.py +854 -0
  29. package/scripts/check_docs_audit.py +463 -0
  30. package/scripts/check_handoffs.py +1855 -0
  31. package/scripts/check_task_briefs.py +376 -0
  32. package/scripts/check_update.py +93 -0
  33. package/scripts/core_hook_gate.py +34 -0
  34. package/scripts/core_hook_settings.py +119 -0
  35. package/scripts/detect_project.py +1517 -0
  36. package/scripts/discussion_records.py +619 -0
  37. package/scripts/discussion_validate.py +1321 -0
  38. package/scripts/dispatch_driver.py +1812 -0
  39. package/scripts/git_guard.py +759 -0
  40. package/scripts/guard_hook.py +2124 -0
  41. package/scripts/install.mjs +3239 -0
  42. package/scripts/install.py +3008 -0
  43. package/scripts/integration.py +1433 -0
  44. package/scripts/isolation.py +3538 -0
  45. package/scripts/lean_verification.py +256 -0
  46. package/scripts/loop_run.py +837 -0
  47. package/scripts/migrate_core.py +169 -0
  48. package/scripts/pipeline_diagnose.py +715 -0
  49. package/scripts/pipeline_git.py +940 -0
  50. package/scripts/pipeline_state.py +2291 -0
  51. package/scripts/pipeline_undo.py +1039 -0
  52. package/scripts/promote_lookahead.py +415 -0
  53. package/scripts/review_findings.py +714 -0
  54. package/scripts/review_panel.py +553 -0
  55. package/scripts/skill-resources.json +340 -0
  56. package/scripts/state_checkpoint.py +1039 -0
  57. package/scripts/state_promote.py +743 -0
  58. package/scripts/status_runtime.py +122 -0
  59. package/scripts/sync_skill_resources.py +259 -0
  60. package/scripts/token_budget.py +146 -0
  61. package/scripts/wizard.mjs +179 -0
  62. package/scripts/workflow_run.py +117 -0
  63. package/skills/gsd-path/BUILD.md +700 -0
  64. package/skills/gsd-path/DECIDE.md +115 -0
  65. package/skills/gsd-path/DEFINE.md +297 -0
  66. package/skills/gsd-path/DOCS-AUDIT.md +190 -0
  67. package/skills/gsd-path/INSPECT.md +167 -0
  68. package/skills/gsd-path/PLAN.md +411 -0
  69. package/skills/gsd-path/RESEARCH.md +159 -0
  70. package/skills/gsd-path/ROADMAP.md +215 -0
  71. package/skills/gsd-path/SHIP.md +412 -0
  72. package/skills/gsd-path/SKILL.md +471 -0
  73. package/skills/gsd-path/agents/openai.yaml +6 -0
  74. package/skills/gsd-path/references/codebase-mapper.md +52 -0
  75. package/skills/gsd-path/references/coder.md +90 -0
  76. package/skills/gsd-path/references/decider.md +36 -0
  77. package/skills/gsd-path/references/dispatch.md +131 -0
  78. package/skills/gsd-path/references/docs-auditor.md +60 -0
  79. package/skills/gsd-path/references/planner.md +104 -0
  80. package/skills/gsd-path/references/researcher.md +42 -0
  81. package/skills/gsd-path/references/reviewer.md +188 -0
  82. package/skills/gsd-path/references/roadmapper.md +48 -0
  83. package/skills/gsd-path/scripts/_common.py +180 -0
  84. package/skills/gsd-path/scripts/archive_milestone.py +2265 -0
  85. package/skills/gsd-path/scripts/bootstrap_repository.py +710 -0
  86. package/skills/gsd-path/scripts/build_state.py +854 -0
  87. package/skills/gsd-path/scripts/check_docs_audit.py +463 -0
  88. package/skills/gsd-path/scripts/check_handoffs.py +1855 -0
  89. package/skills/gsd-path/scripts/check_task_briefs.py +376 -0
  90. package/skills/gsd-path/scripts/check_update.py +93 -0
  91. package/skills/gsd-path/scripts/detect_project.py +1517 -0
  92. package/skills/gsd-path/scripts/discussion_records.py +619 -0
  93. package/skills/gsd-path/scripts/discussion_validate.py +1321 -0
  94. package/skills/gsd-path/scripts/dispatch_driver.py +1812 -0
  95. package/skills/gsd-path/scripts/integration.py +1433 -0
  96. package/skills/gsd-path/scripts/isolation.py +3538 -0
  97. package/skills/gsd-path/scripts/lean_verification.py +256 -0
  98. package/skills/gsd-path/scripts/loop_run.py +837 -0
  99. package/skills/gsd-path/scripts/pipeline_git.py +940 -0
  100. package/skills/gsd-path/scripts/pipeline_state.py +2291 -0
  101. package/skills/gsd-path/scripts/promote_lookahead.py +415 -0
  102. package/skills/gsd-path/scripts/review_findings.py +714 -0
  103. package/skills/gsd-path/scripts/review_panel.py +553 -0
  104. package/skills/gsd-path/scripts/state_checkpoint.py +1039 -0
  105. package/skills/gsd-path/scripts/state_promote.py +743 -0
  106. package/skills/gsd-path/scripts/token_budget.py +146 -0
  107. package/skills/gsd-path/scripts/workflow_run.py +117 -0
  108. package/skills/gsd-path/templates/answers.md +29 -0
  109. package/skills/gsd-path/templates/archive-manifest.md +37 -0
  110. package/skills/gsd-path/templates/charter.md +50 -0
  111. package/skills/gsd-path/templates/codebase.md +46 -0
  112. package/skills/gsd-path/templates/dialogue.md +21 -0
  113. package/skills/gsd-path/templates/docs-audit.md +69 -0
  114. package/skills/gsd-path/templates/evidence.md +24 -0
  115. package/skills/gsd-path/templates/final-review.md +27 -0
  116. package/skills/gsd-path/templates/gap-review.md +20 -0
  117. package/skills/gsd-path/templates/intent.md +98 -0
  118. package/skills/gsd-path/templates/loop.md +59 -0
  119. package/skills/gsd-path/templates/patch-findings.md +16 -0
  120. package/skills/gsd-path/templates/plan-panel.md +25 -0
  121. package/skills/gsd-path/templates/plan.md +89 -0
  122. package/skills/gsd-path/templates/repository.md +16 -0
  123. package/skills/gsd-path/templates/research-handoff.md +25 -0
  124. package/skills/gsd-path/templates/roadmap.md +38 -0
  125. package/skills/gsd-path/templates/skeptic.md +36 -0
  126. package/skills/gsd-path/templates/state.md +27 -0
  127. package/skills/gsd-path/templates/synthesis.md +36 -0
  128. package/skills/gsd-path/templates/task.md +74 -0
  129. package/skills/gsd-path/templates/wave-panel.md +25 -0
  130. package/skills/gsd-path/templates/wave-review.md +64 -0
  131. package/skills/gsd-path-build/SKILL.md +700 -0
  132. package/skills/gsd-path-build/agents/openai.yaml +6 -0
  133. package/skills/gsd-path-build/references/coder.md +90 -0
  134. package/skills/gsd-path-build/references/dispatch.md +131 -0
  135. package/skills/gsd-path-build/references/reviewer.md +188 -0
  136. package/skills/gsd-path-build/scripts/_common.py +180 -0
  137. package/skills/gsd-path-build/scripts/archive_milestone.py +2265 -0
  138. package/skills/gsd-path-build/scripts/build_state.py +854 -0
  139. package/skills/gsd-path-build/scripts/check_handoffs.py +1855 -0
  140. package/skills/gsd-path-build/scripts/check_task_briefs.py +376 -0
  141. package/skills/gsd-path-build/scripts/discussion_records.py +619 -0
  142. package/skills/gsd-path-build/scripts/discussion_validate.py +1321 -0
  143. package/skills/gsd-path-build/scripts/dispatch_driver.py +1812 -0
  144. package/skills/gsd-path-build/scripts/integration.py +1433 -0
  145. package/skills/gsd-path-build/scripts/isolation.py +3538 -0
  146. package/skills/gsd-path-build/scripts/loop_run.py +837 -0
  147. package/skills/gsd-path-build/scripts/pipeline_git.py +940 -0
  148. package/skills/gsd-path-build/scripts/pipeline_state.py +2291 -0
  149. package/skills/gsd-path-build/scripts/review_findings.py +714 -0
  150. package/skills/gsd-path-build/scripts/review_panel.py +553 -0
  151. package/skills/gsd-path-build/scripts/state_checkpoint.py +1039 -0
  152. package/skills/gsd-path-build/scripts/state_promote.py +743 -0
  153. package/skills/gsd-path-build/scripts/token_budget.py +146 -0
  154. package/skills/gsd-path-build/scripts/workflow_run.py +117 -0
  155. package/skills/gsd-path-build/templates/skeptic.md +36 -0
  156. package/skills/gsd-path-build/templates/task.md +74 -0
  157. package/skills/gsd-path-build/templates/wave-panel.md +25 -0
  158. package/skills/gsd-path-build/templates/wave-review.md +64 -0
  159. package/skills/gsd-path-decide/SKILL.md +115 -0
  160. package/skills/gsd-path-decide/agents/openai.yaml +6 -0
  161. package/skills/gsd-path-decide/references/decider.md +36 -0
  162. package/skills/gsd-path-decide/references/dispatch.md +131 -0
  163. package/skills/gsd-path-decide/scripts/_common.py +180 -0
  164. package/skills/gsd-path-decide/scripts/check_handoffs.py +1855 -0
  165. package/skills/gsd-path-decide/scripts/check_task_briefs.py +376 -0
  166. package/skills/gsd-path-decide/scripts/discussion_records.py +619 -0
  167. package/skills/gsd-path-decide/scripts/isolation.py +3538 -0
  168. package/skills/gsd-path-decide/scripts/pipeline_git.py +940 -0
  169. package/skills/gsd-path-decide/scripts/pipeline_state.py +2291 -0
  170. package/skills/gsd-path-decide/scripts/state_checkpoint.py +1039 -0
  171. package/skills/gsd-path-decide/scripts/state_promote.py +743 -0
  172. package/skills/gsd-path-decide/templates/synthesis.md +36 -0
  173. package/skills/gsd-path-define/SKILL.md +297 -0
  174. package/skills/gsd-path-define/agents/openai.yaml +6 -0
  175. package/skills/gsd-path-define/scripts/_common.py +180 -0
  176. package/skills/gsd-path-define/scripts/check_handoffs.py +1855 -0
  177. package/skills/gsd-path-define/scripts/check_task_briefs.py +376 -0
  178. package/skills/gsd-path-define/scripts/detect_project.py +1517 -0
  179. package/skills/gsd-path-define/scripts/discussion_records.py +619 -0
  180. package/skills/gsd-path-define/scripts/isolation.py +3538 -0
  181. package/skills/gsd-path-define/scripts/pipeline_git.py +940 -0
  182. package/skills/gsd-path-define/scripts/pipeline_state.py +2291 -0
  183. package/skills/gsd-path-define/scripts/state_checkpoint.py +1039 -0
  184. package/skills/gsd-path-define/scripts/state_promote.py +743 -0
  185. package/skills/gsd-path-define/templates/charter.md +50 -0
  186. package/skills/gsd-path-define/templates/intent.md +98 -0
  187. package/skills/gsd-path-define/templates/state.md +27 -0
  188. package/skills/gsd-path-discuss/SKILL.md +162 -0
  189. package/skills/gsd-path-discuss/agents/openai.yaml +6 -0
  190. package/skills/gsd-path-discuss/scripts/_common.py +180 -0
  191. package/skills/gsd-path-discuss/scripts/discussion_records.py +619 -0
  192. package/skills/gsd-path-discuss/templates/answers.md +29 -0
  193. package/skills/gsd-path-discuss/templates/dialogue.md +21 -0
  194. package/skills/gsd-path-docs-audit/SKILL.md +190 -0
  195. package/skills/gsd-path-docs-audit/agents/openai.yaml +6 -0
  196. package/skills/gsd-path-docs-audit/references/dispatch.md +131 -0
  197. package/skills/gsd-path-docs-audit/references/docs-auditor.md +60 -0
  198. package/skills/gsd-path-docs-audit/scripts/_common.py +180 -0
  199. package/skills/gsd-path-docs-audit/scripts/check_docs_audit.py +463 -0
  200. package/skills/gsd-path-docs-audit/scripts/check_handoffs.py +1855 -0
  201. package/skills/gsd-path-docs-audit/scripts/check_task_briefs.py +376 -0
  202. package/skills/gsd-path-docs-audit/scripts/discussion_records.py +619 -0
  203. package/skills/gsd-path-docs-audit/scripts/isolation.py +3538 -0
  204. package/skills/gsd-path-docs-audit/scripts/pipeline_git.py +940 -0
  205. package/skills/gsd-path-docs-audit/scripts/pipeline_state.py +2291 -0
  206. package/skills/gsd-path-docs-audit/scripts/state_checkpoint.py +1039 -0
  207. package/skills/gsd-path-docs-audit/scripts/state_promote.py +743 -0
  208. package/skills/gsd-path-docs-audit/templates/docs-audit.md +69 -0
  209. package/skills/gsd-path-forensics/SKILL.md +39 -0
  210. package/skills/gsd-path-forensics/agents/openai.yaml +6 -0
  211. package/skills/gsd-path-forensics/scripts/_common.py +180 -0
  212. package/skills/gsd-path-forensics/scripts/discussion_records.py +619 -0
  213. package/skills/gsd-path-forensics/scripts/pipeline_diagnose.py +715 -0
  214. package/skills/gsd-path-forensics/scripts/pipeline_undo.py +1039 -0
  215. package/skills/gsd-path-inspect/SKILL.md +167 -0
  216. package/skills/gsd-path-inspect/agents/openai.yaml +6 -0
  217. package/skills/gsd-path-inspect/references/codebase-mapper.md +52 -0
  218. package/skills/gsd-path-inspect/references/dispatch.md +131 -0
  219. package/skills/gsd-path-inspect/references/docs-auditor.md +60 -0
  220. package/skills/gsd-path-inspect/scripts/_common.py +180 -0
  221. package/skills/gsd-path-inspect/scripts/check_docs_audit.py +463 -0
  222. package/skills/gsd-path-inspect/scripts/check_handoffs.py +1855 -0
  223. package/skills/gsd-path-inspect/scripts/check_task_briefs.py +376 -0
  224. package/skills/gsd-path-inspect/scripts/detect_project.py +1517 -0
  225. package/skills/gsd-path-inspect/scripts/discussion_records.py +619 -0
  226. package/skills/gsd-path-inspect/scripts/isolation.py +3538 -0
  227. package/skills/gsd-path-inspect/scripts/pipeline_git.py +940 -0
  228. package/skills/gsd-path-inspect/scripts/pipeline_state.py +2291 -0
  229. package/skills/gsd-path-inspect/scripts/state_checkpoint.py +1039 -0
  230. package/skills/gsd-path-inspect/scripts/state_promote.py +743 -0
  231. package/skills/gsd-path-inspect/templates/codebase.md +46 -0
  232. package/skills/gsd-path-inspect/templates/docs-audit.md +69 -0
  233. package/skills/gsd-path-inspect/templates/state.md +27 -0
  234. package/skills/gsd-path-loop/SKILL.md +98 -0
  235. package/skills/gsd-path-loop/agents/openai.yaml +6 -0
  236. package/skills/gsd-path-loop/examples/ci-repair.LOOP.md +61 -0
  237. package/skills/gsd-path-loop/scripts/_common.py +180 -0
  238. package/skills/gsd-path-loop/scripts/discussion_records.py +619 -0
  239. package/skills/gsd-path-loop/scripts/loop_run.py +837 -0
  240. package/skills/gsd-path-loop/templates/loop.md +59 -0
  241. package/skills/gsd-path-migrate/SKILL.md +134 -0
  242. package/skills/gsd-path-migrate/agents/openai.yaml +6 -0
  243. package/skills/gsd-path-migrate/scripts/core_hook_gate.py +34 -0
  244. package/skills/gsd-path-migrate/scripts/core_hook_settings.py +119 -0
  245. package/skills/gsd-path-migrate/scripts/discussion_records.py +619 -0
  246. package/skills/gsd-path-migrate/scripts/migrate_core.py +169 -0
  247. package/skills/gsd-path-plan/SKILL.md +411 -0
  248. package/skills/gsd-path-plan/agents/openai.yaml +6 -0
  249. package/skills/gsd-path-plan/references/dispatch.md +131 -0
  250. package/skills/gsd-path-plan/references/planner.md +104 -0
  251. package/skills/gsd-path-plan/scripts/_common.py +180 -0
  252. package/skills/gsd-path-plan/scripts/check_handoffs.py +1855 -0
  253. package/skills/gsd-path-plan/scripts/check_task_briefs.py +376 -0
  254. package/skills/gsd-path-plan/scripts/discussion_records.py +619 -0
  255. package/skills/gsd-path-plan/scripts/isolation.py +3538 -0
  256. package/skills/gsd-path-plan/scripts/pipeline_git.py +940 -0
  257. package/skills/gsd-path-plan/scripts/pipeline_state.py +2291 -0
  258. package/skills/gsd-path-plan/scripts/review_panel.py +553 -0
  259. package/skills/gsd-path-plan/scripts/state_checkpoint.py +1039 -0
  260. package/skills/gsd-path-plan/scripts/state_promote.py +743 -0
  261. package/skills/gsd-path-plan/scripts/workflow_run.py +117 -0
  262. package/skills/gsd-path-plan/templates/patch-findings.md +16 -0
  263. package/skills/gsd-path-plan/templates/plan-panel.md +25 -0
  264. package/skills/gsd-path-plan/templates/plan.md +89 -0
  265. package/skills/gsd-path-plan/templates/task.md +74 -0
  266. package/skills/gsd-path-research/SKILL.md +159 -0
  267. package/skills/gsd-path-research/agents/openai.yaml +6 -0
  268. package/skills/gsd-path-research/references/dispatch.md +131 -0
  269. package/skills/gsd-path-research/references/researcher.md +42 -0
  270. package/skills/gsd-path-research/scripts/_common.py +180 -0
  271. package/skills/gsd-path-research/scripts/check_handoffs.py +1855 -0
  272. package/skills/gsd-path-research/scripts/check_task_briefs.py +376 -0
  273. package/skills/gsd-path-research/scripts/discussion_records.py +619 -0
  274. package/skills/gsd-path-research/scripts/isolation.py +3538 -0
  275. package/skills/gsd-path-research/scripts/pipeline_git.py +940 -0
  276. package/skills/gsd-path-research/scripts/pipeline_state.py +2291 -0
  277. package/skills/gsd-path-research/scripts/state_checkpoint.py +1039 -0
  278. package/skills/gsd-path-research/scripts/state_promote.py +743 -0
  279. package/skills/gsd-path-research/templates/evidence.md +24 -0
  280. package/skills/gsd-path-research/templates/research-handoff.md +25 -0
  281. package/skills/gsd-path-roadmap/SKILL.md +215 -0
  282. package/skills/gsd-path-roadmap/agents/openai.yaml +6 -0
  283. package/skills/gsd-path-roadmap/references/dispatch.md +131 -0
  284. package/skills/gsd-path-roadmap/references/roadmapper.md +48 -0
  285. package/skills/gsd-path-roadmap/scripts/_common.py +180 -0
  286. package/skills/gsd-path-roadmap/scripts/archive_milestone.py +2265 -0
  287. package/skills/gsd-path-roadmap/scripts/check_handoffs.py +1855 -0
  288. package/skills/gsd-path-roadmap/scripts/check_task_briefs.py +376 -0
  289. package/skills/gsd-path-roadmap/scripts/detect_project.py +1517 -0
  290. package/skills/gsd-path-roadmap/scripts/discussion_records.py +619 -0
  291. package/skills/gsd-path-roadmap/scripts/discussion_validate.py +1321 -0
  292. package/skills/gsd-path-roadmap/scripts/integration.py +1433 -0
  293. package/skills/gsd-path-roadmap/scripts/isolation.py +3538 -0
  294. package/skills/gsd-path-roadmap/scripts/pipeline_git.py +940 -0
  295. package/skills/gsd-path-roadmap/scripts/pipeline_state.py +2291 -0
  296. package/skills/gsd-path-roadmap/scripts/promote_lookahead.py +415 -0
  297. package/skills/gsd-path-roadmap/scripts/review_panel.py +553 -0
  298. package/skills/gsd-path-roadmap/scripts/state_checkpoint.py +1039 -0
  299. package/skills/gsd-path-roadmap/scripts/state_promote.py +743 -0
  300. package/skills/gsd-path-roadmap/templates/roadmap.md +38 -0
  301. package/skills/gsd-path-ship/SKILL.md +412 -0
  302. package/skills/gsd-path-ship/agents/openai.yaml +6 -0
  303. package/skills/gsd-path-ship/references/dispatch.md +131 -0
  304. package/skills/gsd-path-ship/references/reviewer.md +188 -0
  305. package/skills/gsd-path-ship/scripts/_common.py +180 -0
  306. package/skills/gsd-path-ship/scripts/archive_milestone.py +2265 -0
  307. package/skills/gsd-path-ship/scripts/build_state.py +854 -0
  308. package/skills/gsd-path-ship/scripts/check_handoffs.py +1855 -0
  309. package/skills/gsd-path-ship/scripts/check_task_briefs.py +376 -0
  310. package/skills/gsd-path-ship/scripts/discussion_records.py +619 -0
  311. package/skills/gsd-path-ship/scripts/discussion_validate.py +1321 -0
  312. package/skills/gsd-path-ship/scripts/integration.py +1433 -0
  313. package/skills/gsd-path-ship/scripts/isolation.py +3538 -0
  314. package/skills/gsd-path-ship/scripts/lean_verification.py +256 -0
  315. package/skills/gsd-path-ship/scripts/pipeline_git.py +940 -0
  316. package/skills/gsd-path-ship/scripts/pipeline_state.py +2291 -0
  317. package/skills/gsd-path-ship/scripts/review_panel.py +553 -0
  318. package/skills/gsd-path-ship/scripts/state_checkpoint.py +1039 -0
  319. package/skills/gsd-path-ship/scripts/state_promote.py +743 -0
  320. package/skills/gsd-path-ship/scripts/workflow_run.py +117 -0
  321. package/skills/gsd-path-ship/templates/archive-manifest.md +37 -0
  322. package/skills/gsd-path-ship/templates/final-review.md +27 -0
  323. package/skills/gsd-path-ship/templates/gap-review.md +20 -0
  324. package/skills/gsd-path-ship/templates/patch-findings.md +16 -0
  325. package/skills/gsd-path-ship/templates/wave-review.md +64 -0
  326. package/skills/gsd-path-undo/SKILL.md +45 -0
  327. package/skills/gsd-path-undo/agents/openai.yaml +6 -0
  328. package/skills/gsd-path-undo/scripts/_common.py +180 -0
  329. package/skills/gsd-path-undo/scripts/discussion_records.py +619 -0
  330. package/skills/gsd-path-undo/scripts/pipeline_undo.py +1039 -0
  331. package/skills/path/BUILD.md +700 -0
  332. package/skills/path/DECIDE.md +115 -0
  333. package/skills/path/DEFINE.md +297 -0
  334. package/skills/path/DOCS-AUDIT.md +190 -0
  335. package/skills/path/INSPECT.md +167 -0
  336. package/skills/path/PLAN.md +411 -0
  337. package/skills/path/RESEARCH.md +159 -0
  338. package/skills/path/ROADMAP.md +215 -0
  339. package/skills/path/SHIP.md +412 -0
  340. package/skills/path/SKILL.md +471 -0
  341. package/skills/path/agents/openai.yaml +6 -0
  342. package/skills/path/references/codebase-mapper.md +52 -0
  343. package/skills/path/references/coder.md +90 -0
  344. package/skills/path/references/decider.md +36 -0
  345. package/skills/path/references/dispatch.md +131 -0
  346. package/skills/path/references/docs-auditor.md +60 -0
  347. package/skills/path/references/planner.md +104 -0
  348. package/skills/path/references/researcher.md +42 -0
  349. package/skills/path/references/reviewer.md +188 -0
  350. package/skills/path/references/roadmapper.md +48 -0
  351. package/skills/path/scripts/_common.py +180 -0
  352. package/skills/path/scripts/archive_milestone.py +2265 -0
  353. package/skills/path/scripts/bootstrap_repository.py +710 -0
  354. package/skills/path/scripts/build_state.py +854 -0
  355. package/skills/path/scripts/check_docs_audit.py +463 -0
  356. package/skills/path/scripts/check_handoffs.py +1855 -0
  357. package/skills/path/scripts/check_task_briefs.py +376 -0
  358. package/skills/path/scripts/check_update.py +93 -0
  359. package/skills/path/scripts/detect_project.py +1517 -0
  360. package/skills/path/scripts/discussion_records.py +619 -0
  361. package/skills/path/scripts/discussion_validate.py +1321 -0
  362. package/skills/path/scripts/dispatch_driver.py +1812 -0
  363. package/skills/path/scripts/integration.py +1433 -0
  364. package/skills/path/scripts/isolation.py +3538 -0
  365. package/skills/path/scripts/lean_verification.py +256 -0
  366. package/skills/path/scripts/loop_run.py +837 -0
  367. package/skills/path/scripts/pipeline_git.py +940 -0
  368. package/skills/path/scripts/pipeline_state.py +2291 -0
  369. package/skills/path/scripts/promote_lookahead.py +415 -0
  370. package/skills/path/scripts/review_findings.py +714 -0
  371. package/skills/path/scripts/review_panel.py +553 -0
  372. package/skills/path/scripts/state_checkpoint.py +1039 -0
  373. package/skills/path/scripts/state_promote.py +743 -0
  374. package/skills/path/scripts/token_budget.py +146 -0
  375. package/skills/path/scripts/workflow_run.py +117 -0
  376. package/skills/path/templates/answers.md +29 -0
  377. package/skills/path/templates/archive-manifest.md +37 -0
  378. package/skills/path/templates/charter.md +50 -0
  379. package/skills/path/templates/codebase.md +46 -0
  380. package/skills/path/templates/dialogue.md +21 -0
  381. package/skills/path/templates/docs-audit.md +69 -0
  382. package/skills/path/templates/evidence.md +24 -0
  383. package/skills/path/templates/final-review.md +27 -0
  384. package/skills/path/templates/gap-review.md +20 -0
  385. package/skills/path/templates/intent.md +98 -0
  386. package/skills/path/templates/loop.md +59 -0
  387. package/skills/path/templates/patch-findings.md +16 -0
  388. package/skills/path/templates/plan-panel.md +25 -0
  389. package/skills/path/templates/plan.md +89 -0
  390. package/skills/path/templates/repository.md +16 -0
  391. package/skills/path/templates/research-handoff.md +25 -0
  392. package/skills/path/templates/roadmap.md +38 -0
  393. package/skills/path/templates/skeptic.md +36 -0
  394. package/skills/path/templates/state.md +27 -0
  395. package/skills/path/templates/synthesis.md +36 -0
  396. package/skills/path/templates/task.md +74 -0
  397. package/skills/path/templates/wave-panel.md +25 -0
  398. package/skills/path/templates/wave-review.md +64 -0
@@ -0,0 +1,714 @@
1
+ #!/usr/bin/env python3
2
+ # gsd-path project runtime
3
+ """Compute the wave review finding sets for build step 7 without model reasoning.
4
+
5
+ The orchestrator names the wave and cycle. This helper reads PLAN.md Config
6
+ (`max_review_cycles`, `finding_skeptics`, `wave_budget`), the cycle's lens or
7
+ canonical review files, and every skeptic file for this wave. It groups
8
+ blocking findings by canonical criterion locator (`t001_ac2`, `sc3`,
9
+ `t001_files`, `t001_interface_contract`), collapses true duplicate
10
+ observations, selects the groups that need a skeptic, carries refutations
11
+ forward across cycles, batches fix-task groups by disjoint file scope, keeps
12
+ structural blockers out of skeptic accounting, and reports whether the cycle
13
+ cap is reached. It never decides the wave verdict.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import json
20
+ import re
21
+ import sys
22
+ from pathlib import Path
23
+ from typing import Dict, List, Optional, Sequence
24
+
25
+ DEFAULT_PROJECT_DIR = ".project"
26
+ DEFAULT_MAX_REVIEW_CYCLES = 3
27
+ CONFIG_KEYS = ("max_review_cycles", "wave_budget", "review_panel", "finding_skeptics")
28
+ CONFIG_LINE = re.compile(r"^-\s*(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*?)\s*$")
29
+ COMMENT_PATTERN = re.compile(r"<!--.*?-->", re.DOTALL)
30
+ PLAN_WAVE_HEADING = re.compile(r"(?m)^## Wave (?P<wave>\d+) — (?P<title>.+)$")
31
+ TASK_FILE_NAME = re.compile(r"^(?P<id>T\d{3})-[a-z0-9][a-z0-9-]*\.md$")
32
+ WAVE_TASK_HEADING = re.compile(
33
+ r"^## (?P<task>T\d{3}) — (?P<title>.+): (?P<verdict>pass|fail)$"
34
+ )
35
+ WAVE_SC_HEADING = re.compile(r"^### (?P<sc>SC[1-9]\d*) — (?P<rest>.+)$")
36
+ NUMBERED_ITEM = re.compile(r"^(\d+)\.\s+(\S.*)$")
37
+ SKEPTIC_FILE_NAME = re.compile(
38
+ r"^wave-(?P<wave>[1-9]\d*)\.cycle(?P<cycle>[1-9]\d*)\.skeptic-(?P<locator>[a-z0-9_]+)\.md$"
39
+ )
40
+ SKEPTIC_OBSERVATION = re.compile(r"^### Observation (?P<n>\d+) — (?P<lens>contract|adversarial|both)$")
41
+ SKEPTIC_OBSERVATION_VERDICT = re.compile(r"^### Observation (?P<n>\d+): (?P<verdict>refuted|stands)$")
42
+ FAIL_MARKER = "- ❌ "
43
+
44
+
45
+ class ReviewFindingsError(Exception):
46
+ """Raised when an input cannot be read as the templates define it."""
47
+
48
+
49
+ def _normalize(text: str) -> str:
50
+ return " ".join(text.split())
51
+
52
+
53
+ def _strip_comments(text: str) -> str:
54
+ return COMMENT_PATTERN.sub("", text)
55
+
56
+
57
+ def _section(text: str, heading: str) -> Optional[str]:
58
+ match = re.search(rf"(?m)^## {re.escape(heading)}\s*$", text)
59
+ if not match:
60
+ return None
61
+ rest = text[match.end() :]
62
+ following = re.search(r"(?m)^## ", rest)
63
+ return rest[: following.start()] if following else rest
64
+
65
+
66
+ def _read(path: Path, label: str) -> str:
67
+ try:
68
+ return path.read_text(encoding="utf-8")
69
+ except OSError as error:
70
+ raise ReviewFindingsError(f"{label} is not readable: {path} ({error.strerror})")
71
+
72
+
73
+ # --- PLAN.md ---------------------------------------------------------------
74
+
75
+
76
+ def parse_config(plan_text: str) -> Dict[str, object]:
77
+ """Read and validate the PLAN.md Config keys this helper owns."""
78
+
79
+ section = _section(_strip_comments(plan_text), "Config")
80
+ if section is None:
81
+ raise ReviewFindingsError("PLAN.md has no ## Config section")
82
+ seen: Dict[str, str] = {}
83
+ for line in section.splitlines():
84
+ match = CONFIG_LINE.match(line.strip())
85
+ if not match:
86
+ continue
87
+ key, value = match.group("key"), match.group("value")
88
+ if key not in CONFIG_KEYS:
89
+ raise ReviewFindingsError(
90
+ f"PLAN.md Config key `{key}` is unknown; expected one of {', '.join(CONFIG_KEYS)}"
91
+ )
92
+ if key in seen:
93
+ raise ReviewFindingsError(f"PLAN.md Config repeats `{key}`")
94
+ seen[key] = value
95
+ cycles_raw = seen.get("max_review_cycles", str(DEFAULT_MAX_REVIEW_CYCLES))
96
+ if not cycles_raw.isdigit() or int(cycles_raw) < 1:
97
+ raise ReviewFindingsError(
98
+ f"PLAN.md Config `max_review_cycles` must be a positive integer, got `{cycles_raw}`"
99
+ )
100
+ skeptics = seen.get("finding_skeptics", "off")
101
+ if skeptics not in {"off", "on"}:
102
+ raise ReviewFindingsError(
103
+ f"PLAN.md Config `finding_skeptics` must be `off` or `on`, got `{skeptics}`"
104
+ )
105
+ budget = seen.get("wave_budget", "none")
106
+ if not budget:
107
+ raise ReviewFindingsError("PLAN.md Config `wave_budget` must be `none` or a resource cap")
108
+ return {
109
+ "max_review_cycles": int(cycles_raw),
110
+ "finding_skeptics": skeptics,
111
+ "wave_budget": budget,
112
+ }
113
+
114
+
115
+ def wave_depth(plan_text: str, wave: int) -> str:
116
+ headings = list(PLAN_WAVE_HEADING.finditer(plan_text))
117
+ for index, heading in enumerate(headings):
118
+ if int(heading.group("wave")) != wave:
119
+ continue
120
+ end = headings[index + 1].start() if index + 1 < len(headings) else len(plan_text)
121
+ depth = re.search(r"(?m)^Review depth:\s*(\S+)", plan_text[heading.end() : end])
122
+ if not depth or depth.group(1) not in {"full", "deep", "verify-only"}:
123
+ raise ReviewFindingsError(f"PLAN.md Wave {wave} has an invalid Review depth")
124
+ return depth.group(1)
125
+ raise ReviewFindingsError(f"PLAN.md has no Wave {wave}")
126
+
127
+
128
+ # --- task files ------------------------------------------------------------
129
+
130
+
131
+ def _frontmatter_list(frontmatter: str, key: str) -> List[str]:
132
+ inline = re.search(rf"(?m)^{key}:\s*\[(?P<body>.*)\]\s*(?:#.*)?$", frontmatter)
133
+ if inline:
134
+ return [part.strip().strip("\"'") for part in inline.group("body").split(",") if part.strip()]
135
+ block = re.search(rf"(?m)^{key}:\s*(?:#.*)?$\n(?P<items>(?:[ \t]+-\s+.*\n?)*)", frontmatter)
136
+ if not block:
137
+ return []
138
+ return [
139
+ _normalize(re.sub(r"\s+#.*$", "", item.strip()[1:])).strip("\"'")
140
+ for item in block.group("items").splitlines()
141
+ if item.strip().startswith("-")
142
+ ]
143
+
144
+
145
+ def _numbered_items(section: Optional[str]) -> List[str]:
146
+ items: List[str] = []
147
+ if section is None:
148
+ return items
149
+ for line in _strip_comments(section).splitlines():
150
+ match = NUMBERED_ITEM.match(line.strip())
151
+ if match:
152
+ items.append(_normalize(match.group(2)))
153
+ elif items and line.startswith(" ") and line.strip():
154
+ items[-1] = _normalize(items[-1] + " " + line.strip())
155
+ return items
156
+
157
+
158
+ def _bullets(section: Optional[str]) -> List[str]:
159
+ if section is None:
160
+ return []
161
+ return [
162
+ _normalize(line.strip()[1:])
163
+ for line in _strip_comments(section).splitlines()
164
+ if line.strip().startswith("- ")
165
+ ]
166
+
167
+
168
+ def load_wave_tasks(project: Path, wave: int) -> Dict[str, dict]:
169
+ tasks_dir = project / "tasks"
170
+ if not tasks_dir.is_dir():
171
+ raise ReviewFindingsError(f"tasks directory missing: {tasks_dir}")
172
+ tasks: Dict[str, dict] = {}
173
+ for path in sorted(tasks_dir.iterdir()):
174
+ named = TASK_FILE_NAME.fullmatch(path.name)
175
+ if not named:
176
+ continue
177
+ text = _read(path, f"task {path.name}")
178
+ parts = text.split("---", 2)
179
+ if len(parts) < 3:
180
+ raise ReviewFindingsError(f"task {path.name} has no frontmatter")
181
+ frontmatter = parts[1]
182
+ task_wave = re.search(r"(?m)^wave:\s*(\d+)", frontmatter)
183
+ if not task_wave or int(task_wave.group(1)) != wave:
184
+ continue
185
+ body = parts[2]
186
+ tasks[named.group("id")] = {
187
+ "id": named.group("id"),
188
+ "path": str(path),
189
+ "files": _frontmatter_list(frontmatter, "files"),
190
+ "acceptance": _numbered_items(_section(body, "Acceptance criteria")),
191
+ "interface": [
192
+ item for item in _bullets(_section(body, "Interface contract")) if item != "None"
193
+ ],
194
+ "owned": [
195
+ item for item in _bullets(_section(body, "Intent coverage")) if item != "None"
196
+ ],
197
+ }
198
+ if not tasks:
199
+ raise ReviewFindingsError(f"PLAN.md Wave {wave} has no task files")
200
+ return tasks
201
+
202
+
203
+ # --- lens files ------------------------------------------------------------
204
+
205
+
206
+ def review_paths(project: Path, wave: int, cycle: int, depth: str) -> Dict[str, Path]:
207
+ review = project / "review"
208
+ if depth == "deep":
209
+ return {
210
+ lens: review / f"wave-{wave}.cycle{cycle}.{lens}.md"
211
+ for lens in ("contract", "adversarial")
212
+ }
213
+ return {"canonical": review / f"wave-{wave}.cycle{cycle}.md"}
214
+
215
+
216
+ def _fail_items(lines: Sequence[str]) -> List[str]:
217
+ """Collect `- ❌` items with their indented continuation lines."""
218
+
219
+ items: List[str] = []
220
+ open_item = False
221
+ for line in lines:
222
+ stripped = line.strip()
223
+ if stripped.startswith(FAIL_MARKER):
224
+ items.append(stripped[len(FAIL_MARKER) :])
225
+ open_item = True
226
+ elif open_item and line.startswith(" ") and stripped and not stripped.startswith("- "):
227
+ items[-1] += " " + stripped
228
+ elif stripped.startswith("- ") or stripped.startswith("#"):
229
+ open_item = False
230
+ return [_normalize(item) for item in items]
231
+
232
+
233
+ def _criterion_of(observation: str) -> str:
234
+ return _normalize(observation.split(" — ", 1)[0])
235
+
236
+
237
+ def parse_lens(text: str, lens: str, tasks: Dict[str, dict]) -> dict:
238
+ """Return the lens verdict and every blocking observation with its locator."""
239
+
240
+ verdict = re.search(r"(?m)^Wave verdict:\s*(\S+)", text)
241
+ if not verdict or verdict.group(1) not in {"pass", "blocked"}:
242
+ raise ReviewFindingsError(f"{lens} lens has no valid Wave verdict")
243
+ lines = _strip_comments(text).splitlines()
244
+ findings: List[dict] = []
245
+ problems: List[str] = []
246
+ task_headings = [
247
+ (index, match) for index, line in enumerate(lines)
248
+ if (match := WAVE_TASK_HEADING.fullmatch(line.strip()))
249
+ ]
250
+ for start, heading in task_headings:
251
+ task_id = heading.group("task")
252
+ end = next(
253
+ (index for index in range(start + 1, len(lines)) if lines[index].startswith("## ")),
254
+ len(lines),
255
+ )
256
+ if task_id not in tasks:
257
+ problems.append(f"{lens} lens reviews {task_id}, which is not a wave task")
258
+ continue
259
+ task = tasks[task_id]
260
+ block = lines[start + 1 : end]
261
+ zones = {"criteria": [], "warnings": [], "violations": []}
262
+ zone = "criteria"
263
+ for line in block:
264
+ label = line.strip().casefold()
265
+ if label.startswith("warnings (non-blocking)"):
266
+ zone = "warnings"
267
+ continue
268
+ if label.startswith("contract violations (blocking)"):
269
+ zone = "violations"
270
+ continue
271
+ zones[zone].append(line)
272
+ for observation in _fail_items(zones["criteria"]):
273
+ criterion = _criterion_of(observation)
274
+ locator = None
275
+ if criterion in task["acceptance"]:
276
+ locator = f"{task_id.lower()}_ac{task['acceptance'].index(criterion) + 1}"
277
+ elif criterion in task["interface"]:
278
+ locator = f"{task_id.lower()}_interface_contract"
279
+ if locator is None:
280
+ problems.append(
281
+ f"{lens} lens {task_id} finding names no task criterion: {criterion}"
282
+ )
283
+ continue
284
+ findings.append(
285
+ {"locator": locator, "criterion": criterion, "task": task_id,
286
+ "lens": lens, "observation": observation}
287
+ )
288
+ for observation in _fail_items(zones["violations"]):
289
+ findings.append(
290
+ {"locator": f"{task_id.lower()}_files", "criterion": _criterion_of(observation),
291
+ "task": task_id, "lens": lens, "observation": observation}
292
+ )
293
+ coverage = _section("\n".join(lines), "Intent coverage")
294
+ if coverage is not None:
295
+ coverage_lines = coverage.splitlines()
296
+ sc_headings = [
297
+ (index, match) for index, line in enumerate(coverage_lines)
298
+ if (match := WAVE_SC_HEADING.fullmatch(line.strip()))
299
+ ]
300
+ for position, (start, heading) in enumerate(sc_headings):
301
+ text_part, _sep, sc_verdict = heading.group("rest").rpartition(":")
302
+ if sc_verdict.strip() != "fail":
303
+ continue
304
+ sc_id = heading.group("sc")
305
+ end = sc_headings[position + 1][0] if position + 1 < len(sc_headings) else len(coverage_lines)
306
+ observations = _fail_items(coverage_lines[start + 1 : end])
307
+ if not observations:
308
+ problems.append(f"{lens} lens {sc_id} fails without a ❌ observation")
309
+ continue
310
+ owners = [task_id for task_id, task in tasks.items() if sc_id in task["owned"]]
311
+ for observation in observations:
312
+ findings.append(
313
+ {"locator": sc_id.lower(), "criterion": _normalize(text_part),
314
+ "task": ", ".join(owners), "tasks": owners, "lens": lens,
315
+ "observation": observation}
316
+ )
317
+ return {"verdict": verdict.group(1), "findings": findings, "problems": problems}
318
+
319
+
320
+ # --- skeptic files ---------------------------------------------------------
321
+
322
+
323
+ def parse_skeptic(path: Path, wave: int, expected_observations: Optional[Sequence[str]] = None) -> dict:
324
+ """Validate one skeptic file against the template; return its verdict and observations."""
325
+
326
+ named = SKEPTIC_FILE_NAME.fullmatch(path.name)
327
+ if not named or int(named.group("wave")) != wave:
328
+ raise ReviewFindingsError(f"skeptic file name does not belong to wave {wave}: {path.name}")
329
+ text = _strip_comments(_read(path, f"skeptic {path.name}"))
330
+ locator = re.search(r"(?m)^- Criterion locator:\s*(\S+)\s*$", text)
331
+ if not locator or locator.group(1) != named.group("locator"):
332
+ raise ReviewFindingsError(f"{path.name} Criterion locator does not match its filename")
333
+ verdict_section = _section(text, "Verdict")
334
+ verdict = _normalize(verdict_section or "")
335
+ if verdict not in {"refuted", "stands"}:
336
+ raise ReviewFindingsError(f"{path.name} Verdict must be `refuted` or `stands`")
337
+ observations: List[str] = []
338
+ lines = (_section(text, "Observations") or "").splitlines()
339
+ starts = [(index, int(match.group("n"))) for index, line in enumerate(lines)
340
+ if (match := SKEPTIC_OBSERVATION.match(line.strip()))]
341
+ observation_numbers = {number for _, number in starts}
342
+ if len(observation_numbers) != len(starts):
343
+ raise ReviewFindingsError(f"{path.name} has duplicate observation numbers")
344
+ for position, (start, _) in enumerate(starts):
345
+ end = starts[position + 1][0] if position + 1 < len(starts) else len(lines)
346
+ observations.append(_normalize(" ".join(lines[start + 1 : end])))
347
+ if not observations or any(not item for item in observations):
348
+ raise ReviewFindingsError(f"{path.name} records no observation text")
349
+ if expected_observations is not None and not {
350
+ _normalize(item) for item in expected_observations
351
+ } <= set(observations):
352
+ raise ReviewFindingsError(f"{path.name} omits dispatched observations")
353
+ observation_verdicts = [
354
+ (int(match.group("n")), match.group("verdict"))
355
+ for line in (_section(text, "Observation verdicts") or "").splitlines()
356
+ if (match := SKEPTIC_OBSERVATION_VERDICT.match(line.strip()))
357
+ ]
358
+ verdict_numbers = {number for number, _ in observation_verdicts}
359
+ if len(verdict_numbers) != len(observation_verdicts) or verdict_numbers != observation_numbers:
360
+ raise ReviewFindingsError(
361
+ f"{path.name} must have exactly one matching verdict per observation number"
362
+ )
363
+ if verdict == "refuted" and any(item != "refuted" for _, item in observation_verdicts):
364
+ raise ReviewFindingsError(
365
+ f"{path.name} records Verdict refuted while an observation stands"
366
+ )
367
+ return {
368
+ "path": str(path),
369
+ "cycle": int(named.group("cycle")),
370
+ "locator": named.group("locator"),
371
+ "verdict": verdict,
372
+ "observations": observations,
373
+ }
374
+
375
+
376
+ def load_skeptics(project: Path, wave: int, cycle: int) -> tuple:
377
+ """Return (skeptics by cycle then locator, structural blockers for invalid files)."""
378
+
379
+ review = project / "review"
380
+ by_cycle: Dict[int, Dict[str, dict]] = {}
381
+ blockers: List[dict] = []
382
+ if not review.is_dir():
383
+ return by_cycle, blockers
384
+ for path in sorted(review.iterdir()):
385
+ named = SKEPTIC_FILE_NAME.fullmatch(path.name)
386
+ if not named or int(named.group("wave")) != wave or int(named.group("cycle")) > cycle:
387
+ continue
388
+ try:
389
+ parsed = parse_skeptic(path, wave)
390
+ except ReviewFindingsError as error:
391
+ blockers.append({"kind": "invalid-skeptic", "path": str(path), "detail": str(error)})
392
+ continue
393
+ by_cycle.setdefault(parsed["cycle"], {})[parsed["locator"]] = parsed
394
+ return by_cycle, blockers
395
+
396
+
397
+ # --- grouping and dispositions ---------------------------------------------
398
+
399
+
400
+ def group_findings(findings: Sequence[dict]) -> List[dict]:
401
+ """Group by locator; collapse only observations whose text is identical."""
402
+
403
+ groups: Dict[str, dict] = {}
404
+ for finding in findings:
405
+ group = groups.setdefault(
406
+ finding["locator"],
407
+ {"locator": finding["locator"], "criterion": finding["criterion"],
408
+ "tasks": [], "lenses": [], "observations": []},
409
+ )
410
+ for task_id in finding.get("tasks", [finding["task"]]):
411
+ if task_id and task_id not in group["tasks"]:
412
+ group["tasks"].append(task_id)
413
+ if finding["lens"] not in group["lenses"]:
414
+ group["lenses"].append(finding["lens"])
415
+ if all(item["text"] != finding["observation"] for item in group["observations"]):
416
+ group["observations"].append({"lens": finding["lens"], "text": finding["observation"]})
417
+ return [groups[key] for key in sorted(groups)]
418
+
419
+
420
+ def _earlier_lens_locators(project: Path, wave: int, cycle: int, depth: str, tasks: Dict[str, dict]) -> Dict[str, int]:
421
+ first_seen: Dict[str, int] = {}
422
+ for earlier in range(1, cycle):
423
+ for lens, path in review_paths(project, wave, earlier, depth).items():
424
+ if not path.is_file():
425
+ continue
426
+ try:
427
+ parsed = parse_lens(path.read_text(encoding="utf-8"), lens, tasks)
428
+ except (ReviewFindingsError, OSError):
429
+ continue
430
+ for finding in parsed["findings"]:
431
+ first_seen.setdefault(finding["locator"], earlier)
432
+ return first_seen
433
+
434
+
435
+ def _fix_batches(groups: Sequence[dict], tasks: Dict[str, dict]) -> List[dict]:
436
+ """One batch per disjoint file scope: groups whose task files overlap share a batch."""
437
+
438
+ batches: List[dict] = []
439
+ for group in groups:
440
+ files = sorted({path for task_id in group["tasks"] for path in tasks.get(task_id, {}).get("files", [])})
441
+ merged = {"locators": [group["locator"]], "files": files}
442
+ keep: List[dict] = []
443
+ for batch in batches:
444
+ if set(batch["files"]) & set(files):
445
+ merged["locators"] = batch["locators"] + merged["locators"]
446
+ merged["files"] = sorted(set(batch["files"]) | set(merged["files"]))
447
+ else:
448
+ keep.append(batch)
449
+ keep.append(merged)
450
+ batches = keep
451
+ return batches
452
+
453
+
454
+ def compute(
455
+ repo: Path,
456
+ project_dir: str,
457
+ wave: int,
458
+ cycle: int,
459
+ helper_failures: Sequence[str] = (),
460
+ ) -> dict:
461
+ project = repo / project_dir
462
+ plan_path = project / "plan" / "PLAN.md"
463
+ plan_text = _read(plan_path, "PLAN.md")
464
+ config = parse_config(plan_text)
465
+ depth = wave_depth(plan_text, wave)
466
+ tasks = load_wave_tasks(project, wave)
467
+ structural: List[dict] = [
468
+ {"kind": "helper-failure", "path": None, "detail": item} for item in helper_failures
469
+ ]
470
+ findings: List[dict] = []
471
+ verdicts: Dict[str, Optional[str]] = {}
472
+ for lens, path in review_paths(project, wave, cycle, depth).items():
473
+ if not path.is_file():
474
+ structural.append({"kind": "missing-artifact", "path": str(path), "detail": f"{lens} review file is missing"})
475
+ verdicts[lens] = None
476
+ continue
477
+ try:
478
+ parsed = parse_lens(_read(path, f"{lens} review"), lens, tasks)
479
+ except ReviewFindingsError as error:
480
+ structural.append({"kind": "invalid-artifact", "path": str(path), "detail": str(error)})
481
+ verdicts[lens] = None
482
+ continue
483
+ verdicts[lens] = parsed["verdict"]
484
+ findings.extend(parsed["findings"])
485
+ for problem in parsed["problems"]:
486
+ structural.append({"kind": "invalid-artifact", "path": str(path), "detail": problem})
487
+ if parsed["verdict"] == "blocked" and not parsed["findings"] and not parsed["problems"]:
488
+ structural.append({"kind": "invalid-artifact", "path": str(path), "detail": f"{lens} lens is blocked without a ❌ finding"})
489
+
490
+ skeptics_by_cycle, skeptic_blockers = load_skeptics(project, wave, cycle)
491
+ structural.extend(skeptic_blockers)
492
+ panel = None
493
+ panel_path = project / "review" / f"wave-{wave}.cycle{cycle}.panel.md"
494
+ if panel_path.is_file():
495
+ actionable = re.search(r"(?m)^Actionable:\s*(\d+)\s*$", _read(panel_path, "panel"))
496
+ if not actionable:
497
+ structural.append({"kind": "invalid-artifact", "path": str(panel_path), "detail": "panel file has no Actionable count"})
498
+ else:
499
+ panel = {"path": str(panel_path), "actionable": int(actionable.group(1))}
500
+
501
+ skeptics_active = config["finding_skeptics"] == "on" and depth == "deep"
502
+ first_seen = _earlier_lens_locators(project, wave, cycle, depth, tasks)
503
+ earlier_refuted: Dict[str, dict] = {}
504
+ for earlier in sorted(skeptics_by_cycle):
505
+ if earlier >= cycle:
506
+ continue
507
+ for locator, skeptic in skeptics_by_cycle[earlier].items():
508
+ if skeptic["verdict"] == "refuted":
509
+ earlier_refuted.setdefault(locator, skeptic)
510
+ current_skeptics = skeptics_by_cycle.get(cycle, {})
511
+
512
+ groups = group_findings(findings)
513
+ for group in groups:
514
+ locator = group["locator"]
515
+ group["first_seen_cycle"] = first_seen.get(locator, cycle)
516
+ group["repeat"] = locator in first_seen
517
+ group["skeptic"] = None
518
+ earlier = earlier_refuted.get(locator)
519
+ current = current_skeptics.get(locator)
520
+ if current is not None:
521
+ try:
522
+ parse_skeptic(Path(current["path"]), wave,
523
+ [item["text"] for item in group["observations"]])
524
+ except ReviewFindingsError as error:
525
+ structural.append({"kind": "invalid-skeptic", "path": current["path"], "detail": str(error)})
526
+ current = None
527
+ if not skeptics_active:
528
+ group["disposition"] = "fix"
529
+ elif earlier is not None:
530
+ texts = {item["text"] for item in group["observations"]}
531
+ group["skeptic"] = {"cycle": earlier["cycle"], "verdict": "refuted", "path": earlier["path"]}
532
+ if texts <= set(earlier["observations"]):
533
+ group["disposition"] = "refuted-earlier"
534
+ else:
535
+ group["disposition"] = "fix"
536
+ group["new_evidence"] = sorted(texts - set(earlier["observations"]))
537
+ elif current is not None:
538
+ group["skeptic"] = {"cycle": cycle, "verdict": current["verdict"], "path": current["path"]}
539
+ group["disposition"] = "refuted" if current["verdict"] == "refuted" else "fix"
540
+ else:
541
+ group["disposition"] = "needs-skeptic"
542
+
543
+ refuted = [g["locator"] for g in groups if g["disposition"] in {"refuted", "refuted-earlier"}]
544
+ fix_groups = [g for g in groups if g["disposition"] == "fix"]
545
+ skeptic_groups = [g["locator"] for g in groups if g["disposition"] == "needs-skeptic"]
546
+ lens_blocked = any(value != "pass" for value in verdicts.values())
547
+ cap_reached = cycle >= config["max_review_cycles"]
548
+ return {
549
+ "status": "ok",
550
+ "wave": wave,
551
+ "cycle": cycle,
552
+ "depth": depth,
553
+ "config": config,
554
+ "lens_verdicts": verdicts,
555
+ "blocked": lens_blocked or bool(structural),
556
+ "cap_reached": cap_reached,
557
+ "skeptics_active": skeptics_active,
558
+ "groups": groups,
559
+ "skeptic_groups": skeptic_groups,
560
+ "refuted_groups": refuted,
561
+ "carried_refutations": [
562
+ {"locator": locator, "cycle": skeptic["cycle"], "path": skeptic["path"]}
563
+ for locator, skeptic in sorted(earlier_refuted.items())
564
+ ],
565
+ "fix_groups": [g["locator"] for g in fix_groups],
566
+ "fix_batches": _fix_batches(fix_groups, tasks),
567
+ "structural_blockers": structural,
568
+ "all_refuted": bool(refuted) and not fix_groups and not skeptic_groups and not structural and not cap_reached,
569
+ "panel": panel,
570
+ }
571
+
572
+
573
+ def emit(payload: dict) -> int:
574
+ json.dump(payload, sys.stdout, indent=2, sort_keys=True)
575
+ sys.stdout.write("\n")
576
+ return 0
577
+
578
+
579
+ def repair_evidence(repo: Path, project_dir: str, wave: int, cycle: int, task_id: str) -> dict:
580
+ """Bind a carried finding batch to a proven, independently verified repair."""
581
+ try:
582
+ from . import _common, build_state, isolation
583
+ except ImportError: # installed standalone helper
584
+ import _common
585
+ import build_state
586
+ import isolation
587
+
588
+ try:
589
+ repo = isolation.require_directory(repo, "repository")
590
+ project = repo / isolation.relative_posix(project_dir)
591
+ if not re.fullmatch(r"T\d{3}", task_id or ""):
592
+ raise ReviewFindingsError("--task must name a repair task id")
593
+ paths = list((project / "tasks").glob(f"{task_id}-*.md"))
594
+ if len(paths) != 1:
595
+ raise ReviewFindingsError("repair task must resolve to exactly one task file")
596
+ repair_path = paths[0]
597
+ text = _read(repair_path, "repair task")
598
+ fields, error = isolation.task_frontmatter(text)
599
+ if error or fields is None or fields.get("status") != "done":
600
+ raise ReviewFindingsError(error or "repair task must be done")
601
+ head = isolation.current_sha(repo)
602
+ proof = isolation.verify_landed_task_files(
603
+ repo, [repair_path], f"{project_dir}/tasks", head
604
+ )["tasks"][0]
605
+ if proof["verdict"] != "recovered":
606
+ raise ReviewFindingsError("repair requires a proven landing")
607
+ base, commit = proof["base"], proof["commit"]
608
+ findings = _section(text, "Review findings") or ""
609
+ headings = list(re.finditer(r"(?m)^### ([a-z0-9_]+)\s*$", findings))
610
+ locators = [heading.group(1) for heading in headings]
611
+ if not locators or len(set(locators)) != len(locators):
612
+ raise ReviewFindingsError("repair needs unique carried finding locators")
613
+ source = compute(repo, project_dir, wave, cycle)
614
+ if source["structural_blockers"]:
615
+ raise ReviewFindingsError("source findings have structural blockers")
616
+ batches = [batch for batch in source["fix_batches"]
617
+ if set(batch["locators"]) == set(locators)]
618
+ if len(batches) != 1:
619
+ raise ReviewFindingsError("repair must carry one eligible source finding batch")
620
+ groups = [group for group in source["groups"] if group["locator"] in locators]
621
+ for index, heading in enumerate(headings):
622
+ end = headings[index + 1].start() if index + 1 < len(headings) else len(findings)
623
+ block = _normalize(findings[heading.end():end])
624
+ group = next(group for group in groups if group["locator"] == heading.group(1))
625
+ if ("Criterion: " + group["criterion"] not in block
626
+ or any(item["text"] not in block for item in group["observations"])):
627
+ raise ReviewFindingsError("repair does not carry the exact criterion and observations")
628
+ frontmatter = text.split("---", 2)[1]
629
+ files = _frontmatter_list(frontmatter, "files")
630
+ originals = sorted({task for group in groups for task in group["tasks"]})
631
+ if (task_id in originals or not set(originals) <= set(_frontmatter_list(frontmatter, "deps"))
632
+ or set(files) != set(batches[0]["files"])):
633
+ raise ReviewFindingsError("repair dependencies/files do not match its source batch")
634
+ sources = load_wave_tasks(project, wave)
635
+ original_paths = [Path(sources[task]["path"]) for task in originals]
636
+ original_proofs = isolation.verify_landed_task_files(
637
+ repo, original_paths, f"{project_dir}/tasks", head
638
+ )["tasks"]
639
+ for original in original_proofs:
640
+ original_id = original["task_id"]
641
+ if (original["verdict"] != "recovered"
642
+ or isolation.run_git(repo, "merge-base", "--is-ancestor", original["commit"], base).returncode
643
+ or isolation.run_git(repo, "diff", "--quiet", original["commit"], base,
644
+ "--", *sources[original_id]["files"]).returncode):
645
+ raise ReviewFindingsError("repair base contains unlinked changes to original product")
646
+ source_paths = list(review_paths(project, wave, cycle, source["depth"]).values())
647
+ source_paths.extend(Path(group["skeptic"]["path"]) for group in groups if group["skeptic"])
648
+ for path in set(source_paths):
649
+ relative = path.relative_to(repo).as_posix()
650
+ at_base = isolation.run_git(repo, "show", f"{base}:{relative}")
651
+ if at_base.returncode or at_base.stdout != _read(path, "source review"):
652
+ raise ReviewFindingsError("source review must be unchanged and precede the repair")
653
+ verification = build_state.verify_lookup(str(repo), _common.task_verify_command(text), commit)
654
+ if not verification["reuse"]:
655
+ raise ReviewFindingsError("repair has no reusable passing Verify at its landing")
656
+ if isolation.run_git(repo, "diff", "--quiet", commit, "--", *files).returncode:
657
+ raise ReviewFindingsError("product changed after the repair landing")
658
+ return {
659
+ "status": "ok", "command": "repair-evidence", "head": head,
660
+ "source": {"wave": wave, "cycle": cycle}, "groups": groups,
661
+ "originals": [{"task": item["task_id"], "base": item["base"],
662
+ "commit": item["commit"], "files": sources[item["task_id"]]["files"]}
663
+ for item in original_proofs],
664
+ "repair": {"task": task_id, "task_file": repair_path.relative_to(repo).as_posix(),
665
+ "base": base, "commit": commit, "files": files},
666
+ "verification": verification,
667
+ "verdict": "evidence-ready; reviewer judges the original unchanged criteria",
668
+ }
669
+ except (isolation.IsolationError, build_state.BuildStateError, ValueError) as error:
670
+ raise ReviewFindingsError(str(error)) from error
671
+
672
+
673
+ def parser() -> argparse.ArgumentParser:
674
+ result = argparse.ArgumentParser(description=__doc__)
675
+ result.add_argument("command", choices=("collect", "repair-evidence"))
676
+ result.add_argument("--repo", type=Path, required=True, help="absolute repository root")
677
+ result.add_argument("--project-dir", default=DEFAULT_PROJECT_DIR)
678
+ result.add_argument("--wave", type=int, required=True)
679
+ result.add_argument("--cycle", type=int, required=True)
680
+ result.add_argument("--task", help="landed repair task for repair-evidence")
681
+ result.add_argument(
682
+ "--helper-failure",
683
+ action="append",
684
+ default=[],
685
+ help="a bundled helper non-zero exit for this cycle; repeatable; recorded as a structural blocker",
686
+ )
687
+ return result
688
+
689
+
690
+ def main(argv: Optional[Sequence[str]] = None) -> int:
691
+ arguments = parser().parse_args(argv)
692
+ try:
693
+ if arguments.wave < 1 or arguments.cycle < 1:
694
+ raise ReviewFindingsError("--wave and --cycle must be positive integers")
695
+ if arguments.command == "repair-evidence":
696
+ return emit(repair_evidence(arguments.repo, arguments.project_dir,
697
+ arguments.wave, arguments.cycle, arguments.task))
698
+ return emit(
699
+ compute(
700
+ arguments.repo,
701
+ arguments.project_dir,
702
+ arguments.wave,
703
+ arguments.cycle,
704
+ arguments.helper_failure,
705
+ )
706
+ )
707
+ except ReviewFindingsError as error:
708
+ json.dump({"status": "error", "error": str(error)}, sys.stdout, indent=2)
709
+ sys.stdout.write("\n")
710
+ return 2
711
+
712
+
713
+ if __name__ == "__main__":
714
+ sys.exit(main())