devrites 7.2.8 → 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 (330) hide show
  1. package/CHANGELOG.md +32 -0
  2. package/README.md +1 -1
  3. package/docs/candidate-integrity.md +10 -0
  4. package/docs/cli.md +7 -1
  5. package/docs/command-map.md +5 -2
  6. package/docs/engine/commands.md +28 -4
  7. package/docs/orchestration.md +26 -13
  8. package/docs/skills.md +5 -0
  9. package/engine/internal/acceptance/cli.go +27 -26
  10. package/engine/internal/acceptance/preconditions.go +1 -1
  11. package/engine/internal/acceptance/run.go +42 -16
  12. package/engine/internal/acceptance/transcript.go +236 -45
  13. package/engine/internal/devritespaths/paths.go +16 -3
  14. package/engine/internal/gate/gate.go +6 -0
  15. package/engine/internal/lib/candidate.go +20 -7
  16. package/engine/internal/lib/candidate_coverage.go +2 -19
  17. package/engine/internal/lib/closeout.go +16 -4
  18. package/engine/internal/lib/context.go +64 -45
  19. package/engine/internal/lib/context_io.go +171 -0
  20. package/engine/internal/lib/detect.go +8 -8
  21. package/engine/internal/lib/dispatch.go +2 -0
  22. package/engine/internal/lib/packet.go +37 -7
  23. package/engine/internal/lib/packet_freeze.go +60 -12
  24. package/engine/internal/lib/packet_integrity.go +179 -0
  25. package/engine/internal/lib/perturb.go +3 -3
  26. package/engine/internal/lib/slicecheck.go +7 -1
  27. package/engine/internal/lib/taskgraph.go +20 -36
  28. package/engine/internal/lib/triggers.go +28 -29
  29. package/engine/internal/lib/workspace_pool.go +330 -0
  30. package/engine/internal/parallel/cli.go +190 -49
  31. package/engine/internal/parallel/git.go +47 -14
  32. package/engine/internal/parallel/landing.go +232 -0
  33. package/engine/internal/parallel/lease.go +97 -18
  34. package/engine/internal/parallel/ops.go +312 -155
  35. package/engine/internal/parallel/pathdisjoint.go +48 -4
  36. package/engine/internal/parallel/prune.go +133 -0
  37. package/engine/internal/parallel/repository.go +239 -0
  38. package/engine/internal/parallel/select.go +64 -42
  39. package/engine/main.go +2 -1
  40. package/pack/.claude/agents/devrites-proof-runner.md +16 -9
  41. package/pack/.claude/skills/devrites-lib/SKILL.md +2 -0
  42. package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  43. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  44. package/pack/.claude/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  45. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  46. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  47. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  48. package/pack/.claude/skills/devrites-lib/reference/standards/gates.md +18 -9
  49. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  50. package/pack/.claude/skills/rite-autocomplete/SKILL.md +13 -10
  51. package/pack/.claude/skills/rite-autocomplete/reference/lookahead.md +4 -4
  52. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +13 -9
  53. package/pack/.claude/skills/rite-build/SKILL.md +13 -10
  54. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +1 -1
  55. package/pack/.claude/skills/rite-build/reference/checkpoint.md +1 -1
  56. package/pack/.claude/skills/rite-build/reference/parallel-batch.md +57 -36
  57. package/pack/.claude/skills/rite-build/reference/phase-contract.md +8 -5
  58. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +7 -7
  59. package/pack/.claude/skills/rite-clarify/SKILL.md +2 -2
  60. package/pack/.claude/skills/rite-converge/SKILL.md +4 -4
  61. package/pack/.claude/skills/rite-define/SKILL.md +7 -3
  62. package/pack/.claude/skills/rite-define/reference/plan-template.md +4 -2
  63. package/pack/.claude/skills/rite-plan/SKILL.md +11 -8
  64. package/pack/.claude/skills/rite-plan/reference/dependency-graph.md +9 -2
  65. package/pack/.claude/skills/rite-plan/reference/slicing.md +2 -0
  66. package/pack/.claude/skills/rite-polish/SKILL.md +3 -3
  67. package/pack/.claude/skills/rite-prove/SKILL.md +9 -5
  68. package/pack/.claude/skills/rite-resolve/SKILL.md +3 -3
  69. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +2 -0
  70. package/pack/.claude/skills/rite-review/SKILL.md +4 -3
  71. package/pack/.claude/skills/rite-review/reference/feature-scoped-review.md +2 -2
  72. package/pack/.claude/skills/rite-seal/SKILL.md +1 -1
  73. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +1 -1
  74. package/pack/.claude/skills/rite-ship/SKILL.md +2 -2
  75. package/pack/.claude/skills/rite-ship/reference/git-ship.md +2 -2
  76. package/pack/.claude/skills/rite-spec/SKILL.md +8 -5
  77. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +5 -0
  78. package/pack/.claude/skills/rite-temper/SKILL.md +3 -3
  79. package/pack/.claude/skills/rite-vet/SKILL.md +9 -5
  80. package/pack/.claude/skills/rite-vet/reference/review-axes.md +12 -5
  81. package/pack/generated/claude/agents/devrites-proof-runner.md +16 -9
  82. package/pack/generated/claude/skills/devrites-lib/SKILL.md +2 -0
  83. package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  84. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  85. package/pack/generated/claude/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  86. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  87. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +1 -1
  88. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  89. package/pack/generated/claude/skills/devrites-lib/reference/standards/gates.md +18 -9
  90. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  91. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +13 -10
  92. package/pack/generated/claude/skills/rite-autocomplete/reference/lookahead.md +4 -4
  93. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +13 -9
  94. package/pack/generated/claude/skills/rite-build/SKILL.md +13 -10
  95. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +1 -1
  96. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +1 -1
  97. package/pack/generated/claude/skills/rite-build/reference/parallel-batch.md +57 -36
  98. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +8 -5
  99. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +7 -7
  100. package/pack/generated/claude/skills/rite-clarify/SKILL.md +2 -2
  101. package/pack/generated/claude/skills/rite-converge/SKILL.md +4 -4
  102. package/pack/generated/claude/skills/rite-define/SKILL.md +7 -3
  103. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +4 -2
  104. package/pack/generated/claude/skills/rite-plan/SKILL.md +11 -8
  105. package/pack/generated/claude/skills/rite-plan/reference/dependency-graph.md +9 -2
  106. package/pack/generated/claude/skills/rite-plan/reference/slicing.md +2 -0
  107. package/pack/generated/claude/skills/rite-polish/SKILL.md +3 -3
  108. package/pack/generated/claude/skills/rite-prove/SKILL.md +9 -5
  109. package/pack/generated/claude/skills/rite-resolve/SKILL.md +3 -3
  110. package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +2 -0
  111. package/pack/generated/claude/skills/rite-review/SKILL.md +4 -3
  112. package/pack/generated/claude/skills/rite-review/reference/feature-scoped-review.md +2 -2
  113. package/pack/generated/claude/skills/rite-seal/SKILL.md +1 -1
  114. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +1 -1
  115. package/pack/generated/claude/skills/rite-ship/SKILL.md +2 -2
  116. package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +2 -2
  117. package/pack/generated/claude/skills/rite-spec/SKILL.md +8 -5
  118. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +5 -0
  119. package/pack/generated/claude/skills/rite-temper/SKILL.md +3 -3
  120. package/pack/generated/claude/skills/rite-vet/SKILL.md +9 -5
  121. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +12 -5
  122. package/pack/generated/codex/agents/devrites-proof-runner.toml +16 -9
  123. package/pack/generated/codex/skills/devrites-lib/SKILL.md +2 -0
  124. package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  125. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  126. package/pack/generated/codex/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  127. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  128. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +1 -1
  129. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  130. package/pack/generated/codex/skills/devrites-lib/reference/standards/gates.md +18 -9
  131. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  132. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +13 -10
  133. package/pack/generated/codex/skills/rite-autocomplete/reference/lookahead.md +4 -4
  134. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +13 -9
  135. package/pack/generated/codex/skills/rite-build/SKILL.md +13 -10
  136. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +1 -1
  137. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +1 -1
  138. package/pack/generated/codex/skills/rite-build/reference/parallel-batch.md +57 -36
  139. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +8 -5
  140. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +7 -7
  141. package/pack/generated/codex/skills/rite-clarify/SKILL.md +2 -2
  142. package/pack/generated/codex/skills/rite-converge/SKILL.md +4 -4
  143. package/pack/generated/codex/skills/rite-define/SKILL.md +7 -3
  144. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +4 -2
  145. package/pack/generated/codex/skills/rite-plan/SKILL.md +11 -8
  146. package/pack/generated/codex/skills/rite-plan/reference/dependency-graph.md +9 -2
  147. package/pack/generated/codex/skills/rite-plan/reference/slicing.md +2 -0
  148. package/pack/generated/codex/skills/rite-polish/SKILL.md +3 -3
  149. package/pack/generated/codex/skills/rite-prove/SKILL.md +9 -5
  150. package/pack/generated/codex/skills/rite-resolve/SKILL.md +3 -3
  151. package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +2 -0
  152. package/pack/generated/codex/skills/rite-review/SKILL.md +4 -3
  153. package/pack/generated/codex/skills/rite-review/reference/feature-scoped-review.md +2 -2
  154. package/pack/generated/codex/skills/rite-seal/SKILL.md +1 -1
  155. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +1 -1
  156. package/pack/generated/codex/skills/rite-ship/SKILL.md +2 -2
  157. package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +2 -2
  158. package/pack/generated/codex/skills/rite-spec/SKILL.md +8 -5
  159. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +5 -0
  160. package/pack/generated/codex/skills/rite-temper/SKILL.md +3 -3
  161. package/pack/generated/codex/skills/rite-vet/SKILL.md +9 -5
  162. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +12 -5
  163. package/pack/generated/devin/agents/devrites-proof-runner.md +16 -9
  164. package/pack/generated/devin/skills/devrites-lib/SKILL.md +2 -0
  165. package/pack/generated/devin/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  166. package/pack/generated/devin/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  167. package/pack/generated/devin/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  168. package/pack/generated/devin/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  169. package/pack/generated/devin/skills/devrites-lib/reference/standards/agents.md +1 -1
  170. package/pack/generated/devin/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  171. package/pack/generated/devin/skills/devrites-lib/reference/standards/gates.md +18 -9
  172. package/pack/generated/devin/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  173. package/pack/generated/devin/skills/rite-autocomplete/SKILL.md +13 -10
  174. package/pack/generated/devin/skills/rite-autocomplete/reference/lookahead.md +4 -4
  175. package/pack/generated/devin/skills/rite-autocomplete/reference/loop.md +13 -9
  176. package/pack/generated/devin/skills/rite-build/SKILL.md +13 -10
  177. package/pack/generated/devin/skills/rite-build/reference/afk-discipline.md +1 -1
  178. package/pack/generated/devin/skills/rite-build/reference/checkpoint.md +1 -1
  179. package/pack/generated/devin/skills/rite-build/reference/parallel-batch.md +57 -36
  180. package/pack/generated/devin/skills/rite-build/reference/phase-contract.md +8 -5
  181. package/pack/generated/devin/skills/rite-build/reference/wright-dispatch.md +7 -7
  182. package/pack/generated/devin/skills/rite-clarify/SKILL.md +2 -2
  183. package/pack/generated/devin/skills/rite-converge/SKILL.md +4 -4
  184. package/pack/generated/devin/skills/rite-define/SKILL.md +7 -3
  185. package/pack/generated/devin/skills/rite-define/reference/plan-template.md +4 -2
  186. package/pack/generated/devin/skills/rite-plan/SKILL.md +11 -8
  187. package/pack/generated/devin/skills/rite-plan/reference/dependency-graph.md +9 -2
  188. package/pack/generated/devin/skills/rite-plan/reference/slicing.md +2 -0
  189. package/pack/generated/devin/skills/rite-polish/SKILL.md +3 -3
  190. package/pack/generated/devin/skills/rite-prove/SKILL.md +9 -5
  191. package/pack/generated/devin/skills/rite-resolve/SKILL.md +3 -3
  192. package/pack/generated/devin/skills/rite-resolve/reference/answer-protocol.md +2 -0
  193. package/pack/generated/devin/skills/rite-review/SKILL.md +4 -3
  194. package/pack/generated/devin/skills/rite-review/reference/feature-scoped-review.md +2 -2
  195. package/pack/generated/devin/skills/rite-seal/SKILL.md +1 -1
  196. package/pack/generated/devin/skills/rite-seal/reference/phase-contract.md +1 -1
  197. package/pack/generated/devin/skills/rite-ship/SKILL.md +2 -2
  198. package/pack/generated/devin/skills/rite-ship/reference/git-ship.md +2 -2
  199. package/pack/generated/devin/skills/rite-spec/SKILL.md +8 -5
  200. package/pack/generated/devin/skills/rite-spec/reference/state-workspace.md +5 -0
  201. package/pack/generated/devin/skills/rite-temper/SKILL.md +3 -3
  202. package/pack/generated/devin/skills/rite-vet/SKILL.md +9 -5
  203. package/pack/generated/devin/skills/rite-vet/reference/review-axes.md +12 -5
  204. package/pack/generated/dsh/agents/devrites-proof-runner.md +16 -9
  205. package/pack/generated/dsh/runtime/cordis.patch.yml +1 -1
  206. package/pack/generated/dsh/skills/devrites-lib/SKILL.md +2 -0
  207. package/pack/generated/dsh/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  208. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  209. package/pack/generated/dsh/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  210. package/pack/generated/dsh/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  211. package/pack/generated/dsh/skills/devrites-lib/reference/standards/agents.md +1 -1
  212. package/pack/generated/dsh/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  213. package/pack/generated/dsh/skills/devrites-lib/reference/standards/gates.md +18 -9
  214. package/pack/generated/dsh/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  215. package/pack/generated/dsh/skills/rite-autocomplete/SKILL.md +13 -10
  216. package/pack/generated/dsh/skills/rite-autocomplete/reference/lookahead.md +4 -4
  217. package/pack/generated/dsh/skills/rite-autocomplete/reference/loop.md +13 -9
  218. package/pack/generated/dsh/skills/rite-build/SKILL.md +13 -10
  219. package/pack/generated/dsh/skills/rite-build/reference/afk-discipline.md +1 -1
  220. package/pack/generated/dsh/skills/rite-build/reference/checkpoint.md +1 -1
  221. package/pack/generated/dsh/skills/rite-build/reference/parallel-batch.md +57 -36
  222. package/pack/generated/dsh/skills/rite-build/reference/phase-contract.md +8 -5
  223. package/pack/generated/dsh/skills/rite-build/reference/wright-dispatch.md +7 -7
  224. package/pack/generated/dsh/skills/rite-clarify/SKILL.md +2 -2
  225. package/pack/generated/dsh/skills/rite-converge/SKILL.md +4 -4
  226. package/pack/generated/dsh/skills/rite-define/SKILL.md +7 -3
  227. package/pack/generated/dsh/skills/rite-define/reference/plan-template.md +4 -2
  228. package/pack/generated/dsh/skills/rite-plan/SKILL.md +11 -8
  229. package/pack/generated/dsh/skills/rite-plan/reference/dependency-graph.md +9 -2
  230. package/pack/generated/dsh/skills/rite-plan/reference/slicing.md +2 -0
  231. package/pack/generated/dsh/skills/rite-polish/SKILL.md +3 -3
  232. package/pack/generated/dsh/skills/rite-prove/SKILL.md +9 -5
  233. package/pack/generated/dsh/skills/rite-resolve/SKILL.md +3 -3
  234. package/pack/generated/dsh/skills/rite-resolve/reference/answer-protocol.md +2 -0
  235. package/pack/generated/dsh/skills/rite-review/SKILL.md +4 -3
  236. package/pack/generated/dsh/skills/rite-review/reference/feature-scoped-review.md +2 -2
  237. package/pack/generated/dsh/skills/rite-seal/SKILL.md +1 -1
  238. package/pack/generated/dsh/skills/rite-seal/reference/phase-contract.md +1 -1
  239. package/pack/generated/dsh/skills/rite-ship/SKILL.md +2 -2
  240. package/pack/generated/dsh/skills/rite-ship/reference/git-ship.md +2 -2
  241. package/pack/generated/dsh/skills/rite-spec/SKILL.md +8 -5
  242. package/pack/generated/dsh/skills/rite-spec/reference/state-workspace.md +5 -0
  243. package/pack/generated/dsh/skills/rite-temper/SKILL.md +3 -3
  244. package/pack/generated/dsh/skills/rite-vet/SKILL.md +9 -5
  245. package/pack/generated/dsh/skills/rite-vet/reference/review-axes.md +12 -5
  246. package/pack/generated/omp/agents/devrites-proof-runner.md +16 -9
  247. package/pack/generated/omp/commands/rite-build.md +1 -1
  248. package/pack/generated/omp/skills/devrites-lib/SKILL.md +2 -0
  249. package/pack/generated/omp/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  250. package/pack/generated/omp/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  251. package/pack/generated/omp/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  252. package/pack/generated/omp/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  253. package/pack/generated/omp/skills/devrites-lib/reference/standards/agents.md +1 -1
  254. package/pack/generated/omp/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  255. package/pack/generated/omp/skills/devrites-lib/reference/standards/gates.md +18 -9
  256. package/pack/generated/omp/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  257. package/pack/generated/omp/skills/rite-autocomplete/SKILL.md +13 -10
  258. package/pack/generated/omp/skills/rite-autocomplete/reference/lookahead.md +4 -4
  259. package/pack/generated/omp/skills/rite-autocomplete/reference/loop.md +13 -9
  260. package/pack/generated/omp/skills/rite-build/SKILL.md +13 -10
  261. package/pack/generated/omp/skills/rite-build/reference/afk-discipline.md +1 -1
  262. package/pack/generated/omp/skills/rite-build/reference/checkpoint.md +1 -1
  263. package/pack/generated/omp/skills/rite-build/reference/parallel-batch.md +57 -36
  264. package/pack/generated/omp/skills/rite-build/reference/phase-contract.md +8 -5
  265. package/pack/generated/omp/skills/rite-build/reference/wright-dispatch.md +7 -7
  266. package/pack/generated/omp/skills/rite-clarify/SKILL.md +2 -2
  267. package/pack/generated/omp/skills/rite-converge/SKILL.md +4 -4
  268. package/pack/generated/omp/skills/rite-define/SKILL.md +7 -3
  269. package/pack/generated/omp/skills/rite-define/reference/plan-template.md +4 -2
  270. package/pack/generated/omp/skills/rite-plan/SKILL.md +11 -8
  271. package/pack/generated/omp/skills/rite-plan/reference/dependency-graph.md +9 -2
  272. package/pack/generated/omp/skills/rite-plan/reference/slicing.md +2 -0
  273. package/pack/generated/omp/skills/rite-polish/SKILL.md +3 -3
  274. package/pack/generated/omp/skills/rite-prove/SKILL.md +9 -5
  275. package/pack/generated/omp/skills/rite-resolve/SKILL.md +3 -3
  276. package/pack/generated/omp/skills/rite-resolve/reference/answer-protocol.md +2 -0
  277. package/pack/generated/omp/skills/rite-review/SKILL.md +4 -3
  278. package/pack/generated/omp/skills/rite-review/reference/feature-scoped-review.md +2 -2
  279. package/pack/generated/omp/skills/rite-seal/SKILL.md +1 -1
  280. package/pack/generated/omp/skills/rite-seal/reference/phase-contract.md +1 -1
  281. package/pack/generated/omp/skills/rite-ship/SKILL.md +2 -2
  282. package/pack/generated/omp/skills/rite-ship/reference/git-ship.md +2 -2
  283. package/pack/generated/omp/skills/rite-spec/SKILL.md +8 -5
  284. package/pack/generated/omp/skills/rite-spec/reference/state-workspace.md +5 -0
  285. package/pack/generated/omp/skills/rite-temper/SKILL.md +3 -3
  286. package/pack/generated/omp/skills/rite-vet/SKILL.md +9 -5
  287. package/pack/generated/omp/skills/rite-vet/reference/review-axes.md +12 -5
  288. package/pack/generated/pi/agents/devrites-proof-runner.md +16 -9
  289. package/pack/generated/pi/prompts/rite-build.md +1 -1
  290. package/pack/generated/pi/skills/devrites-lib/SKILL.md +2 -0
  291. package/pack/generated/pi/skills/devrites-lib/reference/candidate-integrity.md +7 -3
  292. package/pack/generated/pi/skills/devrites-lib/reference/parallel-dispatch.md +3 -0
  293. package/pack/generated/pi/skills/devrites-lib/reference/parallel-workspaces.md +202 -0
  294. package/pack/generated/pi/skills/devrites-lib/reference/standards/afk-hitl.md +9 -12
  295. package/pack/generated/pi/skills/devrites-lib/reference/standards/agents.md +1 -1
  296. package/pack/generated/pi/skills/devrites-lib/reference/standards/context-hygiene.md +1 -1
  297. package/pack/generated/pi/skills/devrites-lib/reference/standards/gates.md +18 -9
  298. package/pack/generated/pi/skills/devrites-lib/reference/workspace-artifact-schema.md +10 -0
  299. package/pack/generated/pi/skills/rite-autocomplete/SKILL.md +13 -10
  300. package/pack/generated/pi/skills/rite-autocomplete/reference/lookahead.md +4 -4
  301. package/pack/generated/pi/skills/rite-autocomplete/reference/loop.md +13 -9
  302. package/pack/generated/pi/skills/rite-build/SKILL.md +13 -10
  303. package/pack/generated/pi/skills/rite-build/reference/afk-discipline.md +1 -1
  304. package/pack/generated/pi/skills/rite-build/reference/checkpoint.md +1 -1
  305. package/pack/generated/pi/skills/rite-build/reference/parallel-batch.md +57 -36
  306. package/pack/generated/pi/skills/rite-build/reference/phase-contract.md +8 -5
  307. package/pack/generated/pi/skills/rite-build/reference/wright-dispatch.md +7 -7
  308. package/pack/generated/pi/skills/rite-clarify/SKILL.md +2 -2
  309. package/pack/generated/pi/skills/rite-converge/SKILL.md +4 -4
  310. package/pack/generated/pi/skills/rite-define/SKILL.md +7 -3
  311. package/pack/generated/pi/skills/rite-define/reference/plan-template.md +4 -2
  312. package/pack/generated/pi/skills/rite-plan/SKILL.md +11 -8
  313. package/pack/generated/pi/skills/rite-plan/reference/dependency-graph.md +9 -2
  314. package/pack/generated/pi/skills/rite-plan/reference/slicing.md +2 -0
  315. package/pack/generated/pi/skills/rite-polish/SKILL.md +3 -3
  316. package/pack/generated/pi/skills/rite-prove/SKILL.md +9 -5
  317. package/pack/generated/pi/skills/rite-resolve/SKILL.md +3 -3
  318. package/pack/generated/pi/skills/rite-resolve/reference/answer-protocol.md +2 -0
  319. package/pack/generated/pi/skills/rite-review/SKILL.md +4 -3
  320. package/pack/generated/pi/skills/rite-review/reference/feature-scoped-review.md +2 -2
  321. package/pack/generated/pi/skills/rite-seal/SKILL.md +1 -1
  322. package/pack/generated/pi/skills/rite-seal/reference/phase-contract.md +1 -1
  323. package/pack/generated/pi/skills/rite-ship/SKILL.md +2 -2
  324. package/pack/generated/pi/skills/rite-ship/reference/git-ship.md +2 -2
  325. package/pack/generated/pi/skills/rite-spec/SKILL.md +8 -5
  326. package/pack/generated/pi/skills/rite-spec/reference/state-workspace.md +5 -0
  327. package/pack/generated/pi/skills/rite-temper/SKILL.md +3 -3
  328. package/pack/generated/pi/skills/rite-vet/SKILL.md +9 -5
  329. package/pack/generated/pi/skills/rite-vet/reference/review-axes.md +12 -5
  330. package/package.json +1 -1
@@ -1,13 +1,17 @@
1
1
  package acceptance
2
2
 
3
3
  import (
4
+ "errors"
4
5
  "fmt"
6
+ "io"
5
7
  "os"
6
8
  "path/filepath"
7
9
  "regexp"
10
+ "strconv"
8
11
  "strings"
9
12
  "time"
10
13
 
14
+ "github.com/devrites/devrites/internal/fsutil"
11
15
  "github.com/devrites/devrites/internal/secretpattern"
12
16
  )
13
17
 
@@ -19,7 +23,11 @@ var CandidateDigest func(root, slug string) (string, error)
19
23
  // maxTranscriptOutput caps the redacted output a transcript keeps.
20
24
  const maxTranscriptOutput = 1 << 20
21
25
 
26
+ // Redaction can expand short credentials in metadata as well as output.
27
+ const maxTranscriptBytes = 2*MaxLedgerBytes + maxTranscriptOutput + (64 << 10)
28
+
22
29
  var unsafeNameRE = regexp.MustCompile(`[^A-Za-z0-9._-]`)
30
+ var transcriptDigestRE = regexp.MustCompile(`^[0-9a-f]{64}$`)
23
31
 
24
32
  func candidateLabel(root, slug string) string {
25
33
  if CandidateDigest == nil {
@@ -27,77 +35,260 @@ func candidateLabel(root, slug string) string {
27
35
  }
28
36
  digest, err := CandidateDigest(root, slug)
29
37
  if err != nil {
30
- return "unavailable: " + oneLine(err.Error())
38
+ return "unavailable: " + transcriptMetadata(err.Error())
31
39
  }
32
40
  return digest
33
41
  }
34
42
 
35
- // writeTranscripts records one immutable file per executed or reused gate under
36
- // <work>/references/gates/<gate-id>/, binding the exact command, cwd, attempt,
37
- // exit, EXPECT verdict and redacted output to the candidate identity read
38
- // before and after the run. OUTPUT_SHA256 equals the ledger evidence digest.
43
+ func transcriptMetadata(value string) string {
44
+ redacted, _ := secretpattern.Redact(oneLine(value))
45
+ return redacted
46
+ }
47
+
48
+ func transcriptGateName(id string) string {
49
+ name := unsafeNameRE.ReplaceAllString(transcriptMetadata(id), "_")
50
+ if name == "." || name == ".." || name == "" {
51
+ return "_"
52
+ }
53
+ return name
54
+ }
55
+
56
+ // transcriptDirectory opens each directory without accepting symlink parents.
57
+ // Holding the resulting root also confines publication if a parent is renamed.
58
+ func transcriptDirectory(work, id string, create bool) (*os.Root, error) {
59
+ root, err := os.OpenRoot(work)
60
+ if err != nil {
61
+ return nil, err
62
+ }
63
+ for _, name := range []string{"references", "gates", transcriptGateName(id)} {
64
+ info, err := root.Lstat(name)
65
+ if errors.Is(err, os.ErrNotExist) && create {
66
+ if err = root.Mkdir(name, 0o700); err == nil || errors.Is(err, os.ErrExist) {
67
+ info, err = root.Lstat(name)
68
+ }
69
+ }
70
+ if err != nil || !info.IsDir() {
71
+ _ = root.Close()
72
+ if err == nil {
73
+ err = fmt.Errorf("unsafe transcript directory %s", name)
74
+ }
75
+ return nil, err
76
+ }
77
+ next, err := root.OpenRoot(name)
78
+ if err == nil {
79
+ var opened os.FileInfo
80
+ opened, err = next.Stat(".")
81
+ if err == nil && !os.SameFile(info, opened) {
82
+ err = fmt.Errorf("transcript directory changed: %s", name)
83
+ }
84
+ }
85
+ _ = root.Close()
86
+ if err != nil {
87
+ if next != nil {
88
+ _ = next.Close()
89
+ }
90
+ return nil, err
91
+ }
92
+ root = next
93
+ }
94
+ return root, nil
95
+ }
96
+
97
+ // writeTranscripts atomically publishes private, complete executed records.
98
+ // Reuse points to the original file, preserving its historical candidate labels.
99
+ // OUTPUT_SHA256 and OUTPUT_BYTES always describe original, not redacted output.
39
100
  func writeTranscripts(work string, doc *Document, results []GateResult, before, after string) ([]string, error) {
40
101
  gates := map[string]*Gate{}
41
102
  for _, gate := range doc.Gates {
42
103
  gates[gate.ID] = gate
43
104
  }
44
105
  prefix := "nodigest"
45
- if len(before) >= 12 && !strings.HasPrefix(before, "unavailable") {
106
+ if transcriptDigestRE.MatchString(before) {
46
107
  prefix = before[:12]
47
108
  }
48
109
  var paths []string
49
110
  for _, result := range results {
50
111
  gate := gates[result.ID]
51
- if gate == nil || (!result.Ran && !result.Reused) {
112
+ if gate == nil {
52
113
  continue
53
114
  }
54
- name := unsafeNameRE.ReplaceAllString(gate.ID, "_")
55
- if name == "." || name == ".." {
56
- name = "_"
115
+ if result.Reused && result.Origin != "" {
116
+ paths = append(paths, result.Origin)
117
+ continue
57
118
  }
58
- dir := filepath.Join(work, "references", "gates", name)
59
- if err := os.MkdirAll(dir, 0o755); err != nil {
60
- return paths, err
119
+ if !result.Ran {
120
+ continue
61
121
  }
62
- var body strings.Builder
63
- fmt.Fprintf(&body, "DEVRITES-GATE-TRANSCRIPT v1\nGATE: %s\nTITLE: %s\nCOMMAND: %s\nCWD: %s\n", gate.ID, gate.Title, gate.Check, valueOr(gate.Cwd, "."))
64
- fmt.Fprintf(&body, "INPUTS: %s\nINPUTS_SHA256: %s\nCANDIDATE_BEFORE: %s\nCANDIDATE_AFTER: %s\n", valueOr(gate.Inputs, "none"), valueOr(result.Inputs, "none"), before, after)
65
- var file string
66
- if result.Reused {
67
- file = filepath.Join(dir, prefix+"-reused.log")
68
- fmt.Fprintf(&body, "RESULT: reused (inputs unchanged; not executed)\nPRIOR_EVIDENCE: %s\n", gate.Evidence)
69
- } else {
70
- attempt := 1
71
- if entries, err := os.ReadDir(dir); err == nil {
72
- for _, entry := range entries {
73
- if strings.HasPrefix(entry.Name(), prefix+"-a") {
74
- attempt++
75
- }
76
- }
77
- }
78
- file = filepath.Join(dir, fmt.Sprintf("%s-a%d.log", prefix, attempt))
79
- output, masked := secretpattern.Redact(result.Outcome.Output)
80
- truncated := len(output) > maxTranscriptOutput
81
- if truncated {
82
- output = output[:maxTranscriptOutput]
83
- }
84
- verdict := "fail"
85
- if result.Passed {
86
- verdict = "pass"
87
- }
88
- fmt.Fprintf(&body, "ATTEMPT: %d\nSTARTED: %s\nENDED: %s\nEXIT: %s\nEXPECT: %s\nRESULT: %s\n",
89
- attempt, result.Started.Format(time.RFC3339Nano), result.Ended.Format(time.RFC3339Nano), exitLabel(result.Outcome), gate.Expect, verdict)
90
- fmt.Fprintf(&body, "OUTPUT_SHA256: %s\nOUTPUT_BYTES: %d\nREDACTIONS: %d\nTRUNCATED: %t\n--- OUTPUT ---\n%s\n--- END OUTPUT ---\n",
91
- OutputDigest(result.Outcome.Output), result.Outcome.OutputBytes, masked, truncated, output)
122
+ dir, err := transcriptDirectory(work, gate.ID, true)
123
+ if err != nil {
124
+ return paths, err
92
125
  }
93
- if err := os.WriteFile(file, []byte(body.String()), 0o644); err != nil {
126
+ file, err := publishTranscript(dir, gate, result, prefix, before, after)
127
+ _ = dir.Close()
128
+ if err != nil {
94
129
  return paths, err
95
130
  }
96
- paths = append(paths, file)
131
+ paths = append(paths, filepath.Join(work, "references", "gates", transcriptGateName(gate.ID), file))
97
132
  }
98
133
  return paths, nil
99
134
  }
100
135
 
136
+ func publishTranscript(dir *os.Root, gate *Gate, result GateResult, prefix, before, after string) (string, error) {
137
+ attempt := 1
138
+ for {
139
+ file := fmt.Sprintf("%s-a%d.log", prefix, attempt)
140
+ if _, err := dir.Lstat(file); errors.Is(err, os.ErrNotExist) {
141
+ break
142
+ } else if err != nil {
143
+ return "", err
144
+ }
145
+ attempt++
146
+ }
147
+ var body strings.Builder
148
+ fmt.Fprintf(&body, "DEVRITES-GATE-TRANSCRIPT v2\nGATE: %s\nTITLE: %s\nCOMMAND: %s\nCWD: %s\nDEFINITION_SHA256: %s\n",
149
+ transcriptMetadata(gate.ID), transcriptMetadata(gate.Title), transcriptMetadata(gate.Check), transcriptMetadata(valueOr(gate.Cwd, ".")), DefinitionDigest(gate))
150
+ fmt.Fprintf(&body, "INPUTS: %s\nINPUTS_SHA256: %s\nCANDIDATE_BEFORE: %s\nCANDIDATE_AFTER: %s\n",
151
+ transcriptMetadata(valueOr(gate.Inputs, "none")), valueOr(result.Inputs, "none"), transcriptMetadata(before), transcriptMetadata(after))
152
+ output, masked := secretpattern.Redact(result.Outcome.Output)
153
+ truncated := len(output) > maxTranscriptOutput
154
+ if truncated {
155
+ output = output[:maxTranscriptOutput]
156
+ }
157
+ verdict := "fail"
158
+ if result.Passed && !result.Discarded && result.WriteErr == nil {
159
+ verdict = "pass"
160
+ }
161
+ fmt.Fprintf(&body, "ATTEMPT: %d\nSTARTED: %s\nENDED: %s\nEXIT: %s\nEXPECT: %s\nRESULT: %s\n",
162
+ attempt, result.Started.Format(time.RFC3339Nano), result.Ended.Format(time.RFC3339Nano), transcriptMetadata(exitLabel(result.Outcome)), transcriptMetadata(gate.Expect), verdict)
163
+ fmt.Fprintf(&body, "OUTPUT_SHA256: %s\nOUTPUT_BYTES: %d\nREDACTIONS: %d\nTRUNCATED: %t\n--- OUTPUT ---\n%s\n--- END OUTPUT ---\n",
164
+ OutputDigest(result.Outcome.Output), result.Outcome.OutputBytes, masked, truncated, output)
165
+ checksum := OutputDigest(body.String())
166
+ fmt.Fprintf(&body, "TRANSCRIPT_SHA256: %s\n", checksum)
167
+ file := fmt.Sprintf("%s-a%d.log", prefix, attempt)
168
+ return file, fsutil.WriteFileAtomicIn(dir, file, []byte(body.String()), 0o600)
169
+ }
170
+
171
+ // passingOrigin accepts only an executed, complete pass bound to ledger proof.
172
+ // Candidate labels are historical context, not a dependency of scoped INPUTS.
173
+ func passingOrigin(work string, gate *Gate) string {
174
+ evidence := automaticEvidenceRE.FindStringSubmatch(strings.TrimSpace(gate.Evidence))
175
+ if evidence == nil || evidence[1] != DefinitionDigest(gate) {
176
+ return ""
177
+ }
178
+ dir, err := transcriptDirectory(work, gate.ID, false)
179
+ if err != nil {
180
+ return ""
181
+ }
182
+ defer func() { _ = dir.Close() }()
183
+ listing, err := dir.Open(".")
184
+ if err != nil {
185
+ return ""
186
+ }
187
+ entries, err := listing.ReadDir(-1)
188
+ _ = listing.Close()
189
+ if err != nil {
190
+ return ""
191
+ }
192
+ for _, entry := range entries {
193
+ if !strings.HasSuffix(entry.Name(), ".log") {
194
+ continue
195
+ }
196
+ raw, err := readTranscript(dir, entry.Name())
197
+ if err == nil && validOrigin(string(raw), gate, evidence) {
198
+ return filepath.Join(work, "references", "gates", transcriptGateName(gate.ID), entry.Name())
199
+ }
200
+ }
201
+ return ""
202
+ }
203
+
204
+ func readTranscript(dir *os.Root, name string) ([]byte, error) {
205
+ info, err := dir.Lstat(name)
206
+ if err != nil {
207
+ return nil, err
208
+ }
209
+ if !info.Mode().IsRegular() || info.Size() > maxTranscriptBytes {
210
+ return nil, fmt.Errorf("unsafe or oversized transcript")
211
+ }
212
+ file, err := dir.Open(name)
213
+ if err != nil {
214
+ return nil, err
215
+ }
216
+ defer func() { _ = file.Close() }()
217
+ opened, err := file.Stat()
218
+ if err != nil {
219
+ return nil, err
220
+ }
221
+ if !os.SameFile(info, opened) {
222
+ return nil, fmt.Errorf("transcript changed while opening")
223
+ }
224
+ raw, err := io.ReadAll(io.LimitReader(file, maxTranscriptBytes+1))
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ current, err := dir.Lstat(name)
229
+ if err != nil {
230
+ return nil, err
231
+ }
232
+ if len(raw) > maxTranscriptBytes || !current.Mode().IsRegular() || !os.SameFile(opened, current) || current.Size() != int64(len(raw)) {
233
+ return nil, fmt.Errorf("transcript changed while reading")
234
+ }
235
+ return raw, nil
236
+ }
237
+
238
+ func validOrigin(raw string, gate *Gate, evidence []string) bool {
239
+ const footer = "TRANSCRIPT_SHA256: "
240
+ end := strings.LastIndex(raw, footer)
241
+ if end < 0 || raw[end:] != footer+OutputDigest(raw[:end])+"\n" {
242
+ return false
243
+ }
244
+ header, output, ok := strings.Cut(raw[:end], "\n--- OUTPUT ---\n")
245
+ if !ok || !strings.HasSuffix(output, "\n--- END OUTPUT ---\n") {
246
+ return false
247
+ }
248
+ output = strings.TrimSuffix(output, "\n--- END OUTPUT ---\n")
249
+ lines := strings.Split(header, "\n")
250
+ if lines[0] != "DEVRITES-GATE-TRANSCRIPT v2" {
251
+ return false
252
+ }
253
+ fields := map[string]string{}
254
+ for _, line := range lines[1:] {
255
+ key, value, ok := strings.Cut(line, ": ")
256
+ if _, duplicate := fields[key]; !ok || duplicate {
257
+ return false
258
+ }
259
+ fields[key] = value
260
+ }
261
+ for key, want := range map[string]string{
262
+ "GATE": transcriptMetadata(gate.ID), "TITLE": transcriptMetadata(gate.Title),
263
+ "COMMAND": transcriptMetadata(gate.Check), "CWD": transcriptMetadata(valueOr(gate.Cwd, ".")),
264
+ "EXPECT": transcriptMetadata(gate.Expect), "DEFINITION_SHA256": evidence[1],
265
+ "INPUTS": transcriptMetadata(valueOr(gate.Inputs, "none")), "INPUTS_SHA256": valueOr(evidence[4], "none"),
266
+ "RESULT": "pass", "EXIT": "0", "OUTPUT_SHA256": evidence[2], "OUTPUT_BYTES": evidence[3],
267
+ } {
268
+ if got, exists := fields[key]; !exists || got != want {
269
+ return false
270
+ }
271
+ }
272
+ started, err := time.Parse(time.RFC3339Nano, fields["STARTED"])
273
+ if err != nil || started.IsZero() {
274
+ return false
275
+ }
276
+ ended, err := time.Parse(time.RFC3339Nano, fields["ENDED"])
277
+ if err != nil || ended.Before(started) {
278
+ return false
279
+ }
280
+ attempt, err := strconv.Atoi(fields["ATTEMPT"])
281
+ if err != nil || attempt < 1 || fields["CANDIDATE_BEFORE"] == "" || fields["CANDIDATE_AFTER"] == "" {
282
+ return false
283
+ }
284
+ redactions, err := strconv.Atoi(fields["REDACTIONS"])
285
+ if err != nil || redactions < 0 {
286
+ return false
287
+ }
288
+ truncated, err := strconv.ParseBool(fields["TRUNCATED"])
289
+ return err == nil && len(output) <= maxTranscriptOutput && (!truncated || len(output) == maxTranscriptOutput)
290
+ }
291
+
101
292
  func exitLabel(outcome RunOutcome) string {
102
293
  switch {
103
294
  case outcome.TimedOut:
@@ -47,9 +47,13 @@ func WorkspaceOverrideChecked(root, slug string) (string, error) {
47
47
  if raw == "" {
48
48
  return "", nil
49
49
  }
50
+ absoluteRoot, err := filepath.Abs(root)
51
+ if err != nil {
52
+ return "", fmt.Errorf("DRV-WORKSPACE-INVALID: resolve DevRites root %q: %w", root, err)
53
+ }
50
54
  path := raw
51
55
  if !filepath.IsAbs(path) {
52
- path = filepath.Join(filepath.Dir(root), path)
56
+ path = filepath.Join(filepath.Dir(absoluteRoot), path)
53
57
  }
54
58
  abs, err := filepath.Abs(path)
55
59
  if err != nil {
@@ -59,10 +63,10 @@ func WorkspaceOverrideChecked(root, slug string) (string, error) {
59
63
  if err != nil {
60
64
  return "", fmt.Errorf("DRV-WORKSPACE-INVALID: resolve DEVRITES_WORKSPACE %q: %w", raw, err)
61
65
  }
62
- if !safepath.WithinResolved(path, root) {
66
+ if !safepath.WithinResolved(path, absoluteRoot) {
63
67
  return "", fmt.Errorf("DRV-WORKSPACE-OUTSIDE-ROOT: DEVRITES_WORKSPACE %q is outside %q; run `unset DEVRITES_WORKSPACE`", raw, root)
64
68
  }
65
- resolvedRoot, err := safepath.ResolveExisting(root)
69
+ resolvedRoot, err := safepath.ResolveExisting(absoluteRoot)
66
70
  if err != nil {
67
71
  return "", fmt.Errorf("DRV-WORKSPACE-INVALID: resolve DevRites root %q: %w", root, err)
68
72
  }
@@ -114,6 +118,15 @@ func ExistingFeatureDirChecked(root, slug string) (string, error) {
114
118
  }
115
119
  return ws, nil
116
120
  }
121
+ return ExistingCanonicalFeatureDirChecked(root, slug)
122
+ }
123
+
124
+ // ExistingCanonicalFeatureDirChecked resolves an explicit producer workspace
125
+ // inside root without redirecting it through a consumer's workspace override.
126
+ func ExistingCanonicalFeatureDirChecked(root, slug string) (string, error) {
127
+ if !ValidSlug(slug) {
128
+ return "", fmt.Errorf("DRV-WORKSPACE-INVALID: %q is not a feature slug", slug)
129
+ }
117
130
  candidate := filepath.Join(root, "work", slug)
118
131
  resolved, err := checkedCanonicalDir(root, candidate, "work", slug)
119
132
  if err == nil {
@@ -51,6 +51,12 @@ func Check(kind Kind, root, slug string) (*Result, error) {
51
51
  if err != nil {
52
52
  return nil, fmt.Errorf("gate %s: %w", kind, err)
53
53
  }
54
+ return CheckObserved(kind, root, observation)
55
+ }
56
+
57
+ // CheckObserved checks retained facts acquired from root, so a caller can use
58
+ // those same vetted bytes for subsequent admission without rereading the workspace.
59
+ func CheckObserved(kind Kind, root string, observation *state.WorkspaceObservation) (*Result, error) {
54
60
  result, err := checkObservation(kind, observation)
55
61
  if err != nil {
56
62
  return nil, fmt.Errorf("gate %s: %w", kind, err)
@@ -38,6 +38,7 @@ type candidateRow struct {
38
38
  executable bool
39
39
  size int64
40
40
  info os.FileInfo
41
+ fileDigest string
41
42
  }
42
43
 
43
44
  type candidateIdentity struct {
@@ -72,7 +73,7 @@ func loadCandidateIdentity(root, slug string) (candidateIdentity, error) {
72
73
  if err := validateCandidateRows(project, rows); err != nil {
73
74
  return candidateIdentity{}, err
74
75
  }
75
- digest, err := hashCandidate(project, rows)
76
+ digest, err := hashCandidate(project, rows, false)
76
77
  if err != nil {
77
78
  return candidateIdentity{}, err
78
79
  }
@@ -407,15 +408,18 @@ func candidatePathInfo(project, name string) (os.FileInfo, bool, error) {
407
408
  return nil, false, errors.New("path is empty")
408
409
  }
409
410
 
410
- func hashCandidate(project string, rows []candidateRow) (string, error) {
411
+ func hashCandidate(project string, rows []candidateRow, perFile bool) (string, error) {
411
412
  digest := sha256.New()
412
413
  writeCandidateField(digest, candidateDigestDomain)
413
414
  writeCandidateField(digest, candidateDigestVersion)
414
415
  writeCandidateField(digest, fmt.Sprintf("%d", len(rows)))
415
- for _, row := range rows {
416
+ for i, row := range rows {
416
417
  writeCandidateField(digest, row.state)
417
418
  writeCandidateField(digest, row.path)
418
419
  if row.state == "deleted" {
420
+ if err := validateCandidateRow(project, &row); err != nil {
421
+ return "", fmt.Errorf("candidate %q: %w", row.path, err)
422
+ }
419
423
  writeCandidateField(digest, "absent")
420
424
  writeCandidateField(digest, "0")
421
425
  writeCandidateLength(digest, 0)
@@ -428,9 +432,18 @@ func hashCandidate(project string, rows []candidateRow) (string, error) {
428
432
  writeCandidateField(digest, "0")
429
433
  }
430
434
  writeCandidateLength(digest, uint64(row.size)) // #nosec G115 -- validateCandidateRow bounds size to 0..64 MiB
431
- if err := hashCandidateFile(digest, project, row); err != nil {
435
+ var writer io.Writer = digest
436
+ var fileDigest hash.Hash
437
+ if perFile {
438
+ fileDigest = sha256.New()
439
+ writer = io.MultiWriter(digest, fileDigest)
440
+ }
441
+ if err := hashCandidateFile(writer, project, row); err != nil {
432
442
  return "", fmt.Errorf("candidate %q: %w", row.path, err)
433
443
  }
444
+ if perFile {
445
+ rows[i].fileDigest = hex.EncodeToString(fileDigest.Sum(nil))
446
+ }
434
447
  }
435
448
  return hex.EncodeToString(digest.Sum(nil)), nil
436
449
  }
@@ -446,7 +459,7 @@ func writeCandidateLength(writer hash.Hash, size uint64) {
446
459
  _, _ = writer.Write(encoded[:])
447
460
  }
448
461
 
449
- func hashCandidateFile(digest hash.Hash, project string, row candidateRow) error {
462
+ func hashCandidateFile(digest io.Writer, project string, row candidateRow) error {
450
463
  if _, exists, err := candidatePathInfo(project, row.path); err != nil || !exists {
451
464
  if err != nil {
452
465
  return err
@@ -464,7 +477,7 @@ func hashCandidateFile(digest hash.Hash, project string, row candidateRow) error
464
477
  if err != nil {
465
478
  return err
466
479
  }
467
- if !opened.Mode().IsRegular() || opened.Size() != row.size || (opened.Mode().Perm()&0o111 != 0) != row.executable || !os.SameFile(row.info, opened) {
480
+ if !opened.Mode().IsRegular() || opened.Size() != row.size || (opened.Mode().Perm()&0o111 != 0) != row.executable || !os.SameFile(row.info, opened) || !row.info.ModTime().Equal(opened.ModTime()) {
468
481
  return errors.New("file changed size or type before reading")
469
482
  }
470
483
  written, err := io.CopyN(digest, file, row.size)
@@ -483,7 +496,7 @@ func hashCandidateFile(digest hash.Hash, project string, row candidateRow) error
483
496
  if err != nil {
484
497
  return err
485
498
  }
486
- if !exists || !after.Mode().IsRegular() || after.Size() != row.size || (after.Mode().Perm()&0o111 != 0) != row.executable || !os.SameFile(opened, after) || !os.SameFile(opened, current) {
499
+ if !exists || !after.Mode().IsRegular() || after.Size() != row.size || current.Size() != row.size || (after.Mode().Perm()&0o111 != 0) != row.executable || (current.Mode().Perm()&0o111 != 0) != row.executable || !os.SameFile(opened, after) || !os.SameFile(opened, current) || !opened.ModTime().Equal(after.ModTime()) || !opened.ModTime().Equal(current.ModTime()) {
487
500
  return errors.New("file changed size or type while reading")
488
501
  }
489
502
  return nil
@@ -1,9 +1,7 @@
1
1
  package lib
2
2
 
3
3
  import (
4
- "errors"
5
4
  "fmt"
6
- "os"
7
5
  "path/filepath"
8
6
  "regexp"
9
7
  "sort"
@@ -118,7 +116,7 @@ func omissionError(omitted []string) error {
118
116
  len(omitted), strings.Join(shown, ", "), more)
119
117
  }
120
118
 
121
- var diffPathRE = regexp.MustCompile(`[A-Za-z0-9_./@+-]+\.(?:diff|patch)\b`)
119
+ var diffPathRE = regexp.MustCompile(`(?:[A-Za-z]:/)?[A-Za-z0-9_./@+-]+\.(?:diff|patch)\b`)
122
120
 
123
121
  // lintReviewCandidate gates a Review or Seal reviewer wave: the candidate
124
122
  // manifest must cover every path the feature's checkpoints changed, and the
@@ -128,20 +126,5 @@ func lintReviewCandidate(root, slug, packet string) error {
128
126
  if err := CheckCandidateCoverage(root, slug); err != nil {
129
127
  return err
130
128
  }
131
- raw, err := readBoundedRegularFile(packet, maxContractBytes)
132
- if err != nil {
133
- return fmt.Errorf("reviewer packet %s: %w", packet, err)
134
- }
135
- for _, name := range diffPathRE.FindAllString(string(raw), -1) {
136
- for _, dir := range []string{projectDir(root), featureDir(root, slug), ""} {
137
- candidate := name
138
- if dir != "" {
139
- candidate = filepath.Join(dir, filepath.FromSlash(name))
140
- }
141
- if info, err := os.Stat(candidate); err == nil && info.Mode().IsRegular() && info.Size() > 0 {
142
- return nil
143
- }
144
- }
145
- }
146
- return errors.New("a Review or Seal reviewer packet must name the frozen candidate diff (an existing non-empty .diff or .patch file); reviewers cannot judge a digest alone")
129
+ return lintPacketDiffs(root, slug, packet, true)
147
130
  }
@@ -10,6 +10,7 @@ import (
10
10
  "runtime"
11
11
 
12
12
  "github.com/devrites/devrites/internal/devritespaths"
13
+ "github.com/devrites/devrites/internal/parallel"
13
14
  "github.com/devrites/devrites/internal/state"
14
15
  )
15
16
 
@@ -78,11 +79,22 @@ func CloseOut(root string, args []string, stdout, stderr io.Writer) int {
78
79
  // re-create work/<slug>. Windows cannot rename a directory that holds an
79
80
  // open file, so there the lock is released just before the rename.
80
81
  code := 1
81
- if err := state.WithFeatureLockRelease(dv, slug, func(release func()) error {
82
- code = closeOutLocked(release, work, arch, archiveDir, archDisplay, active, slug, stdout, stderr)
83
- return nil
82
+ if err := parallel.WithControlLock(dv, func() error {
83
+ leased, err := parallel.WorkspaceLeased(dv, slug)
84
+ if err != nil {
85
+ return err
86
+ }
87
+ if leased {
88
+ fmt.Fprintf(stderr, "state close: BLOCKED: %s is named by a retained parallel lease; finish and clean up the controller lease before archiving\n", slug)
89
+ code = 3
90
+ return nil
91
+ }
92
+ return state.WithFeatureLockRelease(dv, slug, func(release func()) error {
93
+ code = closeOutLocked(release, work, arch, archiveDir, archDisplay, active, slug, stdout, stderr)
94
+ return nil
95
+ })
84
96
  }); err != nil {
85
- fmt.Fprintf(stderr, "state close: cannot lock feature %s: %v\n", slug, err)
97
+ fmt.Fprintf(stderr, "state close: %v\n", err)
86
98
  return 1
87
99
  }
88
100
  return code