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