devrites 7.2.7 → 7.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 (340) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +6 -1
  3. package/docs/candidate-integrity.md +10 -0
  4. package/docs/cli.md +7 -1
  5. package/docs/command-map.md +9 -2
  6. package/docs/engine/commands.md +50 -4
  7. package/docs/orchestration.md +35 -13
  8. package/docs/skills.md +5 -0
  9. package/engine/internal/acceptance/cli.go +35 -30
  10. package/engine/internal/acceptance/preconditions.go +1 -1
  11. package/engine/internal/acceptance/run.go +138 -53
  12. package/engine/internal/acceptance/transcript.go +236 -45
  13. package/engine/internal/devritespaths/paths.go +23 -5
  14. package/engine/internal/gate/gate.go +6 -0
  15. package/engine/internal/install/apply.go +5 -0
  16. package/engine/internal/install/merge.go +48 -6
  17. package/engine/internal/install/preflight.go +21 -3
  18. package/engine/internal/install/uninstall.go +9 -10
  19. package/engine/internal/lib/apply_bundle.go +7 -1
  20. package/engine/internal/lib/candidate.go +20 -7
  21. package/engine/internal/lib/candidate_coverage.go +2 -19
  22. package/engine/internal/lib/closeout.go +22 -10
  23. package/engine/internal/lib/context.go +67 -47
  24. package/engine/internal/lib/context_io.go +171 -0
  25. package/engine/internal/lib/detect.go +8 -8
  26. package/engine/internal/lib/dispatch.go +6 -0
  27. package/engine/internal/lib/packet.go +37 -7
  28. package/engine/internal/lib/packet_freeze.go +60 -12
  29. package/engine/internal/lib/packet_integrity.go +179 -0
  30. package/engine/internal/lib/perturb.go +3 -3
  31. package/engine/internal/lib/resolve.go +10 -5
  32. package/engine/internal/lib/slicecheck.go +7 -1
  33. package/engine/internal/lib/state_edit.go +9 -2
  34. package/engine/internal/lib/taskgraph.go +20 -36
  35. package/engine/internal/lib/triggers.go +30 -31
  36. package/engine/internal/lib/workspace_pool.go +330 -0
  37. package/engine/internal/parallel/cli.go +190 -49
  38. package/engine/internal/parallel/git.go +47 -14
  39. package/engine/internal/parallel/landing.go +232 -0
  40. package/engine/internal/parallel/lease.go +97 -18
  41. package/engine/internal/parallel/ops.go +312 -155
  42. package/engine/internal/parallel/pathdisjoint.go +48 -4
  43. package/engine/internal/parallel/prune.go +133 -0
  44. package/engine/internal/parallel/repository.go +239 -0
  45. package/engine/internal/parallel/select.go +64 -42
  46. package/engine/internal/state/cursor.go +5 -5
  47. package/engine/main.go +2 -1
  48. package/pack/.claude/agents/devrites-proof-runner.md +16 -9
  49. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  50. package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  51. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  52. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  53. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  54. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +7 -3
  55. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  56. package/pack/.claude/skills/devrites-lib/reference/standards/gates.md +18 -9
  57. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  58. package/pack/.claude/skills/rite-autocomplete/SKILL.md +13 -10
  59. package/pack/.claude/skills/rite-autocomplete/reference/lookahead.md +4 -4
  60. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +13 -9
  61. package/pack/.claude/skills/rite-build/SKILL.md +13 -10
  62. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
  63. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  64. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +60 -39
  65. package/pack/.claude/skills/rite-build/reference/phase-contract.md +8 -5
  66. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -7
  67. package/pack/.claude/skills/rite-clarify/SKILL.md +2 -2
  68. package/pack/.claude/skills/rite-converge/SKILL.md +4 -4
  69. package/pack/.claude/skills/rite-define/SKILL.md +7 -3
  70. package/pack/.claude/skills/rite-define/reference/plan-template.md +4 -2
  71. package/pack/.claude/skills/rite-plan/SKILL.md +11 -8
  72. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +9 -2
  73. package/pack/.claude/skills/rite-plan/reference/slicing.md +2 -0
  74. package/pack/.claude/skills/rite-polish/SKILL.md +3 -3
  75. package/pack/.claude/skills/rite-prove/SKILL.md +9 -5
  76. package/pack/.claude/skills/rite-resolve/SKILL.md +3 -3
  77. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +2 -0
  78. package/pack/.claude/skills/rite-review/SKILL.md +4 -3
  79. package/pack/.claude/skills/rite-review/reference/feature-scoped-review.md +2 -2
  80. package/pack/.claude/skills/rite-seal/SKILL.md +1 -1
  81. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +1 -1
  82. package/pack/.claude/skills/rite-ship/SKILL.md +2 -2
  83. package/pack/.claude/skills/rite-ship/reference/git-ship.md +2 -2
  84. package/pack/.claude/skills/rite-spec/SKILL.md +8 -5
  85. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +5 -0
  86. package/pack/.claude/skills/rite-temper/SKILL.md +3 -3
  87. package/pack/.claude/skills/rite-vet/SKILL.md +9 -5
  88. package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -5
  89. package/pack/generated/claude/agents/devrites-proof-runner.md +16 -9
  90. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  91. package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  92. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  93. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  94. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  95. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +7 -3
  96. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  97. package/pack/generated/claude/skills/devrites-lib/reference/standards/gates.md +18 -9
  98. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  99. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +13 -10
  100. package/pack/generated/claude/skills/rite-autocomplete/reference/lookahead.md +4 -4
  101. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +13 -9
  102. package/pack/generated/claude/skills/rite-build/SKILL.md +13 -10
  103. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
  104. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  105. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +60 -39
  106. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +8 -5
  107. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -7
  108. package/pack/generated/claude/skills/rite-clarify/SKILL.md +2 -2
  109. package/pack/generated/claude/skills/rite-converge/SKILL.md +4 -4
  110. package/pack/generated/claude/skills/rite-define/SKILL.md +7 -3
  111. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +4 -2
  112. package/pack/generated/claude/skills/rite-plan/SKILL.md +11 -8
  113. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +9 -2
  114. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +2 -0
  115. package/pack/generated/claude/skills/rite-polish/SKILL.md +3 -3
  116. package/pack/generated/claude/skills/rite-prove/SKILL.md +9 -5
  117. package/pack/generated/claude/skills/rite-resolve/SKILL.md +3 -3
  118. package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +2 -0
  119. package/pack/generated/claude/skills/rite-review/SKILL.md +4 -3
  120. package/pack/generated/claude/skills/rite-review/reference/feature-scoped-review.md +2 -2
  121. package/pack/generated/claude/skills/rite-seal/SKILL.md +1 -1
  122. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +1 -1
  123. package/pack/generated/claude/skills/rite-ship/SKILL.md +2 -2
  124. package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +2 -2
  125. package/pack/generated/claude/skills/rite-spec/SKILL.md +8 -5
  126. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +5 -0
  127. package/pack/generated/claude/skills/rite-temper/SKILL.md +3 -3
  128. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -5
  129. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -5
  130. package/pack/generated/codex/agents/devrites-proof-runner.toml +16 -9
  131. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  132. package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  133. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  134. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  135. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  136. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +7 -3
  137. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  138. package/pack/generated/codex/skills/devrites-lib/reference/standards/gates.md +18 -9
  139. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  140. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +13 -10
  141. package/pack/generated/codex/skills/rite-autocomplete/reference/lookahead.md +4 -4
  142. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +13 -9
  143. package/pack/generated/codex/skills/rite-build/SKILL.md +13 -10
  144. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
  145. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  146. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +60 -39
  147. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +8 -5
  148. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -7
  149. package/pack/generated/codex/skills/rite-clarify/SKILL.md +2 -2
  150. package/pack/generated/codex/skills/rite-converge/SKILL.md +4 -4
  151. package/pack/generated/codex/skills/rite-define/SKILL.md +7 -3
  152. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +4 -2
  153. package/pack/generated/codex/skills/rite-plan/SKILL.md +11 -8
  154. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +9 -2
  155. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +2 -0
  156. package/pack/generated/codex/skills/rite-polish/SKILL.md +3 -3
  157. package/pack/generated/codex/skills/rite-prove/SKILL.md +9 -5
  158. package/pack/generated/codex/skills/rite-resolve/SKILL.md +3 -3
  159. package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +2 -0
  160. package/pack/generated/codex/skills/rite-review/SKILL.md +4 -3
  161. package/pack/generated/codex/skills/rite-review/reference/feature-scoped-review.md +2 -2
  162. package/pack/generated/codex/skills/rite-seal/SKILL.md +1 -1
  163. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +1 -1
  164. package/pack/generated/codex/skills/rite-ship/SKILL.md +2 -2
  165. package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +2 -2
  166. package/pack/generated/codex/skills/rite-spec/SKILL.md +8 -5
  167. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +5 -0
  168. package/pack/generated/codex/skills/rite-temper/SKILL.md +3 -3
  169. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -5
  170. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -5
  171. package/pack/generated/devin/agents/devrites-proof-runner.md +16 -9
  172. package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
  173. package/pack/generated/devin/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  174. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  175. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  176. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  177. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +7 -3
  178. package/pack/generated/devin/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  179. package/pack/generated/devin/skills/devrites-lib/reference/standards/gates.md +18 -9
  180. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  181. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +13 -10
  182. package/pack/generated/devin/skills/rite-autocomplete/reference/lookahead.md +4 -4
  183. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +13 -9
  184. package/pack/generated/devin/skills/rite-build/SKILL.md +13 -10
  185. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +1 -1
  186. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  187. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +60 -39
  188. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +8 -5
  189. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +7 -7
  190. package/pack/generated/devin/skills/rite-clarify/SKILL.md +2 -2
  191. package/pack/generated/devin/skills/rite-converge/SKILL.md +4 -4
  192. package/pack/generated/devin/skills/rite-define/SKILL.md +7 -3
  193. package/pack/generated/devin/skills/rite-define/reference/plan-template.md +4 -2
  194. package/pack/generated/devin/skills/rite-plan/SKILL.md +11 -8
  195. package/pack/generated/devin/skills/rite-plan/reference/dependency-graph.md +9 -2
  196. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +2 -0
  197. package/pack/generated/devin/skills/rite-polish/SKILL.md +3 -3
  198. package/pack/generated/devin/skills/rite-prove/SKILL.md +9 -5
  199. package/pack/generated/devin/skills/rite-resolve/SKILL.md +3 -3
  200. package/pack/generated/devin/skills/rite-resolve/reference/answer-protocol.md +2 -0
  201. package/pack/generated/devin/skills/rite-review/SKILL.md +4 -3
  202. package/pack/generated/devin/skills/rite-review/reference/feature-scoped-review.md +2 -2
  203. package/pack/generated/devin/skills/rite-seal/SKILL.md +1 -1
  204. package/pack/generated/devin/skills/rite-seal/reference/phase-contract.md +1 -1
  205. package/pack/generated/devin/skills/rite-ship/SKILL.md +2 -2
  206. package/pack/generated/devin/skills/rite-ship/reference/git-ship.md +2 -2
  207. package/pack/generated/devin/skills/rite-spec/SKILL.md +8 -5
  208. package/pack/generated/devin/skills/rite-spec/reference/state-workspace.md +5 -0
  209. package/pack/generated/devin/skills/rite-temper/SKILL.md +3 -3
  210. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -5
  211. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +12 -5
  212. package/pack/generated/dsh/agents/devrites-proof-runner.md +16 -9
  213. package/pack/generated/dsh/runtime/cordis.patch.yml +1 -1
  214. package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
  215. package/pack/generated/dsh/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  216. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  217. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  218. package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  219. package/pack/generated/dsh/skills/devrites-lib/reference/standards/agents.md +7 -3
  220. package/pack/generated/dsh/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  221. package/pack/generated/dsh/skills/devrites-lib/reference/standards/gates.md +18 -9
  222. package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  223. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +13 -10
  224. package/pack/generated/dsh/skills/rite-autocomplete/reference/lookahead.md +4 -4
  225. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +13 -9
  226. package/pack/generated/dsh/skills/rite-build/SKILL.md +13 -10
  227. package/pack/generated/dsh/skills/rite-build/reference/afk-discipline.md +1 -1
  228. package/pack/generated/dsh/skills/rite-build/reference/checkpoint.md +1 -1
  229. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +60 -39
  230. package/pack/generated/dsh/skills/rite-build/reference/phase-contract.md +8 -5
  231. package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +7 -7
  232. package/pack/generated/dsh/skills/rite-clarify/SKILL.md +2 -2
  233. package/pack/generated/dsh/skills/rite-converge/SKILL.md +4 -4
  234. package/pack/generated/dsh/skills/rite-define/SKILL.md +7 -3
  235. package/pack/generated/dsh/skills/rite-define/reference/plan-template.md +4 -2
  236. package/pack/generated/dsh/skills/rite-plan/SKILL.md +11 -8
  237. package/pack/generated/dsh/skills/rite-plan/reference/dependency-graph.md +9 -2
  238. package/pack/generated/dsh/skills/rite-plan/reference/slicing.md +2 -0
  239. package/pack/generated/dsh/skills/rite-polish/SKILL.md +3 -3
  240. package/pack/generated/dsh/skills/rite-prove/SKILL.md +9 -5
  241. package/pack/generated/dsh/skills/rite-resolve/SKILL.md +3 -3
  242. package/pack/generated/dsh/skills/rite-resolve/reference/answer-protocol.md +2 -0
  243. package/pack/generated/dsh/skills/rite-review/SKILL.md +4 -3
  244. package/pack/generated/dsh/skills/rite-review/reference/feature-scoped-review.md +2 -2
  245. package/pack/generated/dsh/skills/rite-seal/SKILL.md +1 -1
  246. package/pack/generated/dsh/skills/rite-seal/reference/phase-contract.md +1 -1
  247. package/pack/generated/dsh/skills/rite-ship/SKILL.md +2 -2
  248. package/pack/generated/dsh/skills/rite-ship/reference/git-ship.md +2 -2
  249. package/pack/generated/dsh/skills/rite-spec/SKILL.md +8 -5
  250. package/pack/generated/dsh/skills/rite-spec/reference/state-workspace.md +5 -0
  251. package/pack/generated/dsh/skills/rite-temper/SKILL.md +3 -3
  252. package/pack/generated/dsh/skills/rite-vet/SKILL.md +9 -5
  253. package/pack/generated/dsh/skills/rite-vet/reference/review-axes.md +12 -5
  254. package/pack/generated/omp/agents/devrites-proof-runner.md +16 -9
  255. package/pack/generated/omp/commands/rite-build.md +1 -1
  256. package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
  257. package/pack/generated/omp/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  258. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  259. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  260. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  261. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +11 -3
  262. package/pack/generated/omp/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  263. package/pack/generated/omp/skills/devrites-lib/reference/standards/gates.md +18 -9
  264. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  265. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +13 -10
  266. package/pack/generated/omp/skills/rite-autocomplete/reference/lookahead.md +4 -4
  267. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +13 -9
  268. package/pack/generated/omp/skills/rite-build/SKILL.md +13 -10
  269. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
  270. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  271. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +60 -39
  272. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +8 -5
  273. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +7 -7
  274. package/pack/generated/omp/skills/rite-clarify/SKILL.md +2 -2
  275. package/pack/generated/omp/skills/rite-converge/SKILL.md +4 -4
  276. package/pack/generated/omp/skills/rite-define/SKILL.md +7 -3
  277. package/pack/generated/omp/skills/rite-define/reference/plan-template.md +4 -2
  278. package/pack/generated/omp/skills/rite-plan/SKILL.md +11 -8
  279. package/pack/generated/omp/skills/rite-plan/reference/dependency-graph.md +9 -2
  280. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +2 -0
  281. package/pack/generated/omp/skills/rite-polish/SKILL.md +3 -3
  282. package/pack/generated/omp/skills/rite-prove/SKILL.md +9 -5
  283. package/pack/generated/omp/skills/rite-resolve/SKILL.md +3 -3
  284. package/pack/generated/omp/skills/rite-resolve/reference/answer-protocol.md +2 -0
  285. package/pack/generated/omp/skills/rite-review/SKILL.md +4 -3
  286. package/pack/generated/omp/skills/rite-review/reference/feature-scoped-review.md +2 -2
  287. package/pack/generated/omp/skills/rite-seal/SKILL.md +1 -1
  288. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +1 -1
  289. package/pack/generated/omp/skills/rite-ship/SKILL.md +2 -2
  290. package/pack/generated/omp/skills/rite-ship/reference/git-ship.md +2 -2
  291. package/pack/generated/omp/skills/rite-spec/SKILL.md +8 -5
  292. package/pack/generated/omp/skills/rite-spec/reference/state-workspace.md +5 -0
  293. package/pack/generated/omp/skills/rite-temper/SKILL.md +3 -3
  294. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -5
  295. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +12 -5
  296. package/pack/generated/pi/agents/devrites-proof-runner.md +16 -9
  297. package/pack/generated/pi/prompts/rite-build.md +1 -1
  298. package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
  299. package/pack/generated/pi/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  300. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  301. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  302. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  303. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +7 -3
  304. package/pack/generated/pi/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  305. package/pack/generated/pi/skills/devrites-lib/reference/standards/gates.md +18 -9
  306. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  307. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +13 -10
  308. package/pack/generated/pi/skills/rite-autocomplete/reference/lookahead.md +4 -4
  309. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +13 -9
  310. package/pack/generated/pi/skills/rite-build/SKILL.md +13 -10
  311. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +1 -1
  312. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  313. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +60 -39
  314. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +8 -5
  315. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +7 -7
  316. package/pack/generated/pi/skills/rite-clarify/SKILL.md +2 -2
  317. package/pack/generated/pi/skills/rite-converge/SKILL.md +4 -4
  318. package/pack/generated/pi/skills/rite-define/SKILL.md +7 -3
  319. package/pack/generated/pi/skills/rite-define/reference/plan-template.md +4 -2
  320. package/pack/generated/pi/skills/rite-plan/SKILL.md +11 -8
  321. package/pack/generated/pi/skills/rite-plan/reference/dependency-graph.md +9 -2
  322. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +2 -0
  323. package/pack/generated/pi/skills/rite-polish/SKILL.md +3 -3
  324. package/pack/generated/pi/skills/rite-prove/SKILL.md +9 -5
  325. package/pack/generated/pi/skills/rite-resolve/SKILL.md +3 -3
  326. package/pack/generated/pi/skills/rite-resolve/reference/answer-protocol.md +2 -0
  327. package/pack/generated/pi/skills/rite-review/SKILL.md +4 -3
  328. package/pack/generated/pi/skills/rite-review/reference/feature-scoped-review.md +2 -2
  329. package/pack/generated/pi/skills/rite-seal/SKILL.md +1 -1
  330. package/pack/generated/pi/skills/rite-seal/reference/phase-contract.md +1 -1
  331. package/pack/generated/pi/skills/rite-ship/SKILL.md +2 -2
  332. package/pack/generated/pi/skills/rite-ship/reference/git-ship.md +2 -2
  333. package/pack/generated/pi/skills/rite-spec/SKILL.md +8 -5
  334. package/pack/generated/pi/skills/rite-spec/reference/state-workspace.md +5 -0
  335. package/pack/generated/pi/skills/rite-temper/SKILL.md +3 -3
  336. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -5
  337. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +12 -5
  338. package/package.json +1 -1
  339. package/scripts/build-host-artifacts.sh +5 -6
  340. package/scripts/omp-generate.sh +24 -14
@@ -0,0 +1,171 @@
1
+ package lib
2
+
3
+ import (
4
+ "bytes"
5
+ "crypto/sha256"
6
+ "fmt"
7
+ "io"
8
+ "os"
9
+ "path/filepath"
10
+ "strings"
11
+
12
+ "github.com/devrites/devrites/internal/fsutil"
13
+ )
14
+
15
+ type contextSource struct {
16
+ data []byte
17
+ err error
18
+ }
19
+
20
+ // contextSources retains one invocation's bytes, including optional absence.
21
+ type contextSources struct {
22
+ dir string
23
+ files map[string]contextSource
24
+ }
25
+
26
+ func (s *contextSources) read(name string) ([]byte, error) {
27
+ if s.dir == "" {
28
+ return nil, os.ErrNotExist
29
+ }
30
+ name = filepath.Clean(filepath.FromSlash(name))
31
+ if !filepath.IsLocal(name) {
32
+ return nil, fmt.Errorf("workspace artifact %q escapes workspace", name)
33
+ }
34
+ if source, ok := s.files[name]; ok {
35
+ return source.data, source.err
36
+ }
37
+ var source contextSource
38
+ if source.err = contextParents(s.dir, name); source.err == nil {
39
+ source.data, source.err = readBoundedRegularFile(filepath.Join(s.dir, name), workspaceArtifactLimit)
40
+ }
41
+ if source.err != nil {
42
+ source.err = fmt.Errorf("workspace artifact %s: %w", name, source.err)
43
+ }
44
+ if s.files == nil {
45
+ s.files = make(map[string]contextSource)
46
+ }
47
+ s.files[name] = source
48
+ return source.data, source.err
49
+ }
50
+
51
+ // Reject even in-workspace parent links: managed paths name actual directories.
52
+ func contextParents(dir, rel string) error {
53
+ parent := filepath.Dir(rel)
54
+ if parent == "." {
55
+ return nil
56
+ }
57
+ for _, part := range strings.Split(parent, string(filepath.Separator)) {
58
+ dir = filepath.Join(dir, part)
59
+ info, err := os.Lstat(dir)
60
+ if os.IsNotExist(err) {
61
+ return nil
62
+ }
63
+ if err != nil {
64
+ return err
65
+ }
66
+ if !info.IsDir() {
67
+ return fmt.Errorf("context parent %s is not a directory (symlinks are not allowed)", dir)
68
+ }
69
+ }
70
+ return nil
71
+ }
72
+
73
+ func publishContext(featureDir string, opts contextOpts, body []byte) (out string, unchanged bool, err error) {
74
+ out = opts.out
75
+ managed := featureDir != "" && (out == "" || !filepath.IsAbs(out))
76
+ if out == "" {
77
+ out = filepath.Join("ctx", bundleName(opts))
78
+ }
79
+ base, rel := featureDir, out
80
+ if !managed && featureDir != "" {
81
+ absoluteFeature, absErr := filepath.Abs(featureDir)
82
+ if absErr != nil {
83
+ return out, false, absErr
84
+ }
85
+ if within, relErr := filepath.Rel(absoluteFeature, out); relErr == nil && filepath.IsLocal(within) {
86
+ managed, rel = true, within
87
+ }
88
+ }
89
+ if managed {
90
+ if !filepath.IsLocal(rel) {
91
+ return out, false, fmt.Errorf("context output %q escapes workspace", out)
92
+ }
93
+ if err := contextParents(base, rel); err != nil {
94
+ return out, false, err
95
+ }
96
+ out = filepath.Join(base, rel)
97
+ } else {
98
+ base, rel = filepath.Dir(out), filepath.Base(out)
99
+ if err := os.MkdirAll(base, 0o755); err != nil {
100
+ return out, false, err
101
+ }
102
+ }
103
+ ws, err := os.OpenRoot(base)
104
+ if err != nil {
105
+ return out, false, err
106
+ }
107
+ defer func() { _ = ws.Close() }()
108
+ if err := ws.MkdirAll(filepath.Dir(rel), 0o755); err != nil {
109
+ return out, false, err
110
+ }
111
+ digest := []byte(fmt.Sprintf("%x\n", sha256.Sum256(body)))
112
+ // Check both destinations before publishing either; never follow a leaf link.
113
+ matches := true
114
+ for _, entry := range []struct {
115
+ name string
116
+ data []byte
117
+ }{{rel, body}, {rel + ".sha256", digest}} {
118
+ info, err := ws.Lstat(entry.name)
119
+ if os.IsNotExist(err) {
120
+ matches = false
121
+ continue
122
+ }
123
+ if err != nil {
124
+ return out, false, err
125
+ }
126
+ if !info.Mode().IsRegular() {
127
+ return out, false, fmt.Errorf("context output %s is not a regular file", entry.name)
128
+ }
129
+ // shortcut: Windows FileMode cannot prove POSIX 0600; rebuild until ACL-aware reuse is needed.
130
+ if info.Mode() != 0o600 || info.Size() != int64(len(entry.data)) {
131
+ matches = false
132
+ continue
133
+ }
134
+ // Open through the confined root; replacement still uses an atomic rename.
135
+ file, err := ws.Open(entry.name)
136
+ if err != nil {
137
+ return out, false, err
138
+ }
139
+ opened, err := file.Stat()
140
+ if err == nil && (opened.Mode() != 0o600 || !os.SameFile(info, opened)) {
141
+ err = fmt.Errorf("context output %s changed while opening", entry.name)
142
+ }
143
+ var data []byte
144
+ if err == nil {
145
+ data, err = io.ReadAll(io.LimitReader(file, int64(len(entry.data))+1))
146
+ }
147
+ if err == nil {
148
+ current, statErr := ws.Lstat(entry.name)
149
+ if statErr != nil {
150
+ err = statErr
151
+ } else if current.Mode() != 0o600 || !os.SameFile(opened, current) {
152
+ err = fmt.Errorf("context output %s changed while reading", entry.name)
153
+ }
154
+ }
155
+ _ = file.Close()
156
+ if err != nil {
157
+ return out, false, err
158
+ }
159
+ matches = matches && bytes.Equal(data, entry.data)
160
+ }
161
+ if matches {
162
+ return out, true, nil
163
+ }
164
+ if err := fsutil.WriteFileAtomicIn(ws, rel, body, 0o600); err != nil {
165
+ return out, false, err
166
+ }
167
+ if err := fsutil.WriteFileAtomicIn(ws, rel+".sha256", digest, 0o600); err != nil {
168
+ return out, false, err
169
+ }
170
+ return out, false, nil
171
+ }
@@ -251,27 +251,27 @@ func manifestFallback(root string, slot string) (detectedCommand, bool) {
251
251
  }
252
252
  manifests := []manifest{
253
253
  {"go.mod", "go.mod", map[string]string{
254
- "unit": "go test ./...", "integration": "go test -tags=integration ./...",
254
+ "unit": "go test ./...",
255
255
  "lint": "golangci-lint run", "vet": "go vet ./...", "build": "go build ./...",
256
256
  }},
257
257
  {"Cargo.toml", "Cargo.toml", map[string]string{
258
- "unit": "cargo test", "integration": "cargo test -- --ignored",
258
+ "unit": "cargo test",
259
259
  "lint": "cargo clippy --all-targets", "vet": "cargo check", "build": "cargo build",
260
260
  }},
261
261
  {"pyproject.toml", "pyproject.toml", map[string]string{
262
- "unit": "python -m pytest", "integration": "python -m pytest -m integration",
262
+ "unit": "python -m pytest",
263
263
  "lint": "ruff check", "vet": "mypy .", "build": "python -m build",
264
264
  }},
265
265
  {"setup.py", "setup.py", map[string]string{
266
- "unit": "python -m pytest", "integration": "python -m pytest -m integration",
266
+ "unit": "python -m pytest",
267
267
  "lint": "flake8", "vet": "mypy .",
268
268
  }},
269
269
  {"pom.xml", "pom.xml", map[string]string{
270
- "unit": "mvn test", "integration": "mvn verify",
271
- "vet": "mvn -q compile", "build": "mvn package",
270
+ "unit": "mvn test",
271
+ "vet": "mvn -q compile", "build": "mvn package",
272
272
  }},
273
273
  {"build.gradle", "build.gradle", map[string]string{
274
- "unit": "gradle test", "integration": "gradle integrationTest",
274
+ "unit": "gradle test",
275
275
  "lint": "gradle check", "build": "gradle build",
276
276
  }},
277
277
  {"composer.json", "composer.json", map[string]string{
@@ -279,7 +279,7 @@ func manifestFallback(root string, slot string) (detectedCommand, bool) {
279
279
  "vet": "vendor/bin/phpstan analyse",
280
280
  }},
281
281
  {"Gemfile", "Gemfile", map[string]string{
282
- "unit": "bundle exec rspec", "integration": "bundle exec rspec --tag integration",
282
+ "unit": "bundle exec rspec",
283
283
  "lint": "bundle exec rubocop",
284
284
  }},
285
285
  {"mix.exs", "mix.exs", map[string]string{
@@ -50,6 +50,7 @@ For a reviewer role, return --result <file> stores the return verbatim as
50
50
  packets/<wave>-<role>.return.md (never overwritten), derives the outcome
51
51
  from its Outcome/Counts head (malformed records gap), refuses an --outcome
52
52
  that disagrees, and records its Finding: keys for metrics summary.
53
+ Reviewer returns require --result or --outcome; omitting both leaves the role pending.
53
54
 
54
55
  A build wave with a wright role must name its slice(s) with --slice; a vet
55
56
  wave may (slice-bound recheck), and so may a reviewer-only wave (uncapped),
@@ -324,6 +325,8 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
324
325
  if err := lintReviewCandidate(root, slug, packet); err != nil {
325
326
  return fail("BLOCKED: %v", err)
326
327
  }
328
+ } else if err := lintPacketDiffs(root, slug, packet, false); err != nil {
329
+ return fail("BLOCKED: %v", err)
327
330
  }
328
331
  break
329
332
  }
@@ -455,6 +458,9 @@ func dispatchLocked(root, slug, sub, phase, wave, role, handle, reason string, r
455
458
  if r.Returned {
456
459
  return fail("role %q already returned", role)
457
460
  }
461
+ if isReviewerRole(role) && result == "" && outcome == "" {
462
+ return fail("reviewer %q needs --result <file> or --outcome <findings|no-findings|gap>", role)
463
+ }
458
464
  if outcome != "" && !slices.Contains([]string{"findings", "no-findings", "gap"}, outcome) {
459
465
  return fail("--outcome %q is not findings, no-findings, or gap", outcome)
460
466
  }
@@ -91,6 +91,21 @@ func RunPacket(root string, args []string, stdout, stderr io.Writer) int {
91
91
  if err != nil {
92
92
  return fail(err)
93
93
  }
94
+ code := 0
95
+ if err := state.WithFeatureLock(root, slug, func() error {
96
+ code = renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks, focus, recheck, wright, doubt, stdout, stderr)
97
+ return nil
98
+ }); err != nil {
99
+ return fail(err)
100
+ }
101
+ return code
102
+ }
103
+
104
+ func renderPacketLocked(root, slug, workspace, wave, phase, role, slice, checks, focus, recheck string, wright, doubt bool, stdout, stderr io.Writer) int {
105
+ fail := func(err error) int {
106
+ fmt.Fprintf(stderr, "packet: %v\n", err)
107
+ return 3
108
+ }
94
109
  rel := workspaceRel(root, workspace)
95
110
  bundle := "ctx/" + bundleName(contextOpts{phase: phase, role: role, slice: slice, recheck: recheck})
96
111
  if info, err := os.Stat(filepath.Join(workspace, filepath.FromSlash(bundle))); err != nil || !info.Mode().IsRegular() {
@@ -188,16 +203,24 @@ func RunPacket(root string, args []string, stdout, stderr io.Writer) int {
188
203
  return fail(err)
189
204
  }
190
205
  // Lint a sibling draft so a refused re-render keeps the packet already on disk.
191
- draft := filepath.Join(workspace, "packets", "."+name+".draft")
192
- if err := state.AtomicWrite(draft, []byte(b.String()), 0o600); err != nil {
206
+ draftFile, err := os.CreateTemp(filepath.Dir(out), "."+name+".draft-*")
207
+ if err != nil {
208
+ return fail(err)
209
+ }
210
+ draft := draftFile.Name()
211
+ defer func() { _ = os.Remove(draft) }()
212
+ _, writeErr := io.WriteString(draftFile, b.String())
213
+ if writeErr == nil {
214
+ writeErr = draftFile.Sync()
215
+ }
216
+ closeErr := draftFile.Close()
217
+ if err := errors.Join(writeErr, closeErr); err != nil {
193
218
  return fail(err)
194
219
  }
195
220
  if err := lintRenderedPacket(root, slug, draft, role, wright, finalPhase); err != nil {
196
- _ = os.Remove(draft)
197
221
  return fail(fmt.Errorf("BLOCKED: %w", err))
198
222
  }
199
223
  if err := os.Rename(draft, out); err != nil {
200
- _ = os.Remove(draft)
201
224
  return fail(err)
202
225
  }
203
226
  fmt.Fprintf(stdout, "packet: %s/packets/%s (%s, %d bytes)\n", rel, name, role, b.Len())
@@ -216,7 +239,7 @@ func lintRenderedPacket(root, slug, path, role string, wright, finalPhase bool)
216
239
  if finalPhase {
217
240
  return lintReviewCandidate(root, slug, path)
218
241
  }
219
- return nil
242
+ return lintPacketDiffs(root, slug, path, false)
220
243
  }
221
244
 
222
245
  // frozenCandidate returns packets/<wave>.sha256 when candidate freeze wrote
@@ -231,8 +254,15 @@ func frozenCandidate(workspace, wave string) (sums string, partial bool, err err
231
254
  if err != nil {
232
255
  return "", false, fmt.Errorf("packets/%s.sha256: %w", wave, err)
233
256
  }
234
- if info, err := os.Stat(filepath.Join(workspace, "packets", wave+".diff")); err != nil || !info.Mode().IsRegular() || info.Size() == 0 {
235
- return "", false, fmt.Errorf("packets/%s.sha256 has no non-empty packets/%s.diff beside it; rerun candidate freeze under a new wave", wave, wave)
257
+ bindings, err := frozenDiffBindings(raw)
258
+ if err != nil || len(bindings) != 1 || bindings[0].name != "packets/"+wave+".diff" {
259
+ return "", false, fmt.Errorf("packets/%s.sha256 must bind exactly packets/%s.diff with a diff SHA256 checksum", wave, wave)
260
+ }
261
+ if _, _, err := candidatePathInfo(workspace, "packets/"+wave+".diff"); err != nil {
262
+ return "", false, err
263
+ }
264
+ if err := verifyFrozenDiff(filepath.Join(workspace, "packets", wave+".diff"), bindings[0].digest); err != nil {
265
+ return "", false, fmt.Errorf("packets/%s.diff: %w", wave, err)
236
266
  }
237
267
  if len(raw) <= maxContractBytes/2 {
238
268
  return string(raw), false, nil
@@ -1,6 +1,7 @@
1
1
  package lib
2
2
 
3
3
  import (
4
+ "bytes"
4
5
  "crypto/sha256"
5
6
  "errors"
6
7
  "fmt"
@@ -80,6 +81,22 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
80
81
  if err != nil {
81
82
  return fail(err)
82
83
  }
84
+ code := 0
85
+ if err := state.WithFeatureLock(root, slug, func() error {
86
+ code = freezeCandidateLocked(root, slug, workspace, wave, slice, base, worktree, paths, stdout, stderr)
87
+ return nil
88
+ }); err != nil {
89
+ return fail(err)
90
+ }
91
+ return code
92
+ }
93
+
94
+ func freezeCandidateLocked(root, slug, workspace, wave, slice, base, worktree string, paths []string, stdout, stderr io.Writer) int {
95
+ fail := func(err error) int {
96
+ fmt.Fprintf(stderr, "candidate freeze: %v\n", err)
97
+ return 3
98
+ }
99
+ var err error
83
100
  if slice != "" {
84
101
  if paths, err = slicePaths(root, slug, slice); err != nil {
85
102
  return fail(err)
@@ -98,13 +115,10 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
98
115
  if err := validateCandidateRows(project, rows); err != nil {
99
116
  return fail(err)
100
117
  }
101
- diff, baseCommit, err := freezeDiff(project, base, rows)
118
+ digest, err := hashCandidate(project, rows, true)
102
119
  if err != nil {
103
120
  return fail(err)
104
121
  }
105
- if len(diff) == 0 {
106
- return fail(errors.New("the path set has no change against the base; there is no candidate to freeze"))
107
- }
108
122
  head := ""
109
123
  if worktree != "" {
110
124
  // Review binds to the transfer commit, so the frozen tree must be exactly it.
@@ -115,24 +129,27 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
115
129
  if len(dirty) != 0 {
116
130
  return fail(errors.New("the worktree has uncommitted changes on the path set; the frozen candidate must be its HEAD transfer commit"))
117
131
  }
118
- head = "head " + commit + "\n"
132
+ head = commit
119
133
  }
120
- digest, err := hashCandidate(project, rows)
134
+ diff, baseCommit, err := freezeDiff(project, base, rows)
121
135
  if err != nil {
122
136
  return fail(err)
123
137
  }
138
+ if len(diff) == 0 {
139
+ return fail(errors.New("the path set has no change against the base; there is no candidate to freeze"))
140
+ }
124
141
  var sums strings.Builder
125
- fmt.Fprintf(&sums, "%s %s\nbase %s\n%sdiff %x packets/%s.diff\n", candidateDigestDomain, candidateDigestVersion, baseCommit, head, sha256.Sum256(diff), wave)
142
+ fmt.Fprintf(&sums, "%s %s\nbase %s\n", candidateDigestDomain, candidateDigestVersion, baseCommit)
143
+ if head != "" {
144
+ fmt.Fprintf(&sums, "head %s\n", head)
145
+ }
146
+ fmt.Fprintf(&sums, "diff %x packets/%s.diff\n", sha256.Sum256(diff), wave)
126
147
  for _, row := range rows {
127
148
  if row.state != "present" {
128
149
  fmt.Fprintf(&sums, "file absent %s\n", row.path)
129
150
  continue
130
151
  }
131
- file := sha256.New()
132
- if err := hashCandidateFile(file, project, row); err != nil {
133
- return fail(fmt.Errorf("candidate %q: %w", row.path, err))
134
- }
135
- fmt.Fprintf(&sums, "file %x %s\n", file.Sum(nil), row.path)
152
+ fmt.Fprintf(&sums, "file %s %s\n", row.fileDigest, row.path)
136
153
  }
137
154
  fmt.Fprintf(&sums, "aggregate %s\n", digest)
138
155
 
@@ -143,6 +160,9 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
143
160
  } else if err != nil && !errors.Is(err, os.ErrNotExist) {
144
161
  return fail(err)
145
162
  }
163
+ if err := revalidateFrozenCandidate(project, baseCommit, rows, digest, diff, head); err != nil {
164
+ return fail(err)
165
+ }
146
166
  if err := os.MkdirAll(dir, 0o755); err != nil {
147
167
  return fail(err)
148
168
  }
@@ -157,6 +177,34 @@ func RunCandidateFreeze(root string, args []string, stdout, stderr io.Writer) in
157
177
  return 0
158
178
  }
159
179
 
180
+ // Bracket Git's mutable worktree reads; refuse observed drift, not a filesystem snapshot guarantee.
181
+ func revalidateFrozenCandidate(project, base string, rows []candidateRow, digest string, diff []byte, head string) error {
182
+ currentDiff, commit, err := freezeDiff(project, base, rows)
183
+ if err != nil {
184
+ return err
185
+ }
186
+ if commit != base || !bytes.Equal(currentDiff, diff) {
187
+ return errors.New("candidate diff changed during freeze; retry under a new wave")
188
+ }
189
+ current, err := hashCandidate(project, rows, false)
190
+ if err != nil {
191
+ return fmt.Errorf("candidate changed during freeze: %w", err)
192
+ }
193
+ if current != digest {
194
+ return errors.New("candidate bytes changed during freeze; retry under a new wave")
195
+ }
196
+ if head != "" {
197
+ dirty, currentHead, err := freezeDiff(project, "HEAD", rows)
198
+ if err != nil {
199
+ return err
200
+ }
201
+ if currentHead != head || len(dirty) != 0 {
202
+ return errors.New("worktree HEAD or path set changed during freeze; candidate must remain its clean transfer commit")
203
+ }
204
+ }
205
+ return nil
206
+ }
207
+
160
208
  // slicePaths is the slice's tasks.md write set: Writer allowlist, else Files
161
209
  // likely touched.
162
210
  func slicePaths(root, slug, slice string) ([]string, error) {
@@ -0,0 +1,179 @@
1
+ package lib
2
+
3
+ import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "errors"
7
+ "fmt"
8
+ "io"
9
+ "os"
10
+ "path/filepath"
11
+ "strings"
12
+ )
13
+
14
+ // Binary patches can exceed the candidate's raw-byte bound; stream verification.
15
+ const maxFrozenDiffBytes = 4 * maxCandidateTotalBytes
16
+
17
+ type frozenDiffBinding struct {
18
+ name string
19
+ digest string
20
+ }
21
+
22
+ func frozenDiffBindings(raw []byte) ([]frozenDiffBinding, error) {
23
+ var bindings []frozenDiffBinding
24
+ for _, line := range strings.Split(string(raw), "\n") {
25
+ fields := strings.Fields(line)
26
+ if len(fields) == 0 || fields[0] != "diff" {
27
+ continue
28
+ }
29
+ if len(fields) != 3 || len(fields[1]) != sha256.Size*2 || diffPathRE.FindString(fields[2]) != fields[2] {
30
+ return nil, errors.New("malformed diff checksum; use diff <SHA256> <path.diff|path.patch>")
31
+ }
32
+ if _, err := hex.DecodeString(fields[1]); err != nil {
33
+ return nil, errors.New("malformed diff SHA256 checksum")
34
+ }
35
+ bindings = append(bindings, frozenDiffBinding{name: fields[2], digest: strings.ToLower(fields[1])})
36
+ }
37
+ return bindings, nil
38
+ }
39
+
40
+ func verifyFrozenDiff(name, expected string) error {
41
+ before, err := os.Lstat(name)
42
+ if err != nil {
43
+ return err
44
+ }
45
+ if !before.Mode().IsRegular() || before.Size() <= 0 {
46
+ return errors.New("frozen candidate diff must be a non-empty regular file")
47
+ }
48
+ if before.Size() > maxFrozenDiffBytes {
49
+ return errors.New("frozen candidate diff exceeds 1 GiB limit")
50
+ }
51
+ // #nosec G304 -- validated regular artifact, bounded streaming read below
52
+ file, err := os.Open(name)
53
+ if err != nil {
54
+ return err
55
+ }
56
+ defer func() { _ = file.Close() }()
57
+ opened, err := file.Stat()
58
+ if err != nil {
59
+ return err
60
+ }
61
+ if !opened.Mode().IsRegular() || !os.SameFile(before, opened) || before.Size() != opened.Size() || !before.ModTime().Equal(opened.ModTime()) {
62
+ return errors.New("frozen candidate diff changed while opening")
63
+ }
64
+ digest := sha256.New()
65
+ count, err := io.Copy(digest, io.LimitReader(file, maxFrozenDiffBytes+1))
66
+ if err != nil {
67
+ return err
68
+ }
69
+ after, err := file.Stat()
70
+ if err != nil {
71
+ return err
72
+ }
73
+ current, err := os.Lstat(name)
74
+ if err != nil {
75
+ return err
76
+ }
77
+ if count != before.Size() || !os.SameFile(opened, current) || after.Size() != count || current.Size() != count || !opened.ModTime().Equal(after.ModTime()) || !opened.ModTime().Equal(current.ModTime()) {
78
+ return errors.New("frozen candidate diff changed while reading")
79
+ }
80
+ if hex.EncodeToString(digest.Sum(nil)) != expected {
81
+ return errors.New("frozen candidate diff SHA256 mismatch; freeze the intended bytes under a new wave")
82
+ }
83
+ return nil
84
+ }
85
+
86
+ func resolvePacketDiff(root, slug, name string) (string, error) {
87
+ clean := filepath.Clean(filepath.FromSlash(name))
88
+ if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) || clean != filepath.FromSlash(name) {
89
+ return "", fmt.Errorf("unsafe frozen candidate diff path %q", name)
90
+ }
91
+ dirs := []string{projectDir(root), featureDir(root, slug)}
92
+ // Freeze's checksum rows use this workspace-relative namespace, never a product packets/ directory.
93
+ if strings.HasPrefix(name, "packets/") {
94
+ dirs = []string{featureDir(root, slug)}
95
+ }
96
+ if filepath.IsAbs(name) {
97
+ parent, err := filepath.EvalSymlinks(filepath.Dir(name))
98
+ if err != nil {
99
+ return "", err
100
+ }
101
+ dirs = []string{parent}
102
+ name = filepath.Base(name)
103
+ }
104
+ for _, dir := range dirs {
105
+ info, exists, err := candidatePathInfo(dir, filepath.ToSlash(name))
106
+ if err != nil {
107
+ return "", err
108
+ }
109
+ if exists {
110
+ if !info.Mode().IsRegular() {
111
+ return "", errors.New("frozen candidate diff is not a regular file")
112
+ }
113
+ parent, err := filepath.EvalSymlinks(dir)
114
+ if err != nil {
115
+ return "", err
116
+ }
117
+ return filepath.Join(parent, filepath.FromSlash(name)), nil
118
+ }
119
+ }
120
+ return "", fmt.Errorf("frozen candidate diff %q does not exist", name)
121
+ }
122
+
123
+ // Caller may already hold the feature lock (dispatch); never acquire it here.
124
+ func lintPacketDiffs(root, slug, packet string, required bool) error {
125
+ raw, err := readBoundedRegularFile(packet, maxContractBytes)
126
+ if err != nil {
127
+ return fmt.Errorf("reviewer packet %s: %w", packet, err)
128
+ }
129
+ bindings, err := frozenDiffBindings(raw)
130
+ if err != nil {
131
+ return err
132
+ }
133
+ bound := make(map[string]string, len(bindings))
134
+ for _, binding := range bindings {
135
+ path, err := resolvePacketDiff(root, slug, binding.name)
136
+ if err != nil {
137
+ return err
138
+ }
139
+ if prior, ok := bound[path]; ok {
140
+ if prior != binding.digest {
141
+ return fmt.Errorf("conflicting diff checksums for %s", binding.name)
142
+ }
143
+ continue
144
+ }
145
+ if err := verifyFrozenDiff(path, binding.digest); err != nil {
146
+ return fmt.Errorf("%s: %w", binding.name, err)
147
+ }
148
+ bound[path] = binding.digest
149
+ }
150
+ var names []string
151
+ for _, line := range strings.Split(string(raw), "\n") {
152
+ if entry, candidate := strings.CutPrefix(line, "file "); candidate {
153
+ identity, path, complete := strings.Cut(entry, " ")
154
+ // Candidate file identities describe product paths, not reviewer inputs.
155
+ if complete && path != "" {
156
+ if identity == "absent" {
157
+ continue
158
+ }
159
+ if digest, err := hex.DecodeString(identity); err == nil && len(digest) == sha256.Size {
160
+ continue
161
+ }
162
+ }
163
+ }
164
+ names = append(names, diffPathRE.FindAllString(line, -1)...)
165
+ }
166
+ if required && len(names) == 0 {
167
+ return errors.New("a Review or Seal reviewer packet must name the frozen candidate diff and bind its bytes with diff <SHA256> <path.diff|path.patch>")
168
+ }
169
+ for _, name := range names {
170
+ path, err := resolvePacketDiff(root, slug, name)
171
+ if err != nil {
172
+ return err
173
+ }
174
+ if _, ok := bound[path]; !ok {
175
+ return fmt.Errorf("unbound frozen candidate diff %s; add diff <SHA256> %s to the packet", name, name)
176
+ }
177
+ }
178
+ return nil
179
+ }
@@ -180,7 +180,7 @@ func pathSetIdentity(project string, paths []string) (candidateIdentity, error)
180
180
  if err != nil {
181
181
  return candidateIdentity{}, err
182
182
  }
183
- digest, err := hashCandidate(project, rows)
183
+ digest, err := hashCandidate(project, rows, false)
184
184
  return candidateIdentity{digest: digest, rows: rows}, err
185
185
  }
186
186
 
@@ -244,7 +244,7 @@ func runPerturbProbe(project string, identity candidateIdentity, gate *acceptanc
244
244
  probe.reason = "candidate copy failed: " + oneLine(err.Error())
245
245
  return probe
246
246
  }
247
- if probe.copyStart, err = hashCandidate(base, rows); err != nil || probe.copyStart != identity.digest {
247
+ if probe.copyStart, err = hashCandidate(base, rows, false); err != nil || probe.copyStart != identity.digest {
248
248
  probe.reason = "the isolated copy does not hash to the candidate digest"
249
249
  return probe
250
250
  }
@@ -267,7 +267,7 @@ func runPerturbProbe(project string, identity candidateIdentity, gate *acceptanc
267
267
  return probe
268
268
  }
269
269
  if err := validateCandidateRows(base, rows); err == nil {
270
- probe.copyRestored, _ = hashCandidate(base, rows)
270
+ probe.copyRestored, _ = hashCandidate(base, rows, false)
271
271
  }
272
272
  if probe.copyRestored != identity.digest {
273
273
  probe.reason = "the reverted copy does not hash to the candidate digest"