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.
- package/CHANGELOG.md +18 -0
- package/README.md +67 -45
- package/SECURITY.md +31 -29
- package/docs/adr/0001-go-engine-as-control-plane.md +1 -1
- package/docs/adr/0002-dual-host-harness.md +1 -1
- package/docs/adr/0004-state-schema-phases-sections.md +1 -2
- package/docs/adr/0006-clock-seam-and-engine-ci-gates.md +3 -4
- package/docs/adr/0009-prebuild-decision-coverage-and-readiness.md +67 -0
- package/docs/adr/0010-agent-first-fresh-context-orchestration.md +71 -0
- package/docs/adr/0011-define-plan-transition-rights.md +28 -0
- package/docs/adr/README.md +3 -0
- package/docs/agents/triage-labels.md +7 -7
- package/docs/architecture.md +157 -140
- package/docs/cli.md +41 -12
- package/docs/command-map.md +49 -35
- package/docs/engine/agent-contract.md +92 -15
- package/docs/engine/commands.md +242 -72
- package/docs/engine/state-schema.md +52 -23
- package/docs/engine/workspace-schema.md +94 -15
- package/docs/extensions.md +1 -1
- package/docs/flow.md +80 -50
- package/docs/harness-compliance.md +29 -5
- package/docs/orchestration.md +107 -79
- package/docs/quick-reference.md +7 -3
- package/docs/release.md +4 -3
- package/docs/skills.md +64 -38
- package/docs/usage.md +57 -38
- package/engine/cmd/releasepack/main.go +219 -0
- package/engine/cmd/releasepack/main_test.go +170 -0
- package/engine/commands.go +170 -23
- package/engine/git_guard.go +187 -0
- package/engine/git_guard_test.go +283 -0
- package/engine/hookpolicy.go +53 -55
- package/engine/hookpolicy_test.go +91 -1
- package/engine/hooks.go +296 -75
- package/engine/hooks_events_test.go +276 -6
- package/engine/hooks_workspace.go +640 -159
- package/engine/internal/devritespaths/paths.go +65 -10
- package/engine/internal/devritespaths/paths_test.go +110 -0
- package/engine/internal/doctor/doctor.go +153 -23
- package/engine/internal/doctor/doctor_test.go +74 -0
- package/engine/internal/forge/forge.go +940 -0
- package/engine/internal/forge/forge_test.go +576 -0
- package/engine/internal/forge/git.go +245 -0
- package/engine/internal/forge/liveness_unix.go +48 -0
- package/engine/internal/forge/liveness_windows.go +67 -0
- package/engine/internal/forge/manifest.go +402 -0
- package/engine/internal/gate/gate.go +90 -71
- package/engine/internal/gate/gate_test.go +71 -2
- package/engine/internal/harness/compliance.go +18 -24
- package/engine/internal/harness/harness.go +37 -44
- package/engine/internal/harness/harness_test.go +21 -5
- package/engine/internal/install/install.go +575 -58
- package/engine/internal/install/install_test.go +490 -9
- package/engine/internal/iohooks/iohooks.go +350 -59
- package/engine/internal/iohooks/iohooks_test.go +421 -1
- package/engine/internal/lib/buildreadiness.go +35 -18
- package/engine/internal/lib/clarifyreturn.go +91 -0
- package/engine/internal/lib/clarifyreturn_test.go +123 -0
- package/engine/internal/lib/context.go +54 -15
- package/engine/internal/lib/cursor_compat_test.go +8 -0
- package/engine/internal/lib/extensions.go +2 -3
- package/engine/internal/lib/gitauthority.go +601 -0
- package/engine/internal/lib/gitauthority_test.go +346 -0
- package/engine/internal/lib/jsonout.go +25 -11
- package/engine/internal/lib/jsonout_test.go +16 -0
- package/engine/internal/lib/lanes.go +14 -8
- package/engine/internal/lib/observability_test.go +358 -0
- package/engine/internal/lib/packageexistence.go +151 -35
- package/engine/internal/lib/packageexistence_test.go +163 -0
- package/engine/internal/lib/progress.go +11 -9
- package/engine/internal/lib/provenance.go +462 -0
- package/engine/internal/lib/provenance_test.go +154 -0
- package/engine/internal/lib/readiness_contract.json +111 -0
- package/engine/internal/lib/readinessartifact.go +510 -0
- package/engine/internal/lib/readinessartifact_test.go +321 -0
- package/engine/internal/lib/reconcile.go +712 -88
- package/engine/internal/lib/reconcile_test.go +335 -16
- package/engine/internal/lib/recoveryattempts.go +298 -0
- package/engine/internal/lib/recoveryattempts_test.go +215 -0
- package/engine/internal/lib/resolve.go +23 -19
- package/engine/internal/lib/runbook_context_test.go +20 -0
- package/engine/internal/lib/session.go +701 -9
- package/engine/internal/lib/session_test.go +80 -13
- package/engine/internal/lib/testintegrity.go +33 -37
- package/engine/internal/lib/testintegrity_test.go +63 -1
- package/engine/internal/migrate/migrate.go +81 -1
- package/engine/internal/migrate/migrate_test.go +22 -1
- package/engine/internal/reason/reason.go +180 -0
- package/engine/internal/reason/reason_test.go +35 -0
- package/engine/internal/rootfacts/facts.go +466 -0
- package/engine/internal/rootfacts/facts_test.go +306 -0
- package/engine/internal/safepath/safepath.go +55 -0
- package/engine/internal/safepath/safepath_test.go +69 -0
- package/engine/internal/safepath/safepath_windows_test.go +26 -0
- package/engine/internal/state/clarify_transition.go +165 -0
- package/engine/internal/state/clarify_transition_test.go +130 -0
- package/engine/internal/state/cmd/workflowmanifest/main.go +12 -8
- package/engine/internal/state/cursor.go +58 -0
- package/engine/internal/state/cursor_test.go +42 -1
- package/engine/internal/state/feature.go +35 -48
- package/engine/internal/state/schema.go +90 -37
- package/engine/internal/state/snapshot.go +18 -2
- package/engine/internal/state/state_test.go +187 -7
- package/engine/internal/state/status.go +48 -11
- package/engine/internal/state/workflow_manifest.json +231 -50
- package/engine/internal/toolpolicy/classifier.go +533 -0
- package/engine/internal/toolpolicy/classifier_test.go +424 -0
- package/engine/internal/toolpolicy/git.go +616 -0
- package/engine/internal/toolpolicy/scanner.go +382 -0
- package/engine/main.go +160 -77
- package/engine/observability_cli_test.go +52 -0
- package/engine/root_routing_test.go +277 -0
- package/engine/testdata/golden/TestParityBuildReadiness/arg=approved.golden +1 -1
- package/engine/testdata/golden/TestParityBuildReadiness/arg=emptystatus.golden +1 -1
- package/engine/testdata/golden/TestParityBuildReadiness/arg=noclarify.golden +1 -0
- package/engine/testdata/golden/TestParityBuildReadiness/arg=novet.golden +1 -0
- package/engine/testdata/golden/TestParityBuildReadiness/arg=stalevet.golden +1 -0
- package/engine/testdata/golden/TestParityBuildReadiness/arg=trailhash.golden +1 -1
- package/engine/testdata/golden/TestParityBuildReadiness/arg=trailpipe.golden +1 -1
- package/engine/testdata/golden/TestParityBuildReadiness/arg=vetnotready.golden +1 -0
- package/engine/testdata/golden/TestParityProgress/arg=allbuilt.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=done.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=mid.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=nophase.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=noslice.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=plan.golden +1 -1
- package/engine/testdata/golden/TestParityProgress/arg=seal.golden +1 -1
- package/engine/testdata/golden/TestParityReconcile/check-clean.golden +1 -1
- package/engine/testdata/golden/TestParityReconcile/inline-fallback.golden +1 -0
- package/engine/testdata/golden/TestParityReconcile/snapshot-no-allowlist.golden +1 -0
- package/engine/testdata/golden/TestParityWrightScope/devrites-edit-denied.golden +2 -0
- package/engine/testdata/golden/TestParityWrightScope/out-of-scope-enforce-denies.golden +1 -1
- package/engine/tests/adr_0011_define_plan_test.go +30 -0
- package/engine/tests/budget_test.go +19 -8
- package/engine/tests/concurrency_cli_test.go +7 -1
- package/engine/tests/doctor_cli_test.go +148 -2
- package/engine/tests/forge_cli_test.go +463 -0
- package/engine/tests/gate_test.go +63 -8
- package/engine/tests/hook_test.go +260 -8
- package/engine/tests/hooks_io_test.go +94 -3
- package/engine/tests/json_contract_test.go +127 -2
- package/engine/tests/migrate_cli_test.go +2 -2
- package/engine/tests/parity_buildreadiness_test.go +141 -10
- package/engine/tests/parity_learnings_test.go +16 -17
- package/engine/tests/parity_reconcile_test.go +20 -17
- package/engine/tests/parity_resolve_test.go +12 -11
- package/engine/tests/parity_test.go +21 -17
- package/pack/.claude/agents/devrites-code-reviewer.md +62 -48
- package/pack/.claude/agents/devrites-devex-reviewer.md +69 -53
- package/pack/.claude/agents/devrites-doubt-reviewer.md +29 -22
- package/pack/.claude/agents/devrites-evidence-scout.md +69 -0
- package/pack/.claude/agents/devrites-forge-judge.md +74 -61
- package/pack/.claude/agents/devrites-frontend-reviewer.md +48 -39
- package/pack/.claude/agents/devrites-performance-reviewer.md +49 -40
- package/pack/.claude/agents/devrites-plan-drafter.md +71 -0
- package/pack/.claude/agents/devrites-plan-reviewer.md +80 -47
- package/pack/.claude/agents/devrites-proof-runner.md +74 -0
- package/pack/.claude/agents/devrites-retrospector.md +48 -45
- package/pack/.claude/agents/devrites-security-auditor.md +46 -36
- package/pack/.claude/agents/devrites-simplifier-reviewer.md +50 -46
- package/pack/.claude/agents/devrites-slice-wright.md +153 -165
- package/pack/.claude/agents/devrites-spec-reviewer.md +34 -32
- package/pack/.claude/agents/devrites-strategy-reviewer.md +63 -34
- package/pack/.claude/agents/devrites-test-analyst.md +40 -30
- package/pack/.claude/settings.json +2 -1
- package/pack/.claude/skills/devrites-audit/SKILL.md +40 -61
- package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +24 -13
- package/pack/.claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
- package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
- package/pack/.claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
- package/pack/.claude/skills/devrites-doubt/SKILL.md +25 -17
- package/pack/.claude/skills/devrites-interview/SKILL.md +53 -52
- package/pack/.claude/skills/devrites-lib/SKILL.md +11 -9
- package/pack/.claude/skills/devrites-lib/reference/intent-map.md +3 -2
- package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
- package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +8 -1
- package/pack/.claude/skills/devrites-lib/reference/standards/README.md +38 -55
- package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
- package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +198 -190
- package/pack/.claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
- package/pack/.claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
- package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
- package/pack/.claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
- package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
- package/pack/.claude/skills/devrites-lib/reference/standards/core.md +23 -18
- package/pack/.claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
- package/pack/.claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
- package/pack/.claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
- package/pack/.claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
- package/pack/.claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
- package/pack/.claude/skills/devrites-lib/reference/standards/performance.md +0 -9
- package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +1 -3
- package/pack/.claude/skills/devrites-lib/reference/standards/security.md +17 -7
- package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +1 -27
- package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
- package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
- package/pack/.claude/skills/devrites-source-driven/SKILL.md +23 -22
- package/pack/.claude/skills/rite/SKILL.md +8 -6
- package/pack/.claude/skills/rite/reference/menu.md +8 -6
- package/pack/.claude/skills/rite-adopt/SKILL.md +33 -37
- package/pack/.claude/skills/rite-autocomplete/SKILL.md +33 -28
- package/pack/.claude/skills/rite-autocomplete/reference/loop.md +21 -21
- package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
- package/pack/.claude/skills/rite-build/SKILL.md +35 -30
- package/pack/.claude/skills/rite-build/reference/afk-discipline.md +38 -38
- package/pack/.claude/skills/rite-build/reference/evidence-standard.md +10 -0
- package/pack/.claude/skills/rite-build/reference/forge.md +186 -156
- package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
- package/pack/.claude/skills/rite-build/reference/phase-contract.md +96 -175
- package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +129 -134
- package/pack/.claude/skills/rite-clarify/SKILL.md +89 -0
- package/pack/.claude/skills/rite-clarify/reference/decision-coverage.md +59 -0
- package/pack/.claude/skills/rite-converge/SKILL.md +35 -25
- package/pack/.claude/skills/rite-define/SKILL.md +57 -32
- package/pack/.claude/skills/rite-define/reference/gates.md +16 -15
- package/pack/.claude/skills/rite-define/reference/plan-template.md +16 -8
- package/pack/.claude/skills/rite-frame/reference/failure-modes.md +22 -24
- package/pack/.claude/skills/rite-plan/SKILL.md +44 -16
- package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +4 -0
- package/pack/.claude/skills/rite-polish/SKILL.md +27 -23
- package/pack/.claude/skills/rite-prototype/SKILL.md +25 -26
- package/pack/.claude/skills/rite-prove/SKILL.md +27 -17
- package/pack/.claude/skills/rite-resolve/SKILL.md +12 -11
- package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
- package/pack/.claude/skills/rite-review/SKILL.md +37 -28
- package/pack/.claude/skills/rite-seal/reference/phase-contract.md +27 -92
- package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
- package/pack/.claude/skills/rite-ship/reference/design-memory.md +31 -36
- package/pack/.claude/skills/rite-spec/SKILL.md +84 -135
- package/pack/.claude/skills/rite-spec/reference/investigation.md +37 -33
- package/pack/.claude/skills/rite-spec/reference/question-protocol.md +6 -2
- package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +25 -24
- package/pack/.claude/skills/rite-spec/reference/spec-template.md +9 -2
- package/pack/.claude/skills/rite-spec/reference/state-workspace.md +13 -3
- package/pack/.claude/skills/rite-temper/SKILL.md +62 -47
- package/pack/.claude/skills/rite-temper/reference/review-dimensions.md +24 -22
- package/pack/.claude/skills/rite-vet/SKILL.md +110 -113
- package/pack/.claude/skills/rite-vet/reference/artifacts.md +42 -9
- package/pack/.claude/skills/rite-vet/reference/review-axes.md +39 -37
- package/pack/generated/claude/agents/devrites-code-reviewer.md +62 -48
- package/pack/generated/claude/agents/devrites-devex-reviewer.md +69 -53
- package/pack/generated/claude/agents/devrites-doubt-reviewer.md +29 -22
- package/pack/generated/claude/agents/devrites-evidence-scout.md +69 -0
- package/pack/generated/claude/agents/devrites-forge-judge.md +74 -61
- package/pack/generated/claude/agents/devrites-frontend-reviewer.md +48 -39
- package/pack/generated/claude/agents/devrites-performance-reviewer.md +49 -40
- package/pack/generated/claude/agents/devrites-plan-drafter.md +71 -0
- package/pack/generated/claude/agents/devrites-plan-reviewer.md +80 -47
- package/pack/generated/claude/agents/devrites-proof-runner.md +74 -0
- package/pack/generated/claude/agents/devrites-retrospector.md +48 -45
- package/pack/generated/claude/agents/devrites-security-auditor.md +46 -36
- package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +50 -46
- package/pack/generated/claude/agents/devrites-slice-wright.md +153 -165
- package/pack/generated/claude/agents/devrites-spec-reviewer.md +34 -32
- package/pack/generated/claude/agents/devrites-strategy-reviewer.md +63 -34
- package/pack/generated/claude/agents/devrites-test-analyst.md +40 -30
- package/pack/generated/claude/settings.json +2 -1
- package/pack/generated/claude/skills/devrites-audit/SKILL.md +40 -61
- package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +24 -13
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
- package/pack/generated/claude/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
- package/pack/generated/claude/skills/devrites-doubt/SKILL.md +25 -17
- package/pack/generated/claude/skills/devrites-interview/SKILL.md +53 -52
- package/pack/generated/claude/skills/devrites-lib/SKILL.md +11 -9
- package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +3 -2
- package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
- package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +8 -1
- package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +38 -55
- package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
- package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +198 -190
- package/pack/generated/claude/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
- package/pack/generated/claude/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
- package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +16 -52
- package/pack/generated/claude/skills/devrites-lib/reference/standards/coding-style.md +2 -10
- package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
- package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +23 -18
- package/pack/generated/claude/skills/devrites-lib/reference/standards/deprecation.md +17 -57
- package/pack/generated/claude/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
- package/pack/generated/claude/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
- package/pack/generated/claude/skills/devrites-lib/reference/standards/hooks.md +3 -14
- package/pack/generated/claude/skills/devrites-lib/reference/standards/patterns.md +9 -25
- package/pack/generated/claude/skills/devrites-lib/reference/standards/performance.md +0 -9
- package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +1 -3
- package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +17 -7
- package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +1 -27
- package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +46 -50
- package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
- package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +23 -22
- package/pack/generated/claude/skills/rite/SKILL.md +8 -6
- package/pack/generated/claude/skills/rite/reference/menu.md +8 -6
- package/pack/generated/claude/skills/rite-adopt/SKILL.md +33 -37
- package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +33 -28
- package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +21 -21
- package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
- package/pack/generated/claude/skills/rite-build/SKILL.md +35 -30
- package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +38 -38
- package/pack/generated/claude/skills/rite-build/reference/evidence-standard.md +10 -0
- package/pack/generated/claude/skills/rite-build/reference/forge.md +186 -156
- package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +4 -3
- package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +96 -175
- package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +129 -134
- package/pack/generated/claude/skills/rite-clarify/SKILL.md +89 -0
- package/pack/generated/claude/skills/rite-clarify/reference/decision-coverage.md +59 -0
- package/pack/generated/claude/skills/rite-converge/SKILL.md +35 -25
- package/pack/generated/claude/skills/rite-define/SKILL.md +57 -32
- package/pack/generated/claude/skills/rite-define/reference/gates.md +16 -15
- package/pack/generated/claude/skills/rite-define/reference/plan-template.md +16 -8
- package/pack/generated/claude/skills/rite-frame/reference/failure-modes.md +22 -24
- package/pack/generated/claude/skills/rite-plan/SKILL.md +44 -16
- package/pack/generated/claude/skills/rite-plan/reference/task-breakdown.md +4 -0
- package/pack/generated/claude/skills/rite-polish/SKILL.md +27 -23
- package/pack/generated/claude/skills/rite-prototype/SKILL.md +25 -26
- package/pack/generated/claude/skills/rite-prove/SKILL.md +27 -17
- package/pack/generated/claude/skills/rite-resolve/SKILL.md +12 -11
- package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +3 -0
- package/pack/generated/claude/skills/rite-review/SKILL.md +37 -28
- package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +27 -92
- package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +13 -0
- package/pack/generated/claude/skills/rite-ship/reference/design-memory.md +31 -36
- package/pack/generated/claude/skills/rite-spec/SKILL.md +84 -135
- package/pack/generated/claude/skills/rite-spec/reference/investigation.md +37 -33
- package/pack/generated/claude/skills/rite-spec/reference/question-protocol.md +6 -2
- package/pack/generated/claude/skills/rite-spec/reference/spec-checklists.md +25 -24
- package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +9 -2
- package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +13 -3
- package/pack/generated/claude/skills/rite-temper/SKILL.md +62 -47
- package/pack/generated/claude/skills/rite-temper/reference/review-dimensions.md +24 -22
- package/pack/generated/claude/skills/rite-vet/SKILL.md +110 -113
- package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +42 -9
- package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +39 -37
- package/pack/generated/codex/AGENTS.md +5 -2
- package/pack/generated/codex/agents/devrites-code-reviewer.toml +69 -47
- package/pack/generated/codex/agents/devrites-devex-reviewer.toml +75 -51
- package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +35 -20
- package/pack/generated/codex/agents/devrites-evidence-scout.toml +75 -0
- package/pack/generated/codex/agents/devrites-forge-judge.toml +80 -59
- package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +54 -37
- package/pack/generated/codex/agents/devrites-performance-reviewer.toml +55 -38
- package/pack/generated/codex/agents/devrites-plan-drafter.toml +77 -0
- package/pack/generated/codex/agents/devrites-plan-reviewer.toml +82 -47
- package/pack/generated/codex/agents/devrites-proof-runner.toml +80 -0
- package/pack/generated/codex/agents/devrites-retrospector.toml +54 -43
- package/pack/generated/codex/agents/devrites-security-auditor.toml +53 -35
- package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +56 -44
- package/pack/generated/codex/agents/devrites-slice-wright.toml +159 -163
- package/pack/generated/codex/agents/devrites-spec-reviewer.toml +40 -30
- package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +65 -34
- package/pack/generated/codex/agents/devrites-test-analyst.toml +46 -28
- package/pack/generated/codex/hooks.json +4 -14
- package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +7 -3
- package/pack/generated/codex/skills/devrites-audit/SKILL.md +47 -64
- package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +7 -3
- package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +31 -16
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/build-the-loop.md +24 -21
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +34 -6
- package/pack/generated/codex/skills/devrites-debug-recovery/reference/instrumentation.md +2 -2
- package/pack/generated/codex/skills/devrites-doubt/SKILL.md +32 -20
- package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +7 -3
- package/pack/generated/codex/skills/devrites-interview/SKILL.md +60 -55
- package/pack/generated/codex/skills/devrites-lib/SKILL.md +18 -12
- package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +3 -2
- package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +82 -137
- package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +8 -1
- package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +38 -55
- package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +53 -27
- package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +198 -190
- package/pack/generated/codex/skills/devrites-lib/reference/standards/anti-patterns.md +5 -15
- package/pack/generated/codex/skills/devrites-lib/reference/standards/ci-cd.md +27 -58
- package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +16 -52
- package/pack/generated/codex/skills/devrites-lib/reference/standards/coding-style.md +2 -10
- package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +18 -61
- package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +23 -18
- package/pack/generated/codex/skills/devrites-lib/reference/standards/deprecation.md +17 -57
- package/pack/generated/codex/skills/devrites-lib/reference/standards/development-workflow.md +4 -33
- package/pack/generated/codex/skills/devrites-lib/reference/standards/git-workflow.md +4 -20
- package/pack/generated/codex/skills/devrites-lib/reference/standards/hooks.md +3 -14
- package/pack/generated/codex/skills/devrites-lib/reference/standards/patterns.md +9 -25
- package/pack/generated/codex/skills/devrites-lib/reference/standards/performance.md +0 -9
- package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +1 -3
- package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +17 -7
- package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +1 -27
- package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +46 -50
- package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +23 -10
- package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +7 -3
- package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +7 -3
- package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +29 -24
- package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite/SKILL.md +19 -13
- package/pack/generated/codex/skills/rite/reference/menu.md +8 -6
- package/pack/generated/codex/skills/rite-adopt/SKILL.md +40 -40
- package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +40 -31
- package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +21 -21
- package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +15 -6
- package/pack/generated/codex/skills/rite-build/SKILL.md +42 -33
- package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +38 -38
- package/pack/generated/codex/skills/rite-build/reference/evidence-standard.md +10 -0
- package/pack/generated/codex/skills/rite-build/reference/forge.md +186 -156
- package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +4 -3
- package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +96 -175
- package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +129 -134
- package/pack/generated/codex/skills/rite-clarify/SKILL.md +105 -0
- package/pack/generated/codex/skills/rite-clarify/reference/decision-coverage.md +59 -0
- package/pack/generated/codex/skills/rite-converge/SKILL.md +42 -28
- package/pack/generated/codex/skills/rite-customize/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-define/SKILL.md +64 -35
- package/pack/generated/codex/skills/rite-define/reference/gates.md +16 -15
- package/pack/generated/codex/skills/rite-define/reference/plan-template.md +16 -8
- package/pack/generated/codex/skills/rite-doctor/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-dogfood/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-explain/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-frame/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-frame/reference/failure-modes.md +22 -24
- package/pack/generated/codex/skills/rite-handoff/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-learn/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-plan/SKILL.md +51 -19
- package/pack/generated/codex/skills/rite-plan/reference/task-breakdown.md +5 -1
- package/pack/generated/codex/skills/rite-polish/SKILL.md +34 -26
- package/pack/generated/codex/skills/rite-pov/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-prototype/SKILL.md +32 -29
- package/pack/generated/codex/skills/rite-prove/SKILL.md +34 -20
- package/pack/generated/codex/skills/rite-quick/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-resolve/SKILL.md +19 -14
- package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +3 -0
- package/pack/generated/codex/skills/rite-review/SKILL.md +44 -31
- package/pack/generated/codex/skills/rite-seal/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +27 -92
- package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +13 -0
- package/pack/generated/codex/skills/rite-ship/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-ship/reference/design-memory.md +31 -36
- package/pack/generated/codex/skills/rite-spec/SKILL.md +91 -138
- package/pack/generated/codex/skills/rite-spec/reference/investigation.md +37 -33
- package/pack/generated/codex/skills/rite-spec/reference/question-protocol.md +6 -2
- package/pack/generated/codex/skills/rite-spec/reference/spec-checklists.md +25 -24
- package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +9 -2
- package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +13 -3
- package/pack/generated/codex/skills/rite-status/SKILL.md +7 -3
- package/pack/generated/codex/skills/rite-temper/SKILL.md +69 -50
- package/pack/generated/codex/skills/rite-temper/reference/review-dimensions.md +24 -22
- package/pack/generated/codex/skills/rite-vet/SKILL.md +117 -116
- package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +42 -9
- package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +40 -38
- package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +7 -3
- package/package.json +1 -1
- package/scripts/build-release-tarball.sh +32 -15
- package/scripts/check-authority-drift.py +125 -0
- package/scripts/check-instruction-size-baseline.mjs +19 -11
- package/scripts/check-invocation-integrity.py +2 -0
- package/scripts/codex-generate.sh +69 -33
- package/scripts/grade-feature.sh +121 -40
- package/scripts/live-hosts/agent-result.schema.json +230 -0
- package/scripts/live-hosts/claude.sh +87 -0
- package/scripts/live-hosts/codex.sh +81 -0
- package/scripts/live-hosts/common.sh +113 -0
- package/scripts/live-hosts/fake-host.py +264 -0
- package/scripts/live-hosts/host-transport.py +287 -0
- package/scripts/release-check.sh +5 -1
- package/scripts/run-agent-contract-evals.py +1380 -0
- package/scripts/run-behavioral-evals.sh +24 -30
- package/scripts/run-evals.sh +1 -5
- package/scripts/run-live-behavioral-evals.py +1274 -144
- package/scripts/run-outcome-evals.sh +414 -88
- package/scripts/run-tests.mjs +30 -2
- package/scripts/skills-inventory.mjs +1 -1
- package/scripts/validate-workflow-security.py +39 -20
- package/scripts/validate-workspace-schema.py +362 -10
- package/scripts/validate.sh +21 -15
- package/engine/testdata/golden/TestParityWrightScope/devrites-edit-allowed.golden +0 -1
- /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
|
+
}
|