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
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
package main
|
|
2
2
|
|
|
3
|
-
// Workspace hooks read
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Workspace hooks read state.md, questions.md, and other runtime files from the
|
|
4
|
+
// active feature directory. work is canonical, while features remains readable
|
|
5
|
+
// for compatibility. These hooks are read only and stay silent outside a
|
|
6
|
+
// DevRites workspace.
|
|
7
7
|
|
|
8
8
|
import (
|
|
9
9
|
"bytes"
|
|
@@ -11,14 +11,20 @@ import (
|
|
|
11
11
|
"fmt"
|
|
12
12
|
"io"
|
|
13
13
|
"os"
|
|
14
|
+
"os/exec"
|
|
14
15
|
"path/filepath"
|
|
15
16
|
"regexp"
|
|
16
17
|
"strconv"
|
|
17
18
|
"strings"
|
|
18
19
|
"time"
|
|
19
20
|
|
|
21
|
+
"github.com/devrites/devrites/internal/forge"
|
|
20
22
|
"github.com/devrites/devrites/internal/harness"
|
|
23
|
+
"github.com/devrites/devrites/internal/lib"
|
|
21
24
|
"github.com/devrites/devrites/internal/orient"
|
|
25
|
+
drvreason "github.com/devrites/devrites/internal/reason"
|
|
26
|
+
"github.com/devrites/devrites/internal/rootfacts"
|
|
27
|
+
"github.com/devrites/devrites/internal/safepath"
|
|
22
28
|
"github.com/devrites/devrites/internal/state"
|
|
23
29
|
)
|
|
24
30
|
|
|
@@ -26,8 +32,8 @@ var (
|
|
|
26
32
|
openStatusRe = regexp.MustCompile(`(?i)^[[:space:]]*status:[[:space:]]*open`)
|
|
27
33
|
)
|
|
28
34
|
|
|
29
|
-
// resolveWorkspaceDir
|
|
30
|
-
//
|
|
35
|
+
// resolveWorkspaceDir finds the active feature directory. New workspaces use
|
|
36
|
+
// work, while existing features workspaces remain readable.
|
|
31
37
|
func resolveWorkspaceDir(root, slug string) string {
|
|
32
38
|
work := filepath.Join(root, "work", slug)
|
|
33
39
|
if wsIsDir(work) {
|
|
@@ -41,8 +47,8 @@ func resolveWorkspaceDir(root, slug string) string {
|
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
// resolveWorkspace resolves the active feature's directory.
|
|
44
|
-
// ok is false
|
|
45
|
-
//
|
|
50
|
+
// ok is false when no .devrites root or active feature exists. Callers use that
|
|
51
|
+
// result to return silently.
|
|
46
52
|
func resolveWorkspace() (root, slug, dir string, ok bool) {
|
|
47
53
|
root, err := orient.ResolveRoot(os.Getenv("DEVRITES_ROOT"))
|
|
48
54
|
if err != nil {
|
|
@@ -55,31 +61,28 @@ func resolveWorkspace() (root, slug, dir string, ok bool) {
|
|
|
55
61
|
return root, slug, resolveWorkspaceDir(root, slug), true
|
|
56
62
|
}
|
|
57
63
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
func recordHookGuard(h harness.Harness, hook string, reasonID drvreason.ID, strength lib.GuardStrength, outcome lib.EventOutcome, evidenceFiles ...string) {
|
|
65
|
+
root, slug, _, ok := resolveWorkspace()
|
|
66
|
+
if !ok {
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
ev := lib.NewEventV1(lib.BoundaryHookGuard, hook, reasonID)
|
|
70
|
+
ev.GuardStrength = strength
|
|
71
|
+
ev.Outcome = outcome
|
|
72
|
+
ev.Host = lib.EventHost(h)
|
|
73
|
+
if ev.Host != lib.HostClaude && ev.Host != lib.HostCodex {
|
|
74
|
+
ev.Host = lib.HostEngine
|
|
75
|
+
}
|
|
76
|
+
ev.EvidencePaths = lib.WorkspaceEvidencePaths(root, slug, evidenceFiles...)
|
|
77
|
+
if err := lib.BindEventWorkspace(root, slug, &ev); err != nil {
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
_ = lib.AppendEventV1(root, ev)
|
|
71
81
|
}
|
|
72
82
|
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// hookAUQ deterministically captures an AskUserQuestion exchange: every
|
|
78
|
-
// question with the user's chosen answer: into the session trace, so HITL
|
|
79
|
-
// decisions are recorded at the substrate rather than trusted to the model's
|
|
80
|
-
// bookkeeping. Capture only: it never tunes, blocks, or replies. Fail-open like
|
|
81
|
-
// every workspace hook: unparseable payload, unknown response shape, or no
|
|
82
|
-
// active workspace all exit 0 silently.
|
|
83
|
+
// hookAUQ records one metadata-only v1 event for each AskUserQuestion item after
|
|
84
|
+
// the tool returns. Prompts and answers remain in questions.md and are not
|
|
85
|
+
// copied into telemetry.
|
|
83
86
|
func hookAUQ(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
84
87
|
data, err := io.ReadAll(io.LimitReader(stdin, 1<<20))
|
|
85
88
|
if err != nil {
|
|
@@ -91,72 +94,43 @@ func hookAUQ(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
91
94
|
Question string `json:"question"`
|
|
92
95
|
} `json:"questions"`
|
|
93
96
|
} `json:"tool_input"`
|
|
94
|
-
ToolResponse json.RawMessage `json:"tool_response"`
|
|
95
97
|
}
|
|
96
98
|
if json.Unmarshal(data, &payload) != nil || len(payload.ToolInput.Questions) == 0 {
|
|
97
99
|
return exitOK
|
|
98
100
|
}
|
|
99
|
-
|
|
100
|
-
root, slug, dir, ok := resolveWorkspace()
|
|
101
|
+
root, slug, _, ok := resolveWorkspace()
|
|
101
102
|
if !ok {
|
|
102
103
|
return exitOK
|
|
103
104
|
}
|
|
104
|
-
|
|
105
|
-
for
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
continue
|
|
105
|
+
const maxObservedQuestions = 64
|
|
106
|
+
for i, q := range payload.ToolInput.Questions {
|
|
107
|
+
if i == maxObservedQuestions {
|
|
108
|
+
break
|
|
109
109
|
}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
Event: "auq",
|
|
113
|
-
Slug: slug,
|
|
114
|
-
Question: question,
|
|
115
|
-
Answer: truncateField(answers[q.Question], auqFieldMax),
|
|
110
|
+
if strings.TrimSpace(q.Question) == "" {
|
|
111
|
+
continue
|
|
116
112
|
}
|
|
117
|
-
|
|
118
|
-
|
|
113
|
+
event := lib.NewEventV1(
|
|
114
|
+
lib.BoundaryHookGuard,
|
|
115
|
+
"human-wait-resumed",
|
|
116
|
+
drvreason.HookStopUnsurfacedHumanGate,
|
|
117
|
+
)
|
|
118
|
+
event.GuardStrength = lib.GuardObserved
|
|
119
|
+
event.Outcome = lib.OutcomeObserved
|
|
120
|
+
event.Host = lib.HostClaude
|
|
121
|
+
if err := lib.BindEventWorkspace(root, slug, &event); err != nil {
|
|
119
122
|
continue
|
|
120
123
|
}
|
|
121
|
-
_ =
|
|
122
|
-
_ = state.AppendLog(filepath.Join(dir, "events.jsonl"), string(line))
|
|
124
|
+
_ = lib.AppendEventV1(root, event)
|
|
123
125
|
}
|
|
124
126
|
return exitOK
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
// string map, a bare string map, or anything else (→ no answers, questions
|
|
130
|
-
// still recorded).
|
|
131
|
-
func auqAnswers(raw json.RawMessage) map[string]string {
|
|
132
|
-
if len(raw) == 0 {
|
|
133
|
-
return nil
|
|
134
|
-
}
|
|
135
|
-
var wrapped struct {
|
|
136
|
-
Answers map[string]string `json:"answers"`
|
|
137
|
-
}
|
|
138
|
-
if json.Unmarshal(raw, &wrapped) == nil && len(wrapped.Answers) > 0 {
|
|
139
|
-
return wrapped.Answers
|
|
140
|
-
}
|
|
141
|
-
var flat map[string]string
|
|
142
|
-
if json.Unmarshal(raw, &flat) == nil {
|
|
143
|
-
return flat
|
|
144
|
-
}
|
|
145
|
-
return nil
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
func truncateField(s string, max int) string {
|
|
149
|
-
if len(s) <= max {
|
|
150
|
-
return s
|
|
151
|
-
}
|
|
152
|
-
return s[:max] + "…"
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// hookEvent records hook/session observability without changing flow. It is
|
|
156
|
-
// fail-open: outside an active workspace it stays silent.
|
|
129
|
+
// hookEvent records host lifecycle notifications as metadata-only v1 events. It
|
|
130
|
+
// does not change workflow state.
|
|
157
131
|
func hookEvent(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
158
132
|
_, _ = io.Copy(io.Discard, stdin)
|
|
159
|
-
event := "
|
|
133
|
+
event := ""
|
|
160
134
|
for _, arg := range args {
|
|
161
135
|
if strings.HasPrefix(arg, "--") {
|
|
162
136
|
continue
|
|
@@ -164,22 +138,31 @@ func hookEvent(args []string, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
164
138
|
event = arg
|
|
165
139
|
break
|
|
166
140
|
}
|
|
167
|
-
|
|
141
|
+
switch event {
|
|
142
|
+
case "session-end":
|
|
143
|
+
event = "session-ended"
|
|
144
|
+
case "subagent-stop":
|
|
145
|
+
event = "agent-finished"
|
|
146
|
+
default:
|
|
147
|
+
return exitOK
|
|
148
|
+
}
|
|
149
|
+
root, slug, _, ok := resolveWorkspace()
|
|
168
150
|
if !ok {
|
|
169
151
|
return exitOK
|
|
170
152
|
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
153
|
+
observation := lib.NewEventV1(lib.BoundaryAgentDispatch, event, drvreason.RootSelected)
|
|
154
|
+
observation.GuardStrength = lib.GuardObserved
|
|
155
|
+
observation.Outcome = lib.OutcomeObserved
|
|
156
|
+
observation.Host = lib.HostClaude
|
|
157
|
+
if err := lib.BindEventWorkspace(root, slug, &observation); err != nil {
|
|
174
158
|
return exitOK
|
|
175
159
|
}
|
|
176
|
-
_ =
|
|
177
|
-
_ = state.AppendLog(filepath.Join(dir, "events.jsonl"), string(line))
|
|
160
|
+
_ = lib.AppendEventV1(root, observation)
|
|
178
161
|
return exitOK
|
|
179
162
|
}
|
|
180
163
|
|
|
181
|
-
// hookHandoffSnapshot
|
|
182
|
-
//
|
|
164
|
+
// hookHandoffSnapshot appends a short resume note before context compaction. It
|
|
165
|
+
// does not rewrite canonical workspace artifacts.
|
|
183
166
|
func hookHandoffSnapshot(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
184
167
|
_, _ = io.Copy(io.Discard, stdin)
|
|
185
168
|
_, slug, dir, ok := resolveWorkspace()
|
|
@@ -209,10 +192,9 @@ func hookHandoffSnapshot(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
209
192
|
return exitOK
|
|
210
193
|
}
|
|
211
194
|
|
|
212
|
-
// hookCursor
|
|
213
|
-
//
|
|
214
|
-
//
|
|
215
|
-
// on a non-DevRites project.
|
|
195
|
+
// hookCursor adds the active feature cursor to each UserPromptSubmit event so the
|
|
196
|
+
// next action appears near the end of the current context. It keeps the plain
|
|
197
|
+
// text output and silent nonproject behavior of devrites-cursor.sh.
|
|
216
198
|
func hookCursor(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
217
199
|
_, _ = io.Copy(io.Discard, stdin)
|
|
218
200
|
root, slug, dir, ok := resolveWorkspace()
|
|
@@ -246,9 +228,9 @@ func hookCursor(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
246
228
|
return exitOK
|
|
247
229
|
}
|
|
248
230
|
|
|
249
|
-
// hookStatusline
|
|
250
|
-
//
|
|
251
|
-
//
|
|
231
|
+
// hookStatusline prints the one-line workspace status configured by settings.json.
|
|
232
|
+
// It reads and ignores the session JSON on stdin, matching
|
|
233
|
+
// devrites-statusline.sh, and stays silent without an active workspace.
|
|
252
234
|
func hookStatusline(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
253
235
|
_, _ = io.Copy(io.Discard, stdin)
|
|
254
236
|
root, slug, dir, ok := resolveWorkspace()
|
|
@@ -268,12 +250,16 @@ func hookStatusline(stdin io.Reader, stdout, stderr io.Writer) int {
|
|
|
268
250
|
if wsIsFile(filepath.Join(dir, ".red")) {
|
|
269
251
|
red = " · RED"
|
|
270
252
|
}
|
|
271
|
-
|
|
253
|
+
obs := lib.ObservabilityStatus(root, slug)
|
|
254
|
+
if obs != "" {
|
|
255
|
+
obs = " · " + obs
|
|
256
|
+
}
|
|
257
|
+
fmt.Fprintf(stdout, "DevRites: %s · %s · gates:%s · %s%s%s\n", slug, phase, gates, mode, red, obs)
|
|
272
258
|
return exitOK
|
|
273
259
|
}
|
|
274
260
|
|
|
275
|
-
// redwatch detection patterns
|
|
276
|
-
//
|
|
261
|
+
// redwatch detection patterns match devrites-redwatch.sh. They are case
|
|
262
|
+
// insensitive, and FAIL/PASS scans are line oriented.
|
|
277
263
|
// pack-scan-ignore: these are the hook's own red/green heuristics, not a payload.
|
|
278
264
|
var (
|
|
279
265
|
redTestCmdRe = regexp.MustCompile(`(?i)(npm|pnpm|yarn|bun)([[:space:]]+run)?[[:space:]]+(test|build|lint|typecheck|check)|jest|vitest|pytest|go[[:space:]]+test|cargo[[:space:]]+(test|build|clippy)|\bmvn\b|gradle|eslint|ruff|mypy|\btsc\b|\bmake[[:space:]]+(test|build|check)`)
|
|
@@ -283,10 +269,10 @@ var (
|
|
|
283
269
|
|
|
284
270
|
const redReasonFmt = "DevRites: tests/build are RED (%s). Fix to green or record the failure + next step in state.md before stopping: the Stop gate blocks an end-of-turn while .red is set."
|
|
285
271
|
|
|
286
|
-
// hookRedwatch
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
//
|
|
272
|
+
// hookRedwatch handles PostToolUse Bash events for test, build, lint, and type
|
|
273
|
+
// checking commands. It sets or clears <featureDir>/.red so the Stop gate can
|
|
274
|
+
// catch a known failing suite. Its command and output checks match
|
|
275
|
+
// devrites-redwatch.sh, and uncertain input leaves state unchanged.
|
|
290
276
|
func hookRedwatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
291
277
|
data, err := io.ReadAll(stdin)
|
|
292
278
|
if err != nil {
|
|
@@ -303,7 +289,7 @@ func hookRedwatch(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer)
|
|
|
303
289
|
if in.ToolName != "Bash" {
|
|
304
290
|
return exitOK
|
|
305
291
|
}
|
|
306
|
-
//
|
|
292
|
+
// Ignore commands unrelated to tests, builds, linting, or type checking.
|
|
307
293
|
if !redTestCmdRe.MatchString(in.Command) {
|
|
308
294
|
return exitOK
|
|
309
295
|
}
|
|
@@ -339,11 +325,13 @@ func redwatchSafe(cmd string) string {
|
|
|
339
325
|
// node regex the shell guards use.
|
|
340
326
|
var patchPathRe = regexp.MustCompile(`(?m)^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$`)
|
|
341
327
|
|
|
342
|
-
// isEditTool reports whether a tool can write source
|
|
343
|
-
//
|
|
328
|
+
// isEditTool reports whether a tool can write source. toolBaseName also handles
|
|
329
|
+
// host-qualified names such as functions.apply_patch and MCP symbol editors.
|
|
344
330
|
func isEditTool(tool string) bool {
|
|
345
|
-
switch tool {
|
|
346
|
-
case "
|
|
331
|
+
switch toolBaseName(tool) {
|
|
332
|
+
case "edit", "write", "multiedit", "multi_edit", "notebookedit", "notebook_edit",
|
|
333
|
+
"apply_patch", "applypatch", "edit_file", "write_file", "create_file", "delete_file",
|
|
334
|
+
"replace_symbol_body", "insert_before_symbol", "insert_after_symbol", "safe_delete_symbol":
|
|
347
335
|
return true
|
|
348
336
|
}
|
|
349
337
|
return false
|
|
@@ -353,24 +341,118 @@ func isEditTool(tool string) bool {
|
|
|
353
341
|
func patchPaths(command string) []string {
|
|
354
342
|
var paths []string
|
|
355
343
|
for _, m := range patchPathRe.FindAllStringSubmatch(command, -1) {
|
|
356
|
-
paths = append(paths, m[1])
|
|
344
|
+
paths = append(paths, strings.TrimSpace(m[1]))
|
|
357
345
|
}
|
|
358
346
|
return paths
|
|
359
347
|
}
|
|
360
348
|
|
|
361
|
-
//
|
|
362
|
-
//
|
|
349
|
+
// shellOutputRedirections removes harmless fd redirects and returns every file
|
|
350
|
+
// target of >, >>, or >|. Dynamic targets are opaque and therefore unsafe.
|
|
351
|
+
func shellOutputRedirections(command string) (sanitized string, paths []string, opaque bool) {
|
|
352
|
+
buf := []byte(command)
|
|
353
|
+
sanitizedBytes := append([]byte(nil), buf...)
|
|
354
|
+
var quote byte
|
|
355
|
+
for i := 0; i < len(buf); i++ {
|
|
356
|
+
switch {
|
|
357
|
+
case quote != 0:
|
|
358
|
+
if buf[i] == '\\' && quote == '"' {
|
|
359
|
+
i++
|
|
360
|
+
} else if buf[i] == quote {
|
|
361
|
+
quote = 0
|
|
362
|
+
}
|
|
363
|
+
continue
|
|
364
|
+
case buf[i] == '\'' || buf[i] == '"':
|
|
365
|
+
quote = buf[i]
|
|
366
|
+
continue
|
|
367
|
+
case buf[i] != '>':
|
|
368
|
+
continue
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
start := i
|
|
372
|
+
for start > 0 && buf[start-1] >= '0' && buf[start-1] <= '9' {
|
|
373
|
+
start--
|
|
374
|
+
}
|
|
375
|
+
i++
|
|
376
|
+
if i < len(buf) && (buf[i] == '>' || buf[i] == '|') {
|
|
377
|
+
i++
|
|
378
|
+
}
|
|
379
|
+
for i < len(buf) && (buf[i] == ' ' || buf[i] == '\t') {
|
|
380
|
+
i++
|
|
381
|
+
}
|
|
382
|
+
if i < len(buf) && buf[i] == '&' {
|
|
383
|
+
i++
|
|
384
|
+
for i < len(buf) && buf[i] >= '0' && buf[i] <= '9' {
|
|
385
|
+
i++
|
|
386
|
+
}
|
|
387
|
+
for j := start; j < i; j++ {
|
|
388
|
+
sanitizedBytes[j] = ' '
|
|
389
|
+
}
|
|
390
|
+
i--
|
|
391
|
+
continue
|
|
392
|
+
}
|
|
393
|
+
if i >= len(buf) || buf[i] == '(' {
|
|
394
|
+
return "", paths, true
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
wordStart := i
|
|
398
|
+
var wordQuote byte
|
|
399
|
+
for i < len(buf) {
|
|
400
|
+
c := buf[i]
|
|
401
|
+
if wordQuote != 0 {
|
|
402
|
+
if c == '\\' && wordQuote == '"' {
|
|
403
|
+
i += 2
|
|
404
|
+
continue
|
|
405
|
+
}
|
|
406
|
+
if c == wordQuote {
|
|
407
|
+
wordQuote = 0
|
|
408
|
+
}
|
|
409
|
+
i++
|
|
410
|
+
continue
|
|
411
|
+
}
|
|
412
|
+
if c == '\'' || c == '"' {
|
|
413
|
+
wordQuote = c
|
|
414
|
+
i++
|
|
415
|
+
continue
|
|
416
|
+
}
|
|
417
|
+
if c == ' ' || c == '\t' || c == '\r' || c == '\n' || c == ';' || c == '|' || c == '&' {
|
|
418
|
+
break
|
|
419
|
+
}
|
|
420
|
+
i++
|
|
421
|
+
}
|
|
422
|
+
if wordQuote != 0 {
|
|
423
|
+
return "", paths, true
|
|
424
|
+
}
|
|
425
|
+
target := strings.Trim(string(buf[wordStart:i]), `"'`)
|
|
426
|
+
if target == "" || strings.ContainsAny(target, "$`*?[]{}()") {
|
|
427
|
+
return "", paths, true
|
|
428
|
+
}
|
|
429
|
+
if target != "/dev/null" {
|
|
430
|
+
paths = append(paths, target)
|
|
431
|
+
}
|
|
432
|
+
for j := start; j < i; j++ {
|
|
433
|
+
sanitizedBytes[j] = ' '
|
|
434
|
+
}
|
|
435
|
+
i--
|
|
436
|
+
}
|
|
437
|
+
return string(sanitizedBytes), paths, false
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// underDevrites reports whether an absolute path is inside the .devrites root.
|
|
363
441
|
func underDevrites(abs, root string) bool {
|
|
364
|
-
return
|
|
442
|
+
return pathWithin(abs, root)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func pathWithin(path, parent string) bool {
|
|
446
|
+
path = filepath.Clean(path)
|
|
447
|
+
parent = filepath.Clean(parent)
|
|
448
|
+
return path == parent || strings.HasPrefix(path, parent+string(filepath.Separator))
|
|
365
449
|
}
|
|
366
450
|
|
|
367
451
|
const a1DenyReason = "DevRites A1: the orchestrator must not edit source mid-build: the slice-wright is the only writer. Re-dispatch the wright (continue it once) or stop + escalate; do not patch the code yourself. (devrites-a1-guard)"
|
|
368
452
|
|
|
369
|
-
// hookA1Guard
|
|
370
|
-
//
|
|
371
|
-
//
|
|
372
|
-
// default (logs to .a1-guard.log), blocking only when the build window is open, the
|
|
373
|
-
// edit is source from the MAIN thread, and enforce mode is on.
|
|
453
|
+
// hookA1Guard prevents /rite-build from editing source while slice-wright owns an
|
|
454
|
+
// open build window. It matches devrites-a1-guard.sh by logging observations to
|
|
455
|
+
// .a1-guard.log unless enforcement is enabled.
|
|
374
456
|
func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
375
457
|
data, err := io.ReadAll(stdin)
|
|
376
458
|
if err != nil {
|
|
@@ -383,8 +465,8 @@ func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) i
|
|
|
383
465
|
if !ok {
|
|
384
466
|
return exitOK
|
|
385
467
|
}
|
|
386
|
-
//
|
|
387
|
-
//
|
|
468
|
+
// The guard is active between a reconcile snapshot and its check. Inline
|
|
469
|
+
// fallback makes the orchestrator the expected writer.
|
|
388
470
|
if !wsIsFile(filepath.Join(dir, ".reconcile-base")) {
|
|
389
471
|
return exitOK
|
|
390
472
|
}
|
|
@@ -396,7 +478,7 @@ func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) i
|
|
|
396
478
|
if !isEditTool(in.ToolName) {
|
|
397
479
|
return exitOK
|
|
398
480
|
}
|
|
399
|
-
//
|
|
481
|
+
// The wright may edit. Claude sends agent_id; Codex sends agent_type.
|
|
400
482
|
if in.AgentID != "" || in.AgentType != "" {
|
|
401
483
|
return exitOK
|
|
402
484
|
}
|
|
@@ -445,18 +527,32 @@ func hookA1Guard(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) i
|
|
|
445
527
|
return exitOK
|
|
446
528
|
}
|
|
447
529
|
fmt.Fprintln(stdout, out)
|
|
530
|
+
recordHookGuard(h, "a1-guard", drvreason.HookA1Denied, lib.GuardEnforced, lib.OutcomeDenied, ".reconcile-base")
|
|
448
531
|
return exitOK
|
|
449
532
|
}
|
|
450
533
|
_ = state.AppendLog(filepath.Join(dir, ".a1-guard.log"), "WOULD-BLOCK\t"+in.ToolName+"\t"+target)
|
|
534
|
+
recordHookGuard(h, "a1-guard", drvreason.HookA1Observed, lib.GuardObserved, lib.OutcomeObserved, ".reconcile-base")
|
|
451
535
|
return exitOK
|
|
452
536
|
}
|
|
453
537
|
|
|
454
|
-
const
|
|
538
|
+
const (
|
|
539
|
+
wrightDenyReason = "DevRites scope: this write is not in the orchestrator-provided exact wright allowlist, or it targets .devrites. Build only the slice contract; return the blocked path to the orchestrator instead of widening scope. (devrites-wright-scope)"
|
|
540
|
+
wrightForbiddenReason = "DevRites scope: slice-wrights cannot dispatch nested agents, commit/push, install dependencies, run live migrations/deployments, or execute an uninspectable write path. (devrites-wright-scope)"
|
|
541
|
+
forgeWrightDenyReason = "DevRites Forge scope: this candidate does not match its root-owned manifest, physical worktree, Git identity, branch, worker identity, or live process token. Return to the orchestrator; never widen or guess Forge ownership. (devrites-forge-wright-scope)"
|
|
542
|
+
wrightAllowlistEnvName = "DEVRITES_WRIGHT_ALLOWLIST_FILE"
|
|
543
|
+
forgeRunEnvName = "DEVRITES_FORGE_RUN_ID"
|
|
544
|
+
forgeCandidateEnvName = "DEVRITES_FORGE_CANDIDATE"
|
|
545
|
+
forgeWorkerEnvName = "DEVRITES_FORGE_WORKER_ID"
|
|
546
|
+
forgePIDEnvName = "DEVRITES_FORGE_WORKER_PID"
|
|
547
|
+
forgeProcessEnvName = "DEVRITES_FORGE_PROCESS_START"
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
var wrightForbiddenShellRe = regexp.MustCompile(`(?im)\bgit[[:space:]]+(add|commit|push|reset|checkout|restore|clean|rm|mv|stash|tag|apply|merge|rebase|cherry-pick|am)\b|\b(npm|pnpm|yarn)[[:space:]]+(install|i|ci|add|publish)\b|\b(pip|pip3)[[:space:]]+install\b|\b(poetry|uv)[[:space:]]+(add|install)\b|\b(bundle|composer)[[:space:]]+(install|update)\b|\bgo[[:space:]]+(get|install|generate)\b|\bcargo[[:space:]]+(add|install|publish)\b|\b(devrites-engine[[:space:]]+migrate|alembic[[:space:]]+(upgrade|downgrade)|prisma[[:space:]]+migrate|flyway[[:space:]]+migrate|liquibase[[:space:]]+update|manage\.py[[:space:]]+migrate|(rails|rake)[[:space:]]+db:(migrate|rollback|seed)|sequelize[[:space:]]+db:migrate|knex[[:space:]]+migrate:latest)\b|\b(terraform[[:space:]]+(apply|destroy)|kubectl[[:space:]]+(apply|create|delete|patch|replace)|helm[[:space:]]+(install|upgrade|uninstall)|docker[[:space:]]+push)\b|\b(curl|wget|scp|ssh|nc|socat|telnet)\b|\b(codex|claude)[[:space:]]+(exec|agent)\b`)
|
|
551
|
+
|
|
552
|
+
var opaqueInterpreterRe = regexp.MustCompile(`(?im)(^|[;&|[:space:]])(python[0-9.]*[[:space:]]+(-c|-)|node[[:space:]]+(-e|--)|ruby[[:space:]]+-e|perl[[:space:]]+(-e|-p|-i)|[.][/][^[:space:]]+\.(sh|py|js|rb))([;&|[:space:]]|$)`)
|
|
455
553
|
|
|
456
|
-
// hookWrightScope
|
|
457
|
-
// path
|
|
458
|
-
// devrites-wright-scope.sh onto the new schema: FAIL-OPEN, OBSERVE by default (logs
|
|
459
|
-
// to .wright-scope.log), enforced with DEVRITES_WRIGHT_SCOPE=enforce.
|
|
554
|
+
// hookWrightScope restricts the write-capable leaf to the orchestrator's exact
|
|
555
|
+
// path allowlist. It always enforces active DevRites runs.
|
|
460
556
|
func hookWrightScope(h harness.Harness, stdin io.Reader, stdout, stderr io.Writer) int {
|
|
461
557
|
data, err := io.ReadAll(stdin)
|
|
462
558
|
if err != nil {
|
|
@@ -465,68 +561,453 @@ func hookWrightScope(h harness.Harness, stdin io.Reader, stdout, stderr io.Write
|
|
|
465
561
|
if !bytes.Contains(data, []byte(`"tool_name"`)) {
|
|
466
562
|
return exitOK
|
|
467
563
|
}
|
|
468
|
-
|
|
469
|
-
|
|
564
|
+
in := h.ParseGuardInput(bytes.NewReader(data))
|
|
565
|
+
kind := devritesAgent(in.AgentType)
|
|
566
|
+
forgeDeclared := forgeWrightDeclared()
|
|
567
|
+
active := kind != devritesAgentNone
|
|
568
|
+
if forgeDeclared && kind != devritesAgentWright {
|
|
569
|
+
return denyWright(h, stdout, stderr, forgeWrightDenyReason, drvreason.HookForgeBindingDenied)
|
|
570
|
+
}
|
|
571
|
+
if kind == devritesAgentNone && (in.AgentType != "" || os.Getenv("DEVRITES_WRIGHT_AGENT_REQUIRED") == "1") {
|
|
470
572
|
return exitOK
|
|
471
573
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
574
|
+
if kind == devritesAgentReadonly {
|
|
575
|
+
return exitOK // reviewer-readonly owns every non-wright leaf
|
|
576
|
+
}
|
|
577
|
+
if kind == devritesAgentInvalid {
|
|
578
|
+
if !isEditTool(in.ToolName) && !isShellTool(in.ToolName) &&
|
|
579
|
+
!isOpaqueExecutionTool(in.ToolName) && !isAgentDispatchTool(in.ToolName) {
|
|
580
|
+
return exitOK
|
|
581
|
+
}
|
|
582
|
+
return denyWright(h, stdout, stderr, reviewerReadonlyDenyReason, drvreason.HookReviewerReadonlyDenied)
|
|
475
583
|
}
|
|
476
|
-
touched := string(tfData)
|
|
477
584
|
|
|
478
|
-
|
|
479
|
-
|
|
585
|
+
var targets []string
|
|
586
|
+
forbidden := isAgentDispatchTool(in.ToolName) || isOpaqueExecutionTool(in.ToolName)
|
|
587
|
+
switch {
|
|
588
|
+
case forbidden:
|
|
589
|
+
case isEditTool(in.ToolName):
|
|
590
|
+
targets = mutationToolPaths(data, in)
|
|
591
|
+
if len(targets) == 0 {
|
|
592
|
+
forbidden = true
|
|
593
|
+
}
|
|
594
|
+
case isShellTool(in.ToolName):
|
|
595
|
+
targets, forbidden = wrightShellWritePaths(in.Command)
|
|
596
|
+
if !forbidden && len(targets) == 0 {
|
|
597
|
+
return exitOK
|
|
598
|
+
}
|
|
599
|
+
default:
|
|
480
600
|
return exitOK
|
|
481
601
|
}
|
|
482
|
-
|
|
602
|
+
|
|
603
|
+
root, slug, dir, ok := resolveWorkspace()
|
|
604
|
+
if !ok {
|
|
605
|
+
if kind == devritesAgentWright {
|
|
606
|
+
return denyWright(h, stdout, stderr, wrightForbiddenReason, drvreason.HookWrightForbiddenDenied)
|
|
607
|
+
}
|
|
483
608
|
return exitOK
|
|
484
609
|
}
|
|
485
610
|
|
|
611
|
+
if wrightForbiddenShellRe.MatchString(in.Command) ||
|
|
612
|
+
(opaqueInterpreterRe.MatchString(in.Command) && !safeReadonlyShellCommand(in.Command)) {
|
|
613
|
+
forbidden = true
|
|
614
|
+
}
|
|
615
|
+
if forbidden {
|
|
616
|
+
return denyOrObserveWright(h, stdout, stderr, dir, active, wrightForbiddenReason, drvreason.HookWrightForbiddenDenied, []string{"opaque/forbidden operation"})
|
|
617
|
+
}
|
|
618
|
+
|
|
486
619
|
projectDir := filepath.Dir(root)
|
|
620
|
+
if !forgeDeclared && kind == devritesAgentWright && insideForgeStaging(projectDir) {
|
|
621
|
+
return denyOrObserveWright(h, stdout, stderr, dir, true, forgeWrightDenyReason, drvreason.HookForgeBindingDenied, []string{"missing Forge binding"})
|
|
622
|
+
}
|
|
623
|
+
if forgeDeclared {
|
|
624
|
+
projectDir, err = forgeWrightProjectDir(projectDir, slug, in)
|
|
625
|
+
if err != nil {
|
|
626
|
+
debugf(stderr, "wright-scope Forge binding: %v", err)
|
|
627
|
+
return denyOrObserveWright(h, stdout, stderr, dir, true, forgeWrightDenyReason, drvreason.HookForgeBindingDenied, []string{"invalid Forge binding"})
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
allow, err := readWrightAllowlist(projectDir, root, dir)
|
|
631
|
+
if err != nil {
|
|
632
|
+
debugf(stderr, "wright-scope allowlist: %v", err)
|
|
633
|
+
return denyOrObserveWright(h, stdout, stderr, dir, active, wrightDenyReason, drvreason.HookWrightScopeDenied, []string{"invalid/missing allowlist"})
|
|
634
|
+
}
|
|
487
635
|
var bad []string
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
636
|
+
for _, target := range targets {
|
|
637
|
+
rel, err := normalizeProjectPath(projectDir, root, target, false)
|
|
638
|
+
if err != nil {
|
|
639
|
+
bad = append(bad, target)
|
|
640
|
+
continue
|
|
641
|
+
}
|
|
642
|
+
if _, ok := allow[rel]; !ok {
|
|
643
|
+
bad = append(bad, target)
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if len(bad) == 0 {
|
|
647
|
+
return exitOK
|
|
648
|
+
}
|
|
649
|
+
return denyOrObserveWright(h, stdout, stderr, dir, active, wrightDenyReason, drvreason.HookWrightScopeDenied, bad)
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
func forgeWrightDeclared() bool {
|
|
653
|
+
for _, name := range []string{
|
|
654
|
+
forgeRunEnvName,
|
|
655
|
+
forgeCandidateEnvName,
|
|
656
|
+
forgeWorkerEnvName,
|
|
657
|
+
forgePIDEnvName,
|
|
658
|
+
forgeProcessEnvName,
|
|
659
|
+
} {
|
|
660
|
+
if os.Getenv(name) != "" {
|
|
661
|
+
return true
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
func insideForgeStaging(primaryProject string) bool {
|
|
668
|
+
cwd, err := physicalWorkingDir()
|
|
669
|
+
if err != nil {
|
|
670
|
+
return false
|
|
671
|
+
}
|
|
672
|
+
primary, err := filepath.Abs(primaryProject)
|
|
673
|
+
if err != nil {
|
|
674
|
+
return false
|
|
675
|
+
}
|
|
676
|
+
primary, err = filepath.EvalSymlinks(primary)
|
|
677
|
+
if err != nil {
|
|
678
|
+
return false
|
|
679
|
+
}
|
|
680
|
+
staging := filepath.Join(filepath.Dir(primary), "."+filepath.Base(primary)+".devrites-forge")
|
|
681
|
+
rel, err := filepath.Rel(staging, cwd)
|
|
682
|
+
return err == nil && rel != ".." && !strings.HasPrefix(rel, ".."+string(filepath.Separator))
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
func forgeWrightProjectDir(primaryProject, activeSlug string, in harness.GuardInput) (string, error) {
|
|
686
|
+
values := map[string]string{
|
|
687
|
+
forgeRunEnvName: os.Getenv(forgeRunEnvName),
|
|
688
|
+
forgeCandidateEnvName: os.Getenv(forgeCandidateEnvName),
|
|
689
|
+
forgeWorkerEnvName: os.Getenv(forgeWorkerEnvName),
|
|
690
|
+
forgePIDEnvName: os.Getenv(forgePIDEnvName),
|
|
691
|
+
forgeProcessEnvName: os.Getenv(forgeProcessEnvName),
|
|
692
|
+
}
|
|
693
|
+
for name, value := range values {
|
|
694
|
+
if value == "" || strings.TrimSpace(value) != value {
|
|
695
|
+
return "", fmt.Errorf("%s must be present and exact", name)
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
manifest, _, err := forge.Load(primaryProject, values[forgeRunEnvName])
|
|
700
|
+
if err != nil {
|
|
701
|
+
return "", err
|
|
702
|
+
}
|
|
703
|
+
if manifest.FeatureSlug != activeSlug {
|
|
704
|
+
return "", fmt.Errorf("forge run belongs to feature %q, not active feature %q", manifest.FeatureSlug, activeSlug)
|
|
705
|
+
}
|
|
706
|
+
candidate, err := manifest.Candidate(forge.CandidateID(values[forgeCandidateEnvName]))
|
|
707
|
+
if err != nil {
|
|
708
|
+
return "", err
|
|
709
|
+
}
|
|
710
|
+
if candidate.State != forge.StateRunning {
|
|
711
|
+
return "", fmt.Errorf("forge candidate %s is %s, not running", candidate.ID, candidate.State)
|
|
712
|
+
}
|
|
713
|
+
pid, err := strconv.Atoi(values[forgePIDEnvName])
|
|
714
|
+
if err != nil || strconv.Itoa(pid) != values[forgePIDEnvName] {
|
|
715
|
+
return "", fmt.Errorf("%s must be a canonical positive PID", forgePIDEnvName)
|
|
716
|
+
}
|
|
717
|
+
if candidate.Worker.ID != values[forgeWorkerEnvName] ||
|
|
718
|
+
candidate.Worker.PID != pid ||
|
|
719
|
+
candidate.Worker.ProcessStart != values[forgeProcessEnvName] {
|
|
720
|
+
return "", fmt.Errorf("forge worker identity does not match the manifest")
|
|
721
|
+
}
|
|
722
|
+
if in.AgentID != "" && in.AgentID != candidate.Worker.ID {
|
|
723
|
+
return "", fmt.Errorf("hook agent identity does not match Forge worker %q", candidate.Worker.ID)
|
|
724
|
+
}
|
|
725
|
+
liveToken, err := forge.ProcessStartToken(candidate.Worker.PID)
|
|
726
|
+
if err != nil || liveToken != candidate.Worker.ProcessStart {
|
|
727
|
+
return "", fmt.Errorf("forge worker liveness token is no longer valid")
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
cwd, err := physicalWorkingDir()
|
|
731
|
+
if err != nil {
|
|
732
|
+
return "", err
|
|
733
|
+
}
|
|
734
|
+
if filepath.Clean(cwd) != filepath.Clean(candidate.Worktree) {
|
|
735
|
+
return "", fmt.Errorf("physical cwd %q is not candidate %s worktree %q", cwd, candidate.ID, candidate.Worktree)
|
|
736
|
+
}
|
|
737
|
+
facts, _ := rootfacts.ResolveFrom(cwd, "")
|
|
738
|
+
if facts.Git.TopLevel != candidate.Worktree || facts.Git.CommonDir != manifest.GitCommonDir {
|
|
739
|
+
return "", fmt.Errorf("forge candidate Git identity does not match the manifest")
|
|
740
|
+
}
|
|
741
|
+
cmd := exec.Command("git", "-C", cwd, "symbolic-ref", "--quiet", "--short", "HEAD")
|
|
742
|
+
cmd.Env = append(os.Environ(), "GIT_TERMINAL_PROMPT=0", "LC_ALL=C")
|
|
743
|
+
branch, err := cmd.Output()
|
|
744
|
+
if err != nil || strings.TrimSpace(string(branch)) != candidate.Branch {
|
|
745
|
+
return "", fmt.Errorf("forge candidate branch does not match the manifest")
|
|
746
|
+
}
|
|
747
|
+
return candidate.Worktree, nil
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
func physicalWorkingDir() (string, error) {
|
|
751
|
+
cwd, err := os.Getwd()
|
|
752
|
+
if err != nil {
|
|
753
|
+
return "", err
|
|
754
|
+
}
|
|
755
|
+
cwd, err = filepath.Abs(cwd)
|
|
756
|
+
if err != nil {
|
|
757
|
+
return "", err
|
|
758
|
+
}
|
|
759
|
+
return filepath.EvalSymlinks(cwd)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
func mutationToolPaths(data []byte, in harness.GuardInput) []string {
|
|
763
|
+
var envelope struct {
|
|
764
|
+
ToolInput any `json:"tool_input"`
|
|
765
|
+
}
|
|
766
|
+
if json.Unmarshal(data, &envelope) != nil {
|
|
767
|
+
return nil
|
|
768
|
+
}
|
|
769
|
+
var paths []string
|
|
770
|
+
if toolBaseName(in.ToolName) == "apply_patch" || toolBaseName(in.ToolName) == "applypatch" {
|
|
771
|
+
collectPatchMutationPaths(envelope.ToolInput, &paths)
|
|
772
|
+
paths = append(paths, patchPaths(in.Command)...)
|
|
773
|
+
return uniqueStrings(paths)
|
|
774
|
+
}
|
|
775
|
+
collectMutationPaths(envelope.ToolInput, &paths)
|
|
776
|
+
if len(paths) == 0 && in.FilePath != "" {
|
|
777
|
+
paths = append(paths, in.FilePath)
|
|
778
|
+
}
|
|
779
|
+
return uniqueStrings(paths)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
func collectPatchMutationPaths(value any, paths *[]string) {
|
|
783
|
+
switch value := value.(type) {
|
|
784
|
+
case string:
|
|
785
|
+
*paths = append(*paths, patchPaths(value)...)
|
|
786
|
+
case map[string]any:
|
|
787
|
+
for _, child := range value {
|
|
788
|
+
collectPatchMutationPaths(child, paths)
|
|
789
|
+
}
|
|
790
|
+
case []any:
|
|
791
|
+
for _, child := range value {
|
|
792
|
+
collectPatchMutationPaths(child, paths)
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
func collectMutationPaths(value any, paths *[]string) {
|
|
798
|
+
switch value := value.(type) {
|
|
799
|
+
case map[string]any:
|
|
800
|
+
for key, child := range value {
|
|
801
|
+
normalizedKey := strings.ReplaceAll(strings.ToLower(key), "-", "_")
|
|
802
|
+
switch normalizedKey {
|
|
803
|
+
case "file_path", "filepath", "path", "relative_path", "notebook_path", "new_path", "target_path":
|
|
804
|
+
if path, ok := child.(string); ok && strings.TrimSpace(path) != "" {
|
|
805
|
+
*paths = append(*paths, strings.TrimSpace(path))
|
|
806
|
+
continue
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
collectMutationPaths(child, paths)
|
|
810
|
+
}
|
|
811
|
+
case []any:
|
|
812
|
+
for _, child := range value {
|
|
813
|
+
collectMutationPaths(child, paths)
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
func uniqueStrings(values []string) []string {
|
|
819
|
+
seen := make(map[string]bool, len(values))
|
|
820
|
+
out := make([]string, 0, len(values))
|
|
821
|
+
for _, value := range values {
|
|
822
|
+
if !seen[value] {
|
|
823
|
+
seen[value] = true
|
|
824
|
+
out = append(out, value)
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
return out
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
func wrightShellWritePaths(command string) ([]string, bool) {
|
|
831
|
+
if strings.TrimSpace(command) == "" || wrightForbiddenShellRe.MatchString(command) {
|
|
832
|
+
return nil, true
|
|
833
|
+
}
|
|
834
|
+
sanitized, paths, opaque := shellOutputRedirections(command)
|
|
835
|
+
if opaque {
|
|
836
|
+
return nil, true
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
patches := patchPaths(command)
|
|
840
|
+
if strings.Contains(command, "apply_patch") {
|
|
841
|
+
if len(patches) == 0 {
|
|
842
|
+
return nil, true
|
|
843
|
+
}
|
|
844
|
+
paths = append(paths, patches...)
|
|
845
|
+
sanitized = strings.ReplaceAll(sanitized, "apply_patch", "true")
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
for _, segment := range shellSegmentRe.Split(sanitized, -1) {
|
|
849
|
+
if safeReadonlyShellSegment(segment) {
|
|
850
|
+
continue
|
|
851
|
+
}
|
|
852
|
+
fields := strings.Fields(strings.TrimSpace(segment))
|
|
853
|
+
if len(fields) == 0 {
|
|
854
|
+
continue
|
|
855
|
+
}
|
|
856
|
+
for len(fields) > 0 && filepath.Base(fields[0]) == "rtk" {
|
|
857
|
+
fields = fields[1:]
|
|
858
|
+
}
|
|
859
|
+
if len(fields) == 0 {
|
|
860
|
+
return nil, true
|
|
861
|
+
}
|
|
862
|
+
base := strings.ToLower(filepath.Base(strings.Trim(fields[0], `"'`)))
|
|
863
|
+
args := fields[1:]
|
|
864
|
+
var found []string
|
|
865
|
+
switch base {
|
|
866
|
+
case "gofmt", "goimports", "shfmt":
|
|
867
|
+
if !hasAnyArg(args, "-w") {
|
|
868
|
+
return nil, true
|
|
869
|
+
}
|
|
870
|
+
found = nonFlagArgs(args)
|
|
871
|
+
case "prettier":
|
|
872
|
+
if !hasAnyArg(args, "--write") {
|
|
873
|
+
return nil, true
|
|
492
874
|
}
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
875
|
+
found = nonFlagArgs(args)
|
|
876
|
+
case "ruff":
|
|
877
|
+
if len(args) == 0 || args[0] != "format" || hasAnyArg(args, "--check") {
|
|
878
|
+
return nil, true
|
|
496
879
|
}
|
|
880
|
+
found = nonFlagArgs(args[1:])
|
|
881
|
+
case "black", "rustfmt":
|
|
882
|
+
found = nonFlagArgs(args)
|
|
883
|
+
case "rm", "unlink", "touch", "mkdir", "rmdir", "truncate":
|
|
884
|
+
found = nonFlagArgs(args)
|
|
885
|
+
default:
|
|
886
|
+
return nil, true
|
|
497
887
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
return exitOK
|
|
888
|
+
if len(found) == 0 {
|
|
889
|
+
return nil, true
|
|
501
890
|
}
|
|
502
|
-
|
|
503
|
-
|
|
891
|
+
for _, path := range found {
|
|
892
|
+
if strings.ContainsAny(path, "$`*?[]{}()") {
|
|
893
|
+
return nil, true
|
|
894
|
+
}
|
|
504
895
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
896
|
+
paths = append(paths, found...)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
if len(paths) == 0 {
|
|
900
|
+
if safeReadonlyShellCommand(command) {
|
|
901
|
+
return nil, false
|
|
508
902
|
}
|
|
903
|
+
return nil, true
|
|
509
904
|
}
|
|
510
|
-
|
|
511
|
-
|
|
905
|
+
return uniqueStrings(paths), false
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
func nonFlagArgs(args []string) []string {
|
|
909
|
+
var out []string
|
|
910
|
+
for _, arg := range args {
|
|
911
|
+
arg = strings.Trim(arg, `"'`)
|
|
912
|
+
if arg == "" || strings.HasPrefix(arg, "-") {
|
|
913
|
+
continue
|
|
914
|
+
}
|
|
915
|
+
out = append(out, arg)
|
|
512
916
|
}
|
|
917
|
+
return out
|
|
918
|
+
}
|
|
513
919
|
|
|
514
|
-
|
|
515
|
-
|
|
920
|
+
func readWrightAllowlist(projectDir, devritesRoot, workspaceDir string) (map[string]struct{}, error) {
|
|
921
|
+
path := strings.TrimSpace(os.Getenv(wrightAllowlistEnvName))
|
|
922
|
+
if path == "" {
|
|
923
|
+
path = filepath.Join(workspaceDir, ".wright-allowlist")
|
|
924
|
+
}
|
|
925
|
+
if !filepath.IsAbs(path) {
|
|
926
|
+
path = filepath.Join(filepath.Dir(devritesRoot), path)
|
|
927
|
+
}
|
|
928
|
+
path = filepath.Clean(path)
|
|
929
|
+
if !safepath.WithinResolved(path, workspaceDir) {
|
|
930
|
+
return nil, fmt.Errorf("allowlist file is outside the active workspace")
|
|
931
|
+
}
|
|
932
|
+
data, err := os.ReadFile(path)
|
|
933
|
+
if err != nil {
|
|
934
|
+
return nil, err
|
|
935
|
+
}
|
|
936
|
+
allow := make(map[string]struct{})
|
|
937
|
+
for _, line := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") {
|
|
938
|
+
if line == "" {
|
|
939
|
+
continue
|
|
940
|
+
}
|
|
941
|
+
if strings.TrimSpace(line) != line || strings.HasPrefix(line, "#") {
|
|
942
|
+
return nil, fmt.Errorf("allowlist entry is not an exact normalized path: %q", line)
|
|
943
|
+
}
|
|
944
|
+
rel, err := normalizeProjectPath(projectDir, devritesRoot, line, true)
|
|
516
945
|
if err != nil {
|
|
517
|
-
|
|
518
|
-
return exitOK
|
|
946
|
+
return nil, err
|
|
519
947
|
}
|
|
520
|
-
|
|
521
|
-
|
|
948
|
+
if _, duplicate := allow[rel]; duplicate {
|
|
949
|
+
return nil, fmt.Errorf("duplicate allowlist path: %q", line)
|
|
950
|
+
}
|
|
951
|
+
if info, statErr := os.Stat(filepath.Join(projectDir, filepath.FromSlash(rel))); statErr == nil && info.IsDir() {
|
|
952
|
+
return nil, fmt.Errorf("allowlist path names a directory: %q", line)
|
|
953
|
+
} else if statErr != nil && !os.IsNotExist(statErr) {
|
|
954
|
+
return nil, statErr
|
|
955
|
+
}
|
|
956
|
+
allow[rel] = struct{}{}
|
|
957
|
+
}
|
|
958
|
+
return allow, nil
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
func normalizeProjectPath(projectDir, devritesRoot, raw string, requireNormalized bool) (string, error) {
|
|
962
|
+
if raw == "" || strings.ContainsRune(raw, 0) || strings.ContainsRune(raw, '\\') {
|
|
963
|
+
return "", fmt.Errorf("invalid project path %q", raw)
|
|
964
|
+
}
|
|
965
|
+
var abs string
|
|
966
|
+
if filepath.IsAbs(raw) {
|
|
967
|
+
if requireNormalized {
|
|
968
|
+
return "", fmt.Errorf("allowlist path must be project-relative: %q", raw)
|
|
969
|
+
}
|
|
970
|
+
abs = filepath.Clean(raw)
|
|
971
|
+
} else {
|
|
972
|
+
clean := filepath.Clean(filepath.FromSlash(raw))
|
|
973
|
+
if requireNormalized && filepath.ToSlash(clean) != raw {
|
|
974
|
+
return "", fmt.Errorf("allowlist path is not normalized: %q", raw)
|
|
975
|
+
}
|
|
976
|
+
abs = filepath.Join(projectDir, clean)
|
|
977
|
+
}
|
|
978
|
+
rel, err := filepath.Rel(projectDir, abs)
|
|
979
|
+
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
|
980
|
+
return "", fmt.Errorf("path is outside the project: %q", raw)
|
|
981
|
+
}
|
|
982
|
+
if !safepath.WithinResolved(abs, projectDir) || safepath.WithinResolved(abs, devritesRoot) {
|
|
983
|
+
return "", fmt.Errorf("path escapes the project or targets .devrites: %q", raw)
|
|
984
|
+
}
|
|
985
|
+
return filepath.ToSlash(rel), nil
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
func denyOrObserveWright(h harness.Harness, stdout, stderr io.Writer, dir string, active bool, message string, reasonID drvreason.ID, bad []string) int {
|
|
989
|
+
if active || hookEnforce("DEVRITES_WRIGHT_SCOPE") {
|
|
990
|
+
return denyWright(h, stdout, stderr, message, reasonID)
|
|
522
991
|
}
|
|
523
992
|
_ = state.AppendLog(filepath.Join(dir, ".wright-scope.log"), "WOULD-BLOCK\t"+strings.Join(bad, ", "))
|
|
993
|
+
recordHookGuard(h, "wright-scope", drvreason.HookWrightScopeObserved, lib.GuardObserved, lib.OutcomeObserved)
|
|
994
|
+
return exitOK
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
func denyWright(h harness.Harness, stdout, stderr io.Writer, message string, reasonID drvreason.ID) int {
|
|
998
|
+
out, err := h.PreToolDeny(message)
|
|
999
|
+
if err != nil {
|
|
1000
|
+
debugf(stderr, "wright-scope: %v", err)
|
|
1001
|
+
return exitOK
|
|
1002
|
+
}
|
|
1003
|
+
fmt.Fprintln(stdout, out)
|
|
1004
|
+
recordHookGuard(h, "wright-scope", reasonID, lib.GuardEnforced, lib.OutcomeDenied)
|
|
524
1005
|
return exitOK
|
|
525
1006
|
}
|
|
526
1007
|
|
|
527
|
-
// wsGateCount mirrors `grep -ciE '^\s*status:\s*open' questions.md || echo 0
|
|
528
|
-
//
|
|
529
|
-
//
|
|
1008
|
+
// wsGateCount mirrors `grep -ciE '^\s*status:\s*open' questions.md || echo 0`.
|
|
1009
|
+
// On a present file with no matches, grep prints "0" and exits 1, so the fallback
|
|
1010
|
+
// appends another "0". The captured value is therefore "0\n0".
|
|
530
1011
|
func wsGateCount(qPath string) string {
|
|
531
1012
|
data, err := os.ReadFile(qPath)
|
|
532
1013
|
if err != nil {
|