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
@@ -6,6 +6,7 @@ import (
6
6
  "compress/gzip"
7
7
  "context"
8
8
  "crypto/sha256"
9
+ "encoding/hex"
9
10
  "encoding/json"
10
11
  "errors"
11
12
  "flag"
@@ -16,6 +17,7 @@ import (
16
17
  "os/exec"
17
18
  "path/filepath"
18
19
  "reflect"
20
+ "runtime"
19
21
  "slices"
20
22
  "sort"
21
23
  "strings"
@@ -25,11 +27,13 @@ import (
25
27
  "github.com/devrites/devrites/internal/fsutil"
26
28
  "github.com/devrites/devrites/internal/hostpack"
27
29
  "github.com/devrites/devrites/internal/iohooks"
30
+ "github.com/devrites/devrites/internal/safepath"
28
31
  "github.com/devrites/devrites/internal/version"
29
32
  )
30
33
 
31
34
  const ManifestName = devritespaths.ManifestName
32
35
  const defaultRepo = "ViktorsBaikers/DevRites"
36
+ const githubBaseURL = "https://github.com"
33
37
 
34
38
  type Mode string
35
39
 
@@ -69,15 +73,27 @@ type stats struct {
69
73
  }
70
74
 
71
75
  type runner struct {
72
- opts Options
73
- target string
74
- payload string
75
- payloadFS fs.FS
76
- source string
77
- manifest []string
78
- records map[string]string
79
- prev map[string]bool
80
- stats stats
76
+ opts Options
77
+ target string
78
+ payload string
79
+ payloadFS fs.FS
80
+ source string
81
+ manifest []string
82
+ records map[string]string
83
+ prev map[string]managedRecord
84
+ preflight map[string]pathSnapshot
85
+ stats stats
86
+ releaseBinaryTag string
87
+ preparedBinary string
88
+ }
89
+
90
+ type managedRecord struct {
91
+ Hash string
92
+ }
93
+
94
+ type pathSnapshot struct {
95
+ missing bool
96
+ hash string
81
97
  }
82
98
 
83
99
  func DefaultOptions(mode Mode) Options {
@@ -209,7 +225,7 @@ func flagWasSet(flags *flag.FlagSet, name string) bool {
209
225
  func usage(mode Mode) string {
210
226
  switch mode {
211
227
  case ModeUninstall:
212
- return "usage: devrites-engine uninstall [--target DIR] [--dry-run] [--keep-binary]\n"
228
+ return "usage: devrites-engine uninstall [--target DIR] [--dry-run] [--force] [--keep-binary]\n"
213
229
  case ModeUpdate:
214
230
  return "usage: devrites-engine update [--target DIR] [--dry-run] [--force] [--check] [--to TAG] [install flags]\n"
215
231
  default:
@@ -273,7 +289,16 @@ func newRunner(opts Options) (*runner, error) {
273
289
  if payload != "" {
274
290
  payload, _ = filepath.Abs(payload)
275
291
  }
276
- r := &runner{opts: opts, target: target, payload: payload, payloadFS: os.DirFS(payload), source: source, prev: map[string]bool{}, records: map[string]string{}}
292
+ r := &runner{
293
+ opts: opts,
294
+ target: target,
295
+ payload: payload,
296
+ payloadFS: os.DirFS(payload),
297
+ source: source,
298
+ prev: map[string]managedRecord{},
299
+ records: map[string]string{},
300
+ preflight: map[string]pathSnapshot{},
301
+ }
277
302
  if opts.Mode != ModeUninstall {
278
303
  if err := r.validatePayload(); err != nil {
279
304
  return nil, fmt.Errorf("validate payload: %w", err)
@@ -339,6 +364,14 @@ func (r *runner) install() error {
339
364
  fmt.Fprintln(r.opts.Stdout, " (dry run - no changes will be made)")
340
365
  }
341
366
  fmt.Fprintln(r.opts.Stdout)
367
+ if r.preparedBinary != "" {
368
+ if _, err := verifyEngineBinary(r.preparedBinary, r.binaryTag(), 30*time.Second); err != nil {
369
+ return fmt.Errorf("verify staged engine binary %s: %w", r.preparedBinary, err)
370
+ }
371
+ }
372
+ if err := r.preflightInstall(); err != nil {
373
+ return err
374
+ }
342
375
 
343
376
  for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
344
377
  if err := r.installTree(tree.PayloadPrefix, tree.TargetPrefix); err != nil {
@@ -420,12 +453,233 @@ func (r *runner) installFile(src, rel string) error {
420
453
  return r.installData(data, rel)
421
454
  }
422
455
 
456
+ func (r *runner) preflightInstall() error {
457
+ desired, err := r.desiredInstallPaths()
458
+ if err != nil {
459
+ return fmt.Errorf("preflight install paths: %w", err)
460
+ }
461
+ var conflicts []string
462
+ for rel := range desired {
463
+ snapshot, err := r.rememberPath(rel)
464
+ if err != nil {
465
+ return err
466
+ }
467
+ record, managed := r.prev[rel]
468
+ if managed && !snapshot.missing {
469
+ if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
470
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
471
+ }
472
+ }
473
+ }
474
+ for rel, record := range r.prev {
475
+ if desired[rel] || hostpack.PreserveOnPrune(rel) {
476
+ continue
477
+ }
478
+ snapshot, err := r.rememberPath(rel)
479
+ if err != nil {
480
+ return err
481
+ }
482
+ if !snapshot.missing {
483
+ if kind := managedConflict(record, snapshot); kind != "" && !r.opts.Force {
484
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
485
+ }
486
+ }
487
+ if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
488
+ if _, err := r.rememberPath(merge.TargetRel); err != nil {
489
+ return err
490
+ }
491
+ }
492
+ }
493
+ for _, rel := range r.installMergeTargets() {
494
+ if _, err := r.rememberPath(rel); err != nil {
495
+ return err
496
+ }
497
+ }
498
+ if _, err := r.rememberPath(ManifestName); err != nil {
499
+ return err
500
+ }
501
+ return managedConflictError(conflicts)
502
+ }
503
+
504
+ func (r *runner) preflightUninstall(entries []string) error {
505
+ var conflicts []string
506
+ for _, rel := range entries {
507
+ if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
508
+ if _, err := r.rememberPath(merge.TargetRel); err != nil {
509
+ return err
510
+ }
511
+ }
512
+ if !hostpack.ShouldRemoveOnUninstall(rel, entries) {
513
+ continue
514
+ }
515
+ snapshot, err := r.rememberPath(rel)
516
+ if err != nil {
517
+ return err
518
+ }
519
+ if snapshot.missing {
520
+ continue
521
+ }
522
+ if kind := managedConflict(r.prev[rel], snapshot); kind != "" && !r.opts.Force {
523
+ conflicts = append(conflicts, fmt.Sprintf("%s: %s", kind, rel))
524
+ }
525
+ }
526
+ if _, err := r.rememberPath(ManifestName); err != nil {
527
+ return err
528
+ }
529
+ return managedConflictError(conflicts)
530
+ }
531
+
532
+ func (r *runner) desiredInstallPaths() (map[string]bool, error) {
533
+ out := map[string]bool{".devrites/README.md": true}
534
+ for _, tree := range hostpack.InstallTrees(r.opts.WithSkills, r.opts.WithAgents, r.opts.WithCodex) {
535
+ err := fs.WalkDir(r.payloadFS, tree.PayloadPrefix, func(path string, d fs.DirEntry, err error) error {
536
+ if err != nil {
537
+ return err
538
+ }
539
+ if d.IsDir() {
540
+ return nil
541
+ }
542
+ rel, err := filepath.Rel(tree.PayloadPrefix, path)
543
+ if err != nil {
544
+ return err
545
+ }
546
+ out[filepath.ToSlash(filepath.Join(tree.TargetPrefix, rel))] = true
547
+ return nil
548
+ })
549
+ if err != nil {
550
+ return nil, err
551
+ }
552
+ }
553
+ if r.opts.WithSkills && r.opts.AliasMode == "all" {
554
+ for _, alias := range hostpack.Aliases {
555
+ for _, rel := range hostpack.AliasTargets(alias, r.opts.WithCodex) {
556
+ out[rel] = true
557
+ }
558
+ }
559
+ }
560
+ if r.opts.WithSkills && r.opts.WithCodex {
561
+ out[hostpack.CodexAgentsMerge.MarkerRel] = true
562
+ out[hostpack.CodexHooksMerge.MarkerRel] = true
563
+ }
564
+ if r.opts.WithSkills {
565
+ out[hostpack.ClaudeSettingsMerge.MarkerRel] = true
566
+ }
567
+ return out, nil
568
+ }
569
+
570
+ func (r *runner) installMergeTargets() []string {
571
+ var out []string
572
+ if r.opts.WithSkills {
573
+ out = append(out, hostpack.ClaudeSettingsMerge.TargetRel)
574
+ if r.opts.WithCodex {
575
+ out = append(out, hostpack.CodexAgentsMerge.TargetRel, hostpack.CodexHooksMerge.TargetRel)
576
+ }
577
+ }
578
+ return out
579
+ }
580
+
581
+ func (r *runner) rememberPath(rel string) (pathSnapshot, error) {
582
+ rel = filepath.ToSlash(rel)
583
+ if snapshot, ok := r.preflight[rel]; ok {
584
+ return snapshot, nil
585
+ }
586
+ snapshot, err := inspectManagedPath(r.target, rel)
587
+ if err != nil {
588
+ return pathSnapshot{}, err
589
+ }
590
+ r.preflight[rel] = snapshot
591
+ return snapshot, nil
592
+ }
593
+
594
+ func (r *runner) recheckPath(rel string) error {
595
+ rel = filepath.ToSlash(rel)
596
+ before, ok := r.preflight[rel]
597
+ if !ok {
598
+ return fmt.Errorf("path %s was not preflighted", rel)
599
+ }
600
+ now, err := inspectManagedPath(r.target, rel)
601
+ if err != nil {
602
+ return err
603
+ }
604
+ if now != before {
605
+ return fmt.Errorf("refusing to change %s: file changed after preflight; retry the operation", rel)
606
+ }
607
+ return nil
608
+ }
609
+
610
+ func inspectManagedPath(target, rel string) (pathSnapshot, error) {
611
+ native := filepath.FromSlash(rel)
612
+ if !filepath.IsLocal(native) {
613
+ return pathSnapshot{}, fmt.Errorf("refusing unsafe managed path %q", rel)
614
+ }
615
+ dest := filepath.Join(target, native)
616
+ if !safepath.WithinResolved(dest, target) {
617
+ return pathSnapshot{}, fmt.Errorf("refusing managed path outside target: %s", rel)
618
+ }
619
+ walk := target
620
+ parts := strings.Split(native, string(filepath.Separator))
621
+ for _, part := range parts {
622
+ walk = filepath.Join(walk, part)
623
+ info, err := os.Lstat(walk)
624
+ if os.IsNotExist(err) {
625
+ return pathSnapshot{missing: true}, nil
626
+ }
627
+ if err != nil {
628
+ return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
629
+ }
630
+ if info.Mode()&os.ModeSymlink != 0 {
631
+ return pathSnapshot{}, fmt.Errorf("refusing symlink or junction in managed path: %s", rel)
632
+ }
633
+ }
634
+ info, err := os.Lstat(dest)
635
+ if os.IsNotExist(err) {
636
+ return pathSnapshot{missing: true}, nil
637
+ }
638
+ if err != nil {
639
+ return pathSnapshot{}, fmt.Errorf("inspect managed path %s: %w", rel, err)
640
+ }
641
+ if !info.Mode().IsRegular() {
642
+ return pathSnapshot{}, fmt.Errorf("refusing non-regular managed path: %s", rel)
643
+ }
644
+ f, err := os.Open(dest)
645
+ if err != nil {
646
+ return pathSnapshot{}, fmt.Errorf("read managed path %s: %w", rel, err)
647
+ }
648
+ defer f.Close()
649
+ sum := sha256.New()
650
+ if _, err := io.Copy(sum, f); err != nil {
651
+ return pathSnapshot{}, fmt.Errorf("hash managed path %s: %w", rel, err)
652
+ }
653
+ return pathSnapshot{hash: "sha256:" + hex.EncodeToString(sum.Sum(nil))}, nil
654
+ }
655
+
656
+ func managedConflict(record managedRecord, snapshot pathSnapshot) string {
657
+ if record.Hash == "" {
658
+ return "legacy manifest entry (no hash)"
659
+ }
660
+ if record.Hash != snapshot.hash {
661
+ return "customized managed file"
662
+ }
663
+ return ""
664
+ }
665
+
666
+ func managedConflictError(conflicts []string) error {
667
+ if len(conflicts) == 0 {
668
+ return nil
669
+ }
670
+ sort.Strings(conflicts)
671
+ return fmt.Errorf("managed files differ from the install manifest:\n %s\nrerun with --force to replace or remove these files", strings.Join(conflicts, "\n "))
672
+ }
673
+
423
674
  func (r *runner) installData(data []byte, rel string) error {
424
675
  dest := filepath.Join(r.target, filepath.FromSlash(rel))
425
676
  action := "install"
426
677
  if exists(dest) {
678
+ record, managed := r.prev[rel]
427
679
  switch {
428
- case r.prev[rel]:
680
+ case managed && managedConflict(record, r.preflight[filepath.ToSlash(rel)]) != "" && r.opts.Force:
681
+ action = "overwrite(force-customized)"
682
+ case managed:
429
683
  action = "overwrite"
430
684
  case r.opts.Force:
431
685
  action = "overwrite(force)"
@@ -441,8 +695,13 @@ func (r *runner) installData(data []byte, rel string) error {
441
695
  }
442
696
  if r.opts.DryRun {
443
697
  fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
444
- } else if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
445
- return fmt.Errorf("cannot write %s: %w", rel, err)
698
+ } else {
699
+ if err := r.recheckPath(rel); err != nil {
700
+ return err
701
+ }
702
+ if err := fsutil.WriteFileAtomic(dest, data, 0o644); err != nil {
703
+ return fmt.Errorf("cannot write %s: %w", rel, err)
704
+ }
446
705
  }
447
706
  r.addManifest(rel)
448
707
  r.addInstallRecord(rel, data)
@@ -488,6 +747,9 @@ func (r *runner) mergeMarkerFile(merge hostpack.MarkerMerge) error {
488
747
  }
489
748
  fmt.Fprintf(r.opts.Stdout, " [merge] %s (%s)\n", merge.TargetRel, verb)
490
749
  } else {
750
+ if err := r.recheckPath(merge.TargetRel); err != nil {
751
+ return err
752
+ }
491
753
  next := block
492
754
  if current, err := os.ReadFile(dest); err == nil {
493
755
  next = hostpack.MergeMarkerBlock(current, block, merge.Begin, merge.End)
@@ -509,6 +771,9 @@ func (r *runner) mergeJSONHooks(merge hostpack.JSONMerge) error {
509
771
  fmt.Fprintf(r.opts.Stdout, " [merge] %s\n", merge.DryRunText)
510
772
  return r.installMarker(merge.MarkerRel, merge.MarkerText)
511
773
  }
774
+ if err := r.recheckPath(merge.TargetRel); err != nil {
775
+ return err
776
+ }
512
777
  next := devrites
513
778
  if current, err := readJSON(dest); err == nil {
514
779
  if currentStatus, exists := current["statusLine"]; exists && !isDevritesHookCommand(currentStatus) {
@@ -542,7 +807,7 @@ func (r *runner) seedClaudeSettings() error {
542
807
  if err != nil {
543
808
  return fmt.Errorf("load Claude settings payload: %w", err)
544
809
  }
545
- managed := r.prev[merge.MarkerRel]
810
+ _, managed := r.prev[merge.MarkerRel]
546
811
  if reflect.DeepEqual(current, devrites) {
547
812
  if managed {
548
813
  return r.installMarker(merge.MarkerRel, merge.MarkerText)
@@ -558,6 +823,9 @@ func (r *runner) seedClaudeSettings() error {
558
823
  if err != nil {
559
824
  return fmt.Errorf("read payload claude/settings.json: %w", err)
560
825
  }
826
+ if err := r.recheckPath(rel); err != nil {
827
+ return err
828
+ }
561
829
  return fsutil.WriteFileAtomic(dest, data, 0o644)
562
830
  }
563
831
  return r.mergeJSONHooks(merge)
@@ -570,6 +838,9 @@ func (r *runner) seedClaudeSettings() error {
570
838
  if err != nil {
571
839
  return fmt.Errorf("read payload claude/settings.json: %w", err)
572
840
  }
841
+ if err := r.recheckPath(rel); err != nil {
842
+ return err
843
+ }
573
844
  return fsutil.WriteFileAtomic(dest, data, 0o644)
574
845
  }
575
846
 
@@ -623,10 +894,20 @@ func (r *runner) pruneDropped() error {
623
894
  if !exists(dead) {
624
895
  continue
625
896
  }
897
+ action := "prune"
898
+ if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
899
+ action = "prune(force-customized)"
900
+ }
626
901
  if r.opts.DryRun {
627
- fmt.Fprintf(r.opts.Stdout, " [prune] %s (dropped from pack)\n", rel)
902
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s (dropped from pack)\n", action, rel)
628
903
  } else {
904
+ if err := r.recheckPath(rel); err != nil {
905
+ return err
906
+ }
629
907
  if merge, ok := hostpack.ManagedMergeForMarker(rel); ok {
908
+ if err := r.recheckPath(merge.TargetRel); err != nil {
909
+ return err
910
+ }
630
911
  if merge.TargetRel == hostpack.CodexHooksMerge.TargetRel || merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
631
912
  preserveEmpty := merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel
632
913
  _ = r.stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), preserveEmpty)
@@ -638,7 +919,7 @@ func (r *runner) pruneDropped() error {
638
919
  return fmt.Errorf("remove %s: %w", rel, err)
639
920
  }
640
921
  pruneEmptyDirs(filepath.Dir(dead), r.target)
641
- fmt.Fprintf(r.opts.Stdout, " [prune] %s\n", rel)
922
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
642
923
  }
643
924
  r.stats.pruned++
644
925
  }
@@ -662,6 +943,9 @@ func (r *runner) writeManifest() error {
662
943
  for _, rel := range r.manifest {
663
944
  b.WriteString(rel + "\n")
664
945
  }
946
+ if err := r.recheckPath(ManifestName); err != nil {
947
+ return err
948
+ }
665
949
  return fsutil.WriteFileAtomic(filepath.Join(r.target, ManifestName), []byte(b.String()), 0o644)
666
950
  }
667
951
 
@@ -701,6 +985,9 @@ func (r *runner) uninstall() error {
701
985
  fmt.Fprintln(r.opts.Stdout, " (dry run - no changes)")
702
986
  }
703
987
  fmt.Fprintln(r.opts.Stdout)
988
+ if err := r.preflightUninstall(entries); err != nil {
989
+ return err
990
+ }
704
991
 
705
992
  for _, rel := range entries {
706
993
  merge, ok := hostpack.ManagedMergeForMarker(rel)
@@ -711,6 +998,12 @@ func (r *runner) uninstall() error {
711
998
  fmt.Fprintf(r.opts.Stdout, " [merge-remove] %s\n", merge.DryRun)
712
999
  continue
713
1000
  }
1001
+ if err := r.recheckPath(rel); err != nil {
1002
+ return err
1003
+ }
1004
+ if err := r.recheckPath(merge.TargetRel); err != nil {
1005
+ return err
1006
+ }
714
1007
  if merge.TargetRel == hostpack.CodexHooksMerge.TargetRel || merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel {
715
1008
  preserveEmpty := merge.TargetRel == hostpack.ClaudeSettingsMerge.TargetRel
716
1009
  if err := r.stripHooksPath(filepath.Join(r.target, filepath.FromSlash(merge.TargetRel)), preserveEmpty); err != nil {
@@ -730,10 +1023,19 @@ func (r *runner) uninstall() error {
730
1023
  }
731
1024
  dest := filepath.Join(r.target, filepath.FromSlash(rel))
732
1025
  if exists(dest) {
1026
+ action := "remove"
1027
+ if managedConflict(r.prev[rel], r.preflight[rel]) != "" && r.opts.Force {
1028
+ action = "remove(force-customized)"
1029
+ }
733
1030
  if r.opts.DryRun {
734
- fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", rel)
735
- } else if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
736
- return fmt.Errorf("remove %s: %w", rel, err)
1031
+ fmt.Fprintf(r.opts.Stdout, " [%s] %s\n", action, rel)
1032
+ } else {
1033
+ if err := r.recheckPath(rel); err != nil {
1034
+ return err
1035
+ }
1036
+ if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
1037
+ return fmt.Errorf("remove %s: %w", rel, err)
1038
+ }
737
1039
  }
738
1040
  dirs = append(dirs, filepath.Dir(dest))
739
1041
  r.stats.removed++
@@ -744,6 +1046,9 @@ func (r *runner) uninstall() error {
744
1046
  if r.opts.DryRun {
745
1047
  fmt.Fprintf(r.opts.Stdout, " [remove] %s\n", ManifestName)
746
1048
  } else {
1049
+ if err := r.recheckPath(ManifestName); err != nil {
1050
+ return err
1051
+ }
747
1052
  _ = os.Remove(mf)
748
1053
  dirs = append(dirs, filepath.Dir(mf))
749
1054
  for _, d := range dirs {
@@ -845,11 +1150,24 @@ func runUpdate(opts Options) error {
845
1150
  next.Target = target
846
1151
  next.PayloadDir = payload
847
1152
  next.SourceDir = source
848
- next.Force = true
1153
+ next.Force = opts.Force
849
1154
  if opts.DryRun {
850
1155
  next.DryRun = true
851
1156
  }
852
- return Apply(next)
1157
+ r, err := newRunner(next)
1158
+ if err != nil {
1159
+ return fmt.Errorf("prepare installer: %w", err)
1160
+ }
1161
+ r.releaseBinaryTag = latestTag
1162
+ if next.WithBinary && os.Getenv("DEVRITES_NO_BINARY") != "1" && !next.DryRun {
1163
+ staged, cleanup, err := r.acquireBinary(latestTag, githubBaseURL)
1164
+ if err != nil {
1165
+ return fmt.Errorf("prepare engine binary: %w", err)
1166
+ }
1167
+ defer cleanup()
1168
+ r.preparedBinary = staged
1169
+ }
1170
+ return r.install()
853
1171
  }
854
1172
 
855
1173
  func resolveUpdateTag(opts Options) (string, error) {
@@ -1285,10 +1603,7 @@ func (r *runner) installBinary() error {
1285
1603
  fmt.Fprintln(r.opts.Stdout, " would install the global devrites-engine control-plane binary")
1286
1604
  return nil
1287
1605
  }
1288
- tag := os.Getenv("DEVRITES_REF")
1289
- if tag == "" {
1290
- tag = "v" + strings.TrimPrefix(installedVersion(r.source), "v")
1291
- }
1606
+ tag := r.binaryTag()
1292
1607
  incoming := strings.TrimPrefix(tag, "v")
1293
1608
  dest := binaryDest()
1294
1609
  if exists(dest) {
@@ -1297,26 +1612,48 @@ func (r *runner) installBinary() error {
1297
1612
  return nil
1298
1613
  }
1299
1614
  }
1300
- staged, cleanup, err := r.acquireBinary(tag)
1301
- if err != nil {
1302
- fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
1303
- return nil
1615
+ staged, cleanup := r.preparedBinary, func() {}
1616
+ if staged == "" {
1617
+ var err error
1618
+ staged, cleanup, err = r.acquireBinary(tag, githubBaseURL)
1619
+ if err != nil {
1620
+ return r.binaryInstallFailure(err)
1621
+ }
1304
1622
  }
1305
1623
  defer cleanup()
1624
+ if _, err := verifyEngineBinary(staged, tag, 30*time.Second); err != nil {
1625
+ return fmt.Errorf("verify staged engine binary %s: %w", staged, err)
1626
+ }
1306
1627
  if err := os.MkdirAll(filepath.Dir(dest), 0o755); err != nil {
1307
- fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
1308
- return nil
1628
+ return err
1309
1629
  }
1310
1630
  data, err := os.ReadFile(staged)
1311
1631
  if err != nil {
1312
- fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
1313
- return nil
1632
+ return err
1633
+ }
1634
+ backup, oldMode, hadOld, err := backupBinary(dest)
1635
+ if err != nil {
1636
+ return err
1637
+ }
1638
+ if backup != "" {
1639
+ defer os.Remove(backup)
1314
1640
  }
1315
1641
  if err := fsutil.WriteFileAtomic(dest, data, 0o755); err != nil {
1316
- fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
1317
- return nil
1642
+ if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
1643
+ return fmt.Errorf("install binary: %v; restore previous binary: %w", err, restoreErr)
1644
+ }
1645
+ return err
1318
1646
  }
1319
1647
  _ = os.Chmod(dest, 0o755)
1648
+ if _, err := verifyEngineBinary(dest, tag, 30*time.Second); err != nil {
1649
+ if restoreErr := restoreBinary(dest, backup, oldMode, hadOld); restoreErr != nil {
1650
+ return fmt.Errorf("verify installed engine binary: %v; restore previous binary: %w", err, restoreErr)
1651
+ }
1652
+ if hadOld {
1653
+ return fmt.Errorf("verify installed engine binary: %w (previous binary restored)", err)
1654
+ }
1655
+ return fmt.Errorf("verify installed engine binary: %w (bad binary removed)", err)
1656
+ }
1320
1657
  fmt.Fprintf(r.opts.Stdout, " engine binary: installed %s\n", dest)
1321
1658
  if !binaryReachableFromPATH(dest) {
1322
1659
  fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %s is not on PATH; installed hooks will fail open until devrites-engine is reachable.\n", dest)
@@ -1324,32 +1661,142 @@ func (r *runner) installBinary() error {
1324
1661
  return nil
1325
1662
  }
1326
1663
 
1327
- func (r *runner) acquireBinary(tag string) (string, func(), error) {
1664
+ func (r *runner) binaryTag() string {
1665
+ if r.releaseBinaryTag != "" {
1666
+ return r.releaseBinaryTag
1667
+ }
1668
+ if tag := os.Getenv("DEVRITES_REF"); tag != "" {
1669
+ return tag
1670
+ }
1671
+ return "v" + strings.TrimPrefix(installedVersion(r.source), "v")
1672
+ }
1673
+
1674
+ func backupBinary(dest string) (string, fs.FileMode, bool, error) {
1675
+ info, err := os.Lstat(dest)
1676
+ if os.IsNotExist(err) {
1677
+ return "", 0, false, nil
1678
+ }
1679
+ if err != nil {
1680
+ return "", 0, false, fmt.Errorf("inspect existing engine binary: %w", err)
1681
+ }
1682
+ if !info.Mode().IsRegular() {
1683
+ return "", 0, false, fmt.Errorf("refusing to replace non-regular engine binary: %s", dest)
1684
+ }
1685
+ data, err := os.ReadFile(dest)
1686
+ if err != nil {
1687
+ return "", 0, false, fmt.Errorf("read existing engine binary: %w", err)
1688
+ }
1689
+ f, err := os.CreateTemp(filepath.Dir(dest), "."+filepath.Base(dest)+".backup-*")
1690
+ if err != nil {
1691
+ return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
1692
+ }
1693
+ backup := f.Name()
1694
+ if err := f.Close(); err != nil {
1695
+ _ = os.Remove(backup)
1696
+ return "", 0, false, fmt.Errorf("create engine binary backup: %w", err)
1697
+ }
1698
+ if err := fsutil.WriteFileAtomic(backup, data, info.Mode().Perm()); err != nil {
1699
+ _ = os.Remove(backup)
1700
+ return "", 0, false, fmt.Errorf("write engine binary backup: %w", err)
1701
+ }
1702
+ return backup, info.Mode().Perm(), true, nil
1703
+ }
1704
+
1705
+ func restoreBinary(dest, backup string, mode fs.FileMode, hadOld bool) error {
1706
+ if !hadOld {
1707
+ if err := os.Remove(dest); err != nil && !os.IsNotExist(err) {
1708
+ return err
1709
+ }
1710
+ return nil
1711
+ }
1712
+ data, err := os.ReadFile(backup)
1713
+ if err != nil {
1714
+ return err
1715
+ }
1716
+ return fsutil.WriteFileAtomic(dest, data, mode)
1717
+ }
1718
+
1719
+ func (r *runner) binaryInstallFailure(err error) error {
1720
+ if r.preparedBinary != "" {
1721
+ return err
1722
+ }
1723
+ fmt.Fprintf(r.opts.Stderr, "warning: engine binary: %v - hooks fail open until you install it.\n", err)
1724
+ return nil
1725
+ }
1726
+
1727
+ func (r *runner) acquireBinary(tag, baseURL string) (string, func(), error) {
1328
1728
  if v := os.Getenv("DEVRITES_ENGINE_CLI"); v != "" {
1329
1729
  if exists(v) {
1330
- return v, func() {}, nil
1730
+ if r.releaseBinaryTag == "" || strings.TrimPrefix(engineVersion(v), "v") == strings.TrimPrefix(r.releaseBinaryTag, "v") {
1731
+ return v, func() {}, nil
1732
+ }
1733
+ } else {
1734
+ return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", v)
1331
1735
  }
1332
- return "", func() {}, fmt.Errorf("DEVRITES_ENGINE_CLI points to a missing binary: %s", v)
1333
1736
  }
1334
1737
  tmp, err := os.MkdirTemp("", "devrites-engine-*")
1335
1738
  if err != nil {
1336
1739
  return "", func() {}, fmt.Errorf("create temp dir: %w", err)
1337
1740
  }
1338
1741
  cleanup := func() { _ = os.RemoveAll(tmp) }
1339
- staged := filepath.Join(tmp, "devrites-engine")
1742
+ staged := filepath.Join(tmp, engineBinaryName())
1743
+ var releaseErr error
1744
+ if r.releaseBinaryTag != "" && os.Getenv("DEVRITES_UPDATE_BUNDLE") == "" {
1745
+ repo := os.Getenv("DEVRITES_REPO")
1746
+ if repo == "" {
1747
+ repo = defaultRepo
1748
+ }
1749
+ downloaded, err := downloadReleaseBinary(baseURL, repo, r.releaseBinaryTag, staged)
1750
+ if err == nil {
1751
+ return staged, cleanup, nil
1752
+ }
1753
+ if downloaded {
1754
+ return "", cleanup, err
1755
+ }
1756
+ releaseErr = err
1757
+ }
1340
1758
  if r.source != "" && exists(filepath.Join(r.source, "engine", "go.mod")) {
1341
1759
  ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
1342
1760
  defer cancel()
1343
- cmd := exec.CommandContext(ctx, "go", "build", "-trimpath", "-ldflags", "-s -w -X github.com/devrites/devrites/internal/version.Version="+tag, "-o", staged, ".")
1761
+ cmd := exec.CommandContext(ctx, "go", "build", "-buildvcs=false", "-trimpath", "-ldflags", "-s -w -X github.com/devrites/devrites/internal/version.Version="+tag, "-o", staged, ".")
1344
1762
  cmd.Dir = filepath.Join(r.source, "engine")
1345
1763
  if out, err := cmd.CombinedOutput(); err != nil {
1764
+ if releaseErr != nil {
1765
+ return "", cleanup, fmt.Errorf("could not download release binary: %v; source fallback failed: %w: %s", releaseErr, err, strings.TrimSpace(string(out)))
1766
+ }
1346
1767
  return "", cleanup, fmt.Errorf("could not build from source: %w: %s", err, strings.TrimSpace(string(out)))
1347
1768
  }
1348
1769
  return staged, cleanup, nil
1349
1770
  }
1771
+ if releaseErr != nil {
1772
+ return "", cleanup, fmt.Errorf("could not obtain a binary: %v; no Go source fallback", releaseErr)
1773
+ }
1350
1774
  return "", cleanup, fmt.Errorf("could not obtain a binary (no DEVRITES_ENGINE_CLI handoff, no Go source)")
1351
1775
  }
1352
1776
 
1777
+ func downloadReleaseBinary(baseURL, repo, tag, dest string) (bool, error) {
1778
+ asset := fmt.Sprintf("devrites-%s-%s", runtime.GOOS, runtime.GOARCH)
1779
+ if runtime.GOOS == "windows" {
1780
+ asset += ".exe"
1781
+ }
1782
+ url := fmt.Sprintf("%s/%s/releases/download/%s/%s", strings.TrimRight(baseURL, "/"), repo, tag, asset)
1783
+ if err := iohooks.DownloadFile(url, dest); err != nil {
1784
+ return false, fmt.Errorf("download %s: %w", asset, err)
1785
+ }
1786
+ sumPath := dest + ".sha256"
1787
+ defer os.Remove(sumPath)
1788
+ if err := iohooks.DownloadFile(url+".sha256", sumPath); err != nil {
1789
+ return true, fmt.Errorf("could not verify release binary %s: missing checksum: %w", asset, err)
1790
+ }
1791
+ if err := verifySHA256(dest, sumPath); err != nil {
1792
+ return true, fmt.Errorf("could not verify release binary %s: %w", asset, err)
1793
+ }
1794
+ if err := os.Chmod(dest, 0o755); err != nil && runtime.GOOS != "windows" {
1795
+ return true, fmt.Errorf("make release binary executable: %w", err)
1796
+ }
1797
+ return true, nil
1798
+ }
1799
+
1353
1800
  func (r *runner) removeBinary() error {
1354
1801
  if r.opts.KeepBinary {
1355
1802
  fmt.Fprintln(r.opts.Stdout, " kept the global devrites-engine binary (--keep-binary).")
@@ -1373,24 +1820,31 @@ func (r *runner) removeBinary() error {
1373
1820
 
1374
1821
  func binaryDest() string {
1375
1822
  if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
1376
- return filepath.Join(dir, "devrites-engine")
1823
+ return filepath.Join(dir, engineBinaryName())
1377
1824
  }
1378
1825
  if home, err := os.UserHomeDir(); err == nil {
1379
- return filepath.Join(home, ".local", "bin", "devrites-engine")
1826
+ return filepath.Join(home, ".local", "bin", engineBinaryName())
1380
1827
  }
1381
- return "/usr/local/bin/devrites-engine"
1828
+ return filepath.Join("/usr/local/bin", engineBinaryName())
1829
+ }
1830
+
1831
+ func engineBinaryName() string {
1832
+ if runtime.GOOS == "windows" {
1833
+ return "devrites-engine.exe"
1834
+ }
1835
+ return "devrites-engine"
1382
1836
  }
1383
1837
 
1384
1838
  func binaryCandidates() []string {
1385
1839
  candidates := []string{}
1386
1840
  if dir := os.Getenv("DEVRITES_BIN_DIR"); dir != "" {
1387
- candidates = append(candidates, filepath.Join(dir, "devrites-engine"))
1841
+ candidates = append(candidates, filepath.Join(dir, engineBinaryName()))
1388
1842
  }
1389
1843
  home, _ := os.UserHomeDir()
1390
1844
  if home != "" {
1391
- candidates = append(candidates, filepath.Join(home, ".local", "bin", "devrites-engine"))
1845
+ candidates = append(candidates, filepath.Join(home, ".local", "bin", engineBinaryName()))
1392
1846
  }
1393
- return append(candidates, "/usr/local/bin/devrites-engine")
1847
+ return append(candidates, filepath.Join("/usr/local/bin", engineBinaryName()))
1394
1848
  }
1395
1849
 
1396
1850
  func binaryReachableFromPATH(dest string) bool {
@@ -1424,31 +1878,81 @@ func pathContainsDir(dir string) bool {
1424
1878
  }
1425
1879
 
1426
1880
  func engineVersion(path string) string {
1427
- ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
1428
- defer cancel()
1429
- out, err := exec.CommandContext(ctx, path, "version").Output()
1881
+ got, err := readEngineVersion(path, 30*time.Second)
1430
1882
  if err != nil {
1431
1883
  return ""
1432
1884
  }
1433
- return strings.TrimSpace(string(out))
1885
+ return got
1434
1886
  }
1435
1887
 
1436
- func readManifest(path string) map[string]bool {
1437
- out := map[string]bool{}
1438
- for _, rel := range readManifestList(path) {
1439
- out[rel] = true
1888
+ func verifyEngineBinary(path, want string, timeout time.Duration) (string, error) {
1889
+ want = strings.TrimPrefix(strings.TrimSpace(want), "v")
1890
+ if want == "" || want == "dev" || !semverLike(want) {
1891
+ return "", fmt.Errorf("invalid requested version %q", want)
1440
1892
  }
1441
- return out
1893
+ got, err := readEngineVersion(path, timeout)
1894
+ if err != nil {
1895
+ return "", err
1896
+ }
1897
+ if strings.TrimPrefix(got, "v") != want {
1898
+ return "", fmt.Errorf("version mismatch: got %s want %s", got, want)
1899
+ }
1900
+ return got, nil
1901
+ }
1902
+
1903
+ func readEngineVersion(path string, timeout time.Duration) (string, error) {
1904
+ ctx, cancel := context.WithTimeout(context.Background(), timeout)
1905
+ defer cancel()
1906
+ out, err := exec.CommandContext(ctx, path, "version").Output()
1907
+ if err != nil {
1908
+ if errors.Is(ctx.Err(), context.DeadlineExceeded) {
1909
+ return "", fmt.Errorf("version command timed out after %s", timeout)
1910
+ }
1911
+ return "", fmt.Errorf("run exact path %s version: %w", path, err)
1912
+ }
1913
+ line := strings.TrimSuffix(string(out), "\n")
1914
+ line = strings.TrimSuffix(line, "\r")
1915
+ if strings.ContainsAny(line, "\r\n") {
1916
+ return "", fmt.Errorf("invalid multi-line version output")
1917
+ }
1918
+ line = strings.TrimSpace(line)
1919
+ if line == "" || line == "dev" || !semverLike(line) {
1920
+ return "", fmt.Errorf("invalid version output %q", line)
1921
+ }
1922
+ return line, nil
1923
+ }
1924
+
1925
+ func readManifest(path string) map[string]managedRecord {
1926
+ records, _ := parseManifest(path)
1927
+ return records
1442
1928
  }
1443
1929
 
1444
1930
  func readManifestList(path string) []string {
1931
+ _, entries := parseManifest(path)
1932
+ return entries
1933
+ }
1934
+
1935
+ func parseManifest(path string) (map[string]managedRecord, []string) {
1445
1936
  data, err := os.ReadFile(path)
1446
1937
  if err != nil {
1447
- return nil
1938
+ return map[string]managedRecord{}, nil
1448
1939
  }
1940
+ hashes := map[string]string{}
1449
1941
  var out []string
1450
1942
  for _, line := range strings.Split(string(data), "\n") {
1451
1943
  line = strings.TrimSpace(line)
1944
+ if strings.HasPrefix(line, "# managed: ") {
1945
+ record := strings.TrimPrefix(line, "# managed: ")
1946
+ i := strings.LastIndex(record, " sha256:")
1947
+ if i > 0 {
1948
+ rel := strings.TrimSpace(record[:i])
1949
+ hash := "sha256:" + strings.ToLower(strings.TrimSpace(record[i+len(" sha256:"):]))
1950
+ if filepath.IsLocal(filepath.FromSlash(rel)) && validManagedHash(hash) {
1951
+ hashes[rel] = hash
1952
+ }
1953
+ }
1954
+ continue
1955
+ }
1452
1956
  if line == "" || strings.HasPrefix(line, "#") {
1453
1957
  continue
1454
1958
  }
@@ -1459,7 +1963,20 @@ func readManifestList(path string) []string {
1459
1963
  }
1460
1964
  out = append(out, line)
1461
1965
  }
1462
- return out
1966
+ records := make(map[string]managedRecord, len(out))
1967
+ for _, rel := range out {
1968
+ records[rel] = managedRecord{Hash: hashes[rel]}
1969
+ }
1970
+ return records, out
1971
+ }
1972
+
1973
+ func validManagedHash(hash string) bool {
1974
+ const prefix = "sha256:"
1975
+ if !strings.HasPrefix(hash, prefix) || len(hash) != len(prefix)+sha256.Size*2 {
1976
+ return false
1977
+ }
1978
+ _, err := hex.DecodeString(strings.TrimPrefix(hash, prefix))
1979
+ return err == nil
1463
1980
  }
1464
1981
 
1465
1982
  func manifestHeader(path, key string) string {