@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,837 @@
1
+ #!/usr/bin/env python3
2
+ """Deterministic gates for a GSD Path LOOP.md loop spec.
3
+
4
+ The loop skill (a model) reads the prose sections of a LOOP.md; this helper
5
+ reads only its fixed machine fields — exact `key: value` lines, one per
6
+ line, no nesting, read only from the header above the first `## `
7
+ section. `check` decides run/skip from status, cooldown, and period
8
+ budgets without running skip_when; `claim` runs skip_when before
9
+ admitting a pass; `verify` re-runs the spec's verifier
10
+ commands independently of any worker; `claim` atomically admits one pass and
11
+ `finish` closes it append-only from helper-owned timing and verify events;
12
+ `status` aggregates completed outcomes and exposes unfinished recovery.
13
+ Relative paths (spec `log:`, skip_when, verify commands) resolve against the
14
+ caller's working directory — invoke from the repository root.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import argparse
20
+ import json
21
+ import math
22
+ import os
23
+ import re
24
+ import subprocess
25
+ import sys
26
+ import time
27
+ import uuid
28
+ from contextlib import contextmanager
29
+ from datetime import datetime, timedelta, timezone
30
+ from pathlib import Path
31
+ from typing import Iterator, Optional, Sequence
32
+
33
+ if sys.platform == "win32":
34
+ import msvcrt
35
+ else:
36
+ import fcntl
37
+
38
+
39
+ class LoopError(RuntimeError):
40
+ pass
41
+
42
+
43
+ MANDATORY_FIELDS = ("loop", "status", "trigger", "verify", "max_iterations", "wall_clock", "log")
44
+ OPTIONAL_FIELDS = ("cooldown", "skip_when", "period", "period_budget")
45
+ KNOWN_FIELDS = frozenset((*MANDATORY_FIELDS, *OPTIONAL_FIELDS))
46
+ REPEATABLE_FIELDS = frozenset({"verify"})
47
+ STATUSES = ("active", "paused", "done")
48
+ RESULTS = ("pass", "fail", "blocked", "skipped")
49
+ CLAIM_EVENT = "claim"
50
+ VERIFY_EVENT = "verify"
51
+ FINISH_EVENT = "finish"
52
+ FIELD_LINE = re.compile(r"^(?P<key>[a-z_]+):(?P<rest>[ \t].*)?$")
53
+ TRAILING_COMMENT = re.compile(r"[ \t]*<!--.*?-->[ \t]*$")
54
+ DURATION = re.compile(r"^(?P<amount>\d+)(?P<unit>s|m|h|d)?$")
55
+ UNIT_SECONDS = {"s": 1, "m": 60, "h": 3600, "d": 86400, None: 1}
56
+ OUTPUT_TAIL_LINES = 20
57
+
58
+
59
+ def parse_duration(raw: str, field: str) -> int:
60
+ match = DURATION.fullmatch(raw.strip())
61
+ if not match:
62
+ raise LoopError(f"{field} must be a duration like 300s, 15m, 2h, or 1d")
63
+ return int(match.group("amount")) * UNIT_SECONDS[match.group("unit")]
64
+
65
+
66
+ def parse_spec(path: Path) -> dict:
67
+ if path.is_symlink() or not path.is_file():
68
+ raise LoopError(f"spec must be a real file: {path}")
69
+ fields: dict[str, object] = {"verify": []}
70
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
71
+ if line.startswith("## "):
72
+ break # machine fields live in the header; prose sections follow
73
+ match = FIELD_LINE.match(line)
74
+ if not match or match.group("key") not in KNOWN_FIELDS:
75
+ continue
76
+ key = match.group("key")
77
+ rest = match.group("rest")
78
+ value = TRAILING_COMMENT.sub("", rest).strip() if rest else ""
79
+ if not value:
80
+ raise LoopError(f"{path}:{number}: field {key} has no value")
81
+ if key in REPEATABLE_FIELDS:
82
+ fields[key].append(value)
83
+ elif key in fields:
84
+ raise LoopError(f"{path}:{number}: duplicate field {key}")
85
+ else:
86
+ fields[key] = value
87
+ missing = [key for key in MANDATORY_FIELDS if not fields.get(key)]
88
+ if missing:
89
+ raise LoopError(f"{path}: missing mandatory fields: {', '.join(missing)}")
90
+ if fields["status"] not in STATUSES:
91
+ raise LoopError(f"{path}: status must be one of {', '.join(STATUSES)}")
92
+ fields["max_iterations"] = parse_positive_int(fields["max_iterations"], "max_iterations")
93
+ fields["wall_clock"] = parse_duration(fields["wall_clock"], "wall_clock")
94
+ for key in ("cooldown", "period", "period_budget"):
95
+ if key in fields:
96
+ fields[key] = parse_duration(fields[key], key)
97
+ if "period_budget" in fields and "period" not in fields:
98
+ raise LoopError(f"{path}: period_budget requires period")
99
+ return fields
100
+
101
+
102
+ def parse_positive_int(raw: str, field: str) -> int:
103
+ try:
104
+ value = int(raw)
105
+ except ValueError as error:
106
+ raise LoopError(f"{field} must be a positive integer") from error
107
+ if value < 1:
108
+ raise LoopError(f"{field} must be a positive integer")
109
+ return value
110
+
111
+
112
+ def log_path(fields: dict) -> Path:
113
+ return Path(fields["log"])
114
+
115
+
116
+ @contextmanager
117
+ def log_lock(fields: dict) -> Iterator[None]:
118
+ path = log_path(fields)
119
+ path.parent.mkdir(parents=True, exist_ok=True)
120
+ lock_path = path.with_name(f".{path.name}.lock")
121
+ with lock_path.open("a+b") as handle:
122
+ if sys.platform == "win32":
123
+ handle.seek(0, os.SEEK_END)
124
+ if handle.tell() == 0:
125
+ handle.write(b"\0")
126
+ handle.flush()
127
+ handle.seek(0)
128
+ msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
129
+ else:
130
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
131
+ try:
132
+ yield
133
+ finally:
134
+ if sys.platform == "win32":
135
+ handle.seek(0)
136
+ msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
137
+ else:
138
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
139
+
140
+
141
+ def load_log(fields: dict) -> list[dict]:
142
+ path = log_path(fields)
143
+ if not path.exists():
144
+ return []
145
+ records = []
146
+ for number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1):
147
+ if not line.strip():
148
+ continue
149
+ try:
150
+ record = json.loads(line)
151
+ except json.JSONDecodeError as error:
152
+ raise LoopError(f"{path}:{number}: log line is not valid JSON") from error
153
+ if not isinstance(record, dict):
154
+ raise LoopError(f"{path}:{number}: log line must be a JSON object")
155
+ records.append(record)
156
+ return records
157
+
158
+
159
+ def append_log(fields: dict, record: dict) -> None:
160
+ path = log_path(fields)
161
+ payload = (json.dumps(record, sort_keys=True) + "\n").encode()
162
+ with path.open("ab", buffering=0) as handle:
163
+ written = handle.write(payload)
164
+ if written != len(payload):
165
+ raise LoopError(f"could not append a complete record to {path}")
166
+ os.fsync(handle.fileno())
167
+
168
+
169
+ def parse_timestamp(record: dict, path: Path, number: int) -> datetime:
170
+ raw = record.get("timestamp")
171
+ if not isinstance(raw, str):
172
+ raise LoopError(f"{path}:{number}: log record lacks a timestamp")
173
+ try:
174
+ parsed = datetime.fromisoformat(raw)
175
+ except ValueError as error:
176
+ raise LoopError(f"{path}:{number}: log record timestamp is invalid") from error
177
+ if parsed.tzinfo is None:
178
+ raise LoopError(f"{path}:{number}: log record timestamp must be timezone-aware")
179
+ return parsed
180
+
181
+
182
+ def claim_id(record: dict, path: Path, number: int) -> str:
183
+ value = record.get("claim")
184
+ if not isinstance(value, str) or not value:
185
+ raise LoopError(f"{path}:{number}: claim event lacks a claim id")
186
+ return value
187
+
188
+
189
+ def require_record_fields(
190
+ record: dict, expected: set[str], path: Path, number: int, event: str
191
+ ) -> None:
192
+ if set(record) != expected:
193
+ raise LoopError(f"{path}:{number}: {event} record has unsupported fields")
194
+
195
+
196
+ def validate_text_field(record: dict, field: str, path: Path, number: int) -> str:
197
+ value = record.get(field)
198
+ if not isinstance(value, str):
199
+ raise LoopError(f"{path}:{number}: {field} must be text")
200
+ return value
201
+
202
+
203
+ def log_state(
204
+ fields: dict, records: list[dict]
205
+ ) -> tuple[list[dict], dict, dict, dict, Optional[dict]]:
206
+ path = log_path(fields)
207
+ outcomes = []
208
+ claims = {}
209
+ verifications: dict[str, list[dict]] = {}
210
+ finishes = {}
211
+ for number, record in enumerate(records, 1):
212
+ event = record.get("event")
213
+ if event is None:
214
+ require_record_fields(
215
+ record,
216
+ {
217
+ "timestamp",
218
+ "loop",
219
+ "trigger",
220
+ "iterations",
221
+ "result",
222
+ "wall_clock_used",
223
+ "rescue",
224
+ "notes",
225
+ },
226
+ path,
227
+ number,
228
+ "legacy outcome",
229
+ )
230
+ parse_timestamp(record, path, number)
231
+ if record.get("loop") != fields["loop"] or record.get("trigger") != fields["trigger"]:
232
+ raise LoopError(f"{path}:{number}: outcome belongs to another loop")
233
+ if record.get("result") not in RESULTS:
234
+ raise LoopError(f"{path}:{number}: outcome has an invalid result")
235
+ if integer_field(record, "iterations", path, number) > fields["max_iterations"]:
236
+ raise LoopError(f"{path}:{number}: outcome exceeds max_iterations")
237
+ integer_field(record, "wall_clock_used", path, number)
238
+ if not isinstance(record.get("rescue"), bool):
239
+ raise LoopError(f"{path}:{number}: rescue must be boolean")
240
+ validate_text_field(record, "notes", path, number)
241
+ outcomes.append(record)
242
+ continue
243
+ if event not in {CLAIM_EVENT, VERIFY_EVENT, FINISH_EVENT}:
244
+ raise LoopError(f"{path}:{number}: unknown log event {event}")
245
+ identifier = claim_id(record, path, number)
246
+ if event == CLAIM_EVENT:
247
+ require_record_fields(
248
+ record,
249
+ {
250
+ "event",
251
+ "claim",
252
+ "timestamp",
253
+ "loop",
254
+ "trigger",
255
+ "max_iterations",
256
+ "wall_clock_limit",
257
+ },
258
+ path,
259
+ number,
260
+ "claim",
261
+ )
262
+ if identifier in claims:
263
+ raise LoopError(f"{path}:{number}: duplicate claim {identifier}")
264
+ parse_timestamp(record, path, number)
265
+ if record.get("loop") != fields["loop"] or record.get("trigger") != fields["trigger"]:
266
+ raise LoopError(f"{path}:{number}: claim belongs to another loop")
267
+ if integer_field(record, "max_iterations", path, number) < 1:
268
+ raise LoopError(f"{path}:{number}: claim max_iterations must be positive")
269
+ if integer_field(record, "wall_clock_limit", path, number) < 1:
270
+ raise LoopError(f"{path}:{number}: claim wall_clock_limit must be positive")
271
+ claims[identifier] = record
272
+ continue
273
+ if event == VERIFY_EVENT:
274
+ require_record_fields(
275
+ record,
276
+ {
277
+ "event",
278
+ "claim",
279
+ "timestamp",
280
+ "sequence",
281
+ "iteration",
282
+ "result",
283
+ "commands",
284
+ },
285
+ path,
286
+ number,
287
+ "verify",
288
+ )
289
+ if identifier not in claims:
290
+ raise LoopError(
291
+ f"{path}:{number}: verify references unknown claim {identifier}"
292
+ )
293
+ if identifier in finishes:
294
+ raise LoopError(
295
+ f"{path}:{number}: verify follows finish for claim {identifier}"
296
+ )
297
+ if record.get("result") not in {"pass", "fail"}:
298
+ raise LoopError(
299
+ f"{path}:{number}: verify result must be pass or fail"
300
+ )
301
+ verified_at = parse_timestamp(record, path, number)
302
+ claim_record = claims[identifier]
303
+ claim_number = records.index(claim_record) + 1
304
+ if verified_at < parse_timestamp(claim_record, path, claim_number):
305
+ raise LoopError(f"{path}:{number}: verify predates its claim")
306
+ sequence = integer_field(record, "sequence", path, number)
307
+ expected = len(verifications.get(identifier, [])) + 1
308
+ if sequence != expected:
309
+ raise LoopError(
310
+ f"{path}:{number}: verify sequence must be {expected}"
311
+ )
312
+ iteration = integer_field(record, "iteration", path, number)
313
+ if iteration != max(0, sequence - 1):
314
+ raise LoopError(
315
+ f"{path}:{number}: verify iteration does not match sequence"
316
+ )
317
+ max_iterations = integer_field(
318
+ claim_record, "max_iterations", path, claim_number
319
+ )
320
+ if sequence > max_iterations + 1:
321
+ raise LoopError(f"{path}:{number}: verify exceeds max_iterations")
322
+ if integer_field(record, "commands", path, number) != len(fields["verify"]):
323
+ raise LoopError(f"{path}:{number}: verify command count does not match spec")
324
+ verifications.setdefault(identifier, []).append(record)
325
+ continue
326
+ if identifier not in claims:
327
+ raise LoopError(f"{path}:{number}: finish references unknown claim {identifier}")
328
+ if identifier in finishes:
329
+ raise LoopError(f"{path}:{number}: duplicate finish for claim {identifier}")
330
+ require_record_fields(
331
+ record,
332
+ {
333
+ "event",
334
+ "claim",
335
+ "timestamp",
336
+ "loop",
337
+ "trigger",
338
+ "iterations",
339
+ "result",
340
+ "wall_clock_used",
341
+ "rescue",
342
+ "notes",
343
+ },
344
+ path,
345
+ number,
346
+ "finish",
347
+ )
348
+ claim_record = claims[identifier]
349
+ claim_number = records.index(claim_record) + 1
350
+ started = parse_timestamp(claim_record, path, claim_number)
351
+ finished_at = parse_timestamp(record, path, number)
352
+ if finished_at < started:
353
+ raise LoopError(f"{path}:{number}: finish predates its claim")
354
+ if record.get("loop") != claim_record.get("loop") or record.get("trigger") != claim_record.get("trigger"):
355
+ raise LoopError(f"{path}:{number}: finish does not match its claim")
356
+ verification_records = verifications.get(identifier, [])
357
+ iterations = integer_field(record, "iterations", path, number)
358
+ if iterations != max(0, len(verification_records) - 1):
359
+ raise LoopError(f"{path}:{number}: finish iterations do not match verify events")
360
+ elapsed = max(0, math.ceil((finished_at - started).total_seconds()))
361
+ if integer_field(record, "wall_clock_used", path, number) != elapsed:
362
+ raise LoopError(f"{path}:{number}: finish wall clock does not match claim timestamps")
363
+ result = record.get("result")
364
+ if result not in RESULTS:
365
+ raise LoopError(f"{path}:{number}: finish has an invalid result")
366
+ if not isinstance(record.get("rescue"), bool):
367
+ raise LoopError(f"{path}:{number}: finish rescue must be boolean")
368
+ validate_text_field(record, "notes", path, number)
369
+ latest = verification_records[-1]["result"] if verification_records else None
370
+ if result == "pass" and latest != "pass":
371
+ raise LoopError(f"{path}:{number}: passing finish lacks a passing verify")
372
+ if latest == "pass" and result != "pass":
373
+ raise LoopError(f"{path}:{number}: passing verify must finish as pass")
374
+ if result == "fail":
375
+ max_iterations = integer_field(
376
+ claim_record, "max_iterations", path, claim_number
377
+ )
378
+ wall_clock_limit = integer_field(
379
+ claim_record, "wall_clock_limit", path, claim_number
380
+ )
381
+ if latest != "fail" or (
382
+ iterations < max_iterations and elapsed < wall_clock_limit
383
+ ):
384
+ raise LoopError(f"{path}:{number}: failing finish has no exhausted limit")
385
+ finishes[identifier] = record
386
+ outcomes.append(record)
387
+ active = [record for identifier, record in claims.items() if identifier not in finishes]
388
+ if len(active) > 1:
389
+ raise LoopError(f"{path}: multiple unfinished claims require recovery")
390
+ return outcomes, claims, verifications, finishes, active[0] if active else None
391
+
392
+
393
+ def integer_field(record: dict, field: str, path: Path, number: int) -> int:
394
+ value = record.get(field, 0)
395
+ if isinstance(value, bool):
396
+ raise LoopError(f"{path}:{number}: {field} must be an integer")
397
+ try:
398
+ parsed = int(value)
399
+ except (TypeError, ValueError) as error:
400
+ raise LoopError(f"{path}:{number}: {field} must be an integer") from error
401
+ if parsed < 0:
402
+ raise LoopError(f"{path}:{number}: {field} must not be negative")
403
+ return parsed
404
+
405
+
406
+ def remaining_period_budget(fields: dict, outcomes: list[dict], now: datetime) -> Optional[int]:
407
+ if "period_budget" not in fields:
408
+ return None
409
+ path = log_path(fields)
410
+ window_start = now - timedelta(seconds=fields["period"])
411
+ consumed = 0
412
+ for number, record in enumerate(outcomes, 1):
413
+ if parse_timestamp(record, path, number) >= window_start:
414
+ consumed += integer_field(record, "wall_clock_used", path, number)
415
+ return max(0, fields["period_budget"] - consumed)
416
+
417
+
418
+ def precheck(fields: dict) -> Optional[dict]:
419
+ if fields["status"] != "active":
420
+ return {"decision": "skip", "reason": f"status {fields['status']}"}
421
+ return None
422
+
423
+
424
+ def skip_when_matched(fields: dict) -> Optional[dict]:
425
+ if "skip_when" in fields:
426
+ exit_code, _ = run_shell(fields["skip_when"], fields["wall_clock"])
427
+ if exit_code == 0:
428
+ return {"decision": "skip", "reason": "skip_when matched"}
429
+ return None
430
+
431
+
432
+ def admission(fields: dict, records: list[dict], now: datetime) -> dict:
433
+ outcomes, _claims, _verifications, _finishes, active = log_state(fields, records)
434
+ if active is not None:
435
+ line = records.index(active) + 1
436
+ started = parse_timestamp(active, log_path(fields), line)
437
+ wall_clock_limit = integer_field(
438
+ active, "wall_clock_limit", log_path(fields), line
439
+ )
440
+ elapsed = max(0, int((now - started).total_seconds()))
441
+ expired = elapsed >= wall_clock_limit
442
+ result = {
443
+ "decision": "skip",
444
+ "reason": (
445
+ f"claim {active['claim']} is expired"
446
+ if expired
447
+ else "another loop pass is still active"
448
+ ),
449
+ "recovery": {
450
+ "elapsed_seconds": min(elapsed, wall_clock_limit),
451
+ "wall_clock_limit": wall_clock_limit,
452
+ "recoverable": expired,
453
+ },
454
+ }
455
+ if expired:
456
+ result["claim"] = active["claim"]
457
+ return result
458
+ path = log_path(fields)
459
+ timestamps = [
460
+ parse_timestamp(record, path, number)
461
+ for number, record in enumerate(outcomes, 1)
462
+ ]
463
+ remaining_cooldown = 0
464
+ if "cooldown" in fields and timestamps:
465
+ elapsed = (now - max(timestamps)).total_seconds()
466
+ remaining_cooldown = max(0, int(fields["cooldown"] - elapsed))
467
+ if remaining_cooldown > 0:
468
+ return {
469
+ "decision": "skip",
470
+ "reason": f"cooldown active ({remaining_cooldown}s remaining)",
471
+ }
472
+ remaining_budget = remaining_period_budget(fields, outcomes, now)
473
+ if remaining_budget == 0:
474
+ return {"decision": "skip", "reason": "period budget exhausted"}
475
+ return {
476
+ "decision": "run",
477
+ "reason": "gates green",
478
+ "remaining": {
479
+ "cooldown_seconds": remaining_cooldown,
480
+ "period_budget_seconds": remaining_budget,
481
+ "wall_clock_seconds": fields["wall_clock"],
482
+ "max_iterations": fields["max_iterations"],
483
+ },
484
+ }
485
+
486
+
487
+ def command_check(fields: dict) -> dict:
488
+ skipped = precheck(fields)
489
+ if skipped is not None:
490
+ return skipped
491
+ decision = admission(fields, load_log(fields), datetime.now(timezone.utc))
492
+ if "skip_when" in fields:
493
+ decision["skip_when"] = "not run by check; claim runs it"
494
+ return decision
495
+
496
+
497
+ def command_claim(fields: dict) -> dict:
498
+ skipped = precheck(fields) or skip_when_matched(fields)
499
+ if skipped is not None:
500
+ return skipped
501
+ with log_lock(fields):
502
+ now = datetime.now(timezone.utc)
503
+ decision = admission(fields, load_log(fields), now)
504
+ if decision["decision"] != "run":
505
+ return decision
506
+ remaining_budget = decision["remaining"]["period_budget_seconds"]
507
+ wall_clock_limit = fields["wall_clock"]
508
+ if remaining_budget is not None:
509
+ wall_clock_limit = min(wall_clock_limit, remaining_budget)
510
+ identifier = uuid.uuid4().hex
511
+ append_log(
512
+ fields,
513
+ {
514
+ "event": CLAIM_EVENT,
515
+ "claim": identifier,
516
+ "timestamp": now.isoformat(),
517
+ "loop": fields["loop"],
518
+ "trigger": fields["trigger"],
519
+ "max_iterations": fields["max_iterations"],
520
+ "wall_clock_limit": wall_clock_limit,
521
+ },
522
+ )
523
+ decision["claim"] = identifier
524
+ decision["remaining"]["wall_clock_seconds"] = wall_clock_limit
525
+ return decision
526
+
527
+
528
+ def run_shell(command: str, timeout: float) -> tuple[Optional[int], str]:
529
+ """Run one spec command; a hung command counts as failed (exit None)."""
530
+ try:
531
+ completed = subprocess.run(
532
+ command, shell=True, capture_output=True, text=True, check=False, timeout=timeout
533
+ )
534
+ except subprocess.TimeoutExpired as expired:
535
+ output = (expired.stdout or b"").decode(errors="replace") + (expired.stderr or b"").decode(errors="replace")
536
+ return None, f"{output}\ntimed out at the admitted wall-clock deadline"
537
+ return completed.returncode, completed.stdout + completed.stderr
538
+
539
+
540
+ def command_verify(fields: dict, claim_identifier: Optional[str]) -> dict:
541
+ if not claim_identifier:
542
+ raise LoopError("verify requires --claim")
543
+ with log_lock(fields):
544
+ records = load_log(fields)
545
+ _outcomes, claims, verifications, _finishes, active = log_state(
546
+ fields, records
547
+ )
548
+ claim = claims.get(claim_identifier)
549
+ if claim is None:
550
+ raise LoopError(f"unknown claim {claim_identifier}")
551
+ if active is None or active.get("claim") != claim_identifier:
552
+ raise LoopError(f"claim {claim_identifier} is not the unfinished claim")
553
+ claim_line = records.index(claim) + 1
554
+ prior = verifications.get(claim_identifier, [])
555
+ max_iterations = integer_field(
556
+ claim, "max_iterations", log_path(fields), claim_line
557
+ )
558
+ if len(prior) > max_iterations:
559
+ raise LoopError(f"claim {claim_identifier} has exhausted max_iterations")
560
+ if prior and prior[-1]["result"] == "pass":
561
+ raise LoopError(f"claim {claim_identifier} already verified pass")
562
+ started = parse_timestamp(claim, log_path(fields), claim_line)
563
+ wall_clock_limit = integer_field(
564
+ claim, "wall_clock_limit", log_path(fields), claim_line
565
+ )
566
+ remaining = (
567
+ started
568
+ + timedelta(seconds=wall_clock_limit)
569
+ - datetime.now(timezone.utc)
570
+ ).total_seconds()
571
+ if remaining <= 0:
572
+ raise LoopError(f"claim {claim_identifier} has exhausted its wall clock")
573
+ deadline = time.monotonic() + remaining
574
+ failures = []
575
+ for command in fields["verify"]:
576
+ command_budget = deadline - time.monotonic()
577
+ if command_budget <= 0:
578
+ failures.append(
579
+ {
580
+ "command": command,
581
+ "exit_code": None,
582
+ "output_tail": "admitted wall-clock deadline exhausted before command",
583
+ }
584
+ )
585
+ break
586
+ exit_code, output = run_shell(command, command_budget)
587
+ if exit_code != 0:
588
+ lines = output.splitlines()
589
+ failures.append(
590
+ {
591
+ "command": command,
592
+ "exit_code": exit_code,
593
+ "output_tail": "\n".join(lines[-OUTPUT_TAIL_LINES:]),
594
+ }
595
+ )
596
+ result = "fail" if failures else "pass"
597
+ sequence = len(prior) + 1
598
+ iteration = max(0, sequence - 1)
599
+ append_log(
600
+ fields,
601
+ {
602
+ "event": VERIFY_EVENT,
603
+ "claim": claim_identifier,
604
+ "timestamp": datetime.now(timezone.utc).isoformat(),
605
+ "sequence": sequence,
606
+ "iteration": iteration,
607
+ "result": result,
608
+ "commands": len(fields["verify"]),
609
+ },
610
+ )
611
+ payload = {
612
+ "result": result,
613
+ "claim": claim_identifier,
614
+ "commands": len(fields["verify"]),
615
+ "iteration": iteration,
616
+ }
617
+ if failures:
618
+ payload["failures"] = failures
619
+ return payload
620
+
621
+
622
+ def command_finish(fields: dict, arguments: argparse.Namespace) -> dict:
623
+ if not arguments.claim:
624
+ raise LoopError("finish requires --claim")
625
+ with log_lock(fields):
626
+ records = load_log(fields)
627
+ _outcomes, claims, verifications, finishes, active = log_state(fields, records)
628
+ claim = claims.get(arguments.claim)
629
+ if claim is None:
630
+ raise LoopError(f"unknown claim {arguments.claim}")
631
+ completed = finishes.get(arguments.claim)
632
+ if completed is not None:
633
+ candidate = {
634
+ "result": arguments.result,
635
+ "iterations": arguments.iterations,
636
+ "rescue": arguments.rescue,
637
+ "notes": arguments.notes,
638
+ }
639
+ if any(completed.get(key) != value for key, value in candidate.items()):
640
+ raise LoopError(f"claim {arguments.claim} is already finished differently")
641
+ return {
642
+ "recorded": completed["result"],
643
+ "claim": arguments.claim,
644
+ "log": str(log_path(fields)),
645
+ "reason": "already finished",
646
+ }
647
+ if active is None or active.get("claim") != arguments.claim:
648
+ raise LoopError(f"claim {arguments.claim} is not the unfinished claim")
649
+ claim_line = records.index(claim) + 1
650
+ started = parse_timestamp(claim, log_path(fields), claim_line)
651
+ max_iterations = integer_field(
652
+ claim, "max_iterations", log_path(fields), claim_line
653
+ )
654
+ wall_clock_limit = integer_field(
655
+ claim, "wall_clock_limit", log_path(fields), claim_line
656
+ )
657
+ verification_records = verifications.get(arguments.claim, [])
658
+ iterations = max(0, len(verification_records) - 1)
659
+ if arguments.iterations != iterations:
660
+ raise LoopError(
661
+ f"--iterations must match helper-observed fix cycles "
662
+ f"({arguments.iterations} != {iterations})"
663
+ )
664
+ now = datetime.now(timezone.utc)
665
+ elapsed = max(0, math.ceil((now - started).total_seconds()))
666
+ if elapsed > wall_clock_limit and arguments.result not in {"fail", "blocked"}:
667
+ raise LoopError(
668
+ f"claim {arguments.claim} exhausted its wall clock; "
669
+ "finish with fail or blocked"
670
+ )
671
+ latest_result = verification_records[-1]["result"] if verification_records else None
672
+ if arguments.result == "pass" and latest_result != "pass":
673
+ raise LoopError("pass requires a recorded passing verify")
674
+ if latest_result == "pass" and arguments.result != "pass":
675
+ raise LoopError("a recorded passing verify must finish as pass")
676
+ if arguments.result == "fail" and (
677
+ latest_result != "fail"
678
+ or (iterations < max_iterations and elapsed < wall_clock_limit)
679
+ ):
680
+ raise LoopError("fail requires a failed verify and an exhausted limit")
681
+ record = {
682
+ "event": FINISH_EVENT,
683
+ "claim": arguments.claim,
684
+ "timestamp": now.isoformat(),
685
+ "loop": fields["loop"],
686
+ "trigger": fields["trigger"],
687
+ "iterations": iterations,
688
+ "result": arguments.result,
689
+ "wall_clock_used": elapsed,
690
+ "rescue": arguments.rescue,
691
+ "notes": arguments.notes,
692
+ }
693
+ append_log(fields, record)
694
+ return {
695
+ "recorded": record["result"],
696
+ "claim": arguments.claim,
697
+ "log": str(log_path(fields)),
698
+ }
699
+
700
+
701
+ def command_recover(fields: dict, claim_identifier: Optional[str]) -> dict:
702
+ """Close one expired unfinished claim without caller-reported telemetry."""
703
+
704
+ if not claim_identifier:
705
+ raise LoopError("recover requires --claim")
706
+ with log_lock(fields):
707
+ records = load_log(fields)
708
+ _outcomes, claims, verifications, finishes, active = log_state(fields, records)
709
+ claim = claims.get(claim_identifier)
710
+ if claim is None:
711
+ raise LoopError(f"unknown claim {claim_identifier}")
712
+ completed = finishes.get(claim_identifier)
713
+ verification_records = verifications.get(claim_identifier, [])
714
+ latest = verification_records[-1]["result"] if verification_records else None
715
+ recovered_result = latest if latest in {"pass", "fail"} else "blocked"
716
+ recovery_notes = f"recovered unfinished claim as {recovered_result}"
717
+ if completed is not None:
718
+ if (
719
+ completed.get("result") != recovered_result
720
+ or completed.get("notes") != recovery_notes
721
+ ):
722
+ raise LoopError(f"claim {claim_identifier} is already finished differently")
723
+ return {
724
+ "recorded": recovered_result,
725
+ "claim": claim_identifier,
726
+ "log": str(log_path(fields)),
727
+ "reason": "already recovered",
728
+ }
729
+ if active is None or active.get("claim") != claim_identifier:
730
+ raise LoopError(f"claim {claim_identifier} is not the unfinished claim")
731
+ claim_line = records.index(claim) + 1
732
+ started = parse_timestamp(claim, log_path(fields), claim_line)
733
+ wall_clock_limit = integer_field(
734
+ claim, "wall_clock_limit", log_path(fields), claim_line
735
+ )
736
+ now = datetime.now(timezone.utc)
737
+ elapsed_raw = max(0.0, (now - started).total_seconds())
738
+ if elapsed_raw < wall_clock_limit:
739
+ raise LoopError(f"claim {claim_identifier} has not expired")
740
+ iterations = max(0, len(verification_records) - 1)
741
+ record = {
742
+ "event": FINISH_EVENT,
743
+ "claim": claim_identifier,
744
+ "timestamp": now.isoformat(),
745
+ "loop": fields["loop"],
746
+ "trigger": fields["trigger"],
747
+ "iterations": iterations,
748
+ "result": recovered_result,
749
+ "wall_clock_used": math.ceil(elapsed_raw),
750
+ "rescue": False,
751
+ "notes": recovery_notes,
752
+ }
753
+ append_log(fields, record)
754
+ return {
755
+ "recorded": recovered_result,
756
+ "claim": claim_identifier,
757
+ "iterations": iterations,
758
+ "log": str(log_path(fields)),
759
+ }
760
+
761
+
762
+ def command_status(fields: dict) -> dict:
763
+ records = load_log(fields)
764
+ outcomes, _claims, _verifications, _finishes, active = log_state(fields, records)
765
+ counts = {result: 0 for result in RESULTS}
766
+ wall_clock_used = 0
767
+ human_rescues = 0
768
+ path = log_path(fields)
769
+ for number, record in enumerate(outcomes, 1):
770
+ result = record.get("result")
771
+ if result in counts:
772
+ counts[result] += 1
773
+ wall_clock_used += integer_field(record, "wall_clock_used", path, number)
774
+ if record.get("rescue"):
775
+ human_rescues += 1
776
+ accepted = counts["pass"]
777
+ evaluated = accepted + counts["fail"]
778
+ return {
779
+ "loop": fields["loop"],
780
+ "runs": len(outcomes),
781
+ "accepted": accepted,
782
+ "rejected": counts["fail"],
783
+ "blocked": counts["blocked"],
784
+ "skipped": counts["skipped"],
785
+ "human_rescues": human_rescues,
786
+ "wall_clock_used_seconds": wall_clock_used,
787
+ "acceptance_rate": round(accepted / evaluated, 4) if evaluated else None,
788
+ "wall_clock_per_accept_seconds": (
789
+ round(wall_clock_used / accepted, 2) if accepted else None
790
+ ),
791
+ "active_claim": active["claim"] if active is not None else None,
792
+ }
793
+
794
+
795
+ def parser() -> argparse.ArgumentParser:
796
+ result = argparse.ArgumentParser(description=__doc__)
797
+ result.add_argument(
798
+ "command", choices=("check", "claim", "verify", "finish", "recover", "status")
799
+ )
800
+ result.add_argument("--spec", type=Path, required=True)
801
+ result.add_argument("--claim")
802
+ result.add_argument("--result", choices=RESULTS)
803
+ result.add_argument("--iterations", type=int, default=0)
804
+ result.add_argument("--notes", default="")
805
+ result.add_argument("--rescue", action="store_true")
806
+ return result
807
+
808
+
809
+ def main(argv: Optional[Sequence[str]] = None) -> int:
810
+ arguments = parser().parse_args(argv)
811
+ try:
812
+ fields = parse_spec(arguments.spec)
813
+ if arguments.command == "check":
814
+ payload = command_check(fields)
815
+ elif arguments.command == "claim":
816
+ payload = command_claim(fields)
817
+ elif arguments.command == "verify":
818
+ payload = command_verify(fields, arguments.claim)
819
+ elif arguments.command == "recover":
820
+ payload = command_recover(fields, arguments.claim)
821
+ elif arguments.command == "finish":
822
+ if arguments.result is None:
823
+ raise LoopError(f"{arguments.command} requires --result")
824
+ payload = command_finish(fields, arguments)
825
+ else:
826
+ payload = command_status(fields)
827
+ except LoopError as error:
828
+ print(str(error), file=sys.stderr)
829
+ return 2
830
+ print(json.dumps(payload, sort_keys=True))
831
+ if arguments.command == "verify" and payload["result"] == "fail":
832
+ return 1
833
+ return 0
834
+
835
+
836
+ if __name__ == "__main__":
837
+ raise SystemExit(main())