devlyn-cli 3.2.0 → 3.3.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 (424) hide show
  1. package/AGENTS.md +2 -2
  2. package/CLAUDE.md +2 -2
  3. package/README.md +22 -113
  4. package/bin/devlyn.js +13 -225
  5. package/bin/instruction-templates.json +752 -1
  6. package/bin/instructions.js +101 -17
  7. package/config/skills/_shared/adapters/grok.md +1 -2
  8. package/config/skills/_shared/archive_run.py +15 -3
  9. package/config/skills/_shared/judge-role-evidence.py +21 -1
  10. package/config/skills/_shared/runtime-principles.md +1 -1
  11. package/config/skills/_shared/spec-verify-check.py +0 -653
  12. package/config/skills/_shared/state-phase-write.py +371 -30
  13. package/config/skills/_shared/task-complete.py +635 -143
  14. package/config/skills/_shared/terminal-claim-check.py +229 -4
  15. package/config/skills/_shared/verify-merge-findings.py +93 -2
  16. package/config/skills/devlyn:ideate/SKILL.md +3 -4
  17. package/config/skills/devlyn:ideate/references/elicitation.md +2 -35
  18. package/config/skills/devlyn:ideate/references/from-spec-mode.md +3 -18
  19. package/config/skills/devlyn:ideate/references/project-mode.md +1 -16
  20. package/config/skills/devlyn:ideate/references/spec-template.md +0 -2
  21. package/config/skills/devlyn:queue/SKILL.md +6 -4
  22. package/config/skills/devlyn:resolve/SKILL.md +16 -25
  23. package/config/skills/devlyn:resolve/references/free-form-mode.md +0 -14
  24. package/config/skills/devlyn:resolve/references/outer-loop.md +1 -1
  25. package/config/skills/devlyn:resolve/references/phases/build-gate.md +1 -1
  26. package/config/skills/devlyn:resolve/references/phases/cleanup.md +3 -3
  27. package/config/skills/devlyn:resolve/references/phases/implement.md +1 -1
  28. package/config/skills/devlyn:resolve/references/phases/probe-derive.md +1 -16
  29. package/config/skills/devlyn:resolve/references/phases/verify.md +4 -9
  30. package/config/skills/devlyn:resolve/references/state-schema.md +9 -7
  31. package/config/skills/devlyn:resolve/references/task-completion.md +46 -37
  32. package/{config/skills → optional-skills}/code-health-standards/SKILL.md +4 -13
  33. package/{config/skills → optional-skills}/code-review-standards/SKILL.md +4 -12
  34. package/{config/skills → optional-skills}/root-cause-analysis/SKILL.md +4 -12
  35. package/{config/skills → optional-skills}/ui-implementation-standards/SKILL.md +4 -14
  36. package/package.json +1 -57
  37. package/benchmark/auto-resolve/BENCHMARK-DESIGN.md +0 -289
  38. package/benchmark/auto-resolve/BENCHMARK-RESULTS.md +0 -341
  39. package/benchmark/auto-resolve/README.md +0 -696
  40. package/benchmark/auto-resolve/RUBRIC.md +0 -177
  41. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/NOTES.md +0 -34
  42. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/expected.json +0 -68
  43. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/metadata.json +0 -10
  44. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/setup.sh +0 -4
  45. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/spec.md +0 -44
  46. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.ko.txt +0 -8
  47. package/benchmark/auto-resolve/fixtures/F1-cli-trivial-flag/task.txt +0 -8
  48. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/NOTES.md +0 -68
  49. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/expected.json +0 -60
  50. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/metadata.json +0 -10
  51. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/setup.sh +0 -17
  52. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/spec.md +0 -51
  53. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/task.txt +0 -9
  54. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/invalid.js +0 -29
  55. package/benchmark/auto-resolve/fixtures/F10-persist-write-collision/verifiers/parallel.js +0 -50
  56. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/NOTES.md +0 -75
  57. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/expected.json +0 -52
  58. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/metadata.json +0 -10
  59. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/setup.sh +0 -171
  60. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/spec.md +0 -50
  61. package/benchmark/auto-resolve/fixtures/F11-batch-import-all-or-nothing/task.txt +0 -9
  62. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/NOTES.md +0 -89
  63. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/expected.json +0 -74
  64. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/metadata.json +0 -10
  65. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/setup.sh +0 -251
  66. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/spec.md +0 -57
  67. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/task.txt +0 -13
  68. package/benchmark/auto-resolve/fixtures/F12-webhook-raw-body-signature/verifiers/replay-malformed-body.js +0 -64
  69. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/NOTES.md +0 -104
  70. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/expected.json +0 -46
  71. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/metadata.json +0 -10
  72. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/setup.sh +0 -336
  73. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/spec.md +0 -51
  74. package/benchmark/auto-resolve/fixtures/F15-frozen-diff-race-review/task.txt +0 -9
  75. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/NOTES.md +0 -38
  76. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/expected.json +0 -64
  77. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/metadata.json +0 -10
  78. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/setup.sh +0 -32
  79. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/spec.md +0 -63
  80. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/task.txt +0 -7
  81. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/exact-success.js +0 -54
  82. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/no-hardcoded-pricing.js +0 -47
  83. package/benchmark/auto-resolve/fixtures/F16-cli-quote-tax-rules/verifiers/stock-error.js +0 -45
  84. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/NOTES.md +0 -57
  85. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected-pair-plan-registry.json +0 -170
  86. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/expected.json +0 -84
  87. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/metadata.json +0 -21
  88. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-fail.json +0 -214
  89. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/pair-plan.sample-pass.json +0 -223
  90. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/setup.sh +0 -5
  91. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/spec.md +0 -55
  92. package/benchmark/auto-resolve/fixtures/F2-cli-medium-subcommand/task.txt +0 -14
  93. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/NOTES.md +0 -39
  94. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/expected.json +0 -62
  95. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/metadata.json +0 -10
  96. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/setup.sh +0 -2
  97. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/spec.md +0 -67
  98. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/task.txt +0 -7
  99. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/error-order.js +0 -55
  100. package/benchmark/auto-resolve/fixtures/F21-cli-scheduler-priority/verifiers/priority-blocked.js +0 -48
  101. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/NOTES.md +0 -35
  102. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/expected.json +0 -56
  103. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/metadata.json +0 -10
  104. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/setup.sh +0 -2
  105. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/spec.md +0 -64
  106. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/task.txt +0 -7
  107. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/conflicting-duplicate.js +0 -34
  108. package/benchmark/auto-resolve/fixtures/F22-cli-ledger-close/verifiers/idempotent-close.js +0 -41
  109. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/NOTES.md +0 -39
  110. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/expected.json +0 -56
  111. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/metadata.json +0 -10
  112. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/setup.sh +0 -2
  113. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/spec.md +0 -76
  114. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/task.txt +0 -7
  115. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/priority-rollback.js +0 -64
  116. package/benchmark/auto-resolve/fixtures/F23-cli-fulfillment-wave/verifiers/single-warehouse-fefo.js +0 -66
  117. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/NOTES.md +0 -40
  118. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/expected.json +0 -66
  119. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/metadata.json +0 -10
  120. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/setup.sh +0 -36
  121. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/spec.md +0 -71
  122. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/task.txt +0 -7
  123. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/catalog-source.js +0 -57
  124. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/exact-success.js +0 -63
  125. package/benchmark/auto-resolve/fixtures/F25-cli-cart-promotion-rules/verifiers/stock-error.js +0 -34
  126. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/NOTES.md +0 -31
  127. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/expected.json +0 -68
  128. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/metadata.json +0 -10
  129. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/setup.sh +0 -17
  130. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/spec.md +0 -68
  131. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/task.txt +0 -7
  132. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/conflicting-duplicate.js +0 -29
  133. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/exact-payout.js +0 -58
  134. package/benchmark/auto-resolve/fixtures/F26-cli-payout-ledger-rules/verifiers/rules-source.js +0 -56
  135. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/NOTES.md +0 -35
  136. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected-pair-plan-registry.json +0 -162
  137. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/expected.json +0 -67
  138. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/metadata.json +0 -19
  139. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/setup.sh +0 -4
  140. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/spec.md +0 -55
  141. package/benchmark/auto-resolve/fixtures/F3-backend-contract-risk/task.txt +0 -9
  142. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/NOTES.md +0 -34
  143. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/expected.json +0 -57
  144. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/metadata.json +0 -10
  145. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/setup.sh +0 -2
  146. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/spec.md +0 -67
  147. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/task.txt +0 -7
  148. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/duplicate-event-error.js +0 -35
  149. package/benchmark/auto-resolve/fixtures/F31-cli-seat-rebalance/verifiers/priority-transfer-rollback.js +0 -53
  150. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/NOTES.md +0 -38
  151. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/expected.json +0 -57
  152. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/metadata.json +0 -10
  153. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/setup.sh +0 -2
  154. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/spec.md +0 -70
  155. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/task.txt +0 -3
  156. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/duplicate-renewal-error.js +0 -42
  157. package/benchmark/auto-resolve/fixtures/F32-cli-subscription-renewal/verifiers/priority-credit-rollback.js +0 -70
  158. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/NOTES.md +0 -47
  159. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/expected.json +0 -57
  160. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/metadata.json +0 -10
  161. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/setup.sh +0 -6
  162. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/spec.md +0 -48
  163. package/benchmark/auto-resolve/fixtures/F4-web-browser-design/task.txt +0 -9
  164. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/NOTES.md +0 -45
  165. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/expected.json +0 -65
  166. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/metadata.json +0 -10
  167. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/setup.sh +0 -55
  168. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/spec.md +0 -48
  169. package/benchmark/auto-resolve/fixtures/F5-fix-loop-red-green/task.txt +0 -7
  170. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/NOTES.md +0 -43
  171. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/expected.json +0 -77
  172. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/metadata.json +0 -10
  173. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/setup.sh +0 -4
  174. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/spec.md +0 -48
  175. package/benchmark/auto-resolve/fixtures/F6-dep-audit-native-module/task.txt +0 -10
  176. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/NOTES.md +0 -57
  177. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/expected.json +0 -76
  178. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/metadata.json +0 -10
  179. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/setup.sh +0 -36
  180. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/spec.md +0 -45
  181. package/benchmark/auto-resolve/fixtures/F7-out-of-scope-trap/task.txt +0 -7
  182. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/NOTES.md +0 -53
  183. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/expected.json +0 -63
  184. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/metadata.json +0 -10
  185. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/setup.sh +0 -4
  186. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/spec.md +0 -48
  187. package/benchmark/auto-resolve/fixtures/F8-known-limit-ambiguous/task.txt +0 -1
  188. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/NOTES.md +0 -105
  189. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/expected.json +0 -74
  190. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/metadata.json +0 -10
  191. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/setup.sh +0 -28
  192. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/spec.md +0 -59
  193. package/benchmark/auto-resolve/fixtures/F9-e2e-ideate-to-resolve/task.txt +0 -5
  194. package/benchmark/auto-resolve/fixtures/SCHEMA.md +0 -188
  195. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/NOTES.md +0 -37
  196. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/RETIRED.md +0 -13
  197. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/expected.json +0 -56
  198. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/metadata.json +0 -10
  199. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/setup.sh +0 -18
  200. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/spec.md +0 -69
  201. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/task.txt +0 -7
  202. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/exact-proration.js +0 -48
  203. package/benchmark/auto-resolve/fixtures/retired/F27-cli-subscription-proration/verifiers/rules-source-and-conflict.js +0 -79
  204. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/NOTES.md +0 -54
  205. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/RETIRED.md +0 -7
  206. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/expected.json +0 -67
  207. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/metadata.json +0 -10
  208. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/setup.sh +0 -2
  209. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/spec.md +0 -67
  210. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/task.txt +0 -5
  211. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/policy-precedence.js +0 -72
  212. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-and-immutability.js +0 -43
  213. package/benchmark/auto-resolve/fixtures/retired/F28-cli-return-authorization/verifiers/validation-boundary.js +0 -116
  214. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/NOTES.md +0 -35
  215. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/RETIRED.md +0 -12
  216. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/expected.json +0 -58
  217. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/metadata.json +0 -10
  218. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/setup.sh +0 -2
  219. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/spec.md +0 -73
  220. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/task.txt +0 -17
  221. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/mixed-idempotent-settlement.js +0 -53
  222. package/benchmark/auto-resolve/fixtures/retired/F30-cli-credit-hold-settlement/verifiers/rejection-boundaries.js +0 -74
  223. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/NOTES.md +0 -28
  224. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/RETIRED.md +0 -15
  225. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/expected.json +0 -57
  226. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/metadata.json +0 -10
  227. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/setup.sh +0 -50
  228. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/spec.md +0 -59
  229. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/task.txt +0 -9
  230. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/ordered-settlement.js +0 -88
  231. package/benchmark/auto-resolve/fixtures/retired/F34-cli-settle-inflight/verifiers/reject-invalid.js +0 -45
  232. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/NOTES.md +0 -30
  233. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/RETIRED.md +0 -16
  234. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/expected.json +0 -64
  235. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/metadata.json +0 -10
  236. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/setup.sh +0 -21
  237. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/spec.md +0 -58
  238. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/task.txt +0 -9
  239. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/dup-op.js +0 -54
  240. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/priority-apply.js +0 -82
  241. package/benchmark/auto-resolve/fixtures/retired/F35-cli-apply-journal/verifiers/rollback.js +0 -77
  242. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/NOTES.md +0 -99
  243. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/RETIRED.md +0 -18
  244. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/expected.json +0 -64
  245. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/metadata.json +0 -10
  246. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/setup.sh +0 -4
  247. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/spec.md +0 -61
  248. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/task.txt +0 -9
  249. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/correctness-small.js +0 -135
  250. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/scale.js +0 -100
  251. package/benchmark/auto-resolve/fixtures/retired/F36-cli-session-admission/verifiers/validation.js +0 -75
  252. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/NOTES.md +0 -93
  253. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/RETIRED.md +0 -16
  254. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/expected.json +0 -65
  255. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/metadata.json +0 -10
  256. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/setup.sh +0 -18
  257. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/spec.md +0 -66
  258. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/task.txt +0 -9
  259. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/correctness-small.js +0 -84
  260. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/scale.js +0 -184
  261. package/benchmark/auto-resolve/fixtures/retired/F37-cli-rule-lookup/verifiers/validation.js +0 -74
  262. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/NOTES.md +0 -60
  263. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/RETIRED.md +0 -29
  264. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/expected.json +0 -73
  265. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/metadata.json +0 -10
  266. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/setup.sh +0 -28
  267. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/spec.md +0 -58
  268. package/benchmark/auto-resolve/fixtures/retired/F9-e2e-ideate-to-preflight/task.txt +0 -5
  269. package/benchmark/auto-resolve/fixtures/test-repo/README.md +0 -12
  270. package/benchmark/auto-resolve/fixtures/test-repo/bin/cli.js +0 -63
  271. package/benchmark/auto-resolve/fixtures/test-repo/package-lock.json +0 -823
  272. package/benchmark/auto-resolve/fixtures/test-repo/package.json +0 -22
  273. package/benchmark/auto-resolve/fixtures/test-repo/playwright.config.js +0 -17
  274. package/benchmark/auto-resolve/fixtures/test-repo/server/index.js +0 -37
  275. package/benchmark/auto-resolve/fixtures/test-repo/tests/cli.test.js +0 -25
  276. package/benchmark/auto-resolve/fixtures/test-repo/tests/server.test.js +0 -58
  277. package/benchmark/auto-resolve/fixtures/test-repo/web/index.html +0 -37
  278. package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.json +0 -25
  279. package/benchmark/auto-resolve/results/20260507-f10-f11-tier1-full-pipeline/headroom-gate.md +0 -10
  280. package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.json +0 -18
  281. package/benchmark/auto-resolve/results/20260508-f22-exact-error-headroom/headroom-gate.md +0 -9
  282. package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.json +0 -18
  283. package/benchmark/auto-resolve/results/20260508-f26-headroom/headroom-gate.md +0 -9
  284. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F16-cli-quote-tax-rules/l2_risk_probes/result.json +0 -28
  285. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F23-cli-fulfillment-wave/l2_risk_probes/result.json +0 -28
  286. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/F25-cli-cart-promotion-rules/l2_risk_probes/result.json +0 -30
  287. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.json +0 -82
  288. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/full-pipeline-pair-gate.md +0 -18
  289. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.json +0 -46
  290. package/benchmark/auto-resolve/results/20260510-f16-f23-f25-combined-proof/headroom-gate.md +0 -17
  291. package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.json +0 -28
  292. package/benchmark/auto-resolve/results/20260511-f12-webhook-headroom/headroom-gate.md +0 -15
  293. package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.json +0 -28
  294. package/benchmark/auto-resolve/results/20260511-f15-concurrency-headroom/headroom-gate.md +0 -15
  295. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/F21-cli-scheduler-priority/l2_risk_probes/result.json +0 -32
  296. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.json +0 -44
  297. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/full-pipeline-pair-gate.md +0 -16
  298. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.json +0 -18
  299. package/benchmark/auto-resolve/results/20260511-f21-current-riskprobes-v1/headroom-gate.md +0 -9
  300. package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.json +0 -28
  301. package/benchmark/auto-resolve/results/20260511-f3-http-error-headroom/headroom-gate.md +0 -15
  302. package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.json +0 -28
  303. package/benchmark/auto-resolve/results/20260512-f2-medium-headroom/headroom-gate.md +0 -15
  304. package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.json +0 -28
  305. package/benchmark/auto-resolve/results/20260512-f31-seat-rebalance-headroom/headroom-gate.md +0 -15
  306. package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.json +0 -28
  307. package/benchmark/auto-resolve/results/20260512-f32-subscription-renewal-headroom/headroom-gate.md +0 -15
  308. package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.json +0 -28
  309. package/benchmark/auto-resolve/results/20260512-f4-web-headroom/headroom-gate.md +0 -15
  310. package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.json +0 -28
  311. package/benchmark/auto-resolve/results/20260512-f5-fixloop-headroom/headroom-gate.md +0 -15
  312. package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.json +0 -28
  313. package/benchmark/auto-resolve/results/20260512-f6-checksum-headroom/headroom-gate.md +0 -15
  314. package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.json +0 -28
  315. package/benchmark/auto-resolve/results/20260512-f7-scope-headroom/headroom-gate.md +0 -15
  316. package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.json +0 -28
  317. package/benchmark/auto-resolve/results/20260512-f9-e2e-headroom/headroom-gate.md +0 -15
  318. package/benchmark/auto-resolve/run-real-benchmark.md +0 -303
  319. package/benchmark/auto-resolve/scripts/audit-headroom-rejections.py +0 -441
  320. package/benchmark/auto-resolve/scripts/audit-pair-evidence.py +0 -1256
  321. package/benchmark/auto-resolve/scripts/build-pair-eligible-manifest.py +0 -306
  322. package/benchmark/auto-resolve/scripts/check-f9-artifacts.py +0 -268
  323. package/benchmark/auto-resolve/scripts/collect-swebench-predictions.py +0 -108
  324. package/benchmark/auto-resolve/scripts/compile-report.py +0 -493
  325. package/benchmark/auto-resolve/scripts/fetch-swebench-instances.py +0 -129
  326. package/benchmark/auto-resolve/scripts/frozen-verify-gate.py +0 -434
  327. package/benchmark/auto-resolve/scripts/full-pipeline-pair-gate.py +0 -612
  328. package/benchmark/auto-resolve/scripts/headroom-gate.py +0 -378
  329. package/benchmark/auto-resolve/scripts/iter-0033c-compare.py +0 -683
  330. package/benchmark/auto-resolve/scripts/iter-0033c-l1-summary.py +0 -97
  331. package/benchmark/auto-resolve/scripts/judge-opus-pass.sh +0 -542
  332. package/benchmark/auto-resolve/scripts/judge.sh +0 -565
  333. package/benchmark/auto-resolve/scripts/oracle-scope-tier-a.py +0 -256
  334. package/benchmark/auto-resolve/scripts/oracle-scope-tier-b.py +0 -287
  335. package/benchmark/auto-resolve/scripts/oracle-test-fidelity.py +0 -328
  336. package/benchmark/auto-resolve/scripts/pair-candidate-frontier.py +0 -469
  337. package/benchmark/auto-resolve/scripts/pair-plan-idgen.py +0 -401
  338. package/benchmark/auto-resolve/scripts/pair-plan-lint.py +0 -475
  339. package/benchmark/auto-resolve/scripts/pair-rejected-fixtures.sh +0 -103
  340. package/benchmark/auto-resolve/scripts/pair_evidence_contract.py +0 -270
  341. package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-case.py +0 -273
  342. package/benchmark/auto-resolve/scripts/prepare-swebench-frozen-corpus.py +0 -148
  343. package/benchmark/auto-resolve/scripts/prepare-swebench-solver-worktree.py +0 -210
  344. package/benchmark/auto-resolve/scripts/recent-benchmark-summary.py +0 -232
  345. package/benchmark/auto-resolve/scripts/run-fixture.sh +0 -1024
  346. package/benchmark/auto-resolve/scripts/run-frozen-verify-pair.sh +0 -683
  347. package/benchmark/auto-resolve/scripts/run-full-pipeline-pair-candidate.sh +0 -458
  348. package/benchmark/auto-resolve/scripts/run-headroom-candidate.sh +0 -336
  349. package/benchmark/auto-resolve/scripts/run-iter-0033c.sh +0 -208
  350. package/benchmark/auto-resolve/scripts/run-suite.sh +0 -251
  351. package/benchmark/auto-resolve/scripts/run-swebench-frozen-corpus.sh +0 -310
  352. package/benchmark/auto-resolve/scripts/run-swebench-solver-batch.sh +0 -257
  353. package/benchmark/auto-resolve/scripts/ship-gate.py +0 -391
  354. package/benchmark/auto-resolve/scripts/solo-ceiling-avoidance.py +0 -53
  355. package/benchmark/auto-resolve/scripts/solo-headroom-hypothesis.py +0 -77
  356. package/benchmark/auto-resolve/scripts/swebench-frozen-matrix.py +0 -478
  357. package/benchmark/auto-resolve/scripts/test-audit-headroom-rejections.sh +0 -288
  358. package/benchmark/auto-resolve/scripts/test-audit-pair-evidence.sh +0 -1672
  359. package/benchmark/auto-resolve/scripts/test-benchmark-arg-parsing.sh +0 -1015
  360. package/benchmark/auto-resolve/scripts/test-build-pair-eligible-manifest.sh +0 -491
  361. package/benchmark/auto-resolve/scripts/test-check-f9-artifacts.sh +0 -91
  362. package/benchmark/auto-resolve/scripts/test-frozen-verify-gate.sh +0 -517
  363. package/benchmark/auto-resolve/scripts/test-full-pipeline-pair-gate.sh +0 -610
  364. package/benchmark/auto-resolve/scripts/test-headroom-gate.sh +0 -401
  365. package/benchmark/auto-resolve/scripts/test-iter-0033c-compare.sh +0 -525
  366. package/benchmark/auto-resolve/scripts/test-iter-0033c-l1-summary.sh +0 -254
  367. package/benchmark/auto-resolve/scripts/test-lint-fixtures.sh +0 -580
  368. package/benchmark/auto-resolve/scripts/test-pair-candidate-frontier.sh +0 -591
  369. package/benchmark/auto-resolve/scripts/test-run-full-pipeline-pair-candidate.sh +0 -497
  370. package/benchmark/auto-resolve/scripts/test-run-headroom-candidate.sh +0 -401
  371. package/benchmark/auto-resolve/scripts/test-run-swebench-solver-batch.sh +0 -111
  372. package/benchmark/auto-resolve/scripts/test-ship-gate.sh +0 -1189
  373. package/benchmark/auto-resolve/scripts/test-swebench-frozen-case.sh +0 -1221
  374. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/NOTES.md +0 -28
  375. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/expected.json +0 -63
  376. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/metadata.json +0 -10
  377. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/setup.sh +0 -3
  378. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/spec.md +0 -47
  379. package/benchmark/auto-resolve/shadow-fixtures/S1-cli-lang-flag/task.txt +0 -1
  380. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/NOTES.md +0 -34
  381. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/expected.json +0 -53
  382. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/metadata.json +0 -10
  383. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/setup.sh +0 -3
  384. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/spec.md +0 -50
  385. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/task.txt +0 -1
  386. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/duplicate-order-error.js +0 -27
  387. package/benchmark/auto-resolve/shadow-fixtures/S2-cli-inventory-reservation/verifiers/priority-stock-reservation.js +0 -44
  388. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/NOTES.md +0 -34
  389. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/expected.json +0 -55
  390. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/metadata.json +0 -10
  391. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/setup.sh +0 -3
  392. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/spec.md +0 -52
  393. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/task.txt +0 -1
  394. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/duplicate-ticket-error.js +0 -29
  395. package/benchmark/auto-resolve/shadow-fixtures/S3-cli-ticket-assignment/verifiers/priority-agent-assignment.js +0 -48
  396. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/NOTES.md +0 -34
  397. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/expected.json +0 -55
  398. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/metadata.json +0 -10
  399. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/setup.sh +0 -3
  400. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/spec.md +0 -55
  401. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/task.txt +0 -1
  402. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/duplicate-return-error.js +0 -43
  403. package/benchmark/auto-resolve/shadow-fixtures/S4-cli-return-routing/verifiers/priority-return-routing.js +0 -70
  404. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/NOTES.md +0 -37
  405. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/expected.json +0 -54
  406. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/metadata.json +0 -10
  407. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/setup.sh +0 -3
  408. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/spec.md +0 -59
  409. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/task.txt +0 -1
  410. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/credit-ledger-priority.js +0 -98
  411. package/benchmark/auto-resolve/shadow-fixtures/S5-cli-credit-grant-ledger/verifiers/duplicate-charge-error.js +0 -38
  412. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/NOTES.md +0 -36
  413. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/expected.json +0 -56
  414. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/metadata.json +0 -10
  415. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/setup.sh +0 -3
  416. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/spec.md +0 -59
  417. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/task.txt +0 -1
  418. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/duplicate-refund-error.js +0 -41
  419. package/benchmark/auto-resolve/shadow-fixtures/S6-cli-refund-window-ledger/verifiers/priority-refund-ledger.js +0 -65
  420. package/config/skills/_shared/pair-plan-schema.md +0 -298
  421. package/scripts/lint-fixtures.sh +0 -352
  422. package/scripts/lint-shadow-fixtures.sh +0 -58
  423. package/scripts/lint-skills.sh +0 -4265
  424. package/scripts/test-owner-phases.py +0 -193
@@ -39,6 +39,7 @@ import sys
39
39
  import tempfile
40
40
 
41
41
  VALID_VERDICTS = {"PASS", "PASS_WITH_ISSUES", "FAIL", "NEEDS_WORK", "BLOCKED"}
42
+ FINAL_VERDICTS = {"PASS", "PASS_WITH_ISSUES", "NEEDS_WORK", "BLOCKED"}
42
43
  VALID_TRIGGERS = {"build_gate", "cleanup", "verify"}
43
44
  SPAWN_TRIGGERS = VALID_TRIGGERS | {"plan"}
44
45
  PHASE_NAMES = {"plan", "probe_derive", "implement", "surface_close", "build_gate", "cleanup", "verify", "final_report"}
@@ -1682,8 +1683,109 @@ def freeze_roles(state: dict, work: pathlib.Path, default_engine: str) -> dict:
1682
1683
  return resolved
1683
1684
 
1684
1685
 
1686
+ REPAIR_PHASES = ("implement", "build_gate", "cleanup", "verify")
1687
+
1688
+
1689
+ def valid_phase_gate_progress(progress: object) -> bool:
1690
+ if not isinstance(progress, dict):
1691
+ return False
1692
+ total, current, statuses = progress.get("total"), progress.get("current"), progress.get("statuses")
1693
+ return (type(total) is int and total > 1 and type(current) is int and 1 <= current <= total
1694
+ and isinstance(statuses, list) and len(statuses) == total
1695
+ and all(status in ("PASS", "FAIL", None) for status in statuses)
1696
+ and all(status == "PASS" for status in statuses[:current - 1]))
1697
+
1698
+
1699
+ def repair_predecessor(state: dict) -> tuple[str, dict] | None:
1700
+ """Select the most recent started repair boundary by invocation identity."""
1701
+ phases = state.get("phases", {})
1702
+ selected = None
1703
+ for order, name in enumerate(REPAIR_PHASES):
1704
+ entry = phases.get(name) if isinstance(phases, dict) else None
1705
+ if not isinstance(entry, dict) or entry.get("started_at") is None:
1706
+ continue
1707
+ round_ = entry.get("round")
1708
+ if type(round_) is not int or round_ < 0:
1709
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1710
+ if selected is None or (round_, order) > selected[0]:
1711
+ selected = ((round_, order), name, entry)
1712
+ return None if selected is None else (selected[1], selected[2])
1713
+
1714
+
1715
+ def repair_admission(state: dict, phase: str, round_: int,
1716
+ triggered_by: str | None, source_phase: str | None = None) -> None:
1717
+ if phase not in REPAIR_PHASES:
1718
+ return
1719
+ predecessor = repair_predecessor(state)
1720
+ if phase == "implement":
1721
+ if predecessor is None:
1722
+ if triggered_by not in {None, "plan"}:
1723
+ raise SystemExit("BLOCKED:repair-trigger-mismatch: expected=null supplied=" + triggered_by)
1724
+ expected_round, origin = 0, None
1725
+ else:
1726
+ prior_name, prior = predecessor
1727
+ if prior.get("completed_at") is None or (source_phase is not None and prior_name != source_phase):
1728
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1729
+ verdict = prior.get("verdict")
1730
+ origin = None
1731
+ if prior_name in {"build_gate", "cleanup", "verify"}:
1732
+ valid = (verdict == "NEEDS_WORK" and isinstance(prior.get("merged"), dict)
1733
+ and prior["merged"].get("verdict") == "NEEDS_WORK") if prior_name == "verify" else verdict == "FAIL"
1734
+ if prior_name == "cleanup":
1735
+ valid = valid and orchestrator_phase(prior, "cleanup")
1736
+ if not valid:
1737
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1738
+ origin = prior_name
1739
+ elif prior_name == "implement":
1740
+ progress = prior.get("exec")
1741
+ if not valid_phase_gate_progress(progress):
1742
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1743
+ current, statuses = progress["current"], progress["statuses"]
1744
+ status = statuses[current - 1]
1745
+ if verdict == "FAIL" and status == "FAIL":
1746
+ origin = "phase_gate"
1747
+ elif verdict not in {"PASS", "PASS_WITH_ISSUES"} or current < 2 or status is not None:
1748
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1749
+ else:
1750
+ raise SystemExit("BLOCKED:repair-edge-invalid")
1751
+ expected_round = prior["round"] + 1
1752
+ expected_trigger = origin if origin in VALID_TRIGGERS else None
1753
+ if triggered_by != expected_trigger:
1754
+ supplied = triggered_by if triggered_by is not None else "null"
1755
+ expected = expected_trigger if expected_trigger is not None else "null"
1756
+ raise SystemExit(f"BLOCKED:repair-trigger-mismatch: expected={expected} supplied={supplied}")
1757
+ if round_ != expected_round:
1758
+ raise SystemExit(f"BLOCKED:implement-round-nonmonotonic: expected={expected_round} supplied={round_}")
1759
+ if origin is not None:
1760
+ rounds = state.get("rounds")
1761
+ if (not isinstance(rounds, dict) or type(rounds.get("global")) is not int
1762
+ or rounds["global"] < 0 or type(rounds.get("max_rounds")) is not int
1763
+ or rounds["max_rounds"] < 1):
1764
+ raise SystemExit("BLOCKED:rounds-malformed")
1765
+ if rounds["global"] >= rounds["max_rounds"]:
1766
+ raise SystemExit(
1767
+ f"BLOCKED:repair-budget-exhausted: global={rounds['global']} "
1768
+ f"max_rounds={rounds['max_rounds']} origin={origin}"
1769
+ )
1770
+ rounds["global"] += 1
1771
+ elif phase in {"build_gate", "cleanup", "verify"}:
1772
+ implement = (state.get("phases") or {}).get("implement")
1773
+ expected_round = implement.get("round", 0) if isinstance(implement, dict) and implement.get("started_at") else 0
1774
+ if round_ != expected_round:
1775
+ raise SystemExit(f"BLOCKED:phase-round-mismatch: phase={phase} expected={expected_round} supplied={round_}")
1776
+
1777
+
1778
+ def validate_verdict(phase: str, verdict: str | None) -> None:
1779
+ allowed = (verdict in FINAL_VERDICTS or (
1780
+ isinstance(verdict, str) and verdict.startswith("BLOCKED:") and len(verdict) > 8
1781
+ )) if phase == "final_report" else verdict in VALID_VERDICTS
1782
+ if verdict is not None and not allowed:
1783
+ raise SystemExit(f"error: invalid verdict for phases.{phase}: {verdict}")
1784
+
1785
+
1685
1786
  def do_spawn(state: dict, phase: str, round_: int, triggered_by: str | None,
1686
1787
  pre_sha: str | None, engine: str | None, model: str | None, *,
1788
+ source_phase: str | None = None,
1687
1789
  input_patch_sha256: str | None = None,
1688
1790
  prompt_sha256: str | None = None,
1689
1791
  untracked_before: list[str] | None = None,
@@ -1804,6 +1906,7 @@ def do_spawn(state: dict, phase: str, round_: int, triggered_by: str | None,
1804
1906
  raise SystemExit(
1805
1907
  f"error: phases.{phase} has an open span — complete it before respawn"
1806
1908
  )
1909
+ repair_admission(state, phase, round_, triggered_by, source_phase)
1807
1910
  append_phase_history(entry, phase)
1808
1911
  if phase in WORKER_SESSION_ARTIFACT_PHASES:
1809
1912
  entry.pop("invocation_receipt", None)
@@ -1812,7 +1915,10 @@ def do_spawn(state: dict, phase: str, round_: int, triggered_by: str | None,
1812
1915
  entry["completed_at"] = None
1813
1916
  entry["duration_ms"] = None
1814
1917
  entry["round"] = round_
1815
- entry["triggered_by"] = triggered_by
1918
+ entry["triggered_by"] = (
1919
+ "cleanup" if phase == "implement" and triggered_by is None
1920
+ and entry.get("triggered_by") == "cleanup" else triggered_by
1921
+ )
1816
1922
  entry["verdict"] = None
1817
1923
  entry["artifacts"] = {"findings_file": None, "log_file": None}
1818
1924
  entry["sub_verdicts"] = None
@@ -1881,6 +1987,7 @@ def do_complete(state: dict, phase: str, verdict: str | None,
1881
1987
  entry = phases.get(phase)
1882
1988
  if not isinstance(entry, dict) or not entry.get("started_at"):
1883
1989
  raise SystemExit(f"error: phases.{phase} was never spawned (no started_at) — cannot complete")
1990
+ validate_verdict(phase, verdict)
1884
1991
  if phase == "verify" and verdict is not None:
1885
1992
  raise SystemExit(
1886
1993
  "error: phases.verify.verdict is owned by verify-merge-findings.py "
@@ -1903,9 +2010,13 @@ def do_complete(state: dict, phase: str, verdict: str | None,
1903
2010
  ["git", "diff", "--quiet", entry["pre_sha"], "--"], cwd=work,
1904
2011
  capture_output=True, check=False,
1905
2012
  )
2013
+ indexed = subprocess.run(
2014
+ ["git", "diff", "--cached", "--quiet", entry["pre_sha"], "--"], cwd=work,
2015
+ capture_output=True, check=False,
2016
+ )
1906
2017
  head = subprocess.run(["git", "rev-parse", "HEAD"], cwd=work,
1907
2018
  capture_output=True, text=True, check=False)
1908
- if checked.returncode != 0 or head.returncode != 0 or head.stdout.strip() != post_sha:
2019
+ if checked.returncode != 0 or indexed.returncode != 0 or head.returncode != 0 or head.stdout.strip() != post_sha:
1909
2020
  raise SystemExit("BLOCKED:owner-cleanup-source-changed: return code cleanup to IMPLEMENT")
1910
2021
  checker = runpy.run_path(pathlib.Path(__file__).with_name("spec-verify-check.py"))
1911
2022
  baseline, baseline_error = checker["load_untracked_baseline"](devlyn)
@@ -1950,6 +2061,13 @@ def do_complete(state: dict, phase: str, verdict: str | None,
1950
2061
  entry["verdict"] = verdict
1951
2062
  else:
1952
2063
  raise SystemExit(f"error: --verdict is required to complete phases.{phase}")
2064
+ if phase == "implement" and verdict == "FAIL" and isinstance(entry.get("exec"), dict):
2065
+ progress = entry["exec"]
2066
+ total, current, statuses = progress.get("total"), progress.get("current"), progress.get("statuses")
2067
+ if (type(total) is not int or total <= 1 or type(current) is not int
2068
+ or not 1 <= current <= total or not isinstance(statuses, list) or len(statuses) != total):
2069
+ raise SystemExit("BLOCKED:repair-edge-invalid")
2070
+ statuses[current - 1] = "FAIL"
1953
2071
  if phase == "plan":
1954
2072
  if state.get("version") == "3.0":
1955
2073
  bind_plan_output(state, devlyn)
@@ -2100,11 +2218,23 @@ def do_transition(
2100
2218
  """
2101
2219
  if next_phase not in LEGAL_TRANSITIONS.get(phase, set()):
2102
2220
  raise SystemExit(f"error: illegal phase transition: {phase} -> {next_phase}")
2103
- if phase == "cleanup" and orchestrator_phase(state.get("phases", {}).get(phase), phase):
2104
- if next_phase == "verify" and verdict not in {"PASS", "PASS_WITH_ISSUES"}:
2221
+ validate_verdict(phase, verdict)
2222
+ if (phase == "implement" and next_phase != "implement"
2223
+ and state.get("phases", {}).get(phase, {}).get("triggered_by") == "cleanup"):
2224
+ bypasses = state.get("bypasses", [])
2225
+ required = "build_gate" if "build-gate" not in bypasses else (
2226
+ "cleanup" if "cleanup" not in bypasses else "verify"
2227
+ )
2228
+ if next_phase not in {required, "final_report"}:
2229
+ raise SystemExit(f"BLOCKED:owner-cleanup-repair-route: next phase must be {required}")
2230
+ if next_phase != "final_report" and next_round != state["phases"][phase]["round"]:
2231
+ raise SystemExit("BLOCKED:owner-cleanup-repair-route: preserve the IMPLEMENT repair round")
2232
+ if phase == "cleanup":
2233
+ owner = orchestrator_phase(state.get("phases", {}).get(phase), phase)
2234
+ if owner and next_phase == "verify" and verdict not in {"PASS", "PASS_WITH_ISSUES"}:
2105
2235
  raise SystemExit("BLOCKED:owner-cleanup-failed: repair before VERIFY")
2106
- if next_phase == "implement" and (verdict != "FAIL" or next_triggered_by != "cleanup"):
2107
- raise SystemExit("BLOCKED:owner-cleanup-failed: repair requires FAIL and cleanup trigger")
2236
+ if next_phase == "implement" and (not owner or verdict != "FAIL" or next_triggered_by != "cleanup"):
2237
+ raise SystemExit("BLOCKED:owner-cleanup-failed: repair requires owner FAIL and cleanup trigger")
2108
2238
  candidate = copy.deepcopy(state)
2109
2239
  attestation_error = do_complete(
2110
2240
  candidate, phase, verdict, post_sha, findings_file, log_file,
@@ -2117,6 +2247,7 @@ def do_transition(
2117
2247
  do_spawn(
2118
2248
  candidate, next_phase, next_round, next_triggered_by,
2119
2249
  next_pre_sha, next_engine, next_model,
2250
+ source_phase=phase,
2120
2251
  input_patch_sha256=next_input_patch_sha256,
2121
2252
  prompt_sha256=next_prompt_sha256,
2122
2253
  untracked_before=next_untracked_before,
@@ -2195,6 +2326,14 @@ def final_report_self_test() -> None:
2195
2326
  assert result.returncode == 1 and "BLOCKED:final-report-invalid" in result.stderr, (case, result)
2196
2327
  assert state_path.read_bytes() == before, case
2197
2328
  report.write_bytes(valid)
2329
+ for invalid in ("FAIL", "BLOCKED:", "unknown"):
2330
+ result = subprocess.run(command + ["complete", "--verdict", invalid,
2331
+ "--log-file", ".devlyn/final-report.md"],
2332
+ cwd=work, capture_output=True, text=True, encoding="utf-8")
2333
+ assert result.returncode == 1 and result.stderr.strip() == (
2334
+ f"error: invalid verdict for phases.final_report: {invalid}"
2335
+ ), result.stderr
2336
+ assert state_path.read_bytes() == before
2198
2337
  result = subprocess.run(command + ["complete", "--verdict", "PASS_WITH_ISSUES", "--log-file", ".devlyn/final-report.md"],
2199
2338
  cwd=work, capture_output=True, text=True, encoding="utf-8")
2200
2339
  assert result.returncode == 0, result.stderr
@@ -2240,12 +2379,142 @@ def final_report_self_test() -> None:
2240
2379
  assert (target / "pipeline.state.json").read_bytes() == completed_bytes
2241
2380
  assert not state_path.exists() and not report.exists()
2242
2381
  assert outside.read_bytes() == valid
2243
- print("PASS iter-0126 final report: 11 invalid CLI completions preserve state; exact binding/archive; 6 archive refusals preserve artifacts")
2382
+ print("PASS iter-0126 final report: 14 invalid CLI completions preserve state; exact binding/archive; 6 archive refusals preserve artifacts")
2383
+
2384
+
2385
+ def repair_admission_self_test() -> None:
2386
+ stamp = "2026-01-01T00:00:00.000Z"
2387
+ origins = {
2388
+ "build_gate": ({"started_at": stamp, "completed_at": stamp, "round": 0, "verdict": "FAIL"}, "build_gate"),
2389
+ "cleanup": ({"started_at": stamp, "completed_at": stamp, "round": 0, "verdict": "FAIL",
2390
+ "execution_kind": "orchestrator_commands"}, "cleanup"),
2391
+ "verify": ({"started_at": stamp, "completed_at": stamp, "round": 0,
2392
+ "verdict": "NEEDS_WORK", "merged": {"verdict": "NEEDS_WORK"}}, "verify"),
2393
+ "phase_gate": ({"started_at": stamp, "completed_at": stamp, "round": 0, "verdict": "FAIL",
2394
+ "exec": {"total": 2, "current": 1, "statuses": ["FAIL", None]}}, None),
2395
+ }
2396
+ for origin, (entry, trigger) in origins.items():
2397
+ source = "implement" if origin == "phase_gate" else origin
2398
+ initial = {"phases": {source: entry}, "rounds": {"global": 0, "max_rounds": 1}}
2399
+ for wrong in ((None, "plan", "verify") if trigger else ("plan", "verify")):
2400
+ if wrong == trigger:
2401
+ continue
2402
+ candidate = copy.deepcopy(initial)
2403
+ try:
2404
+ do_spawn(candidate, "implement", 1, wrong, None, None, None)
2405
+ except SystemExit as exc:
2406
+ assert "repair-trigger-mismatch" in str(exc), (origin, wrong, exc)
2407
+ else:
2408
+ raise AssertionError((origin, wrong))
2409
+ for malformed in ({"global": True, "max_rounds": 1}, {"global": -1, "max_rounds": 1},
2410
+ {"global": 0, "max_rounds": True}, {"global": 0, "max_rounds": 0}, None):
2411
+ candidate = copy.deepcopy(initial)
2412
+ candidate["rounds"] = malformed
2413
+ try:
2414
+ do_spawn(candidate, "implement", 1, trigger, None, None, None)
2415
+ except SystemExit as exc:
2416
+ assert str(exc) == "BLOCKED:rounds-malformed"
2417
+ else:
2418
+ raise AssertionError((origin, malformed))
2419
+ candidate = copy.deepcopy(initial)
2420
+ try:
2421
+ do_spawn(candidate, "implement", 2, trigger, None, None, None)
2422
+ except SystemExit as exc:
2423
+ assert "implement-round-nonmonotonic" in str(exc)
2424
+ else:
2425
+ raise AssertionError((origin, "skipped round"))
2426
+ candidate = copy.deepcopy(initial)
2427
+ do_spawn(candidate, "implement", 1, trigger, None, None, None)
2428
+ assert candidate["rounds"]["global"] == 1, origin
2429
+ try:
2430
+ do_spawn(candidate, "implement", 1, trigger, None, None, None)
2431
+ except SystemExit:
2432
+ pass
2433
+ else:
2434
+ raise AssertionError((origin, "duplicate admission"))
2435
+ exhausted = copy.deepcopy(initial)
2436
+ exhausted["rounds"]["global"] = 1
2437
+ try:
2438
+ do_spawn(exhausted, "implement", 1, trigger, None, None, None)
2439
+ except SystemExit as exc:
2440
+ assert str(exc) == f"BLOCKED:repair-budget-exhausted: global=1 max_rounds=1 origin={origin}"
2441
+ else:
2442
+ raise AssertionError((origin, "shared exhaustion"))
2443
+ phased = {"rounds": {"global": 0, "max_rounds": 1}, "phases": {}}
2444
+ do_spawn(phased, "implement", 0, None, None, None, None)
2445
+ phased["phases"]["implement"].update({"completed_at": stamp, "verdict": "PASS",
2446
+ "exec": {"total": 2, "current": 2, "statuses": ["PASS", None]}})
2447
+ do_spawn(phased, "implement", 1, None, None, None, None)
2448
+ assert phased["rounds"]["global"] == 0
2449
+ do_complete(phased, "implement", "FAIL", None, None, None, None, None)
2450
+ assert phased["phases"]["implement"]["exec"]["statuses"] == ["PASS", "FAIL"]
2451
+ do_spawn(phased, "implement", 2, None, None, None, None)
2452
+ assert phased["rounds"]["global"] == 1
2453
+ do_complete(phased, "implement", "PASS", None, None, None, None, None)
2454
+ do_spawn(phased, "verify", 2, None, None, None, None)
2455
+ with tempfile.TemporaryDirectory() as tmp:
2456
+ transition_state = {"rounds": {"global": 0, "max_rounds": 1}, "phases": {}}
2457
+ do_spawn(transition_state, "implement", 0, None, None, None, None)
2458
+ transition_state["phases"]["implement"]["exec"] = {
2459
+ "total": 2, "current": 1, "statuses": [None, None],
2460
+ }
2461
+ transitioned = do_transition(
2462
+ transition_state, "implement", "implement", "FAIL", None, None, None,
2463
+ None, None, None, pathlib.Path(tmp), 1, None, None, None, None,
2464
+ )
2465
+ assert transition_state["rounds"]["global"] == 0
2466
+ assert transitioned["rounds"]["global"] == 1
2467
+ assert transitioned["phases"]["implement"]["exec"]["statuses"][0] == "FAIL"
2468
+ print("PASS repair admission: four origins, trigger/counter/round refusal, phased invocation and last repair")
2469
+
2470
+
2471
+ def cleanup_origin_phase_gate_reentry_self_test() -> None:
2472
+ with tempfile.TemporaryDirectory() as tmp:
2473
+ stamp = "2026-01-01T00:00:00.000Z"
2474
+ state = {"version": "3.0", "rounds": {"global": 1, "max_rounds": 4}, "phases": {
2475
+ "cleanup": {"started_at": stamp, "completed_at": stamp, "round": 0,
2476
+ "verdict": "FAIL", "execution_kind": "orchestrator_commands"},
2477
+ "implement": {"started_at": stamp, "completed_at": None, "round": 1,
2478
+ "triggered_by": "cleanup", "verdict": None,
2479
+ "exec": {"total": 2, "current": 1, "statuses": [None, None]}},
2480
+ }}
2481
+ try:
2482
+ do_transition(
2483
+ state, "implement", "verify", "PASS", None, None, None,
2484
+ None, None, None, pathlib.Path(tmp), 1, None, None, None, None,
2485
+ )
2486
+ except SystemExit as exc:
2487
+ assert str(exc) == "BLOCKED:owner-cleanup-repair-route: next phase must be build_gate", exc
2488
+ else:
2489
+ raise AssertionError("cleanup repair bypassed BUILD_GATE")
2490
+ transitioned = do_transition(
2491
+ state, "implement", "implement", "FAIL", None, None, None,
2492
+ None, None, None, pathlib.Path(tmp), 2, None, None, None, None,
2493
+ )
2494
+ assert transitioned["rounds"]["global"] == 2
2495
+ assert transitioned["phases"]["implement"]["exec"]["statuses"] == ["FAIL", None]
2496
+ assert transitioned["phases"]["implement"]["triggered_by"] == "cleanup"
2497
+ try:
2498
+ do_transition(
2499
+ transitioned, "implement", "verify", "PASS", None, None, None,
2500
+ None, None, None, pathlib.Path(tmp), 2, None, None, None, None,
2501
+ )
2502
+ except SystemExit as exc:
2503
+ assert str(exc) == "BLOCKED:owner-cleanup-repair-route: next phase must be build_gate", exc
2504
+ else:
2505
+ raise AssertionError("phase-gate retry bypassed BUILD_GATE")
2506
+ spawned = copy.deepcopy(state)
2507
+ do_complete(spawned, "implement", "FAIL", None, None, None, None, None)
2508
+ do_spawn(spawned, "implement", 2, None, None, None, None)
2509
+ assert spawned["rounds"]["global"] == 2
2510
+ assert spawned["phases"]["implement"]["triggered_by"] == "cleanup"
2244
2511
 
2245
2512
 
2246
2513
  def self_test() -> int:
2247
2514
  import time
2248
2515
 
2516
+ repair_admission_self_test()
2517
+ cleanup_origin_phase_gate_reentry_self_test()
2249
2518
  final_report_self_test()
2250
2519
  with tempfile.TemporaryDirectory() as tmp:
2251
2520
  work = pathlib.Path(tmp); devlyn = work / ".devlyn"; devlyn.mkdir()
@@ -2521,7 +2790,7 @@ def self_test() -> int:
2521
2790
  assert archived_legacy == old_receipt
2522
2791
  print("PASS iter-0089 PLAN ledger: P-0089-1/2/6")
2523
2792
 
2524
- write_state(state_path, {"phases": {}})
2793
+ write_state(state_path, {"phases": {}, "rounds": {"global": 0, "max_rounds": 4}})
2525
2794
 
2526
2795
  # Round 0: spawn -> complete.
2527
2796
  state = read_state(state_path)
@@ -2541,9 +2810,14 @@ def self_test() -> int:
2541
2810
  expected_ms = round((parse_iso(entry["completed_at"]) - parse_iso(entry["started_at"])).total_seconds() * 1000)
2542
2811
  assert entry["duration_ms"] == expected_ms, (entry["duration_ms"], expected_ms)
2543
2812
 
2544
- # Round 1: fix-loop respawn — this is the literal iter-0042 regression.
2813
+ # Round 1: VERIFY finding admits a fix-loop respawn.
2545
2814
  time.sleep(0.05)
2546
2815
  state = read_state(state_path)
2816
+ do_spawn(state, "verify", 0, None, None, None, None)
2817
+ state["phases"]["verify"].update({
2818
+ "completed_at": now_iso(), "verdict": "NEEDS_WORK",
2819
+ "merged": {"verdict": "NEEDS_WORK"},
2820
+ })
2547
2821
  do_spawn(state, "implement", 1, "verify", None, None, None)
2548
2822
  write_state(state_path, state)
2549
2823
  respawned = read_state(state_path)["phases"]["implement"]
@@ -2582,6 +2856,10 @@ def self_test() -> int:
2582
2856
  evidence_spec_dir = evidence_work / "docs" / "evidence"
2583
2857
  evidence_devlyn.mkdir(parents=True)
2584
2858
  evidence_spec_dir.mkdir(parents=True)
2859
+ subprocess.run(["git", "init", "-q"], cwd=evidence_work, check=True)
2860
+ subprocess.run(["git", "config", "user.name", "Evidence test"], cwd=evidence_work, check=True)
2861
+ subprocess.run(["git", "config", "user.email", "evidence@example.invalid"], cwd=evidence_work, check=True)
2862
+ (evidence_work / ".gitignore").write_text(".devlyn/\n", encoding="utf-8")
2585
2863
  (evidence_spec_dir / "spec.md").write_text("# Evidence fixture\n", encoding="utf-8")
2586
2864
  (evidence_spec_dir / "spec.expected.json").write_text(json.dumps({
2587
2865
  "verification_commands": [{"cmd": "printf build-gate"}],
@@ -2593,9 +2871,12 @@ def self_test() -> int:
2593
2871
  "stdout_contains": ["red-before-fix"],
2594
2872
  }],
2595
2873
  }) + "\n", encoding="utf-8")
2874
+ subprocess.run(["git", "add", "."], cwd=evidence_work, check=True)
2875
+ subprocess.run(["git", "commit", "-qm", "fixture"], cwd=evidence_work, check=True)
2596
2876
  evidence_state = {
2597
2877
  "run_id": "rs-state-evidence",
2598
2878
  "source": {"type": "spec", "spec_path": "docs/evidence/spec.md"},
2879
+ "rounds": {"global": 0, "max_rounds": 4},
2599
2880
  "phases": {"implement": None, "build_gate": None},
2600
2881
  }
2601
2882
  do_spawn(evidence_state, "implement", 0, None, None, "codex", None)
@@ -2722,11 +3003,28 @@ def self_test() -> int:
2722
3003
  return build_carrier
2723
3004
 
2724
3005
  build_carrier_0 = write_build_gate_results()
2725
- completed = build_gate_state_cli("complete", "--verdict", "PASS")
3006
+ completed = build_gate_state_cli("complete", "--verdict", "FAIL")
2726
3007
  assert completed.returncode == 0, completed.stderr
2727
3008
  completed_state = read_state(evidence_state_path)
2728
3009
  assert completed_state["process_evidence"] == [carrier, build_carrier_0]
2729
3010
 
3011
+ repaired = evidence_state_cli("spawn", "--round", "1", "--triggered-by", "build_gate")
3012
+ assert repaired.returncode == 0, repaired.stderr
3013
+ captured = subprocess.run(
3014
+ [sys.executable, runner_script, "--devlyn-dir", ".devlyn", "run",
3015
+ "--phase", "implement", "--id", "red-first"],
3016
+ cwd=evidence_work, capture_output=True, text=True, encoding="utf-8",
3017
+ )
3018
+ assert captured.returncode == 0, captured.stderr
3019
+ completed = evidence_state_cli("complete", "--verdict", "PASS")
3020
+ assert completed.returncode == 0, completed.stderr
3021
+ implement_carrier_1 = read_state(evidence_state_path)["process_evidence"][-1]
3022
+ subprocess.run(["git", "commit", "--allow-empty", "-qm", "chore(pipeline): implement fix round 1"],
3023
+ cwd=evidence_work, check=True)
3024
+ (evidence_devlyn / "build_gate.findings.jsonl").write_text("", encoding="utf-8")
3025
+ durable = evidence_state_cli("durability-enforce", "--round", "1", "--origin-phase", "build_gate")
3026
+ assert durable.returncode == 0, durable.stderr
3027
+
2730
3028
  spawned = build_gate_state_cli(
2731
3029
  "spawn", "--round", "1", "--triggered-by", "build_gate",
2732
3030
  )
@@ -2734,12 +3032,12 @@ def self_test() -> int:
2734
3032
  build_carrier_1 = write_build_gate_results()
2735
3033
  transitioned = build_gate_state_cli(
2736
3034
  "transition", "--verdict", "PASS", "--next-phase", "cleanup",
2737
- "--next-round", "0", "--next-pre-sha", "test-pre-sha",
3035
+ "--next-round", "1", "--next-pre-sha", "test-pre-sha",
2738
3036
  )
2739
3037
  assert transitioned.returncode == 0, transitioned.stderr
2740
3038
  build_bound_state = read_state(evidence_state_path)
2741
3039
  assert build_bound_state["process_evidence"] == [
2742
- carrier, build_carrier_0, build_carrier_1,
3040
+ carrier, build_carrier_0, implement_carrier_1, build_carrier_1,
2743
3041
  ]
2744
3042
  assert build_bound_state["phases"]["cleanup"]["started_at"] is not None
2745
3043
  print("PASS iter-0111 BUILD_GATE evidence: completion/transition state binding")
@@ -2748,6 +3046,7 @@ def self_test() -> int:
2748
3046
  # bytes only, so a failed entry or capability denial could still be completed
2749
3047
  # with caller-supplied PASS.
2750
3048
  build_bound_state["phases"]["build_gate"] = None
3049
+ build_bound_state["phases"]["implement"]["round"] = 2
2751
3050
  do_spawn(build_bound_state, "build_gate", 2, "build_gate", None, None, None)
2752
3051
  write_state(evidence_state_path, build_bound_state)
2753
3052
  mismatch = runner.normalize_obligation({
@@ -2793,6 +3092,7 @@ def self_test() -> int:
2793
3092
  assert failed.returncode == 0, failed.stderr
2794
3093
 
2795
3094
  failed_state = read_state(evidence_state_path)
3095
+ failed_state["phases"]["implement"]["round"] = 3
2796
3096
  do_spawn(failed_state, "build_gate", 3, "build_gate", None, None, None)
2797
3097
  write_state(evidence_state_path, failed_state)
2798
3098
  denied = runner.normalize_obligation({
@@ -2905,7 +3205,7 @@ def self_test() -> int:
2905
3205
  completed = read_state(state_file)["phases"]["build_gate"]
2906
3206
  command_identity(completed)
2907
3207
  assert completed["verdict"] == "PASS" and completed["completed_at"]
2908
- result = cli("build_gate", "spawn", "--round", "1")
3208
+ result = cli("build_gate", "spawn", "--round", "0")
2909
3209
  assert result.returncode == 0, result.stderr
2910
3210
  reentered = read_state(state_file)["phases"]["build_gate"]
2911
3211
  command_identity(reentered)
@@ -2954,6 +3254,11 @@ def self_test() -> int:
2954
3254
  old = legacy_done["phases"]["build_gate"]
2955
3255
  assert "execution_kind" not in old and old["model_effective"] is None
2956
3256
  assert old["invocation_receipt"]["sandbox_network_access"] is True
3257
+ legacy_done["phases"]["implement"] = {
3258
+ "started_at": now_iso(), "completed_at": now_iso(), "round": 1,
3259
+ "verdict": "PASS",
3260
+ }
3261
+ write_state(state_file, legacy_done)
2957
3262
  result = cli("build_gate", "spawn", "--round", "1")
2958
3263
  assert result.returncode == 0, result.stderr
2959
3264
  fresh = read_state(state_file)["phases"]["build_gate"]
@@ -3237,6 +3542,10 @@ def self_test() -> int:
3237
3542
  print("PASS iter-0119 nonzero receipt persists BLOCKED before exit1; transition stays atomic")
3238
3543
 
3239
3544
  candidate = read_state(state_file)
3545
+ candidate["phases"]["implement"] = {
3546
+ "started_at": now_iso(), "completed_at": now_iso(), "round": 1,
3547
+ "verdict": "PASS",
3548
+ }
3240
3549
  do_spawn(candidate, "build_gate", 1, None, None, None, None)
3241
3550
  write_state(state_file, candidate)
3242
3551
  next_relative = runner.manifest_relative_path(candidate, "build_gate")
@@ -3258,6 +3567,10 @@ def self_test() -> int:
3258
3567
  # A post-spec denial refresh must preserve earlier failed checks.
3259
3568
  refresh_state = copy.deepcopy(fixture)
3260
3569
  refresh_state["phases"]["build_gate"] = None
3570
+ refresh_state["phases"]["implement"] = {
3571
+ "started_at": now_iso(), "completed_at": now_iso(), "round": 2,
3572
+ "verdict": "PASS",
3573
+ }
3261
3574
  do_spawn(refresh_state, "build_gate", 2, None, None, None, None, devlyn=active)
3262
3575
  write_state(state_file, refresh_state)
3263
3576
  refresh_relative = runner.manifest_relative_path(refresh_state, "build_gate")
@@ -3307,7 +3620,7 @@ def self_test() -> int:
3307
3620
  active.mkdir(parents=True)
3308
3621
  state_file = active / "pipeline.state.json"
3309
3622
  results_file = active / "spec-verify.results.json"
3310
- checker = pathlib.Path(__file__).with_name("spec-verify-check.py")
3623
+ checker = pathlib.Path(__file__).resolve().with_name("spec-verify-check.py")
3311
3624
  env = {key: value for key, value in os.environ.items() if key not in {
3312
3625
  "BENCH_WORKDIR", "SPEC_VERIFY_PHASE", "SPEC_VERIFY_FINDINGS_FILE",
3313
3626
  "SPEC_VERIFY_FINDING_PREFIX",
@@ -3327,7 +3640,10 @@ def self_test() -> int:
3327
3640
  fixture = {
3328
3641
  "version": "3.0", "run_id": "rs-preflight-build-gate",
3329
3642
  "source": {"type": "generated", "criteria_path": ".devlyn/criteria.generated.md"},
3330
- "phases": {"build_gate": None}, "process_evidence": copy.deepcopy(history) or None,
3643
+ "phases": {"build_gate": None, "implement": {
3644
+ "started_at": now_iso(), "completed_at": now_iso(),
3645
+ "round": round_, "verdict": "PASS",
3646
+ }}, "process_evidence": copy.deepcopy(history) or None,
3331
3647
  }
3332
3648
  do_spawn(fixture, "build_gate", round_, None, None, None, None)
3333
3649
  write_state(state_file, fixture)
@@ -3609,7 +3925,7 @@ def self_test() -> int:
3609
3925
  assert state_path.read_bytes() == open_next_before
3610
3926
  print("PASS self-test transition open-span guard: rejected without mutation")
3611
3927
 
3612
- # A completed FAIL round must be retained before a fix-loop respawn
3928
+ # A completed FAIL span must be retained before a same-invocation respawn
3613
3929
  # resets the live record.
3614
3930
  write_state(state_path, {"phases": {}})
3615
3931
  state = read_state(state_path)
@@ -3621,7 +3937,7 @@ def self_test() -> int:
3621
3937
  write_state(state_path, state)
3622
3938
  failed_round = read_state(state_path)["phases"]["build_gate"]
3623
3939
  state = read_state(state_path)
3624
- do_spawn(state, "build_gate", 1, "build_gate", None, None, None)
3940
+ do_spawn(state, "build_gate", 0, None, None, None, None)
3625
3941
  write_state(state_path, state)
3626
3942
  respawned_fail = read_state(state_path)["phases"]["build_gate"]
3627
3943
  assert respawned_fail["verdict"] is None
@@ -3657,7 +3973,7 @@ def self_test() -> int:
3657
3973
  assert "owned by verify-merge-findings.py" in str(e)
3658
3974
  else:
3659
3975
  raise AssertionError("complete() must reject an explicit --verdict for VERIFY")
3660
- do_spawn(state, "verify", 1, "verify", None, None, None)
3976
+ do_spawn(state, "verify", 0, None, None, None, None)
3661
3977
  assert state["phases"]["verify"]["judge_durations_ms"] is None
3662
3978
 
3663
3979
  # Non-VERIFY phases require --verdict explicitly; complete() must not
@@ -3726,21 +4042,21 @@ def self_test() -> int:
3726
4042
  assert (devlyn / "codex-primary-judge.prompt.md").exists(), "judge prompts must survive VERIFY spawn"
3727
4043
  assert (devlyn / "spec-verify.json").exists(), "non-VERIFY-round files must survive"
3728
4044
 
3729
- # Fix-loop respawn of phase-gated IMPLEMENT must preserve `exec`
4045
+ # Normal phase-gated IMPLEMENT advancement must preserve `exec`
3730
4046
  # (routing truth for large runs, state-schema.md line 55) — spawn
3731
4047
  # merges into the existing entry rather than replacing it wholesale.
3732
4048
  write_state(state_path, {"phases": {}})
3733
4049
  state = read_state(state_path)
3734
4050
  do_spawn(state, "implement", 0, None, None, "claude", None)
3735
4051
  state["phases"]["implement"]["exec"] = {
3736
- "total": 3, "current": 3, "statuses": ["PASS", "PASS", "PASS"], "commits": ["a", "b", "c"],
4052
+ "total": 3, "current": 3, "statuses": ["PASS", "PASS", None], "commits": ["a", "b", "c"],
3737
4053
  }
3738
4054
  write_state(state_path, state)
3739
4055
  state = read_state(state_path)
3740
4056
  do_complete(state, "implement", "PASS", None, None, None, None, None)
3741
4057
  write_state(state_path, state)
3742
4058
  state = read_state(state_path)
3743
- do_spawn(state, "implement", 1, "verify", None, None, None)
4059
+ do_spawn(state, "implement", 1, None, None, None, None)
3744
4060
  write_state(state_path, state)
3745
4061
  respawned_exec = read_state(state_path)["phases"]["implement"]
3746
4062
  assert respawned_exec["exec"]["current"] == 3, "spawn must not clobber unowned fields like exec"
@@ -3751,6 +4067,7 @@ def self_test() -> int:
3751
4067
  # Existing history is append-only; a respawn must not clobber prior
3752
4068
  # entries that were already preserved from older rounds.
3753
4069
  write_state(state_path, {
4070
+ "rounds": {"global": 0, "max_rounds": 4},
3754
4071
  "phases": {
3755
4072
  "implement": {
3756
4073
  "started_at": "2026-01-01T00:00:02.000Z",
@@ -3759,6 +4076,7 @@ def self_test() -> int:
3759
4076
  "round": 2,
3760
4077
  "triggered_by": "verify",
3761
4078
  "verdict": "FAIL",
4079
+ "exec": {"total": 2, "current": 2, "statuses": ["PASS", "FAIL"]},
3762
4080
  "engine": "codex",
3763
4081
  "history": [{
3764
4082
  "started_at": "2026-01-01T00:00:00.000Z",
@@ -3770,7 +4088,7 @@ def self_test() -> int:
3770
4088
  }
3771
4089
  })
3772
4090
  state = read_state(state_path)
3773
- do_spawn(state, "implement", 3, "verify", None, None, None)
4091
+ do_spawn(state, "implement", 3, None, None, None, None)
3774
4092
  write_state(state_path, state)
3775
4093
  history_preserved = read_state(state_path)["phases"]["implement"]["history"]
3776
4094
  assert len(history_preserved) == 2
@@ -4656,9 +4974,13 @@ def self_test() -> int:
4656
4974
  )
4657
4975
  assert reroute_error and "model reroute" in reroute_error
4658
4976
  assert rerouted_state["phases"]["implement"]["verdict"] == "BLOCKED"
4659
- do_spawn(rerouted_state, "implement", 1, None, None, "codex", receipt_model,
4660
- prompt_sha256=receipt_prompt_sha, devlyn=receipt_devlyn)
4661
- assert rerouted_state["phases"]["implement"]["history"][-1]["verdict"] == "BLOCKED"
4977
+ try:
4978
+ do_spawn(rerouted_state, "implement", 1, None, None, "codex", receipt_model,
4979
+ prompt_sha256=receipt_prompt_sha, devlyn=receipt_devlyn)
4980
+ except SystemExit as exc:
4981
+ assert str(exc) == "BLOCKED:repair-edge-invalid"
4982
+ else:
4983
+ raise AssertionError("BLOCKED IMPLEMENT was admitted as a product repair")
4662
4984
 
4663
4985
  plan_prompt = receipt_devlyn / "plan.prompt.0"
4664
4986
  plan_prompt.write_text("plan exactly\n", encoding="utf-8")
@@ -5144,7 +5466,10 @@ def self_test() -> int:
5144
5466
  {"a.txt": "old\n"}, "build_gate",
5145
5467
  [{"id": "BG", "file": "a.txt", "line": 8}],
5146
5468
  )
5147
- build_state["phases"]["implement"] = {"round": 1, "triggered_by": "build_gate"}
5469
+ build_state["phases"]["implement"] = {
5470
+ "started_at": now_iso(), "completed_at": now_iso(), "round": 1,
5471
+ "triggered_by": "build_gate", "verdict": "PASS",
5472
+ }
5148
5473
  write_state(build_devlyn / "pipeline.state.json", build_state)
5149
5474
  subprocess.run(
5150
5475
  [sys.executable, str(pathlib.Path(__file__).resolve()), "--devlyn-dir", ".devlyn",
@@ -5401,7 +5726,7 @@ def self_test() -> int:
5401
5726
  return 0
5402
5727
 
5403
5728
 
5404
- def main() -> int:
5729
+ def _main_unlocked() -> int:
5405
5730
  ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])
5406
5731
  ap.add_argument("--devlyn-dir", default=".devlyn")
5407
5732
  ap.add_argument("--phase", choices=sorted(PHASE_NAMES))
@@ -5421,7 +5746,7 @@ def main() -> int:
5421
5746
  spawn_p.add_argument("--model", default=None)
5422
5747
 
5423
5748
  complete_p = sub.add_parser("complete")
5424
- complete_p.add_argument("--verdict", choices=sorted(VALID_VERDICTS), default=None)
5749
+ complete_p.add_argument("--verdict", default=None)
5425
5750
  complete_p.add_argument("--post-sha", default=None)
5426
5751
  complete_p.add_argument("--findings-file", default=None)
5427
5752
  complete_p.add_argument("--log-file", default=None)
@@ -5430,7 +5755,7 @@ def main() -> int:
5430
5755
  complete_p.add_argument("--engine-session-log", default=None)
5431
5756
 
5432
5757
  transition_p = sub.add_parser("transition")
5433
- transition_p.add_argument("--verdict", choices=sorted(VALID_VERDICTS), default=None)
5758
+ transition_p.add_argument("--verdict", default=None)
5434
5759
  transition_p.add_argument("--post-sha", default=None)
5435
5760
  transition_p.add_argument("--findings-file", default=None)
5436
5761
  transition_p.add_argument("--log-file", default=None)
@@ -5556,7 +5881,8 @@ def main() -> int:
5556
5881
  spawn_phase in VALID_TRIGGERS and spawn_round >= 1
5557
5882
  and isinstance(implement, dict)
5558
5883
  and implement.get("round") == spawn_round
5559
- and (origin == spawn_phase or (origin == "cleanup" and spawn_phase == "build_gate"))
5884
+ and (origin == spawn_phase or origin == "cleanup")
5885
+ and (origin != "cleanup" or args.event == "spawn" or args.phase == "implement")
5560
5886
  )
5561
5887
  if fix_reentry:
5562
5888
  enforce_closure_durability_reentry(
@@ -5643,6 +5969,21 @@ def main() -> int:
5643
5969
  return 0
5644
5970
 
5645
5971
 
5972
+ def main() -> int:
5973
+ if "--self-test" in sys.argv[1:]:
5974
+ return _main_unlocked()
5975
+ selector = argparse.ArgumentParser(add_help=False)
5976
+ selector.add_argument("--devlyn-dir", default=".devlyn")
5977
+ known, _ = selector.parse_known_args()
5978
+ devlyn = pathlib.Path(known.devlyn_dir).resolve()
5979
+ if not devlyn.is_dir():
5980
+ sys.stderr.write(f"error: {devlyn} is not a directory\n")
5981
+ return 1
5982
+ lock = runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["file_lock"]
5983
+ with lock(devlyn / "pipeline.state.lock", blocking=True):
5984
+ return _main_unlocked()
5985
+
5986
+
5646
5987
  if __name__ == "__main__":
5647
5988
  runpy.run_path(str(pathlib.Path(__file__).with_name("platform-support.py")))["configure_utf8"]()
5648
5989
  raise SystemExit(main())