@getarbiter/cli 0.5.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 +713 -0
- package/LICENSE +202 -0
- package/NOTICE +31 -0
- package/PRIVACY.md +111 -0
- package/README.md +237 -0
- package/THIRD_PARTY_LICENSES.md +472 -0
- package/dist/.src-manifest.json +1 -0
- package/dist/capabilities/host-probe.js +3 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +87 -0
- package/dist/commands/adopt-policy.js +3 -0
- package/dist/commands/configure-interactive.js +3 -0
- package/dist/commands/configure.js +5 -0
- package/dist/commands/cross-model-review.js +3 -0
- package/dist/commands/diff.js +20 -0
- package/dist/commands/doc-set.js +3 -0
- package/dist/commands/doctor/clean.js +2 -0
- package/dist/commands/doctor/fail-open-census.js +12 -0
- package/dist/commands/doctor/health.js +11 -0
- package/dist/commands/doctor/prove-gates.js +13 -0
- package/dist/commands/doctor/recover-lock.js +12 -0
- package/dist/commands/doctor/repair-state.js +6 -0
- package/dist/commands/doctor/tool-pin-extract.js +4 -0
- package/dist/commands/doctor/tool-pins.js +9 -0
- package/dist/commands/doctor-interactive.js +2 -0
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/explain.js +19 -0
- package/dist/commands/gate-exec.js +4 -0
- package/dist/commands/gold-audit.js +14 -0
- package/dist/commands/graph.js +5 -0
- package/dist/commands/init/build-arbiter-config.js +1 -0
- package/dist/commands/init/generate.js +42 -0
- package/dist/commands/init/github-setup.js +5 -0
- package/dist/commands/init/maturity-gates.js +10 -0
- package/dist/commands/init/resolve-config.js +3 -0
- package/dist/commands/init/toolchain.js +10 -0
- package/dist/commands/init/types.js +1 -0
- package/dist/commands/init.js +17 -0
- package/dist/commands/method-interactive.js +4 -0
- package/dist/commands/method.js +11 -0
- package/dist/commands/obsidian.js +2 -0
- package/dist/commands/plugin.js +8 -0
- package/dist/commands/pr-merged.js +2 -0
- package/dist/commands/retire-policy.js +3 -0
- package/dist/commands/review-diff.js +3 -0
- package/dist/commands/settings.js +7 -0
- package/dist/commands/ship-config.js +2 -0
- package/dist/commands/ship-profile.js +2 -0
- package/dist/commands/ship-review.js +1 -0
- package/dist/commands/ship-tier.js +2 -0
- package/dist/commands/ship-train.js +2 -0
- package/dist/commands/task-mark.js +2 -0
- package/dist/commands/task-note.js +3 -0
- package/dist/commands/task-record-red.js +2 -0
- package/dist/commands/task-record-tech-debt.js +4 -0
- package/dist/commands/task-ship.js +3 -0
- package/dist/commands/task-state.js +6 -0
- package/dist/commands/task.js +59 -0
- package/dist/commands/update.js +60 -0
- package/dist/commands/upgrade-level-interactive.js +2 -0
- package/dist/commands/upgrade-level.js +7 -0
- package/dist/commands/verify-plan.js +8 -0
- package/dist/commands/verify-tdd.js +2 -0
- package/dist/commands/verify.js +7 -0
- package/dist/commands/wizard/coherence.js +1 -0
- package/dist/commands/worktree-prune.js +4 -0
- package/dist/commands/worktree.js +39 -0
- package/dist/compatibility/cross-language-matrix.json +540 -0
- package/dist/compatibility/index.d.ts +4 -0
- package/dist/compatibility/index.js +2 -0
- package/dist/compatibility/matcher.js +1 -0
- package/dist/compatibility/matrix.json +27 -0
- package/dist/compatibility/parsers.d.ts +38 -0
- package/dist/compatibility/parsers.js +1 -0
- package/dist/compatibility/probe.d.ts +41 -0
- package/dist/compatibility/probe.js +4 -0
- package/dist/compatibility/report.js +2 -0
- package/dist/compatibility/schema.d.ts +80 -0
- package/dist/compatibility/schema.js +1 -0
- package/dist/compatibility/skills-matrix.json +98 -0
- package/dist/compatibility/skills-validator.d.ts +30 -0
- package/dist/compatibility/skills-validator.js +4 -0
- package/dist/compatibility/tooling-proposals.json +5 -0
- package/dist/config/arbiter-ignore.js +3 -0
- package/dist/config/collaboration-mode-defaults.js +1 -0
- package/dist/config/diff.js +1 -0
- package/dist/config/env-overrides.js +3 -0
- package/dist/config/env-registry.js +2 -0
- package/dist/config/levels.js +1 -0
- package/dist/config/migrations/index.js +2 -0
- package/dist/config/migrations/v0-to-v1.js +2 -0
- package/dist/config/migrations/v1-to-v2.js +2 -0
- package/dist/config/migrations/v2-to-v3.js +2 -0
- package/dist/config/migrations/v3-to-v4.js +2 -0
- package/dist/config/override-resolver.js +2 -0
- package/dist/config/resolve-project-config.js +2 -0
- package/dist/config/resolve-project-name.js +2 -0
- package/dist/config/schema.d.ts +480 -0
- package/dist/config/schema.js +1 -0
- package/dist/config/test-pyramid-profiles.js +1 -0
- package/dist/config/thresholds.js +2 -0
- package/dist/conformance/dimensions.js +3 -0
- package/dist/conformance/engine.js +7 -0
- package/dist/conformance/gate-proofs.js +4 -0
- package/dist/conformance/shared.js +2 -0
- package/dist/detectors/axis.js +1 -0
- package/dist/detectors/build.js +2 -0
- package/dist/detectors/existing.js +2 -0
- package/dist/detectors/external-model.js +2 -0
- package/dist/detectors/framework.js +2 -0
- package/dist/detectors/git.js +11 -0
- package/dist/detectors/github.js +2 -0
- package/dist/detectors/lanes.js +5 -0
- package/dist/detectors/language-hooks.js +117 -0
- package/dist/detectors/language.js +2 -0
- package/dist/detectors/package-manager.js +2 -0
- package/dist/detectors/package.js +2 -0
- package/dist/detectors/scaffold-wiring.js +2 -0
- package/dist/detectors/workflow-collision.js +4 -0
- package/dist/evidence/gate-binding.js +4 -0
- package/dist/evidence/git-checks.js +3 -0
- package/dist/evidence/load.js +2 -0
- package/dist/evidence/provenance.js +2 -0
- package/dist/evidence/summary.js +2 -0
- package/dist/evidence/tdd-reexecute.js +2 -0
- package/dist/evidence/tdd.js +4 -0
- package/dist/experimental/index.js +2 -0
- package/dist/experimental/parse-argv.js +2 -0
- package/dist/experimental/registry.js +2 -0
- package/dist/generators/agents-claude.js +2 -0
- package/dist/generators/agents-md.js +2 -0
- package/dist/generators/anti-drift-validators.js +3 -0
- package/dist/generators/anti-proforma.js +2 -0
- package/dist/generators/api-e2e.js +2 -0
- package/dist/generators/api-middleware.js +2 -0
- package/dist/generators/archunit.js +2 -0
- package/dist/generators/audit-toolchain.js +2 -0
- package/dist/generators/auth.js +2 -0
- package/dist/generators/behavioral-tests.js +2 -0
- package/dist/generators/boundaries.js +2 -0
- package/dist/generators/check-all.js +2 -0
- package/dist/generators/check-stack-conformity.js +2 -0
- package/dist/generators/ci-five-lane.js +2 -0
- package/dist/generators/ci-tier.js +2 -0
- package/dist/generators/claude.js +3 -0
- package/dist/generators/codex-hooks.js +2 -0
- package/dist/generators/codex-known-limitations.js +2 -0
- package/dist/generators/codex.js +15 -0
- package/dist/generators/commit-footer.js +2 -0
- package/dist/generators/compliance-menu.js +2 -0
- package/dist/generators/compliance.js +2 -0
- package/dist/generators/conformance.js +2 -0
- package/dist/generators/contract-testing.js +2 -0
- package/dist/generators/coverage.js +2 -0
- package/dist/generators/debt-gates.js +24 -0
- package/dist/generators/debt-ratchet.js +2 -0
- package/dist/generators/derived-class.js +2 -0
- package/dist/generators/doc-set.js +4 -0
- package/dist/generators/docs.js +2 -0
- package/dist/generators/duplication.js +2 -0
- package/dist/generators/e2e-constitution.js +2 -0
- package/dist/generators/env-template.js +2 -0
- package/dist/generators/evidence-retention.js +2 -0
- package/dist/generators/feature-matrix.js +2 -0
- package/dist/generators/frontend-governance.js +2 -0
- package/dist/generators/frontend-quality.js +2 -0
- package/dist/generators/gap.js +2 -0
- package/dist/generators/gdpr.js +2 -0
- package/dist/generators/githooks.js +2 -0
- package/dist/generators/github-setup.js +2 -0
- package/dist/generators/github.js +2 -0
- package/dist/generators/gitignore.js +2 -0
- package/dist/generators/global-invariants.js +3 -0
- package/dist/generators/go-boundaries.js +2 -0
- package/dist/generators/gold-kit.js +2 -0
- package/dist/generators/infra.js +2 -0
- package/dist/generators/integration-testing.js +7 -0
- package/dist/generators/iso27001.js +2 -0
- package/dist/generators/iso9001.js +2 -0
- package/dist/generators/labels.js +2 -0
- package/dist/generators/local-wrapper.js +2 -0
- package/dist/generators/modulith.js +2 -0
- package/dist/generators/mutation.js +2 -0
- package/dist/generators/observability.js +2 -0
- package/dist/generators/operations.js +2 -0
- package/dist/generators/perf-k6.js +2 -0
- package/dist/generators/pharma.js +2 -0
- package/dist/generators/playwright-python.js +2 -0
- package/dist/generators/playwright-ts.js +2 -0
- package/dist/generators/pr-tooling.js +2 -0
- package/dist/generators/python-boundaries.js +2 -0
- package/dist/generators/quality.js +2 -0
- package/dist/generators/registry.js +2 -0
- package/dist/generators/regulated.js +2 -0
- package/dist/generators/resilience.js +2 -0
- package/dist/generators/risk-register.js +2 -0
- package/dist/generators/root.js +2 -0
- package/dist/generators/rust-boundaries.js +2 -0
- package/dist/generators/safety-class.js +2 -0
- package/dist/generators/security.js +2 -0
- package/dist/generators/seed.js +2 -0
- package/dist/generators/self-validation.js +2 -0
- package/dist/generators/ship-driver.js +2 -0
- package/dist/generators/skill-names.json +17 -0
- package/dist/generators/skills.js +2 -0
- package/dist/generators/smoke-journeys.js +2 -0
- package/dist/generators/solo-exception.js +2 -0
- package/dist/generators/ssot.js +2 -0
- package/dist/generators/stride-enforcement.js +2 -0
- package/dist/generators/suppressions.js +2 -0
- package/dist/generators/test-pyramid-manifest.js +3 -0
- package/dist/generators/test-taxonomy.js +2 -0
- package/dist/generators/wiki.js +2 -0
- package/dist/github/branch-protection.js +2 -0
- package/dist/github/classify-gh-error.js +2 -0
- package/dist/github/labels.js +2 -0
- package/dist/github/project-board.js +2 -0
- package/dist/graph/builders/adr.js +4 -0
- package/dist/graph/builders/ast.js +3 -0
- package/dist/graph/builders/canon.js +2 -0
- package/dist/graph/builders/evidence.js +2 -0
- package/dist/graph/builders/inv.js +1 -0
- package/dist/graph/builders/req.js +3 -0
- package/dist/graph/builders/test-nodes.js +3 -0
- package/dist/graph/builders/utils.js +2 -0
- package/dist/graph/load.js +2 -0
- package/dist/graph/model.js +2 -0
- package/dist/graph/query.js +3 -0
- package/dist/graph/store.js +1 -0
- package/dist/i18n/en.json +293 -0
- package/dist/i18n/index.js +4 -0
- package/dist/i18n/types.js +2 -0
- package/dist/integrations/companions.js +2 -0
- package/dist/integrations/external-review.js +10 -0
- package/dist/integrations/plugin-schema.js +2 -0
- package/dist/integrations/skill-detector.js +7 -0
- package/dist/integrations/skills-matrix.js +2 -0
- package/dist/integrations/types.d.ts +23 -0
- package/dist/integrations/types.js +2 -0
- package/dist/internal/cli-deprecation-registry.js +2 -0
- package/dist/internal/deprecate.js +3 -0
- package/dist/internal/experimental-warn.js +3 -0
- package/dist/invariants/catalog.d.ts +2 -0
- package/dist/invariants/catalog.js +1 -0
- package/dist/invariants/filter.d.ts +28 -0
- package/dist/invariants/filter.js +1 -0
- package/dist/invariants/index.d.ts +4 -0
- package/dist/invariants/index.js +2 -0
- package/dist/invariants/tiers.js +1 -0
- package/dist/invariants/types.d.ts +1 -0
- package/dist/invariants/types.js +1 -0
- package/dist/kit/applicability.js +3 -0
- package/dist/kit/audit-report.js +3 -0
- package/dist/kit/brownfield-detect.js +4 -0
- package/dist/kit/canonical-mapping.json +1 -0
- package/dist/kit/catalog.js +2 -0
- package/dist/kit/catalog.json +1 -0
- package/dist/kit/derived.json +1 -0
- package/dist/kit/emit-issues.js +8 -0
- package/dist/kit/measure.js +5 -0
- package/dist/kit/redaction.js +3 -0
- package/dist/kit/schema.js +2 -0
- package/dist/kit/taxonomy.js +2 -0
- package/dist/kit/thresholds.d.ts +12 -0
- package/dist/kit/thresholds.js +2 -0
- package/dist/kit/wave-engine.js +1 -0
- package/dist/recipes/loader.js +6 -0
- package/dist/recipes/schema.js +2 -0
- package/dist/risk/classifier.js +2 -0
- package/dist/risk/sha-check.js +2 -0
- package/dist/state/backups.js +2 -0
- package/dist/state/envelope.js +2 -0
- package/dist/state/generated-manifest.js +3 -0
- package/dist/state/manifest-key.js +2 -0
- package/dist/state/migrations/index.js +1 -0
- package/dist/state/migrations/v0-to-v1.js +2 -0
- package/dist/state/retired-renders.js +2 -0
- package/dist/templates/.nvmrc.ejs +1 -0
- package/dist/templates/HANDOFF.template.md +45 -0
- package/dist/templates/__tests__/contract/error-shape.contract.test.ts.ejs +47 -0
- package/dist/templates/agents-md/AGENTS.md.ejs +517 -0
- package/dist/templates/api-e2e/README.md.ejs +33 -0
- package/dist/templates/api-e2e/go-httptest.go.ejs +43 -0
- package/dist/templates/api-e2e/java-restassured.java.ejs +28 -0
- package/dist/templates/api-e2e/kotlin-restassured.kt.ejs +30 -0
- package/dist/templates/api-e2e/manifest.json.ejs +8 -0
- package/dist/templates/api-e2e/postman.collection.json.ejs +30 -0
- package/dist/templates/api-e2e/python-httpx.py.ejs +17 -0
- package/dist/templates/api-e2e/run.sh.ejs +61 -0
- package/dist/templates/api-e2e/ts-supertest.test.ts.ejs +18 -0
- package/dist/templates/archunit/AntiCyclicTest.java.ejs +29 -0
- package/dist/templates/archunit/AntiProformaExempt.java.ejs +35 -0
- package/dist/templates/archunit/AntiProformaTest.java.ejs +104 -0
- package/dist/templates/archunit/ArchitectureTest.java.ejs +87 -0
- package/dist/templates/archunit/DependencyFlowTest.java.ejs +55 -0
- package/dist/templates/archunit/DomainPurityTest.java.ejs +47 -0
- package/dist/templates/archunit/NamingConventionsTest.java.ejs +54 -0
- package/dist/templates/archunit/NoH2ArchTest.java.ejs +30 -0
- package/dist/templates/archunit/NoMockMvcTest.java.ejs +71 -0
- package/dist/templates/archunit/PortsIndependenceTest.java.ejs +55 -0
- package/dist/templates/archunit/RestAssuredArchTest.java.ejs +32 -0
- package/dist/templates/archunit/RestAssuredBaseIT.java.ejs +66 -0
- package/dist/templates/archunit/TestCoverageArchTest.java.ejs +29 -0
- package/dist/templates/archunit/arch-test-deps-maven.md.ejs +42 -0
- package/dist/templates/archunit/arch-test-deps.gradle.ejs +14 -0
- package/dist/templates/audit/gdpr/check-gdpr-controls.mjs.ejs +102 -0
- package/dist/templates/audit/gdpr/controls-to-gates.md.ejs +42 -0
- package/dist/templates/audit/gdpr/data-flow-map.md.ejs +30 -0
- package/dist/templates/audit/gdpr/dpia.md.ejs +45 -0
- package/dist/templates/audit/generic/audit-gate-rules.md.ejs +24 -0
- package/dist/templates/audit/generic/audit-trail-policy.md.ejs +38 -0
- package/dist/templates/audit/iso27001/iso27001-controls-gate-map.md.ejs +60 -0
- package/dist/templates/auth/setup.md.ejs +661 -0
- package/dist/templates/behavioral-tests/ExampleBehavioralTest.java.ejs +67 -0
- package/dist/templates/behavioral-tests/TESTING_POLICY.md.ejs +116 -0
- package/dist/templates/behavioral-tests/bdd/ExampleBddIT.java.ejs +19 -0
- package/dist/templates/behavioral-tests/bdd/ExampleSteps.java.ejs +61 -0
- package/dist/templates/behavioral-tests/bdd/example.feature.ejs +14 -0
- package/dist/templates/behavioral-tests/bdd/example.steps.ts.ejs +37 -0
- package/dist/templates/behavioral-tests/bdd/example_bdd_test.rs.ejs +64 -0
- package/dist/templates/behavioral-tests/bdd/example_test.go.ejs +80 -0
- package/dist/templates/behavioral-tests/bdd/test_example_bdd.py.ejs +47 -0
- package/dist/templates/behavioral-tests/eslint-playwright.json.ejs +10 -0
- package/dist/templates/behavioral-tests/eslint.config.playwright.mjs.ejs +29 -0
- package/dist/templates/behavioral-tests/example.behavioral.test.ts.ejs +47 -0
- package/dist/templates/behavioral-tests/example_behavioral_test.go.ejs +51 -0
- package/dist/templates/behavioral-tests/example_behavioral_test.rs.ejs +47 -0
- package/dist/templates/behavioral-tests/test_example_behavioral.py.ejs +40 -0
- package/dist/templates/boundaries/.eslintrc-boundaries.cjs.ejs +81 -0
- package/dist/templates/boundaries/.eslintrc-frontend-spa.cjs.ejs +79 -0
- package/dist/templates/boundaries/.importlinter.ejs +48 -0
- package/dist/templates/boundaries/check-boundaries-go.mjs.ejs +94 -0
- package/dist/templates/boundaries/check-boundaries-python.mjs.ejs +118 -0
- package/dist/templates/boundaries/check-boundaries-rust.mjs.ejs +111 -0
- package/dist/templates/boundaries/check-boundaries.mjs.ejs +34 -0
- package/dist/templates/boundaries/clippy.toml.ejs +50 -0
- package/dist/templates/boundaries/deny.toml.ejs +56 -0
- package/dist/templates/boundaries/eslint.config.boundaries.mjs.ejs +112 -0
- package/dist/templates/boundaries/eslint.config.frontend-spa.mjs.ejs +93 -0
- package/dist/templates/boundaries/golangci-boundaries.yml.ejs +70 -0
- package/dist/templates/boundaries/ruff-boundaries.toml.ejs +32 -0
- package/dist/templates/claude/CLAUDE.md.ejs +78 -0
- package/dist/templates/claude/agents/agent-write-classes.json +30 -0
- package/dist/templates/claude/agents/bridge-reviewer.md.ejs +68 -0
- package/dist/templates/claude/agents/codebase-scanner.md.ejs +69 -0
- package/dist/templates/claude/agents/context-checker.md.ejs +73 -0
- package/dist/templates/claude/agents/red-team.md.ejs +134 -0
- package/dist/templates/claude/commands/close-gold-gap.md.ejs +43 -0
- package/dist/templates/claude/commands/drain.md.ejs +81 -0
- package/dist/templates/claude/commands/gold-audit.md.ejs +43 -0
- package/dist/templates/claude/commands/impact.md.ejs +39 -0
- package/dist/templates/claude/commands/levelup.md.ejs +61 -0
- package/dist/templates/claude/commands/ship.md.ejs +902 -0
- package/dist/templates/claude/commands/tabletop.md.ejs +46 -0
- package/dist/templates/claude/commands/task.md.ejs +87 -0
- package/dist/templates/claude/commands/wt-close.md.ejs +58 -0
- package/dist/templates/claude/commands/wt-list.md.ejs +48 -0
- package/dist/templates/claude/commands/wt-open.md.ejs +61 -0
- package/dist/templates/claude/commands/wt-prune.md.ejs +38 -0
- package/dist/templates/claude/hooks/check-circular-deps.mjs.ejs +56 -0
- package/dist/templates/claude/hooks/check-no-orphan-todo.mjs.ejs +45 -0
- package/dist/templates/claude/hooks/check-no-pii.mjs.ejs +121 -0
- package/dist/templates/claude/hooks/check-no-placeholders.mjs.ejs +71 -0
- package/dist/templates/claude/hooks/check-no-skipped-tests.mjs +80 -0
- package/dist/templates/claude/hooks/check-no-unused-exports.mjs +62 -0
- package/dist/templates/claude/hooks/closer-mode-guard.mjs.ejs +102 -0
- package/dist/templates/claude/hooks/debug-state-on-failure.mjs.ejs +98 -0
- package/dist/templates/claude/hooks/enforce-gate-before-pr.mjs +142 -0
- package/dist/templates/claude/hooks/enforce-read-only.mjs +28 -0
- package/dist/templates/claude/hooks/exitplanmode-banner.mjs.ejs +43 -0
- package/dist/templates/claude/hooks/guard-done-evidence.mjs.ejs +85 -0
- package/dist/templates/claude/hooks/guard-task-completion.mjs.ejs +56 -0
- package/dist/templates/claude/hooks/hooks.mjs.ejs +127 -0
- package/dist/templates/claude/hooks/lib.mjs.ejs +523 -0
- package/dist/templates/claude/hooks/post-brainstorm-stop.mjs +48 -0
- package/dist/templates/claude/hooks/post-commit-check.mjs.ejs +65 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/go/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/java/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/python/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/backend.ejs +5 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/rust/frontend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/backend.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/docs.ejs +4 -0
- package/dist/templates/claude/hooks/post-commit-checklists/ts/frontend.ejs +5 -0
- package/dist/templates/claude/hooks/post-edit-dispatch.mjs.ejs +73 -0
- package/dist/templates/claude/hooks/post-subagent-release.mjs +80 -0
- package/dist/templates/claude/hooks/pre-compact.mjs.ejs +79 -0
- package/dist/templates/claude/hooks/pre-edit-plan-anchor.mjs.ejs +225 -0
- package/dist/templates/claude/hooks/pre-edit-ssot-guard.mjs +204 -0
- package/dist/templates/claude/hooks/pre-spawn-worktree-guard.mjs +129 -0
- package/dist/templates/claude/hooks/skill-forced-eval.mjs.ejs +281 -0
- package/dist/templates/claude/hooks/stop-dangerous.mjs +86 -0
- package/dist/templates/claude/hooks/stop-evidence-guard.mjs.ejs +245 -0
- package/dist/templates/claude/hooks/stop-finding-loss.mjs.ejs +168 -0
- package/dist/templates/claude/hooks/wiki-on-commit.mjs.ejs +38 -0
- package/dist/templates/claude/knowledge-map.json.ejs +26 -0
- package/dist/templates/claude/rules/05-agent-lifecycle.md +7 -0
- package/dist/templates/claude/rules/25-todo-folder-policy.md +5 -0
- package/dist/templates/claude/rules/40-context-economy.md +32 -0
- package/dist/templates/claude/rules/45-mcp-fallback.md +25 -0
- package/dist/templates/claude/rules/50-batch-execution.md +75 -0
- package/dist/templates/claude/rules/55-brainstorm-terminal-state.md +44 -0
- package/dist/templates/claude/rules/60-incidental-capture.md +43 -0
- package/dist/templates/claude/rules/75-impact-vault-reading.md +43 -0
- package/dist/templates/claude/rules/90-exec-protocol.md.ejs +40 -0
- package/dist/templates/claude/rules/95-closer-mode.md +45 -0
- package/dist/templates/claude/settings.json.ejs +165 -0
- package/dist/templates/claude/skills/architect-review/SKILL.md.ejs +82 -0
- package/dist/templates/claude/skills/brainstorming/SKILL.md.ejs +86 -0
- package/dist/templates/claude/skills/clean-code/SKILL.md.ejs +39 -0
- package/dist/templates/claude/skills/close-gold-gap/SKILL.md.ejs +58 -0
- package/dist/templates/claude/skills/codebase-audit/SKILL.md.ejs +88 -0
- package/dist/templates/claude/skills/configure/SKILL.md.ejs +72 -0
- package/dist/templates/claude/skills/epic-decompose/SKILL.md.ejs +86 -0
- package/dist/templates/claude/skills/gold-audit/SKILL.md.ejs +91 -0
- package/dist/templates/claude/skills/impact/SKILL.md.ejs +104 -0
- package/dist/templates/claude/skills/levelup/SKILL.md.ejs +146 -0
- package/dist/templates/claude/skills/tabletop/SKILL.md.ejs +118 -0
- package/dist/templates/claude/skills/tdd/SKILL.md.ejs +157 -0
- package/dist/templates/claude/skills/understand-code/SKILL.md.ejs +93 -0
- package/dist/templates/claude/skills/verification/SKILL.md.ejs +68 -0
- package/dist/templates/claude/skills/wave-drain/SKILL.md.ejs +491 -0
- package/dist/templates/codex/CODEX.md.ejs +140 -0
- package/dist/templates/codex/codex-adapter.mjs +132 -0
- package/dist/templates/codex/config.toml.ejs +189 -0
- package/dist/templates/compliance/compliance-mapping.md.ejs +124 -0
- package/dist/templates/compliance/compliance-menu.md.ejs +89 -0
- package/dist/templates/contract-testing/CONTRACTS_POLICY.md.ejs +30 -0
- package/dist/templates/contract-testing/api-snapshots/config-keys-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/config-response-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/enum-values-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/error-shape-baseline.json.ejs +6 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-baseline.json.ejs +12 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-content-types-baseline.json.ejs +3 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-paths-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-required-fields-baseline.json.ejs +1 -0
- package/dist/templates/contract-testing/api-snapshots/openapi-response-status-baseline.json.ejs +3 -0
- package/dist/templates/contract-testing/api-snapshots/test-snapshot.json.ejs +4 -0
- package/dist/templates/contract-testing/env/.env.pact.ejs +4 -0
- package/dist/templates/contract-testing/graphql/GraphqlSchemaTest.java.ejs +29 -0
- package/dist/templates/contract-testing/graphql/graphql-inspector.test.ts.ejs +20 -0
- package/dist/templates/contract-testing/graphql/graphql_schema_test.go.ejs +27 -0
- package/dist/templates/contract-testing/graphql/graphql_schema_test.rs.ejs +27 -0
- package/dist/templates/contract-testing/graphql/test_graphql_schema.py.ejs +21 -0
- package/dist/templates/contract-testing/grpc/GrpcContractTest.java.ejs +23 -0
- package/dist/templates/contract-testing/grpc/buf-breaking.yml.ejs +6 -0
- package/dist/templates/contract-testing/grpc/buf.yaml.ejs +11 -0
- package/dist/templates/contract-testing/grpc/grpc-contract.test.ts.ejs +16 -0
- package/dist/templates/contract-testing/grpc/grpc_contract_test.go.ejs +22 -0
- package/dist/templates/contract-testing/grpc/grpc_contract_test.rs.ejs +22 -0
- package/dist/templates/contract-testing/grpc/test_grpc_contract.py.ejs +18 -0
- package/dist/templates/contract-testing/message-queue/SchemaRegistryCheckIT.java.ejs +44 -0
- package/dist/templates/contract-testing/message-queue/schema-registry-check.ts.ejs +42 -0
- package/dist/templates/contract-testing/message-queue/schema_registry_test.go.ejs +71 -0
- package/dist/templates/contract-testing/message-queue/schema_registry_test.rs.ejs +61 -0
- package/dist/templates/contract-testing/message-queue/test_schema_registry.py.ejs +31 -0
- package/dist/templates/contract-testing/pact-samples/assignment-response.json.ejs +7 -0
- package/dist/templates/contract-testing/pact-samples/availability-response.json.ejs +13 -0
- package/dist/templates/contract-testing/pact-samples/availability-rule-response.json.ejs +8 -0
- package/dist/templates/contract-testing/pact-samples/capacity-response.json.ejs +11 -0
- package/dist/templates/contract-testing/pact-samples/fully-booked-response.json.ejs +6 -0
- package/dist/templates/contract-testing/pact-samples/schedule-override-response.json.ejs +7 -0
- package/dist/templates/contract-testing/rest-owned/PactVerificationIT.java.ejs +31 -0
- package/dist/templates/contract-testing/rest-owned/pact-consumer.test.ts.ejs +36 -0
- package/dist/templates/contract-testing/rest-owned/pact-deps.gradle.ejs +15 -0
- package/dist/templates/contract-testing/rest-owned/pact_consumer_test.go.ejs +48 -0
- package/dist/templates/contract-testing/rest-owned/pact_consumer_test.rs.ejs +23 -0
- package/dist/templates/contract-testing/rest-owned/test_pact_consumer.py.ejs +35 -0
- package/dist/templates/contract-testing/rest-public/OpenApiDiffIT.java.ejs +44 -0
- package/dist/templates/contract-testing/rest-public/export-openapi-java.gradle.ejs +13 -0
- package/dist/templates/contract-testing/rest-public/export-openapi.mjs.ejs +21 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.go.ejs +30 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.py.ejs +27 -0
- package/dist/templates/contract-testing/rest-public/export_openapi.rs.ejs +32 -0
- package/dist/templates/contract-testing/rest-public/openapi-diff.ts.ejs +42 -0
- package/dist/templates/contract-testing/rest-public/openapi_diff_test.go.ejs +35 -0
- package/dist/templates/contract-testing/rest-public/openapi_diff_test.rs.ejs +36 -0
- package/dist/templates/contract-testing/rest-public/test_openapi_diff.py.ejs +32 -0
- package/dist/templates/coverage/.coveragerc.ejs +26 -0
- package/dist/templates/coverage/.tarpaulin.toml.ejs +30 -0
- package/dist/templates/coverage/Cargo.toml.profile.release.ejs +24 -0
- package/dist/templates/coverage/jacoco-maven-setup.md.ejs +72 -0
- package/dist/templates/coverage/jacoco.gradle.ejs +49 -0
- package/dist/templates/coverage/kover.gradle.ejs +35 -0
- package/dist/templates/coverage/vitest.browser.config.ts.ejs +47 -0
- package/dist/templates/coverage/vitest.config.ts.ejs +47 -0
- package/dist/templates/docs/CODING_STANDARDS.md.ejs +81 -0
- package/dist/templates/docs/FEATURE_MATRIX.md.ejs +84 -0
- package/dist/templates/docs/GAP.md.ejs +58 -0
- package/dist/templates/docs/ISO27001_ANNEX_A.md.ejs +144 -0
- package/dist/templates/docs/MASTER_TEST_PLAN.md.ejs +52 -0
- package/dist/templates/docs/POST_MERGE_REVIEW_TEMPLATE.md.ejs +63 -0
- package/dist/templates/docs/SECURE_CODING_CHECKLIST.md.ejs +61 -0
- package/dist/templates/docs/adr/ADR-000_template.md.ejs +28 -0
- package/dist/templates/docs/bugs/bug-analysis.md.ejs +47 -0
- package/dist/templates/docs/bugs/bug-report.md.ejs +51 -0
- package/dist/templates/docs/bugs/bug-verification.md.ejs +42 -0
- package/dist/templates/docs/runbooks/deployment.md.ejs +119 -0
- package/dist/templates/docs/runbooks/prod-checklist.md.ejs +90 -0
- package/dist/templates/docs/runbooks/rollback.md.ejs +124 -0
- package/dist/templates/docs/runbooks/troubleshooting.md.ejs +130 -0
- package/dist/templates/docs/skeletons/arc42-canvas.md.ejs +46 -0
- package/dist/templates/docs/skeletons/arc42-full.md.ejs +59 -0
- package/dist/templates/docs/skeletons/decision-registry.md.ejs +38 -0
- package/dist/templates/docs/skeletons/er-model.md.ejs +19 -0
- package/dist/templates/docs/skeletons/glossary.md.ejs +8 -0
- package/dist/templates/docs/skeletons/governance.md.ejs +13 -0
- package/dist/templates/docs/skeletons/slo.md.ejs +21 -0
- package/dist/templates/docs/skeletons/technical-debt.md.ejs +8 -0
- package/dist/templates/docs/skeletons/test-strategy.md.ejs +14 -0
- package/dist/templates/docs/skeletons/threat-model-4q.md.ejs +20 -0
- package/dist/templates/docs/skeletons/threat-model-stride.md.ejs +22 -0
- package/dist/templates/docs/specs/atomic-task-list.md.ejs +57 -0
- package/dist/templates/docs/steering/product.md.ejs +44 -0
- package/dist/templates/docs/steering/structure.md.ejs +49 -0
- package/dist/templates/docs/steering/tech.md.ejs +53 -0
- package/dist/templates/documentation/cli-catalog.md.ejs +21 -0
- package/dist/templates/e2e/E2E_CONSTITUTION.md.ejs +77 -0
- package/dist/templates/e2e/playwright-python/conftest.py.ejs +22 -0
- package/dist/templates/e2e/playwright-python/run_axe.py.ejs +99 -0
- package/dist/templates/e2e/playwright-python/test_a11y.py.ejs +38 -0
- package/dist/templates/e2e/playwright-python/test_smoke.py.ejs +28 -0
- package/dist/templates/e2e/playwright-ts/a11y.spec.ts.ejs +20 -0
- package/dist/templates/e2e/playwright-ts/render-smoke.spec.ts.ejs +90 -0
- package/dist/templates/e2e/playwright-ts/run-axe.ts.ejs +74 -0
- package/dist/templates/e2e/smoke-journeys/journeys.spec.ts.ejs +53 -0
- package/dist/templates/evidence-files.json.ejs +13 -0
- package/dist/templates/frontend/FE_DESIGN_PRINCIPLES.md.ejs +115 -0
- package/dist/templates/frontend/FRONTEND_CONSTITUTION.md.ejs +170 -0
- package/dist/templates/frontend/design-tokens.json.ejs +107 -0
- package/dist/templates/frontend/vrt-setup.md.ejs +83 -0
- package/dist/templates/githooks/commit-msg.ejs +60 -0
- package/dist/templates/githooks/pre-commit.ejs +95 -0
- package/dist/templates/githooks/pre-push.ejs +251 -0
- package/dist/templates/githooks/setup-hooks.sh.ejs +8 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md +37 -0
- package/dist/templates/github/PULL_REQUEST_TEMPLATE.md.ejs +52 -0
- package/dist/templates/github/actions/build-cache/action.yml.ejs +84 -0
- package/dist/templates/github/actions/nas-ssh/action.yml.ejs +53 -0
- package/dist/templates/github/actions/setup-java-maven/action.yml.ejs +53 -0
- package/dist/templates/github/actions/setup-node-pnpm/action.yml.ejs +39 -0
- package/dist/templates/github/actions/sign-and-attest/action.yml.ejs +53 -0
- package/dist/templates/github/dependabot.yml.ejs +66 -0
- package/dist/templates/github/extended-ci-paths.txt.ejs +38 -0
- package/dist/templates/github/issue-templates/bug-report.yml +72 -0
- package/dist/templates/github/issue-templates/compliance-item.yml.ejs +75 -0
- package/dist/templates/github/issue-templates/config.yml +2 -0
- package/dist/templates/github/issue-templates/epic.yml +66 -0
- package/dist/templates/github/issue-templates/feature-request.yml +24 -0
- package/dist/templates/github/issue-templates/task-brief.yml.ejs +125 -0
- package/dist/templates/github/labels.yml.ejs +161 -0
- package/dist/templates/github/scripts/sticky-failure-issue.sh.ejs +61 -0
- package/dist/templates/github/workflows/01-pr-fast.yml.ejs +1089 -0
- package/dist/templates/github/workflows/02-pr-extended.yml.ejs +512 -0
- package/dist/templates/github/workflows/03-human-approval.yml.ejs +76 -0
- package/dist/templates/github/workflows/04-deploy-test.yml.ejs +425 -0
- package/dist/templates/github/workflows/05-release.yml.ejs +813 -0
- package/dist/templates/github/workflows/06-nightly-lite.yml.ejs +125 -0
- package/dist/templates/github/workflows/06-nightly.yml.ejs +25 -0
- package/dist/templates/github/workflows/07-weekly-lite.yml.ejs +221 -0
- package/dist/templates/github/workflows/07-weekly.yml.ejs +23 -0
- package/dist/templates/github/workflows/08-monthly.yml.ejs +23 -0
- package/dist/templates/github/workflows/09-heartbeat.yml.ejs +268 -0
- package/dist/templates/github/workflows/10-deploy-prod.yml.ejs +144 -0
- package/dist/templates/github/workflows/11-k6-on-demand.yml.ejs +43 -0
- package/dist/templates/github/workflows/12-mutation-scheduled.yml.ejs +100 -0
- package/dist/templates/github/workflows/13-archunit-extended.yml.ejs +33 -0
- package/dist/templates/github/workflows/14-license-scan.yml.ejs +45 -0
- package/dist/templates/github/workflows/15-codeql.yml.ejs +117 -0
- package/dist/templates/github/workflows/16-frontend-quality.yml.ejs +154 -0
- package/dist/templates/github/workflows/17-ossf-scorecard.yml.ejs +46 -0
- package/dist/templates/github/workflows/18-frontend-lane.yml.ejs +50 -0
- package/dist/templates/github/workflows/_ai-draft-check.yml.ejs +44 -0
- package/dist/templates/github/workflows/_contract-postman.yml.ejs +93 -0
- package/dist/templates/github/workflows/_cosign-copy/aws-ecs.ejs +90 -0
- package/dist/templates/github/workflows/_cosign-copy/azure-container-app.ejs +97 -0
- package/dist/templates/github/workflows/_cosign-copy/gcp-cloud-run.ejs +102 -0
- package/dist/templates/github/workflows/_cosign-copy/ghcr.ejs +76 -0
- package/dist/templates/github/workflows/_cosign-copy/nas-compose.ejs +69 -0
- package/dist/templates/github/workflows/_deploy/aws-ecs.ejs +78 -0
- package/dist/templates/github/workflows/_deploy/azure-container-app.ejs +59 -0
- package/dist/templates/github/workflows/_deploy/gcp-cloud-run.ejs +71 -0
- package/dist/templates/github/workflows/_deploy/ghcr.ejs +19 -0
- package/dist/templates/github/workflows/_deploy/nas-compose.ejs +45 -0
- package/dist/templates/github/workflows/_k6-runner.yml.ejs +107 -0
- package/dist/templates/github/workflows/_label-on-approve.yml.ejs +44 -0
- package/dist/templates/github/workflows/_label-sync.yml.ejs +58 -0
- package/dist/templates/github/workflows/_monthly.yml.ejs +343 -0
- package/dist/templates/github/workflows/_nightly.yml.ejs +826 -0
- package/dist/templates/github/workflows/_notify.yml.ejs +96 -0
- package/dist/templates/github/workflows/_partials/macos-keychain-bypass.ejs +6 -0
- package/dist/templates/github/workflows/_partials/macos-keychain-restore.ejs +6 -0
- package/dist/templates/github/workflows/_partials/scheduled-heavy-jobs.ejs +117 -0
- package/dist/templates/github/workflows/_partials/sigstore-preflight.ejs +8 -0
- package/dist/templates/github/workflows/_partials/trivy-fs-java.ejs +27 -0
- package/dist/templates/github/workflows/_post-merge-notify.yml.ejs +228 -0
- package/dist/templates/github/workflows/_pr-staleness.yml.ejs +85 -0
- package/dist/templates/github/workflows/_shared-security.yml.ejs +132 -0
- package/dist/templates/github/workflows/_sigstore-retry-sign.yml.ejs +102 -0
- package/dist/templates/github/workflows/_weekly.yml.ejs +313 -0
- package/dist/templates/github/workflows/drift-shadow.yml.ejs +51 -0
- package/dist/templates/github/workflows/five-lane/ci.yml.ejs +54 -0
- package/dist/templates/github/workflows/five-lane/nightly.yml.ejs +70 -0
- package/dist/templates/github/workflows/five-lane/release.yml.ejs +53 -0
- package/dist/templates/github/workflows/five-lane/weekly.yml.ejs +72 -0
- package/dist/templates/github/workflows/issue-state.yml.ejs +93 -0
- package/dist/templates/global-invariants/GLOBAL_INVARIANTS.md.ejs +34 -0
- package/dist/templates/governance/RACI.md.ejs +21 -0
- package/dist/templates/governance/ci-mental-model.md.ejs +268 -0
- package/dist/templates/governance/contract-integrity-policy.md.ejs +116 -0
- package/dist/templates/governance/enterprise-compliance-baseline.md.ejs +111 -0
- package/dist/templates/governance/evidence-retention.md.ejs +43 -0
- package/dist/templates/governance/gdpr-erasure-runbook.md.ejs +165 -0
- package/dist/templates/governance/qa-audit-phases.md.ejs +129 -0
- package/dist/templates/governance/risk-assessment-template.md.ejs +105 -0
- package/dist/templates/governance/risk-register.md.ejs +76 -0
- package/dist/templates/governance/solo-dev-exception.md.ejs +96 -0
- package/dist/templates/governance/validation-evidence.md.ejs +131 -0
- package/dist/templates/infra/azure/containerapp.tpl.yaml.ejs +74 -0
- package/dist/templates/infra/nas-compose/deploy.sh.ejs +114 -0
- package/dist/templates/integration-testing/AbstractIntegrationTest.java.ejs +36 -0
- package/dist/templates/integration-testing/NoH2ArchTest.java.ejs +24 -0
- package/dist/templates/integration-testing/conftest.py.ejs +20 -0
- package/dist/templates/integration-testing/db_fixture.rs.ejs +48 -0
- package/dist/templates/integration-testing/eslint-no-fake-db.json.ejs +19 -0
- package/dist/templates/integration-testing/eslint.config.no-fake-db.mjs.ejs +46 -0
- package/dist/templates/integration-testing/main_test.go.ejs +53 -0
- package/dist/templates/integration-testing/main_test_sqlite.go.ejs +37 -0
- package/dist/templates/integration-testing/test-setup.ts.ejs +26 -0
- package/dist/templates/integration-testing/testcontainers-deps.gradle.ejs +9 -0
- package/dist/templates/java/DeprecationInterceptor.java.ejs +50 -0
- package/dist/templates/java/archunit-hexagonal.ejs +69 -0
- package/dist/templates/java/modulith/ApplicationModulesTest.java.ejs +21 -0
- package/dist/templates/java/modulith/modulith-deps.gradle.ejs +13 -0
- package/dist/templates/java/modulith/modulith-maven-setup.md.ejs +21 -0
- package/dist/templates/java/pharma/AuditEvent.java.ejs +138 -0
- package/dist/templates/java/pharma/AuditMapper.java.ejs +60 -0
- package/dist/templates/java/pharma/archunit-pharma.ejs +128 -0
- package/dist/templates/kit/GLOBAL_KIT.md.ejs +11 -0
- package/dist/templates/kit/dim.md.ejs +33 -0
- package/dist/templates/llms-txt.config.json.ejs +22 -0
- package/dist/templates/local-wrapper/.env.example.ejs +15 -0
- package/dist/templates/local-wrapper/Makefile.ejs +44 -0
- package/dist/templates/local-wrapper/run.sh.ejs +5 -0
- package/dist/templates/middleware/410-gone-handler.ts.ejs +41 -0
- package/dist/templates/middleware/correlation-id.ts.ejs +32 -0
- package/dist/templates/middleware/deprecation.ts.ejs +45 -0
- package/dist/templates/middleware/error-handler.ts.ejs +73 -0
- package/dist/templates/middleware/payload-size-limit.ts.ejs +51 -0
- package/dist/templates/mutation/README.md.ejs +67 -0
- package/dist/templates/mutation/cargo-mutants.toml.ejs +9 -0
- package/dist/templates/mutation/mutmut-config.toml.ejs +7 -0
- package/dist/templates/mutation/parse-mutants.mjs.ejs +44 -0
- package/dist/templates/mutation/parse-mutmut.py.ejs +44 -0
- package/dist/templates/mutation/pitest-l2-setup.md.ejs +66 -0
- package/dist/templates/mutation/pitest-maven-setup.md.ejs +64 -0
- package/dist/templates/mutation/pitest.gradle.ejs +23 -0
- package/dist/templates/mutation/stryker.conf.json.ejs +17 -0
- package/dist/templates/observability/setup.md.ejs +495 -0
- package/dist/templates/operations/handbook.md.ejs +217 -0
- package/dist/templates/perf/bundle-budget.json.ejs +29 -0
- package/dist/templates/perf/k6/reports/csv-report.py.ejs +91 -0
- package/dist/templates/perf/k6/reports/html-report.py.ejs +114 -0
- package/dist/templates/perf/k6/reports/json-report.py.ejs +74 -0
- package/dist/templates/perf/k6/scenarios/breakpoint.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/burst.js.ejs +43 -0
- package/dist/templates/perf/k6/scenarios/endurance.js.ejs +36 -0
- package/dist/templates/perf/k6/scenarios/load.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/ramp-down.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/ramp-up.js.ejs +32 -0
- package/dist/templates/perf/k6/scenarios/smoke.js.ejs +23 -0
- package/dist/templates/perf/k6/scenarios/soak.js.ejs +26 -0
- package/dist/templates/perf/k6/scenarios/spike.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/steady-state.js.ejs +31 -0
- package/dist/templates/perf/k6/scenarios/stress.js.ejs +29 -0
- package/dist/templates/perf/k6/scenarios/volume.js.ejs +36 -0
- package/dist/templates/perf/k6/seed/test-data.sql.ejs +59 -0
- package/dist/templates/perf/lighthouserc.json.ejs +41 -0
- package/dist/templates/quality/iso9001/CAPA_LOG.md.ejs +36 -0
- package/dist/templates/quality/iso9001/DOCUMENT_CONTROL.md.ejs +36 -0
- package/dist/templates/quality/iso9001/REQUIREMENTS_TRACEABILITY.md.ejs +61 -0
- package/dist/templates/regulated/overlay.json.ejs +26 -0
- package/dist/templates/regulated/regulated-overlay.md.ejs +40 -0
- package/dist/templates/resilience/RESILIENCE.md.ejs +310 -0
- package/dist/templates/root/.gitignore.ejs +85 -0
- package/dist/templates/root/CODEOWNERS.ejs +18 -0
- package/dist/templates/root/CONTRIBUTING.md.ejs +62 -0
- package/dist/templates/root/SECURITY.md.ejs +19 -0
- package/dist/templates/root/TEST_TAXONOMY.md.ejs +100 -0
- package/dist/templates/root/commitlint.config.js.ejs +4 -0
- package/dist/templates/root/docs/METHOD/CANONICAL_PATHS.md.ejs +24 -0
- package/dist/templates/root/docs/METHOD/ENGINEERING_DEFAULTS.md.ejs +136 -0
- package/dist/templates/root/docs/METHOD/KNOWLEDGE_MAP.md.ejs +90 -0
- package/dist/templates/root/docs/METHOD/PATTERNS_CATALOG.md.ejs +51 -0
- package/dist/templates/root/docs/METHOD/REUSE_REGISTRY.md.ejs +26 -0
- package/dist/templates/root/docs/METHOD/REUSE_REGISTRY_SPEC.md.ejs +80 -0
- package/dist/templates/root/docs/METHOD/SSOT_CORE_SET.md.ejs +62 -0
- package/dist/templates/root/docs/METHOD/TRACK_ROUTER.md.ejs +39 -0
- package/dist/templates/root/docs/SYSTEM/CANON.md.ejs +359 -0
- package/dist/templates/root/docs/SYSTEM/FAIL_CLOSED.md.ejs +171 -0
- package/dist/templates/root/editorconfig.ejs +39 -0
- package/dist/templates/root/tsconfig.json.ejs +21 -0
- package/dist/templates/rust/clippy.toml.ejs +14 -0
- package/dist/templates/schemas/milestone.schema.json.ejs +164 -0
- package/dist/templates/schemas/rtm-verdict.schema.json.ejs +52 -0
- package/dist/templates/schemas/source-record.schema.json.ejs +91 -0
- package/dist/templates/schemas/tabletop-evidence.schema.json.ejs +53 -0
- package/dist/templates/schemas/use-case.schema.json.ejs +59 -0
- package/dist/templates/scripts/apply-branch-protection.mjs.ejs +70 -0
- package/dist/templates/scripts/audit-toolchain.mjs.ejs +112 -0
- package/dist/templates/scripts/bg-run.sh.ejs +59 -0
- package/dist/templates/scripts/capacity-probe.mjs.ejs +147 -0
- package/dist/templates/scripts/capture-debt-baseline.mjs.ejs +99 -0
- package/dist/templates/scripts/check-acceptance.mjs.ejs +298 -0
- package/dist/templates/scripts/check-action-pins.mjs.ejs +215 -0
- package/dist/templates/scripts/check-agent-return.mjs.ejs +177 -0
- package/dist/templates/scripts/check-all.mjs.ejs +1746 -0
- package/dist/templates/scripts/check-anti-fake-green.mjs.ejs +80 -0
- package/dist/templates/scripts/check-anti-proforma.mjs.ejs +547 -0
- package/dist/templates/scripts/check-api-e2e.mjs.ejs +106 -0
- package/dist/templates/scripts/check-arc42-slots.mjs.ejs +25 -0
- package/dist/templates/scripts/check-assertion-delta.mjs.ejs +287 -0
- package/dist/templates/scripts/check-audit-dry-pass.mjs.ejs +176 -0
- package/dist/templates/scripts/check-bundle-size.mjs.ejs +151 -0
- package/dist/templates/scripts/check-canonical-paths.mjs.ejs +39 -0
- package/dist/templates/scripts/check-ci-tiers.mjs.ejs +111 -0
- package/dist/templates/scripts/check-claude-md-lint.mjs.ejs +142 -0
- package/dist/templates/scripts/check-collab-mode-wired.mjs.ejs +60 -0
- package/dist/templates/scripts/check-commit-footer-rationale.mjs.ejs +170 -0
- package/dist/templates/scripts/check-constraint-scan.mjs.ejs +681 -0
- package/dist/templates/scripts/check-consumer-audit.mjs.ejs +414 -0
- package/dist/templates/scripts/check-continue-on-error.mjs.ejs +227 -0
- package/dist/templates/scripts/check-cross-model-review.mjs.ejs +458 -0
- package/dist/templates/scripts/check-decision-registry.mjs.ejs +101 -0
- package/dist/templates/scripts/check-doc-freshness.mjs.ejs +17 -0
- package/dist/templates/scripts/check-doc-links.mjs.ejs +98 -0
- package/dist/templates/scripts/check-doc-set.mjs.ejs +18 -0
- package/dist/templates/scripts/check-docs.mjs.ejs +71 -0
- package/dist/templates/scripts/check-domain-api-surface.mjs.ejs +80 -0
- package/dist/templates/scripts/check-drift.mjs.ejs +167 -0
- package/dist/templates/scripts/check-duplication.mjs.ejs +45 -0
- package/dist/templates/scripts/check-e2e-escalation.mjs.ejs +199 -0
- package/dist/templates/scripts/check-e2e-quarantine.mjs.ejs +68 -0
- package/dist/templates/scripts/check-emission-parity.mjs.ejs +91 -0
- package/dist/templates/scripts/check-exit-code-contract.mjs.ejs +138 -0
- package/dist/templates/scripts/check-fail-closed-audit.mjs.ejs +389 -0
- package/dist/templates/scripts/check-fe-boundaries.mjs.ejs +184 -0
- package/dist/templates/scripts/check-feature-matrix.mjs.ejs +835 -0
- package/dist/templates/scripts/check-frontend-lane.mjs.ejs +73 -0
- package/dist/templates/scripts/check-grace-window.mjs.ejs +102 -0
- package/dist/templates/scripts/check-handoff-doc.mjs.ejs +192 -0
- package/dist/templates/scripts/check-hook-routing.mjs.ejs +224 -0
- package/dist/templates/scripts/check-image-pins.mjs.ejs +90 -0
- package/dist/templates/scripts/check-inline-suppressions.mjs.ejs +153 -0
- package/dist/templates/scripts/check-iso9001.mjs.ejs +146 -0
- package/dist/templates/scripts/check-knowledge-map.mjs.ejs +49 -0
- package/dist/templates/scripts/check-local-ci-parity.mjs.ejs +382 -0
- package/dist/templates/scripts/check-m16-handoff.mjs.ejs +112 -0
- package/dist/templates/scripts/check-merge-method.mjs.ejs +92 -0
- package/dist/templates/scripts/check-milestones.mjs.ejs +678 -0
- package/dist/templates/scripts/check-min-test-execution.mjs.ejs +225 -0
- package/dist/templates/scripts/check-muted-test.mjs.ejs +270 -0
- package/dist/templates/scripts/check-no-stub-redirects.mjs.ejs +154 -0
- package/dist/templates/scripts/check-no-tracked-artifacts.mjs.ejs +134 -0
- package/dist/templates/scripts/check-oracle-discrimination.mjs.ejs +244 -0
- package/dist/templates/scripts/check-pii-scan.mjs.ejs +67 -0
- package/dist/templates/scripts/check-pipe-tee-hazard.mjs.ejs +85 -0
- package/dist/templates/scripts/check-pr-size-gate.mjs.ejs +85 -0
- package/dist/templates/scripts/check-refutation-verdicts.mjs.ejs +256 -0
- package/dist/templates/scripts/check-regulated-overlay.mjs.ejs +158 -0
- package/dist/templates/scripts/check-render-smoke.mjs.ejs +100 -0
- package/dist/templates/scripts/check-reuse-registry.mjs.ejs +44 -0
- package/dist/templates/scripts/check-review-completion.mjs.ejs +708 -0
- package/dist/templates/scripts/check-safety-adopt-ratchet.mjs.ejs +142 -0
- package/dist/templates/scripts/check-secret-presence.mjs.ejs +191 -0
- package/dist/templates/scripts/check-secret-scan.mjs.ejs +82 -0
- package/dist/templates/scripts/check-skip-critical-e2e.mjs.ejs +142 -0
- package/dist/templates/scripts/check-smoke-journeys.mjs.ejs +272 -0
- package/dist/templates/scripts/check-solo-reactivation.mjs.ejs +102 -0
- package/dist/templates/scripts/check-sources.mjs.ejs +245 -0
- package/dist/templates/scripts/check-ssot-core.mjs.ejs +45 -0
- package/dist/templates/scripts/check-stack-conformity.mjs.ejs +160 -0
- package/dist/templates/scripts/check-stride-traceability.mjs.ejs +101 -0
- package/dist/templates/scripts/check-suppression-expiry.mjs.ejs +79 -0
- package/dist/templates/scripts/check-suppression-rationale.mjs.ejs +76 -0
- package/dist/templates/scripts/check-suppressions.mjs.ejs +150 -0
- package/dist/templates/scripts/check-tabletop-evidence.mjs.ejs +499 -0
- package/dist/templates/scripts/check-tdd-evidence.mjs.ejs +381 -0
- package/dist/templates/scripts/check-test-naming.mjs.ejs +151 -0
- package/dist/templates/scripts/check-test-pyramid.mjs.ejs +170 -0
- package/dist/templates/scripts/check-test-scope-tier.mjs.ejs +208 -0
- package/dist/templates/scripts/check-tier-coverage.mjs.ejs +71 -0
- package/dist/templates/scripts/check-todo-max-age.mjs.ejs +205 -0
- package/dist/templates/scripts/check-touched-vs-manifest.mjs.ejs +179 -0
- package/dist/templates/scripts/check-unwired-guards.mjs.ejs +367 -0
- package/dist/templates/scripts/check-use-cases.mjs.ejs +345 -0
- package/dist/templates/scripts/check-validator-helptext.mjs.ejs +80 -0
- package/dist/templates/scripts/check-wiki-lint.mjs.ejs +162 -0
- package/dist/templates/scripts/check-workflow-docs-sync.mjs.ejs +91 -0
- package/dist/templates/scripts/check-workflow-job-naming.mjs.ejs +102 -0
- package/dist/templates/scripts/check-workflow-perms.mjs.ejs +54 -0
- package/dist/templates/scripts/check-workflow-runners.mjs.ejs +87 -0
- package/dist/templates/scripts/check-workflow-sha-pinning.mjs.ejs +89 -0
- package/dist/templates/scripts/check-workflow-test-integrity.mjs.ejs +235 -0
- package/dist/templates/scripts/checks/check-rust-no-unsafe.mjs.ejs +58 -0
- package/dist/templates/scripts/checks/check-rust-no-unwrap.mjs.ejs +61 -0
- package/dist/templates/scripts/ci-classify-changes.mjs.ejs +119 -0
- package/dist/templates/scripts/conformance.mjs.ejs +37 -0
- package/dist/templates/scripts/constraint-map.json.ejs +25 -0
- package/dist/templates/scripts/debt-lib.mjs.ejs +1015 -0
- package/dist/templates/scripts/debt-report.mjs.ejs +199 -0
- package/dist/templates/scripts/domain-api-surface.json.ejs +23 -0
- package/dist/templates/scripts/done-evidence.mjs.ejs +359 -0
- package/dist/templates/scripts/evidence-collect.mjs.ejs +264 -0
- package/dist/templates/scripts/evidence-prune.mjs.ejs +96 -0
- package/dist/templates/scripts/evidence-rotate.mjs.ejs +53 -0
- package/dist/templates/scripts/evidence-writer.sh.ejs +220 -0
- package/dist/templates/scripts/gate-registry.yml.ejs +286 -0
- package/dist/templates/scripts/gen-doc-index.mjs.ejs +19 -0
- package/dist/templates/scripts/gen-gap.mjs.ejs +153 -0
- package/dist/templates/scripts/gen-llms-txt.mjs.ejs +18 -0
- package/dist/templates/scripts/gen-wiki.mjs.ejs +196 -0
- package/dist/templates/scripts/gh-cleanup-expired-artifacts.mjs.ejs +134 -0
- package/dist/templates/scripts/gold-audit.mjs.ejs +19 -0
- package/dist/templates/scripts/i18n-literal-scanner.mjs.ejs +175 -0
- package/dist/templates/scripts/ingest-zap-report.mjs.ejs +107 -0
- package/dist/templates/scripts/inject-pact-samples.sh.ejs +96 -0
- package/dist/templates/scripts/issue-readiness.mjs.ejs +84 -0
- package/dist/templates/scripts/knowledge-map-update.mjs.ejs +32 -0
- package/dist/templates/scripts/lib/acceptance-criteria.mjs.ejs +213 -0
- package/dist/templates/scripts/lib/agent-return-validate.mjs.ejs +481 -0
- package/dist/templates/scripts/lib/coverage-gate.mjs.ejs +54 -0
- package/dist/templates/scripts/lib/e2e-reliability.mjs.ejs +207 -0
- package/dist/templates/scripts/lib/ephemeral-server.mjs.ejs +148 -0
- package/dist/templates/scripts/lib/evidence-binding.mjs.ejs +140 -0
- package/dist/templates/scripts/lib/exact-sha-policy.mjs.ejs +233 -0
- package/dist/templates/scripts/lib/gate-args.mjs.ejs +20 -0
- package/dist/templates/scripts/lib/gate-evidence.mjs.ejs +622 -0
- package/dist/templates/scripts/lib/gate-mutex.mjs.ejs +340 -0
- package/dist/templates/scripts/lib/glob-walk.mjs.ejs +148 -0
- package/dist/templates/scripts/lib/run-helpers.mjs.ejs +416 -0
- package/dist/templates/scripts/lib/seed-common.sh.ejs +105 -0
- package/dist/templates/scripts/lib/waiter-count.mjs.ejs +62 -0
- package/dist/templates/scripts/muted-tests-baseline.json.ejs +4 -0
- package/dist/templates/scripts/optional-emissions.json.ejs +117 -0
- package/dist/templates/scripts/oracle-discrimination-baseline.json.ejs +6 -0
- package/dist/templates/scripts/pid-watch.sh.ejs +78 -0
- package/dist/templates/scripts/pii-scan.mjs.ejs +125 -0
- package/dist/templates/scripts/pr-merge-watch.mjs.ejs +445 -0
- package/dist/templates/scripts/record-agent-return.mjs.ejs +279 -0
- package/dist/templates/scripts/record-journey-evidence.mjs.ejs +69 -0
- package/dist/templates/scripts/rework-log.mjs.ejs +172 -0
- package/dist/templates/scripts/run-postman-tests.sh.ejs +112 -0
- package/dist/templates/scripts/schemas/agent-return-external.schema.json +74 -0
- package/dist/templates/scripts/schemas/agent-return.schema.json.ejs +181 -0
- package/dist/templates/scripts/schemas/cross-model-dispatch.schema.json +95 -0
- package/dist/templates/scripts/seed-test-data.sh.ejs +99 -0
- package/dist/templates/scripts/self-validation.mjs.ejs +126 -0
- package/dist/templates/scripts/setup-repo.sh.ejs +78 -0
- package/dist/templates/scripts/spotbugs-baseline.json.ejs +4 -0
- package/dist/templates/scripts/test-gate-layering.mjs.ejs +92 -0
- package/dist/templates/scripts/validate-api-snapshots.mjs.ejs +86 -0
- package/dist/templates/scripts/validate-k6-scenarios.mjs.ejs +96 -0
- package/dist/templates/scripts/validate-openapi-field-types.mjs.ejs +124 -0
- package/dist/templates/scripts/validate-postman-collection.mjs.ejs +159 -0
- package/dist/templates/scripts/verify-fe-coverage.mjs.ejs +138 -0
- package/dist/templates/scripts/verify-i18n-parity.mjs.ejs +114 -0
- package/dist/templates/scripts/verify-module-coverage.mjs.ejs +207 -0
- package/dist/templates/scripts/verify-mutation-baseline.mjs.ejs +209 -0
- package/dist/templates/scripts/verify-spotbugs.mjs.ejs +216 -0
- package/dist/templates/scripts/verify-tokens.mjs.ejs +177 -0
- package/dist/templates/security/RISK_ASSESSMENT.md.ejs +35 -0
- package/dist/templates/security/STRIDE.md.ejs +24 -0
- package/dist/templates/security/gitleaks.toml.ejs +21 -0
- package/dist/templates/security/zap/baseline-auth.context.ejs +58 -0
- package/dist/templates/security/zap/rules.tsv.ejs +100 -0
- package/dist/templates/ship/TICK_PROMPT.md.ejs +55 -0
- package/dist/templates/ship/supervisor.sh.ejs +46 -0
- package/dist/templates/smoke-journeys/manifest.json.ejs +1 -0
- package/dist/templates/sonar-project.properties.ejs +25 -0
- package/dist/templates/standards/doc-profile.ejs +27 -0
- package/dist/templates/standards/gold-doc-set.yml.ejs +528 -0
- package/dist/templates/standards/gold-registry.java.yml.ejs +266 -0
- package/dist/templates/standards/gold-registry.typescript.yml.ejs +104 -0
- package/dist/templates/standards/gold-registry.yml.ejs +119 -0
- package/dist/templates/standards/thresholds.yml.ejs +86 -0
- package/dist/templates/static-analysis/.dependency-cruiser.cjs.ejs +78 -0
- package/dist/templates/static-analysis/.golangci.yml.ejs +70 -0
- package/dist/templates/static-analysis/.shellcheckrc.ejs +13 -0
- package/dist/templates/static-analysis/.yamllint.yml.ejs +41 -0
- package/dist/templates/static-analysis/checkstyle.xml.ejs +83 -0
- package/dist/templates/static-analysis/detekt.yml.ejs +26 -0
- package/dist/templates/static-analysis/eslint.config.mjs.ejs +54 -0
- package/dist/templates/static-analysis/eslint.config.static.mjs.ejs +38 -0
- package/dist/templates/static-analysis/eslintrc-static.json.ejs +30 -0
- package/dist/templates/static-analysis/jscpd.json.ejs +25 -0
- package/dist/templates/static-analysis/knip.json.ejs +26 -0
- package/dist/templates/static-analysis/pmd-ruleset.xml.ejs +92 -0
- package/dist/templates/static-analysis/prettierignore.ejs +59 -0
- package/dist/templates/static-analysis/prettierrc.json.ejs +10 -0
- package/dist/templates/static-analysis/requirements-dev.txt.ejs +11 -0
- package/dist/templates/static-analysis/ruff.toml.ejs +40 -0
- package/dist/templates/static-analysis/rustfmt.toml.ejs +3 -0
- package/dist/templates/static-analysis/spotbugs-exclude.xml.ejs +120 -0
- package/dist/templates/static-analysis/spotbugs.gradle.ejs +18 -0
- package/dist/templates/static-analysis/spotless.gradle.ejs +38 -0
- package/dist/templates/static-analysis/stylelintrc.json.ejs +16 -0
- package/dist/templates/suppressions/archunit-baseline.json.ejs +1 -0
- package/dist/templates/suppressions/consumer-audit-allowlist.json.ejs +1 -0
- package/dist/templates/suppressions/gitleaksignore.ejs +20 -0
- package/dist/templates/suppressions/pii-allowlist.json.ejs +1 -0
- package/dist/templates/suppressions/suppressions-schema.json.ejs +39 -0
- package/dist/templates/suppressions/trivyignore.ejs +17 -0
- package/dist/templates/testing/test-taxonomy.md.ejs +120 -0
- package/dist/test/example.behavioral.test.js +2 -0
- package/dist/types/memory.d.ts +19 -0
- package/dist/types/memory.js +1 -0
- package/dist/types/plan.d.ts +102 -0
- package/dist/types/plan.js +2 -0
- package/dist/types/plugin.d.ts +45 -0
- package/dist/types/plugin.js +1 -0
- package/dist/utils/canon-loader.js +2 -0
- package/dist/utils/channel.js +1 -0
- package/dist/utils/config.d.ts +79 -0
- package/dist/utils/config.js +5 -0
- package/dist/utils/confirm-downgrade.js +5 -0
- package/dist/utils/env.js +2 -0
- package/dist/utils/error-catalog.js +2 -0
- package/dist/utils/errors.js +2 -0
- package/dist/utils/evidence-log.js +3 -0
- package/dist/utils/file-lock.js +2 -0
- package/dist/utils/first-run.js +8 -0
- package/dist/utils/fs.js +3 -0
- package/dist/utils/github-issue-helper.js +4 -0
- package/dist/utils/gradle.js +21 -0
- package/dist/utils/json-output.js +2 -0
- package/dist/utils/logger-config.js +4 -0
- package/dist/utils/logger.js +3 -0
- package/dist/utils/maturity-check.js +2 -0
- package/dist/utils/pkg.js +3 -0
- package/dist/utils/platform.js +2 -0
- package/dist/utils/plugin-loader.js +2 -0
- package/dist/utils/plugin-worker.js +2 -0
- package/dist/utils/prettier-format.js +2 -0
- package/dist/utils/profiler.js +2 -0
- package/dist/utils/render.js +2 -0
- package/dist/utils/replay.js +3 -0
- package/dist/utils/run-cli.js +6 -0
- package/dist/utils/run-id.js +3 -0
- package/dist/utils/safe-read.js +2 -0
- package/dist/utils/task-id.js +2 -0
- package/dist/utils/today.js +2 -0
- package/dist/utils/tty.js +2 -0
- package/dist/utils/walk-dir.js +2 -0
- package/dist/verify/rules/drive-by-scope.js +1 -0
- package/dist/verify/rules/orphan-todos.js +1 -0
- package/dist/verify/rules/registry.js +1 -0
- package/dist/verify/rules/skip-patterns.js +1 -0
- package/dist/verify/rules/types.d.ts +15 -0
- package/dist/verify/rules/types.js +1 -0
- package/dist/verify/rules/ui-language.js +1 -0
- package/dist/verify/run.js +3 -0
- package/dist/wizard/archetype-defaults.js +1 -0
- package/dist/wizard/presets.js +1 -0
- package/dist/wizard/prompts.js +44 -0
- package/dist/wizard/types.d.ts +637 -0
- package/dist/wizard/types.js +1 -0
- package/dist/worktree/harvest.js +2 -0
- package/dist/worktree/links.js +2 -0
- package/dist/worktree/paths.js +2 -0
- package/dist/worktree/validate.js +3 -0
- package/package.json +124 -0
- package/scripts/check-arc42-slots.mjs +738 -0
- package/scripts/check-doc-freshness.mjs +236 -0
- package/scripts/check-doc-set.mjs +243 -0
- package/scripts/check-doc-style.mjs +228 -0
- package/scripts/gold-audit.mjs +319 -0
- package/scripts/lib/doc-gate-allowlist.mjs +123 -0
- package/scripts/lib/doc-set-resolve.mjs +274 -0
- package/scripts/lib/glob-walk.mjs +148 -0
- package/scripts/lib/gold-audit-lib.mjs +1260 -0
- package/scripts/lib/run-helpers.mjs +413 -0
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{ensureDir,writeFileTranslated}from"../utils/fs.js";import{UserFacingError}from"../utils/errors.js";import{t}from"../i18n/index.js";import{join}from"node:path";import{z}from"zod";import{PHASE_ORDER,readUnifiedState,writeUnifiedState,appendLog,normalizeChainId,reviewStateOf}from"./task-state.js";import{runTaskAdvance}from"./task.js";import{sanitizeTaskId}from"../worktree/paths.js";import{autonomyAllows,CONSUMER_DEFAULT_PROFILE,SELF_ONLY_GATES,resolveShipProfile}from"./ship-profile.js";import{companionGreenInstruction,companionStatusLine}from"../integrations/companions.js";import{runCli}from"../utils/run-cli.js";import{planCrossModelSlots}from"../integrations/external-review.js";import{gatherTierSignals,normTier,widenTier}from"./ship-tier.js";import{appendChainIds,evaluateSeal,evaluateSeedSize,hasShipTaskId,resolveTrainLimits,shipTaskChanged}from"./ship-train.js";import{evaluateReviewRound,resolveReviewMaxRounds,reviewScopeLine}from"./ship-review.js";import{shipConfigFor}from"./ship-config.js";function normalizeShipTaskId(raw){const id=sanitizeTaskId(raw);if(!/^#\d+$/.test(id))throw new Error(`Invalid ship task id "${raw}" \u2014 expected a GitHub issue number like "1280" or "#1280".`);return id}const CompanionEvidenceV1=z.object({$schemaVersion:z.literal(1),companions:z.array(z.object({id:z.string().min(1),mode:z.enum(["lite","full"])})).min(1),diffStats:z.object({files:z.number().int().nonnegative(),insertions:z.number().int().nonnegative(),deletions:z.number().int().nonnegative()}),recordedAt:z.iso.datetime()}),REDTEAM_AGENTS={XS:1,S:2,Standard:3},REVIEW_AGENTS={XS:1,S:1,Standard:2};export const REVIEW_AGENTS_SECURITY_SURFACE=3;const FLOOR_XS=["bugs","type-safety","domain"],FLOOR_S_ADD=["test-quality"],FLOOR_STD_ADD=["security","data-integrity","silent-failures"];export function verticalsForTier(tier){return tier==="XS"?[...FLOOR_XS]:tier==="S"?[...FLOOR_XS,...FLOOR_S_ADD]:[...FLOOR_XS,...FLOOR_S_ADD,...FLOOR_STD_ADD]}function normalizeShipStepTail(tail){return Array.isArray(tail)?{chainIds:tail}:tail}export function shipStepFor(phase,tier,profile=CONSUMER_DEFAULT_PROFILE,taskId,tail=[]){const t2=normTier(tier),verticals=verticalsForTier(t2),normalizedTail=normalizeShipStepTail(tail);return(step=>({...step,verticals}))(shipStepBody(phase,t2,profile,{...normalizedTail,verticals,...taskId!==void 0?{taskId}:{}}))}function greenAction(profile){const base="Implement the minimum to make the tests pass.",companion=companionGreenInstruction(profile.companions);return companion?`${base} ${companion}`:base}function closeAction(){return"CLOSER mode: single named target, no new issues or refactor beyond the diff (findings \u2192 PARKING list, one line, no action). Same error twice \u2192 5-line root-cause, else declare BLOCKED. Commit the candidate, then run `node scripts/check-all.mjs L2` once before push. Foreground-wait on the PR/gate checks; never end the turn on a promise."}function closeIssuesPhrase(taskId,chainIds){if(chainIds.length===0)return"Close the issue, clean up the worktree.";const display=id=>id.startsWith("#")?id:`#${id}`;return`Close issues ${[taskId,...chainIds].filter(v=>!!v).map(display).join(", ")}, clean up the worktree.`}function completeAction(profile,taskId,chainIds=[]){const closePhrase=closeIssuesPhrase(taskId,chainIds);return profile.collaborationMode!=="trunk-solo"?`Commit, push, open a PR; await required review + checks, then merge. ${closePhrase}`:profile.mergeMode==="direct"?`Commit and push to the project's default branch through its gate (no PR). ${closePhrase}`:`Commit, push, open a PR, fast-forward merge once checks pass. ${closePhrase}`}function verificationSelfOnlyChecks(profile){return profile.isArbiterSelf?[...SELF_ONLY_GATES]:[]}const RED_TEAM_REVIEW_PHASE="red-team-review",RED_TEAM_REWORK_PHASE="red-team-rework",REFACTOR_PHASE="refactor";function isReviewPhase(phase){return phase===RED_TEAM_REVIEW_PHASE||phase===RED_TEAM_REWORK_PHASE||phase===REFACTOR_PHASE}function reviewScopeFor(plan){if(!(plan===void 0||plan.rounds<2||plan.base===null))return reviewScopeLine(plan.base,plan.rounds,plan.maxRounds)}function reviewPhaseStepBody(phase,t2,profile,context){const{verticals,externalModelAccess,review:reviewPlan}=context;if(phase===RED_TEAM_REVIEW_PHASE)return{phase,action:`Dispatch ${REDTEAM_AGENTS[t2]} red-team agent(s); route CRITICAL findings to red-team-rework.`,reviewAgents:REDTEAM_AGENTS[t2]};if(phase===RED_TEAM_REWORK_PHASE)return{phase,action:"Revise the plan for CRITICAL findings, then re-run red-team-review.",reviewAgents:0};const reviewAgents=profile.collaborationMode==="trunk-solo"?1:REVIEW_AGENTS[t2],externalCount=planCrossModelSlots({tier:t2,phase,totalSlots:reviewAgents,verticals,...profile.crossModelReview!==void 0?{cfg:profile.crossModelReview}:{},...externalModelAccess!==void 0?{access:externalModelAccess}:{}}).external.length,scope=reviewScopeFor(reviewPlan),step={phase,action:externalCount>0?`Clean up, then dispatch ${reviewAgents-externalCount} Anthropic code-review agent(s) + ${externalCount} Codex reviewer(s); panel total: ${reviewAgents}.`:`Clean up, then dispatch ${reviewAgents} code-review agent(s) + 1 adversarial verifier.`,reviewAgents,...scope!==void 0?{reviewScope:scope}:{}};return externalCount>0?{...step,externalReviewers:externalCount}:step}function shipStepBody(phase,t2,profile,context){if(isReviewPhase(phase))return reviewPhaseStepBody(phase,t2,profile,context);switch(phase){case"preflight":return{phase,action:"Open the worktree, read the issue, write task state.",command:"arbiter task init --id <id> --tier <tier> --plan <path>",reviewAgents:0};case"plan":return{phase,action:"Write the plan, then dispatch the plan-review agents; their PASS verdict in .arbiter/evidence/plan-review/<id>/latest.json is the gate.",command:`arbiter task advance --to ${nextPhase(phase)??"red-team-review"}`,reviewAgents:0};case"red":return{phase,action:"Write failing tests first (TDD red); record evidence.",command:"arbiter task record-red --test-path <path>",reviewAgents:0};case"green":return{phase,action:greenAction(profile),reviewAgents:0};case"verification":return{phase,action:`Run the ${profile.defaultGateLevel} gate; fix any failures.`,command:`node scripts/check-all.mjs ${profile.defaultGateLevel}`,reviewAgents:0,selfOnlyChecks:verificationSelfOnlyChecks(profile)};case"close":return{phase,action:closeAction(),reviewAgents:0};case"complete":return{phase,action:completeAction(profile,context.taskId,context.chainIds),reviewAgents:0}}}export function nextPhase(current){const idx=PHASE_ORDER.indexOf(current);return idx===-1||idx>=PHASE_ORDER.length-1?null:PHASE_ORDER[idx+1]??null}function advanceTargetFor(phase){return phase==="red-team-rework"?"red-team-review":nextPhase(phase)}function optionalShipStepLines(result){const lines=[];return result.step.command&&lines.push(`Command: ${result.step.command}`),result.step.reviewAgents>0&&lines.push(`Review agents: ${result.step.reviewAgents}`),result.step.externalReviewers!==void 0&&lines.push(`External reviewers: ${result.step.externalReviewers}`),result.step.reviewScope!==void 0&&lines.push(`Review scope: ${result.step.reviewScope}`),lines}export function buildShipStepLines(result,legacyTier){const tier=result.tier??normTier(legacyTier),lines=[`Phase: ${result.phase}${result.done?" (done)":""}`,`Action: ${result.step.action}`];lines.push(...optionalShipStepLines(result)),lines.push(`Tier: ${tier} \xB7 verticals: ${result.step.verticals.join(", ")}`),lines.push(`Governance: ${result.profile.governanceLevel}`),lines.push(`Autonomy: ${result.profile.autonomy}`),result.phase==="complete"&&!autonomyAllows(result.profile.autonomy,"auto-merge")&&lines.push("Autonomy gate: STOP \u2014 merging requires a human at L0 (set automation.autonomy or pass --autonomy).");const selfOnly=result.step.selfOnlyChecks??[];selfOnly.length>0&&lines.push(`Self-only checks: ${selfOnly.join(", ")}`);const companions=result.profile.companions;return companions.length>0&&lines.push(`Companion: ${companionStatusLine(companions)} \xB7 arbiter gates remain the safety net`),lines}function assertSeedWithinLimit(existing,taskId,chainIds,limits){const verdict=evaluateSeedSize(existing,taskId,chainIds,limits);if(verdict.ok)return;const seal={reason:"max-chain",detail:verdict.detail};throw new UserFacingError(t("errors.E_TRAIN_SEALED",seal))}function trainLimitsFor(ship,opts){return opts.trainLimits??resolveTrainLimits(ship)}function seedShipState(root,opts,limits){const taskId=opts.taskId!==void 0?normalizeShipTaskId(opts.taskId):void 0,chainIds=opts.chainIds!==void 0?opts.chainIds.map(normalizeChainId):void 0,existing=readUnifiedState(root);assertSeedWithinLimit(existing,taskId,chainIds,limits),(existing===null||taskId!==void 0||opts.tier!==void 0||opts.overrides!==void 0||chainIds!==void 0)&&writeUnifiedState(root,{...taskId!==void 0?{taskId}:{},...opts.tier!==void 0?{tier:opts.tier}:{},...opts.overrides!==void 0?{overrides:opts.overrides}:{},...chainIds!==void 0?{chainIds}:{}})}function shipProfileFor(root,opts){return opts.profileOverride??resolveShipProfile(root,{...opts.autonomy!==void 0?{autonomyOverride:opts.autonomy}:{},...opts.overrides!==void 0?{overrides:opts.overrides}:{}})}function advanceShipPhase(root,phase,opts){if(!opts.advance)return{phase,advanced:!1};const target=advanceTargetFor(phase);return target===null?{phase,advanced:!1}:(runTaskAdvance({to:target,dir:root,...opts.advanceOpts??{}}),appendLog(root,`ship \u2192 advanced to ${target}`),{phase:target,advanced:!0})}function companionEvidencePath(taskId,repoDir){return join(repoDir,".arbiter","evidence","companions",`${taskId}.json`)}function writeCompanionEvidence(root,taskId,profile,opts){if(profile.isArbiterSelf||profile.companions.length===0)return null;const evidence={$schemaVersion:1,companions:profile.companions.map(c=>({id:c.id,mode:c.mode})),diffStats:(opts.gatherCompanionDiffStats??gatherCompanionDiffStats)(root),recordedAt:opts.recordedAt??new Date().toISOString()},parsed=CompanionEvidenceV1.safeParse(evidence);if(!parsed.success)throw new Error(`Invalid companion evidence: ${parsed.error.message}`);const out=companionEvidencePath(taskId,root);return ensureDir(join(root,".arbiter","evidence","companions")),writeFileTranslated(out,`${JSON.stringify(parsed.data,null,2)}
|
|
3
|
+
`),out}function gatherCompanionDiffStats(repoDir){const base=diffBase(repoDir),range=base?`${base}...HEAD`:"HEAD";try{return parseShortstat(runCli("git",["diff","--shortstat",range],{cwd:repoDir}).stdout)}catch{return{files:0,insertions:0,deletions:0}}}function diffBase(repoDir){for(const ref of["origin/main","main"])try{return runCli("git",["merge-base","HEAD",ref],{cwd:repoDir}).stdout.trim()}catch{}return null}function parseShortstat(shortstat){const files=Number((shortstat.match(/(\d+) files? changed/)??[])[1]??0),insertions=Number((shortstat.match(/(\d+) insertions?\(\+\)/)??[])[1]??0),deletions=Number((shortstat.match(/(\d+) deletions?\(-\)/)??[])[1]??0);return{files,insertions,deletions}}function writeVerificationCompanionEvidence(root,phase,taskId,profile,opts){phase!=="verification"||taskId===void 0||writeCompanionEvidence(root,taskId,profile,opts)}function shipTierFor(root,state,opts){const base=normTier(opts.tier??state?.tier);return base==="Standard"?base:widenTier(base,(opts.gatherTierSignals??gatherTierSignals)(root,state?.taskId??opts.taskId))}function trainSignalsFor(root,opts,state,now,chainSize=(state?.taskId?1:0)+(state?.chainIds??[]).length){const gather=opts.gatherTierSignals??gatherTierSignals;return{chainSize,openedAt:state?.timestamps.chainOpened,now,widenedTier:(opts.chainAddIds??[]).reduce((acc,raw)=>widenTier(acc,gather(root,normalizeChainId(raw))),"XS"),explicitSeal:opts.seal===!0}}function chainAddContext(opts,state){const additions=opts.chainAddIds??[],taskId=opts.taskId!==void 0?normalizeShipTaskId(opts.taskId):state?.taskId,taskChanged=shipTaskChanged(state,taskId),existing=opts.chainIds!==void 0?opts.chainIds.map(normalizeChainId):taskChanged?[]:state?.chainIds??[],primaryCount=hasShipTaskId(taskId)?1:0;return{signalState:taskChanged?null:state,currentSize:primaryCount+existing.length,projectedSize:primaryCount+appendChainIds(existing,additions).length}}function assertChainAddAllowed(root,opts,state,now,limits){const{signalState,currentSize,projectedSize}=chainAddContext(opts,state),currentVerdict=evaluateSeal(trainSignalsFor(root,opts,signalState,now,currentSize),limits);if(currentVerdict.sealed){const seal2={reason:currentVerdict.reason,detail:currentVerdict.detail};throw new UserFacingError(t("errors.E_TRAIN_SEALED",seal2))}if(projectedSize<=limits.maxChain)return;const seal={reason:"max-chain",detail:`the requested append would make the train carry ${projectedSize} issue(s), the limit is ${limits.maxChain}`};throw new UserFacingError(t("errors.E_TRAIN_SEALED",seal))}function prepareChainAdd(root,opts,limits){const additions=opts.chainAddIds??[];if(additions.length===0&&opts.seal!==!0)return null;const state=readUnifiedState(root),now=opts.now??new Date;return assertChainAddAllowed(root,opts,state,now,limits),{additions,now}}function persistTrainAppend(root,state,additions,now){const existing=state?.chainIds??[],chainIds=appendChainIds(existing,additions);writeUnifiedState(root,{chainIds,...state?.timestamps.chainOpened===void 0?{timestamps:{chainOpened:now.toISOString()}}:{}}),appendLog(root,`ship \u2192 train +${chainIds.length-existing.length} (${chainIds.join(", ")})`)}function applyPreparedChainAdd(root,prepared){prepared!==null&&persistTrainAppend(root,readUnifiedState(root),prepared.additions,prepared.now)}function headShaFor(root,opts){if(opts.headSha!==void 0)return opts.headSha;try{const sha=runCli("git",["rev-parse","HEAD"],{cwd:root}).stdout.trim();return sha.length>0?sha:null}catch{return null}}function resultingPhase(phase,opts){return opts.advance!==!0?phase:advanceTargetFor(phase)??phase}function prepareReviewRound(root,phase,opts,ship){if(resultingPhase(phase,opts)!==REFACTOR_PHASE||opts.advance!==!0&&opts.reviewRound!==!0)return null;const previous=reviewStateOf(readUnifiedState(root)),maxRounds=opts.reviewMaxRounds??resolveReviewMaxRounds(ship),forced=opts.forceReview===!0,verdict=evaluateReviewRound({rounds:previous.rounds,maxRounds,forced});if(!verdict.allowed)throw new UserFacingError(t("errors.E_REVIEW_ROUNDS_EXHAUSTED",{detail:verdict.detail}));return{rounds:previous.rounds+1,maxRounds,base:previous.lastReviewedSha,head:headShaFor(root,opts),forced}}function applyReviewRound(root,plan){if(plan===null)return;const wasForced=reviewStateOf(readUnifiedState(root)).forced===!0;writeUnifiedState(root,{review:{rounds:plan.rounds,lastReviewedSha:plan.head,...plan.forced||wasForced?{forced:!0}:{}}});const at=plan.head===null?"an unknown sha":plan.head.slice(0,7);appendLog(root,`review \u2192 round ${plan.rounds} at ${at}${plan.forced?" (forced)":""}`)}export function runTaskShip(opts={}){const root=opts.dir??process.cwd(),shipConfig=shipConfigFor(root),trainLimits=trainLimitsFor(shipConfig,opts),preparedChainAdd=prepareChainAdd(root,opts,trainLimits);seedShipState(root,opts,trainLimits),applyPreparedChainAdd(root,preparedChainAdd);const state=readUnifiedState(root);let phase=state?.phase??"preflight";const tier=shipTierFor(root,state,opts),preparedRound=prepareReviewRound(root,phase,opts,shipConfig),profile=shipProfileFor(root,opts),advancedPhase=advanceShipPhase(root,phase,opts);return phase=advancedPhase.phase,applyReviewRound(root,preparedRound),writeVerificationCompanionEvidence(root,phase,state?.taskId,profile,opts),{phase,step:shipStepFor(phase,tier,profile,state?.taskId,{chainIds:state?.chainIds??[],...opts.externalModelAccess!==void 0?{externalModelAccess:opts.externalModelAccess}:{},...preparedRound!==null?{review:preparedRound}:{}}),advanced:advancedPhase.advanced,done:phase==="complete",tier,profile}}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync}from"node:fs";import{join}from"node:path";import{appendFileTranslated,ensureDir,rmTranslated,writeFile,readFileTranslated,assertWritten}from"../utils/fs.js";import{sanitizeTaskId}from"../utils/task-id.js";export const PHASE_ORDER=["preflight","plan","red-team-review","red","green","refactor","verification","close","complete"],LATERAL_PHASES=["red-team-rework"];export function isValidPhase(s){return PHASE_ORDER.includes(s)||LATERAL_PHASES.includes(s)}export function isTddPhase(s){return s==="RED"||s==="GREEN"||s==="REFACTOR"}export function reviewStateOf(state){return state?.review??{rounds:0,lastReviewedSha:null}}export function normalizeChainId(raw){const core=raw.trim().replace(/^#/,"");if(!/^\d+$/.test(core))throw new Error(`Invalid chain id "${raw}" \u2014 expected a GitHub issue number like "1280" or "#1280".`);return`#${core}`}const TASK_DIRNAME=".task",STATUS_FILENAME="status.json";export function taskStateDir(root){return join(root,".claude",TASK_DIRNAME)}export function statusPath(root){return join(taskStateDir(root),STATUS_FILENAME)}function logPath(root){return join(taskStateDir(root),"log.md")}function emptyCursor(){return{tddPhase:null,lastAction:"",nextAction:""}}function defaultState(){return{taskId:"",phase:"preflight",tier:"",plan:"",cursor:emptyCursor(),handoffStrategy:null,handoffReady:!1,runId:"",timestamps:{},gateDecisions:[]}}function canonicalTaskId(raw){const trimmed=raw.trim(),numeric=/^#?(\d+)$/.exec(trimmed);return numeric?`#${numeric[1]}`:trimmed}function msg(err){return err instanceof Error?err.message:String(err)}export function normalizePhase(raw,sourceLabel=STATUS_FILENAME){if(raw===void 0||raw==="")return"preflight";if(raw==="implementation")return"red";if(!isValidPhase(raw))throw new Error(`Corrupted phase value "${raw}" in ${sourceLabel}. Valid phases: ${PHASE_ORDER.join(", ")}. Remove .claude/.task/ and re-run with --to preflight to reset.`);return raw}function normalize(raw){if(raw.chainIds!==void 0&&(!Array.isArray(raw.chainIds)||raw.chainIds.some(id=>typeof id!="string")))throw new Error("Corrupted chainIds in status.json: expected an array of strings.");return{...defaultState(),...raw,phase:normalizePhase(raw.phase),cursor:{...emptyCursor(),...raw.cursor??{}},timestamps:raw.timestamps??{},gateDecisions:raw.gateDecisions??[]}}export function readUnifiedState(root){const p=statusPath(root);if(existsSync(p)){let parsed;try{parsed=JSON.parse(readFileTranslated(p,"utf-8"))}catch(err){throw new Error(`readUnifiedState: corrupted status at ${p}: ${msg(err)}. Remove .claude/.task/ and re-run with --to preflight to reset.`,{cause:err})}return normalize(parsed)}return seedFromLegacy(root)}export function readTaskId(root){const id=readUnifiedState(root)?.taskId.trim();return id&&id.length>0?id:void 0}export function writeUnifiedState(root,patch){const prev=readUnifiedState(root)??defaultState(),nextTaskId=patch.taskId===void 0?void 0:canonicalTaskId(patch.taskId),base=nextTaskId!==void 0&&nextTaskId.length>0&&prev.taskId.length>0&&canonicalTaskId(prev.taskId)!==nextTaskId?defaultState():prev,normalizedPatch={...patch,...nextTaskId!==void 0?{taskId:nextTaskId}:{}},merged={...base,...normalizedPatch,cursor:{...base.cursor,...normalizedPatch.cursor??{}},timestamps:{...base.timestamps,...normalizedPatch.timestamps??{}}};normalizedPatch.phase&&(merged.timestamps={...merged.timestamps,[normalizedPatch.phase]:new Date().toISOString()}),merged.runId||(merged.runId=`${process.pid}-${Date.now()}`);const result=writeFile(statusPath(root),JSON.stringify(merged,null,2)+`
|
|
3
|
+
`,{skipPreserveCheck:!0});return assertWritten(result,`task-state document at ${statusPath(root)}`),merged}export function readOverride(root,path){return readUnifiedState(root)?.overrides?.[path]}export function writeOverride(root,path,value){const prev=readUnifiedState(root)?.overrides??{};writeUnifiedState(root,{overrides:{...prev,[path]:value}})}export function appendLog(root,line){ensureDir(taskStateDir(root)),appendFileTranslated(logPath(root),`- ${new Date().toISOString()} ${line}
|
|
4
|
+
`)}const DOT_PHASE=".task-phase",DOT_ID=".task-id",DOT_TIER=".task-tier",DOT_PLAN=".task-plan",DOT_HANDOFF_READY=".task-handoff-ready",DOT_HISTORY=".task-phase-history",LEGACY_DOTFILES=[DOT_PHASE,DOT_ID,DOT_TIER,DOT_PLAN,DOT_HANDOFF_READY,DOT_HISTORY];function readDotfile(claudeDir,name){const p=join(claudeDir,name);if(!existsSync(p))return;const raw=readFileTranslated(p,"utf-8").trim();return raw.length>0?raw:void 0}function legacyPerIdStatusPath(claudeDir,idRaw){return join(claudeDir,".task-"+sanitizeTaskId(idRaw),STATUS_FILENAME)}export function seedFromLegacy(root){const claudeDir=join(root,".claude"),phaseRaw=readDotfile(claudeDir,DOT_PHASE),idRaw=readDotfile(claudeDir,DOT_ID);if(phaseRaw===void 0&&idRaw===void 0)return null;let rich={},richCorrupt=!1;if(idRaw){const richPath=legacyPerIdStatusPath(claudeDir,idRaw);if(existsSync(richPath))try{rich=JSON.parse(readFileTranslated(richPath,"utf-8"))}catch(err){richCorrupt=!0,process.stderr.write(`[arbiter] warn: legacy rich state at ${richPath} is unreadable (${msg(err)}); migrating flat dotfiles only and preserving the corrupt file for recovery.
|
|
5
|
+
`)}}const seeded=normalize({...rich,taskId:idRaw??rich.taskId??"",phase:normalizePhase(phaseRaw,DOT_PHASE),tier:readDotfile(claudeDir,DOT_TIER)??rich.tier??"",plan:readDotfile(claudeDir,DOT_PLAN)??rich.plan??"",handoffReady:existsSync(join(claudeDir,DOT_HANDOFF_READY)),cursor:emptyCursor()}),result=writeFile(statusPath(root),JSON.stringify(seeded,null,2)+`
|
|
6
|
+
`,{skipPreserveCheck:!0});assertWritten(result,`task-state document at ${statusPath(root)}`);for(const f of LEGACY_DOTFILES)rmTranslated(join(claudeDir,f),{force:!0});return idRaw&&!richCorrupt&&rmTranslated(join(claudeDir,".task-"+sanitizeTaskId(idRaw)),{recursive:!0,force:!0}),seeded}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{createHash}from"node:crypto";import{existsSync}from"node:fs";import{join}from"node:path";import{ensureDir,writeFileTranslated,readFileTranslated}from"../utils/fs.js";import{sanitizeTaskId}from"../utils/task-id.js";import{normalizeChainId}from"./task-state.js";import{getBoolFlag,getNumberFlag}from"../config/env-registry.js";import{PHASE_ORDER,LATERAL_PHASES,isValidPhase,readUnifiedState,writeUnifiedState,readTaskId,appendLog}from"./task-state.js";import{runCli}from"../utils/run-cli.js";import{evaluateMerged}from"./pr-merged.js";import{shipConfigFor,permitsGitHubCalls}from"./ship-config.js";import{evaluateSeedSize,resolveTrainLimits}from"./ship-train.js";import{UserFacingError}from"../utils/errors.js";import{t}from"../i18n/index.js";import{loadTddEvidence,extractFailureSignature}from"../evidence/tdd.js";import{pathExistsInCommit,resolveEvidenceCommit,tddEvidenceProducedOnBranch}from"../evidence/git-checks.js";import{detectHostCapabilities}from"../capabilities/host-probe.js";import{loadConfig}from"../utils/config.js";import{verifyGatePassMarker,verifyDoneEvidenceReceipt}from"../evidence/gate-binding.js";export class HandoffRequiredError extends Error{constructor(message){super(message),this.name="HandoffRequiredError"}}function currentPhase(root){return readUnifiedState(root)?.phase??"preflight"}const RECOVERY_TABLE={preflight:`Phase: preflight
|
|
3
|
+
Action: Run /task #NNN to initialize the task branch and plan.
|
|
4
|
+
Command: node scripts/check-all.mjs L1`,plan:`Phase: plan
|
|
5
|
+
Action: Plan is being written. Review .claude/plans/ for existing plan draft.
|
|
6
|
+
Next: Await user GO before editing files.`,"red-team-review":`Phase: red-team-review
|
|
7
|
+
Action: Red-team agents running. Review .arbiter/evidence/redteam/<task-id>.json.
|
|
8
|
+
Next: CRITICAL findings \u2192 arbiter task advance --to red-team-rework. All clear \u2192 arbiter task advance --to red.`,"red-team-rework":`Phase: red-team-rework
|
|
9
|
+
Action: Critical findings require plan revision. Fix plan, then re-run red-team.
|
|
10
|
+
Next: arbiter task advance --to red-team-review (re-triggers review) or --to plan (full replan).`,red:`Phase: red
|
|
11
|
+
Action: Write failing tests first. No implementation yet.
|
|
12
|
+
Next: Tests written \u2192 arbiter task advance --to green.`,green:`Phase: green
|
|
13
|
+
Action: Make tests pass with minimal implementation.
|
|
14
|
+
Next: All tests green \u2192 arbiter task advance --to refactor.`,refactor:`Phase: refactor
|
|
15
|
+
Action: Clean up implementation. Tests must stay green.
|
|
16
|
+
Next: Refactor done \u2192 arbiter task advance --to verification.`,verification:`Phase: verification
|
|
17
|
+
Action: Gate running. Re-run: node scripts/check-all.mjs L2
|
|
18
|
+
Next: Fix any failures, then arbiter task advance --to close.`,close:`Phase: close
|
|
19
|
+
Action: CLOSER mode active \u2014 see .claude/rules/95-closer-mode.md. Single named target, no new issues/refactor beyond the diff (findings \u2192 PARKING), no gate-appeasement deletions. Same error twice \u2192 5-line root-cause or declare BLOCKED.
|
|
20
|
+
Next: Commit, push, open/land the PR; foreground-wait on its checks. Merged + evidence \u2192 arbiter task advance --to complete.`,complete:`Phase: complete
|
|
21
|
+
Action: Task is complete. Check if PR was created: gh pr list --head $(git branch --show-current)
|
|
22
|
+
Next: Verify PR merged and issue closed.`};export function runTaskResume({dir}={}){const root=dir??process.cwd(),state=readUnifiedState(root),phase=state?.phase??"preflight",taskId=state?.taskId&&state.taskId.length>0?state.taskId:void 0,header=taskId?`Task: ${taskId}
|
|
23
|
+
`:"",cursor=state?.cursor;if(cursor&&cursor.nextAction.trim().length>0){const lines=[`${header}Phase: ${phase}${cursor.tddPhase?` (${cursor.tddPhase})`:""}`,cursor.lastAction.trim().length>0?`Last action: ${cursor.lastAction}`:void 0,`Next action: ${cursor.nextAction}`].filter(l=>l!==void 0);process.stdout.write(lines.join(`
|
|
24
|
+
`)+`
|
|
25
|
+
`);return}process.stdout.write(`${header}${RECOVERY_TABLE[phase]}
|
|
26
|
+
`)}export function runTaskInit(opts={}){const root=opts.dir??process.cwd(),patch={};opts.id!==void 0&&(patch.taskId=opts.id),opts.tier!==void 0&&(patch.tier=opts.tier),opts.plan!==void 0&&(patch.plan=opts.plan),opts.chainIds!==void 0&&(patch.chainIds=opts.chainIds.map(normalizeChainId)),assertSeedWithinTrainLimit(root,opts.id,patch.chainIds);const branch=detectCurrentBranch(root);branch!==void 0&&(patch.branch=branch);const state=writeUnifiedState(root,patch);appendLog(root,`init task ${state.taskId||"(unset)"} tier=${state.tier||"(unset)"}`)}function assertSeedWithinTrainLimit(root,taskId,chainIds){const verdict=evaluateSeedSize(readUnifiedState(root),taskId,chainIds,resolveTrainLimits(shipConfigFor(root)));if(verdict.ok)return;const seal={reason:"max-chain",detail:verdict.detail};throw new UserFacingError(t("errors.E_TRAIN_SEALED",seal))}function detectCurrentBranch(root){try{const name=runCli("git",["branch","--show-current"],{cwd:root,timeoutMs:5e3}).stdout.trim();return name.length>0?name:void 0}catch{return}}const GETTABLE_FIELDS=["phase","taskId","tier","plan","tddPhase","lastAction","nextAction"];function warnOnForeignTaskState(root,state){const recorded=state.branch;if(recorded===void 0||recorded.length===0)return;const actual=detectCurrentBranch(root);actual===void 0||actual===recorded||process.stderr.write(`WARNING: task state is foreign \u2014 .claude/.task/status.json records task ${state.taskId||"(none)"} on branch "${recorded}", but the checkout is on "${actual}". The value below belongs to that other task. Run \`arbiter task init --id <id>\` to seed this one.
|
|
27
|
+
`)}export function runTaskGet(opts){const root=opts.dir??process.cwd(),s=readUnifiedState(root);s&&warnOnForeignTaskState(root,s);const value=(s?{phase:s.phase,taskId:s.taskId,tier:s.tier,plan:s.plan,tddPhase:s.cursor.tddPhase??"",lastAction:s.cursor.lastAction,nextAction:s.cursor.nextAction}:Object.fromEntries(GETTABLE_FIELDS.map(f=>[f,f==="phase"?"preflight":""])))[opts.field];value===void 0&&(process.stderr.write(`Unknown field "${opts.field}". Valid: ${GETTABLE_FIELDS.join(", ")}
|
|
28
|
+
`),process.exit(2)),process.stdout.write(`${value}
|
|
29
|
+
`)}const defaultRunner=(cmd,args)=>runCli(cmd,args,{timeoutMs:3e4});function planReviewDir(dir,sanitisedId){return join(dir,".arbiter","evidence","plan-review",sanitisedId)}function backlogPath(dir,sanitisedId){return join(dir,".arbiter","evidence",sanitisedId,"BACKLOG.md")}function readTaskIdFromDisk(dir){return readTaskId(dir)}export function runTaskRecover(opts={}){const dir=opts.dir??process.cwd(),rawId=opts.taskId!==void 0&&opts.taskId.length>0?opts.taskId:readTaskIdFromDisk(dir);if(rawId===void 0){process.stdout.write(`No task id provided and no active task found. Pass --task <id> to recover.
|
|
30
|
+
`);return}const sanit=sanitizeTaskId(rawId),runner=opts.runner??defaultRunner,parts=[];parts.push(`\u2501\u2501\u2501 Recovery for task ${rawId} (sanitized: ${sanit}) \u2501\u2501\u2501
|
|
31
|
+
`);const backlog=backlogPath(dir,sanit);existsSync(backlog)?(parts.push("\u2501\u2501\u2501 Layer 1: BACKLOG.md \u2501\u2501\u2501"),parts.push(readFileTranslated(backlog,"utf-8")),parts.push(`\u2501\u2501\u2501 END Layer 1 \u2501\u2501\u2501
|
|
32
|
+
`)):parts.push(`Layer 1: no BACKLOG.md at ${backlog}
|
|
33
|
+
`),parts.push(`\u2501\u2501\u2501 Layer 2: CHECKPOINT(#${sanit}) commits \u2501\u2501\u2501`);try{const r=runner("git",["log","-F","--grep",`CHECKPOINT(#${sanit})`,"--max-count=10","--format=%h %s %ad","--date=iso"]);parts.push(r.stdout.trim()||"(no matching CHECKPOINT commits)")}catch(err){parts.push(`(git log failed: ${err instanceof Error?err.message:String(err)})`)}parts.push(`\u2501\u2501\u2501 END Layer 2 \u2501\u2501\u2501
|
|
34
|
+
`),parts.push("\u2501\u2501\u2501 Layer 3: last 20 commits \u2501\u2501\u2501");try{const r=runner("git",["log","--max-count=20","--format=%h %s %ad","--date=iso"]);parts.push(r.stdout.trim()||"(no commits)")}catch(err){parts.push(`(git log failed: ${err instanceof Error?err.message:String(err)})`)}parts.push(`\u2501\u2501\u2501 END Layer 3 \u2501\u2501\u2501
|
|
35
|
+
`),parts.push("If still unclear, run /clear and reopen with manual MCP context \u2014 feed the issue body, the plan file, and the last few commits to the next session."),process.stdout.write(parts.join(`
|
|
36
|
+
`)+`
|
|
37
|
+
`)}function requirePlanReviewPass(opts){const sanit=sanitizeTaskId(opts.taskId),latestPath=join(planReviewDir(opts.dir,sanit),"latest.json");if(!existsSync(latestPath))return{ok:!1,reason:`no plan-review evidence at ${latestPath} \u2014 record a PASS verdict there first`};let parsed;try{parsed=JSON.parse(readFileTranslated(latestPath,"utf-8"))}catch(err){return{ok:!1,reason:`unreadable latest.json: ${err instanceof Error?err.message:String(err)}`}}return parsed.verdict!=="PASS"?{ok:!1,reason:`last plan-review verdict was ${parsed.verdict}`}:opts.planContent!==void 0&&parsed.planDigest!==void 0&&createHash("sha256").update(opts.planContent).digest("hex")!==parsed.planDigest?{ok:!1,reason:"plan changed since last review \u2014 re-review and update latest.json"}:{ok:!0}}function gateEnabled(dir){return existsSync(join(dir,".arbiter","plan-review.enabled"))}function readGitUserName(){try{return runCli("git",["config","user.name"],{timeoutMs:5e3}).stdout.trim()||"unknown"}catch{return"unknown"}}function writeBypassRecord(dir,sanitisedId,reason){const evDir=planReviewDir(dir,sanitisedId);ensureDir(evDir);const ts=new Date().toISOString(),record={reason,git_user:readGitUserName(),ts};writeFileTranslated(join(evDir,`bypass-${ts.replace(/[:.]/g,"-")}.json`),JSON.stringify(record,null,2))}function loadPlanContentIfAvailable(dir){const planPath=readUnifiedState(dir)?.plan.trim();if(!planPath||planPath.length===0)return;const resolved=join(dir,planPath),candidate=existsSync(planPath)?planPath:existsSync(resolved)?resolved:void 0;if(candidate!==void 0)return readFileTranslated(candidate,"utf-8")}function checkPlanReviewGate(dir,claudeDir,opts){if(!gateEnabled(dir))return;const rawId=readTaskIdFromDisk(dir)??"unknown",sanit=sanitizeTaskId(rawId),inCi=process.env.CI==="true",envBypass=getBoolFlag("ARBITER_SKIP_PLAN_REVIEW");if(opts.skipPlanReview===!0){writeBypassRecord(dir,sanit,"flag"),process.stderr.write(`WARNING: plan-review gate bypassed (reason=flag, --skip-plan-review)
|
|
38
|
+
`);return}if(envBypass&&!inCi){writeBypassRecord(dir,sanit,"env"),process.stderr.write(`WARNING: plan-review gate bypassed (reason=env, ARBITER_SKIP_PLAN_REVIEW=1)
|
|
39
|
+
`);return}const planContent=loadPlanContentIfAvailable(dir),result=requirePlanReviewPass({dir,taskId:rawId,...planContent!==void 0?{planContent}:{}});if(!result.ok){const hint=inCi?"--skip-plan-review (env ARBITER_SKIP_PLAN_REVIEW is refused under CI)":"--skip-plan-review (or env ARBITER_SKIP_PLAN_REVIEW=1)";throw new Error(`plan-review gate: ${result.reason}. Use ${hint} to bypass with an audit record.`)}}function readBranchPrs(branch,dir,candidateSha){const out=runCli("gh",["pr","list","--head",branch,"--state","all","--json","number,state,mergeStateStatus,statusCheckRollup,headRefOid,mergeCommit,mergedAt,baseRefName"],{cwd:dir,timeoutMs:3e4}).stdout,parsed=JSON.parse(out),prs=Array.isArray(parsed)?parsed:[];if(!candidateSha||!prs.some(pr=>pr.state==="MERGED"&&pr.headRefOid===candidateSha))return prs;const checks=readCommitCi(candidateSha,dir);return prs.map(pr=>pr.headRefOid===candidateSha?{...pr,statusCheckRollup:checks}:pr)}const COMMIT_CI_QUERY=`query($owner:String!,$name:String!,$sha:GitObjectID!,$endCursor:String) {
|
|
40
|
+
repository(owner:$owner,name:$name) { object(oid:$sha) { ... on Commit {
|
|
41
|
+
statusCheckRollup { contexts(first:100,after:$endCursor) {
|
|
42
|
+
pageInfo { hasNextPage endCursor }
|
|
43
|
+
nodes {
|
|
44
|
+
... on CheckRun { name conclusion completedAt checkSuite { createdAt branch { name } workflowRun { event } } }
|
|
45
|
+
... on StatusContext { context state createdAt }
|
|
46
|
+
}
|
|
47
|
+
} }
|
|
48
|
+
} } }
|
|
49
|
+
}`;function normalizeGraphqlCiCheck(check){const{checkSuite:suite,...rest}=check;if(suite===void 0)return rest;if(suite===null)return{...rest,checkSuite:null};const{branch:graphqlBranch,...suiteRest}=suite,branch=graphqlBranch?.name;return{...rest,checkSuite:{...suiteRest,...branch===void 0?{}:{branch}}}}function readCommitCi(sha,dir){const out=runCli("gh",["api","graphql","--paginate","--slurp","-F","owner={owner}","-F","name={repo}","-f",`sha=${sha}`,"-f",`query=${COMMIT_CI_QUERY}`],{cwd:dir,timeoutMs:3e4}).stdout,contexts=JSON.parse(out).map(page=>page.data.repository.object.statusCheckRollup.contexts);if(contexts.length===0||contexts.some(page=>!Array.isArray(page.nodes)||typeof page.pageInfo.hasNextPage!="boolean")||contexts.at(-1)?.pageInfo.hasNextPage!==!1)throw new Error("Incomplete candidate CI pages");return contexts.flatMap(page=>page.nodes.map(normalizeGraphqlCiCheck))}function prGateRefusal(detail){return new UserFacingError(t("errors.E_PR_NOT_MERGED",{detail}))}function prGateBranch(dir){const branch=detectCurrentBranch(dir)??readUnifiedState(dir)?.branch;if(branch===void 0||branch.length===0)throw prGateRefusal("the current branch could not be determined, so the PR cannot be verified. Pass --no-pr if this repo lands by direct push.");return branch}function prGateSnapshots(dir,branch,opts,candidateSha){try{return opts.readPrs?opts.readPrs(branch,dir):readBranchPrs(branch,dir,candidateSha)}catch(err){throw prGateRefusal(`\`gh pr list --head ${branch}\` failed (${err instanceof Error?err.message:String(err)}). An unverifiable landing is not a landing: fix \`gh\`, or pass --no-pr if this repo lands by direct push.`)}}function prGateSkipped(dir,opts){if(opts.noPr===!0){const rawConfig=readRawArbiterConfig(dir),policy=resolveDirectCompletionPolicy(rawConfig);if(!policy.ok)throw prGateRefusal(policy.reason);if(!hasRawGitHubPermission(rawConfig))throw prGateRefusal("direct post-main CI requires raw permitGitHub: true.");const head=assertDirectHeadOnMain(dir),checks=opts.readCommitCi?.(head,dir)??readCommitCi(head,dir);if(!successfulPostMainCi(checks))throw prGateRefusal("direct post-main CI cannot establish a successful candidate result.");return appendLog(dir,"complete \u2190 no-pr (direct landing)"),!0}return permitsGitHubCalls(dir)?!1:(appendLog(dir,"complete \u2190 pr-check skipped (permitGitHub not set)"),!0)}function assertDirectHeadOnMain(dir){try{const head=runCli("git",["rev-parse","HEAD"],{cwd:dir,timeoutMs:15e3}).stdout.trim(),main=runCli("git",["rev-parse","origin/main"],{cwd:dir,timeoutMs:15e3}).stdout.trim();if(head!==main)throw prGateRefusal("direct landing HEAD does not match current origin/main.");return head}catch(err){throw err instanceof UserFacingError?err:prGateRefusal(`direct landing cannot prove current origin/main (${err instanceof Error?err.message:String(err)}).`)}}function checkPrMergedGate(dir,opts,candidateSha){if(opts.noPr!==!0){const rawConfig=readRawArbiterConfig(dir);if(!hasRawGitHubPermission(rawConfig))throw prGateRefusal("PR completion requires raw permitGitHub: true.");if(candidateSha!==void 0){const policy=resolveEvidenceCompletionPolicy(rawConfig,!0);if(!policy.ok)throw prGateRefusal(policy.reason);if(policy.policy==="direct")throw prGateRefusal("direct completion requires --no-pr.")}}if(prGateSkipped(dir,opts))return;const branch=prGateBranch(dir),snapshots=prGateSnapshots(dir,branch,opts,candidateSha),verdict=candidateSha===void 0?evaluateMerged(snapshots,branch,opts.pr):evaluateHarnessCompletion(dir,snapshots,candidateSha,opts);if(!verdict.merged)throw prGateRefusal(verdict.detail);appendLog(dir,`complete \u2190 PR #${verdict.number} MERGED`)}function evaluateHarnessCompletion(dir,snapshots,candidateSha,opts){const policy=resolveEvidenceCompletionPolicy(readRawArbiterConfig(dir),!0);if(!policy.ok)return{merged:!1,detail:policy.reason};if(policy.policy==="direct")return{merged:!1,detail:"direct completion requires --no-pr."};if(policy.policy==="legacy")return{merged:!1,detail:"evidenceHarness requires an explicit completion policy."};const mergeSha=snapshots.find(pr=>pr.state==="MERGED"&&pr.headRefOid===candidateSha&&pr.mergeCommit?.oid&&(opts.pr===void 0||pr.number===opts.pr))?.mergeCommit?.oid,reachable=mergeSha?opts.isMergeReachable?.(mergeSha,dir)??isMergeReachable(mergeSha,dir):!1;return evaluateMerged(snapshots,"",opts.pr,candidateSha,{policy:policy.policy,mergeReachableFromMain:reachable,requireMainBase:!0})}function isMergeReachable(mergeSha,dir){try{return runCli("git",["merge-base","--is-ancestor",mergeSha,"origin/main"],{cwd:dir,timeoutMs:15e3}),!0}catch{return!1}}function checkCompletionEvidence(dir){const config=loadConfig(dir);if(config?.features.evidenceHarness!==!0){checkGatePassMarkerGate(dir);return}const taskId=readTaskId(dir);if(!taskId)throw new Error("done receipt requires the current task id");const verdict=verifyDoneEvidenceReceipt({root:dir,taskId,archetype:config.archetype??"library",maxAgeMin:getNumberFlag("ARBITER_EVIDENCE_MAX_AGE_MIN")});if(!verdict.ok)throw new Error(verdict.reason);return runCli("git",["rev-parse","HEAD"],{cwd:dir,timeoutMs:15e3}).stdout.trim()}function checkEvidenceCompletionPreflight(dir){if(loadConfig(dir)?.features.evidenceHarness!==!0)return;const policy=resolveEvidenceCompletionPolicy(readRawArbiterConfig(dir),!0);if(!policy.ok)throw new Error(`completion policy: ${policy.reason}`)}function readRawArbiterConfig(dir){try{return JSON.parse(readFileTranslated(join(dir,"arbiter.json"),"utf-8"))}catch(err){throw new Error(`completion policy cannot read raw arbiter.json: ${err instanceof Error?err.message:String(err)}`,{cause:err})}}function hasRawGitHubPermission(rawConfig){return isRecord(rawConfig)&&rawConfig.permitGitHub===!0}function isRecord(value){return typeof value=="object"&&value!==null&&!Array.isArray(value)}function policyRefusal(reason){return{ok:!1,reason}}function resolveExplicitCompletionPolicy(rawConfig){if(!isRecord(rawConfig))return policyRefusal("completion policy requires an object arbiter.json");const policy=explicitPolicy(rawConfig);return policy===void 0?policyRefusal("completion policy requires an explicit supported collaborationMode and merge mode"):hasRawGitHubPermission(rawConfig)?{ok:!0,policy}:policyRefusal("completion requires raw permitGitHub: true")}function explicitPolicy(rawConfig){const mode=rawConfig.collaborationMode;if(mode==="peer-review"||mode==="gated-review")return"reviewed-pr";if(mode!=="trunk-solo"||!isRecord(rawConfig.solo))return;const mergeMode=rawConfig.solo.mergeMode;return mergeMode==="direct"?"direct":mergeMode==="pr-ff"?"exact-pr":void 0}function resolveEvidenceCompletionPolicy(rawConfig,requireExplicit=!1){if(!isRecord(rawConfig))return policyRefusal("completion policy requires an object arbiter.json");const features=rawConfig.features;return isRecord(features)?!requireExplicit&&features.evidenceHarness!==!0?{ok:!0,policy:"legacy"}:resolveExplicitCompletionPolicy(rawConfig):policyRefusal("completion policy requires an object features config")}function resolveDirectCompletionPolicy(rawConfig){const policy=resolveExplicitCompletionPolicy(rawConfig);return policy.ok?policy.policy==="direct"?policy:policyRefusal("`--no-pr` requires raw trunk-solo with solo.mergeMode direct"):policy}function successfulPostMainCi(checks){return checks.every(isSuccessfulPostMainCheck)&&checks.some(check=>check.conclusion==="SUCCESS")}function isSuccessfulPostMainCheck(check){const completed=Date.parse(check.completedAt??""),outcome=check.conclusion??check.state??"";return check.checkSuite?.branch==="main"&&check.checkSuite.workflowRun?.event==="push"&&Number.isFinite(completed)&&completed<=Date.now()&&["SUCCESS","SKIPPED","NEUTRAL"].includes(outcome)}function checkGatePassMarkerGate(dir,minLevel="L2"){const inCi=process.env.CI==="true";if(getBoolFlag("ARBITER_SKIP_GATE_MARKER")&&!inCi){process.stderr.write(`WARNING: gate-pass marker gate bypassed (reason=env, ARBITER_SKIP_GATE_MARKER=1)
|
|
50
|
+
`);return}const markerPath=join(dir,".arbiter","gate-pass.json");if(!existsSync(markerPath))throw new Error(`gate-pass marker missing at ${markerPath}. Run \`node scripts/check-all.mjs ${minLevel}\` first.`);let parsed;try{parsed=JSON.parse(readFileTranslated(markerPath,"utf-8"))}catch(err){throw new Error(`gate-pass marker corrupt at ${markerPath}: ${err instanceof Error?err.message:String(err)}. Run \`node scripts/check-all.mjs ${minLevel}\` first.`,{cause:err})}const verdict=verifyGatePassMarker(parsed,{root:dir,minLevel,maxAgeMin:getNumberFlag("ARBITER_EVIDENCE_MAX_AGE_MIN")});if(!verdict.ok)throw new Error(`${verdict.reason}. Run \`node scripts/check-all.mjs ${minLevel}\` again.`)}export function runTaskAdvance(opts){const dir=opts.dir??process.cwd(),claudeDir=join(dir,".claude"),{to}=opts;if(!isValidPhase(to))throw new Error(`Invalid --to value: "${String(to)}". Valid phases: ${[...PHASE_ORDER,...LATERAL_PHASES].join(", ")}`);const current=currentPhase(dir);if(current===to)return;const isLateralTarget=LATERAL_PHASES.includes(to),isLateralCurrent=LATERAL_PHASES.includes(current);if(!isLateralTarget&&!isLateralCurrent){const currentIdx=PHASE_ORDER.indexOf(current),targetIdx=PHASE_ORDER.indexOf(to);if(targetIdx<currentIdx&&!opts.reverse)throw new Error(`Backward transition "${current}" \u2192 "${to}" blocked. Use --reverse to allow backward transitions.`);if(targetIdx>currentIdx+1)throw new Error(`Illegal skip: cannot advance from "${current}" to "${to}" (missing intermediate phases). Advance one phase at a time.`)}const PLANNING_PHASES=new Set(["red-team-review","red-team-rework"]);({plan:()=>{checkTaskSeededGate(dir),checkEvidenceCompletionPreflight(dir)},"red-team-review":()=>{checkPlanReviewGate(dir,claudeDir,opts)},"red-team-rework":()=>{checkRedTeamEvidenceGate(dir,current,PLANNING_PHASES)},red:()=>{checkAcceptancePlanGate(dir),checkPlanReviewGate(dir,claudeDir,opts),checkRedTeamEvidenceGate(dir,current,PLANNING_PHASES),PLANNING_PHASES.has(current)&&checkHandoffGate(dir,claudeDir,opts)},green:()=>{checkTddEvidenceGate(dir,claudeDir)},refactor:()=>{checkTaskSeededGate(dir)},verification:()=>{checkChainTddEvidenceGate(dir),checkTddEvidenceProvenanceGate(dir)},close:()=>{checkGatePassMarkerGate(dir,"L1")},complete:()=>{const candidateSha=checkCompletionEvidence(dir);checkPrMergedGate(dir,opts,candidateSha)}})[to]?.(),writeUnifiedState(dir,{phase:to}),appendLog(dir,`${current} \u2192 ${to}`)}function checkAcceptancePlanGate(dir){if(!acceptanceProfileEnabled(dir))return;const script=join(dir,"scripts","check-acceptance.mjs");if(!existsSync(script))throw new Error(`acceptance-anchor gate: profile is enabled but ${script} is missing. Restore the emitted checker before entering red.`);const plan=readUnifiedState(dir)?.plan.trim()??"";try{runCli("node",[script,"--plan",plan],{cwd:dir,timeoutMs:5e3})}catch(err){throw new Error(`acceptance-anchor gate: ${err instanceof Error?err.message:String(err)}. The red phase was not entered; restore the emitted checker or repair the anchored Markdown plan.`,{cause:err})}}function acceptanceProfileEnabled(dir){const override=process.env.ARBITER_ACCEPTANCE_ANCHOR;if(override==="1"||override==="true"||override==="0"||override==="false")return getBoolFlag("ARBITER_ACCEPTANCE_ANCHOR");try{return JSON.parse(readFileTranslated(join(dir,"arbiter.json"),"utf-8")).features?.acceptanceAnchor===!0}catch{return!1}}function checkTddEvidenceGate(dir,claudeDir){assertTddEvidenceFor(readTaskIdFromDisk(dir)??"unknown",dir)}function checkTaskSeededGate(dir){const taskId=readTaskIdFromDisk(dir);if(!(taskId!==void 0&&taskId.trim().length>0))throw new Error("task-seed gate: no task id in .claude/.task/status.json \u2014 `preflight` promises seeded task state, and every later evidence gate keys on that id. Run `arbiter task init --id <id> --tier <tier>` first.")}function checkRedTeamEvidenceGate(dir,current,planningPhases){if(!planningPhases.has(current))return;const taskId=readTaskIdFromDisk(dir)??"unknown",path=join(dir,".arbiter","evidence","redteam",`${taskId}.json`);if(!existsSync(path))throw new Error(`red-team evidence gate: no red-team evidence at ${path}. The \`red-team-review\` phase dispatches tier-N red-team agents and records their findings there (see .claude/commands/ship.md \xA7Red-team review) \u2014 record it before advancing.`)}function checkChainTddEvidenceGate(dir){const chainIds=readUnifiedState(dir)?.chainIds??[];if(chainIds.length===0)return;const failures=[];for(const id of chainIds)try{assertTddEvidenceFor(id,dir)}catch(err){failures.push(` ${id}: ${err.message}`)}if(failures.length>0)throw new Error(`Chain TDD evidence gate: ${failures.length} of ${chainIds.length} chained issue(s) lack valid RED evidence.
|
|
51
|
+
${failures.join(`
|
|
52
|
+
`)}
|
|
53
|
+
Every issue batched onto this branch must have failed a test before it was implemented.`)}function checkTddEvidenceProvenanceGate(dir){const state=readUnifiedState(dir),primary=state?.taskId??"unknown",ids=[primary,...state?.chainIds??[]],failures=[];try{assertTddEvidenceFor(primary,dir)}catch(err){failures.push(` ${primary}: ${err.message}`)}for(const id of ids)tddEvidenceProducedOnBranch(id,dir)||failures.push(` ${id}: receipt is not committed on this branch`);if(failures.length!==0)throw new Error(`TDD evidence provenance gate: ${failures.length} failure(s) before verification:
|
|
54
|
+
${failures.join(`
|
|
55
|
+
`)}
|
|
56
|
+
Receipts must be committed and produced on this branch after origin/main. Commit the real RED evidence before entering verification.`)}function assertTddEvidenceFor(rawId,dir){const result=loadTddEvidence(rawId,dir);if(!result.ok)throw new Error(`TDD evidence gate: ${result.reason}. Run \`arbiter task record-red --test-path <path>\` to capture failing test evidence first.`);const ev=result.data;if(ev.task_id!==rawId)throw new Error(`TDD evidence task_id mismatch: evidence has "${ev.task_id}" but active task is "${rawId}".`);if(extractFailureSignature(ev.test_run_log)===null)throw new Error("TDD evidence gate: no recognised failure signature found in test_run_log. The test must actually fail before recording evidence.");const resolved=resolveEvidenceCommit(ev,dir);if(resolved===null)throw new Error(`TDD evidence gate: test_commit_sha "${ev.test_commit_sha}" is not reachable from HEAD. Ensure the test was committed before running \`arbiter task record-red\`, or re-record the evidence after a rebase.`);if("degraded"in resolved)throw new Error(`TDD evidence gate: ${resolved.reason}`);if(!pathExistsInCommit(resolved.sha,ev.test_path,dir))throw new Error(`TDD evidence gate: test_path "${ev.test_path}" not found in commit ${resolved.sha}. Verify the test file was committed at that sha.`)}const INLINE_MAX=10,SUBAGENT_MAX=20,STRATEGY_DESCRIPTION={inline:"Strategy: inline (context is small \u2014 continuing in-context)","sub-agent":"Strategy: sub-agent (medium context \u2014 spawn a sub-agent for the exec phase)",stop:"Strategy: stop (large context \u2014 run /clear then re-invoke to free context)"};export function decideClearStrategy({units,modelSwitch}){return modelSwitch?units===void 0?"stop":units<=INLINE_MAX?"inline":units<=SUBAGENT_MAX?"sub-agent":"stop":"inline"}export function buildHandoffBanner({taskId,strategy,units,tier}){const numericId=taskId.replace(/^#/,""),tierInfo=tier!==void 0?` (tier: ${tier})`:"",unitsInfo=units!==void 0?`, units: ${units}`:"",resumeCmd=`arbiter ship #${numericId} --advance --post-clear`,continueHint=strategy==="inline"?`Continue in this context: run \`${resumeCmd}\` or \`arbiter task advance --to red --post-clear\``:strategy==="sub-agent"?`Spawn a sub-agent for the exec phase, then pass \`--post-clear\` on re-entry:
|
|
57
|
+
\`${resumeCmd}\``:`1. Run: /clear
|
|
58
|
+
2. Re-invoke: \`${resumeCmd}\``;return["\u2501\u2501\u2501 Plan complete \u2014 handoff required \u2501\u2501\u2501",`Task: ${taskId}${tierInfo}${unitsInfo}`,STRATEGY_DESCRIPTION[strategy],"",continueHint,"","Flag: --post-clear signals post-/clear re-entry (marks task state resumed)."].join(`
|
|
59
|
+
`)}function handlePostClearReEntry(rawId,dir){const existing=readUnifiedState(dir)??{};if(existing.postClearResumed!==void 0)return;if(!(existing.taskId||(rawId!=="unknown"?rawId:void 0)))throw new Error(`Post-clear re-entry: task state has no taskId. Re-initialize the task with \`arbiter task init --id #NNN\` before resuming. (rawId="${rawId}", existing.taskId="${existing.taskId??""}")`);writeUnifiedState(dir,{postClearResumed:new Date().toISOString()})}function checkHandoffGate(dir,claudeDir,opts){const rawId=readTaskIdFromDisk(dir)??"unknown";if(opts.postClear===!0||getBoolFlag("ARBITER_POST_CLEAR")){handlePostClearReEntry(rawId,dir);return}const caps=detectHostCapabilities();if(!caps.modelSwitch){writeUnifiedState(dir,{handoffStrategy:"inline"});return}const strategy=decideClearStrategy({units:opts.units,modelSwitch:caps.modelSwitch});if(strategy==="inline"){writeUnifiedState(dir,{handoffStrategy:"inline"});return}writeUnifiedState(dir,{handoffStrategy:"interactive",handoffReady:!0,planningHandoffReady:new Date().toISOString()});const tier=readUnifiedState(dir)?.tier;throw new HandoffRequiredError(buildHandoffBanner({taskId:rawId,strategy,units:opts.units,tier}))}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{resolve,join}from"node:path";import{acquireLock}from"../utils/file-lock.js";import{UserFacingError,FatalError,ArbiterError}from"../utils/errors.js";import{beginGenerationSession,endGenerationSession,ensureDir}from"../utils/fs.js";import{loadGeneratedManifest,saveGeneratedManifest,manifestKey}from"../state/generated-manifest.js";import{isGateSpineKey,isSafetyClassKey}from"../generators/safety-class.js";import{t}from"../i18n/index.js";import{jsonOutput,statusToExitCode}from"../utils/json-output.js";import{getLogger}from"../utils/logger.js";import{detectAdverseGitState}from"../detectors/git.js";import{detectGithubAccess}from"../detectors/github.js";import{detectLegacyWorkflowCollisionWarning}from"../detectors/workflow-collision.js";import{resolveAxisFields}from"../detectors/axis.js";import{detectInstalledSkills}from"../integrations/skill-detector.js";import{excludeOwnEmittedSkills}from"../generators/skills.js";import{loadConfig,loadSnapshot,saveConfigAndSnapshot}from"../utils/config.js";import{runGithubSetup,printResults,runPlugins,slugifyProjectName}from"./init.js";import{resolveProjectName}from"../config/resolve-project-name.js";import{diffConfig,impactedGenerators}from"../config/diff.js";import{validateConfig}from"../config/schema.js";import{resolveProjectConfig}from"../config/resolve-project-config.js";import{buildRegistry,runGeneratorsFromRegistry,runGeneratorsSelective}from"../generators/registry.js";import{buildAdoptPredicate,recordLocalOverride}from"./adopt-policy.js";import{IGNORE_FILE_NAME,buildSelectionPredicate,loadIgnorePatterns,matchesOnly}from"../config/arbiter-ignore.js";import{applyRetirement,diskHasher,planRetirement,retirementWarning}from"./retire-policy.js";export{buildAdoptPredicate}from"./adopt-policy.js";export function withheldSafetyKeys(results,targetDir){return results.filter(r=>r.withheld===!0&&r.adopted!==!0).map(r=>manifestKey(targetDir,r.path)).filter(k=>k!==null&&(isSafetyClassKey(k)||isGateSpineKey(k)))}function restoredKeys(results,targetDir){return results.filter(r=>r.restored===!0).map(r=>manifestKey(targetDir,r.path)).filter(k=>k!==null).sort((a,b)=>a<b?-1:a>b?1:0)}const RESTORE_REPORT_CAP=20;function detectRestoredFilesWarning(keys){if(keys.length===0)return null;const shown=keys.slice(0,RESTORE_REPORT_CAP),hidden=keys.length-shown.length;return`Warning: restored ${keys.length} file(s) this repository had DELETED after arbiter emitted them \u2014 each has a recorded render baseline in .arbiter-generated-manifest.json but was absent from disk. arbiter re-emits any path it holds a baseline for, so deleting a generated file does not stick, and until now it did not say so. Keep the file and commit it, or make the removal permanent by dropping its entry from the manifest. Restored: ${shown.join(", ")}`+(hidden>0?` (+${hidden} more)`:"")}export function adoptedKeys(results,targetDir){return results.filter(r=>r.withheld===!0&&r.adopted===!0).map(r=>manifestKey(targetDir,r.path)).filter(k=>k!==null)}function printStats(results){const created=results.filter(r=>r.action==="created").length,replaced=results.filter(r=>r.action==="backed-up-and-replaced").length,skipped=results.filter(r=>r.action==="skipped"||r.action==="dry-run").length,withheld=results.filter(r=>r.withheld===!0&&r.adopted!==!0).length;process.stdout.write(`${t("cli.update.done",{created,replaced,skipped,withheld})}
|
|
3
|
+
`)}function isNewlyLandedCheckScript(r){if(r.action!=="created"&&r.action!=="backed-up-and-replaced")return!1;const norm=r.path.replace(/\\/g,"/");return/(^|\/)scripts\/check-[^/]+\.mjs$/.test(norm)}function unwiredGuardResults(results){return results.some(r=>r.withheld===!0&&r.adopted!==!0&&r.path.replace(/\\/g,"/").endsWith("/scripts/check-all.mjs"))?results.filter(isNewlyLandedCheckScript):[]}export function unwiredGuardKeys(results,targetDir){return unwiredGuardResults(results).map(r=>manifestKey(targetDir,r.path)).filter(k=>k!==null)}export function detectUnwiredGateWarning(results){const newGates=unwiredGuardResults(results).map(r=>{const norm=r.path.replace(/\\/g,"/");return norm.slice(norm.lastIndexOf("/")+1)});return newGates.length===0?null:`Warning: ${newGates.join(", ")} added but check-all.mjs is withheld \u2014 the new gate is NOT wired (it will never run). Your check-all.mjs is user-modified, so the template fix that wires it was preserved, not applied. Please re-sync check-all.mjs (delete it and re-run \`arbiter update\`, or manually add the runCheck line) to activate the gate.`}const GATE_INVOKING_WORKFLOWS=["01-pr-fast.yml","06-nightly.yml","drift-shadow.yml"];export function detectGateSignatureWarning(results){return!results.some(r=>r.withheld===!0&&r.path.replace(/\\/g,"/").endsWith("/scripts/check-all.mjs"))||!results.some(r=>{if(r.action!=="created"&&r.action!=="backed-up-and-replaced")return!1;const norm=r.path.replace(/\\/g,"/");return GATE_INVOKING_WORKFLOWS.some(w=>norm.endsWith(`/.github/workflows/${w}`))})?null:"Warning: a CI workflow that invokes `node scripts/check-all.mjs <level> --json <path>` was (re)written but check-all.mjs is withheld (user-modified) \u2014 its arg parser may not match this invocation. Verify your check-all.mjs accepts a POSITIONAL level (`L2`) and `--json [path]`; a parser that reads the level only as `process.argv[2]` or ignores `--json` will silently run the wrong level and write no gate-result artifact while the job stays green (a fake-green). Re-sync check-all.mjs to the template, or update its parser."}function selectAndRun(specs,snapshot,stored,dryRun){const errors=[];if(!snapshot)return{results:runGeneratorsFromRegistry(specs,errors,{dryRun}),keysRun:null,errors};const diff=diffConfig(snapshot,stored);if(diff.paths.length===0)return process.stdout.write(`${t("cli.update.no_config_changes")}
|
|
4
|
+
`),{results:runGeneratorsFromRegistry(specs,errors,{dryRun}),keysRun:null,errors};const keys=impactedGenerators(diff);if(keys.has("*")||keys.size===0){const reason=keys.size===0?"Unknown config change":"Governance/axis change";return process.stdout.write(`${t("cli.update.reason_regen",{reason})}
|
|
5
|
+
`),{results:runGeneratorsFromRegistry(specs,errors,{dryRun}),keysRun:keys,errors}}return keys.add("agents-md"),keys.add("claude"),process.stdout.write(`${t("cli.update.selective",{count:keys.size})}
|
|
6
|
+
`),{results:runGeneratorsSelective(specs,keys,errors,{dryRun}),keysRun:keys,errors}}function selectAndRunWithManifest(specs,snapshot,stored,targetDir,adoptOpts){const prevManifest=loadGeneratedManifest(targetDir);beginGenerationSession({targetDir,prevHashes:prevManifest,adoptPredicate:adoptOpts.adoptPredicate,selectPredicate:adoptOpts.selectPredicate,onAdopt:adoptOpts.onAdopt});const out=selectAndRun(specs,snapshot,stored,!1),generatedHashes=endGenerationSession(),unwired=unwiredGuardKeys(out.results,targetDir),stillWithheldSafety=withheldSafetyKeys(out.results,targetDir),fullRegistryRun=out.keysRun===null||out.keysRun.has("*"),retainedWithheldHashes=Object.fromEntries(out.results.flatMap(result=>{if(!(result.excluded!==void 0)&&(result.withheld!==!0||result.adopted===!0))return[];const key=manifestKey(targetDir,result.path);return key!==null&&prevManifest[key]!==void 0?[[key,prevManifest[key]]]:[]})),nextHashes=fullRegistryRun?{...retainedWithheldHashes,...generatedHashes}:{...prevManifest,...generatedHashes},retirement=planRetirement({prevManifest,results:out.results,targetDir,fullRegistryRun,diskHash:diskHasher(targetDir)});applyRetirement(targetDir,retirement),saveGeneratedManifest(targetDir,nextHashes,unwired,stillWithheldSafety);const report=retirementWarning(retirement);return report!==null&&process.stderr.write(`
|
|
7
|
+
${report}
|
|
8
|
+
`),out}function runAdoptPlan(specs,snapshot,stored,targetDir,policy){const prevManifest=loadGeneratedManifest(targetDir),collected=[];beginGenerationSession({targetDir,prevHashes:prevManifest,adoptPredicate:policy.adoptPredicate,selectPredicate:policy.selectPredicate,onAdopt:(key,priorContent,newContent)=>collected.push({key,priorContent,newContent})});let out;try{out=selectAndRun(specs,snapshot,stored,!0)}finally{endGenerationSession()}const retirement=planRetirement({prevManifest,results:out.results,targetDir,fullRegistryRun:out.keysRun===null||out.keysRun.has("*"),diskHash:diskHasher(targetDir)});return{records:collected,results:out.results,retirement}}function summarizeDiff(priorContent,newContent){const oldLines=priorContent.split(`
|
|
9
|
+
`),newLines=newContent.split(`
|
|
10
|
+
`),oldSet=new Map;for(const l of oldLines)oldSet.set(l,(oldSet.get(l)??0)+1);const newSet=new Map;for(const l of newLines)newSet.set(l,(newSet.get(l)??0)+1);let removed=0;for(const[line,count]of oldSet)removed+=Math.max(0,count-(newSet.get(line)??0));let added=0;for(const[line,count]of newSet)added+=Math.max(0,count-(oldSet.get(line)??0));return{removed,added}}function partitionPlanResults(results){return{regenerate:results.filter(r=>(r.action==="replaced"||r.action==="backed-up-and-replaced")&&r.withheld!==!0),withheld:results.filter(r=>r.withheld===!0&&r.adopted!==!0),restore:results.filter(r=>r.restored===!0)}}function printAdoptPlan(records,results,retirement,targetDir,json){const{regenerate,withheld,restore}=partitionPlanResults(results),rel=r=>manifestKey(targetDir,r.path)??r.path;if(json){jsonOutput("update","ok",{adoptPlan:records.map(r=>({path:r.key,...summarizeDiff(r.priorContent,r.newContent)})),wouldRegenerate:regenerate.map(rel),withheld:withheld.map(rel),wouldRestore:restore.map(rel),wouldRetire:retirement.retire,orphans:retirement.orphans,stale:retirement.stale});return}if(retirement.retire.length>0&&process.stdout.write(`
|
|
11
|
+
would retire ${retirement.retire.length} file(s) (arbiter-owned, no longer emitted, unmodified since arbiter wrote them \u2014 they are DELETED):
|
|
12
|
+
`+retirement.retire.map(k=>` - ${k}
|
|
13
|
+
`).join("")),records.length===0)process.stdout.write(`
|
|
14
|
+
adopt-plan: nothing to adopt (no withheld file matches the adopt policy).
|
|
15
|
+
`);else{process.stdout.write(`
|
|
16
|
+
adopt-plan: ${records.length} file(s) would be adopted:
|
|
17
|
+
`);for(const r of records){const{removed,added}=summarizeDiff(r.priorContent,r.newContent);process.stdout.write(` - ${r.key} (-${removed} +${added} lines vs. current on-disk content)
|
|
18
|
+
`)}}if(regenerate.length>0&&(process.stdout.write(`
|
|
19
|
+
would regenerate ${regenerate.length} file(s) (always-rewrite \u2014 local edits are lost, prior content goes to <file>.arbiter-backup where the generator asks for a backup):
|
|
20
|
+
`),printResults(regenerate,targetDir)),withheld.length>0){process.stdout.write(`
|
|
21
|
+
would withhold ${withheld.length} file(s) (locally diverged, no adopt policy matches):
|
|
22
|
+
`);for(const r of withheld)process.stdout.write(` - ${rel(r)}
|
|
23
|
+
`)}if(restore.length>0){process.stdout.write(`
|
|
24
|
+
would restore ${restore.length} file(s) (arbiter-owned, DELETED from disk since arbiter wrote them \u2014 the run re-emits them):
|
|
25
|
+
`);for(const r of restore)process.stdout.write(` - ${rel(r)}
|
|
26
|
+
`)}process.stdout.write(` Re-run without --adopt-plan to apply. Nothing was written.
|
|
27
|
+
`)}function buildNextConfig(stored,axisFields,language,needsMigration,projectName){const{archetype,architectureStyle,isMultiTenant,hasDatabase,databaseEngine,hasPublicApi,contractType,lanes}=axisFields;return{...stored,projectName,archetype,architectureStyle,isMultiTenant,hasDatabase,databaseEngine,hasPublicApi,contractType,language,...lanes.length>0&&{lanes},...needsMigration&&{collaborationMode:"trunk-solo"}}}function detectProjectInfo(targetDir,projectName,stored,options,log){log(" Detecting project...");const arbGhEnv=process.env.ARBITER_GITHUB,envGitHub=arbGhEnv==="1";arbGhEnv!==void 0&&!envGitHub&&process.stderr.write(`Warning: ARBITER_GITHUB=${arbGhEnv} is not '1' \u2014 only ARBITER_GITHUB=1 activates GitHub API calls. Ignored.
|
|
28
|
+
`);const githubAccess=detectGithubAccess();if((options.github||envGitHub)&&!githubAccess.authenticated)throw ArbiterError.fromKey("E_UPDATE_GITHUB_UNAUTHENTICATED","errors.E_UPDATE_GITHUB_UNAUTHENTICATED",{error:githubAccess.error??"gh is not authenticated"});const useGitHub=options.github||envGitHub,{config}=resolveProjectConfig(targetDir,projectName,stored,useGitHub),{language,framework}=config;log(` \u251C\u2500\u2500 Language: ${language}${framework?` / ${framework}`:""}`),log(` \u251C\u2500\u2500 Config: tools=[${stored.tools.join(",")}] level=${stored.governanceLevel}`);const axisFields=resolveAxisFields(stored,targetDir,language,framework),claudeHome=process.env.HOME?`${process.env.HOME}/.claude`:"",installedSkills=excludeOwnEmittedSkills(detectInstalledSkills({targetDir,claudeHome})),specs=buildRegistry(config,installedSkills);return{config,specs,useGitHub,axisFields}}function handlePluginError(err,json){const msg=err instanceof Error?err.message:String(err);throw json&&(jsonOutput("update","error",{},[msg],{errorClass:"fatal"}),process.exit(2)),new FatalError("E_PLUGIN_FATAL",msg)}function emitUpdateOutcome(options,summary,generatorErrors,backendWarnings){const generatorErrorLines=generatorErrors.map(e=>`${e.key}: ${e.message}`);if(options.json){emitJsonUpdateOutcome(summary,generatorErrorLines,backendWarnings);return}generatorErrorLines.length>0&&(printGeneratorFailures(generatorErrorLines,backendWarnings),process.exit(statusToExitCode("error"))),backendWarnings.length>0&&(printBackendWarnings(backendWarnings),process.exit(statusToExitCode("warning"))),process.stdout.write(`${t("cli.update.verify_hint")}
|
|
29
|
+
`)}function emitJsonUpdateOutcome(summary,generatorErrorLines,backendWarnings){const status=updateOutcomeStatus(generatorErrorLines,backendWarnings),jsonOpts=jsonOutcomeOpts(status,backendWarnings);jsonOutput("update",status,summary,generatorErrorLines.length>0?generatorErrorLines:void 0,jsonOpts),status!=="ok"&&process.exit(statusToExitCode(status))}function updateOutcomeStatus(generatorErrorLines,backendWarnings){return generatorErrorLines.length>0?"error":backendWarnings.length>0?"warning":"ok"}function jsonOutcomeOpts(status,backendWarnings){if(status==="ok"&&backendWarnings.length===0)return;const jsonOpts={};return backendWarnings.length>0&&(jsonOpts.warnings=backendWarnings),status==="error"&&(jsonOpts.errorClass="fatal"),status==="warning"&&(jsonOpts.errorClass="recoverable"),jsonOpts}function printBackendWarnings(backendWarnings){process.stderr.write(`
|
|
30
|
+
GitHub warnings (${backendWarnings.length}):
|
|
31
|
+
${backendWarnings.map(w=>` - ${w}`).join(`
|
|
32
|
+
`)}
|
|
33
|
+
`)}function printGeneratorFailures(generatorErrorLines,backendWarnings){process.stdout.write(`
|
|
34
|
+
Generator failures (${generatorErrorLines.length}):
|
|
35
|
+
${generatorErrorLines.map(line=>` - ${line}`).join(`
|
|
36
|
+
`)}
|
|
37
|
+
`),backendWarnings.length>0&&printBackendWarnings(backendWarnings)}function excludedKeys(results,targetDir,kind){return results.filter(r=>r.excluded===kind).map(r=>manifestKey(targetDir,r.path)).filter(k=>k!==null).sort((a,b)=>a<b?-1:a>b?1:0)}function reportSelection(results,targetDir,only,json){const ignored=excludedKeys(results,targetDir,"ignored"),deselected=excludedKeys(results,targetDir,"deselected");if(json)return{ignored,deselected};const conflicted=only.length>0?ignored.filter(k=>matchesOnly(only,k)):[];conflicted.length>0&&process.stdout.write(`
|
|
38
|
+
--only named ${conflicted.length} file(s) that ${IGNORE_FILE_NAME} also matches; the ignore WINS \u2014 a committed opt-out outranks one run's flag. Not written: ${conflicted.join(", ")}
|
|
39
|
+
`);const onlyMatchedSomething=results.some(r=>{const key=manifestKey(targetDir,r.path);return key!==null&&matchesOnly(only,key)});only.length>0&&!onlyMatchedSomething&&process.stderr.write(`
|
|
40
|
+
Warning: --only (${only.join(", ")}) matched no managed file \u2014 nothing was written. Patterns match manifest keys; .arbiter-generated-manifest.json lists them.
|
|
41
|
+
`);const ignoredSafety=ignored.filter(k=>isSafetyClassKey(k));return ignoredSafety.length>0&&process.stderr.write(`
|
|
42
|
+
Note: ${ignoredSafety.length} safety-class file(s) listed in ${IGNORE_FILE_NAME} were not emitted: ${ignoredSafety.join(", ")}
|
|
43
|
+
They no longer adopt shipped security/enforcement fixes, and the safety-adopt ratchet cannot see them. Remove the pattern to resume enforcement.
|
|
44
|
+
`),{ignored,deselected}}function reportAdoption(results,targetDir,json){const adopted=adoptedKeys(results,targetDir);!json&&adopted.length>0&&process.stdout.write(`
|
|
45
|
+
Adopted ${adopted.length} safety-class/withheld file(s) (template fix landed over user-modified content; prior content preserved in .arbiter/evidence/local-overrides/):
|
|
46
|
+
`+adopted.map(k=>` - ${k}
|
|
47
|
+
`).join(""));const stillWithheldSafety=withheldSafetyKeys(results,targetDir);return!json&&stillWithheldSafety.length>0&&process.stderr.write(`
|
|
48
|
+
Warning: ${stillWithheldSafety.length} protected file(s) remain withheld (user-modified; safety hooks adopt by default \u2014 a gate-spine file adopts only under --adopt-gate-spine): ${stillWithheldSafety.join(", ")}
|
|
49
|
+
\`scripts/check-safety-adopt-ratchet.mjs\` will FAIL for any of these NOT marked \`arbiter:preserve\` \u2014 re-adopt it, wire the new checks by hand, or mark it.
|
|
50
|
+
`),{adopted,stillWithheldSafety}}function prepareUpdateConfig(targetDir,projectName,stored,options,log){const{config,specs,axisFields}=detectProjectInfo(targetDir,projectName,stored,options,log),snapshot=loadSnapshot(targetDir);log(`
|
|
51
|
+
Updating...`);const needsMigration=stored.features.soloDevMode===!0&&!stored.collaborationMode;needsMigration&&log(" Migrating soloDevMode=true \u2192 collaborationMode='trunk-solo' (ADR-051)");const nextConfig=buildNextConfig(stored,axisFields,config.language,needsMigration,projectName);return{config,specs,snapshot,nextConfig}}function handleAdverseState(adverseState,force){if(!adverseState)return;const warning=`
|
|
52
|
+
Warning: ${adverseState.message}
|
|
53
|
+
${adverseState.suggestedFix}
|
|
54
|
+
`;if(!force)throw new UserFacingError(`${adverseState.message}
|
|
55
|
+
${adverseState.suggestedFix}
|
|
56
|
+
${t("cli.shared.force_override_hint")}`);getLogger().warn("update.adverse_git_state",{message:adverseState.message,suggested_fix:adverseState.suggestedFix},warning)}function updateLogger(json){return json?()=>{}:msg=>{process.stdout.write(`${msg}
|
|
57
|
+
`)}}function missingConfigResult(options,log){return options.json?jsonOutput("update","error",{},["No arbiter.json found. Run `arbiter init` first."],{errorClass:"config"}):log(" No arbiter.json found. Run `arbiter init` first.\n"),process.exit(78),{keysRun:null}}function saveValidatedConfig(options,nextConfig,targetDir){const validation=validateConfig(nextConfig);if(!validation.ok){options.json?jsonOutput("update","error",{},[`Config invalid after update: ${validation.errors.join("; ")}`]):process.stderr.write(`${t("cli.update.config_invalid",{errors:validation.errors.join("; ")})}
|
|
58
|
+
`),process.exit(2);return}saveConfigAndSnapshot(targetDir,validation.config)}function resolveSelectionPolicy(options,targetDir){const only=options.only??[],ignorePatterns=loadIgnorePatterns(targetDir);return{only,selectPredicate:buildSelectionPredicate({patterns:ignorePatterns,only})}}export async function runUpdate(options){const targetDir=resolve(options.dir??process.cwd()),log=updateLogger(options.json);log(`
|
|
59
|
+
Arbiter \u2014 update
|
|
60
|
+
`),ensureDir(join(targetDir,".arbiter"));const lock=await acquireLock(join(targetDir,".arbiter",".lock"));try{const stored=loadConfig(targetDir);if(!stored)return missingConfigResult(options,log);const projectName=slugifyProjectName(resolveProjectName(targetDir,stored));handleAdverseState(detectAdverseGitState(targetDir),options.force);const{config,specs,snapshot,nextConfig}=prepareUpdateConfig(targetDir,projectName,stored,options,log),adoptPredicate=buildAdoptPredicate(options),{only,selectPredicate}=resolveSelectionPolicy(options,targetDir);if(options.adoptPlan){const plan=runAdoptPlan(specs,snapshot,nextConfig,targetDir,{adoptPredicate,selectPredicate});return printAdoptPlan(plan.records,plan.results,plan.retirement,targetDir,options.json),{keysRun:null}}const onAdopt=(key,priorContent,newContent)=>{recordLocalOverride(targetDir,{key,priorContent,newContent})},{results,keysRun,errors:generatorErrors}=selectAndRunWithManifest(specs,snapshot,nextConfig,targetDir,{adoptPredicate,selectPredicate,onAdopt}),selection=reportSelection(results,targetDir,only,options.json),pluginResults=await runPlugins(targetDir,Array.isArray(stored.plugins)?stored.plugins:[],stored).catch(err=>handlePluginError(err,options.json));results.push(...pluginResults),options.json||(printResults(results,targetDir),printStats(results));const{adopted,stillWithheldSafety}=reportAdoption(results,targetDir,options.json),backendResult=runGithubSetup(config,log),unwiredWarning=detectUnwiredGateWarning(results),gateSigWarning=detectGateSignatureWarning(results),legacyCollisionWarning=detectLegacyWorkflowCollisionWarning(targetDir),restored=restoredKeys(results,targetDir),restoredWarning=detectRestoredFilesWarning(restored),allWarnings=[...backendResult.warnings,...unwiredWarning?[unwiredWarning]:[],...gateSigWarning?[gateSigWarning]:[],...legacyCollisionWarning?[legacyCollisionWarning]:[],...restoredWarning?[restoredWarning]:[]];saveValidatedConfig(options,nextConfig,targetDir);const summary={created:results.filter(r=>r.action==="created").length,updated:results.filter(r=>r.action==="backed-up-and-replaced").length,skipped:results.filter(r=>r.action==="skipped"||r.action==="dry-run").length,withheld:results.filter(r=>r.withheld===!0&&r.adopted!==!0).length,adopted:adopted.length,withheldSafety:stillWithheldSafety.length,restored:restored.length,ignored:selection.ignored.length,deselected:selection.deselected.length};return emitUpdateOutcome(options,summary,generatorErrors,allWarnings),{keysRun}}finally{endGenerationSession(),await lock.release()}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{intro,outro,select,confirm,isCancel,cancel}from"@clack/prompts";import{runUpgradeLevel}from"./upgrade-level.js";const LEVELS=[{value:"L2",label:"L2 \u2014 standard governance (tests, gates, coverage)"}];export async function runInteractiveUpgradeLevel(opts={}){const dir=opts.dir;intro("arbiter upgrade-level");const target=await select({message:"Upgrade to which governance level?",options:LEVELS});if(isCancel(target)){cancel("Cancelled.");return}const proceed=await confirm({message:`Apply upgrade to ${target}?`});if(isCancel(proceed)||!proceed){cancel("Cancelled.");return}outro(`Upgrading to ${target}\u2026`),await runUpgradeLevel({...dir!==void 0?{dir}:{},target,extend:!1,json:!1})}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";import{ensureDir,writeFileTranslated}from"../utils/fs.js";import{acquireLock}from"../utils/file-lock.js";import{loadConfig,saveConfig}from"../utils/config.js";import{runCli}from"../utils/run-cli.js";import{getLogger}from"../utils/logger.js";import{jsonOutput}from"../utils/json-output.js";import{validateConfig}from"../config/schema.js";import{ArbiterError}from"../utils/errors.js";import{t}from"../i18n/index.js";const LEVEL_RANK={L1:1,L2:2,L3:3,L4:4},DEFAULT_GRACE_DAYS=30;export const GRACE_MAX_DAYS=35;function clampGraceEndsAt(endsAtMs,nowMs){const maxMs=nowMs+GRACE_MAX_DAYS*864e5;return endsAtMs>maxMs?{ms:maxMs,clamped:!0}:{ms:endsAtMs,clamped:!1}}function resolveGraceWindow(days,nowMs){const{ms,clamped}=clampGraceEndsAt(nowMs+days*864e5,nowMs);return clamped&&process.stderr.write(`${t("cli.upgrade_level.grace_capped",{max:GRACE_MAX_DAYS,requested:days})}
|
|
3
|
+
`),{graceEndsAt:new Date(ms).toISOString(),effectiveDays:clamped?GRACE_MAX_DAYS:days}}function assertValidGraceDays(days){if(days!==void 0&&(!Number.isInteger(days)||days<1))throw new Error(`--days must be a positive integer (>= 1); got "${String(days)}". Example: --days 30.`)}function captureBaselineIfPresent(dir){const baselineScript=join(dir,"scripts","capture-debt-baseline.mjs");if(existsSync(baselineScript))try{runCli("node",["scripts/capture-debt-baseline.mjs","--update"],{cwd:dir})}catch(err){getLogger().warn("upgrade_level.baseline_capture_failed",{err:err instanceof Error?err.message:String(err)},"Baseline capture skipped/failed; re-run after `arbiter update`.")}}function rejectUnsupportedUpgradeTarget(current,target,json){if(current==="L1"&&target==="L2")return!1;const error=ArbiterError.fromKey("E_GRACE_NOT_SUPPORTED","errors.E_GRACE_NOT_SUPPORTED",{},{hint:"To move to a higher level, run `arbiter configure --set governanceLevel=<L3|L4>` followed by `arbiter update`."});if(json)return jsonOutput("upgrade-level","error",{},[error.message]),process.exit(1),!0;throw error}export async function runUpgradeLevel(opts){assertValidGraceDays(opts.days);const dir=resolve(opts.dir??"."),stored=loadConfig(dir);if(!stored){if(opts.json){jsonOutput("upgrade-level","error",{},["No arbiter.json found. Run arbiter init first."]),process.exit(1);return}throw ArbiterError.fromKey("E_CONFIG_NOT_FOUND","errors.E_CONFIG_NOT_FOUND",{},{hint:"Run `arbiter init` to initialize governance in this directory.",docUrl:"https://arbiter.dev/reference/cli#init"})}if(opts.extend){await handleExtend(dir,stored,opts.days??DEFAULT_GRACE_DAYS,opts.json);return}if(!opts.target)throw ArbiterError.fromKey("E_TARGET_REQUIRED","errors.E_TARGET_REQUIRED",{},{hint:"Example: `arbiter upgrade-level --target L2`."});const target=opts.target,current=stored.governanceLevel;if(target===current)throw ArbiterError.fromKey("E_ALREADY_AT_LEVEL","errors.E_ALREADY_AT_LEVEL",{level:current},{hint:"Run `arbiter update` to regenerate governance files at the current level."});if(LEVEL_RANK[target]<LEVEL_RANK[current])throw ArbiterError.fromKey("E_DOWNGRADE_NOT_SUPPORTED","errors.E_DOWNGRADE_NOT_SUPPORTED",{},{hint:"Set `governanceLevel` in arbiter.json to the desired level, then run `arbiter update`."});if(rejectUnsupportedUpgradeTarget(current,target,opts.json))return;const days=opts.days??DEFAULT_GRACE_DAYS,{graceEndsAt,effectiveDays}=resolveGraceWindow(days,Date.now()),upgraded={...stored,governanceLevel:target,graceFromLevel:current,graceEndsAt},validation=validateConfig(upgraded);if(!validation.ok)throw ArbiterError.fromKey("E_CONFIG_INVALID","errors.E_UPGRADE_CONFIG_INVALID",{errors:validation.errors.join("; ")},{hint:"Fix the errors above, or delete arbiter.json and re-run `arbiter init`."});captureBaselineIfPresent(dir),ensureDir(join(dir,".arbiter"));const lock=await acquireLock(join(dir,".arbiter",".lock"));try{await saveConfig(dir,validation.config)}finally{await lock.release()}if(opts.json){jsonOutput("upgrade-level","ok",{from:current,to:target,graceEndsAt,graceDays:effectiveDays});return}const endsDate=graceEndsAt.slice(0,10);process.stdout.write(`${t("cli.upgrade_level.grace_ends_warn",{date:endsDate,days:effectiveDays,target})}
|
|
4
|
+
`)}async function handleExtend(dir,stored,days,json){const existing=stored.graceEndsAt;if(!existing||Date.parse(existing)<=Date.now())throw ArbiterError.fromKey("E_NO_GRACE_PERIOD","errors.E_NO_GRACE_PERIOD",{},{hint:"Run `arbiter upgrade-level --target L2` to start a new grace period."});const _now=Date.now(),{ms:newEndsMs,clamped}=clampGraceEndsAt(Date.parse(existing)+days*864e5,_now);clamped&&process.stderr.write(`${t("cli.upgrade_level.grace_extend_capped",{max:GRACE_MAX_DAYS,requested:days})}
|
|
5
|
+
`);const newEndsAt=new Date(newEndsMs).toISOString(),appliedDays=Math.round((newEndsMs-Date.parse(existing))/864e5),arbiterDir=join(dir,".arbiter");ensureDir(arbiterDir);const logPath=join(arbiterDir,"grace-log.json");let log=[];if(existsSync(logPath)){let raw;try{raw=JSON.parse(readFileSync(logPath,"utf-8"))}catch{throw ArbiterError.fromKey("E_GRACE_LOG_MALFORMED","errors.E_GRACE_LOG_MALFORMED",{path:logPath},{hint:`Delete ${logPath} and retry.`})}if(!Array.isArray(raw))throw ArbiterError.fromKey("E_GRACE_LOG_MALFORMED","errors.E_GRACE_LOG_MALFORMED",{path:logPath},{hint:`Delete ${logPath} and retry.`});log=raw}log.push({action:"extend",at:new Date().toISOString(),previousEndsAt:existing,newEndsAt,days,...clamped?{appliedDays,clampedToMaxDays:GRACE_MAX_DAYS}:{}});const lock=await acquireLock(join(arbiterDir,".lock"));try{writeFileTranslated(logPath,JSON.stringify(log,null,2)+`
|
|
6
|
+
`),await saveConfig(dir,{...stored,graceEndsAt:newEndsAt})}finally{await lock.release()}if(json){jsonOutput("upgrade-level","ok",{action:"extend",newEndsAt,extensionDays:appliedDays});return}const endsDate=newEndsAt.slice(0,10);process.stdout.write(`${t("cli.upgrade_level.grace_extended",{date:endsDate,days:appliedDays})}
|
|
7
|
+
`)}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync,readFileSync}from"node:fs";import{ensureDir,writeFileTranslated}from"../utils/fs.js";import{join,resolve}from"node:path";import{PlanJsonV1}from"../types/plan.js";import{runVerify}from"../verify/run.js";import{jsonOutput}from"../utils/json-output.js";function writeErrorReview(pointerDir,taskId,reviewer,ruleId,message){try{ensureDir(pointerDir)}catch(err){process.stderr.write(`[arbiter] could not create pointer dir ${pointerDir}: ${err instanceof Error?err.message:String(err)}
|
|
3
|
+
`);return}const review={timestamp:new Date().toISOString(),task_id:taskId,run_id:"error",status:"ERROR",reviewer,verification:{ok:!1,violations:[{rule_id:ruleId,severity:"ERROR",message,ssot_pointer:null,evidence:{paths:[]}}],notes:[]},blocking:!0,blocking_reason:message};try{writeFileTranslated(join(pointerDir,"REVIEW.json"),JSON.stringify(review,null,2))}catch{}}function reportError(pointerDir,reviewer,ruleId,msg){writeErrorReview(pointerDir,"unknown",reviewer,ruleId,msg)}function parsePlan(planSource){let raw;try{raw=JSON.parse(planSource)}catch(err){return{ok:!1,msg:`invalid JSON: ${err instanceof Error?err.message:String(err)}`,ruleId:"SCHEMA"}}const parsed=PlanJsonV1.safeParse(raw);return parsed.success?{ok:!0,data:parsed.data}:{ok:!1,msg:`schema validation failed: ${parsed.error.issues.map(i=>i.message).join("; ")}`,ruleId:"SCHEMA"}}function emitError(json,filePath,msg){json?jsonOutput("verify plan","error",{file:filePath},[msg]):process.stderr.write(`Error: ${msg}
|
|
4
|
+
`)}function emitVerifyResult(json,result,exitCode,reviewPath){if(json){jsonOutput("verify plan",exitCode===0?"ok":"error",{status:result.status,runId:result.runId,violations:result.review.verification.violations.length,reviewPath,runDir:result.runDir});return}process.stdout.write(`verify plan: ${result.status} (runId=${result.runId}, violations=${result.review.verification.violations.length})
|
|
5
|
+
`);for(const v of result.review.verification.violations)process.stdout.write(` [${v.severity} ${v.rule_id}] ${v.message}
|
|
6
|
+
`);process.stdout.write(`review: ${reviewPath}
|
|
7
|
+
`)}export function runVerifyPlan(opts){const dir=resolve(opts.dir??"."),filePath=resolve(opts.file),pointerDir=join(dir,".arbiter","plan"),reviewer=opts.reviewer??"bridge-reviewer";if(!existsSync(filePath)){const msg=`plan file not found: ${filePath}`;return reportError(pointerDir,reviewer,"IO",msg),emitError(opts.json,filePath,msg),{exitCode:2,status:"ERROR"}}let planSource;try{planSource=readFileSync(filePath,"utf-8")}catch(err){const msg=`failed to read plan file: ${err instanceof Error?err.message:String(err)}`;return reportError(pointerDir,reviewer,"IO",msg),emitError(opts.json,filePath,msg),{exitCode:2,status:"ERROR"}}const parseResult=parsePlan(planSource);if(!parseResult.ok)return reportError(pointerDir,reviewer,parseResult.ruleId,parseResult.msg),emitError(opts.json,filePath,parseResult.msg),{exitCode:2,status:"ERROR"};const plan=parseResult.data;if(!plan.review_bridge.enabled){const skippedReview={timestamp:new Date().toISOString(),task_id:plan.task_id,run_id:"skipped",status:"SKIPPED",reviewer,verification:{ok:!0,violations:[],notes:["review_bridge.enabled:false"]},blocking:!1,blocking_reason:null};ensureDir(pointerDir);const reviewPath2=join(pointerDir,"REVIEW.json");return writeFileTranslated(join(pointerDir,"PLAN.json"),planSource),writeFileTranslated(reviewPath2,JSON.stringify(skippedReview,null,2)),opts.json?jsonOutput("verify plan","ok",{status:"SKIPPED",reviewPath:reviewPath2}):process.stdout.write(`verify plan: SKIPPED (review_bridge.enabled:false)
|
|
8
|
+
`),{exitCode:0,status:"SKIPPED",reviewPath:reviewPath2}}let result;try{result=runVerify({plan,planSource,targetDir:dir,...opts.reviewer!==void 0?{reviewer:opts.reviewer}:{},...opts.failOnWarn!==void 0?{failOnWarn:opts.failOnWarn}:{},extraRules:opts.extraRules??[]})}catch(err){const msg=`verification engine failed: ${err instanceof Error?err.message:String(err)}`;return reportError(pointerDir,reviewer,"RUNTIME",msg),emitError(opts.json,filePath,msg),{exitCode:2,status:"ERROR"}}const exitCode=result.status==="APPROVED"?0:2,reviewPath=join(result.pointerDir,"REVIEW.json");return emitVerifyResult(opts.json,result,exitCode,reviewPath),{exitCode,status:result.status,runId:result.runId,reviewPath}}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{loadTddEvidence,extractFailureSignature}from"../evidence/tdd.js";import{resolveEvidenceCommit,pathExistsInCommit}from"../evidence/git-checks.js";import{verifyRedExecution}from"../evidence/tdd-reexecute.js";export function runVerifyTdd(opts){const dir=opts.dir??process.cwd(),{taskId}=opts,checks=[],loadResult=loadTddEvidence(taskId,dir);if(!loadResult.ok)return fail(taskId,loadResult.reason,[{name:"evidence-file",pass:!1,reason:loadResult.reason}]);checks.push({name:"evidence-file",pass:!0});const ev=loadResult.data;if(ev.task_id!==taskId){const reason=`task_id mismatch: evidence contains "${ev.task_id}", expected "${taskId}"`;return fail(taskId,reason,[...checks,{name:"task-id-match",pass:!1,reason}])}if(checks.push({name:"task-id-match",pass:!0}),extractFailureSignature(ev.test_run_log)===null){const reason="no recognised failure signature found in test_run_log";return fail(taskId,reason,[...checks,{name:"failure-signature",pass:!1,reason}])}checks.push({name:"failure-signature",pass:!0});const resolved=resolveEvidenceCommit(ev,dir);if(resolved===null){const reason=unresolvedCommitReason(ev);return fail(taskId,reason,[...checks,{name:"sha-on-branch",pass:!1,reason}])}if("degraded"in resolved)return degrade(taskId,resolved.reason,[...checks,{name:"sha-on-branch",pass:!1,reason:resolved.reason}]);const redSha=resolved.sha;if(checks.push(reachableCommitCheck(ev,resolved)),!pathExistsInCommit(redSha,ev.test_path,dir)){const reason=`test_path "${ev.test_path}" not found in commit ${redSha}`;return fail(taskId,reason,[...checks,{name:"test-path-in-commit",pass:!1,reason}])}checks.push({name:"test-path-in-commit",pass:!0});const reExec=verifyRedExecution({...ev,test_commit_sha:redSha},dir);if(!reExec.ok){const reason=reExec.reason??"red-phase re-execution failed";return fail(taskId,reason,[...checks,{name:"red-execution",pass:!1,reason}])}return checks.push({name:"red-execution",pass:!0}),{status:"PASS",exitCode:0,taskId,checks}}function fail(taskId,reason,checks){return{status:"FAIL",exitCode:1,taskId,reason,checks}}function degrade(taskId,reason,checks){return{status:"DEGRADED",exitCode:2,taskId,reason,checks}}function unresolvedCommitReason(ev){const prefix=`test_commit_sha ${ev.test_commit_sha} is not reachable from HEAD`;return ev.test_blob_sha!==void 0?`${prefix} and no commit on this branch carries the recorded RED test content`:`${prefix} and this evidence predates the rebase-stable blob pin \u2014 re-record it with \`arbiter task record-red --test-path ${ev.test_path}\``}function reachableCommitCheck(ev,resolved){return resolved.healed?{name:"sha-on-branch",pass:!0,reason:`pinned sha ${ev.test_commit_sha} was rewritten (rebase) \u2014 resolved to ${resolved.sha} via the recorded RED test blob`}:{name:"sha-on-branch",pass:!0}}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync,readdirSync,statSync}from"node:fs";import{appendFileTranslated,ensureDir}from"../utils/fs.js";import{join,resolve}from"node:path";import{runProbes}from"../compatibility/probe.js";import{formatText}from"../compatibility/report.js";import{loadConfig}from"../utils/config.js";import{jsonOutput}from"../utils/json-output.js";import{detectLanguage}from"../detectors/language.js";import{verifySummarySha}from"../risk/sha-check.js";import{classifyPath,highestRisk,UNCLASSIFIED_LEVEL}from"../risk/classifier.js";import{loadSummaryFile}from"../evidence/load.js";import{validateSummarySchema}from"../evidence/summary.js";const FRESHNESS_DAYS=7,MS_PER_DAY=864e5,SKIP_REASON_PATTERN=/^(?:flake|infra|external):#\d+(?::[\w-]+)?$/;function isValidSkipReason(raw){return SKIP_REASON_PATTERN.test(raw)}function writeSkipEntry(dir,reason){const evidenceDir=join(dir,".evidence"),logPath=join(evidenceDir,"skip-log.jsonl");try{ensureDir(evidenceDir),appendFileTranslated(logPath,JSON.stringify({ts:new Date().toISOString(),reason})+`
|
|
3
|
+
`)}catch(err){const errno=err&&typeof err=="object"&&"code"in err?String(err.code):"unknown";throw process.stderr.write(`arbiter verify evidence: refusing to honour E2E_RISK_SKIP \u2014 audit log write failed (${errno}) at ${logPath}
|
|
4
|
+
`),err}}const KNOWN_STACKS=new Set(["typescript","java","kotlin","rust","python","go"]);function resolveStack(summary,dir){const stored=summary.stack;return typeof stored=="string"&&KNOWN_STACKS.has(stored)?stored:detectLanguage(dir)}function classifyFiles(summary,stack){const raw=summary.files;if(!Array.isArray(raw)||raw.length===0)return null;const levels=[];for(const f of raw)typeof f=="string"&&levels.push(classifyPath(f,stack));return levels.length===0?null:highestRisk(levels)}function staleSeverity(level){return level==="R0"||level==="R1"||level==="R2"?{exitCode:2,status:"error"}:{exitCode:1,status:"warning"}}function handleRiskSkip(dir){const skip=process.env.E2E_RISK_SKIP;if(!skip||skip.trim()==="")return null;const trimmed=skip.trim();return isValidSkipReason(trimmed)?(writeSkipEntry(dir,trimmed),process.stderr.write(`arbiter verify evidence: E2E_RISK_SKIP honoured \u2014 reason="${trimmed}" log=${join(dir,".evidence","skip-log.jsonl")}
|
|
5
|
+
`),{status:"ok",exitCode:0,skipped:!0,reason:trimmed}):(process.stderr.write(`arbiter verify evidence: E2E_RISK_SKIP="${trimmed}" rejected \u2014 must match <flake|infra|external>:#<issue>[:<slug>] (e.g. flake:#123). Falling through to normal verification.
|
|
6
|
+
`),null)}function toRunIdCandidate(evidenceDir,name){const candidate=join(evidenceDir,name,"SUMMARY.json");if(!existsSync(candidate))return null;try{return{name,mtimeMs:statSync(join(evidenceDir,name)).mtimeMs,path:candidate}}catch{return null}}function isNewerCandidate(best,next){return best===null?!0:next.mtimeMs!==best.mtimeMs?next.mtimeMs>best.mtimeMs:next.name>best.name}function pickNewestRunIdSummary(evidenceDir,entries){let best=null;for(const name of entries){const candidate=toRunIdCandidate(evidenceDir,name);candidate&&isNewerCandidate(best,candidate)&&(best=candidate)}return best?.path??null}function resolveSummaryPath(dir){const rootPath=join(dir,".evidence","SUMMARY.json");if(existsSync(rootPath))return rootPath;const evidenceDir=join(dir,".evidence");if(!existsSync(evidenceDir))return rootPath;let entries;try{entries=readdirSync(evidenceDir,{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name)}catch{return rootPath}return pickNewestRunIdSummary(evidenceDir,entries)??rootPath}function loadSummary(summaryPath){const loaded=loadSummaryFile(summaryPath);return loaded.ok?loaded.body:{status:"error",exitCode:1,reason:loaded.reason}}function isResult(v){return"exitCode"in v&&"status"in v}function makeStaleResult(riskLevel,ageStr){if(riskLevel===null)return{status:"warning",exitCode:1,reason:ageStr};const sev=staleSeverity(riskLevel);return{status:sev.status,exitCode:sev.exitCode,riskLevel,reason:sev.exitCode===2?`${ageStr}; high-risk change set requires fresh evidence`:ageStr}}function checkFreshness(summary,riskLevel){const ts=summary.timestamp;if(typeof ts!="string")return null;const tsMs=Date.parse(ts);if(!Number.isFinite(tsMs))return null;const ageDays=(Date.now()-tsMs)/MS_PER_DAY;if(ageDays<=FRESHNESS_DAYS)return null;const ageStr=`summary is ${ageDays.toFixed(1)} days old (>${FRESHNESS_DAYS})`;return makeStaleResult(riskLevel,ageStr)}export function runVerifyEvidence(opts){const dir=resolve(opts.dir??"."),skipResult=handleRiskSkip(dir);if(skipResult)return skipResult;const summaryPath=resolveSummaryPath(dir),loaded=loadSummary(summaryPath);if(isResult(loaded))return loaded;const shaResult=verifySummarySha(loaded);if(!shaResult.ok)return{status:"error",exitCode:2,reason:shaResult.reason??"sha mismatch"};const schema=validateSummarySchema(loaded);if(!schema.ok)return{status:"error",exitCode:1,reason:schema.errors.join("; ")};const stack=resolveStack(loaded,dir),aggregated=classifyFiles(loaded,stack);if(aggregated===UNCLASSIFIED_LEVEL)return{status:"warning",exitCode:1,riskLevel:aggregated,reason:"one or more files could not be classified \u2014 manual review required"};const riskLevel=aggregated,stale=checkFreshness(loaded,riskLevel);if(stale)return stale;const result={status:"ok",exitCode:0};return riskLevel!==null&&(result.riskLevel=riskLevel),attachProvenance(result,loaded),result}function attachProvenance(result,loaded){loaded.provenance!==void 0&&(result.provenance=loaded.provenance)}export function runVerify(opts){const dir=resolve(opts.dir??"."),report=runProbes(dir);if(opts.json){const cfg=loadConfig(dir),enriched={...report,effectiveConfig:cfg};jsonOutput("validate",report.hasFailures?"error":report.hasWarnings?"warning":"ok",enriched)}else process.stdout.write(formatText(report)+`
|
|
7
|
+
`);report.hasFailures&&process.exit(1)}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{AUTONOMY_LEVELS}from"../../config/schema.js";import{levelAtLeast}from"../../config/levels.js";const COHERENCE_MATRIX={"trunk-solo":{L1:{severity:"OK",message:""},L2:{severity:"OK",message:""},L3:{severity:"WARN",message:"trunk-solo at L3: no human-approval gate is active. Cosign signatures bind to the dev identity only \u2014 no reviewer attestation. Acceptable under the \xA711.10(k) single-developer exception (ADR-091): arbiter generates the attestation doc, validation-evidence template, and reactivation trigger check (\u22653 authors or EXTERNAL_AUDIT=true \u2192 CI fails, requiring manual switch to CODEOWNERS). See docs/governance/SOLO_DEV_EXCEPTION.md after running arbiter update."},L4:{severity:"CRITICAL",message:"L4 requires CODEOWNERS + human-approval attestation (ADR-050). trunk-solo bypasses both \u2014 the combination is incoherent.",remediation:"Switch collaborationMode to peer-review or gated-review, or downgrade governanceLevel to L3."}},"peer-review":{L1:{severity:"OK",message:""},L2:{severity:"OK",message:""},L3:{severity:"OK",message:""},L4:{severity:"WARN",message:'peer-review at L4: pr-ff merge mode is mandatory (no direct push allowed). If your config sets solo.mergeMode="direct", it will be overridden to "pr-ff".'}},"gated-review":{L1:{severity:"WARN",message:"gated-review at L1: uncommon cell. CODEOWNERS + merge queue overhead is unusual for a lenient governance project. Consider peer-review unless you have specific audit requirements at L1."},L2:{severity:"OK",message:""},L3:{severity:"OK",message:""},L4:{severity:"OK",message:""}}};export function validateCollaborationCoherence(mode,level){const entry=COHERENCE_MATRIX[mode][level];return{valid:entry.severity!=="CRITICAL",severity:entry.severity,message:entry.message,...entry.remediation!==void 0?{remediation:entry.remediation}:{}}}const HEAVY_OVERLAYS=new Set(["pharma","iso27001","regulated"]),MEDIUM_OVERLAYS=new Set(["sox","gdpr","iso9001"]),HEAVY_MIN_LEVEL="L3",MEDIUM_MIN_LEVEL="L2",LEVEL_RANK={L1:1,L2:2,L3:3,L4:4};function belowLevel(level,min){return LEVEL_RANK[level]<LEVEL_RANK[min]}export function validateOverlayCoherence(overlay,level){return HEAVY_OVERLAYS.has(overlay)&&belowLevel(level,HEAVY_MIN_LEVEL)?{valid:!0,severity:"WARN",message:`industryOverlay='${overlay}' is a heavy regulated overlay (audit-trail / security controls) but governanceLevel=${level} does not activate the mutation, evidence-harness, or human-approval gates these controls rely on. The overlay scaffolds the artefacts, but the gate set will not enforce them. Recommended: raise governanceLevel to ${HEAVY_MIN_LEVEL}+ (or downgrade the overlay).`}:MEDIUM_OVERLAYS.has(overlay)&&belowLevel(level,MEDIUM_MIN_LEVEL)?{valid:!0,severity:"WARN",message:`industryOverlay='${overlay}' is a compliance overlay but governanceLevel=${level} does not activate the debt + security-scan gates it complements. Recommended: raise governanceLevel to ${MEDIUM_MIN_LEVEL}+ so the overlay's controls are backed by enforced gates.`}:{valid:!0,severity:"OK",message:""}}export function validateAutonomyCoherence(autonomy,level,hasCi){if(!AUTONOMY_LEVELS.includes(autonomy))return{valid:!0,severity:"WARN",message:`automation.autonomy='${autonomy}' is not a recognized autonomy level (${AUTONOMY_LEVELS.join("|")}) \u2014 run \`arbiter update\` to repair the automation block.`};const a=autonomy;return a==="L3"&&!hasCi?{valid:!1,severity:"CRITICAL",message:"automation.autonomy=L3 (full-auto wave) requires CI: without a red signal the wave cannot verify its own pushes (ADR-093 \xA74).",remediation:"Set automation.autonomy to L2 or lower, or enable CI (add a workflow file under .github/workflows/)."}:level==="L4"&&a==="L3"?{valid:!1,severity:"CRITICAL",message:"governanceLevel=L4 mandates human approval (ADR-050); automation.autonomy=L3 autonomous fix-on-red autopush violates the human-in-loop control.",remediation:"Set automation.autonomy to L2 or lower, or downgrade governanceLevel."}:{valid:!0,severity:"OK",message:""}}export function validateProfileCoherence(maxParallelWorktrees,defaultGateLevel,mode,level){const mpw=typeof maxParallelWorktrees=="number"?maxParallelWorktrees:1;return mpw>1&&mode==="trunk-solo"?{valid:!1,severity:"CRITICAL",message:`automation.maxParallelWorktrees=${mpw} is incoherent with collaborationMode='trunk-solo': a solo-trunk workflow never uses worktrees, so the parallelism can never take effect.`,remediation:"Set automation.maxParallelWorktrees to 1, or switch collaborationMode to peer-review/gated-review."}:defaultGateLevel==="L1"&&levelAtLeast(level,"L3")?{valid:!0,severity:"WARN",message:`automation.defaultGateLevel='L1' under governanceLevel=${level}: the L1 gate skips the coverage, mutation, and evidence gates the rigour tier relies on. Default verification runs leaner than the governance level implies.`}:{valid:!0,severity:"OK",message:""}}const INCOMPATIBLE_LANGUAGE_ARCHETYPES=new Map([["go",new Set(["frontend-spa"])],["java",new Set(["frontend-spa"])],["kotlin",new Set(["frontend-spa"])],["python",new Set(["embedded"])]]);export function validateLanguageArchetypeCoherence(language,archetype){return archetype===void 0?{valid:!0,severity:"OK",message:""}:INCOMPATIBLE_LANGUAGE_ARCHETYPES.get(language)?.has(archetype)===!0?{valid:!0,severity:"WARN",message:`language='${language}' with archetype='${archetype}' is an unusual pairing: arbiter does not scaffold a meaningful ${archetype} toolchain for ${language}. The project will be generated, but the archetype-specific gates and structure will be absent. Recommended: pick an archetype that matches ${language}, or switch language.`}:{valid:!0,severity:"OK",message:""}}export function validateTrunkSoloParityCoherence(mode,wiring){if(mode!=="trunk-solo")return{valid:!0,severity:"OK",message:""};const{hasParityCheck,hasPushGating}=wiring;if(hasParityCheck&&hasPushGating)return{valid:!0,severity:"OK",message:""};const missing=[];return hasParityCheck||missing.push("a local-ci-parity check (run.sh gate full \u2261 CI, INV-59)"),hasPushGating||missing.push("push-gating (the full gate wired to run before push reaches trunk)"),{valid:!1,severity:"CRITICAL",message:`collaborationMode='trunk-solo' is missing ${missing.join(" and ")}. trunk-solo is only sound as 'no PR ceremony, FULL gate locally, CI as verification mirror' \u2014 without both, there is no independent CI net before trunk.`,remediation:"Run `arbiter update` to wire scripts/check-local-ci-parity.mjs and the pre-push hook, or switch collaborationMode to peer-review/gated-review."}}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync,readFileSync}from"node:fs";import{join}from"node:path";import{runCli}from"../utils/run-cli.js";import{jsonOutput}from"../utils/json-output.js";import{isRunningFromMainRepo,workingTreeDirty,branchFullyMerged}from"../worktree/validate.js";import{isOpenLogEntry,runWorktreeClose}from"./worktree.js";const DEFAULT_STALE_HOURS=24;function readOpenLog(gitRoot){const logPath=join(gitRoot,".arbiter","worktree-open.log.json");if(!existsSync(logPath))return[];try{const raw=JSON.parse(readFileSync(logPath,"utf-8"));return Array.isArray(raw)?raw.filter(isOpenLogEntry):[]}catch{return[]}}function lastCommitIso(branch,gitRoot){try{const out=runCli("git",["log","-1","--format=%cI",branch,"--"],{cwd:gitRoot}).stdout.trim();return out.length>0?out:null}catch{return null}}export function detectPruneCandidates(opts){const now=opts.now??new Date,thresholdMs=opts.staleHours*60*60*1e3,candidates=[],skipped=[];if(!opts.noFetch)try{runCli("git",["fetch","origin"],{cwd:opts.gitRoot,timeoutMs:3e4})}catch{process.stderr.write(`Warning: git fetch failed \u2014 merged checks may use stale refs
|
|
3
|
+
`)}for(const entry of readOpenLog(opts.gitRoot)){const verdict=classifyEntry(entry,opts.gitRoot,now,thresholdMs);verdict.kind==="candidate"?candidates.push(verdict.candidate):skipped.push(verdict.skip)}return{candidates,skipped}}function mergedOrNull(entry,gitRoot){try{return branchFullyMerged(entry.branch,entry.baseBranch,gitRoot,!1)}catch{return null}}function skip(taskId,reason){return{kind:"skip",skip:{taskId,reason}}}function classifyEntry(entry,gitRoot,now,thresholdMs){if(!existsSync(entry.worktreePath))return skip(entry.taskId,"missing-dir");if(workingTreeDirty(entry.worktreePath))return skip(entry.taskId,"dirty");const merged=mergedOrNull(entry,gitRoot);if(merged===null)return skip(entry.taskId,"branch-missing");if(merged)return{kind:"candidate",candidate:{taskId:entry.taskId,worktreePath:entry.worktreePath,branch:entry.branch,reason:"merged",lastActivity:entry.openedAt}};const commitIso=lastCommitIso(entry.branch,gitRoot);if(commitIso===null)return skip(entry.taskId,"branch-missing");const lastActivityMs=Math.max(new Date(entry.openedAt).getTime(),new Date(commitIso).getTime());return now.getTime()-lastActivityMs>thresholdMs?{kind:"candidate",candidate:{taskId:entry.taskId,worktreePath:entry.worktreePath,branch:entry.branch,reason:"inactive",lastActivity:new Date(lastActivityMs).toISOString()}}:skip(entry.taskId,"active")}function executeCandidates(candidates,noFetch,closeFn){const closedIds=[],failed=[];for(const c of candidates){if(workingTreeDirty(c.worktreePath)){failed.push({taskId:c.taskId,error:"tree became dirty between detect and close"});continue}try{closeFn({taskId:c.taskId,noFetch,...c.reason==="inactive"?{force:!0,keepBranch:!0}:{}}),closedIds.push(c.taskId)}catch(err){failed.push({taskId:c.taskId,error:err instanceof Error?err.message:String(err)})}}return{closedIds,failed}}function emitPruneReport(emit,detected,execution,staleHours,execute){emit(`Prune candidates (${detected.candidates.length}), threshold ${staleHours}h:`);for(const c of detected.candidates)emit(` ${c.reason.padEnd(8)} ${c.taskId} ${c.branch} ${c.worktreePath}`);if(detected.skipped.length>0){emit(`Skipped (${detected.skipped.length}):`);for(const s of detected.skipped)emit(` ${s.reason.padEnd(13)} ${s.taskId}`)}if(!execute){emit("Dry-run \u2014 nothing closed. Re-run with --execute to close these worktrees.");return}emit(`Closed ${execution.closedIds.length} worktree(s).`);for(const f of execution.failed)emit(`Failed to close ${f.taskId}: ${f.error}`);if(execution.failed.length>0)throw new Error(`worktree prune: ${execution.failed.length} candidate(s) failed to close`)}export function runWorktreePrune(opts={}){const cwd=opts.cwd??process.cwd(),gitRoot=runCli("git",["rev-parse","--show-toplevel"],{cwd}).stdout.trim()||cwd,emit=opts.onLine??(line=>{process.stdout.write(`${line}
|
|
4
|
+
`)}),closeFn=opts.closeFn??runWorktreeClose,staleHours=opts.staleHours??DEFAULT_STALE_HOURS,noFetch=opts.noFetch??!1,execute=opts.execute??!1;if(!isRunningFromMainRepo(gitRoot))throw new Error("Must run 'worktree prune' from the main repository, not a worktree.");const detected=detectPruneCandidates({gitRoot,staleHours,noFetch,...opts.now!==void 0?{now:opts.now}:{}}),execution=execute?executeCandidates(detected.candidates,noFetch,closeFn):{closedIds:[],failed:[]};if(opts.json){jsonOutput("worktree-prune",execution.failed.length===0?"ok":"error",{dryRun:!execute,staleHours,candidates:detected.candidates,skipped:detected.skipped,closed:execution.closedIds,failed:execution.failed});return}emitPruneReport(emit,detected,execution,staleHours,execute)}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import{existsSync,readFileSync}from"node:fs";import{ensureDir,renameTranslated,toFsError,writeFileTranslated}from"../utils/fs.js";import{join,resolve}from"node:path";import{runCli,CliError}from"../utils/run-cli.js";import{t}from"../i18n/index.js";import{loadConfig}from"../utils/config.js";import{acquireLock}from"../utils/file-lock.js";import{jsonOutput}from"../utils/json-output.js";import{sanitizeTaskId,branchNameFor,resolveWorktreeBase,worktreePathFor,siblingWorktreePathFor,worktreeDirectoryName}from"../worktree/paths.js";import{materializeLink,checkLinkIntegrity}from"../worktree/links.js";import{harvestFiles}from"../worktree/harvest.js";import{isRunningFromMainRepo,workingTreeDirty,branchFullyMerged}from"../worktree/validate.js";const DEFAULT_LINKS=[{path:".claude/settings.local.json",required:!1},{path:".env",template:".env.example",required:!1},{path:"node_modules",required:!1,type:"directory",strategy:"symlink-children"}];function defaultWorktreeConfig(){return{base:null,links:DEFAULT_LINKS,closeHook:null}}function arbiterLogDir(gitRoot){return join(gitRoot,".arbiter")}function hasLogEntryShape(x){if(typeof x!="object"||x===null)return!1;const e=x;return typeof e.taskId=="string"&&typeof e.worktreePath=="string"&&typeof e.branch=="string"}export function isOpenLogEntry(x){return hasLogEntryShape(x)&&typeof x.openedAt=="string"}export function isCloseLogEntry(x){return hasLogEntryShape(x)&&typeof x.closedAt=="string"}function readJsonArray(path){if(!existsSync(path))return[];try{const raw=JSON.parse(readFileSync(path,"utf-8"));if(!Array.isArray(raw))throw new SyntaxError(`Expected JSON array, got ${typeof raw}`);return raw}catch(err){if(!(err instanceof SyntaxError))throw toFsError(err,path);const ts=new Date().toISOString().replace(/[:.]/g,"-"),backupPath=`${path}.corrupt-${ts}`;let moved=!1,renameErrMsg="";try{renameTranslated(path,backupPath),moved=!0}catch(renameErr){renameErrMsg=renameErr instanceof Error?renameErr.message:String(renameErr)}return process.stderr.write(moved?`Warning: corrupt JSON at ${path} \u2014 moved to ${backupPath}
|
|
3
|
+
`:`Warning: corrupt JSON at ${path} \u2014 could not back up (rename failed: ${renameErrMsg}); original may be overwritten
|
|
4
|
+
`),[]}}function writeJsonArray(path,entries){ensureDir(resolve(path,"..")),writeFileTranslated(path,JSON.stringify(entries,null,2)+`
|
|
5
|
+
`)}function getGitRoot(cwd){return runCli("git",["rev-parse","--show-toplevel"],{cwd}).stdout.trim()}function materializeLinks(specs,gitRoot,worktreePath){const summary={linked:0,linkedDir:0,linkedChildren:0,copied:0,copiedDir:0,missing:0};for(const spec of specs){const result=materializeLink(spec,gitRoot,worktreePath);result.result==="LINKED"?summary.linked++:result.result==="LINKED_DIR"?summary.linkedDir++:result.result==="LINKED_CHILDREN"?summary.linkedChildren++:result.result==="COPIED_TEMPLATE"?summary.copied++:result.result==="COPIED_DIR"?summary.copiedDir++:summary.missing++}return summary}function printLinkSummary(summary){process.stdout.write(`Links: ${summary.linked} linked, ${summary.linkedDir} linked-dir, ${summary.linkedChildren} linked-children, ${summary.copied} copied-from-template, ${summary.copiedDir} copied-dir, ${summary.missing} missing
|
|
6
|
+
`)}function resolveEffectiveBase(baseBranch,gitRoot){try{return runCli("git",["rev-parse","--verify",`refs/heads/${baseBranch}`],{cwd:gitRoot}),baseBranch}catch(err){if(err instanceof CliError&&!err.notFound&&!err.timedOut)try{return runCli("git",["rev-parse","--verify",`refs/remotes/origin/${baseBranch}`],{cwd:gitRoot}),`origin/${baseBranch}`}catch(innerErr){throw innerErr instanceof CliError&&!innerErr.notFound&&!innerErr.timedOut?new Error(`Base branch '${baseBranch}' does not exist. Create it or specify a different base with --base.`,{cause:innerErr}):innerErr}throw err}}function resolveOpenWorktreePath(opts,gitRoot,worktreeConfig,taskId,slug){if(opts.sibling!==void 0){const siblingSlug=opts.sibling||worktreeDirectoryName(taskId,slug);return siblingWorktreePathFor(gitRoot,siblingSlug)}const worktreeBase=resolveWorktreeBase(gitRoot,worktreeConfig.base,opts.worktreesDir??process.env.ARBITER_WORKTREES_DIR);return worktreePathFor(worktreeBase,taskId,slug)}export async function runWorktreeOpen(opts){const cwd=opts.cwd??process.cwd(),warn=opts.onWarning??(msg=>{process.stdout.write(`${msg}
|
|
7
|
+
`)}),gitRoot=getGitRoot(cwd);if(!isRunningFromMainRepo(gitRoot))throw new Error("Must run from the main repository, not a worktree. The .git entry at this path is a file (gitdir pointer), not a directory.");if(workingTreeDirty(cwd,"exclude"))throw new Error("Working tree has uncommitted changes. Commit or stash your changes before opening a worktree.");const taskId=sanitizeTaskId(opts.taskId),slug=opts.slug,branchName=branchNameFor(taskId,slug),baseBranch=opts.base??"main",wtConfig=loadConfig(gitRoot)?.worktree??defaultWorktreeConfig(),worktreePath=resolveOpenWorktreePath(opts,gitRoot,wtConfig,taskId,slug);if(existsSync(worktreePath))throw new Error(`Worktree already exists at: ${worktreePath}
|
|
8
|
+
Run 'arbiter worktree list' to see open worktrees.`);const effectiveBase=resolveEffectiveBase(baseBranch,gitRoot),baseRef=runCli("git",["rev-parse","--short",effectiveBase],{cwd:gitRoot}).stdout.trim();ensureDir(resolve(worktreePath,"..")),runCli("git",["worktree","add","-b",branchName,worktreePath,effectiveBase],{cwd:gitRoot});const linkSpecs=opts.withBuildLinks?[...wtConfig.links,...wtConfig.buildLinks??[]]:wtConfig.links,linkSummary=materializeLinks(linkSpecs,gitRoot,worktreePath);warnDanglingLinks(linkSpecs,worktreePath,warn);const arbiterDir=arbiterLogDir(gitRoot);ensureDir(arbiterDir);const lock=await acquireLock(join(arbiterDir,".lock"));try{const logPath=join(arbiterDir,"worktree-open.log.json"),entries=readJsonArray(logPath).filter(isOpenLogEntry);entries.push({taskId,slug:slug??null,worktreePath,branch:branchName,baseBranch,baseRef,openedAt:new Date().toISOString()}),writeJsonArray(logPath,entries)}finally{await lock.release()}if(opts.json){jsonOutput("worktree-open","ok",{worktreePath,branch:branchName,baseBranch,baseRef});return}process.stdout.write(`${t("cli.worktree.ready",{path:worktreePath})}
|
|
9
|
+
`),process.stdout.write(`${t("cli.worktree.branch",{branch:branchName})}
|
|
10
|
+
`),process.stdout.write(`${t("cli.worktree.base",{base:baseBranch,ref:baseRef})}
|
|
11
|
+
`),printLinkSummary(linkSummary),process.stdout.write(`${t("cli.worktree.next",{path:worktreePath})}
|
|
12
|
+
`)}function runCloseHookIfConfigured(hookPath,worktreePath,gitRoot,force,warn){if(!hookPath)return;const absPath=resolve(gitRoot,hookPath);if(!existsSync(absPath)){if(!force)throw new Error(`Close hook not found: ${absPath}
|
|
13
|
+
Fix the path in arbiter.json or use --force.`);return}try{runCli(absPath,[resolve(worktreePath)],{timeoutMs:6e4})}catch(err){const msg=err instanceof Error?err.message:String(err);if(!force)throw new Error(`Close hook failed: ${msg}`,{cause:err});warn(`Warning: close hook failed: ${msg}`)}}function deleteTaskBranch(branch,gitRoot,force){try{return runCli("git",["branch","-d",branch],{cwd:gitRoot}),!0}catch(softErr){if(!force)return process.stderr.write(`Warning: could not delete branch '${branch}': ${softErr instanceof Error?softErr.message:String(softErr)}
|
|
14
|
+
`),!1;try{return runCli("git",["branch","-D",branch],{cwd:gitRoot}),!0}catch(hardErr){return process.stderr.write(`Warning: could not force-delete branch '${branch}': ${hardErr instanceof Error?hardErr.message:String(hardErr)}
|
|
15
|
+
`),!1}}}function assertBranchMerged(branch,baseBranch,gitRoot,noFetch,force){if(force)return;if(!branchFullyMerged(branch,baseBranch,gitRoot,!noFetch))throw new Error(`Branch '${branch}' has not been merged into '${baseBranch}'.
|
|
16
|
+
Run '/complete-task' to create and merge the PR first.
|
|
17
|
+
Use --force to close anyway.`)}function harvestAndReport(worktreePath,gitRoot,harvestAll,onHarvestFile){const harvestOpts={worktreePath,mainRepoPath:gitRoot,autoConfirm:harvestAll,captureParentState:!0};onHarvestFile&&(harvestOpts.onFile=onHarvestFile);const result=harvestFiles(harvestOpts);if(result.copied.length>0){process.stdout.write(`${t("cli.worktree.harvested",{count:result.copied.length})}
|
|
18
|
+
`);for(const f of result.copied)process.stdout.write(`${t("cli.worktree.harvest_copied",{file:f})}
|
|
19
|
+
`)}if(result.skipped.length>0){process.stdout.write(`${t("cli.worktree.skipped_count",{count:result.skipped.length})}
|
|
20
|
+
`);for(const f of result.skipped)process.stdout.write(`${t("cli.worktree.harvest_skipped",{file:f})}
|
|
21
|
+
`)}if(result.protectedUntracked.length>0){process.stdout.write(`Protected ${result.protectedUntracked.length} untracked file(s) in main repo from overwrite:
|
|
22
|
+
`);for(const f of result.protectedUntracked)process.stdout.write(`${t("cli.worktree.harvest_protected",{file:f})}
|
|
23
|
+
`)}return result.copied.length+result.skipped.length+result.protectedUntracked.length===0&&process.stdout.write(`${t("cli.worktree.no_harvest")}
|
|
24
|
+
`),result}function validateBeforeClose(params){const{worktreePath,branch,baseBranch,gitRoot,force,harvestAll,noFetch}=params;if(workingTreeDirty(worktreePath)&&!force&&!harvestAll)throw new Error(`Worktree has uncommitted or untracked changes at: ${worktreePath}
|
|
25
|
+
Untracked files also block close to protect never-added work. Commit or stash your changes, then retry. Use --force to close anyway.`);harvestAll?process.stderr.write(`Warning: harvest-all: skipping merge check; any un-merged commits on '${branch}' will be permanently lost.
|
|
26
|
+
`):assertBranchMerged(branch,baseBranch,gitRoot,noFetch,force)}function resolveOpenEntry(logPath,taskId){const openEntries=readJsonArray(logPath).filter(isOpenLogEntry),entry=openEntries.find(e=>e.taskId===taskId&&existsSync(e.worktreePath));if(!entry){const staleIdx=openEntries.findIndex(e=>e.taskId===taskId&&!existsSync(e.worktreePath)),staleEntry=staleIdx!==-1?openEntries[staleIdx]:void 0;throw staleEntry!==void 0&&(process.stderr.write(`Worktree directory missing \u2014 removing stale log entry for task ${taskId} (was: ${staleEntry.worktreePath})
|
|
27
|
+
`),openEntries.splice(staleIdx,1),writeJsonArray(logPath,openEntries)),new Error(`No open worktree found for task ${taskId}. Run 'arbiter worktree list' to see open worktrees.`)}return entry}function warnDanglingLinks(links,worktreePath,warn){for(const d of checkLinkIntegrity(links,worktreePath))warn(`Warning: dangling symlink: ${d}`)}export function runWorktreeClose(opts){const cwd=opts.cwd??process.cwd(),force=opts.force??!1,noFetch=opts.noFetch??!1,harvest=opts.harvest??!1,harvestAll=opts.harvestAll??!1,effectiveForce=force||harvestAll,warn=opts.onWarning??(msg=>{process.stdout.write(`${msg}
|
|
28
|
+
`)}),gitRoot=getGitRoot(cwd);if(!isRunningFromMainRepo(gitRoot))throw new Error("Must run 'worktree close' from the main repository, not a worktree.");const taskId=sanitizeTaskId(opts.taskId),logPath=join(arbiterLogDir(gitRoot),"worktree-open.log.json"),entry=resolveOpenEntry(logPath,taskId),{worktreePath,branch,baseBranch}=entry;let harvestResult=null;(harvest||harvestAll)&&(harvestResult=harvestAndReport(worktreePath,gitRoot,harvestAll,opts.onHarvestFile)),validateBeforeClose({worktreePath,branch,baseBranch,gitRoot,force,harvestAll,noFetch});const wtConfig=loadConfig(gitRoot)?.worktree??defaultWorktreeConfig();warnDanglingLinks(wtConfig.links,worktreePath,warn),runCloseHookIfConfigured(wtConfig.closeHook,worktreePath,gitRoot,effectiveForce,warn),runCli("git",["worktree","remove","--force",worktreePath],{cwd:gitRoot}),runCli("git",["worktree","prune"],{cwd:gitRoot}),opts.keepBranch||deleteTaskBranch(branch,gitRoot,effectiveForce)&&process.stdout.write(`${t("cli.worktree.branch_deleted",{branch})}
|
|
29
|
+
`),writeHarvestAuditIfNeeded(harvestResult,join(arbiterLogDir(gitRoot),"harvest-audit.log.json"),taskId,worktreePath),appendCloseLogEntry(join(arbiterLogDir(gitRoot),"worktree-close.log.json"),{taskId,branch,worktreePath,closedAt:new Date().toISOString(),force:effectiveForce}),emitCloseResult(opts.json,{worktreePath,branch,taskId})}export function runWorktreeRelink(opts){const cwd=opts.cwd??process.cwd(),warn=opts.onWarning??(msg=>{process.stdout.write(`${msg}
|
|
30
|
+
`)}),gitRoot=getGitRoot(cwd);if(!isRunningFromMainRepo(gitRoot))throw new Error("Must run 'worktree relink' from the main repository, not a worktree.");const taskId=sanitizeTaskId(opts.taskId),entry=resolveOpenEntry(join(arbiterLogDir(gitRoot),"worktree-open.log.json"),taskId),wtConfig=loadConfig(gitRoot)?.worktree??defaultWorktreeConfig(),linkSpecs=opts.withBuildLinks?[...wtConfig.links,...wtConfig.buildLinks??[]]:wtConfig.links,linkSummary=materializeLinks(linkSpecs,gitRoot,entry.worktreePath);opts.json?jsonOutput("worktree-relink","ok",{taskId,worktreePath:entry.worktreePath,linkSummary}):(process.stdout.write(`${t("cli.worktree.relinked",{path:entry.worktreePath})}
|
|
31
|
+
`),printLinkSummary(linkSummary)),warnDanglingLinks(linkSpecs,entry.worktreePath,warn)}function appendHarvestAuditEntry(logPath,entry){const entries=readJsonArray(logPath);entries.push(entry),writeJsonArray(logPath,entries)}function writeHarvestAuditIfNeeded(harvestResult,auditLogPath,taskId,worktreePath){harvestResult!==null&&appendHarvestAuditEntry(auditLogPath,{taskId,worktreePath,harvestedAt:new Date().toISOString(),copied:harvestResult.copied,skipped:harvestResult.skipped,protectedUntracked:harvestResult.protectedUntracked,parentBranchBefore:harvestResult.parentBranchBefore,parentUntrackedBefore:harvestResult.parentUntrackedBefore})}function appendCloseLogEntry(logPath,entry){const entries=readJsonArray(logPath).filter(isCloseLogEntry);entries.push(entry),writeJsonArray(logPath,entries)}function emitCloseResult(json,result){if(json){jsonOutput("worktree-close","ok",result);return}process.stdout.write(`${t("cli.worktree.closed",{path:result.worktreePath})}
|
|
32
|
+
`),process.stdout.write(`
|
|
33
|
+
`)}export function runWorktreeList(opts={}){const cwd=opts.cwd??process.cwd(),emit=opts.onLine??(line=>{process.stdout.write(`${line}
|
|
34
|
+
`)}),gitRoot=getGitRoot(cwd),result=runCli("git",["worktree","list","--porcelain"],{cwd:gitRoot}),worktrees=[];let currentPath,currentBranch=null;for(const line of result.stdout.split(`
|
|
35
|
+
`))line.startsWith("worktree ")?(currentPath!==void 0&&worktrees.push({path:currentPath,branch:currentBranch}),currentPath=line.slice(9),currentBranch=null):line.startsWith("branch ")&&(currentBranch=line.slice(7).replace("refs/heads/",""));currentPath!==void 0&&worktrees.push({path:currentPath,branch:currentBranch});const taskWorktrees=worktrees.slice(1).filter(w=>w.branch?.startsWith("task/"));if(opts.json){jsonOutput("worktree-list","ok",{worktrees:taskWorktrees});return}if(taskWorktrees.length===0){emit(`
|
|
36
|
+
No open task worktrees.
|
|
37
|
+
`);return}emit(`
|
|
38
|
+
Open task worktrees (${taskWorktrees.length}):
|
|
39
|
+
`);for(const wt of taskWorktrees)emit(` ${wt.branch??"(detached)"} ${wt.path}`);emit("")}
|