devrites 3.0.6 → 3.1.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 (472) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +67 -45
  3. package/SECURITY.md +31 -29
  4. package/docs/adr/0001-go-engine-as-control-plane.md +1 -1
  5. package/docs/adr/0002-dual-host-harness.md +1 -1
  6. package/docs/adr/0004-state-schema-phases-sections.md +1 -2
  7. package/docs/adr/0006-clock-seam-and-engine-ci-gates.md +3 -4
  8. package/docs/adr/0009-prebuild-decision-coverage-and-readiness.md +67 -0
  9. package/docs/adr/0010-agent-first-fresh-context-orchestration.md +71 -0
  10. package/docs/adr/0011-define-plan-transition-rights.md +28 -0
  11. package/docs/adr/README.md +3 -0
  12. package/docs/agents/triage-labels.md +7 -7
  13. package/docs/architecture.md +157 -140
  14. package/docs/cli.md +41 -12
  15. package/docs/command-map.md +49 -35
  16. package/docs/engine/agent-contract.md +92 -15
  17. package/docs/engine/commands.md +242 -72
  18. package/docs/engine/state-schema.md +52 -23
  19. package/docs/engine/workspace-schema.md +94 -15
  20. package/docs/extensions.md +1 -1
  21. package/docs/flow.md +80 -50
  22. package/docs/harness-compliance.md +29 -5
  23. package/docs/orchestration.md +107 -79
  24. package/docs/quick-reference.md +7 -3
  25. package/docs/release.md +4 -3
  26. package/docs/skills.md +64 -38
  27. package/docs/usage.md +57 -38
  28. package/engine/cmd/releasepack/main.go +219 -0
  29. package/engine/cmd/releasepack/main_test.go +170 -0
  30. package/engine/commands.go +170 -23
  31. package/engine/git_guard.go +187 -0
  32. package/engine/git_guard_test.go +283 -0
  33. package/engine/hookpolicy.go +53 -55
  34. package/engine/hookpolicy_test.go +91 -1
  35. package/engine/hooks.go +296 -75
  36. package/engine/hooks_events_test.go +276 -6
  37. package/engine/hooks_workspace.go +640 -159
  38. package/engine/internal/devritespaths/paths.go +65 -10
  39. package/engine/internal/devritespaths/paths_test.go +110 -0
  40. package/engine/internal/doctor/doctor.go +153 -23
  41. package/engine/internal/doctor/doctor_test.go +74 -0
  42. package/engine/internal/forge/forge.go +940 -0
  43. package/engine/internal/forge/forge_test.go +576 -0
  44. package/engine/internal/forge/git.go +245 -0
  45. package/engine/internal/forge/liveness_unix.go +48 -0
  46. package/engine/internal/forge/liveness_windows.go +67 -0
  47. package/engine/internal/forge/manifest.go +402 -0
  48. package/engine/internal/gate/gate.go +90 -71
  49. package/engine/internal/gate/gate_test.go +71 -2
  50. package/engine/internal/harness/compliance.go +18 -24
  51. package/engine/internal/harness/harness.go +37 -44
  52. package/engine/internal/harness/harness_test.go +21 -5
  53. package/engine/internal/install/install.go +575 -58
  54. package/engine/internal/install/install_test.go +490 -9
  55. package/engine/internal/iohooks/iohooks.go +350 -59
  56. package/engine/internal/iohooks/iohooks_test.go +421 -1
  57. package/engine/internal/lib/buildreadiness.go +35 -18
  58. package/engine/internal/lib/clarifyreturn.go +91 -0
  59. package/engine/internal/lib/clarifyreturn_test.go +123 -0
  60. package/engine/internal/lib/context.go +54 -15
  61. package/engine/internal/lib/cursor_compat_test.go +8 -0
  62. package/engine/internal/lib/extensions.go +2 -3
  63. package/engine/internal/lib/gitauthority.go +601 -0
  64. package/engine/internal/lib/gitauthority_test.go +346 -0
  65. package/engine/internal/lib/jsonout.go +25 -11
  66. package/engine/internal/lib/jsonout_test.go +16 -0
  67. package/engine/internal/lib/lanes.go +14 -8
  68. package/engine/internal/lib/observability_test.go +358 -0
  69. package/engine/internal/lib/packageexistence.go +151 -35
  70. package/engine/internal/lib/packageexistence_test.go +163 -0
  71. package/engine/internal/lib/progress.go +11 -9
  72. package/engine/internal/lib/provenance.go +462 -0
  73. package/engine/internal/lib/provenance_test.go +154 -0
  74. package/engine/internal/lib/readiness_contract.json +111 -0
  75. package/engine/internal/lib/readinessartifact.go +510 -0
  76. package/engine/internal/lib/readinessartifact_test.go +321 -0
  77. package/engine/internal/lib/reconcile.go +712 -88
  78. package/engine/internal/lib/reconcile_test.go +335 -16
  79. package/engine/internal/lib/recoveryattempts.go +298 -0
  80. package/engine/internal/lib/recoveryattempts_test.go +215 -0
  81. package/engine/internal/lib/resolve.go +23 -19
  82. package/engine/internal/lib/runbook_context_test.go +20 -0
  83. package/engine/internal/lib/session.go +701 -9
  84. package/engine/internal/lib/session_test.go +80 -13
  85. package/engine/internal/lib/testintegrity.go +33 -37
  86. package/engine/internal/lib/testintegrity_test.go +63 -1
  87. package/engine/internal/migrate/migrate.go +81 -1
  88. package/engine/internal/migrate/migrate_test.go +22 -1
  89. package/engine/internal/reason/reason.go +180 -0
  90. package/engine/internal/reason/reason_test.go +35 -0
  91. package/engine/internal/rootfacts/facts.go +466 -0
  92. package/engine/internal/rootfacts/facts_test.go +306 -0
  93. package/engine/internal/safepath/safepath.go +55 -0
  94. package/engine/internal/safepath/safepath_test.go +69 -0
  95. package/engine/internal/safepath/safepath_windows_test.go +26 -0
  96. package/engine/internal/state/clarify_transition.go +165 -0
  97. package/engine/internal/state/clarify_transition_test.go +130 -0
  98. package/engine/internal/state/cmd/workflowmanifest/main.go +12 -8
  99. package/engine/internal/state/cursor.go +58 -0
  100. package/engine/internal/state/cursor_test.go +42 -1
  101. package/engine/internal/state/feature.go +35 -48
  102. package/engine/internal/state/schema.go +90 -37
  103. package/engine/internal/state/snapshot.go +18 -2
  104. package/engine/internal/state/state_test.go +187 -7
  105. package/engine/internal/state/status.go +48 -11
  106. package/engine/internal/state/workflow_manifest.json +231 -50
  107. package/engine/internal/toolpolicy/classifier.go +533 -0
  108. package/engine/internal/toolpolicy/classifier_test.go +424 -0
  109. package/engine/internal/toolpolicy/git.go +616 -0
  110. package/engine/internal/toolpolicy/scanner.go +382 -0
  111. package/engine/main.go +160 -77
  112. package/engine/observability_cli_test.go +52 -0
  113. package/engine/root_routing_test.go +277 -0
  114. package/engine/testdata/golden/TestParityBuildReadiness/arg=approved.golden +1 -1
  115. package/engine/testdata/golden/TestParityBuildReadiness/arg=emptystatus.golden +1 -1
  116. package/engine/testdata/golden/TestParityBuildReadiness/arg=noclarify.golden +1 -0
  117. package/engine/testdata/golden/TestParityBuildReadiness/arg=novet.golden +1 -0
  118. package/engine/testdata/golden/TestParityBuildReadiness/arg=stalevet.golden +1 -0
  119. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailhash.golden +1 -1
  120. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailpipe.golden +1 -1
  121. package/engine/testdata/golden/TestParityBuildReadiness/arg=vetnotready.golden +1 -0
  122. package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
  123. package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
  124. package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
  125. package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
  126. package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
  127. package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
  128. package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
  129. package/engine/testdata/golden/TestParityReconcile/check-clean.golden +1 -1
  130. package/engine/testdata/golden/TestParityReconcile/inline-fallback.golden +1 -0
  131. package/engine/testdata/golden/TestParityReconcile/snapshot-no-allowlist.golden +1 -0
  132. package/engine/testdata/golden/TestParityWrightScope/devrites-edit-denied.golden +2 -0
  133. package/engine/testdata/golden/TestParityWrightScope/out-of-scope-enforce-denies.golden +1 -1
  134. package/engine/tests/adr_0011_define_plan_test.go +30 -0
  135. package/engine/tests/budget_test.go +19 -8
  136. package/engine/tests/concurrency_cli_test.go +7 -1
  137. package/engine/tests/doctor_cli_test.go +148 -2
  138. package/engine/tests/forge_cli_test.go +463 -0
  139. package/engine/tests/gate_test.go +63 -8
  140. package/engine/tests/hook_test.go +260 -8
  141. package/engine/tests/hooks_io_test.go +94 -3
  142. package/engine/tests/json_contract_test.go +127 -2
  143. package/engine/tests/migrate_cli_test.go +2 -2
  144. package/engine/tests/parity_buildreadiness_test.go +141 -10
  145. package/engine/tests/parity_learnings_test.go +16 -17
  146. package/engine/tests/parity_reconcile_test.go +20 -17
  147. package/engine/tests/parity_resolve_test.go +12 -11
  148. package/engine/tests/parity_test.go +21 -17
  149. package/pack/.claude/agents/devrites-code-reviewer.md +62 -48
  150. package/pack/.claude/agents/devrites-devex-reviewer.md +69 -53
  151. package/pack/.claude/agents/devrites-doubt-reviewer.md +29 -22
  152. package/pack/.claude/agents/devrites-evidence-scout.md +69 -0
  153. package/pack/.claude/agents/devrites-forge-judge.md +74 -61
  154. package/pack/.claude/agents/devrites-frontend-reviewer.md +48 -39
  155. package/pack/.claude/agents/devrites-performance-reviewer.md +49 -40
  156. package/pack/.claude/agents/devrites-plan-drafter.md +71 -0
  157. package/pack/.claude/agents/devrites-plan-reviewer.md +80 -47
  158. package/pack/.claude/agents/devrites-proof-runner.md +74 -0
  159. package/pack/.claude/agents/devrites-retrospector.md +48 -45
  160. package/pack/.claude/agents/devrites-security-auditor.md +46 -36
  161. package/pack/.claude/agents/devrites-simplifier-reviewer.md +50 -46
  162. package/pack/.claude/agents/devrites-slice-wright.md +153 -165
  163. package/pack/.claude/agents/devrites-spec-reviewer.md +34 -32
  164. package/pack/.claude/agents/devrites-strategy-reviewer.md +63 -34
  165. package/pack/.claude/agents/devrites-test-analyst.md +40 -30
  166. package/pack/.claude/settings.json +2 -1
  167. package/pack/.claude/skills/devrites-audit/SKILL.md +40 -61
  168. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +24 -13
  169. package/pack/.claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  170. package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  171. package/pack/.claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  172. package/pack/.claude/skills/devrites-doubt/SKILL.md +25 -17
  173. package/pack/.claude/skills/devrites-interview/SKILL.md +53 -52
  174. package/pack/.claude/skills/devrites-lib/SKILL.md +11 -9
  175. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +3 -2
  176. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  177. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +8 -1
  178. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +38 -55
  179. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  180. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +198 -190
  181. package/pack/.claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  182. package/pack/.claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  183. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
  184. package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  185. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  186. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +23 -18
  187. package/pack/.claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  188. package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  189. package/pack/.claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  190. package/pack/.claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
  191. package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
  192. package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +0 -9
  193. package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +1 -3
  194. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +17 -7
  195. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +1 -27
  196. package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
  197. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
  198. package/pack/.claude/skills/devrites-source-driven/SKILL.md +23 -22
  199. package/pack/.claude/skills/rite/SKILL.md +8 -6
  200. package/pack/.claude/skills/rite/reference/menu.md +8 -6
  201. package/pack/.claude/skills/rite-adopt/SKILL.md +33 -37
  202. package/pack/.claude/skills/rite-autocomplete/SKILL.md +33 -28
  203. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +21 -21
  204. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  205. package/pack/.claude/skills/rite-build/SKILL.md +35 -30
  206. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +38 -38
  207. package/pack/.claude/skills/rite-build/reference/evidence-standard.md +10 -0
  208. package/pack/.claude/skills/rite-build/reference/forge.md +186 -156
  209. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
  210. package/pack/.claude/skills/rite-build/reference/phase-contract.md +96 -175
  211. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +129 -134
  212. package/pack/.claude/skills/rite-clarify/SKILL.md +89 -0
  213. package/pack/.claude/skills/rite-clarify/reference/decision-coverage.md +59 -0
  214. package/pack/.claude/skills/rite-converge/SKILL.md +35 -25
  215. package/pack/.claude/skills/rite-define/SKILL.md +57 -32
  216. package/pack/.claude/skills/rite-define/reference/gates.md +16 -15
  217. package/pack/.claude/skills/rite-define/reference/plan-template.md +16 -8
  218. package/pack/.claude/skills/rite-frame/reference/failure-modes.md +22 -24
  219. package/pack/.claude/skills/rite-plan/SKILL.md +44 -16
  220. package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +4 -0
  221. package/pack/.claude/skills/rite-polish/SKILL.md +27 -23
  222. package/pack/.claude/skills/rite-prototype/SKILL.md +25 -26
  223. package/pack/.claude/skills/rite-prove/SKILL.md +27 -17
  224. package/pack/.claude/skills/rite-resolve/SKILL.md +12 -11
  225. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
  226. package/pack/.claude/skills/rite-review/SKILL.md +37 -28
  227. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +27 -92
  228. package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  229. package/pack/.claude/skills/rite-ship/reference/design-memory.md +31 -36
  230. package/pack/.claude/skills/rite-spec/SKILL.md +84 -135
  231. package/pack/.claude/skills/rite-spec/reference/investigation.md +37 -33
  232. package/pack/.claude/skills/rite-spec/reference/question-protocol.md +6 -2
  233. package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +25 -24
  234. package/pack/.claude/skills/rite-spec/reference/spec-template.md +9 -2
  235. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +13 -3
  236. package/pack/.claude/skills/rite-temper/SKILL.md +62 -47
  237. package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +24 -22
  238. package/pack/.claude/skills/rite-vet/SKILL.md +110 -113
  239. package/pack/.claude/skills/rite-vet/reference/artifacts.md +42 -9
  240. package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -37
  241. package/pack/generated/claude/agents/devrites-code-reviewer.md +62 -48
  242. package/pack/generated/claude/agents/devrites-devex-reviewer.md +69 -53
  243. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +29 -22
  244. package/pack/generated/claude/agents/devrites-evidence-scout.md +69 -0
  245. package/pack/generated/claude/agents/devrites-forge-judge.md +74 -61
  246. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +48 -39
  247. package/pack/generated/claude/agents/devrites-performance-reviewer.md +49 -40
  248. package/pack/generated/claude/agents/devrites-plan-drafter.md +71 -0
  249. package/pack/generated/claude/agents/devrites-plan-reviewer.md +80 -47
  250. package/pack/generated/claude/agents/devrites-proof-runner.md +74 -0
  251. package/pack/generated/claude/agents/devrites-retrospector.md +48 -45
  252. package/pack/generated/claude/agents/devrites-security-auditor.md +46 -36
  253. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +50 -46
  254. package/pack/generated/claude/agents/devrites-slice-wright.md +153 -165
  255. package/pack/generated/claude/agents/devrites-spec-reviewer.md +34 -32
  256. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +63 -34
  257. package/pack/generated/claude/agents/devrites-test-analyst.md +40 -30
  258. package/pack/generated/claude/settings.json +2 -1
  259. package/pack/generated/claude/skills/devrites-audit/SKILL.md +40 -61
  260. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +24 -13
  261. package/pack/generated/claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  262. package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  263. package/pack/generated/claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  264. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +25 -17
  265. package/pack/generated/claude/skills/devrites-interview/SKILL.md +53 -52
  266. package/pack/generated/claude/skills/devrites-lib/SKILL.md +11 -9
  267. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +3 -2
  268. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  269. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +8 -1
  270. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +38 -55
  271. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  272. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +198 -190
  273. package/pack/generated/claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  274. package/pack/generated/claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  275. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
  276. package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  277. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  278. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +23 -18
  279. package/pack/generated/claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  280. package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  281. package/pack/generated/claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  282. package/pack/generated/claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
  283. package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
  284. package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +0 -9
  285. package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +1 -3
  286. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +17 -7
  287. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +1 -27
  288. package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
  289. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
  290. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +23 -22
  291. package/pack/generated/claude/skills/rite/SKILL.md +8 -6
  292. package/pack/generated/claude/skills/rite/reference/menu.md +8 -6
  293. package/pack/generated/claude/skills/rite-adopt/SKILL.md +33 -37
  294. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +33 -28
  295. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +21 -21
  296. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  297. package/pack/generated/claude/skills/rite-build/SKILL.md +35 -30
  298. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +38 -38
  299. package/pack/generated/claude/skills/rite-build/reference/evidence-standard.md +10 -0
  300. package/pack/generated/claude/skills/rite-build/reference/forge.md +186 -156
  301. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
  302. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +96 -175
  303. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +129 -134
  304. package/pack/generated/claude/skills/rite-clarify/SKILL.md +89 -0
  305. package/pack/generated/claude/skills/rite-clarify/reference/decision-coverage.md +59 -0
  306. package/pack/generated/claude/skills/rite-converge/SKILL.md +35 -25
  307. package/pack/generated/claude/skills/rite-define/SKILL.md +57 -32
  308. package/pack/generated/claude/skills/rite-define/reference/gates.md +16 -15
  309. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +16 -8
  310. package/pack/generated/claude/skills/rite-frame/reference/failure-modes.md +22 -24
  311. package/pack/generated/claude/skills/rite-plan/SKILL.md +44 -16
  312. package/pack/generated/claude/skills/rite-plan/reference/task-breakdown.md +4 -0
  313. package/pack/generated/claude/skills/rite-polish/SKILL.md +27 -23
  314. package/pack/generated/claude/skills/rite-prototype/SKILL.md +25 -26
  315. package/pack/generated/claude/skills/rite-prove/SKILL.md +27 -17
  316. package/pack/generated/claude/skills/rite-resolve/SKILL.md +12 -11
  317. package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
  318. package/pack/generated/claude/skills/rite-review/SKILL.md +37 -28
  319. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +27 -92
  320. package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  321. package/pack/generated/claude/skills/rite-ship/reference/design-memory.md +31 -36
  322. package/pack/generated/claude/skills/rite-spec/SKILL.md +84 -135
  323. package/pack/generated/claude/skills/rite-spec/reference/investigation.md +37 -33
  324. package/pack/generated/claude/skills/rite-spec/reference/question-protocol.md +6 -2
  325. package/pack/generated/claude/skills/rite-spec/reference/spec-checklists.md +25 -24
  326. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +9 -2
  327. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +13 -3
  328. package/pack/generated/claude/skills/rite-temper/SKILL.md +62 -47
  329. package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +24 -22
  330. package/pack/generated/claude/skills/rite-vet/SKILL.md +110 -113
  331. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +42 -9
  332. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -37
  333. package/pack/generated/codex/AGENTS.md +5 -2
  334. package/pack/generated/codex/agents/devrites-code-reviewer.toml +69 -47
  335. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +75 -51
  336. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +35 -20
  337. package/pack/generated/codex/agents/devrites-evidence-scout.toml +75 -0
  338. package/pack/generated/codex/agents/devrites-forge-judge.toml +80 -59
  339. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +54 -37
  340. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +55 -38
  341. package/pack/generated/codex/agents/devrites-plan-drafter.toml +77 -0
  342. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +82 -47
  343. package/pack/generated/codex/agents/devrites-proof-runner.toml +80 -0
  344. package/pack/generated/codex/agents/devrites-retrospector.toml +54 -43
  345. package/pack/generated/codex/agents/devrites-security-auditor.toml +53 -35
  346. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +56 -44
  347. package/pack/generated/codex/agents/devrites-slice-wright.toml +159 -163
  348. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +40 -30
  349. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +65 -34
  350. package/pack/generated/codex/agents/devrites-test-analyst.toml +46 -28
  351. package/pack/generated/codex/hooks.json +4 -14
  352. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +7 -3
  353. package/pack/generated/codex/skills/devrites-audit/SKILL.md +47 -64
  354. package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +7 -3
  355. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +31 -16
  356. package/pack/generated/codex/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  357. package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  358. package/pack/generated/codex/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  359. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +32 -20
  360. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +7 -3
  361. package/pack/generated/codex/skills/devrites-interview/SKILL.md +60 -55
  362. package/pack/generated/codex/skills/devrites-lib/SKILL.md +18 -12
  363. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +3 -2
  364. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  365. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +8 -1
  366. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +38 -55
  367. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  368. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +198 -190
  369. package/pack/generated/codex/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  370. package/pack/generated/codex/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  371. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +16 -52
  372. package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  373. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  374. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +23 -18
  375. package/pack/generated/codex/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  376. package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  377. package/pack/generated/codex/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  378. package/pack/generated/codex/skills/devrites-lib/reference/standards/hooks.md +3 -14
  379. package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +9 -25
  380. package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +0 -9
  381. package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +1 -3
  382. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +17 -7
  383. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +1 -27
  384. package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +46 -50
  385. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
  386. package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +7 -3
  387. package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +7 -3
  388. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +29 -24
  389. package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +7 -3
  390. package/pack/generated/codex/skills/rite/SKILL.md +19 -13
  391. package/pack/generated/codex/skills/rite/reference/menu.md +8 -6
  392. package/pack/generated/codex/skills/rite-adopt/SKILL.md +40 -40
  393. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +40 -31
  394. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +21 -21
  395. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  396. package/pack/generated/codex/skills/rite-build/SKILL.md +42 -33
  397. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +38 -38
  398. package/pack/generated/codex/skills/rite-build/reference/evidence-standard.md +10 -0
  399. package/pack/generated/codex/skills/rite-build/reference/forge.md +186 -156
  400. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +4 -3
  401. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +96 -175
  402. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +129 -134
  403. package/pack/generated/codex/skills/rite-clarify/SKILL.md +105 -0
  404. package/pack/generated/codex/skills/rite-clarify/reference/decision-coverage.md +59 -0
  405. package/pack/generated/codex/skills/rite-converge/SKILL.md +42 -28
  406. package/pack/generated/codex/skills/rite-customize/SKILL.md +7 -3
  407. package/pack/generated/codex/skills/rite-define/SKILL.md +64 -35
  408. package/pack/generated/codex/skills/rite-define/reference/gates.md +16 -15
  409. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +16 -8
  410. package/pack/generated/codex/skills/rite-doctor/SKILL.md +7 -3
  411. package/pack/generated/codex/skills/rite-dogfood/SKILL.md +7 -3
  412. package/pack/generated/codex/skills/rite-explain/SKILL.md +7 -3
  413. package/pack/generated/codex/skills/rite-frame/SKILL.md +7 -3
  414. package/pack/generated/codex/skills/rite-frame/reference/failure-modes.md +22 -24
  415. package/pack/generated/codex/skills/rite-handoff/SKILL.md +7 -3
  416. package/pack/generated/codex/skills/rite-learn/SKILL.md +7 -3
  417. package/pack/generated/codex/skills/rite-plan/SKILL.md +51 -19
  418. package/pack/generated/codex/skills/rite-plan/reference/task-breakdown.md +5 -1
  419. package/pack/generated/codex/skills/rite-polish/SKILL.md +34 -26
  420. package/pack/generated/codex/skills/rite-pov/SKILL.md +7 -3
  421. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -3
  422. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +7 -3
  423. package/pack/generated/codex/skills/rite-prototype/SKILL.md +32 -29
  424. package/pack/generated/codex/skills/rite-prove/SKILL.md +34 -20
  425. package/pack/generated/codex/skills/rite-quick/SKILL.md +7 -3
  426. package/pack/generated/codex/skills/rite-resolve/SKILL.md +19 -14
  427. package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +3 -0
  428. package/pack/generated/codex/skills/rite-review/SKILL.md +44 -31
  429. package/pack/generated/codex/skills/rite-seal/SKILL.md +7 -3
  430. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +27 -92
  431. package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  432. package/pack/generated/codex/skills/rite-ship/SKILL.md +7 -3
  433. package/pack/generated/codex/skills/rite-ship/reference/design-memory.md +31 -36
  434. package/pack/generated/codex/skills/rite-spec/SKILL.md +91 -138
  435. package/pack/generated/codex/skills/rite-spec/reference/investigation.md +37 -33
  436. package/pack/generated/codex/skills/rite-spec/reference/question-protocol.md +6 -2
  437. package/pack/generated/codex/skills/rite-spec/reference/spec-checklists.md +25 -24
  438. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +9 -2
  439. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +13 -3
  440. package/pack/generated/codex/skills/rite-status/SKILL.md +7 -3
  441. package/pack/generated/codex/skills/rite-temper/SKILL.md +69 -50
  442. package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +24 -22
  443. package/pack/generated/codex/skills/rite-vet/SKILL.md +117 -116
  444. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +42 -9
  445. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +40 -38
  446. package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +7 -3
  447. package/package.json +1 -1
  448. package/scripts/build-release-tarball.sh +32 -15
  449. package/scripts/check-authority-drift.py +125 -0
  450. package/scripts/check-instruction-size-baseline.mjs +19 -11
  451. package/scripts/check-invocation-integrity.py +2 -0
  452. package/scripts/codex-generate.sh +69 -33
  453. package/scripts/grade-feature.sh +121 -40
  454. package/scripts/live-hosts/agent-result.schema.json +230 -0
  455. package/scripts/live-hosts/claude.sh +87 -0
  456. package/scripts/live-hosts/codex.sh +81 -0
  457. package/scripts/live-hosts/common.sh +113 -0
  458. package/scripts/live-hosts/fake-host.py +264 -0
  459. package/scripts/live-hosts/host-transport.py +287 -0
  460. package/scripts/release-check.sh +5 -1
  461. package/scripts/run-agent-contract-evals.py +1380 -0
  462. package/scripts/run-behavioral-evals.sh +24 -30
  463. package/scripts/run-evals.sh +1 -5
  464. package/scripts/run-live-behavioral-evals.py +1274 -144
  465. package/scripts/run-outcome-evals.sh +414 -88
  466. package/scripts/run-tests.mjs +30 -2
  467. package/scripts/skills-inventory.mjs +1 -1
  468. package/scripts/validate-workflow-security.py +39 -20
  469. package/scripts/validate-workspace-schema.py +362 -10
  470. package/scripts/validate.sh +21 -15
  471. package/engine/testdata/golden/TestParityWrightScope/devrites-edit-allowed.golden +0 -1
  472. /package/engine/testdata/golden/{TestParityReconcile/check-no-claimed.golden → TestParityBuildReadiness/arg=clarifyopen.golden} +0 -0
@@ -1,24 +1,48 @@
1
1
  package lib
2
2
 
3
3
  import (
4
+ "crypto/sha256"
5
+ "encoding/hex"
6
+ "encoding/json"
4
7
  "fmt"
5
8
  "io"
6
9
  "os"
7
10
  "os/exec"
11
+ "path"
8
12
  "path/filepath"
13
+ "sort"
9
14
  "strings"
15
+
16
+ "github.com/devrites/devrites/internal/forge"
17
+ "github.com/devrites/devrites/internal/safepath"
18
+ )
19
+
20
+ const (
21
+ reconcileBaseName = ".reconcile-base"
22
+ reconcileAllowlistName = ".reconcile-allowlist"
23
+ reconcileCheckedName = ".reconcile-checked"
24
+ reconcileDevritesName = ".reconcile-devrites"
25
+ reconcileInlineName = ".reconcile-inline"
26
+ reconcileObjectsName = ".reconcile-objects"
27
+ reconcileObjectMarkerName = ".devrites-baseline"
28
+ reconcileLegacyClaimedName = ".reconcile-claimed"
29
+ defaultWrightAllowlistName = ".wright-allowlist"
30
+ wrightAllowlistFileEnv = "DEVRITES_WRIGHT_ALLOWLIST_FILE"
31
+ reconcileDevritesPathPrefix = ".devrites/"
10
32
  )
11
33
 
12
- // Reconcile enforces that the orchestrator never edits source itself: only the
13
- // dispatched slice-wright writes code and tests, while the orchestrator writes
14
- // bookkeeping under .devrites/. `snapshot` records the working tree just before
15
- // the wright runs; `check` diffs it afterward and flags any changed path that is
16
- // neither under .devrites/ nor in the wright's claimed set. The workspace is
17
- // <root>/features/<slug>.
34
+ // Reconcile enforces the source-write boundary around one dispatched
35
+ // slice-wright. The first `snapshot` captures the dirty worktree, private object
36
+ // database, exact orchestrator-authored allowlist, and .devrites state. After a
37
+ // clean check, another snapshot re-arms only the dispatch state for a retry while
38
+ // retaining the original source baseline.
39
+ // `check` compares the captured state with the current state and retains the
40
+ // immutable baseline for the later test-integrity and package-existence gates.
41
+ // `close` explicitly ends the window and removes its private artifacts.
18
42
  //
19
- // 0 clean check, snapshot written, inline fallback, or skipped (not a git repo)
20
- // 5 VIOLATION: source changed outside the wright's claimed set
21
- // 6 setup error: bad args, missing snapshot, or missing claimed manifest
43
+ // 0 clean check, snapshot/close completed, or skipped (not a git repo)
44
+ // 5 VIOLATION: a path changed outside the orchestrator allowlist
45
+ // 6 setup error: bad args or missing/corrupt lifecycle state
22
46
  func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
23
47
  mode := argAt(args, 0)
24
48
  slug := argAt(args, 1)
@@ -26,19 +50,7 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
26
50
  slug = activeSlug(root)
27
51
  }
28
52
 
29
- cwd, _ := os.Getwd()
30
- gitRoot := gitToplevel(cwd)
31
- if gitRoot == "" {
32
- fmt.Fprintln(stderr, "reconcile: not a git repo: gate skipped, verify the diff by hand.")
33
- return 0
34
- }
35
-
36
53
  d := featureDir(root, slug)
37
- base := filepath.Join(d, ".reconcile-base")
38
- claimed := filepath.Join(d, ".reconcile-claimed")
39
- inline := filepath.Join(d, ".reconcile-inline")
40
- objects := filepath.Join(d, ".reconcile-objects")
41
-
42
54
  if slug == "" || !isDir(d) {
43
55
  s := slug
44
56
  if s == "" {
@@ -48,115 +60,539 @@ func Reconcile(root string, args []string, stdout, stderr io.Writer) int {
48
60
  return 6
49
61
  }
50
62
 
51
- closeWindow := func() {
52
- _ = os.Remove(base)
53
- _ = os.Remove(claimed)
54
- _ = os.Remove(inline)
55
- _ = os.RemoveAll(objects)
63
+ base := filepath.Join(d, reconcileBaseName)
64
+ capturedAllowlist := filepath.Join(d, reconcileAllowlistName)
65
+ devritesSnapshot := filepath.Join(d, reconcileDevritesName)
66
+ inline := filepath.Join(d, reconcileInlineName)
67
+ objects := filepath.Join(d, reconcileObjectsName)
68
+ legacyClaimed := filepath.Join(d, reconcileLegacyClaimedName)
69
+ checked := filepath.Join(d, reconcileCheckedName)
70
+
71
+ closeWindow := func() error {
72
+ var failures []string
73
+ for _, privateFile := range []string{base, capturedAllowlist, devritesSnapshot, inline, legacyClaimed, checked} {
74
+ if err := os.Remove(privateFile); err != nil && !os.IsNotExist(err) {
75
+ failures = append(failures, fmt.Sprintf("%s: %v", filepath.Base(privateFile), err))
76
+ }
77
+ }
78
+ if err := os.RemoveAll(objects); err != nil {
79
+ failures = append(failures, fmt.Sprintf("%s: %v", filepath.Base(objects), err))
80
+ }
81
+ if len(failures) > 0 {
82
+ return fmt.Errorf("%s", strings.Join(failures, "; "))
83
+ }
84
+ return nil
85
+ }
86
+
87
+ if mode == "close" {
88
+ if err := closeWindow(); err != nil {
89
+ fmt.Fprintf(stderr, "reconcile: cannot close slice window: %v\n", err)
90
+ return 6
91
+ }
92
+ fmt.Fprintf(stdout, "reconcile: closed slice window for %s.\n", slug)
93
+ return 0
94
+ }
95
+
96
+ cwd, _ := os.Getwd()
97
+ gitRoot := gitToplevel(cwd)
98
+ if gitRoot == "" {
99
+ fmt.Fprintln(stderr, "reconcile: not a git repo: gate skipped, verify the diff by hand.")
100
+ return 0
56
101
  }
57
102
 
58
103
  switch mode {
59
104
  case "snapshot":
60
- _ = os.Remove(inline) // clear any stale sentinel / prior-slice manifest
61
- _ = os.Remove(claimed)
62
- _ = os.RemoveAll(objects)
63
- tree, err := worktreeTree(gitRoot, objects)
105
+ _, _, activeObjects, active, err := loadReconcileBaseline(gitRoot, d)
64
106
  if err != nil {
65
- _ = os.RemoveAll(objects)
107
+ fmt.Fprintf(stderr, "reconcile: cannot refresh invalid slice window: %v\n", err)
108
+ return 6
109
+ }
110
+ if active {
111
+ checkedTree, err := os.ReadFile(checked)
112
+ if err != nil {
113
+ fmt.Fprintln(stderr, `reconcile: active slice window has no clean check marker; run "reconcile check" before refreshing the dispatch snapshot.`)
114
+ return 6
115
+ }
116
+ nowTree, err := worktreeTree(gitRoot, activeObjects, root)
117
+ if err != nil {
118
+ fmt.Fprintf(stderr, "reconcile: cannot refresh worktree: %v\n", err)
119
+ return 6
120
+ }
121
+ if strings.TrimSpace(string(checkedTree)) != nowTree {
122
+ fmt.Fprintln(stderr, `reconcile: source changed after the last clean check; run "reconcile check" before refreshing the dispatch snapshot.`)
123
+ return 6
124
+ }
125
+ allowlistPath, err := wrightAllowlistPath(gitRoot, d)
126
+ if err != nil {
127
+ fmt.Fprintf(stderr, "reconcile: invalid wright allowlist location: %v\n", err)
128
+ return 6
129
+ }
130
+ allowed, err := readWrightAllowlist(gitRoot, allowlistPath)
131
+ if err != nil {
132
+ fmt.Fprintf(stderr, "reconcile: invalid wright allowlist %s: %v\n", allowlistPath, err)
133
+ return 6
134
+ }
135
+ if err := os.WriteFile(capturedAllowlist, renderWrightAllowlist(allowed), 0o600); err != nil {
136
+ fmt.Fprintf(stderr, "reconcile: cannot refresh wright allowlist: %v\n", err)
137
+ return 6
138
+ }
139
+ state, err := captureReconcileDevritesState(root, devritesSnapshot, activeObjects, checked, inline)
140
+ if err != nil {
141
+ fmt.Fprintf(stderr, "reconcile: cannot refresh .devrites snapshot: %v\n", err)
142
+ return 6
143
+ }
144
+ if err := writeDevritesState(devritesSnapshot, state); err != nil {
145
+ fmt.Fprintf(stderr, "reconcile: cannot write refreshed .devrites snapshot: %v\n", err)
146
+ return 6
147
+ }
148
+ if err := os.Remove(checked); err != nil {
149
+ fmt.Fprintf(stderr, "reconcile: cannot arm refreshed slice window: %v\n", err)
150
+ return 6
151
+ }
152
+ fmt.Fprintf(stdout, "reconcile: dispatch snapshot refreshed for %s; original slice baseline retained.\n", slug)
153
+ return 0
154
+ }
155
+
156
+ if err := closeWindow(); err != nil {
157
+ fmt.Fprintf(stderr, "reconcile: cannot clear stale slice window: %v\n", err)
158
+ return 6
159
+ }
160
+
161
+ allowlistPath, err := wrightAllowlistPath(gitRoot, d)
162
+ if err != nil {
163
+ fmt.Fprintf(stderr, "reconcile: invalid wright allowlist location: %v\n", err)
164
+ return 6
165
+ }
166
+ allowed, err := readWrightAllowlist(gitRoot, allowlistPath)
167
+ if err != nil {
168
+ fmt.Fprintf(stderr, "reconcile: invalid wright allowlist %s: %v\n", allowlistPath, err)
169
+ return 6
170
+ }
171
+
172
+ tree, err := worktreeTree(gitRoot, objects, root)
173
+ if err != nil {
174
+ _ = closeWindow()
66
175
  fmt.Fprintf(stderr, "reconcile: cannot snapshot worktree: %v\n", err)
67
176
  return 6
68
177
  }
69
178
  if err := os.WriteFile(base, []byte(tree+"\n"), 0o644); err != nil {
70
- _ = os.RemoveAll(objects)
179
+ _ = closeWindow()
71
180
  fmt.Fprintf(stderr, "reconcile: cannot write snapshot: %v\n", err)
72
181
  return 6
73
182
  }
183
+ if err := os.WriteFile(filepath.Join(objects, reconcileObjectMarkerName), []byte(tree+"\n"), 0o600); err != nil {
184
+ _ = closeWindow()
185
+ fmt.Fprintf(stderr, "reconcile: cannot seal snapshot object database: %v\n", err)
186
+ return 6
187
+ }
188
+ if err := os.WriteFile(capturedAllowlist, renderWrightAllowlist(allowed), 0o600); err != nil {
189
+ _ = closeWindow()
190
+ fmt.Fprintf(stderr, "reconcile: cannot capture wright allowlist: %v\n", err)
191
+ return 6
192
+ }
193
+ state, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, inline)
194
+ if err != nil {
195
+ _ = closeWindow()
196
+ fmt.Fprintf(stderr, "reconcile: cannot snapshot .devrites: %v\n", err)
197
+ return 6
198
+ }
199
+ if err := writeDevritesState(devritesSnapshot, state); err != nil {
200
+ _ = closeWindow()
201
+ fmt.Fprintf(stderr, "reconcile: cannot write .devrites snapshot: %v\n", err)
202
+ return 6
203
+ }
74
204
  fmt.Fprintf(stdout, "reconcile: snapshot captured for %s.\n", slug)
75
205
  return 0
76
206
 
77
207
  case "check":
78
- if isFile(inline) {
79
- fmt.Fprintln(stderr, "reconcile: inline-fallback run (no wright dispatched): gate skipped.")
80
- closeWindow()
81
- return 0
82
- }
83
- if !isFile(base) {
84
- fmt.Fprintln(stderr, `reconcile: no snapshot (.reconcile-base): run "devrites-engine reconcile snapshot" before dispatch.`)
208
+ baseTree, env, _, active, err := loadReconcileBaseline(gitRoot, d)
209
+ if err != nil {
210
+ fmt.Fprintf(stderr, "reconcile: invalid snapshot: %v\n", err)
85
211
  return 6
86
212
  }
87
- if !isFile(claimed) {
88
- fmt.Fprintln(stderr, "reconcile: no claimed manifest (.reconcile-claimed): write the wright Files-changed paths first.")
213
+ if !active {
214
+ fmt.Fprintln(stderr, `reconcile: no snapshot (.reconcile-base/.reconcile-objects): run "devrites-engine reconcile snapshot" before dispatch.`)
89
215
  return 6
90
216
  }
91
- baseBytes, err := os.ReadFile(base)
217
+
218
+ allowed, err := readWrightAllowlist(gitRoot, capturedAllowlist)
92
219
  if err != nil {
93
- fmt.Fprintf(stderr, "reconcile: cannot read snapshot: %v\n", err)
220
+ fmt.Fprintf(stderr, "reconcile: invalid captured allowlist: %v\n", err)
94
221
  return 6
95
222
  }
96
- baseTree := strings.TrimSpace(string(baseBytes))
97
- if baseTree == "" {
98
- fmt.Fprintln(stderr, "reconcile: empty snapshot (.reconcile-base): re-run \"devrites-engine reconcile snapshot\" before dispatch.")
223
+ beforeDevrites, err := readDevritesState(devritesSnapshot)
224
+ if err != nil {
225
+ fmt.Fprintf(stderr, "reconcile: invalid .devrites snapshot: %v\n", err)
99
226
  return 6
100
227
  }
101
- if !isDir(objects) {
102
- fmt.Fprintln(stderr, "reconcile: missing snapshot object database (.reconcile-objects): re-run \"devrites-engine reconcile snapshot\" before dispatch.")
228
+ afterDevrites, err := captureReconcileDevritesState(root, devritesSnapshot, objects, checked, inline)
229
+ if err != nil {
230
+ fmt.Fprintf(stderr, "reconcile: cannot compare .devrites: %v\n", err)
103
231
  return 6
104
232
  }
105
- nowTree, err := worktreeTree(gitRoot, objects)
233
+
234
+ nowTree, err := worktreeTree(gitRoot, objects, root)
106
235
  if err != nil {
107
236
  fmt.Fprintf(stderr, "reconcile: cannot capture worktree: %v\n", err)
108
237
  return 6
109
238
  }
110
- diff := exec.Command("git", "-C", gitRoot, "diff", "--name-only", baseTree, nowTree)
111
- diff.Env, err = reconcileGitEnv(gitRoot, objects)
239
+ changed, err := changedTreePaths(gitRoot, env, baseTree, nowTree)
112
240
  if err != nil {
113
241
  fmt.Fprintf(stderr, "reconcile: cannot compare worktree: %v\n", err)
114
242
  return 6
115
243
  }
116
- diffOut, err := diff.CombinedOutput()
117
- if err != nil {
118
- fmt.Fprintf(stderr, "reconcile: cannot compare worktree: git diff --name-only: %v: %s\n", err, strings.TrimSpace(string(diffOut)))
119
- return 6
120
- }
121
244
 
122
- claimedSet := claimedPaths(claimed)
123
- viol := 0
124
- var violist strings.Builder
125
- for _, f := range splitLinesNoTrailing(diffOut) {
126
- if f == "" || strings.HasPrefix(f, ".devrites/") {
245
+ violations := map[string]struct{}{}
246
+ for _, changedPath := range changed {
247
+ // Canonical DevRites state is fingerprinted below. It may be tracked
248
+ // or unignored, so do not treat the same path as source as well.
249
+ if changedPath == ".devrites" || strings.HasPrefix(changedPath, reconcileDevritesPathPrefix) {
127
250
  continue
128
251
  }
129
- if _, ok := claimedSet[f]; !ok {
130
- viol++
131
- fmt.Fprintf(&violist, " - %s\n", f)
252
+ if _, ok := allowed[changedPath]; !ok {
253
+ violations[changedPath] = struct{}{}
132
254
  }
133
255
  }
134
- if viol > 0 {
135
- fmt.Fprintf(stderr, "reconcile: STOP: %d source file(s) changed OUTSIDE the wright (A1 breach):\n", viol)
136
- fmt.Fprint(stderr, violist.String())
137
- fmt.Fprintln(stderr, "The orchestrator must NOT edit source. Re-dispatch the wright (continue it once) or revert these.")
256
+ for _, changedPath := range changedDevritesPaths(beforeDevrites, afterDevrites) {
257
+ violations[reconcileDevritesPathPrefix+changedPath] = struct{}{}
258
+ }
259
+
260
+ if len(violations) > 0 {
261
+ paths := make([]string, 0, len(violations))
262
+ for changedPath := range violations {
263
+ paths = append(paths, changedPath)
264
+ }
265
+ sort.Strings(paths)
266
+ fmt.Fprintf(stderr, "reconcile: STOP: %d path(s) changed outside the orchestrator-authorized wright allowlist (A1 breach):\n", len(paths))
267
+ for _, changedPath := range paths {
268
+ fmt.Fprintf(stderr, " - %s\n", changedPath)
269
+ }
270
+ fmt.Fprintln(stderr, "Reject this writer result. Preserve pre-snapshot user changes, restore only the unauthorized slice delta, then re-dispatch with an exact allowlist.")
138
271
  return 5
139
272
  }
140
- closeWindow()
141
- fmt.Fprintln(stdout, "reconcile: OK: every source change is the wright's; orchestrator touched only bookkeeping.")
273
+ if err := os.WriteFile(checked, []byte(nowTree+"\n"), 0o600); err != nil {
274
+ fmt.Fprintf(stderr, "reconcile: cannot record clean check: %v\n", err)
275
+ return 6
276
+ }
277
+ fmt.Fprintln(stdout, `reconcile: OK: every slice change matches the orchestrator allowlist; baseline retained for post-slice gates (run "reconcile close" afterward).`)
142
278
  return 0
143
279
 
144
280
  default:
145
- fmt.Fprintln(stderr, "reconcile: usage: devrites-engine reconcile <snapshot|check> [slug]")
281
+ fmt.Fprintln(stderr, "reconcile: usage: devrites-engine reconcile <snapshot|check|close> [slug]")
146
282
  return 6
147
283
  }
148
284
  }
149
285
 
150
- // worktreeTree captures the whole working tree (tracked + untracked, .gitignore
151
- // honored) as a git tree object via a throwaway index, so the user's real index
152
- // is never touched.
286
+ func wrightAllowlistPath(gitRoot, workspace string) (string, error) {
287
+ configured := os.Getenv(wrightAllowlistFileEnv)
288
+ var filename string
289
+ if configured == "" {
290
+ filename = filepath.Join(workspace, defaultWrightAllowlistName)
291
+ } else if filepath.IsAbs(configured) {
292
+ filename = filepath.Clean(configured)
293
+ } else {
294
+ filename = filepath.Join(gitRoot, filepath.FromSlash(configured))
295
+ }
296
+ if !safepath.WithinResolved(filename, workspace) {
297
+ return "", fmt.Errorf("path must remain inside the active workspace")
298
+ }
299
+ return filename, nil
300
+ }
301
+
302
+ func readWrightAllowlist(gitRoot, filename string) (map[string]struct{}, error) {
303
+ data, err := os.ReadFile(filename)
304
+ if err != nil {
305
+ return nil, err
306
+ }
307
+ return parseWrightAllowlist(gitRoot, data)
308
+ }
309
+
310
+ func parseWrightAllowlist(gitRoot string, data []byte) (map[string]struct{}, error) {
311
+ allowed := map[string]struct{}{}
312
+ for lineNumber, candidate := range splitLinesNoTrailing(data) {
313
+ line := lineNumber + 1
314
+ switch {
315
+ case candidate == "":
316
+ return nil, fmt.Errorf("line %d is empty", line)
317
+ case strings.TrimSpace(candidate) != candidate:
318
+ return nil, fmt.Errorf("line %d is not normalized: %q", line, candidate)
319
+ case strings.Contains(candidate, `\`):
320
+ return nil, fmt.Errorf("line %d uses a non-Git path separator: %q", line, candidate)
321
+ case filepath.IsAbs(candidate) || path.IsAbs(candidate) || windowsAbsolutePath(candidate):
322
+ return nil, fmt.Errorf("line %d is absolute: %q", line, candidate)
323
+ case candidate == ".." || strings.HasPrefix(candidate, "../"):
324
+ return nil, fmt.Errorf("line %d traverses outside the repository: %q", line, candidate)
325
+ case candidate == "." || path.Clean(candidate) != candidate:
326
+ return nil, fmt.Errorf("line %d is not normalized: %q", line, candidate)
327
+ case candidate == ".devrites" || strings.HasPrefix(candidate, reconcileDevritesPathPrefix):
328
+ return nil, fmt.Errorf("line %d targets orchestrator-owned .devrites state: %q", line, candidate)
329
+ }
330
+ if _, duplicate := allowed[candidate]; duplicate {
331
+ return nil, fmt.Errorf("line %d duplicates %q", line, candidate)
332
+ }
333
+ fullPath := filepath.Join(gitRoot, filepath.FromSlash(candidate))
334
+ if !safepath.WithinResolved(fullPath, gitRoot) {
335
+ return nil, fmt.Errorf("line %d escapes the repository through a symlink: %q", line, candidate)
336
+ }
337
+ info, err := os.Stat(fullPath)
338
+ switch {
339
+ case err == nil && info.IsDir():
340
+ return nil, fmt.Errorf("line %d names a directory, not a file: %q", line, candidate)
341
+ case err != nil && !os.IsNotExist(err):
342
+ return nil, fmt.Errorf("line %d cannot inspect %q: %w", line, candidate, err)
343
+ }
344
+ allowed[candidate] = struct{}{}
345
+ }
346
+ return allowed, nil
347
+ }
348
+
349
+ func windowsAbsolutePath(candidate string) bool {
350
+ return len(candidate) >= 3 &&
351
+ ((candidate[0] >= 'A' && candidate[0] <= 'Z') || (candidate[0] >= 'a' && candidate[0] <= 'z')) &&
352
+ candidate[1] == ':' && (candidate[2] == '/' || candidate[2] == '\\')
353
+ }
354
+
355
+ func renderWrightAllowlist(allowed map[string]struct{}) []byte {
356
+ paths := make([]string, 0, len(allowed))
357
+ for allowedPath := range allowed {
358
+ paths = append(paths, allowedPath)
359
+ }
360
+ sort.Strings(paths)
361
+ if len(paths) == 0 {
362
+ return nil
363
+ }
364
+ return []byte(strings.Join(paths, "\n") + "\n")
365
+ }
366
+
367
+ type devritesStateEntry struct {
368
+ Path string `json:"path"`
369
+ Fingerprint string `json:"fingerprint"`
370
+ }
371
+
372
+ // captureReconcileDevritesState keeps Forge's typed lifecycle manifest private
373
+ // while continuing to fingerprint every other .forge path, including the human
374
+ // forge-report.md. A manifest is ignored only after the Forge owner validates
375
+ // its unique run, derived path, and primary root.
376
+ func captureReconcileDevritesState(root string, ignoredPaths ...string) ([]devritesStateEntry, error) {
377
+ manifests, err := validatedForgeManifestPaths(root)
378
+ if err != nil {
379
+ return nil, err
380
+ }
381
+ return captureDevritesState(root, append(ignoredPaths, manifests...)...)
382
+ }
383
+
384
+ func validatedForgeManifestPaths(root string) ([]string, error) {
385
+ work := filepath.Join(root, "work")
386
+ info, err := os.Lstat(work)
387
+ if os.IsNotExist(err) {
388
+ return nil, nil
389
+ }
390
+ if err != nil {
391
+ return nil, err
392
+ }
393
+ if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
394
+ return nil, nil
395
+ }
396
+ slugs, err := os.ReadDir(work)
397
+ if err != nil {
398
+ return nil, err
399
+ }
400
+ var paths []string
401
+ for _, slug := range slugs {
402
+ if !slug.IsDir() || slug.Type()&os.ModeSymlink != 0 {
403
+ continue
404
+ }
405
+ forgeDir := filepath.Join(work, slug.Name(), ".forge")
406
+ forgeInfo, err := os.Lstat(forgeDir)
407
+ if os.IsNotExist(err) {
408
+ continue
409
+ }
410
+ if err != nil {
411
+ return nil, err
412
+ }
413
+ if !forgeInfo.IsDir() || forgeInfo.Mode()&os.ModeSymlink != 0 {
414
+ continue
415
+ }
416
+ runs, err := os.ReadDir(forgeDir)
417
+ if err != nil {
418
+ return nil, err
419
+ }
420
+ for _, run := range runs {
421
+ if !run.IsDir() || run.Type()&os.ModeSymlink != 0 {
422
+ continue
423
+ }
424
+ manifestPath := filepath.Join(forgeDir, run.Name(), "manifest.json")
425
+ manifestInfo, err := os.Lstat(manifestPath)
426
+ if os.IsNotExist(err) {
427
+ continue
428
+ }
429
+ if err != nil {
430
+ return nil, err
431
+ }
432
+ if !manifestInfo.Mode().IsRegular() {
433
+ continue
434
+ }
435
+ manifest, validatedPath, err := forge.Load(filepath.Dir(root), run.Name())
436
+ if err != nil {
437
+ return nil, fmt.Errorf("reconcile: invalid Forge manifest %s: %w", manifestPath, err)
438
+ }
439
+ if filepath.Clean(validatedPath) != filepath.Clean(manifestPath) || manifest.FeatureSlug != slug.Name() {
440
+ return nil, fmt.Errorf("reconcile: Forge manifest ownership mismatch: %s", manifestPath)
441
+ }
442
+ paths = append(paths, validatedPath)
443
+ }
444
+ }
445
+ sort.Strings(paths)
446
+ return paths, nil
447
+ }
448
+
449
+ func captureDevritesState(root string, ignoredPaths ...string) ([]devritesStateEntry, error) {
450
+ ignored := make(map[string]struct{}, len(ignoredPaths))
451
+ for _, ignoredPath := range ignoredPaths {
452
+ rel, err := filepath.Rel(root, ignoredPath)
453
+ if err != nil {
454
+ return nil, err
455
+ }
456
+ ignored[filepath.Clean(rel)] = struct{}{}
457
+ }
458
+
459
+ var state []devritesStateEntry
460
+ err := filepath.WalkDir(root, func(filename string, entry os.DirEntry, walkErr error) error {
461
+ if walkErr != nil {
462
+ return walkErr
463
+ }
464
+ if filename == root {
465
+ return nil
466
+ }
467
+ rel, err := filepath.Rel(root, filename)
468
+ if err != nil {
469
+ return err
470
+ }
471
+ rel = filepath.Clean(rel)
472
+ if _, skip := ignored[rel]; skip {
473
+ if entry.IsDir() {
474
+ return filepath.SkipDir
475
+ }
476
+ return nil
477
+ }
478
+
479
+ fingerprint, err := devritesFingerprint(filename, entry)
480
+ if err != nil {
481
+ return err
482
+ }
483
+ state = append(state, devritesStateEntry{
484
+ Path: filepath.ToSlash(rel),
485
+ Fingerprint: fingerprint,
486
+ })
487
+ return nil
488
+ })
489
+ if err != nil {
490
+ return nil, err
491
+ }
492
+ sort.Slice(state, func(i, j int) bool { return state[i].Path < state[j].Path })
493
+ return state, nil
494
+ }
495
+
496
+ func devritesFingerprint(filename string, entry os.DirEntry) (string, error) {
497
+ info, err := entry.Info()
498
+ if err != nil {
499
+ return "", err
500
+ }
501
+ mode := info.Mode()
502
+ switch {
503
+ case mode.IsRegular():
504
+ data, err := os.ReadFile(filename)
505
+ if err != nil {
506
+ return "", err
507
+ }
508
+ sum := sha256.Sum256(data)
509
+ return fmt.Sprintf("file:%#o:%s", mode.Perm(), hex.EncodeToString(sum[:])), nil
510
+ case mode.IsDir():
511
+ return fmt.Sprintf("dir:%#o", mode.Perm()), nil
512
+ case mode&os.ModeSymlink != 0:
513
+ target, err := os.Readlink(filename)
514
+ if err != nil {
515
+ return "", err
516
+ }
517
+ sum := sha256.Sum256([]byte(target))
518
+ return fmt.Sprintf("symlink:%s", hex.EncodeToString(sum[:])), nil
519
+ default:
520
+ return fmt.Sprintf("other:%s", mode.String()), nil
521
+ }
522
+ }
523
+
524
+ func writeDevritesState(filename string, state []devritesStateEntry) error {
525
+ data, err := json.Marshal(state)
526
+ if err != nil {
527
+ return err
528
+ }
529
+ data = append(data, '\n')
530
+ return os.WriteFile(filename, data, 0o600)
531
+ }
532
+
533
+ func readDevritesState(filename string) ([]devritesStateEntry, error) {
534
+ data, err := os.ReadFile(filename)
535
+ if err != nil {
536
+ return nil, err
537
+ }
538
+ var state []devritesStateEntry
539
+ if err := json.Unmarshal(data, &state); err != nil {
540
+ return nil, err
541
+ }
542
+ seen := map[string]struct{}{}
543
+ for i, entry := range state {
544
+ if entry.Path == "" || entry.Fingerprint == "" ||
545
+ path.Clean(entry.Path) != entry.Path || path.IsAbs(entry.Path) ||
546
+ entry.Path == ".." || strings.HasPrefix(entry.Path, "../") || strings.Contains(entry.Path, `\`) {
547
+ return nil, fmt.Errorf("entry %d is invalid", i+1)
548
+ }
549
+ if _, duplicate := seen[entry.Path]; duplicate {
550
+ return nil, fmt.Errorf("entry %d duplicates %q", i+1, entry.Path)
551
+ }
552
+ seen[entry.Path] = struct{}{}
553
+ }
554
+ return state, nil
555
+ }
556
+
557
+ func changedDevritesPaths(before, after []devritesStateEntry) []string {
558
+ beforeMap := make(map[string]string, len(before))
559
+ afterMap := make(map[string]string, len(after))
560
+ for _, entry := range before {
561
+ beforeMap[entry.Path] = entry.Fingerprint
562
+ }
563
+ for _, entry := range after {
564
+ afterMap[entry.Path] = entry.Fingerprint
565
+ }
566
+ changed := map[string]struct{}{}
567
+ for statePath, fingerprint := range beforeMap {
568
+ if afterMap[statePath] != fingerprint {
569
+ changed[statePath] = struct{}{}
570
+ }
571
+ }
572
+ for statePath, fingerprint := range afterMap {
573
+ if beforeMap[statePath] != fingerprint {
574
+ changed[statePath] = struct{}{}
575
+ }
576
+ }
577
+ paths := make([]string, 0, len(changed))
578
+ for statePath := range changed {
579
+ paths = append(paths, statePath)
580
+ }
581
+ sort.Strings(paths)
582
+ return paths
583
+ }
584
+
585
+ // worktreeTree captures the working tree (tracked + untracked, .gitignore
586
+ // honored) as a git tree object via a throwaway index, excluding separately
587
+ // fingerprinted DevRites roots. Seeding from HEAD keeps committed paths tracked
588
+ // even when a later ignore rule matches them. The user's real index is never
589
+ // touched.
153
590
  //
154
591
  // A failing "git add -A" must not be ignored: write-tree would still succeed
155
- // against the empty throwaway index and return the empty-tree sha. Both the
156
- // snapshot and the check would then record that same sha, the diff would come
157
- // back empty, and the gate would report OK on precisely the A1 breach it exists
158
- // to catch. Surface the error and let the caller fail closed.
159
- func worktreeTree(gitRoot, objectDir string) (string, error) {
592
+ // against the seeded throwaway index. Both the snapshot and the check could then
593
+ // record the same stale tree, and the gate would report OK on precisely the A1
594
+ // breach it exists to catch. Surface the error and let the caller fail closed.
595
+ func worktreeTree(gitRoot, objectDir string, excludedRoots ...string) (string, error) {
160
596
  idx := filepath.Join(os.TempDir(), fmt.Sprintf("devrites-reconcile-%d.idx", os.Getpid()))
161
597
  _ = os.Remove(idx)
162
598
  defer func() { _ = os.Remove(idx) }()
@@ -169,6 +605,42 @@ func worktreeTree(gitRoot, objectDir string) (string, error) {
169
605
  }
170
606
  env = append(env, "GIT_INDEX_FILE="+idx)
171
607
 
608
+ read := exec.Command("git", "-C", gitRoot, "read-tree", "HEAD")
609
+ read.Env = env
610
+ readOut, readErr := read.CombinedOutput()
611
+ if readErr != nil {
612
+ readFailure := fmt.Errorf("git read-tree HEAD: %w", readErr)
613
+ if detail := strings.TrimSpace(string(readOut)); detail != "" {
614
+ readFailure = fmt.Errorf("git read-tree HEAD: %w: %s", readErr, detail)
615
+ }
616
+ // An unborn branch is the one valid reason HEAD cannot seed the index.
617
+ // Distinguish it from a corrupt or unreadable existing ref so the gate
618
+ // still fails closed on real repository errors.
619
+ symbolic := exec.Command("git", "-C", gitRoot, "symbolic-ref", "--quiet", "HEAD")
620
+ symbolic.Env = env
621
+ refOut, symbolicErr := symbolic.CombinedOutput()
622
+ if symbolicErr != nil {
623
+ return "", readFailure
624
+ }
625
+ ref := strings.TrimSpace(string(refOut))
626
+ exists := exec.Command("git", "-C", gitRoot, "show-ref", "--verify", "--quiet", ref)
627
+ exists.Env = env
628
+ if err := exists.Run(); err == nil {
629
+ return "", readFailure
630
+ } else if exitErr, ok := err.(*exec.ExitError); !ok || exitErr.ExitCode() != 1 {
631
+ return "", fmt.Errorf("inspect HEAD ref %s: %w", ref, err)
632
+ }
633
+
634
+ empty := exec.Command("git", "-C", gitRoot, "read-tree", "--empty")
635
+ empty.Env = env
636
+ if out, err := empty.CombinedOutput(); err != nil {
637
+ if detail := strings.TrimSpace(string(out)); detail != "" {
638
+ return "", fmt.Errorf("git read-tree --empty: %w: %s", err, detail)
639
+ }
640
+ return "", fmt.Errorf("git read-tree --empty: %w", err)
641
+ }
642
+ }
643
+
172
644
  add := exec.Command("git", "-C", gitRoot, "add", "-A")
173
645
  add.Env = env
174
646
  addOut, err := add.CombinedOutput()
@@ -178,6 +650,20 @@ func worktreeTree(gitRoot, objectDir string) (string, error) {
178
650
  }
179
651
  return "", fmt.Errorf("git add -A: %w", err)
180
652
  }
653
+ for _, excludedRoot := range excludedRoots {
654
+ rel, err := filepath.Rel(gitRoot, excludedRoot)
655
+ if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
656
+ continue
657
+ }
658
+ rm := exec.Command(
659
+ "git", "-C", gitRoot, "rm", "-r", "--cached", "--ignore-unmatch", "--",
660
+ ":(top,literal)"+filepath.ToSlash(rel),
661
+ )
662
+ rm.Env = env
663
+ if out, err := rm.CombinedOutput(); err != nil {
664
+ return "", fmt.Errorf("exclude %s from worktree snapshot: %w: %s", rel, err, strings.TrimSpace(string(out)))
665
+ }
666
+ }
181
667
 
182
668
  write := exec.Command("git", "-C", gitRoot, "write-tree")
183
669
  write.Env = env
@@ -218,16 +704,154 @@ func reconcileGitEnv(gitRoot, objectDir string) ([]string, error) {
218
704
  ), nil
219
705
  }
220
706
 
221
- // claimedPaths reads the .reconcile-claimed manifest into a set of the exact
222
- // paths the wright reported changing.
223
- func claimedPaths(path string) map[string]struct{} {
224
- set := map[string]struct{}{}
225
- data, err := os.ReadFile(path)
707
+ // loadReconcileBaseline returns the retained slice baseline and its private Git
708
+ // environment. With no lifecycle state at all it reports active=false so gates
709
+ // invoked outside a slice can compare against HEAD. A partial or corrupt
710
+ // lifecycle never silently falls back to HEAD.
711
+ func loadReconcileBaseline(gitRoot, workspace string) (baseTree string, env []string, objectDir string, active bool, err error) {
712
+ basePath := filepath.Join(workspace, reconcileBaseName)
713
+ objectDir = filepath.Join(workspace, reconcileObjectsName)
714
+
715
+ baseInfo, baseErr := os.Lstat(basePath)
716
+ objectsInfo, objectsErr := os.Lstat(objectDir)
717
+ baseMissing := os.IsNotExist(baseErr)
718
+ objectsMissing := os.IsNotExist(objectsErr)
719
+ if baseMissing && objectsMissing {
720
+ return "", nil, "", false, nil
721
+ }
722
+ if baseErr != nil && !baseMissing {
723
+ return "", nil, "", false, fmt.Errorf("cannot inspect %s: %w", reconcileBaseName, baseErr)
724
+ }
725
+ if objectsErr != nil && !objectsMissing {
726
+ return "", nil, "", false, fmt.Errorf("cannot inspect %s: %w", reconcileObjectsName, objectsErr)
727
+ }
728
+ if baseMissing || objectsMissing {
729
+ return "", nil, "", false, fmt.Errorf("partial lifecycle: %s and %s must both exist", reconcileBaseName, reconcileObjectsName)
730
+ }
731
+ if !baseInfo.Mode().IsRegular() {
732
+ return "", nil, "", false, fmt.Errorf("%s is not a regular file", reconcileBaseName)
733
+ }
734
+ if !objectsInfo.IsDir() {
735
+ return "", nil, "", false, fmt.Errorf("%s is not a directory", reconcileObjectsName)
736
+ }
737
+
738
+ data, err := os.ReadFile(basePath)
739
+ if err != nil {
740
+ return "", nil, "", false, fmt.Errorf("read %s: %w", reconcileBaseName, err)
741
+ }
742
+ baseTree = strings.TrimSpace(string(data))
743
+ if baseTree == "" || strings.ContainsAny(baseTree, " \t\r\n") {
744
+ return "", nil, "", false, fmt.Errorf("%s does not contain one tree id", reconcileBaseName)
745
+ }
746
+ markerPath := filepath.Join(objectDir, reconcileObjectMarkerName)
747
+ markerInfo, err := os.Lstat(markerPath)
748
+ if err != nil {
749
+ return "", nil, "", false, fmt.Errorf("%s is not a sealed object database: %w", reconcileObjectsName, err)
750
+ }
751
+ if !markerInfo.Mode().IsRegular() {
752
+ return "", nil, "", false, fmt.Errorf("%s marker is not a regular file", reconcileObjectsName)
753
+ }
754
+ marker, err := os.ReadFile(markerPath)
755
+ if err != nil {
756
+ return "", nil, "", false, fmt.Errorf("read %s marker: %w", reconcileObjectsName, err)
757
+ }
758
+ if strings.TrimSpace(string(marker)) != baseTree {
759
+ return "", nil, "", false, fmt.Errorf("%s marker does not match %s", reconcileObjectsName, reconcileBaseName)
760
+ }
761
+ env, err = reconcileGitEnv(gitRoot, objectDir)
762
+ if err != nil {
763
+ return "", nil, "", false, err
764
+ }
765
+ if _, err := reconcileGitOutput(gitRoot, env, "cat-file", "-e", baseTree+"^{tree}"); err != nil {
766
+ return "", nil, "", false, fmt.Errorf("%s tree is unavailable: %w", reconcileBaseName, err)
767
+ }
768
+ return baseTree, env, objectDir, true, nil
769
+ }
770
+
771
+ type sliceTreeRange struct {
772
+ base string
773
+ current string
774
+ env []string
775
+ cleanup func()
776
+ }
777
+
778
+ // captureSliceTreeRange returns an immutable base→current pair. During an open
779
+ // reconcile window it reuses the retained private object database. For a direct
780
+ // standalone gate it captures the current dirty tree in a temporary private
781
+ // object database and compares it with HEAD.
782
+ func captureSliceTreeRange(gitRoot, devritesRoot, workspace string) (sliceTreeRange, error) {
783
+ base, _, objectDir, active, err := loadReconcileBaseline(gitRoot, workspace)
784
+ if err != nil {
785
+ return sliceTreeRange{}, err
786
+ }
787
+ cleanup := func() {}
788
+ if !active {
789
+ objectDir, err = os.MkdirTemp("", "devrites-gate-objects-*")
790
+ if err != nil {
791
+ return sliceTreeRange{}, fmt.Errorf("create temporary object database: %w", err)
792
+ }
793
+ cleanup = func() { _ = os.RemoveAll(objectDir) }
794
+ base = "HEAD"
795
+ }
796
+ current, err := worktreeTree(gitRoot, objectDir, devritesRoot)
797
+ if err != nil {
798
+ cleanup()
799
+ return sliceTreeRange{}, err
800
+ }
801
+ env, err := reconcileGitEnv(gitRoot, objectDir)
802
+ if err != nil {
803
+ cleanup()
804
+ return sliceTreeRange{}, err
805
+ }
806
+ return sliceTreeRange{base: base, current: current, env: env, cleanup: cleanup}, nil
807
+ }
808
+
809
+ func changedTreePaths(gitRoot string, env []string, baseTree, currentTree string) ([]string, error) {
810
+ out, err := reconcileGitOutput(
811
+ gitRoot,
812
+ env,
813
+ "diff", "--name-only", "--no-renames", "-z", baseTree, currentTree, "--",
814
+ )
815
+ if err != nil {
816
+ return nil, err
817
+ }
818
+ var paths []string
819
+ for rawPath := range strings.SplitSeq(string(out), "\x00") {
820
+ if rawPath != "" {
821
+ paths = append(paths, filepath.ToSlash(filepath.Clean(rawPath)))
822
+ }
823
+ }
824
+ return paths, nil
825
+ }
826
+
827
+ func treeFileContent(gitRoot string, env []string, tree, filename string) ([]byte, bool, error) {
828
+ entry, err := reconcileGitOutput(gitRoot, env, "ls-tree", "-z", tree, "--", ":(literal)"+filename)
226
829
  if err != nil {
227
- return set
830
+ return nil, false, err
831
+ }
832
+ if len(entry) == 0 {
833
+ return nil, false, nil
228
834
  }
229
- for _, line := range splitLinesNoTrailing(data) {
230
- set[line] = struct{}{}
835
+ out, err := reconcileGitOutput(gitRoot, env, "show", tree+":"+filename)
836
+ if err != nil {
837
+ return nil, false, err
838
+ }
839
+ return out, true, nil
840
+ }
841
+
842
+ func reconcileGitOutput(gitRoot string, env []string, args ...string) ([]byte, error) {
843
+ commandArgs := append([]string{"-C", gitRoot}, args...)
844
+ cmd := exec.Command("git", commandArgs...)
845
+ if env != nil {
846
+ cmd.Env = env
847
+ }
848
+ out, err := cmd.CombinedOutput()
849
+ if err != nil {
850
+ detail := strings.TrimSpace(string(out))
851
+ if detail != "" {
852
+ return nil, fmt.Errorf("git %s: %w: %s", strings.Join(args, " "), err, detail)
853
+ }
854
+ return nil, fmt.Errorf("git %s: %w", strings.Join(args, " "), err)
231
855
  }
232
- return set
856
+ return out, nil
233
857
  }