devrites 3.0.7 → 3.2.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 (484) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +82 -50
  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/0012-semantic-workspace-upgrades.md +77 -0
  12. package/docs/adr/README.md +4 -0
  13. package/docs/agents/triage-labels.md +7 -7
  14. package/docs/architecture.md +179 -141
  15. package/docs/cli.md +47 -12
  16. package/docs/command-map.md +61 -38
  17. package/docs/engine/agent-contract.md +92 -15
  18. package/docs/engine/commands.md +248 -72
  19. package/docs/engine/state-schema.md +52 -23
  20. package/docs/engine/workspace-schema.md +103 -15
  21. package/docs/extensions.md +1 -1
  22. package/docs/flow.md +86 -51
  23. package/docs/harness-compliance.md +29 -5
  24. package/docs/orchestration.md +107 -79
  25. package/docs/quick-reference.md +11 -3
  26. package/docs/release.md +4 -3
  27. package/docs/skills.md +67 -38
  28. package/docs/usage.md +84 -39
  29. package/engine/cmd/releasepack/main.go +219 -0
  30. package/engine/cmd/releasepack/main_test.go +170 -0
  31. package/engine/commands.go +170 -23
  32. package/engine/git_guard.go +187 -0
  33. package/engine/git_guard_test.go +283 -0
  34. package/engine/hookpolicy.go +53 -55
  35. package/engine/hookpolicy_test.go +91 -1
  36. package/engine/hooks.go +296 -75
  37. package/engine/hooks_events_test.go +276 -6
  38. package/engine/hooks_workspace.go +640 -159
  39. package/engine/internal/devritespaths/paths.go +65 -10
  40. package/engine/internal/devritespaths/paths_test.go +110 -0
  41. package/engine/internal/doctor/doctor.go +153 -23
  42. package/engine/internal/doctor/doctor_test.go +74 -0
  43. package/engine/internal/forge/forge.go +940 -0
  44. package/engine/internal/forge/forge_test.go +576 -0
  45. package/engine/internal/forge/git.go +245 -0
  46. package/engine/internal/forge/liveness_unix.go +48 -0
  47. package/engine/internal/forge/liveness_windows.go +67 -0
  48. package/engine/internal/forge/manifest.go +402 -0
  49. package/engine/internal/gate/gate.go +90 -71
  50. package/engine/internal/gate/gate_test.go +71 -2
  51. package/engine/internal/harness/compliance.go +18 -24
  52. package/engine/internal/harness/harness.go +37 -44
  53. package/engine/internal/harness/harness_test.go +21 -5
  54. package/engine/internal/install/install.go +486 -38
  55. package/engine/internal/install/install_test.go +383 -10
  56. package/engine/internal/iohooks/iohooks.go +350 -59
  57. package/engine/internal/iohooks/iohooks_test.go +421 -1
  58. package/engine/internal/lib/buildreadiness.go +43 -18
  59. package/engine/internal/lib/clarifyreturn.go +91 -0
  60. package/engine/internal/lib/clarifyreturn_test.go +123 -0
  61. package/engine/internal/lib/context.go +54 -15
  62. package/engine/internal/lib/cursor_compat_test.go +8 -0
  63. package/engine/internal/lib/extensions.go +2 -3
  64. package/engine/internal/lib/gitauthority.go +601 -0
  65. package/engine/internal/lib/gitauthority_test.go +346 -0
  66. package/engine/internal/lib/jsonout.go +25 -11
  67. package/engine/internal/lib/jsonout_test.go +16 -0
  68. package/engine/internal/lib/lanes.go +14 -8
  69. package/engine/internal/lib/observability_test.go +358 -0
  70. package/engine/internal/lib/packageexistence.go +151 -35
  71. package/engine/internal/lib/packageexistence_test.go +163 -0
  72. package/engine/internal/lib/progress.go +11 -9
  73. package/engine/internal/lib/provenance.go +462 -0
  74. package/engine/internal/lib/provenance_test.go +154 -0
  75. package/engine/internal/lib/readiness_contract.json +118 -0
  76. package/engine/internal/lib/readinessartifact.go +533 -0
  77. package/engine/internal/lib/readinessartifact_test.go +422 -0
  78. package/engine/internal/lib/reconcile.go +712 -88
  79. package/engine/internal/lib/reconcile_test.go +335 -16
  80. package/engine/internal/lib/recoveryattempts.go +298 -0
  81. package/engine/internal/lib/recoveryattempts_test.go +215 -0
  82. package/engine/internal/lib/resolve.go +23 -19
  83. package/engine/internal/lib/runbook_context_test.go +20 -0
  84. package/engine/internal/lib/session.go +701 -9
  85. package/engine/internal/lib/session_test.go +80 -13
  86. package/engine/internal/lib/testintegrity.go +33 -37
  87. package/engine/internal/lib/testintegrity_test.go +63 -1
  88. package/engine/internal/migrate/migrate.go +81 -1
  89. package/engine/internal/migrate/migrate_test.go +22 -1
  90. package/engine/internal/reason/reason.go +180 -0
  91. package/engine/internal/reason/reason_test.go +35 -0
  92. package/engine/internal/rootfacts/facts.go +466 -0
  93. package/engine/internal/rootfacts/facts_test.go +306 -0
  94. package/engine/internal/safepath/safepath.go +55 -0
  95. package/engine/internal/safepath/safepath_test.go +69 -0
  96. package/engine/internal/safepath/safepath_windows_test.go +26 -0
  97. package/engine/internal/state/clarify_transition.go +165 -0
  98. package/engine/internal/state/clarify_transition_test.go +130 -0
  99. package/engine/internal/state/cmd/workflowmanifest/main.go +12 -8
  100. package/engine/internal/state/cursor.go +58 -0
  101. package/engine/internal/state/cursor_test.go +42 -1
  102. package/engine/internal/state/feature.go +35 -48
  103. package/engine/internal/state/schema.go +90 -37
  104. package/engine/internal/state/snapshot.go +18 -2
  105. package/engine/internal/state/state_test.go +187 -7
  106. package/engine/internal/state/status.go +48 -11
  107. package/engine/internal/state/workflow_manifest.json +231 -50
  108. package/engine/internal/toolpolicy/classifier.go +533 -0
  109. package/engine/internal/toolpolicy/classifier_test.go +424 -0
  110. package/engine/internal/toolpolicy/git.go +616 -0
  111. package/engine/internal/toolpolicy/scanner.go +382 -0
  112. package/engine/main.go +160 -77
  113. package/engine/observability_cli_test.go +52 -0
  114. package/engine/root_routing_test.go +277 -0
  115. package/engine/testdata/golden/TestParityBuildReadiness/arg=approved.golden +1 -1
  116. package/engine/testdata/golden/TestParityBuildReadiness/arg=emptystatus.golden +1 -1
  117. package/engine/testdata/golden/TestParityBuildReadiness/arg=legacycontract.golden +1 -0
  118. package/engine/testdata/golden/TestParityBuildReadiness/arg=noclarify.golden +1 -0
  119. package/engine/testdata/golden/TestParityBuildReadiness/arg=novet.golden +1 -0
  120. package/engine/testdata/golden/TestParityBuildReadiness/arg=stalevet.golden +1 -0
  121. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailhash.golden +1 -1
  122. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailpipe.golden +1 -1
  123. package/engine/testdata/golden/TestParityBuildReadiness/arg=vetnotready.golden +1 -0
  124. package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
  125. package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
  126. package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
  127. package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
  128. package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
  129. package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
  130. package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
  131. package/engine/testdata/golden/TestParityReconcile/check-clean.golden +1 -1
  132. package/engine/testdata/golden/TestParityReconcile/inline-fallback.golden +1 -0
  133. package/engine/testdata/golden/TestParityReconcile/snapshot-no-allowlist.golden +1 -0
  134. package/engine/testdata/golden/TestParityWrightScope/devrites-edit-denied.golden +2 -0
  135. package/engine/testdata/golden/TestParityWrightScope/out-of-scope-enforce-denies.golden +1 -1
  136. package/engine/tests/adr_0011_define_plan_test.go +30 -0
  137. package/engine/tests/budget_test.go +19 -8
  138. package/engine/tests/concurrency_cli_test.go +7 -1
  139. package/engine/tests/doctor_cli_test.go +148 -2
  140. package/engine/tests/forge_cli_test.go +463 -0
  141. package/engine/tests/gate_test.go +63 -8
  142. package/engine/tests/hook_test.go +260 -8
  143. package/engine/tests/hooks_io_test.go +94 -3
  144. package/engine/tests/json_contract_test.go +127 -2
  145. package/engine/tests/migrate_cli_test.go +2 -2
  146. package/engine/tests/parity_buildreadiness_test.go +167 -10
  147. package/engine/tests/parity_learnings_test.go +16 -17
  148. package/engine/tests/parity_reconcile_test.go +20 -17
  149. package/engine/tests/parity_resolve_test.go +12 -11
  150. package/engine/tests/parity_test.go +21 -17
  151. package/pack/.claude/agents/devrites-code-reviewer.md +62 -48
  152. package/pack/.claude/agents/devrites-devex-reviewer.md +69 -53
  153. package/pack/.claude/agents/devrites-doubt-reviewer.md +29 -22
  154. package/pack/.claude/agents/devrites-evidence-scout.md +69 -0
  155. package/pack/.claude/agents/devrites-forge-judge.md +74 -61
  156. package/pack/.claude/agents/devrites-frontend-reviewer.md +48 -39
  157. package/pack/.claude/agents/devrites-performance-reviewer.md +49 -40
  158. package/pack/.claude/agents/devrites-plan-drafter.md +73 -0
  159. package/pack/.claude/agents/devrites-plan-reviewer.md +80 -47
  160. package/pack/.claude/agents/devrites-proof-runner.md +74 -0
  161. package/pack/.claude/agents/devrites-retrospector.md +48 -45
  162. package/pack/.claude/agents/devrites-security-auditor.md +46 -36
  163. package/pack/.claude/agents/devrites-simplifier-reviewer.md +50 -46
  164. package/pack/.claude/agents/devrites-slice-wright.md +153 -165
  165. package/pack/.claude/agents/devrites-spec-reviewer.md +34 -32
  166. package/pack/.claude/agents/devrites-strategy-reviewer.md +63 -34
  167. package/pack/.claude/agents/devrites-test-analyst.md +40 -30
  168. package/pack/.claude/agents/devrites-upgrade-planner.md +100 -0
  169. package/pack/.claude/settings.json +2 -1
  170. package/pack/.claude/skills/devrites-audit/SKILL.md +40 -61
  171. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +24 -13
  172. package/pack/.claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  173. package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  174. package/pack/.claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  175. package/pack/.claude/skills/devrites-doubt/SKILL.md +25 -17
  176. package/pack/.claude/skills/devrites-interview/SKILL.md +53 -52
  177. package/pack/.claude/skills/devrites-lib/SKILL.md +12 -9
  178. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +4 -2
  179. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  180. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +8 -1
  181. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +38 -55
  182. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  183. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +200 -190
  184. package/pack/.claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  185. package/pack/.claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  186. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
  187. package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  188. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  189. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +23 -18
  190. package/pack/.claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  191. package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  192. package/pack/.claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  193. package/pack/.claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
  194. package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
  195. package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +0 -9
  196. package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +1 -3
  197. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +17 -7
  198. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +1 -27
  199. package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
  200. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +40 -10
  201. package/pack/.claude/skills/devrites-source-driven/SKILL.md +23 -22
  202. package/pack/.claude/skills/rite/SKILL.md +10 -6
  203. package/pack/.claude/skills/rite/reference/menu.md +13 -7
  204. package/pack/.claude/skills/rite-adopt/SKILL.md +33 -37
  205. package/pack/.claude/skills/rite-autocomplete/SKILL.md +33 -28
  206. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +21 -21
  207. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  208. package/pack/.claude/skills/rite-build/SKILL.md +35 -30
  209. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +41 -39
  210. package/pack/.claude/skills/rite-build/reference/evidence-standard.md +10 -0
  211. package/pack/.claude/skills/rite-build/reference/forge.md +186 -156
  212. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
  213. package/pack/.claude/skills/rite-build/reference/phase-contract.md +96 -175
  214. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +10 -4
  215. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +129 -134
  216. package/pack/.claude/skills/rite-clarify/SKILL.md +90 -0
  217. package/pack/.claude/skills/rite-clarify/reference/decision-coverage.md +61 -0
  218. package/pack/.claude/skills/rite-converge/SKILL.md +35 -25
  219. package/pack/.claude/skills/rite-define/SKILL.md +58 -32
  220. package/pack/.claude/skills/rite-define/reference/gates.md +16 -15
  221. package/pack/.claude/skills/rite-define/reference/plan-template.md +18 -8
  222. package/pack/.claude/skills/rite-frame/reference/failure-modes.md +22 -24
  223. package/pack/.claude/skills/rite-plan/SKILL.md +47 -16
  224. package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +4 -0
  225. package/pack/.claude/skills/rite-polish/SKILL.md +27 -23
  226. package/pack/.claude/skills/rite-prototype/SKILL.md +25 -26
  227. package/pack/.claude/skills/rite-prove/SKILL.md +27 -17
  228. package/pack/.claude/skills/rite-resolve/SKILL.md +12 -11
  229. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
  230. package/pack/.claude/skills/rite-review/SKILL.md +37 -28
  231. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +27 -92
  232. package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  233. package/pack/.claude/skills/rite-ship/reference/design-memory.md +31 -36
  234. package/pack/.claude/skills/rite-spec/SKILL.md +84 -135
  235. package/pack/.claude/skills/rite-spec/reference/investigation.md +37 -33
  236. package/pack/.claude/skills/rite-spec/reference/question-protocol.md +6 -2
  237. package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +25 -24
  238. package/pack/.claude/skills/rite-spec/reference/spec-template.md +9 -2
  239. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +13 -3
  240. package/pack/.claude/skills/rite-temper/SKILL.md +62 -47
  241. package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +24 -22
  242. package/pack/.claude/skills/rite-upgrade/SKILL.md +125 -0
  243. package/pack/.claude/skills/rite-vet/SKILL.md +114 -113
  244. package/pack/.claude/skills/rite-vet/reference/artifacts.md +50 -9
  245. package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -37
  246. package/pack/generated/claude/agents/devrites-code-reviewer.md +62 -48
  247. package/pack/generated/claude/agents/devrites-devex-reviewer.md +69 -53
  248. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +29 -22
  249. package/pack/generated/claude/agents/devrites-evidence-scout.md +69 -0
  250. package/pack/generated/claude/agents/devrites-forge-judge.md +74 -61
  251. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +48 -39
  252. package/pack/generated/claude/agents/devrites-performance-reviewer.md +49 -40
  253. package/pack/generated/claude/agents/devrites-plan-drafter.md +73 -0
  254. package/pack/generated/claude/agents/devrites-plan-reviewer.md +80 -47
  255. package/pack/generated/claude/agents/devrites-proof-runner.md +74 -0
  256. package/pack/generated/claude/agents/devrites-retrospector.md +48 -45
  257. package/pack/generated/claude/agents/devrites-security-auditor.md +46 -36
  258. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +50 -46
  259. package/pack/generated/claude/agents/devrites-slice-wright.md +153 -165
  260. package/pack/generated/claude/agents/devrites-spec-reviewer.md +34 -32
  261. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +63 -34
  262. package/pack/generated/claude/agents/devrites-test-analyst.md +40 -30
  263. package/pack/generated/claude/agents/devrites-upgrade-planner.md +100 -0
  264. package/pack/generated/claude/settings.json +2 -1
  265. package/pack/generated/claude/skills/devrites-audit/SKILL.md +40 -61
  266. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +24 -13
  267. package/pack/generated/claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  268. package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  269. package/pack/generated/claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  270. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +25 -17
  271. package/pack/generated/claude/skills/devrites-interview/SKILL.md +53 -52
  272. package/pack/generated/claude/skills/devrites-lib/SKILL.md +12 -9
  273. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +4 -2
  274. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  275. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +8 -1
  276. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +38 -55
  277. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  278. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +200 -190
  279. package/pack/generated/claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  280. package/pack/generated/claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  281. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
  282. package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  283. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  284. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +23 -18
  285. package/pack/generated/claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  286. package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  287. package/pack/generated/claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  288. package/pack/generated/claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
  289. package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
  290. package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +0 -9
  291. package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +1 -3
  292. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +17 -7
  293. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +1 -27
  294. package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
  295. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +40 -10
  296. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +23 -22
  297. package/pack/generated/claude/skills/rite/SKILL.md +10 -6
  298. package/pack/generated/claude/skills/rite/reference/menu.md +13 -7
  299. package/pack/generated/claude/skills/rite-adopt/SKILL.md +33 -37
  300. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +33 -28
  301. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +21 -21
  302. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  303. package/pack/generated/claude/skills/rite-build/SKILL.md +35 -30
  304. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +41 -39
  305. package/pack/generated/claude/skills/rite-build/reference/evidence-standard.md +10 -0
  306. package/pack/generated/claude/skills/rite-build/reference/forge.md +186 -156
  307. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
  308. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +96 -175
  309. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +10 -4
  310. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +129 -134
  311. package/pack/generated/claude/skills/rite-clarify/SKILL.md +90 -0
  312. package/pack/generated/claude/skills/rite-clarify/reference/decision-coverage.md +61 -0
  313. package/pack/generated/claude/skills/rite-converge/SKILL.md +35 -25
  314. package/pack/generated/claude/skills/rite-define/SKILL.md +58 -32
  315. package/pack/generated/claude/skills/rite-define/reference/gates.md +16 -15
  316. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +18 -8
  317. package/pack/generated/claude/skills/rite-frame/reference/failure-modes.md +22 -24
  318. package/pack/generated/claude/skills/rite-plan/SKILL.md +47 -16
  319. package/pack/generated/claude/skills/rite-plan/reference/task-breakdown.md +4 -0
  320. package/pack/generated/claude/skills/rite-polish/SKILL.md +27 -23
  321. package/pack/generated/claude/skills/rite-prototype/SKILL.md +25 -26
  322. package/pack/generated/claude/skills/rite-prove/SKILL.md +27 -17
  323. package/pack/generated/claude/skills/rite-resolve/SKILL.md +12 -11
  324. package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
  325. package/pack/generated/claude/skills/rite-review/SKILL.md +37 -28
  326. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +27 -92
  327. package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  328. package/pack/generated/claude/skills/rite-ship/reference/design-memory.md +31 -36
  329. package/pack/generated/claude/skills/rite-spec/SKILL.md +84 -135
  330. package/pack/generated/claude/skills/rite-spec/reference/investigation.md +37 -33
  331. package/pack/generated/claude/skills/rite-spec/reference/question-protocol.md +6 -2
  332. package/pack/generated/claude/skills/rite-spec/reference/spec-checklists.md +25 -24
  333. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +9 -2
  334. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +13 -3
  335. package/pack/generated/claude/skills/rite-temper/SKILL.md +62 -47
  336. package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +24 -22
  337. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +125 -0
  338. package/pack/generated/claude/skills/rite-vet/SKILL.md +114 -113
  339. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +50 -9
  340. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -37
  341. package/pack/generated/codex/AGENTS.md +5 -2
  342. package/pack/generated/codex/agents/devrites-code-reviewer.toml +69 -47
  343. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +75 -51
  344. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +35 -20
  345. package/pack/generated/codex/agents/devrites-evidence-scout.toml +75 -0
  346. package/pack/generated/codex/agents/devrites-forge-judge.toml +80 -59
  347. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +54 -37
  348. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +55 -38
  349. package/pack/generated/codex/agents/devrites-plan-drafter.toml +79 -0
  350. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +82 -47
  351. package/pack/generated/codex/agents/devrites-proof-runner.toml +80 -0
  352. package/pack/generated/codex/agents/devrites-retrospector.toml +54 -43
  353. package/pack/generated/codex/agents/devrites-security-auditor.toml +53 -35
  354. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +56 -44
  355. package/pack/generated/codex/agents/devrites-slice-wright.toml +159 -163
  356. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +40 -30
  357. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +65 -34
  358. package/pack/generated/codex/agents/devrites-test-analyst.toml +46 -28
  359. package/pack/generated/codex/agents/devrites-upgrade-planner.toml +106 -0
  360. package/pack/generated/codex/hooks.json +4 -14
  361. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +7 -3
  362. package/pack/generated/codex/skills/devrites-audit/SKILL.md +47 -64
  363. package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +7 -3
  364. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +31 -16
  365. package/pack/generated/codex/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
  366. package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
  367. package/pack/generated/codex/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
  368. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +32 -20
  369. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +7 -3
  370. package/pack/generated/codex/skills/devrites-interview/SKILL.md +60 -55
  371. package/pack/generated/codex/skills/devrites-lib/SKILL.md +19 -12
  372. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +4 -2
  373. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
  374. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +8 -1
  375. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +38 -55
  376. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
  377. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +200 -190
  378. package/pack/generated/codex/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
  379. package/pack/generated/codex/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
  380. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +16 -52
  381. package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +2 -10
  382. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
  383. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +23 -18
  384. package/pack/generated/codex/skills/devrites-lib/reference/standards/deprecation.md +17 -57
  385. package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
  386. package/pack/generated/codex/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
  387. package/pack/generated/codex/skills/devrites-lib/reference/standards/hooks.md +3 -14
  388. package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +9 -25
  389. package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +0 -9
  390. package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +1 -3
  391. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +17 -7
  392. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +1 -27
  393. package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +46 -50
  394. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +40 -10
  395. package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +7 -3
  396. package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +7 -3
  397. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +29 -24
  398. package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +7 -3
  399. package/pack/generated/codex/skills/rite/SKILL.md +21 -13
  400. package/pack/generated/codex/skills/rite/reference/menu.md +13 -7
  401. package/pack/generated/codex/skills/rite-adopt/SKILL.md +40 -40
  402. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +40 -31
  403. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +21 -21
  404. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
  405. package/pack/generated/codex/skills/rite-build/SKILL.md +42 -33
  406. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +41 -39
  407. package/pack/generated/codex/skills/rite-build/reference/evidence-standard.md +10 -0
  408. package/pack/generated/codex/skills/rite-build/reference/forge.md +186 -156
  409. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +4 -3
  410. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +96 -175
  411. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +10 -4
  412. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +129 -134
  413. package/pack/generated/codex/skills/rite-clarify/SKILL.md +106 -0
  414. package/pack/generated/codex/skills/rite-clarify/reference/decision-coverage.md +61 -0
  415. package/pack/generated/codex/skills/rite-converge/SKILL.md +42 -28
  416. package/pack/generated/codex/skills/rite-customize/SKILL.md +7 -3
  417. package/pack/generated/codex/skills/rite-define/SKILL.md +65 -35
  418. package/pack/generated/codex/skills/rite-define/reference/gates.md +16 -15
  419. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +18 -8
  420. package/pack/generated/codex/skills/rite-doctor/SKILL.md +7 -3
  421. package/pack/generated/codex/skills/rite-dogfood/SKILL.md +7 -3
  422. package/pack/generated/codex/skills/rite-explain/SKILL.md +7 -3
  423. package/pack/generated/codex/skills/rite-frame/SKILL.md +7 -3
  424. package/pack/generated/codex/skills/rite-frame/reference/failure-modes.md +22 -24
  425. package/pack/generated/codex/skills/rite-handoff/SKILL.md +7 -3
  426. package/pack/generated/codex/skills/rite-learn/SKILL.md +7 -3
  427. package/pack/generated/codex/skills/rite-plan/SKILL.md +54 -19
  428. package/pack/generated/codex/skills/rite-plan/reference/task-breakdown.md +5 -1
  429. package/pack/generated/codex/skills/rite-polish/SKILL.md +34 -26
  430. package/pack/generated/codex/skills/rite-pov/SKILL.md +7 -3
  431. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -3
  432. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +7 -3
  433. package/pack/generated/codex/skills/rite-prototype/SKILL.md +32 -29
  434. package/pack/generated/codex/skills/rite-prove/SKILL.md +34 -20
  435. package/pack/generated/codex/skills/rite-quick/SKILL.md +7 -3
  436. package/pack/generated/codex/skills/rite-resolve/SKILL.md +19 -14
  437. package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +3 -0
  438. package/pack/generated/codex/skills/rite-review/SKILL.md +44 -31
  439. package/pack/generated/codex/skills/rite-seal/SKILL.md +7 -3
  440. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +27 -92
  441. package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +13 -0
  442. package/pack/generated/codex/skills/rite-ship/SKILL.md +7 -3
  443. package/pack/generated/codex/skills/rite-ship/reference/design-memory.md +31 -36
  444. package/pack/generated/codex/skills/rite-spec/SKILL.md +91 -138
  445. package/pack/generated/codex/skills/rite-spec/reference/investigation.md +37 -33
  446. package/pack/generated/codex/skills/rite-spec/reference/question-protocol.md +6 -2
  447. package/pack/generated/codex/skills/rite-spec/reference/spec-checklists.md +25 -24
  448. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +9 -2
  449. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +13 -3
  450. package/pack/generated/codex/skills/rite-status/SKILL.md +7 -3
  451. package/pack/generated/codex/skills/rite-temper/SKILL.md +69 -50
  452. package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +24 -22
  453. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +141 -0
  454. package/pack/generated/codex/skills/rite-upgrade/agents/openai.yaml +2 -0
  455. package/pack/generated/codex/skills/rite-vet/SKILL.md +121 -116
  456. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +50 -9
  457. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +40 -38
  458. package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +7 -3
  459. package/package.json +1 -1
  460. package/scripts/build-release-tarball.sh +32 -15
  461. package/scripts/check-authority-drift.py +125 -0
  462. package/scripts/check-instruction-size-baseline.mjs +19 -11
  463. package/scripts/check-invocation-integrity.py +2 -0
  464. package/scripts/codex-generate.sh +69 -33
  465. package/scripts/grade-feature.sh +121 -40
  466. package/scripts/live-hosts/agent-result.schema.json +230 -0
  467. package/scripts/live-hosts/claude.sh +87 -0
  468. package/scripts/live-hosts/codex.sh +81 -0
  469. package/scripts/live-hosts/common.sh +113 -0
  470. package/scripts/live-hosts/fake-host.py +264 -0
  471. package/scripts/live-hosts/host-transport.py +287 -0
  472. package/scripts/release-check.sh +5 -1
  473. package/scripts/run-agent-contract-evals.py +1380 -0
  474. package/scripts/run-behavioral-evals.sh +24 -30
  475. package/scripts/run-evals.sh +1 -5
  476. package/scripts/run-live-behavioral-evals.py +1274 -144
  477. package/scripts/run-outcome-evals.sh +445 -88
  478. package/scripts/run-tests.mjs +30 -2
  479. package/scripts/skills-inventory.mjs +1 -1
  480. package/scripts/validate-workflow-security.py +39 -20
  481. package/scripts/validate-workspace-schema.py +362 -10
  482. package/scripts/validate.sh +21 -15
  483. package/engine/testdata/golden/TestParityWrightScope/devrites-edit-allowed.golden +0 -1
  484. /package/engine/testdata/golden/{TestParityReconcile/check-no-claimed.golden → TestParityBuildReadiness/arg=clarifyopen.golden} +0 -0
@@ -0,0 +1,940 @@
1
+ package forge
2
+
3
+ import (
4
+ "bytes"
5
+ "encoding/hex"
6
+ "encoding/json"
7
+ "errors"
8
+ "flag"
9
+ "fmt"
10
+ "io"
11
+ "os"
12
+ "path/filepath"
13
+ "sort"
14
+ "strconv"
15
+ "strings"
16
+ )
17
+
18
+ type PlanOptions struct {
19
+ SliceID string
20
+ Slug string
21
+ Strategies map[CandidateID]string
22
+ AcceptanceHash string
23
+ TestPlanHash string
24
+ }
25
+
26
+ type PlanResult struct {
27
+ Status string `json:"status"`
28
+ Mode string `json:"mode,omitempty"`
29
+ Reason string `json:"reason,omitempty"`
30
+ Manifest *Manifest `json:"manifest,omitempty"`
31
+ }
32
+
33
+ const WorkerBindingManifestEnvV1 = "manifest-env-v1"
34
+
35
+ type ProcessTokenResult struct {
36
+ Status string `json:"status"`
37
+ PID int `json:"pid"`
38
+ ProcessStart string `json:"process_start"`
39
+ }
40
+
41
+ type RecordOptions struct {
42
+ RunID string
43
+ Candidate CandidateID
44
+ State CandidateState
45
+ WorkerID string
46
+ PID int
47
+ ProcessStart string
48
+ }
49
+
50
+ type ReapResult struct {
51
+ RunID string `json:"run_id"`
52
+ Removed []CandidateID `json:"removed,omitempty"`
53
+ Preserved map[string]string `json:"preserved,omitempty"`
54
+ Error string `json:"error,omitempty"`
55
+ }
56
+
57
+ // Run is the Forge command entry point. It never accepts a worktree path or
58
+ // branch name; every destructive target is loaded from the unique manifest.
59
+ func Run(root string, args []string, stdout, stderr io.Writer) int {
60
+ if len(args) == 0 {
61
+ fmt.Fprintln(stderr, usage)
62
+ return 2
63
+ }
64
+ var (
65
+ result any
66
+ err error
67
+ )
68
+ switch args[0] {
69
+ case "plan":
70
+ result, err = runPlan(root, args[1:])
71
+ case "process-token":
72
+ if len(args) != 2 {
73
+ err = errors.New("usage: forge process-token <pid>")
74
+ break
75
+ }
76
+ result, err = runProcessToken(args[1])
77
+ case "record":
78
+ result, err = runRecord(root, args[1:])
79
+ case "extract":
80
+ if len(args) != 3 {
81
+ err = errors.New("usage: forge extract <run-id> <A|B|C>")
82
+ break
83
+ }
84
+ result, err = Extract(root, args[1], CandidateID(args[2]))
85
+ case "merge":
86
+ if len(args) != 3 {
87
+ err = errors.New("usage: forge merge <run-id> <A|B|C>")
88
+ break
89
+ }
90
+ result, err = Merge(root, args[1], CandidateID(args[2]))
91
+ case "cleanup":
92
+ if len(args) != 2 {
93
+ err = errors.New("usage: forge cleanup <run-id>")
94
+ break
95
+ }
96
+ result, err = Cleanup(root, args[1])
97
+ case "reap":
98
+ if len(args) > 2 {
99
+ err = errors.New("usage: forge reap [slug]")
100
+ break
101
+ }
102
+ slug := ""
103
+ if len(args) == 2 {
104
+ slug = args[1]
105
+ }
106
+ result, err = Reap(root, slug)
107
+ default:
108
+ fmt.Fprintln(stderr, usage)
109
+ return 2
110
+ }
111
+ if err != nil {
112
+ fmt.Fprintf(stderr, "%v\n", err)
113
+ return 1
114
+ }
115
+ raw, marshalErr := json.MarshalIndent(result, "", " ")
116
+ if marshalErr != nil {
117
+ fmt.Fprintf(stderr, "forge: encode result: %v\n", marshalErr)
118
+ return 1
119
+ }
120
+ fmt.Fprintln(stdout, string(raw))
121
+ return 0
122
+ }
123
+
124
+ const usage = "usage: forge plan|process-token|record|extract|merge|cleanup|reap ..."
125
+
126
+ func runPlan(root string, args []string) (PlanResult, error) {
127
+ var (
128
+ strategies strategyFlags
129
+ slug, acceptance, testPlan string
130
+ workerBinding string
131
+ workerBindingSet bool
132
+ positional []string
133
+ )
134
+ for i := 0; i < len(args); i++ {
135
+ arg := args[i]
136
+ switch {
137
+ case arg == "--strategy" || arg == "--slug" || arg == "--acceptance-hash" || arg == "--test-plan-hash" || arg == "--worker-binding":
138
+ if i+1 == len(args) {
139
+ return PlanResult{}, fmt.Errorf("forge: %s requires a value", arg)
140
+ }
141
+ i++
142
+ switch arg {
143
+ case "--strategy":
144
+ if err := strategies.Set(args[i]); err != nil {
145
+ return PlanResult{}, err
146
+ }
147
+ case "--slug":
148
+ slug = args[i]
149
+ case "--acceptance-hash":
150
+ acceptance = args[i]
151
+ case "--test-plan-hash":
152
+ testPlan = args[i]
153
+ case "--worker-binding":
154
+ if workerBindingSet {
155
+ return PlanResult{}, errors.New("forge: worker binding may be declared only once")
156
+ }
157
+ workerBinding, workerBindingSet = args[i], true
158
+ }
159
+ case strings.HasPrefix(arg, "--strategy="):
160
+ if err := strategies.Set(strings.TrimPrefix(arg, "--strategy=")); err != nil {
161
+ return PlanResult{}, err
162
+ }
163
+ case strings.HasPrefix(arg, "--slug="):
164
+ slug = strings.TrimPrefix(arg, "--slug=")
165
+ case strings.HasPrefix(arg, "--acceptance-hash="):
166
+ acceptance = strings.TrimPrefix(arg, "--acceptance-hash=")
167
+ case strings.HasPrefix(arg, "--test-plan-hash="):
168
+ testPlan = strings.TrimPrefix(arg, "--test-plan-hash=")
169
+ case strings.HasPrefix(arg, "--worker-binding="):
170
+ if workerBindingSet {
171
+ return PlanResult{}, errors.New("forge: worker binding may be declared only once")
172
+ }
173
+ workerBinding, workerBindingSet = strings.TrimPrefix(arg, "--worker-binding="), true
174
+ case strings.HasPrefix(arg, "-"):
175
+ return PlanResult{}, fmt.Errorf("forge: unknown plan flag %s", arg)
176
+ default:
177
+ positional = append(positional, arg)
178
+ }
179
+ }
180
+ if len(positional) < 1 || len(positional) > 2 {
181
+ return PlanResult{}, errors.New("usage: forge plan <slice-id> [slug] --strategy A=text --strategy B=text --acceptance-hash <sha256> --test-plan-hash <sha256>")
182
+ }
183
+ if slug == "" && len(positional) == 2 {
184
+ slug = positional[1]
185
+ }
186
+ switch {
187
+ case !workerBindingSet:
188
+ return degraded("supported worker binding was not declared"), nil
189
+ case workerBinding != WorkerBindingManifestEnvV1:
190
+ return PlanResult{}, fmt.Errorf("forge: unsupported worker binding %q (want %q)", workerBinding, WorkerBindingManifestEnvV1)
191
+ }
192
+ return Plan(root, PlanOptions{
193
+ SliceID: positional[0],
194
+ Slug: slug,
195
+ Strategies: strategies.values,
196
+ AcceptanceHash: acceptance,
197
+ TestPlanHash: testPlan,
198
+ })
199
+ }
200
+
201
+ func runProcessToken(value string) (ProcessTokenResult, error) {
202
+ pid, err := parsePID(value)
203
+ if err != nil {
204
+ return ProcessTokenResult{}, err
205
+ }
206
+ token, err := ProcessStartToken(pid)
207
+ if err != nil {
208
+ return ProcessTokenResult{}, err
209
+ }
210
+ return ProcessTokenResult{Status: "ok", PID: pid, ProcessStart: token}, nil
211
+ }
212
+
213
+ func runRecord(root string, args []string) (*Manifest, error) {
214
+ if len(args) < 3 {
215
+ return nil, errors.New("usage: forge record <run-id> <candidate|winner|verification> <state> [flags]")
216
+ }
217
+ runID, subject, state := args[0], args[1], args[2]
218
+ fs := flag.NewFlagSet("forge record", flag.ContinueOnError)
219
+ fs.SetOutput(io.Discard)
220
+ var workerID, start string
221
+ var pid int
222
+ fs.StringVar(&workerID, "worker-id", "", "worker or recorder identity")
223
+ fs.IntVar(&pid, "pid", 0, "worker PID")
224
+ fs.StringVar(&start, "process-start", "", "worker process start token")
225
+ if err := fs.Parse(args[3:]); err != nil || fs.NArg() != 0 {
226
+ return nil, errors.New("forge: invalid record flags")
227
+ }
228
+ switch subject {
229
+ case "winner":
230
+ return RecordWinner(root, runID, CandidateID(state), workerID)
231
+ case "verification":
232
+ switch state {
233
+ case "verified":
234
+ return RecordVerification(root, runID, true, workerID)
235
+ case "failed":
236
+ return RecordVerification(root, runID, false, workerID)
237
+ default:
238
+ return nil, fmt.Errorf("forge: invalid verification state %q", state)
239
+ }
240
+ default:
241
+ if state == "started" {
242
+ state = string(StateRunning)
243
+ }
244
+ return Record(root, RecordOptions{
245
+ RunID: runID,
246
+ Candidate: CandidateID(subject),
247
+ State: CandidateState(state),
248
+ WorkerID: workerID,
249
+ PID: pid,
250
+ ProcessStart: start,
251
+ })
252
+ }
253
+ }
254
+
255
+ type strategyFlags struct {
256
+ values map[CandidateID]string
257
+ }
258
+
259
+ func (s *strategyFlags) String() string { return "" }
260
+
261
+ func (s *strategyFlags) Set(value string) error {
262
+ idText, text, ok := strings.Cut(value, "=")
263
+ id := CandidateID(idText)
264
+ if !ok || !validCandidateID(id) || strings.TrimSpace(text) == "" {
265
+ return fmt.Errorf("strategy must be A=text, B=text, or C=text")
266
+ }
267
+ if s.values == nil {
268
+ s.values = map[CandidateID]string{}
269
+ }
270
+ if _, exists := s.values[id]; exists {
271
+ return fmt.Errorf("duplicate strategy %s", id)
272
+ }
273
+ s.values[id] = strings.TrimSpace(text)
274
+ return nil
275
+ }
276
+
277
+ func Plan(root string, opts PlanOptions) (PlanResult, error) {
278
+ if strings.TrimSpace(opts.SliceID) == "" {
279
+ return PlanResult{}, errors.New("forge: slice ID is required")
280
+ }
281
+ if !safeSlug.MatchString(opts.Slug) {
282
+ return PlanResult{}, fmt.Errorf("forge: invalid feature slug %q", opts.Slug)
283
+ }
284
+ if len(opts.Strategies) < 2 || len(opts.Strategies) > 3 {
285
+ return PlanResult{}, errors.New("forge: exactly 2 or 3 candidate strategies are required")
286
+ }
287
+ for id, strategy := range opts.Strategies {
288
+ if !validCandidateID(id) || strings.TrimSpace(strategy) == "" {
289
+ return PlanResult{}, fmt.Errorf("forge: invalid strategy %q", id)
290
+ }
291
+ }
292
+ if !validSHA256(opts.AcceptanceHash) || !validSHA256(opts.TestPlanHash) {
293
+ return PlanResult{}, errors.New("forge: acceptance and test-plan SHA-256 scorecard hashes are required")
294
+ }
295
+ physical, err := physicalExisting(root)
296
+ if err != nil {
297
+ return PlanResult{}, err
298
+ }
299
+ id, err := inspectRepo(physical)
300
+ if err != nil {
301
+ return degraded(err.Error()), nil
302
+ }
303
+ if super, _ := gitText(physical, "rev-parse", "--show-superproject-working-tree"); super != "" {
304
+ return degraded("submodule checkout is not safe for parallel Forge"), nil
305
+ }
306
+ if inProgress(id.commonDir) {
307
+ return degraded("repository has an in-progress Git operation"), nil
308
+ }
309
+ existingManifests, err := validatedManifestPaths(physical)
310
+ if err != nil {
311
+ return PlanResult{}, err
312
+ }
313
+ if dirty, err := dirtyStatus(physical, false, existingManifests); err != nil {
314
+ return PlanResult{}, err
315
+ } else if dirty {
316
+ return degraded("primary index or worktree is dirty"), nil
317
+ }
318
+ if _, err := ProcessStartToken(os.Getpid()); err != nil {
319
+ return degraded("worker liveness cannot be proven on this host"), nil
320
+ }
321
+ runID, nonce, err := newIdentity()
322
+ if err != nil {
323
+ return PlanResult{}, err
324
+ }
325
+ staging := forgeRoot(physical, runID)
326
+ if err := validateStagingParent(staging); err != nil {
327
+ return degraded(err.Error()), nil
328
+ }
329
+ registrations, err := registeredWorktrees(physical)
330
+ if err != nil {
331
+ return PlanResult{}, err
332
+ }
333
+ for path := range registrations {
334
+ if path == staging || strings.HasPrefix(path, staging+string(filepath.Separator)) {
335
+ return degraded("derived staging path collides with a registered worktree"), nil
336
+ }
337
+ }
338
+ if _, err := os.Lstat(staging); err == nil {
339
+ return degraded("derived staging path already exists"), nil
340
+ } else if !errors.Is(err, os.ErrNotExist) {
341
+ return PlanResult{}, fmt.Errorf("forge: inspect staging path: %w", err)
342
+ }
343
+ manifestPath, err := ManifestPath(physical, opts.Slug, runID)
344
+ if err != nil {
345
+ return PlanResult{}, err
346
+ }
347
+ if err := validateManifestParentBeforeCreate(physical, manifestPath); err != nil {
348
+ return degraded(err.Error()), nil
349
+ }
350
+ ids := make([]CandidateID, 0, len(opts.Strategies))
351
+ for candidateID := range opts.Strategies {
352
+ ids = append(ids, candidateID)
353
+ }
354
+ sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
355
+ for i, candidateID := range ids {
356
+ want := CandidateID(rune('A' + i))
357
+ if candidateID != want {
358
+ return PlanResult{}, errors.New("forge: candidate IDs must be contiguous from A")
359
+ }
360
+ if _, err := gitText(physical, "show-ref", "--verify", "--quiet", "refs/heads/"+candidateBranch(runID, candidateID)); err == nil {
361
+ return degraded("derived candidate branch already exists"), nil
362
+ }
363
+ }
364
+ createdAt := now()
365
+ m := &Manifest{
366
+ Schema: Schema,
367
+ RunID: runID,
368
+ CreationNonce: nonce,
369
+ CreatedAt: createdAt,
370
+ FeatureSlug: opts.Slug,
371
+ SliceID: strings.TrimSpace(opts.SliceID),
372
+ AcceptanceHash: strings.ToLower(opts.AcceptanceHash),
373
+ TestPlanHash: strings.ToLower(opts.TestPlanHash),
374
+ PrimaryRoot: physical,
375
+ GitCommonDir: id.commonDir,
376
+ ForgeRoot: staging,
377
+ Primary: Primary{
378
+ Branch: id.branch,
379
+ BaseCommit: id.head,
380
+ BaselineTree: id.tree,
381
+ BaselineSHA256: id.treeHash,
382
+ },
383
+ }
384
+ for _, candidateID := range ids {
385
+ m.Candidates = append(m.Candidates, Candidate{
386
+ ID: candidateID,
387
+ Strategy: strings.TrimSpace(opts.Strategies[candidateID]),
388
+ Worktree: filepath.Join(staging, "candidate-"+strings.ToLower(string(candidateID))),
389
+ Branch: candidateBranch(runID, candidateID),
390
+ InitialBase: id.head,
391
+ State: StatePlanned,
392
+ LastTransition: createdAt,
393
+ })
394
+ }
395
+ // The manifest owns every derived path before the first Git side effect.
396
+ if err := save(manifestPath, m); err != nil {
397
+ return PlanResult{}, err
398
+ }
399
+ for i := range m.Candidates {
400
+ c := &m.Candidates[i]
401
+ if _, err := gitBytes(physical, nil, "worktree", "add", "-b", c.Branch, c.Worktree, c.InitialBase); err != nil {
402
+ c.Preservation = err.Error()
403
+ c.LastTransition = now()
404
+ _ = save(manifestPath, m)
405
+ return PlanResult{}, fmt.Errorf("forge: create candidate %s: %w", c.ID, err)
406
+ }
407
+ c.State = StateCreated
408
+ c.LastTransition = now()
409
+ if err := save(manifestPath, m); err != nil {
410
+ return PlanResult{}, err
411
+ }
412
+ }
413
+ return PlanResult{Status: "planned", Manifest: m}, nil
414
+ }
415
+
416
+ func Record(root string, opts RecordOptions) (*Manifest, error) {
417
+ m, path, err := Load(root, opts.RunID)
418
+ if err != nil {
419
+ return nil, err
420
+ }
421
+ c, err := m.Candidate(opts.Candidate)
422
+ if err != nil {
423
+ return nil, err
424
+ }
425
+ if strings.TrimSpace(opts.WorkerID) == "" {
426
+ return nil, errors.New("forge: worker ID is required")
427
+ }
428
+ switch opts.State {
429
+ case StateRunning:
430
+ if c.State == StateRunning && c.Worker.ID == opts.WorkerID && c.Worker.PID == opts.PID && c.Worker.ProcessStart == opts.ProcessStart {
431
+ return m, nil
432
+ }
433
+ if c.State != StateCreated {
434
+ return nil, fmt.Errorf("forge: candidate %s cannot transition from %s to running", c.ID, c.State)
435
+ }
436
+ token, err := ProcessStartToken(opts.PID)
437
+ if err != nil || token != opts.ProcessStart {
438
+ return nil, errors.New("forge: worker liveness token is not currently provable")
439
+ }
440
+ c.Worker = Worker{ID: opts.WorkerID, PID: opts.PID, ProcessStart: opts.ProcessStart, StartedAt: now()}
441
+ c.State = StateRunning
442
+ case StateFinished, StateFailed:
443
+ if (c.State == opts.State || c.State == StateExtracted || c.State == StateMerged || c.State == StateCleaned) &&
444
+ c.Worker.ID == opts.WorkerID {
445
+ return m, nil
446
+ }
447
+ if c.State != StateRunning || c.Worker.ID != opts.WorkerID {
448
+ return nil, fmt.Errorf("forge: candidate %s terminal transition does not match its live owner", c.ID)
449
+ }
450
+ c.State = opts.State
451
+ c.Worker.FinishedAt = now()
452
+ c.Worker.Result = string(opts.State)
453
+ default:
454
+ return nil, fmt.Errorf("forge: unsupported candidate state %q", opts.State)
455
+ }
456
+ c.LastTransition = now()
457
+ if err := save(path, m); err != nil {
458
+ return nil, err
459
+ }
460
+ return m, nil
461
+ }
462
+
463
+ func RecordWinner(root, runID string, winner CandidateID, recorder string) (*Manifest, error) {
464
+ m, path, err := Load(root, runID)
465
+ if err != nil {
466
+ return nil, err
467
+ }
468
+ if strings.TrimSpace(recorder) == "" || !validCandidateID(winner) {
469
+ return nil, errors.New("forge: valid winner and recorder identity are required")
470
+ }
471
+ if _, err := m.Candidate(winner); err != nil {
472
+ return nil, err
473
+ }
474
+ if m.Winner.Candidate != "" {
475
+ if m.Winner.Candidate == winner && m.Winner.RecordedBy == recorder {
476
+ return m, nil
477
+ }
478
+ return nil, errors.New("forge: winner is already recorded and immutable")
479
+ }
480
+ m.Winner = Winner{Candidate: winner, RecordedBy: recorder, RecordedAt: now()}
481
+ if err := save(path, m); err != nil {
482
+ return nil, err
483
+ }
484
+ return m, nil
485
+ }
486
+
487
+ func RecordVerification(root, runID string, passed bool, recorder string) (*Manifest, error) {
488
+ m, path, err := Load(root, runID)
489
+ if err != nil {
490
+ return nil, err
491
+ }
492
+ if strings.TrimSpace(recorder) == "" || m.Merge.State != "landed" {
493
+ return nil, errors.New("forge: landed merge and verifier identity are required")
494
+ }
495
+ state := "failed"
496
+ if passed {
497
+ state = "verified"
498
+ }
499
+ if m.Verification.State != "" {
500
+ if m.Verification.State == state && m.Verification.RecordedBy == recorder {
501
+ return m, nil
502
+ }
503
+ return nil, errors.New("forge: verification result is already recorded and immutable")
504
+ }
505
+ m.Verification = VerificationState{State: state, RecordedBy: recorder, RecordedAt: now()}
506
+ if err := save(path, m); err != nil {
507
+ return nil, err
508
+ }
509
+ return m, nil
510
+ }
511
+
512
+ func Extract(root, runID string, candidateID CandidateID) (*Manifest, error) {
513
+ m, path, err := Load(root, runID)
514
+ if err != nil {
515
+ return nil, err
516
+ }
517
+ c, err := m.Candidate(candidateID)
518
+ if err != nil {
519
+ return nil, err
520
+ }
521
+ if c.State == StateExtracted || c.State == StateMerged || c.State == StateCleaned {
522
+ return verifyExtracted(m, c, path)
523
+ }
524
+ if c.State != StateFinished {
525
+ return nil, fmt.Errorf("forge: candidate %s must be finished before extraction", c.ID)
526
+ }
527
+ if live, reason := workerLiveness(c.Worker); live != livenessDead {
528
+ return nil, fmt.Errorf("forge: candidate %s preserved: %s", c.ID, reason)
529
+ }
530
+ if err := validateCandidateRepo(m, c); err != nil {
531
+ return nil, err
532
+ }
533
+ head, err := gitText(c.Worktree, "rev-parse", "--verify", "HEAD^{commit}")
534
+ if err != nil {
535
+ return nil, err
536
+ }
537
+ if _, err := gitBytes(c.Worktree, nil, "merge-base", "--is-ancestor", c.InitialBase, head); err != nil {
538
+ return nil, fmt.Errorf("forge: candidate %s no longer descends from its initial base", c.ID)
539
+ }
540
+ if ignored, err := gitBytes(c.Worktree, nil, "ls-files", "--others", "--ignored", "--exclude-standard", "-z"); err != nil {
541
+ return nil, err
542
+ } else if len(ignored) != 0 {
543
+ return preserve(path, m, c, "ignored content is not representable in the candidate tree")
544
+ }
545
+ if _, err := gitBytes(c.Worktree, nil, "add", "-A", "--", "."); err != nil {
546
+ return nil, err
547
+ }
548
+ changed, err := gitBytes(c.Worktree, nil, "diff", "--cached", "--quiet", "--exit-code")
549
+ if err != nil {
550
+ _ = changed
551
+ if _, commitErr := gitBytes(c.Worktree, nil,
552
+ "-c", "user.name=DevRites Forge",
553
+ "-c", "user.email=forge@devrites.local",
554
+ "commit", "--no-gpg-sign", "--no-verify",
555
+ "-m", "devrites forge snapshot "+m.RunID+" "+string(c.ID)); commitErr != nil {
556
+ return nil, commitErr
557
+ }
558
+ }
559
+ c.Commit, err = gitText(c.Worktree, "rev-parse", "--verify", "HEAD^{commit}")
560
+ if err != nil {
561
+ return nil, err
562
+ }
563
+ c.Tree, err = gitText(c.Worktree, "rev-parse", "--verify", "HEAD^{tree}")
564
+ if err != nil {
565
+ return nil, err
566
+ }
567
+ if err := rejectPrivateOrGitlinkChanges(c); err != nil {
568
+ return preserve(path, m, c, err.Error())
569
+ }
570
+ delta, err := gitBytes(c.Worktree, nil, "diff", "--binary", "--full-index", "--no-renames", c.InitialBase, c.Commit, "--")
571
+ if err != nil {
572
+ return nil, err
573
+ }
574
+ c.DeltaSHA256 = sha256Hex(delta)
575
+ if dirty, err := dirtyStatus(c.Worktree, true, nil); err != nil {
576
+ return nil, err
577
+ } else if dirty {
578
+ return preserve(path, m, c, "candidate remains dirty after snapshot")
579
+ }
580
+ c.State = StateExtracted
581
+ c.Preservation = ""
582
+ c.LastTransition = now()
583
+ if err := save(path, m); err != nil {
584
+ return nil, err
585
+ }
586
+ return m, nil
587
+ }
588
+
589
+ func verifyExtracted(m *Manifest, c *Candidate, path string) (*Manifest, error) {
590
+ if c.Commit == "" || c.Tree == "" || c.DeltaSHA256 == "" {
591
+ return nil, fmt.Errorf("forge: candidate %s has incomplete extraction state", c.ID)
592
+ }
593
+ if c.State == StateCleaned {
594
+ return m, nil
595
+ }
596
+ if err := validateCandidateRepo(m, c); err != nil {
597
+ return nil, err
598
+ }
599
+ head, err := gitText(c.Worktree, "rev-parse", "HEAD^{commit}")
600
+ if err != nil || head != c.Commit {
601
+ return nil, fmt.Errorf("forge: candidate %s extracted commit drifted", c.ID)
602
+ }
603
+ delta, err := gitBytes(c.Worktree, nil, "diff", "--binary", "--full-index", "--no-renames", c.InitialBase, c.Commit, "--")
604
+ if err != nil || sha256Hex(delta) != c.DeltaSHA256 {
605
+ return nil, fmt.Errorf("forge: candidate %s extracted delta drifted", c.ID)
606
+ }
607
+ return m, nil
608
+ }
609
+
610
+ func Merge(root, runID string, winnerID CandidateID) (*Manifest, error) {
611
+ m, path, err := Load(root, runID)
612
+ if err != nil {
613
+ return nil, err
614
+ }
615
+ if m.Winner.Candidate == "" || m.Winner.Candidate != winnerID {
616
+ return nil, errors.New("forge: merge winner does not match the recorded judge winner")
617
+ }
618
+ winner, err := m.Candidate(winnerID)
619
+ if err != nil {
620
+ return nil, err
621
+ }
622
+ for i := range m.Candidates {
623
+ c := &m.Candidates[i]
624
+ if c.State != StateExtracted && c.State != StateMerged && c.State != StateCleaned {
625
+ return nil, fmt.Errorf("forge: candidate %s has not been extracted", c.ID)
626
+ }
627
+ }
628
+ id, err := inspectRepo(m.PrimaryRoot)
629
+ if err != nil {
630
+ return nil, err
631
+ }
632
+ if id.commonDir != m.GitCommonDir || id.branch != m.Primary.Branch {
633
+ return nil, errors.New("forge: primary repository identity drifted")
634
+ }
635
+ manifestFiles, err := validatedManifestPaths(m.PrimaryRoot)
636
+ if err != nil {
637
+ return nil, err
638
+ }
639
+ if dirty, err := dirtyStatus(m.PrimaryRoot, false, manifestFiles); err != nil {
640
+ return nil, err
641
+ } else if dirty {
642
+ return nil, errors.New("forge: primary index or worktree is dirty")
643
+ }
644
+ if id.head == winner.Commit && id.tree == winner.Tree {
645
+ m.Merge = MergeState{State: "landed", Commit: winner.Commit, Tree: winner.Tree, RecordedAt: now()}
646
+ winner.State = StateMerged
647
+ winner.LastTransition = now()
648
+ if err := save(path, m); err != nil {
649
+ return nil, err
650
+ }
651
+ return m, nil
652
+ }
653
+ if id.head != m.Primary.BaseCommit || id.tree != m.Primary.BaselineTree {
654
+ return nil, errors.New("forge: primary baseline changed before merge")
655
+ }
656
+ for i := range m.Candidates {
657
+ c := &m.Candidates[i]
658
+ if _, err := verifyExtracted(m, c, path); err != nil {
659
+ return nil, err
660
+ }
661
+ if live, reason := workerLiveness(c.Worker); live != livenessDead {
662
+ return nil, fmt.Errorf("forge: candidate %s preserved: %s", c.ID, reason)
663
+ }
664
+ }
665
+ branchHead, err := gitText(m.PrimaryRoot, "rev-parse", "--verify", "refs/heads/"+winner.Branch+"^{commit}")
666
+ if err != nil || branchHead != winner.Commit {
667
+ return nil, errors.New("forge: winner branch no longer pins the recorded commit")
668
+ }
669
+ delta, err := gitBytes(m.PrimaryRoot, nil, "diff", "--binary", "--full-index", "--no-renames", winner.InitialBase, winner.Commit, "--")
670
+ if err != nil || sha256Hex(delta) != winner.DeltaSHA256 {
671
+ return nil, errors.New("forge: winner delta hash mismatch")
672
+ }
673
+ if _, err := gitBytes(m.PrimaryRoot, delta, "apply", "--check", "--index", "--binary", "--whitespace=nowarn", "-"); err != nil {
674
+ return nil, fmt.Errorf("forge: merge conflict preflight failed; primary was not changed: %w", err)
675
+ }
676
+ m.Merge = MergeState{State: "preflight", Commit: winner.Commit, Tree: winner.Tree, RecordedAt: now()}
677
+ if err := save(path, m); err != nil {
678
+ return nil, err
679
+ }
680
+ if _, err := gitBytes(m.PrimaryRoot, nil, "merge", "--ff-only", "--no-edit", winner.Commit); err != nil {
681
+ return nil, fmt.Errorf("forge: fast-forward merge failed; inspect primary before retry: %w", err)
682
+ }
683
+ id, err = inspectRepo(m.PrimaryRoot)
684
+ if err != nil || id.head != winner.Commit || id.tree != winner.Tree {
685
+ return nil, errors.New("forge: merge result did not match recorded winner")
686
+ }
687
+ if dirty, err := dirtyStatus(m.PrimaryRoot, false, manifestFiles); err != nil || dirty {
688
+ return nil, errors.New("forge: primary is not clean after merge")
689
+ }
690
+ m.Merge = MergeState{State: "landed", Commit: winner.Commit, Tree: winner.Tree, RecordedAt: now()}
691
+ winner.State = StateMerged
692
+ winner.LastTransition = now()
693
+ if err := save(path, m); err != nil {
694
+ return nil, err
695
+ }
696
+ return m, nil
697
+ }
698
+
699
+ func Cleanup(root, runID string) (*Manifest, error) {
700
+ m, path, err := Load(root, runID)
701
+ if err != nil {
702
+ return nil, err
703
+ }
704
+ if m.Merge.State != "landed" || m.Verification.State != "verified" {
705
+ return nil, errors.New("forge: cleanup requires a landed winner and recorded successful verification")
706
+ }
707
+ if _, err := validatePrimary(m, true); err != nil {
708
+ return nil, err
709
+ }
710
+ preserved := map[string]string{}
711
+ for i := range m.Candidates {
712
+ c := &m.Candidates[i]
713
+ removed, reason := removeEligible(m, c)
714
+ if reason != "" {
715
+ preserved[string(c.ID)] = reason
716
+ c.Preservation = reason
717
+ continue
718
+ }
719
+ if removed {
720
+ c.State = StateCleaned
721
+ c.Preservation = ""
722
+ c.LastTransition = now()
723
+ if err := save(path, m); err != nil {
724
+ return nil, err
725
+ }
726
+ }
727
+ }
728
+ state := "complete"
729
+ if len(preserved) != 0 {
730
+ state = "partial"
731
+ }
732
+ m.Cleanup = CleanupState{State: state, Preserved: preserved, RecordedAt: now()}
733
+ if err := save(path, m); err != nil {
734
+ return nil, err
735
+ }
736
+ return m, nil
737
+ }
738
+
739
+ func Reap(root, slug string) ([]ReapResult, error) {
740
+ root, err := physicalExisting(root)
741
+ if err != nil {
742
+ return nil, err
743
+ }
744
+ if slug != "" && !safeSlug.MatchString(slug) {
745
+ return nil, fmt.Errorf("forge: invalid feature slug %q", slug)
746
+ }
747
+ paths, err := manifestPaths(root, slug)
748
+ if err != nil {
749
+ return nil, err
750
+ }
751
+ counts := map[string]int{}
752
+ for _, path := range paths {
753
+ counts[filepath.Base(filepath.Dir(path))]++
754
+ }
755
+ loaded := make(map[string]*Manifest, len(paths))
756
+ for _, path := range paths {
757
+ m, loadErr := loadAt(root, path)
758
+ if loadErr == nil {
759
+ loaded[path] = m
760
+ }
761
+ }
762
+ results := make([]ReapResult, 0, len(paths))
763
+ for _, path := range paths {
764
+ m, ok := loaded[path]
765
+ if !ok {
766
+ _, loadErr := loadAt(root, path)
767
+ results = append(results, ReapResult{Error: loadErr.Error()})
768
+ continue
769
+ }
770
+ if counts[m.RunID] != 1 {
771
+ results = append(results, ReapResult{RunID: m.RunID, Error: "duplicate manifest; preserved"})
772
+ continue
773
+ }
774
+ result := ReapResult{RunID: m.RunID, Preserved: map[string]string{}}
775
+ changed := false
776
+ for i := range m.Candidates {
777
+ c := &m.Candidates[i]
778
+ removed, reason := removeEligible(m, c)
779
+ if reason != "" {
780
+ result.Preserved[string(c.ID)] = reason
781
+ if c.Preservation != reason {
782
+ c.Preservation = reason
783
+ c.LastTransition = now()
784
+ changed = true
785
+ }
786
+ continue
787
+ }
788
+ if removed {
789
+ result.Removed = append(result.Removed, c.ID)
790
+ c.State = StateCleaned
791
+ c.Preservation = ""
792
+ c.LastTransition = now()
793
+ changed = true
794
+ }
795
+ }
796
+ if len(result.Preserved) == 0 {
797
+ result.Preserved = nil
798
+ }
799
+ if changed {
800
+ if err := save(path, m); err != nil {
801
+ result.Error = err.Error()
802
+ }
803
+ }
804
+ results = append(results, result)
805
+ }
806
+ return results, nil
807
+ }
808
+
809
+ func removeEligible(m *Manifest, c *Candidate) (bool, string) {
810
+ if c.State == StateCleaned {
811
+ return false, ""
812
+ }
813
+ if c.State != StateExtracted && c.State != StateMerged {
814
+ return false, "candidate is not terminal and extracted"
815
+ }
816
+ if live, reason := workerLiveness(c.Worker); live != livenessDead {
817
+ return false, reason
818
+ }
819
+ if c.Commit == "" || c.Tree == "" {
820
+ return false, "candidate content is not recorded as a reachable commit"
821
+ }
822
+ head, err := gitText(m.PrimaryRoot, "rev-parse", "--verify", "refs/heads/"+c.Branch+"^{commit}")
823
+ if err != nil || head != c.Commit {
824
+ return false, "candidate branch no longer pins the recorded commit"
825
+ }
826
+ if _, err := gitBytes(m.PrimaryRoot, nil, "cat-file", "-e", c.Commit+"^{commit}"); err != nil {
827
+ return false, "candidate commit is not reachable"
828
+ }
829
+ if _, err := os.Lstat(c.Worktree); errors.Is(err, os.ErrNotExist) {
830
+ registered, listErr := registeredWorktrees(m.PrimaryRoot)
831
+ if listErr != nil {
832
+ return false, "cannot verify missing worktree registration"
833
+ }
834
+ if _, exists := registered[c.Worktree]; exists {
835
+ return false, "worktree path is missing but remains registered"
836
+ }
837
+ return true, ""
838
+ } else if err != nil {
839
+ return false, "cannot inspect candidate worktree"
840
+ }
841
+ if err := validateCandidateRepo(m, c); err != nil {
842
+ return false, err.Error()
843
+ }
844
+ if dirty, err := dirtyStatus(c.Worktree, true, nil); err != nil {
845
+ return false, err.Error()
846
+ } else if dirty {
847
+ return false, "candidate contains tracked, untracked, or ignored dirt"
848
+ }
849
+ if _, err := gitBytes(m.PrimaryRoot, nil, "worktree", "remove", c.Worktree); err != nil {
850
+ return false, err.Error()
851
+ }
852
+ if _, err := os.Lstat(c.Worktree); !errors.Is(err, os.ErrNotExist) {
853
+ return false, "candidate worktree still exists after removal"
854
+ }
855
+ return true, ""
856
+ }
857
+
858
+ func preserve(path string, m *Manifest, c *Candidate, reason string) (*Manifest, error) {
859
+ c.Preservation = reason
860
+ c.LastTransition = now()
861
+ if err := save(path, m); err != nil {
862
+ return nil, err
863
+ }
864
+ return nil, fmt.Errorf("forge: candidate %s preserved: %s", c.ID, reason)
865
+ }
866
+
867
+ func rejectPrivateOrGitlinkChanges(c *Candidate) error {
868
+ names, err := gitBytes(c.Worktree, nil, "diff", "--name-only", "-z", c.InitialBase, c.Commit, "--")
869
+ if err != nil {
870
+ return err
871
+ }
872
+ for _, name := range bytes.Split(names, []byte{0}) {
873
+ clean := filepath.ToSlash(string(name))
874
+ if clean == ".devrites" || strings.HasPrefix(clean, ".devrites/") {
875
+ return errors.New("candidate changed root-owned .devrites content")
876
+ }
877
+ }
878
+ tree, err := gitBytes(c.Worktree, nil, "ls-tree", "-r", "-z", c.Commit)
879
+ if err != nil {
880
+ return err
881
+ }
882
+ for _, entry := range bytes.Split(tree, []byte{0}) {
883
+ if bytes.HasPrefix(entry, []byte("160000 ")) {
884
+ return errors.New("candidate tree contains a gitlink")
885
+ }
886
+ }
887
+ return nil
888
+ }
889
+
890
+ func validateStagingParent(staging string) error {
891
+ parent := filepath.Dir(staging)
892
+ if info, err := os.Lstat(parent); err == nil && info.Mode()&os.ModeSymlink != 0 {
893
+ return fmt.Errorf("forge: staging parent is a symlink: %s", parent)
894
+ } else if err != nil && !errors.Is(err, os.ErrNotExist) {
895
+ return fmt.Errorf("forge: inspect staging parent: %w", err)
896
+ }
897
+ return nil
898
+ }
899
+
900
+ func validateManifestParentBeforeCreate(root, path string) error {
901
+ current := root
902
+ rel, err := filepath.Rel(root, filepath.Dir(path))
903
+ if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
904
+ return errors.New("forge: manifest path escapes primary root")
905
+ }
906
+ for _, part := range strings.Split(rel, string(filepath.Separator)) {
907
+ current = filepath.Join(current, part)
908
+ info, err := os.Lstat(current)
909
+ if errors.Is(err, os.ErrNotExist) {
910
+ return nil
911
+ }
912
+ if err != nil {
913
+ return err
914
+ }
915
+ if info.Mode()&os.ModeSymlink != 0 {
916
+ return fmt.Errorf("forge: manifest parent is a symlink: %s", current)
917
+ }
918
+ }
919
+ return nil
920
+ }
921
+
922
+ func degraded(reason string) PlanResult {
923
+ return PlanResult{Status: "degraded", Mode: "serial", Reason: strings.TrimPrefix(reason, "forge: ")}
924
+ }
925
+
926
+ func validSHA256(value string) bool {
927
+ if len(value) != 64 {
928
+ return false
929
+ }
930
+ _, err := hex.DecodeString(value)
931
+ return err == nil
932
+ }
933
+
934
+ func parsePID(value string) (int, error) {
935
+ pid, err := strconv.Atoi(value)
936
+ if err != nil || pid <= 0 || strconv.Itoa(pid) != value {
937
+ return 0, errors.New("forge: PID must be a canonical positive integer")
938
+ }
939
+ return pid, nil
940
+ }