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
|
@@ -127,6 +127,169 @@ func TestPackageExistenceJavaScriptWorkspaces(t *testing.T) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
+
func TestPackageExistenceCSSImports(t *testing.T) {
|
|
131
|
+
tests := []struct {
|
|
132
|
+
name string
|
|
133
|
+
imports string
|
|
134
|
+
manifest string
|
|
135
|
+
wantCode int
|
|
136
|
+
wantFinding string
|
|
137
|
+
}{
|
|
138
|
+
{
|
|
139
|
+
name: "declared multi import ignores comment prefix",
|
|
140
|
+
imports: "/* @import \"comment-only\"; */ @import \"tailwindcss\"; @import \"./theme.css\";",
|
|
141
|
+
manifest: `{"dependencies":{"tailwindcss":"^4.1.0"}}`,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "undeclared url import",
|
|
145
|
+
imports: `@import url("missing-theme");`,
|
|
146
|
+
manifest: `{}`,
|
|
147
|
+
wantCode: 3,
|
|
148
|
+
wantFinding: "missing-theme",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "remote imports ignored",
|
|
152
|
+
imports: `@import url("https://cdn.example.invalid/theme.css"); @import "//cdn.example.invalid/other.css";`,
|
|
153
|
+
manifest: `{}`,
|
|
154
|
+
},
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
for _, test := range tests {
|
|
158
|
+
t.Run(test.name, func(t *testing.T) {
|
|
159
|
+
repo := t.TempDir()
|
|
160
|
+
writePackageExistenceFile(t, repo, "package.json", test.manifest+"\n")
|
|
161
|
+
writePackageExistenceFile(t, repo, "styles/app.css", "/* baseline */\n")
|
|
162
|
+
gitPackageExistence(t, repo, "init", "-q")
|
|
163
|
+
gitPackageExistence(t, repo, "config", "user.email", "test@example.com")
|
|
164
|
+
gitPackageExistence(t, repo, "config", "user.name", "Test")
|
|
165
|
+
gitPackageExistence(t, repo, "add", ".")
|
|
166
|
+
gitPackageExistence(t, repo, "commit", "-qm", "baseline")
|
|
167
|
+
if err := os.MkdirAll(featureDir(repo, "feat"), 0o755); err != nil {
|
|
168
|
+
t.Fatal(err)
|
|
169
|
+
}
|
|
170
|
+
writePackageExistenceFile(t, repo, "styles/app.css", "/* baseline */\n"+test.imports+"\n")
|
|
171
|
+
t.Chdir(repo)
|
|
172
|
+
|
|
173
|
+
var stdout, stderr bytes.Buffer
|
|
174
|
+
gotCode := PackageExistence(repo, []string{"feat"}, &stdout, &stderr)
|
|
175
|
+
if gotCode != test.wantCode {
|
|
176
|
+
t.Fatalf("code = %d, want %d\nstdout: %s\nstderr: %s", gotCode, test.wantCode, stdout.String(), stderr.String())
|
|
177
|
+
}
|
|
178
|
+
if test.wantFinding != "" && !strings.Contains(stderr.String(), test.wantFinding) {
|
|
179
|
+
t.Fatalf("stderr does not name %q: %s", test.wantFinding, stderr.String())
|
|
180
|
+
}
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
func TestPackageExistenceCSSResolvesBareSiblingPath(t *testing.T) {
|
|
186
|
+
repo := t.TempDir()
|
|
187
|
+
writePackageExistenceFile(t, repo, "package.json", "{}\n")
|
|
188
|
+
writePackageExistenceFile(t, repo, "styles/app.css", "/* baseline */\n")
|
|
189
|
+
writePackageExistenceFile(t, repo, "styles/theme.css", ":root { color-scheme: dark; }\n")
|
|
190
|
+
gitPackageExistence(t, repo, "init", "-q")
|
|
191
|
+
gitPackageExistence(t, repo, "config", "user.email", "test@example.com")
|
|
192
|
+
gitPackageExistence(t, repo, "config", "user.name", "Test")
|
|
193
|
+
gitPackageExistence(t, repo, "add", ".")
|
|
194
|
+
gitPackageExistence(t, repo, "commit", "-qm", "baseline")
|
|
195
|
+
if err := os.MkdirAll(featureDir(repo, "feat"), 0o755); err != nil {
|
|
196
|
+
t.Fatal(err)
|
|
197
|
+
}
|
|
198
|
+
writePackageExistenceFile(t, repo, "styles/app.css", "/* baseline */\n@import \"theme.css\";\n")
|
|
199
|
+
t.Chdir(repo)
|
|
200
|
+
|
|
201
|
+
var stdout, stderr bytes.Buffer
|
|
202
|
+
if code := PackageExistence(repo, []string{"feat"}, &stdout, &stderr); code != 0 {
|
|
203
|
+
t.Fatalf("code = %d, want 0\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
func TestPackageExistenceCSSHonorsBaselineCommentContext(t *testing.T) {
|
|
208
|
+
repo := t.TempDir()
|
|
209
|
+
baseline := "/* comment opened on an unchanged line\nunchanged content\n*/\n"
|
|
210
|
+
current := "/* comment opened on an unchanged line\n@import \"comment-only-package\";\nunchanged content\n*/\n"
|
|
211
|
+
writePackageExistenceFile(t, repo, "package.json", "{}\n")
|
|
212
|
+
writePackageExistenceFile(t, repo, "styles/app.css", baseline)
|
|
213
|
+
gitPackageExistence(t, repo, "init", "-q")
|
|
214
|
+
gitPackageExistence(t, repo, "config", "user.email", "test@example.com")
|
|
215
|
+
gitPackageExistence(t, repo, "config", "user.name", "Test")
|
|
216
|
+
gitPackageExistence(t, repo, "add", ".")
|
|
217
|
+
gitPackageExistence(t, repo, "commit", "-qm", "baseline")
|
|
218
|
+
if err := os.MkdirAll(featureDir(repo, "feat"), 0o755); err != nil {
|
|
219
|
+
t.Fatal(err)
|
|
220
|
+
}
|
|
221
|
+
writePackageExistenceFile(t, repo, "styles/app.css", current)
|
|
222
|
+
t.Chdir(repo)
|
|
223
|
+
|
|
224
|
+
var stdout, stderr bytes.Buffer
|
|
225
|
+
if code := PackageExistence(repo, []string{"feat"}, &stdout, &stderr); code != 0 {
|
|
226
|
+
t.Fatalf("code = %d, want 0\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
func TestPackageExistenceScansUntrackedSourceFiles(t *testing.T) {
|
|
231
|
+
repo := t.TempDir()
|
|
232
|
+
writePackageExistenceFile(t, repo, "package.json", "{}\n")
|
|
233
|
+
gitPackageExistence(t, repo, "init", "-q")
|
|
234
|
+
gitPackageExistence(t, repo, "config", "user.email", "test@example.com")
|
|
235
|
+
gitPackageExistence(t, repo, "config", "user.name", "Test")
|
|
236
|
+
gitPackageExistence(t, repo, "add", ".")
|
|
237
|
+
gitPackageExistence(t, repo, "commit", "-qm", "baseline")
|
|
238
|
+
if err := os.MkdirAll(featureDir(repo, "feat"), 0o755); err != nil {
|
|
239
|
+
t.Fatal(err)
|
|
240
|
+
}
|
|
241
|
+
writePackageExistenceFile(t, repo, "src/new.ts", `import missing from "untracked-missing-package";`+"\n")
|
|
242
|
+
t.Chdir(repo)
|
|
243
|
+
|
|
244
|
+
var stdout, stderr bytes.Buffer
|
|
245
|
+
gotCode := PackageExistence(repo, []string{"feat"}, &stdout, &stderr)
|
|
246
|
+
if gotCode != 3 {
|
|
247
|
+
t.Fatalf("code = %d, want 3\nstdout: %s\nstderr: %s", gotCode, stdout.String(), stderr.String())
|
|
248
|
+
}
|
|
249
|
+
if !strings.Contains(stderr.String(), "untracked-missing-package") {
|
|
250
|
+
t.Fatalf("stderr does not name untracked dependency: %s", stderr.String())
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
func TestPackageExistenceScansUntrackedFilesAddedAfterSliceSnapshot(t *testing.T) {
|
|
255
|
+
repo := newGitRepo(t)
|
|
256
|
+
root := workspace(t, "feat")
|
|
257
|
+
writePackageExistenceFile(t, repo, "package.json", "{}\n")
|
|
258
|
+
commitAll(t, repo, "manifest")
|
|
259
|
+
writeWrightAllowlist(t, root, "feat", "src/new.ts")
|
|
260
|
+
if code, out := runReconcile(t, root, "snapshot", "feat"); code != 0 {
|
|
261
|
+
t.Fatalf("snapshot = %d, want 0\n%s", code, out)
|
|
262
|
+
}
|
|
263
|
+
writePackageExistenceFile(t, repo, "src/new.ts", `import missing from "slice-untracked-package";`+"\n")
|
|
264
|
+
if code, out := runReconcile(t, root, "check", "feat"); code != 0 {
|
|
265
|
+
t.Fatalf("reconcile check = %d, want 0\n%s", code, out)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var stdout, stderr bytes.Buffer
|
|
269
|
+
code := PackageExistence(root, []string{"feat"}, &stdout, &stderr)
|
|
270
|
+
if code != 3 {
|
|
271
|
+
t.Fatalf("code = %d, want 3\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
272
|
+
}
|
|
273
|
+
if !strings.Contains(stderr.String(), "slice-untracked-package") {
|
|
274
|
+
t.Fatalf("stderr does not name slice dependency: %s", stderr.String())
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
func TestPackageExistenceFailsClosedOnPartialReconcileBaseline(t *testing.T) {
|
|
279
|
+
newGitRepo(t)
|
|
280
|
+
root := workspace(t, "feat")
|
|
281
|
+
writeFile(t, filepath.Join(featureDir(root, "feat"), reconcileBaseName), "deadbeef\n")
|
|
282
|
+
|
|
283
|
+
var stdout, stderr bytes.Buffer
|
|
284
|
+
code := PackageExistence(root, []string{"feat"}, &stdout, &stderr)
|
|
285
|
+
if code != 2 {
|
|
286
|
+
t.Fatalf("code = %d, want 2\nstdout: %s\nstderr: %s", code, stdout.String(), stderr.String())
|
|
287
|
+
}
|
|
288
|
+
if !strings.Contains(stderr.String(), "partial lifecycle") {
|
|
289
|
+
t.Fatalf("missing fail-closed lifecycle diagnostic:\n%s", stderr.String())
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
130
293
|
func TestPackageExistenceNonJavaScriptWorkspaces(t *testing.T) {
|
|
131
294
|
tests := []struct {
|
|
132
295
|
name string
|
|
@@ -11,9 +11,9 @@ import (
|
|
|
11
11
|
"github.com/devrites/devrites/internal/state"
|
|
12
12
|
)
|
|
13
13
|
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
14
|
+
// These state parsing patterns match progress.sh's awk expressions. Each
|
|
15
|
+
// name-removal pattern is anchored, so ReplaceAllString behaves like awk's
|
|
16
|
+
// single-match sub.
|
|
17
17
|
var (
|
|
18
18
|
sliceCheckRe = regexp.MustCompile(`^- \[[^\]]*\][[:space:]]*`) // drop "- [x] "
|
|
19
19
|
sliceNameRe = regexp.MustCompile(`^Slice[[:space:]]*[0-9]+:[[:space:]]*`) // drop "Slice N: "
|
|
@@ -23,10 +23,9 @@ var (
|
|
|
23
23
|
sliceHeadingRe = regexp.MustCompile(`^##[[:space:]]+(SLICE-[0-9]{3})(?:[[:space:]]+(.*))?$`)
|
|
24
24
|
)
|
|
25
25
|
|
|
26
|
-
// Progress renders the
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// is the .devrites directory; slug defaults to the active feature at <root>/ACTIVE.
|
|
26
|
+
// Progress renders the footer printed by workspace-aware rite-* skills. It reads
|
|
27
|
+
// from the .devrites root and returns 0 without output when no workspace is
|
|
28
|
+
// active. An empty slug uses <root>/ACTIVE.
|
|
30
29
|
func Progress(root string, args []string, stdout, stderr io.Writer) int {
|
|
31
30
|
slug := ""
|
|
32
31
|
if len(args) > 0 {
|
|
@@ -82,8 +81,8 @@ func Progress(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
82
81
|
}
|
|
83
82
|
}
|
|
84
83
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
84
|
+
// The flow ribbon includes optional phases when their artifact exists or while
|
|
85
|
+
// they are active. It always includes the current phase.
|
|
87
86
|
order := make([]string, 0, len(state.LifecyclePhases()))
|
|
88
87
|
for _, lifecyclePhase := range state.LifecyclePhases() {
|
|
89
88
|
name := string(lifecyclePhase)
|
|
@@ -128,6 +127,9 @@ func Progress(root string, args []string, stdout, stderr io.Writer) int {
|
|
|
128
127
|
ribbon += " " + name + " " + g
|
|
129
128
|
}
|
|
130
129
|
fmt.Fprintf(stdout, "%s\n", ribbon)
|
|
130
|
+
if obs := ObservabilityStatus(root, slug); obs != "" {
|
|
131
|
+
fmt.Fprintf(stdout, "Obs %s\n", obs)
|
|
132
|
+
}
|
|
131
133
|
return 0
|
|
132
134
|
}
|
|
133
135
|
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
package lib
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"crypto/rand"
|
|
5
|
+
"crypto/sha256"
|
|
6
|
+
"encoding/hex"
|
|
7
|
+
"encoding/json"
|
|
8
|
+
"fmt"
|
|
9
|
+
"os"
|
|
10
|
+
"path"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"regexp"
|
|
13
|
+
"strings"
|
|
14
|
+
"time"
|
|
15
|
+
|
|
16
|
+
"github.com/devrites/devrites/internal/reason"
|
|
17
|
+
"github.com/devrites/devrites/internal/rootfacts"
|
|
18
|
+
"github.com/devrites/devrites/internal/safepath"
|
|
19
|
+
"github.com/devrites/devrites/internal/state"
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
const EventSchemaV1 = "devrites-event/v1"
|
|
23
|
+
|
|
24
|
+
type EventBoundary string
|
|
25
|
+
type RootSource string
|
|
26
|
+
type ExecutionMode string
|
|
27
|
+
type GuardStrength string
|
|
28
|
+
type EventOutcome string
|
|
29
|
+
type EventHost string
|
|
30
|
+
|
|
31
|
+
const (
|
|
32
|
+
BoundaryRootSelection EventBoundary = "root-selection"
|
|
33
|
+
BoundaryLifecycleGate EventBoundary = "lifecycle-gate"
|
|
34
|
+
BoundaryHookGuard EventBoundary = "hook-guard"
|
|
35
|
+
BoundaryAgentDispatch EventBoundary = "agent-dispatch"
|
|
36
|
+
BoundaryResultReconcile EventBoundary = "result-reconciliation"
|
|
37
|
+
BoundaryGitPolicy EventBoundary = "git-policy"
|
|
38
|
+
|
|
39
|
+
RootSourceExplicit RootSource = "DEVRITES_ROOT"
|
|
40
|
+
RootSourceGit RootSource = "git-ancestor"
|
|
41
|
+
RootSourceGitMarker RootSource = "git-marker-ancestor"
|
|
42
|
+
RootSourceFilesystem RootSource = "filesystem-ancestor"
|
|
43
|
+
RootSourceNone RootSource = "none"
|
|
44
|
+
|
|
45
|
+
ExecutionNamed ExecutionMode = "named"
|
|
46
|
+
ExecutionGeneric ExecutionMode = "generic"
|
|
47
|
+
ExecutionInline ExecutionMode = "inline"
|
|
48
|
+
ExecutionNone ExecutionMode = "none"
|
|
49
|
+
|
|
50
|
+
GuardEnforced GuardStrength = "enforced"
|
|
51
|
+
GuardObserved GuardStrength = "observed"
|
|
52
|
+
GuardUnavailable GuardStrength = "unavailable"
|
|
53
|
+
GuardBypassed GuardStrength = "bypassed"
|
|
54
|
+
GuardNA GuardStrength = "n/a"
|
|
55
|
+
|
|
56
|
+
OutcomePassed EventOutcome = "passed"
|
|
57
|
+
OutcomeBlocked EventOutcome = "blocked"
|
|
58
|
+
OutcomeDenied EventOutcome = "denied"
|
|
59
|
+
OutcomeAllowed EventOutcome = "allowed"
|
|
60
|
+
OutcomeObserved EventOutcome = "observed"
|
|
61
|
+
OutcomeBypassed EventOutcome = "bypassed"
|
|
62
|
+
OutcomeUnavailable EventOutcome = "unavailable"
|
|
63
|
+
OutcomeWarning EventOutcome = "warning"
|
|
64
|
+
OutcomeRecorded EventOutcome = "recorded"
|
|
65
|
+
|
|
66
|
+
HostEngine EventHost = "engine"
|
|
67
|
+
HostClaude EventHost = "claude"
|
|
68
|
+
HostCodex EventHost = "codex"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
// EventV1 is the privacy-bounded execution/provenance record shared by the
|
|
72
|
+
// existing root timeline and per-workspace event logs.
|
|
73
|
+
type EventV1 struct {
|
|
74
|
+
Schema string `json:"schema"`
|
|
75
|
+
TS string `json:"ts"`
|
|
76
|
+
RunID string `json:"run_id"`
|
|
77
|
+
Boundary EventBoundary `json:"boundary"`
|
|
78
|
+
RootSource RootSource `json:"root_source"`
|
|
79
|
+
Workspace string `json:"workspace,omitempty"`
|
|
80
|
+
PhaseBefore state.Phase `json:"phase_before,omitempty"`
|
|
81
|
+
Event string `json:"event"`
|
|
82
|
+
RuleIDs []reason.ID `json:"rule_ids"`
|
|
83
|
+
EvidencePaths []string `json:"evidence_paths"`
|
|
84
|
+
PhaseAfter state.Phase `json:"phase_after,omitempty"`
|
|
85
|
+
ExecutionMode ExecutionMode `json:"execution_mode"`
|
|
86
|
+
GuardStrength GuardStrength `json:"guard_strength"`
|
|
87
|
+
ReasonID reason.ID `json:"reason_id"`
|
|
88
|
+
Outcome EventOutcome `json:"outcome"`
|
|
89
|
+
Host EventHost `json:"host"`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
var (
|
|
93
|
+
eventToken = regexp.MustCompile(`^[a-z][a-z0-9.-]{0,63}$`)
|
|
94
|
+
slugToken = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$`)
|
|
95
|
+
runIDToken = regexp.MustCompile(`^drv-run-v1:[0-9a-f]{32}$`)
|
|
96
|
+
processRun = makeProcessRunID()
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
func makeProcessRunID() string {
|
|
100
|
+
var raw [16]byte
|
|
101
|
+
if _, err := rand.Read(raw[:]); err == nil {
|
|
102
|
+
return "drv-run-v1:" + hex.EncodeToString(raw[:])
|
|
103
|
+
}
|
|
104
|
+
sum := sha256.Sum256([]byte(fmt.Sprintf("%d:%s", os.Getpid(), time.Now().UTC().Format(time.RFC3339Nano))))
|
|
105
|
+
return "drv-run-v1:" + hex.EncodeToString(sum[:16])
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// CurrentRunID returns one opaque ID per engine process. Tests and hermetic
|
|
109
|
+
// runners may inject an already-opaque canonical ID; arbitrary environment
|
|
110
|
+
// values are never persisted.
|
|
111
|
+
func CurrentRunID() string {
|
|
112
|
+
if value := os.Getenv("DEVRITES_RUN_ID"); runIDToken.MatchString(value) {
|
|
113
|
+
return value
|
|
114
|
+
}
|
|
115
|
+
return processRun
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// NewEventV1 fills the version, timestamp, and opaque process identity.
|
|
119
|
+
func NewEventV1(boundary EventBoundary, event string, reasonID reason.ID) EventV1 {
|
|
120
|
+
return EventV1{
|
|
121
|
+
Schema: EventSchemaV1,
|
|
122
|
+
TS: nowUTC(),
|
|
123
|
+
RunID: CurrentRunID(),
|
|
124
|
+
Boundary: boundary,
|
|
125
|
+
Event: event,
|
|
126
|
+
RuleIDs: []reason.ID{reasonID},
|
|
127
|
+
EvidencePaths: []string{},
|
|
128
|
+
ExecutionMode: ExecutionNone,
|
|
129
|
+
GuardStrength: GuardNA,
|
|
130
|
+
ReasonID: reasonID,
|
|
131
|
+
Host: HostEngine,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// NewRootSelectionEvent records the canonical root chosen for one command
|
|
136
|
+
// invocation without persisting that root's absolute path.
|
|
137
|
+
func NewRootSelectionEvent(root, command string) (EventV1, error) {
|
|
138
|
+
ev := NewEventV1(BoundaryRootSelection, "invoke."+command, reason.RootSelected)
|
|
139
|
+
ev.Outcome = OutcomePassed
|
|
140
|
+
if err := BindEventWorkspace(root, activeSlug(root), &ev); err != nil {
|
|
141
|
+
return EventV1{}, err
|
|
142
|
+
}
|
|
143
|
+
return ev, nil
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// NewLifecycleGateEvent turns a typed gate result into a v1 provenance record.
|
|
147
|
+
func NewLifecycleGateEvent(root, slug, gateName string, phase state.Phase, blocked bool, reasonID reason.ID, evidenceFiles []string) (EventV1, error) {
|
|
148
|
+
ev := NewEventV1(BoundaryLifecycleGate, gateName, reasonID)
|
|
149
|
+
ev.PhaseBefore = phase
|
|
150
|
+
ev.PhaseAfter = phase
|
|
151
|
+
ev.EvidencePaths = WorkspaceEvidencePaths(root, slug, evidenceFiles...)
|
|
152
|
+
ev.Outcome = OutcomePassed
|
|
153
|
+
if blocked {
|
|
154
|
+
ev.Outcome = OutcomeBlocked
|
|
155
|
+
}
|
|
156
|
+
if err := BindEventWorkspace(root, slug, &ev); err != nil {
|
|
157
|
+
return EventV1{}, err
|
|
158
|
+
}
|
|
159
|
+
return ev, nil
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// BindEventWorkspace adds only canonical, project-relative root/workspace
|
|
163
|
+
// identity and the current phase. It never copies an absolute path into ev.
|
|
164
|
+
func BindEventWorkspace(root, slug string, ev *EventV1) error {
|
|
165
|
+
if ev == nil {
|
|
166
|
+
return fmt.Errorf("nil event")
|
|
167
|
+
}
|
|
168
|
+
ev.RootSource = eventRootSource(root)
|
|
169
|
+
if slug == "" {
|
|
170
|
+
return nil
|
|
171
|
+
}
|
|
172
|
+
dir := featureDir(root, slug)
|
|
173
|
+
rel, err := filepath.Rel(filepath.Dir(root), dir)
|
|
174
|
+
if err != nil {
|
|
175
|
+
return fmt.Errorf("derive workspace identity: %w", err)
|
|
176
|
+
}
|
|
177
|
+
ev.Workspace = filepath.ToSlash(rel)
|
|
178
|
+
if feature, err := state.LoadFeature(root, slug); err == nil {
|
|
179
|
+
if ev.PhaseBefore == "" {
|
|
180
|
+
ev.PhaseBefore = feature.Phase
|
|
181
|
+
}
|
|
182
|
+
if ev.PhaseAfter == "" {
|
|
183
|
+
ev.PhaseAfter = feature.Phase
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return nil
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
func eventRootSource(root string) RootSource {
|
|
190
|
+
override := os.Getenv("DEVRITES_ROOT")
|
|
191
|
+
facts, err := rootfacts.Resolve(override)
|
|
192
|
+
if err == nil && sameEventPath(facts.PhysicalRoot, root) {
|
|
193
|
+
switch facts.SelectionReason {
|
|
194
|
+
case string(RootSourceExplicit):
|
|
195
|
+
return RootSourceExplicit
|
|
196
|
+
case string(RootSourceGit):
|
|
197
|
+
return RootSourceGit
|
|
198
|
+
case string(RootSourceGitMarker):
|
|
199
|
+
return RootSourceGitMarker
|
|
200
|
+
case string(RootSourceFilesystem):
|
|
201
|
+
return RootSourceFilesystem
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if strings.TrimSpace(override) != "" {
|
|
205
|
+
return RootSourceExplicit
|
|
206
|
+
}
|
|
207
|
+
return RootSourceNone
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
func sameEventPath(a, b string) bool {
|
|
211
|
+
if a == "" || b == "" {
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
ai, aerr := os.Stat(a)
|
|
215
|
+
bi, berr := os.Stat(b)
|
|
216
|
+
if aerr == nil && berr == nil {
|
|
217
|
+
return os.SameFile(ai, bi)
|
|
218
|
+
}
|
|
219
|
+
return filepath.Clean(a) == filepath.Clean(b)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// WorkspaceEvidencePaths converts workspace-local names to project-relative
|
|
223
|
+
// paths. Invalid or absolute names are omitted; final validation remains the
|
|
224
|
+
// append boundary.
|
|
225
|
+
func WorkspaceEvidencePaths(root, slug string, names ...string) []string {
|
|
226
|
+
if slug == "" {
|
|
227
|
+
return []string{}
|
|
228
|
+
}
|
|
229
|
+
dir := featureDir(root, slug)
|
|
230
|
+
project := filepath.Dir(root)
|
|
231
|
+
out := make([]string, 0, len(names))
|
|
232
|
+
for _, name := range names {
|
|
233
|
+
name = filepath.ToSlash(name)
|
|
234
|
+
if !validRelativePath(name) {
|
|
235
|
+
continue
|
|
236
|
+
}
|
|
237
|
+
rel, err := filepath.Rel(project, filepath.Join(dir, filepath.FromSlash(name)))
|
|
238
|
+
if err == nil {
|
|
239
|
+
out = append(out, filepath.ToSlash(rel))
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ValidateEventV1 enforces the privacy and compatibility boundary before disk.
|
|
246
|
+
func ValidateEventV1(ev EventV1) error {
|
|
247
|
+
if ev.Schema != EventSchemaV1 {
|
|
248
|
+
return fmt.Errorf("schema must be %q", EventSchemaV1)
|
|
249
|
+
}
|
|
250
|
+
if len(ev.TS) > 64 {
|
|
251
|
+
return fmt.Errorf("timestamp is too long")
|
|
252
|
+
}
|
|
253
|
+
if _, err := time.Parse(time.RFC3339, ev.TS); err != nil {
|
|
254
|
+
return fmt.Errorf("timestamp must be RFC3339: %w", err)
|
|
255
|
+
}
|
|
256
|
+
if !runIDToken.MatchString(ev.RunID) {
|
|
257
|
+
return fmt.Errorf("run_id must be an opaque drv-run-v1 token")
|
|
258
|
+
}
|
|
259
|
+
if !knownBoundary(ev.Boundary) {
|
|
260
|
+
return fmt.Errorf("unknown boundary %q", ev.Boundary)
|
|
261
|
+
}
|
|
262
|
+
if !knownRootSource(ev.RootSource) {
|
|
263
|
+
return fmt.Errorf("unknown root_source %q", ev.RootSource)
|
|
264
|
+
}
|
|
265
|
+
if ev.Workspace != "" && !validWorkspaceIdentity(ev.Workspace) {
|
|
266
|
+
return fmt.Errorf("workspace must be a canonical project-relative DevRites workspace")
|
|
267
|
+
}
|
|
268
|
+
if !validPhase(ev.PhaseBefore) || !validPhase(ev.PhaseAfter) {
|
|
269
|
+
return fmt.Errorf("phase_before and phase_after must be canonical phases")
|
|
270
|
+
}
|
|
271
|
+
if !eventToken.MatchString(ev.Event) {
|
|
272
|
+
return fmt.Errorf("event must be a bounded lowercase identifier")
|
|
273
|
+
}
|
|
274
|
+
if len(ev.RuleIDs) == 0 || len(ev.RuleIDs) > 16 {
|
|
275
|
+
return fmt.Errorf("rule_ids must contain 1..16 entries")
|
|
276
|
+
}
|
|
277
|
+
if err := validateReasonIDs(ev.RuleIDs); err != nil {
|
|
278
|
+
return err
|
|
279
|
+
}
|
|
280
|
+
if len(ev.EvidencePaths) > 32 {
|
|
281
|
+
return fmt.Errorf("evidence_paths exceeds 32 entries")
|
|
282
|
+
}
|
|
283
|
+
if err := validateRelativePaths(ev.EvidencePaths); err != nil {
|
|
284
|
+
return err
|
|
285
|
+
}
|
|
286
|
+
if !knownExecutionMode(ev.ExecutionMode) {
|
|
287
|
+
return fmt.Errorf("unknown execution_mode %q", ev.ExecutionMode)
|
|
288
|
+
}
|
|
289
|
+
if !knownGuardStrength(ev.GuardStrength) {
|
|
290
|
+
return fmt.Errorf("unknown guard_strength %q", ev.GuardStrength)
|
|
291
|
+
}
|
|
292
|
+
if !reason.Known(ev.ReasonID) {
|
|
293
|
+
return fmt.Errorf("unknown reason_id %q", ev.ReasonID)
|
|
294
|
+
}
|
|
295
|
+
if !knownOutcome(ev.Outcome) {
|
|
296
|
+
return fmt.Errorf("unknown outcome %q", ev.Outcome)
|
|
297
|
+
}
|
|
298
|
+
if ev.Host != HostEngine && ev.Host != HostClaude && ev.Host != HostCodex {
|
|
299
|
+
return fmt.Errorf("unknown host %q", ev.Host)
|
|
300
|
+
}
|
|
301
|
+
return nil
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
func validateReasonIDs(ids []reason.ID) error {
|
|
305
|
+
seen := make(map[reason.ID]bool, len(ids))
|
|
306
|
+
for _, id := range ids {
|
|
307
|
+
if !reason.Known(id) {
|
|
308
|
+
return fmt.Errorf("unknown rule id %q", id)
|
|
309
|
+
}
|
|
310
|
+
if seen[id] {
|
|
311
|
+
return fmt.Errorf("duplicate rule id %q", id)
|
|
312
|
+
}
|
|
313
|
+
seen[id] = true
|
|
314
|
+
}
|
|
315
|
+
return nil
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
func validateRelativePaths(paths []string) error {
|
|
319
|
+
seen := make(map[string]bool, len(paths))
|
|
320
|
+
for _, value := range paths {
|
|
321
|
+
if !validRelativePath(value) {
|
|
322
|
+
return fmt.Errorf("evidence path %q is not canonical project-relative", value)
|
|
323
|
+
}
|
|
324
|
+
if seen[value] {
|
|
325
|
+
return fmt.Errorf("duplicate evidence path %q", value)
|
|
326
|
+
}
|
|
327
|
+
seen[value] = true
|
|
328
|
+
}
|
|
329
|
+
return nil
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
func validRelativePath(value string) bool {
|
|
333
|
+
if value == "" || len(value) > 512 || strings.ContainsAny(value, "\\\x00") ||
|
|
334
|
+
path.IsAbs(value) || filepath.IsAbs(value) {
|
|
335
|
+
return false
|
|
336
|
+
}
|
|
337
|
+
clean := path.Clean(value)
|
|
338
|
+
return clean == value && clean != "." && clean != ".." && !strings.HasPrefix(clean, "../")
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
func validWorkspaceIdentity(value string) bool {
|
|
342
|
+
if !validRelativePath(value) {
|
|
343
|
+
return false
|
|
344
|
+
}
|
|
345
|
+
parts := strings.Split(value, "/")
|
|
346
|
+
return len(parts) == 3 && parts[0] == ".devrites" &&
|
|
347
|
+
(parts[1] == "work" || parts[1] == "features") &&
|
|
348
|
+
slugToken.MatchString(parts[2])
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
func validPhase(phase state.Phase) bool {
|
|
352
|
+
return phase == "" || state.KnownPhase(phase)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func knownBoundary(value EventBoundary) bool {
|
|
356
|
+
switch value {
|
|
357
|
+
case BoundaryRootSelection, BoundaryLifecycleGate, BoundaryHookGuard,
|
|
358
|
+
BoundaryAgentDispatch, BoundaryResultReconcile, BoundaryGitPolicy:
|
|
359
|
+
return true
|
|
360
|
+
}
|
|
361
|
+
return false
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
func knownRootSource(value RootSource) bool {
|
|
365
|
+
switch value {
|
|
366
|
+
case RootSourceExplicit, RootSourceGit, RootSourceGitMarker,
|
|
367
|
+
RootSourceFilesystem, RootSourceNone:
|
|
368
|
+
return true
|
|
369
|
+
}
|
|
370
|
+
return false
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
func knownExecutionMode(value ExecutionMode) bool {
|
|
374
|
+
switch value {
|
|
375
|
+
case ExecutionNamed, ExecutionGeneric, ExecutionInline, ExecutionNone:
|
|
376
|
+
return true
|
|
377
|
+
}
|
|
378
|
+
return false
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
func knownGuardStrength(value GuardStrength) bool {
|
|
382
|
+
switch value {
|
|
383
|
+
case GuardEnforced, GuardObserved, GuardUnavailable, GuardBypassed, GuardNA:
|
|
384
|
+
return true
|
|
385
|
+
}
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
func knownOutcome(value EventOutcome) bool {
|
|
390
|
+
switch value {
|
|
391
|
+
case OutcomePassed, OutcomeBlocked, OutcomeDenied, OutcomeAllowed,
|
|
392
|
+
OutcomeObserved, OutcomeBypassed, OutcomeUnavailable, OutcomeWarning,
|
|
393
|
+
OutcomeRecorded:
|
|
394
|
+
return true
|
|
395
|
+
}
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// AppendEventV1 appends one validated record to the existing root timeline and,
|
|
400
|
+
// when present, the existing per-workspace event log. Callers intentionally
|
|
401
|
+
// treat errors as telemetry degradation rather than a policy decision.
|
|
402
|
+
func AppendEventV1(root string, ev EventV1) error {
|
|
403
|
+
if err := ValidateEventV1(ev); err != nil {
|
|
404
|
+
return err
|
|
405
|
+
}
|
|
406
|
+
info, err := os.Stat(root)
|
|
407
|
+
if err != nil || !info.IsDir() {
|
|
408
|
+
if err == nil {
|
|
409
|
+
err = fmt.Errorf("not a directory")
|
|
410
|
+
}
|
|
411
|
+
return fmt.Errorf("event root unavailable: %w", err)
|
|
412
|
+
}
|
|
413
|
+
rootLog := filepath.Join(root, "timeline.jsonl")
|
|
414
|
+
if err := validateEventLineTarget(rootLog, root); err != nil {
|
|
415
|
+
return err
|
|
416
|
+
}
|
|
417
|
+
workspaceLog := ""
|
|
418
|
+
if ev.Workspace != "" {
|
|
419
|
+
workspace := filepath.Join(filepath.Dir(root), filepath.FromSlash(ev.Workspace))
|
|
420
|
+
workspaceInfo, err := os.Stat(workspace)
|
|
421
|
+
if err != nil || !workspaceInfo.IsDir() || !safepath.WithinResolved(workspace, root) {
|
|
422
|
+
if err == nil {
|
|
423
|
+
err = fmt.Errorf("not a workspace directory inside root")
|
|
424
|
+
}
|
|
425
|
+
return fmt.Errorf("event workspace unavailable: %w", err)
|
|
426
|
+
}
|
|
427
|
+
workspaceLog = filepath.Join(workspace, "events.jsonl")
|
|
428
|
+
if err := validateEventLineTarget(workspaceLog, root); err != nil {
|
|
429
|
+
return err
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
line, err := json.Marshal(ev)
|
|
433
|
+
if err != nil {
|
|
434
|
+
return fmt.Errorf("marshal event: %w", err)
|
|
435
|
+
}
|
|
436
|
+
if err := appendEventLine(rootLog, root, line); err != nil {
|
|
437
|
+
return err
|
|
438
|
+
}
|
|
439
|
+
if workspaceLog == "" {
|
|
440
|
+
return nil
|
|
441
|
+
}
|
|
442
|
+
return appendEventLine(workspaceLog, root, line)
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
func appendEventLine(file, root string, line []byte) error {
|
|
446
|
+
if err := validateEventLineTarget(file, root); err != nil {
|
|
447
|
+
return err
|
|
448
|
+
}
|
|
449
|
+
return appendJSONLine(file, json.RawMessage(line))
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
func validateEventLineTarget(file, root string) error {
|
|
453
|
+
if info, err := os.Lstat(file); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
|
454
|
+
return fmt.Errorf("event log is a symlink: %s", filepath.Base(file))
|
|
455
|
+
} else if err != nil && !os.IsNotExist(err) {
|
|
456
|
+
return fmt.Errorf("inspect event log: %w", err)
|
|
457
|
+
}
|
|
458
|
+
if !safepath.WithinResolved(filepath.Dir(file), root) {
|
|
459
|
+
return fmt.Errorf("event log escapes root")
|
|
460
|
+
}
|
|
461
|
+
return nil
|
|
462
|
+
}
|