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