@webpresso/agent-kit 3.1.28 → 3.2.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.
Potentially problematic release.
This version of @webpresso/agent-kit might be problematic. Click here for more details.
- package/README.md +53 -15
- package/bin/_run.js +179 -19
- package/catalog/AGENTS.md.tpl +6 -6
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +30 -2
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +22 -2
- package/catalog/docs/templates/blueprint.md +1 -1
- package/dist/esm/audit/agents.js +1 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
- package/dist/esm/audit/blueprint-pr-coverage.js +72 -7
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +90 -0
- package/dist/esm/audit/ci-test-perf.js +30 -0
- package/dist/esm/audit/command-surface.js +4 -2
- package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
- package/dist/esm/audit/host-substitution-risk.js +256 -0
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +11 -1
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- package/dist/esm/audit/toolchain-isolation.js +1 -2
- package/dist/esm/audit/typescript-version.d.ts +2 -0
- package/dist/esm/audit/typescript-version.js +123 -0
- package/dist/esm/blueprint/core/parser.js +43 -1
- package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
- package/dist/esm/blueprint/core/validation/criteria.js +41 -2
- package/dist/esm/blueprint/core/validation/state.js +25 -14
- package/dist/esm/blueprint/db/migrations/run.js +4 -2
- package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
- package/dist/esm/blueprint/lifecycle/audit.js +65 -10
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
- package/dist/esm/blueprint/lifecycle/local.js +19 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +118 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
- package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
- package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
- package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
- package/dist/esm/blueprint/trust/command-runner.js +148 -0
- package/dist/esm/blueprint/trust/gates.d.ts +7 -0
- package/dist/esm/blueprint/trust/gates.js +20 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
- package/dist/esm/blueprint/trust/promotion.js +62 -112
- package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
- package/dist/esm/blueprint/trust/scaffold.js +64 -0
- package/dist/esm/blueprint/trust/validator.d.ts +1 -0
- package/dist/esm/blueprint/trust/validator.js +7 -1
- package/dist/esm/blueprint/utils/archive.js +27 -5
- package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
- package/dist/esm/blueprint/utils/document-paths.js +45 -12
- package/dist/esm/build/cli-mcp-parity.js +10 -0
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -0
- package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
- package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +34 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
- package/dist/esm/cli/commands/agent-launch.js +82 -4
- package/dist/esm/cli/commands/audit-core.d.ts +1 -1
- package/dist/esm/cli/commands/audit-core.js +8 -1
- package/dist/esm/cli/commands/audit.js +20 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +83 -47
- package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
- package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
- package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
- package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
- package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
- package/dist/esm/cli/commands/blueprint/router.js +40 -3
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +6 -3
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- package/dist/esm/cli/commands/compile.js +5 -31
- package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
- package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
- package/dist/esm/cli/commands/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/doctor.js +5 -0
- package/dist/esm/cli/commands/format.js +0 -2
- package/dist/esm/cli/commands/hook.d.ts +5 -0
- package/dist/esm/cli/commands/hook.js +15 -9
- package/dist/esm/cli/commands/init/config.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +3 -6
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +221 -0
- package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
- package/dist/esm/cli/commands/init/convergence-state.js +54 -0
- package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
- package/dist/esm/cli/commands/init/convergence.js +57 -0
- package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
- package/dist/esm/cli/commands/init/git-convergence.js +168 -0
- package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
- package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
- package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
- package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
- package/dist/esm/cli/commands/init/host-visibility.js +94 -5
- package/dist/esm/cli/commands/init/index.d.ts +1 -0
- package/dist/esm/cli/commands/init/index.js +326 -173
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
- package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
- package/dist/esm/cli/commands/init/merge.d.ts +1 -0
- package/dist/esm/cli/commands/init/merge.js +30 -1
- package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
- package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
- package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +11 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +51 -23
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +297 -173
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
- package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
- package/dist/esm/cli/commands/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
- package/dist/esm/cli/commands/opencode-rotate.js +135 -0
- package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
- package/dist/esm/cli/commands/public-readiness.js +42 -0
- package/dist/esm/cli/commands/quality-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +734 -217
- package/dist/esm/cli/commands/run-wait.d.ts +2 -0
- package/dist/esm/cli/commands/run-wait.js +122 -0
- package/dist/esm/cli/commands/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- package/dist/esm/cli/commands/setup-help.js +1 -1
- package/dist/esm/cli/commands/setup-repair-help.js +1 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.js +13 -8
- package/dist/esm/cli/direct-provider-launch.js +31 -3
- package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
- package/dist/esm/cli/optional-tool-freshness.js +219 -9
- package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
- package/dist/esm/daemon/domains/git-actions.js +136 -0
- package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
- package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
- package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
- package/dist/esm/daemon/domains/sessions-search.js +29 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
- package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
- package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
- package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
- package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/git-handlers.js +27 -0
- package/dist/esm/daemon/handlers/product.d.ts +7 -0
- package/dist/esm/daemon/handlers/product.js +10 -119
- package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
- package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
- package/dist/esm/daemon/handlers/shared.d.ts +5 -0
- package/dist/esm/daemon/handlers/shared.js +16 -0
- package/dist/esm/daemon/methods.d.ts +8 -1
- package/dist/esm/daemon/methods.js +7 -0
- package/dist/esm/daemon/protocol/contract.d.ts +43 -0
- package/dist/esm/daemon/protocol/contract.js +26 -0
- package/dist/esm/daemon/protocol/index.d.ts +1 -1
- package/dist/esm/daemon/protocol/index.js +1 -1
- package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
- package/dist/esm/hooks/__conformance__/matrix.js +137 -0
- package/dist/esm/hooks/conformance/batch.js +5 -0
- package/dist/esm/hooks/doctor.d.ts +15 -0
- package/dist/esm/hooks/doctor.js +79 -8
- package/dist/esm/hooks/permission-request/index.js +9 -3
- package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
- package/dist/esm/hooks/permission-request/policy.js +13 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
- package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/logger.js +16 -0
- package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
- package/dist/esm/hooks/pretool-guard/runner.js +68 -16
- package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +58 -4
- package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
- package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
- package/dist/esm/hooks/shared/guard-outcome.js +215 -0
- package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
- package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
- package/dist/esm/hooks/shared/types.d.ts +8 -5
- package/dist/esm/hooks/shared/types.js +23 -8
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
- package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
- package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
- package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
- package/dist/esm/mcp/blueprint/registration.js +3 -2
- package/dist/esm/mcp/server.d.ts +1 -0
- package/dist/esm/mcp/server.js +27 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +5 -0
- package/dist/esm/mcp/tools/_registry.js +10 -0
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/_shared/result.js +44 -1
- package/dist/esm/mcp/tools/audit.d.ts +5 -1
- package/dist/esm/mcp/tools/audit.js +40 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
- package/dist/esm/mcp/tools/ci-preflight.js +183 -0
- package/dist/esm/mcp/tools/pr-upsert.js +39 -1
- package/dist/esm/mcp/tools/review-run.d.ts +28 -0
- package/dist/esm/mcp/tools/review-run.js +216 -0
- package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
- package/dist/esm/mcp/tools/run-wait.js +593 -0
- package/dist/esm/mcp/tools/session-id.d.ts +3 -0
- package/dist/esm/mcp/tools/session-id.js +45 -0
- package/dist/esm/mcp/tools/session-info.d.ts +3 -0
- package/dist/esm/mcp/tools/session-info.js +49 -0
- package/dist/esm/output-transforms/audit.d.ts +23 -0
- package/dist/esm/output-transforms/audit.js +47 -0
- package/dist/esm/output-transforms/index.js +2 -0
- package/dist/esm/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +30 -7
- package/dist/esm/review/availability.d.ts +22 -0
- package/dist/esm/review/availability.js +123 -23
- package/dist/esm/review/delivery-verifier.d.ts +27 -0
- package/dist/esm/review/delivery-verifier.js +115 -0
- package/dist/esm/review/events.d.ts +3 -2
- package/dist/esm/review/events.js +23 -3
- package/dist/esm/review/execution/adapters.js +26 -9
- package/dist/esm/review/execution/output-policy.d.ts +2 -0
- package/dist/esm/review/execution/output-policy.js +12 -0
- package/dist/esm/review/execution/review-checkout.js +87 -6
- package/dist/esm/review/execution/supervisor.js +5 -0
- package/dist/esm/review/execution/types.d.ts +8 -0
- package/dist/esm/review/opencode-account.d.ts +24 -0
- package/dist/esm/review/opencode-account.js +68 -0
- package/dist/esm/review/opencode-auth.d.ts +33 -0
- package/dist/esm/review/opencode-auth.js +79 -0
- package/dist/esm/review/subject.d.ts +27 -0
- package/dist/esm/review/subject.js +179 -32
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/status/snapshot.js +30 -19
- package/dist/esm/test/duration-sequencer.d.ts +20 -0
- package/dist/esm/test/duration-sequencer.js +54 -0
- package/dist/esm/test/shard-durations.json +922 -0
- package/dist/esm/test/worker-budget.d.ts +6 -0
- package/dist/esm/test/worker-budget.js +59 -1
- package/dist/esm/test-helpers/global-setup.d.ts +1 -1
- package/dist/esm/test-helpers/global-setup.js +68 -24
- package/dist/esm/ultragoal/service.d.ts +7 -0
- package/dist/esm/ultragoal/service.js +42 -2
- package/dist/esm/utils/write-json-file.js +3 -1
- package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
- package/dist/esm/worktrees/codex-project-trust.js +56 -0
- package/dist/esm/worktrees/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -15
|
@@ -7,19 +7,26 @@ import { z } from "zod";
|
|
|
7
7
|
import { MAX_PORTABLE_REPOSITORY_PATH_LENGTH } from "#audit/absolute-path-policy.js";
|
|
8
8
|
import { getProjectRoot } from "#cli/utils";
|
|
9
9
|
import { relativeBlueprintSlug, resolveBlueprintFile } from "#lifecycle/local.js";
|
|
10
|
+
import { readBlueprintOverviewAtRef } from "#lifecycle/review-provenance.js";
|
|
10
11
|
import { scanBlueprintDirectory } from "#service/scanner.js";
|
|
11
12
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
13
|
+
import { assertFolderBlueprintIdentity } from "#utils/document-paths.js";
|
|
12
14
|
import { CLAUDE_DEFAULT_REVIEW_MODEL, CODEX_DEFAULT_REVIEW_MODEL, GROK_DEFAULT_REVIEW_MODEL, DEFAULT_REVIEW_MAX_PROGRESS_EVENTS, runReviewExecution, } from "#review/execution/index.js";
|
|
13
15
|
import { REVIEW_OUTPUT_MAX_BYTES, REVIEW_OUTPUT_TOO_LARGE, applyReviewOutputPolicy, } from "#review/execution/output-policy.js";
|
|
14
16
|
import { removeOwnedRunningReviewState } from "#review/execution/artifacts.js";
|
|
15
17
|
import { createReviewTargetCheckout, } from "#review/execution/review-checkout.js";
|
|
16
18
|
import { loadReviewProcessRecords, readReviewProcessIdentity, removeReviewProcessRecord, } from "#review/execution/process-registry.js";
|
|
17
|
-
import { isProviderSuppressed, loadReviewAvailability, pruneExpiredAvailabilitySuppressions, recordReviewAvailability, recordReviewAvailabilityWithLock, reviewAvailabilityPath, saveReviewAvailability, suppressedReviewers, } from "#review/availability.js";
|
|
19
|
+
import { isProviderSuppressed, loadOpencodeAccountAvailability, loadReviewAvailability, pruneExpiredAvailabilitySuppressions, pruneExpiredOpencodeAccountSuppressions, recordOpencodeAccountAvailabilityWithLock, recordReviewAvailability, recordReviewAvailabilityWithLock, reviewAvailabilityPath, saveReviewAvailability, suppressedReviewers, } from "#review/availability.js";
|
|
20
|
+
import { opencodeAccountCanonicalId, opencodeAccountDataDir, selectActiveOpencodeAccount, } from "#review/opencode-account.js";
|
|
21
|
+
import { materializeOpencodeGoAuth } from "#review/opencode-auth.js";
|
|
22
|
+
import { resolveOpencodeAccountsGracefully } from "#runtime/opencode-account-materializer.js";
|
|
23
|
+
import { resolveAgentSecretProfile } from "#cli/commands/agent-launch.js";
|
|
18
24
|
import { evaluateApprovalGate, evaluateApprovalReviewers, formatApprovalGateRequirement, } from "#lifecycle/audit";
|
|
19
25
|
import { assertReviewRefUnchanged, readReviewAuthorityAtRef, readWorkingReviewEvents, } from "#review/authority.js";
|
|
20
|
-
import { appendReviewEvent, evaluateReviewEvents } from "#review/events.js";
|
|
26
|
+
import { appendReviewEvent, evaluateReviewEvents, MAINTAINER_OVERRIDE_REVIEWER, parseReviewEventLog, } from "#review/events.js";
|
|
21
27
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
22
|
-
import {
|
|
28
|
+
import { verificationArtifactSection, verificationPromptSummary, verifyDeliveryPromotionGates, } from "#review/delivery-verifier.js";
|
|
29
|
+
import { createDeliverySubjectAtRef, createPlanSubjectAtRef, findUnreviewedWorkingTreeChanges, } from "#review/subject.js";
|
|
23
30
|
import { runCommand } from "#mcp/tools/_shared/run-command.js";
|
|
24
31
|
import { NotInGitRepoError, withLock } from "#paths/state-root.js";
|
|
25
32
|
import { writeFileAtomic, writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
|
|
@@ -148,6 +155,18 @@ function defaultReviewModel(provider) {
|
|
|
148
155
|
return GROK_DEFAULT_REVIEW_MODEL;
|
|
149
156
|
return undefined;
|
|
150
157
|
}
|
|
158
|
+
/**
|
|
159
|
+
* Claude's CLI accepts `fable` as its moving alias and `claude-fable-N` as a
|
|
160
|
+
* versioned identifier. Accept the common human shorthand `fableN` too, so a
|
|
161
|
+
* request for a specific Fable generation is never forwarded as an invalid
|
|
162
|
+
* provider model id.
|
|
163
|
+
*/
|
|
164
|
+
function resolveReviewModel(provider, model) {
|
|
165
|
+
if (provider !== "claude")
|
|
166
|
+
return model;
|
|
167
|
+
const fableVersion = /^fable[-\s]?(\d+)$/iu.exec(model)?.[1];
|
|
168
|
+
return fableVersion ? `claude-fable-${fableVersion}` : model;
|
|
169
|
+
}
|
|
151
170
|
function classificationForFailureCode(code) {
|
|
152
171
|
if (!code)
|
|
153
172
|
return undefined;
|
|
@@ -188,9 +207,10 @@ export async function runReviewRunCommand(projectRoot, options, deps = {}) {
|
|
|
188
207
|
if (!options.artifactRoot)
|
|
189
208
|
throw new Error("Missing required --artifact-root.");
|
|
190
209
|
const provider = parseReviewProvider(options.provider);
|
|
191
|
-
const
|
|
192
|
-
if (!
|
|
210
|
+
const requestedModel = options.model?.trim() || defaultReviewModel(provider);
|
|
211
|
+
if (!requestedModel)
|
|
193
212
|
throw new Error(`Missing required --model for ${provider}.`);
|
|
213
|
+
const model = resolveReviewModel(provider, requestedModel);
|
|
194
214
|
const effort = parseReviewEffort(options.effort);
|
|
195
215
|
const stage = parseReviewStage(options.stage);
|
|
196
216
|
const idleSeconds = options.idleSeconds === undefined ? 180 : Number(options.idleSeconds);
|
|
@@ -353,6 +373,13 @@ function reviewsPathForBlueprint(blueprintPath) {
|
|
|
353
373
|
}
|
|
354
374
|
return path.join(path.dirname(blueprintPath), "reviews.md");
|
|
355
375
|
}
|
|
376
|
+
function assertReviewEvidenceCapable(location) {
|
|
377
|
+
assertFolderBlueprintIdentity({
|
|
378
|
+
documentPath: location.path,
|
|
379
|
+
lifecycleSlug: location.slug,
|
|
380
|
+
shape: location.shape,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
356
383
|
function parseLegacyTableEntries(markdown, blueprintSlug, blueprintPath, reviewsPath) {
|
|
357
384
|
const entries = [];
|
|
358
385
|
for (const line of markdown.split("\n")) {
|
|
@@ -687,6 +714,7 @@ async function resolveReviewBlueprint(projectRoot, slug) {
|
|
|
687
714
|
}
|
|
688
715
|
export async function logReviewEntry(projectRoot, slug, input) {
|
|
689
716
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
717
|
+
assertReviewEvidenceCapable(location);
|
|
690
718
|
const reviewsPath = reviewsPathForBlueprint(location.path);
|
|
691
719
|
const timestamp = new Date().toISOString();
|
|
692
720
|
const reviewer = parseReviewerId(input.reviewer);
|
|
@@ -748,6 +776,7 @@ export async function logReviewEntry(projectRoot, slug, input) {
|
|
|
748
776
|
}
|
|
749
777
|
export async function logReviewEvent(projectRoot, slug, input) {
|
|
750
778
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
779
|
+
assertReviewEvidenceCapable(location);
|
|
751
780
|
const stableSlug = relativeBlueprintSlug(location.slug);
|
|
752
781
|
const purpose = normalizeReviewPurpose(input.purpose);
|
|
753
782
|
const targetSha = resolveImmutableSha(projectRoot, input.target);
|
|
@@ -809,6 +838,127 @@ export async function logReviewEvent(projectRoot, slug, input) {
|
|
|
809
838
|
updateReviewCache(projectRoot, scanAllReviewLedgers(projectRoot));
|
|
810
839
|
return event;
|
|
811
840
|
}
|
|
841
|
+
function renderMaintainerOverrideArtifact(input) {
|
|
842
|
+
return [
|
|
843
|
+
`# Maintainer override — ${input.blueprintSlug} (${input.purpose})`,
|
|
844
|
+
"",
|
|
845
|
+
"A maintainer has adjudicated this subject after a formal Claude/Codex/Grok/OpenCode-Go",
|
|
846
|
+
"reviewer rejected it and the blocking finding cannot be remediated (a physical or",
|
|
847
|
+
"maximalist residual). This override satisfies the promotion approval gate DISTINCTLY;",
|
|
848
|
+
"it is never counted as a strong-reviewer or OpenCode-Go approval.",
|
|
849
|
+
"",
|
|
850
|
+
`- Subject scheme: ${input.subjectScheme}`,
|
|
851
|
+
`- Subject digest: ${input.subjectDigest}`,
|
|
852
|
+
`- Reviewed commit: ${input.reviewedCommit}`,
|
|
853
|
+
`- Overridden rejection artifact: ${input.rejectionArtifactPath}`,
|
|
854
|
+
`- Recorded: ${input.timestamp}`,
|
|
855
|
+
"",
|
|
856
|
+
"## Maintainer reason",
|
|
857
|
+
"",
|
|
858
|
+
input.reason,
|
|
859
|
+
"",
|
|
860
|
+
].join("\n");
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* `wp review override` — the gated, auditable human escape hatch for the
|
|
864
|
+
* promotion approval gate. It records a `maintainer-override` approval that
|
|
865
|
+
* satisfies the gate distinctly (never as a strong/OpenCode-Go approval).
|
|
866
|
+
*
|
|
867
|
+
* Trust properties:
|
|
868
|
+
* - REFUSES unless a formal strong-reviewer gate has already REJECTED the exact
|
|
869
|
+
* current subject (precondition + target binding are the same lookup).
|
|
870
|
+
* - Requires an explicit `--reason` and `--confirm` — no accidental invocation.
|
|
871
|
+
* - Reviewer id `maintainer-override` is not in the `wp review log` reviewer
|
|
872
|
+
* enum, so neither `wp review log` nor `wp_blueprint_review_log` can mint it;
|
|
873
|
+
* this command is the only writer. The read-time policy in
|
|
874
|
+
* `#review/events` treats it as human-like, so its manual-stage record is a
|
|
875
|
+
* legitimate governance approval rather than a disguised machine approval.
|
|
876
|
+
*/
|
|
877
|
+
export async function runReviewOverride(projectRoot, slug, input) {
|
|
878
|
+
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
879
|
+
assertReviewEvidenceCapable(location);
|
|
880
|
+
if (lstatSync(location.path).isSymbolicLink()) {
|
|
881
|
+
throw new Error(`Blueprint overview must not be a symlink: ${location.path}`);
|
|
882
|
+
}
|
|
883
|
+
const reason = normalizeOptionalText(input.reason);
|
|
884
|
+
if (!reason) {
|
|
885
|
+
throw new Error("Missing required --reason for `review override`: a maintainer adjudication must state why an automated reviewer cannot converge.");
|
|
886
|
+
}
|
|
887
|
+
if (input.confirm !== true) {
|
|
888
|
+
throw new Error("`review override` requires explicit --confirm: it mints a promotion-gate-satisfying human adjudication that bypasses the strong-reviewer gate.");
|
|
889
|
+
}
|
|
890
|
+
const stableSlug = relativeBlueprintSlug(location.slug);
|
|
891
|
+
const purpose = normalizeReviewPurpose(input.purpose);
|
|
892
|
+
const targetSha = resolveImmutableSha(projectRoot, input.target);
|
|
893
|
+
const repositoryBlueprintPath = blueprintPathAtGitRoot(projectRoot, location.path);
|
|
894
|
+
const subject = purpose === "plan"
|
|
895
|
+
? createPlanSubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath)
|
|
896
|
+
: createDeliverySubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath);
|
|
897
|
+
const ledgerPath = path.join(path.dirname(location.path), "review-events.jsonl");
|
|
898
|
+
const events = existsSync(ledgerPath)
|
|
899
|
+
? parseReviewEventLog(readFileSync(ledgerPath, "utf8"))
|
|
900
|
+
: [];
|
|
901
|
+
// Precondition AND target binding in one lookup: the override is valid only
|
|
902
|
+
// when a formal strong-reviewer gate REJECTED the exact current subject. This
|
|
903
|
+
// refuses overriding content that was never properly reviewed, and refuses a
|
|
904
|
+
// stale rejection of content that has since changed.
|
|
905
|
+
const rejection = rejectedReviewSubjects({
|
|
906
|
+
events,
|
|
907
|
+
projectRoot,
|
|
908
|
+
purpose,
|
|
909
|
+
stableSlug,
|
|
910
|
+
}).find((entry) => entry.subjectDigest === subject.digest);
|
|
911
|
+
if (!rejection) {
|
|
912
|
+
throw new Error(`Refusing to record a maintainer override: no formal Claude/Codex/Grok/OpenCode-Go rejection exists for the current ${purpose} subject (${subject.digest.slice(0, 12)}) of ${stableSlug}. A maintainer override is only valid after a strong reviewer has rejected the exact content under review.`);
|
|
913
|
+
}
|
|
914
|
+
const timestamp = new Date().toISOString();
|
|
915
|
+
const artifactSuffix = timestamp.replace(/[^0-9]/gu, "").slice(0, 14);
|
|
916
|
+
const artifactPath = `review-artifacts/maintainer-override-${subject.digest.slice(0, 12)}-${artifactSuffix}.md`;
|
|
917
|
+
const artifactAbsolute = safeArtifactPath(location.path, artifactPath);
|
|
918
|
+
const artifactContent = Buffer.from(renderMaintainerOverrideArtifact({
|
|
919
|
+
blueprintSlug: stableSlug,
|
|
920
|
+
purpose,
|
|
921
|
+
subjectScheme: subject.scheme,
|
|
922
|
+
subjectDigest: subject.digest,
|
|
923
|
+
reviewedCommit: targetSha,
|
|
924
|
+
rejectionArtifactPath: rejection.artifactPath,
|
|
925
|
+
reason,
|
|
926
|
+
timestamp,
|
|
927
|
+
}), "utf8");
|
|
928
|
+
const event = {
|
|
929
|
+
schemaVersion: 2,
|
|
930
|
+
// The timestamp keeps the id unique across re-invocations. Re-running the
|
|
931
|
+
// override for the same subject therefore appends a second approve event
|
|
932
|
+
// rather than being idempotent; that is harmless (each is gated by the same
|
|
933
|
+
// real rejection, and evaluateReviewEvents keeps the latest per reviewer)
|
|
934
|
+
// and avoids a same-id/different-timestamp collision in appendReviewEvent.
|
|
935
|
+
id: `${purpose}:maintainer-override:${subject.digest}:${timestamp}`,
|
|
936
|
+
purpose,
|
|
937
|
+
blueprintSlug: stableSlug,
|
|
938
|
+
subjectScheme: subject.scheme,
|
|
939
|
+
subjectDigest: subject.digest,
|
|
940
|
+
reviewedCommit: targetSha,
|
|
941
|
+
gateId: "manual",
|
|
942
|
+
promptHash: createHash("sha256")
|
|
943
|
+
.update(`maintainer-override\0${purpose}\0${subject.scheme}\0${subject.digest}`)
|
|
944
|
+
.digest("hex"),
|
|
945
|
+
reviewer: MAINTAINER_OVERRIDE_REVIEWER,
|
|
946
|
+
// No `provider`: a maintainer override has no AI backend.
|
|
947
|
+
stage: "manual",
|
|
948
|
+
status: "complete",
|
|
949
|
+
verdict: "approve",
|
|
950
|
+
timestamp,
|
|
951
|
+
artifactPath,
|
|
952
|
+
artifactSha256: createHash("sha256").update(artifactContent).digest("hex"),
|
|
953
|
+
};
|
|
954
|
+
appendReviewEvent({
|
|
955
|
+
ledgerPath,
|
|
956
|
+
event,
|
|
957
|
+
artifact: { path: artifactAbsolute, content: artifactContent },
|
|
958
|
+
});
|
|
959
|
+
updateReviewCache(projectRoot, scanAllReviewLedgers(projectRoot));
|
|
960
|
+
return event;
|
|
961
|
+
}
|
|
812
962
|
function resolveImmutableSha(projectRoot, target) {
|
|
813
963
|
let resolved;
|
|
814
964
|
try {
|
|
@@ -860,14 +1010,10 @@ function readBlueprintAtTarget(projectRoot, blueprintPath, targetSha) {
|
|
|
860
1010
|
throw new Error(`Blueprint path is outside Git toplevel: ${blueprintPath}`);
|
|
861
1011
|
}
|
|
862
1012
|
const relative = gitRelativePath.replace(/\\/gu, "/");
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
throw new Error(`Blueprint ${relative} is not tracked at target ${targetSha}.`, {
|
|
868
|
-
cause: error,
|
|
869
|
-
});
|
|
870
|
-
}
|
|
1013
|
+
const content = readBlueprintOverviewAtRef(gitRoot, targetSha, absoluteBlueprintPath);
|
|
1014
|
+
if (content)
|
|
1015
|
+
return content.toString("utf8");
|
|
1016
|
+
throw new Error(`Blueprint ${relative} is not tracked at target ${targetSha}.`);
|
|
871
1017
|
}
|
|
872
1018
|
function blueprintPathAtGitRoot(projectRoot, blueprintPath) {
|
|
873
1019
|
const gitRoot = resolveReviewGitToplevel(projectRoot);
|
|
@@ -949,8 +1095,15 @@ function dedupeGateAttempts(attempts) {
|
|
|
949
1095
|
return result;
|
|
950
1096
|
}
|
|
951
1097
|
function gateAttemptFromEvent(event) {
|
|
1098
|
+
// Only real gate events reach here (callers filter by the content-hash gateId
|
|
1099
|
+
// + promptHash); those always carry a provider. A `provider`-less event (e.g.
|
|
1100
|
+
// a maintainer override) is a caller bug — fail loudly rather than fabricate.
|
|
1101
|
+
const provider = event.provider;
|
|
1102
|
+
if (!provider) {
|
|
1103
|
+
throw new Error(`Gate attempt event ${event.id} is missing a provider.`);
|
|
1104
|
+
}
|
|
952
1105
|
return {
|
|
953
|
-
provider
|
|
1106
|
+
provider,
|
|
954
1107
|
...(event.model ? { model: event.model } : {}),
|
|
955
1108
|
...(event.effort ? { effort: event.effort } : {}),
|
|
956
1109
|
reviewer: event.reviewer,
|
|
@@ -1002,6 +1155,19 @@ const reviewGateAttemptSchema = z.object({
|
|
|
1002
1155
|
retryAfterMs: z.number().optional(),
|
|
1003
1156
|
artifact: z.string().optional(),
|
|
1004
1157
|
});
|
|
1158
|
+
const reviewGateVerificationCommandSchema = z.object({
|
|
1159
|
+
command: z.string(),
|
|
1160
|
+
outcome: z.enum(["passed", "failed", "timed-out"]),
|
|
1161
|
+
durationMs: z.number(),
|
|
1162
|
+
exitCode: z.number().nullable(),
|
|
1163
|
+
signal: z.string().optional(),
|
|
1164
|
+
});
|
|
1165
|
+
const reviewGateVerificationSchema = z.object({
|
|
1166
|
+
status: z.enum(["passed", "failed"]),
|
|
1167
|
+
commands: z.array(reviewGateVerificationCommandSchema),
|
|
1168
|
+
failureCode: z.string().optional(),
|
|
1169
|
+
logPath: z.string().optional(),
|
|
1170
|
+
});
|
|
1005
1171
|
const reviewGateStateSchema = z
|
|
1006
1172
|
.object({
|
|
1007
1173
|
schemaVersion: z.literal(2),
|
|
@@ -1014,10 +1180,18 @@ const reviewGateStateSchema = z
|
|
|
1014
1180
|
targetSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u),
|
|
1015
1181
|
baseSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u),
|
|
1016
1182
|
promptHash: z.string().length(64),
|
|
1017
|
-
status: z.enum([
|
|
1183
|
+
status: z.enum([
|
|
1184
|
+
"approved",
|
|
1185
|
+
"needs-review",
|
|
1186
|
+
"rejected",
|
|
1187
|
+
"scope-exhausted",
|
|
1188
|
+
"recorded-untracked",
|
|
1189
|
+
"verification-failed",
|
|
1190
|
+
]),
|
|
1018
1191
|
approvalGate: z.object({
|
|
1019
1192
|
distinct: z.number(),
|
|
1020
1193
|
openCodeGoModelReviewers: z.array(z.string()),
|
|
1194
|
+
overrideReviewers: z.array(z.string()),
|
|
1021
1195
|
satisfied: z.boolean(),
|
|
1022
1196
|
strongReviewers: z.array(z.string()),
|
|
1023
1197
|
}),
|
|
@@ -1039,6 +1213,7 @@ const reviewGateStateSchema = z
|
|
|
1039
1213
|
// Optional so resuming a gate state persisted before issue tracking existed
|
|
1040
1214
|
// still validates under .strict().
|
|
1041
1215
|
issues: z.array(z.string()).optional(),
|
|
1216
|
+
verification: reviewGateVerificationSchema.optional(),
|
|
1042
1217
|
message: z.string(),
|
|
1043
1218
|
})
|
|
1044
1219
|
.strict();
|
|
@@ -1080,6 +1255,16 @@ function buildGatePrompt(input) {
|
|
|
1080
1255
|
`Review purpose: ${input.purpose}`,
|
|
1081
1256
|
`Reviewed content subject: ${input.subject.scheme}:${input.subject.digest}`,
|
|
1082
1257
|
`Immutable target SHA: ${input.targetSha}`,
|
|
1258
|
+
...(input.verificationSummary ? ["", input.verificationSummary, ""] : []),
|
|
1259
|
+
...(input.priorRejections && input.priorRejections.length > 0
|
|
1260
|
+
? [
|
|
1261
|
+
"",
|
|
1262
|
+
"Prior formal rejection evidence (this is the final semantic remediation review):",
|
|
1263
|
+
...input.priorRejections.map((entry) => `- ${entry.subjectDigest}: ${entry.artifactPath}`),
|
|
1264
|
+
"Inspect every prior finding and its disposition. Return all remaining blockers together; do not defer discoverable blockers to another review round.",
|
|
1265
|
+
"",
|
|
1266
|
+
]
|
|
1267
|
+
: []),
|
|
1083
1268
|
"Inspect the target SHA and blueprint directly. Focus on correctness, security, concurrency, recovery, tests, and scope compliance.",
|
|
1084
1269
|
"End with exactly one standalone verdict line: APPROVED, APPROVE-WITH-NITS, or BLOCKED.",
|
|
1085
1270
|
"APPROVE-WITH-NITS satisfies the gate only when every finding is non-blocking.",
|
|
@@ -1171,7 +1356,10 @@ function chooseOpenCodeModels(catalog, explicitModel) {
|
|
|
1171
1356
|
return selected;
|
|
1172
1357
|
}
|
|
1173
1358
|
function isProviderWideOpenCodeGateResult(result) {
|
|
1174
|
-
|
|
1359
|
+
// A per-account usage limit (failureCode "opencode-go-usage-limit") is NO
|
|
1360
|
+
// LONGER provider-wide — the gate fails over to the next account first. Only a
|
|
1361
|
+
// systemic opencode failure (the family-agnostic "opencode" reviewer) is.
|
|
1362
|
+
return result.reviewer === "opencode";
|
|
1175
1363
|
}
|
|
1176
1364
|
function contaminatedReviewResult(result, changes) {
|
|
1177
1365
|
return {
|
|
@@ -1229,12 +1417,18 @@ function gateFailureFromSummary(summary) {
|
|
|
1229
1417
|
verdict: "no-verdict",
|
|
1230
1418
|
terminalClassification,
|
|
1231
1419
|
...(summary.failureCode ? { failureCode: summary.failureCode } : {}),
|
|
1420
|
+
...(summary.providerErrorDetail ? { providerErrorDetail: summary.providerErrorDetail } : {}),
|
|
1232
1421
|
...(summary.limitWindow ? { limitWindow: summary.limitWindow } : {}),
|
|
1233
1422
|
...(summary.retryAfterMs === undefined ? {} : { retryAfterMs: summary.retryAfterMs }),
|
|
1234
1423
|
};
|
|
1235
1424
|
}
|
|
1236
|
-
export async function invokeProvider(input, executeReview = runReviewExecution, runCatalogCommand = runCommand, createTargetCheckout = createReviewTargetCheckout,
|
|
1425
|
+
export async function invokeProvider(input, executeReview = runReviewExecution, runCatalogCommand = runCommand, createTargetCheckout = createReviewTargetCheckout,
|
|
1426
|
+
// A thunk (not a checkout instance) so callers that never reach a real
|
|
1427
|
+
// provider attempt — injected fakes included — never pay the checkout's
|
|
1428
|
+
// git-archive/tar setup cost.
|
|
1429
|
+
getSharedCheckout) {
|
|
1237
1430
|
let checkout;
|
|
1431
|
+
const existingCheckout = getSharedCheckout?.();
|
|
1238
1432
|
const ownsCheckout = existingCheckout === undefined;
|
|
1239
1433
|
try {
|
|
1240
1434
|
checkout = existingCheckout ?? createTargetCheckout(input.projectRoot, input.targetSha);
|
|
@@ -1249,7 +1443,8 @@ export async function invokeProvider(input, executeReview = runReviewExecution,
|
|
|
1249
1443
|
// The attempt already fails closed as an isolation failure.
|
|
1250
1444
|
}
|
|
1251
1445
|
}
|
|
1252
|
-
const
|
|
1446
|
+
const requestedModel = input.model ?? defaultReviewModel(input.provider) ?? "unresolved";
|
|
1447
|
+
const model = resolveReviewModel(input.provider, requestedModel);
|
|
1253
1448
|
return {
|
|
1254
1449
|
output: "Unable to create the disposable review checkout; review was not started.",
|
|
1255
1450
|
model,
|
|
@@ -1283,6 +1478,7 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1283
1478
|
cwd: input.executionCwd,
|
|
1284
1479
|
registryRoot: input.projectRoot,
|
|
1285
1480
|
...(input.signal ? { signal: input.signal } : {}),
|
|
1481
|
+
...(input.authDataDir ? { authDataDir: input.authDataDir } : {}),
|
|
1286
1482
|
};
|
|
1287
1483
|
async function probeProvider(args) {
|
|
1288
1484
|
const probe = await executeReview({
|
|
@@ -1305,7 +1501,7 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1305
1501
|
};
|
|
1306
1502
|
}
|
|
1307
1503
|
if (input.provider === "claude") {
|
|
1308
|
-
const model = input.model ?? defaultReviewModel("claude");
|
|
1504
|
+
const model = resolveReviewModel("claude", input.model ?? defaultReviewModel("claude"));
|
|
1309
1505
|
const probeFailure = await probeProvider({ provider: "claude", model, reviewer: "claude" });
|
|
1310
1506
|
if (probeFailure)
|
|
1311
1507
|
return probeFailure;
|
|
@@ -1445,6 +1641,101 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1445
1641
|
: gateFailureFromSummary(result)),
|
|
1446
1642
|
};
|
|
1447
1643
|
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Default credential preparer: materialize the account's key into its own
|
|
1646
|
+
* isolated `<stateRoot>/opencode-accounts/<id>/opencode/auth.json` (0600) and
|
|
1647
|
+
* return the `XDG_DATA_HOME` directory. Never mutates the user's global store.
|
|
1648
|
+
*/
|
|
1649
|
+
function prepareOpencodeCredentialDefault(account) {
|
|
1650
|
+
const dataDir = opencodeAccountDataDir(account.id);
|
|
1651
|
+
materializeOpencodeGoAuth(account.key, { dataDir });
|
|
1652
|
+
return dataDir;
|
|
1653
|
+
}
|
|
1654
|
+
/**
|
|
1655
|
+
* Multi-account failover for the opencode-go gateway. Resolves Doppler accounts
|
|
1656
|
+
* once, materializes the freshest non-cooled account's isolated auth store, and
|
|
1657
|
+
* rotates to the next account on a per-account usage limit — never touching the
|
|
1658
|
+
* user's global `auth.json`. Degrades to a no-op (undefined `authDataDir`, the
|
|
1659
|
+
* historical global-auth behavior) when no accounts are provisioned.
|
|
1660
|
+
*/
|
|
1661
|
+
function createOpencodeFailoverController(deps) {
|
|
1662
|
+
// Resolve through the same agent secret profile the launch path uses; without
|
|
1663
|
+
// an environment selector the secret manager falls back to its own ambient,
|
|
1664
|
+
// path-scoped config, which only resolves inside a checkout that happens to
|
|
1665
|
+
// sit under that scope (so account discovery silently finds nothing in a
|
|
1666
|
+
// managed worktree, CI clone, or fresh checkout).
|
|
1667
|
+
const resolution = deps.resolveAccounts({
|
|
1668
|
+
cwd: deps.projectRoot,
|
|
1669
|
+
secretProfile: resolveAgentSecretProfile(),
|
|
1670
|
+
});
|
|
1671
|
+
const accounts = resolution.ok ? resolution.accounts : [];
|
|
1672
|
+
const tried = new Set();
|
|
1673
|
+
let active;
|
|
1674
|
+
let authDataDir;
|
|
1675
|
+
function prepare(account) {
|
|
1676
|
+
try {
|
|
1677
|
+
authDataDir = deps.prepareCredential(account);
|
|
1678
|
+
active = account;
|
|
1679
|
+
tried.add(account.id);
|
|
1680
|
+
return true;
|
|
1681
|
+
}
|
|
1682
|
+
catch {
|
|
1683
|
+
return false;
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
/**
|
|
1687
|
+
* @param allowCooled Accept the soonest-expiring account when every account is
|
|
1688
|
+
* already cooled. True for the initial pick (mirrors `wp opencode`: a known
|
|
1689
|
+
* account beats falling back to whatever the global auth store happens to
|
|
1690
|
+
* hold); false when rotating after a usage limit, where retrying a capped
|
|
1691
|
+
* account would only burn an attempt.
|
|
1692
|
+
*/
|
|
1693
|
+
function selectNext(allowCooled) {
|
|
1694
|
+
const state = pruneExpiredOpencodeAccountSuppressions(loadOpencodeAccountAvailability()).state;
|
|
1695
|
+
const selection = selectActiveOpencodeAccount(accounts, state);
|
|
1696
|
+
if (!selection || tried.has(selection.account.id))
|
|
1697
|
+
return undefined;
|
|
1698
|
+
if (selection.cooled && !allowCooled)
|
|
1699
|
+
return undefined;
|
|
1700
|
+
return selection.account;
|
|
1701
|
+
}
|
|
1702
|
+
if (accounts.length > 0) {
|
|
1703
|
+
const first = selectNext(true);
|
|
1704
|
+
if (first)
|
|
1705
|
+
prepare(first);
|
|
1706
|
+
}
|
|
1707
|
+
return {
|
|
1708
|
+
authDataDir: () => authDataDir,
|
|
1709
|
+
async onUsageLimit(event) {
|
|
1710
|
+
if (active) {
|
|
1711
|
+
await recordOpencodeAccountAvailabilityWithLock(opencodeAccountCanonicalId(active.id), {
|
|
1712
|
+
failureCode: "opencode-go-usage-limit",
|
|
1713
|
+
terminalClassification: "provider-limit-reached",
|
|
1714
|
+
...(event.limitWindow ? { limitWindow: event.limitWindow } : {}),
|
|
1715
|
+
...(event.retryAfterMs === undefined ? {} : { retryAfterMs: event.retryAfterMs }),
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
const next = selectNext(false);
|
|
1719
|
+
if (next && prepare(next))
|
|
1720
|
+
return "retry";
|
|
1721
|
+
// No account left: suppress the opencode PROVIDER (the per-attempt record
|
|
1722
|
+
// is family-scoped and does not) so the gate moves to the next provider.
|
|
1723
|
+
// Sync (unlocked) write — the gate already holds the worktree lock, so
|
|
1724
|
+
// re-locking here would self-deadlock (ELOCKED).
|
|
1725
|
+
recordReviewAvailability(deps.projectRoot, {
|
|
1726
|
+
provider: "opencode",
|
|
1727
|
+
reviewer: "opencode",
|
|
1728
|
+
effort: event.effort,
|
|
1729
|
+
stage: event.stage,
|
|
1730
|
+
terminalClassification: "provider-limit-reached",
|
|
1731
|
+
failureCode: "opencode-go-usage-limit",
|
|
1732
|
+
...(event.limitWindow ? { limitWindow: event.limitWindow } : {}),
|
|
1733
|
+
...(event.retryAfterMs === undefined ? {} : { retryAfterMs: event.retryAfterMs }),
|
|
1734
|
+
});
|
|
1735
|
+
return "suppress-provider";
|
|
1736
|
+
},
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1448
1739
|
function currentReviewHost(env = process.env) {
|
|
1449
1740
|
const host = env["WP_REVIEW_HOST"] ?? env["WP_HOOK_HOST"];
|
|
1450
1741
|
return host === "claude" || host === "codex" || host === "opencode" || host === "grok"
|
|
@@ -1527,11 +1818,52 @@ function reviewArtifactRelativePath(input) {
|
|
|
1527
1818
|
return path.posix.join(artifactDirectory, filename);
|
|
1528
1819
|
}
|
|
1529
1820
|
function reviewGateEventId(input) {
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1821
|
+
if (input.stage !== "approval-gate") {
|
|
1822
|
+
return `gate:${input.gateId}:${input.provider}:${input.stage}:${input.ordinal}:${input.model}`;
|
|
1823
|
+
}
|
|
1824
|
+
return input.ordinal === 1
|
|
1825
|
+
? `gate:${input.gateId}:${input.provider}:${input.model}`
|
|
1826
|
+
: `gate:${input.gateId}:${input.provider}:approval-retry:${input.ordinal}:${input.model}`;
|
|
1827
|
+
}
|
|
1828
|
+
function isFormalGateRejection(event) {
|
|
1829
|
+
const provider = event.provider;
|
|
1830
|
+
if (event.status !== "complete" ||
|
|
1831
|
+
event.verdict !== "reject" ||
|
|
1832
|
+
event.stage !== "approval-gate" ||
|
|
1833
|
+
!/^[a-f0-9]{16}$/u.test(event.gateId) ||
|
|
1834
|
+
!provider ||
|
|
1835
|
+
!event.model ||
|
|
1836
|
+
!event.artifactPath) {
|
|
1837
|
+
return false;
|
|
1838
|
+
}
|
|
1839
|
+
return (event.id ===
|
|
1840
|
+
reviewGateEventId({
|
|
1841
|
+
gateId: event.gateId,
|
|
1842
|
+
provider,
|
|
1843
|
+
model: event.model,
|
|
1844
|
+
stage: "approval-gate",
|
|
1845
|
+
ordinal: event.attemptOrdinal ?? 1,
|
|
1846
|
+
}));
|
|
1847
|
+
}
|
|
1848
|
+
function rejectedReviewSubjects(input) {
|
|
1849
|
+
const bySubject = new Map();
|
|
1850
|
+
for (const event of input.events) {
|
|
1851
|
+
if (event.blueprintSlug !== input.stableSlug ||
|
|
1852
|
+
event.purpose !== input.purpose ||
|
|
1853
|
+
!isFormalGateRejection(event)) {
|
|
1854
|
+
continue;
|
|
1855
|
+
}
|
|
1856
|
+
const canonical = input.purpose === "plan"
|
|
1857
|
+
? createPlanSubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug)
|
|
1858
|
+
: createDeliverySubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug);
|
|
1859
|
+
if (!bySubject.has(canonical.digest)) {
|
|
1860
|
+
bySubject.set(canonical.digest, {
|
|
1861
|
+
artifactPath: event.artifactPath,
|
|
1862
|
+
subjectDigest: canonical.digest,
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
return [...bySubject.values()];
|
|
1535
1867
|
}
|
|
1536
1868
|
/**
|
|
1537
1869
|
* Sanctioned fallback when `wp review gate` does not reach "approved" — points
|
|
@@ -1540,13 +1872,46 @@ function reviewGateEventId(input) {
|
|
|
1540
1872
|
* workaround. See catalog/agent/rules/pre-implementation.md.
|
|
1541
1873
|
*/
|
|
1542
1874
|
export function reviewGateNonConvergenceHint(slug, context = {}) {
|
|
1875
|
+
if (context.status === "scope-exhausted") {
|
|
1876
|
+
return `hint: formal review budget is exhausted for ${slug}. Stop automated review retries and obtain an explicit human scope decision before creating a successor blueprint.`;
|
|
1877
|
+
}
|
|
1543
1878
|
if (context.failureCode === "provider-event-too-large" && context.gateId) {
|
|
1544
1879
|
return `hint: provider transport emitted a single provider JSON event above the bounded capture limit for ${slug}; no review verdict was produced. Retry the unchanged gate with "wp review gate ${slug} --resume ${context.gateId} --provider <provider>" and select a different provider when possible. Retrying the same provider/model is allowed for recovery but is likely to recur for deterministic oversized output. An independent "wp review run" can be recorded only as a human-adjudicated verdict through wp_blueprint_review_log; that tool cannot create a Claude/Codex/Grok/OpenCode-Go approval. The event-size and timeout bounds were not increased. See catalog/agent/rules/pre-implementation.md.`;
|
|
1545
1880
|
}
|
|
1546
1881
|
return `hint: wp review gate did not satisfy the approval policy for ${slug}. Retry interrupted or structural failures with --resume${context.gateId ? ` ${context.gateId}` : " <gate-id>"} and optionally switch --provider. For a separate review, wp_blueprint_review_log can record only a human-adjudicated approval; it cannot create a Claude/Codex/Grok/OpenCode-Go approval and cannot substitute for the strong-reviewer gate. It is NOT the same as this CLI's own "wp review log" subcommand, which writes only the machine ledger and requires --provider plus a human reviewer for any approval verdict. See catalog/agent/rules/pre-implementation.md.`;
|
|
1547
1882
|
}
|
|
1883
|
+
/**
|
|
1884
|
+
* Guards the invariant this file must never silently violate: a
|
|
1885
|
+
* `wp-git-tree-v1` delivery subject digest is computed purely from the
|
|
1886
|
+
* committed tree at `targetSha` (see createDeliverySubjectAtRef). When that
|
|
1887
|
+
* target is the currently checked-out HEAD — the default and overwhelmingly
|
|
1888
|
+
* common case — any uncommitted non-evidence change in the working tree is
|
|
1889
|
+
* content nobody has reviewed yet. Reporting a "satisfied" verdict in that
|
|
1890
|
+
* state (whether by reusing a prior approval for `targetSha` or by running a
|
|
1891
|
+
* fresh review whose prompt still only sees `targetSha`'s committed content)
|
|
1892
|
+
* would silently ship edits that were never reviewed. Fail closed instead.
|
|
1893
|
+
*
|
|
1894
|
+
* Plan purpose is intentionally exempt: its `wp-blueprint-v1` digest is
|
|
1895
|
+
* scoped to a single file and already neutralizes lifecycle bookkeeping by
|
|
1896
|
+
* design (see normalizePlanSubjectContent), so uncommitted lifecycle moves
|
|
1897
|
+
* are expected to keep reusing an existing plan approval.
|
|
1898
|
+
*/
|
|
1899
|
+
function assertWorkingTreeMatchesDeliveryTarget(projectRoot, purpose, targetSha, baseSha) {
|
|
1900
|
+
if (purpose !== "delivery" || targetSha !== baseSha)
|
|
1901
|
+
return;
|
|
1902
|
+
const gitRoot = resolveReviewGitToplevel(projectRoot);
|
|
1903
|
+
const divergentPaths = findUnreviewedWorkingTreeChanges(gitRoot);
|
|
1904
|
+
if (divergentPaths.length === 0)
|
|
1905
|
+
return;
|
|
1906
|
+
throw new Error(`Review target ${targetSha} is the current HEAD, but the working tree has uncommitted ` +
|
|
1907
|
+
`changes not reflected in that reviewed commit: ${divergentPaths.join(", ")}. A ` +
|
|
1908
|
+
`wp-git-tree-v1 approval bound to ${targetSha} does not cover these edits. Commit (or ` +
|
|
1909
|
+
`discard) the changes so the reviewed content matches what will actually ship, then ` +
|
|
1910
|
+
`re-run "wp review gate".`);
|
|
1911
|
+
}
|
|
1548
1912
|
export async function runReviewGate(projectRoot, slug, input, dependencies = { invokeProvider }) {
|
|
1549
1913
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
1914
|
+
assertReviewEvidenceCapable(location);
|
|
1550
1915
|
if (lstatSync(location.path).isSymbolicLink()) {
|
|
1551
1916
|
throw new Error(`Blueprint overview must not be a symlink: ${location.path}`);
|
|
1552
1917
|
}
|
|
@@ -1560,6 +1925,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1560
1925
|
const subject = purpose === "plan"
|
|
1561
1926
|
? createPlanSubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath)
|
|
1562
1927
|
: createDeliverySubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath);
|
|
1928
|
+
assertWorkingTreeMatchesDeliveryTarget(projectRoot, purpose, targetSha, baseSha);
|
|
1563
1929
|
const promptHash = hashPrompt({
|
|
1564
1930
|
blueprintContent,
|
|
1565
1931
|
blueprintSlug: stableSlug,
|
|
@@ -1578,13 +1944,16 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1578
1944
|
}
|
|
1579
1945
|
const explicitProvider = normalizeGateProvider(input.provider);
|
|
1580
1946
|
const effort = normalizeGateEffort(input.effort);
|
|
1581
|
-
const
|
|
1947
|
+
const renderPrompt = (verificationSummary, priorRejections) => buildGatePrompt({
|
|
1582
1948
|
blueprintPath: projectBlueprintPath,
|
|
1583
1949
|
blueprintSlug: stableSlug,
|
|
1584
1950
|
purpose,
|
|
1585
1951
|
subject,
|
|
1586
1952
|
targetSha,
|
|
1953
|
+
...(verificationSummary ? { verificationSummary } : {}),
|
|
1954
|
+
...(priorRejections ? { priorRejections } : {}),
|
|
1587
1955
|
});
|
|
1956
|
+
let prompt = renderPrompt();
|
|
1588
1957
|
return withLock("worktree", async () => {
|
|
1589
1958
|
const resumed = loadGateState(projectRoot, gateId, {
|
|
1590
1959
|
purpose,
|
|
@@ -1597,17 +1966,32 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1597
1966
|
resumed.subjectDigest !== subject.digest)) {
|
|
1598
1967
|
throw new Error(`Gate ${gateId} does not match target ${targetSha} and prompt ${promptHash}.`);
|
|
1599
1968
|
}
|
|
1600
|
-
let reusedEvents = [];
|
|
1601
1969
|
const gateIssues = [];
|
|
1970
|
+
let lineageFailure;
|
|
1971
|
+
let rejectedSubjects = [];
|
|
1972
|
+
let reusedApprovals = [];
|
|
1602
1973
|
try {
|
|
1603
1974
|
const reusedAuthority = readReviewAuthorityAtRef(projectRoot, targetSha, stableSlug, purpose);
|
|
1604
|
-
|
|
1975
|
+
reusedApprovals = reusedAuthority.approvals;
|
|
1605
1976
|
gateIssues.push(...reusedAuthority.issues);
|
|
1977
|
+
const formalRejectionIds = new Set(reusedAuthority.events.filter(isFormalGateRejection).map((event) => event.id));
|
|
1978
|
+
const compromisedRejection = reusedAuthority.issues.find((issue) => [...formalRejectionIds].some((eventId) => issue.includes(`(${eventId})`)));
|
|
1979
|
+
if (compromisedRejection) {
|
|
1980
|
+
lineageFailure = `Formal rejection evidence failed integrity verification: ${compromisedRejection}`;
|
|
1981
|
+
}
|
|
1982
|
+
else {
|
|
1983
|
+
rejectedSubjects = rejectedReviewSubjects({
|
|
1984
|
+
events: reusedAuthority.events,
|
|
1985
|
+
projectRoot,
|
|
1986
|
+
purpose,
|
|
1987
|
+
stableSlug,
|
|
1988
|
+
});
|
|
1989
|
+
}
|
|
1606
1990
|
}
|
|
1607
|
-
catch {
|
|
1608
|
-
|
|
1991
|
+
catch (error) {
|
|
1992
|
+
lineageFailure = `Review lineage authority is unreadable: ${error instanceof Error ? error.message : String(error)}`;
|
|
1993
|
+
gateIssues.push(lineageFailure);
|
|
1609
1994
|
}
|
|
1610
|
-
const reusedApprovals = reusedEvents;
|
|
1611
1995
|
const statePath = reviewGateStatePath(projectRoot, gateId);
|
|
1612
1996
|
const reusedSummary = reusedApprovals.map((entry) => ({
|
|
1613
1997
|
reviewer: entry.reviewer,
|
|
@@ -1617,7 +2001,9 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1617
2001
|
targetHash: entry.subjectDigest,
|
|
1618
2002
|
rev: entry.id,
|
|
1619
2003
|
}));
|
|
1620
|
-
const workingAtStart =
|
|
2004
|
+
const workingAtStart = lineageFailure
|
|
2005
|
+
? { events: [], compromisedIds: new Set(), issues: [] }
|
|
2006
|
+
: readWorkingReviewEvents(location.path, stableSlug);
|
|
1621
2007
|
gateIssues.push(...workingAtStart.issues);
|
|
1622
2008
|
const durableGateEvents = workingAtStart.events.filter((event) => event.gateId === gateId &&
|
|
1623
2009
|
event.promptHash === promptHash &&
|
|
@@ -1630,6 +2016,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1630
2016
|
...(resumed?.attempts ?? []),
|
|
1631
2017
|
...durableGateEvents.map(gateAttemptFromEvent),
|
|
1632
2018
|
]);
|
|
2019
|
+
let verification;
|
|
1633
2020
|
let publicationProgress = resumed?.publicationProgress ??
|
|
1634
2021
|
(durableGateEvents.length > 0
|
|
1635
2022
|
? { artifact: true, ledger: true, frontmatter: false }
|
|
@@ -1648,7 +2035,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1648
2035
|
}, working.compromisedIds);
|
|
1649
2036
|
return evaluateApprovalReviewers(evaluation.approvals.map((entry) => entry.reviewer));
|
|
1650
2037
|
};
|
|
1651
|
-
const makeResult = (approvalGate, status) => ({
|
|
2038
|
+
const makeResult = (approvalGate, status, messageOverride) => ({
|
|
1652
2039
|
schemaVersion: 2,
|
|
1653
2040
|
gateId,
|
|
1654
2041
|
purpose,
|
|
@@ -1666,222 +2053,336 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1666
2053
|
publicationProgress,
|
|
1667
2054
|
reusedApprovals: reusedSummary,
|
|
1668
2055
|
...(gateIssues.length > 0 ? { issues: [...gateIssues] } : {}),
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
2056
|
+
...(verification ? { verification } : {}),
|
|
2057
|
+
message: messageOverride ??
|
|
2058
|
+
`${approvalGate.satisfied
|
|
2059
|
+
? `Approval gate satisfied (${formatApprovalGateRequirement(approvalGate)}).`
|
|
2060
|
+
: `Approval gate not satisfied (${formatApprovalGateRequirement(approvalGate)}).`}${gateIssues.length > 0 ? ` Warnings: ${gateIssues.join("; ")}` : ""}`,
|
|
1672
2061
|
});
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
const result = makeResult(approvalGate, reusedApprovals.length > 0 ? "approved" : "recorded-untracked");
|
|
1676
|
-
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1677
|
-
persistGateState(projectRoot, result);
|
|
1678
|
-
return result;
|
|
1679
|
-
}
|
|
1680
|
-
if (attempts.some((attempt) => attempt.status === "complete" && attempt.verdict === "reject")) {
|
|
1681
|
-
const result = makeResult(approvalGate, "rejected");
|
|
2062
|
+
if (lineageFailure) {
|
|
2063
|
+
const result = makeResult(evaluateApprovalReviewers([]), "verification-failed", `${lineageFailure} No review provider was invoked.`);
|
|
1682
2064
|
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1683
2065
|
persistGateState(projectRoot, result);
|
|
1684
2066
|
return result;
|
|
1685
2067
|
}
|
|
1686
|
-
const artifactDirectory = path.join(path.dirname(location.path), "review-artifacts");
|
|
1687
|
-
if (existsSync(artifactDirectory) && lstatSync(artifactDirectory).isSymbolicLink()) {
|
|
1688
|
-
throw new Error(`Review artifact directory must not be a symlink: ${artifactDirectory}`);
|
|
1689
|
-
}
|
|
1690
|
-
const availabilityPrune = pruneExpiredAvailabilitySuppressions(loadReviewAvailability(projectRoot));
|
|
1691
|
-
if (availabilityPrune.pruned.length > 0) {
|
|
1692
|
-
saveReviewAvailability(projectRoot, availabilityPrune.state);
|
|
1693
|
-
}
|
|
1694
|
-
const availability = availabilityPrune.state;
|
|
1695
|
-
const providerQueue = selectGateProviders({
|
|
1696
|
-
availability,
|
|
1697
|
-
...(explicitProvider ? { explicitProvider } : {}),
|
|
1698
|
-
});
|
|
1699
|
-
const usedReviewers = new Set([
|
|
1700
|
-
...attempts
|
|
1701
|
-
.filter((attempt) => !isRetryableGateAttempt(attempt))
|
|
1702
|
-
.map((attempt) => attempt.reviewer)
|
|
1703
|
-
.filter((value) => Boolean(value)),
|
|
1704
|
-
...suppressedReviewers(availability),
|
|
1705
|
-
]);
|
|
1706
2068
|
const checkoutFactory = dependencies.createTargetCheckout ?? createReviewTargetCheckout;
|
|
1707
2069
|
let sharedCheckout;
|
|
1708
2070
|
let checkoutSetupFailed = false;
|
|
1709
|
-
|
|
2071
|
+
// Deferred: a review checkout costs a full `git archive` + tar tree copy
|
|
2072
|
+
// plus status fingerprints, so it must not be created on gate calls that
|
|
2073
|
+
// never need one (plan-purpose reuse of an existing approval, terminal
|
|
2074
|
+
// rejections, exhausted scope). A setup failure is remembered so provider
|
|
2075
|
+
// attempts still surface the same isolation-failure evidence.
|
|
2076
|
+
const ensureSharedCheckout = () => {
|
|
2077
|
+
if (sharedCheckout !== undefined || checkoutSetupFailed)
|
|
2078
|
+
return sharedCheckout;
|
|
1710
2079
|
try {
|
|
1711
2080
|
sharedCheckout = checkoutFactory(projectRoot, targetSha);
|
|
1712
2081
|
}
|
|
1713
2082
|
catch {
|
|
1714
2083
|
checkoutSetupFailed = true;
|
|
1715
2084
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
2085
|
+
return sharedCheckout;
|
|
2086
|
+
};
|
|
2087
|
+
const createCheckoutForAttempt = () => {
|
|
2088
|
+
// Only reachable when ensureSharedCheckout() already failed — a live
|
|
2089
|
+
// shared checkout is always passed to the attempt as existingCheckout.
|
|
2090
|
+
throw new Error("review checkout setup failed");
|
|
2091
|
+
};
|
|
2092
|
+
let sharedCheckoutCleaned = false;
|
|
2093
|
+
const cleanupSharedCheckout = () => {
|
|
2094
|
+
if (sharedCheckoutCleaned)
|
|
2095
|
+
return;
|
|
2096
|
+
sharedCheckoutCleaned = true;
|
|
2097
|
+
sharedCheckout?.cleanup();
|
|
2098
|
+
};
|
|
1722
2099
|
try {
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
2100
|
+
// Only the delivery purpose runs checkout-backed gate verification
|
|
2101
|
+
// (verifyDeliveryPromotionGates is a no-op for every other purpose).
|
|
2102
|
+
const deliveryCheckout = purpose === "delivery" ? ensureSharedCheckout() : undefined;
|
|
2103
|
+
if (deliveryCheckout) {
|
|
2104
|
+
verification = verifyDeliveryPromotionGates({
|
|
2105
|
+
purpose,
|
|
2106
|
+
blueprintContent,
|
|
2107
|
+
projectRoot,
|
|
2108
|
+
gateId,
|
|
2109
|
+
checkout: deliveryCheckout,
|
|
2110
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
2111
|
+
});
|
|
2112
|
+
const summary = verificationPromptSummary(verification);
|
|
2113
|
+
if (summary) {
|
|
2114
|
+
prompt = renderPrompt(summary);
|
|
2115
|
+
}
|
|
2116
|
+
if (verification?.status === "failed") {
|
|
2117
|
+
const result = makeResult(currentApprovalGate(), "verification-failed");
|
|
2118
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2119
|
+
persistGateState(projectRoot, result);
|
|
2120
|
+
cleanupSharedCheckout();
|
|
2121
|
+
return result;
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
let approvalGate = currentApprovalGate();
|
|
2125
|
+
if (approvalGate.satisfied) {
|
|
2126
|
+
const result = makeResult(approvalGate, reusedApprovals.length > 0 ? "approved" : "recorded-untracked");
|
|
2127
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2128
|
+
persistGateState(projectRoot, result);
|
|
2129
|
+
cleanupSharedCheckout();
|
|
2130
|
+
return result;
|
|
2131
|
+
}
|
|
2132
|
+
if (attempts.some((attempt) => attempt.status === "complete" && attempt.verdict === "reject")) {
|
|
2133
|
+
const result = makeResult(approvalGate, "rejected");
|
|
2134
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2135
|
+
persistGateState(projectRoot, result);
|
|
2136
|
+
cleanupSharedCheckout();
|
|
2137
|
+
return result;
|
|
2138
|
+
}
|
|
2139
|
+
const currentSubjectRejection = rejectedSubjects.find((entry) => entry.subjectDigest === subject.digest);
|
|
2140
|
+
if (currentSubjectRejection) {
|
|
2141
|
+
const result = makeResult(approvalGate, "rejected", `This semantic review subject was already rejected (${currentSubjectRejection.artifactPath}). No review provider was invoked.`);
|
|
2142
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2143
|
+
persistGateState(projectRoot, result);
|
|
2144
|
+
cleanupSharedCheckout();
|
|
2145
|
+
return result;
|
|
2146
|
+
}
|
|
2147
|
+
if (rejectedSubjects.length >= 2) {
|
|
2148
|
+
const result = makeResult(approvalGate, "scope-exhausted", "Formal review budget exhausted after two rejected semantic subjects. No review provider was invoked; stop and obtain an explicit human scope decision before creating a successor blueprint.");
|
|
2149
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2150
|
+
persistGateState(projectRoot, result);
|
|
2151
|
+
cleanupSharedCheckout();
|
|
2152
|
+
return result;
|
|
2153
|
+
}
|
|
2154
|
+
if (rejectedSubjects.length > 0) {
|
|
2155
|
+
prompt = renderPrompt(verification ? verificationPromptSummary(verification) : undefined, rejectedSubjects.slice(0, 2));
|
|
2156
|
+
}
|
|
2157
|
+
const artifactDirectory = path.join(path.dirname(location.path), "review-artifacts");
|
|
2158
|
+
if (existsSync(artifactDirectory) && lstatSync(artifactDirectory).isSymbolicLink()) {
|
|
2159
|
+
throw new Error(`Review artifact directory must not be a symlink: ${artifactDirectory}`);
|
|
2160
|
+
}
|
|
2161
|
+
const availabilityPrune = pruneExpiredAvailabilitySuppressions(loadReviewAvailability(projectRoot));
|
|
2162
|
+
if (availabilityPrune.pruned.length > 0) {
|
|
2163
|
+
saveReviewAvailability(projectRoot, availabilityPrune.state);
|
|
2164
|
+
}
|
|
2165
|
+
const availability = availabilityPrune.state;
|
|
2166
|
+
const providerQueue = selectGateProviders({
|
|
2167
|
+
availability,
|
|
2168
|
+
...(explicitProvider ? { explicitProvider } : {}),
|
|
2169
|
+
});
|
|
2170
|
+
const usedReviewers = new Set([
|
|
2171
|
+
...attempts
|
|
2172
|
+
.filter((attempt) => !isRetryableGateAttempt(attempt))
|
|
2173
|
+
.map((attempt) => attempt.reviewer)
|
|
2174
|
+
.filter((value) => Boolean(value)),
|
|
2175
|
+
...suppressedReviewers(availability),
|
|
2176
|
+
]);
|
|
2177
|
+
// Prepare opencode-go multi-account failover only when opencode is
|
|
2178
|
+
// actually queued (avoids a needless secret fetch otherwise). Resolves
|
|
2179
|
+
// Doppler accounts, materializes the freshest non-cooled account's
|
|
2180
|
+
// isolated auth store, and hands the loop its XDG_DATA_HOME. Degrades to
|
|
2181
|
+
// undefined (today's global-auth behavior) when unprovisioned.
|
|
2182
|
+
const opencodeFailover = providerQueue.includes("opencode")
|
|
2183
|
+
? createOpencodeFailoverController({
|
|
2184
|
+
projectRoot,
|
|
2185
|
+
resolveAccounts: dependencies.resolveOpencodeAccounts ?? resolveOpencodeAccountsGracefully,
|
|
2186
|
+
prepareCredential: dependencies.prepareOpencodeCredential ?? prepareOpencodeCredentialDefault,
|
|
2187
|
+
})
|
|
2188
|
+
: undefined;
|
|
2189
|
+
try {
|
|
2190
|
+
reviewAttempts: for (const provider of providerQueue) {
|
|
2191
|
+
const maxAttempts = provider === "claude" ||
|
|
2192
|
+
provider === "codex" ||
|
|
2193
|
+
provider === "grok" ||
|
|
2194
|
+
(provider === explicitProvider && input.model)
|
|
2195
|
+
? 1
|
|
2196
|
+
: OPENCODE_FAMILY_ORDER.length;
|
|
2197
|
+
for (let index = 0; index < maxAttempts && !approvalGate.satisfied; index += 1) {
|
|
2198
|
+
if (dependencies.signal?.aborted)
|
|
2199
|
+
break reviewAttempts;
|
|
2200
|
+
const opencodeAuthDataDir = provider === "opencode" ? opencodeFailover?.authDataDir() : undefined;
|
|
2201
|
+
const providerResult = enforceReviewPublicationOutput(await (dependencies.invokeProvider ?? invokeProvider)({
|
|
2202
|
+
provider,
|
|
1752
2203
|
projectRoot,
|
|
1753
|
-
|
|
1754
|
-
|
|
2204
|
+
prompt,
|
|
2205
|
+
effort,
|
|
2206
|
+
...(provider === explicitProvider && input.model ? { model: input.model } : {}),
|
|
2207
|
+
excludedReviewers: usedReviewers,
|
|
2208
|
+
targetSha,
|
|
2209
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
2210
|
+
...(opencodeAuthDataDir ? { authDataDir: opencodeAuthDataDir } : {}),
|
|
2211
|
+
}, dependencies.runReviewExecution, dependencies.runCatalogCommand, createCheckoutForAttempt, ensureSharedCheckout));
|
|
2212
|
+
if (providerResult.reviewer !== "opencode")
|
|
2213
|
+
usedReviewers.add(providerResult.reviewer);
|
|
2214
|
+
const providerStage = gateAttemptStage(providerResult.stage ?? "review");
|
|
2215
|
+
let attemptOrdinal = nextGateAttemptOrdinal(attempts, provider, providerResult.model, providerStage);
|
|
2216
|
+
let artifactRelative;
|
|
2217
|
+
let artifactAbsolute;
|
|
2218
|
+
let rev;
|
|
2219
|
+
while (true) {
|
|
2220
|
+
artifactRelative = reviewArtifactRelativePath({
|
|
2221
|
+
projectRoot,
|
|
2222
|
+
blueprintPath: location.path,
|
|
2223
|
+
gateId,
|
|
2224
|
+
provider,
|
|
2225
|
+
reviewer: providerResult.reviewer,
|
|
2226
|
+
stage: providerStage,
|
|
2227
|
+
ordinal: attemptOrdinal,
|
|
2228
|
+
});
|
|
2229
|
+
artifactAbsolute = safeArtifactPath(location.path, artifactRelative);
|
|
2230
|
+
rev = reviewGateEventId({
|
|
2231
|
+
gateId,
|
|
2232
|
+
provider,
|
|
2233
|
+
model: providerResult.model,
|
|
2234
|
+
stage: providerStage,
|
|
2235
|
+
ordinal: attemptOrdinal,
|
|
2236
|
+
});
|
|
2237
|
+
if (!existsSync(artifactAbsolute) &&
|
|
2238
|
+
!gateArtifactPaths.has(artifactRelative) &&
|
|
2239
|
+
!gateEventIds.has(rev)) {
|
|
2240
|
+
break;
|
|
2241
|
+
}
|
|
2242
|
+
attemptOrdinal += 1;
|
|
2243
|
+
}
|
|
2244
|
+
const persistedOrdinal = providerStage === "approval-gate" && attemptOrdinal === 1
|
|
2245
|
+
? undefined
|
|
2246
|
+
: attemptOrdinal;
|
|
2247
|
+
const attempt = {
|
|
2248
|
+
provider,
|
|
2249
|
+
model: providerResult.model,
|
|
2250
|
+
effort,
|
|
2251
|
+
reviewer: providerResult.reviewer,
|
|
2252
|
+
promptHash,
|
|
2253
|
+
...(persistedOrdinal === undefined ? {} : { ordinal: persistedOrdinal }),
|
|
2254
|
+
stage: providerStage,
|
|
2255
|
+
status: providerResult.status,
|
|
2256
|
+
verdict: providerResult.verdict,
|
|
2257
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2258
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2259
|
+
...(providerResult.providerErrorDetail
|
|
2260
|
+
? { providerErrorDetail: providerResult.providerErrorDetail }
|
|
2261
|
+
: {}),
|
|
2262
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2263
|
+
...(providerResult.retryAfterMs === undefined
|
|
2264
|
+
? {}
|
|
2265
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2266
|
+
artifact: artifactRelative,
|
|
2267
|
+
};
|
|
2268
|
+
attempts = dedupeGateAttempts([...attempts, attempt]);
|
|
2269
|
+
recordReviewAvailability(projectRoot, {
|
|
1755
2270
|
provider,
|
|
1756
2271
|
reviewer: providerResult.reviewer,
|
|
2272
|
+
model: providerResult.model,
|
|
2273
|
+
effort,
|
|
1757
2274
|
stage: providerStage,
|
|
1758
|
-
|
|
2275
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2276
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2277
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2278
|
+
...(providerResult.retryAfterMs === undefined
|
|
2279
|
+
? {}
|
|
2280
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1759
2281
|
});
|
|
1760
|
-
|
|
1761
|
-
|
|
2282
|
+
const artifactContent = Buffer.from(`${verificationArtifactSection(verification)}${providerResult.output}`, "utf8");
|
|
2283
|
+
const event = {
|
|
2284
|
+
schemaVersion: 2,
|
|
2285
|
+
id: rev,
|
|
2286
|
+
purpose,
|
|
2287
|
+
blueprintSlug: stableSlug,
|
|
2288
|
+
subjectScheme: subject.scheme,
|
|
2289
|
+
subjectDigest: subject.digest,
|
|
2290
|
+
reviewedCommit: targetSha,
|
|
1762
2291
|
gateId,
|
|
2292
|
+
promptHash,
|
|
2293
|
+
reviewer: providerResult.reviewer,
|
|
1763
2294
|
provider,
|
|
1764
2295
|
model: providerResult.model,
|
|
2296
|
+
effort,
|
|
1765
2297
|
stage: providerStage,
|
|
1766
|
-
|
|
2298
|
+
...(persistedOrdinal === undefined ? {} : { attemptOrdinal: persistedOrdinal }),
|
|
2299
|
+
status: providerResult.status,
|
|
2300
|
+
verdict: providerResult.verdict,
|
|
2301
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2302
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2303
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2304
|
+
...(providerResult.retryAfterMs === undefined
|
|
2305
|
+
? {}
|
|
2306
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2307
|
+
timestamp: new Date().toISOString(),
|
|
2308
|
+
artifactPath: artifactRelative,
|
|
2309
|
+
artifactSha256: createHash("sha256").update(artifactContent).digest("hex"),
|
|
2310
|
+
};
|
|
2311
|
+
appendReviewEvent({
|
|
2312
|
+
ledgerPath: path.join(path.dirname(location.path), "review-events.jsonl"),
|
|
2313
|
+
event,
|
|
2314
|
+
artifact: { path: artifactAbsolute, content: artifactContent },
|
|
1767
2315
|
});
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
2316
|
+
gateEventIds.add(rev);
|
|
2317
|
+
gateArtifactPaths.add(artifactRelative);
|
|
2318
|
+
publicationProgress = { artifact: true, ledger: true, frontmatter: false };
|
|
2319
|
+
approvalGate = currentApprovalGate();
|
|
2320
|
+
const interimStatus = providerResult.verdict === "reject"
|
|
2321
|
+
? "rejected"
|
|
2322
|
+
: approvalGate.satisfied
|
|
2323
|
+
? "recorded-untracked"
|
|
2324
|
+
: "needs-review";
|
|
2325
|
+
persistGateState(projectRoot, makeResult(approvalGate, interimStatus));
|
|
2326
|
+
if (providerResult.verdict === "reject") {
|
|
2327
|
+
return makeResult(approvalGate, "rejected");
|
|
2328
|
+
}
|
|
2329
|
+
if (providerResult.failureCode === "review-worktree-contaminated" ||
|
|
2330
|
+
providerResult.failureCode === "review-isolation-failure") {
|
|
2331
|
+
break reviewAttempts;
|
|
2332
|
+
}
|
|
2333
|
+
if (dependencies.signal?.aborted)
|
|
2334
|
+
break reviewAttempts;
|
|
2335
|
+
if (provider === "opencode" &&
|
|
2336
|
+
opencodeFailover &&
|
|
2337
|
+
providerResult.terminalClassification === "provider-limit-reached" &&
|
|
2338
|
+
providerResult.failureCode === "opencode-go-usage-limit") {
|
|
2339
|
+
const outcome = await opencodeFailover.onUsageLimit({
|
|
2340
|
+
effort,
|
|
2341
|
+
stage: providerStage,
|
|
2342
|
+
...(providerResult.limitWindow
|
|
2343
|
+
? { limitWindow: providerResult.limitWindow }
|
|
2344
|
+
: {}),
|
|
2345
|
+
...(providerResult.retryAfterMs === undefined
|
|
2346
|
+
? {}
|
|
2347
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2348
|
+
});
|
|
2349
|
+
if (outcome === "retry") {
|
|
2350
|
+
// The family failed only because its ACCOUNT was capped. Make it
|
|
2351
|
+
// eligible again and retry under the next account without
|
|
2352
|
+
// consuming a family-rotation slot.
|
|
2353
|
+
usedReviewers.delete(providerResult.reviewer);
|
|
2354
|
+
index -= 1;
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
1771
2357
|
break;
|
|
1772
2358
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
reviewer: providerResult.reviewer,
|
|
1783
|
-
promptHash,
|
|
1784
|
-
...(persistedOrdinal === undefined ? {} : { ordinal: persistedOrdinal }),
|
|
1785
|
-
stage: providerStage,
|
|
1786
|
-
status: providerResult.status,
|
|
1787
|
-
verdict: providerResult.verdict,
|
|
1788
|
-
terminalClassification: providerResult.terminalClassification,
|
|
1789
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1790
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1791
|
-
...(providerResult.retryAfterMs === undefined
|
|
1792
|
-
? {}
|
|
1793
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1794
|
-
artifact: artifactRelative,
|
|
1795
|
-
};
|
|
1796
|
-
attempts = dedupeGateAttempts([...attempts, attempt]);
|
|
1797
|
-
recordReviewAvailability(projectRoot, {
|
|
1798
|
-
provider,
|
|
1799
|
-
reviewer: providerResult.reviewer,
|
|
1800
|
-
model: providerResult.model,
|
|
1801
|
-
effort,
|
|
1802
|
-
stage: providerStage,
|
|
1803
|
-
terminalClassification: providerResult.terminalClassification,
|
|
1804
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1805
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1806
|
-
...(providerResult.retryAfterMs === undefined
|
|
1807
|
-
? {}
|
|
1808
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1809
|
-
});
|
|
1810
|
-
const artifactContent = Buffer.from(providerResult.output, "utf8");
|
|
1811
|
-
const event = {
|
|
1812
|
-
schemaVersion: 2,
|
|
1813
|
-
id: rev,
|
|
1814
|
-
purpose,
|
|
1815
|
-
blueprintSlug: stableSlug,
|
|
1816
|
-
subjectScheme: subject.scheme,
|
|
1817
|
-
subjectDigest: subject.digest,
|
|
1818
|
-
reviewedCommit: targetSha,
|
|
1819
|
-
gateId,
|
|
1820
|
-
promptHash,
|
|
1821
|
-
reviewer: providerResult.reviewer,
|
|
1822
|
-
provider,
|
|
1823
|
-
model: providerResult.model,
|
|
1824
|
-
effort,
|
|
1825
|
-
stage: providerStage,
|
|
1826
|
-
...(persistedOrdinal === undefined ? {} : { attemptOrdinal: persistedOrdinal }),
|
|
1827
|
-
status: providerResult.status,
|
|
1828
|
-
verdict: providerResult.verdict,
|
|
1829
|
-
terminalClassification: providerResult.terminalClassification,
|
|
1830
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1831
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1832
|
-
...(providerResult.retryAfterMs === undefined
|
|
1833
|
-
? {}
|
|
1834
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1835
|
-
timestamp: new Date().toISOString(),
|
|
1836
|
-
artifactPath: artifactRelative,
|
|
1837
|
-
artifactSha256: createHash("sha256").update(artifactContent).digest("hex"),
|
|
1838
|
-
};
|
|
1839
|
-
appendReviewEvent({
|
|
1840
|
-
ledgerPath: path.join(path.dirname(location.path), "review-events.jsonl"),
|
|
1841
|
-
event,
|
|
1842
|
-
artifact: { path: artifactAbsolute, content: artifactContent },
|
|
1843
|
-
});
|
|
1844
|
-
gateEventIds.add(rev);
|
|
1845
|
-
gateArtifactPaths.add(artifactRelative);
|
|
1846
|
-
publicationProgress = { artifact: true, ledger: true, frontmatter: false };
|
|
1847
|
-
approvalGate = currentApprovalGate();
|
|
1848
|
-
const interimStatus = providerResult.verdict === "reject"
|
|
1849
|
-
? "rejected"
|
|
1850
|
-
: approvalGate.satisfied
|
|
1851
|
-
? "recorded-untracked"
|
|
1852
|
-
: "needs-review";
|
|
1853
|
-
persistGateState(projectRoot, makeResult(approvalGate, interimStatus));
|
|
1854
|
-
if (providerResult.verdict === "reject") {
|
|
1855
|
-
return makeResult(approvalGate, "rejected");
|
|
1856
|
-
}
|
|
1857
|
-
if (providerResult.failureCode === "review-worktree-contaminated" ||
|
|
1858
|
-
providerResult.failureCode === "review-isolation-failure") {
|
|
1859
|
-
break reviewAttempts;
|
|
1860
|
-
}
|
|
1861
|
-
if (dependencies.signal?.aborted)
|
|
1862
|
-
break reviewAttempts;
|
|
1863
|
-
if (providerResult.terminalClassification === "provider-limit-reached" &&
|
|
1864
|
-
(provider !== "opencode" || isProviderWideOpenCodeGateResult(providerResult))) {
|
|
1865
|
-
break;
|
|
2359
|
+
if (providerResult.terminalClassification === "provider-limit-reached" &&
|
|
2360
|
+
(provider !== "opencode" || isProviderWideOpenCodeGateResult(providerResult))) {
|
|
2361
|
+
break;
|
|
2362
|
+
}
|
|
2363
|
+
if ((provider === "claude" || provider === "codex") &&
|
|
2364
|
+
providerResult.status === "complete")
|
|
2365
|
+
break;
|
|
2366
|
+
if (provider === "opencode" && providerResult.reviewer === "opencode")
|
|
2367
|
+
break;
|
|
1866
2368
|
}
|
|
1867
|
-
if ((provider === "claude" || provider === "codex") &&
|
|
1868
|
-
providerResult.status === "complete")
|
|
1869
|
-
break;
|
|
1870
|
-
if (provider === "opencode" && providerResult.reviewer === "opencode")
|
|
1871
|
-
break;
|
|
1872
2369
|
}
|
|
1873
2370
|
}
|
|
2371
|
+
finally {
|
|
2372
|
+
cleanupSharedCheckout();
|
|
2373
|
+
}
|
|
2374
|
+
const status = approvalGate.satisfied
|
|
2375
|
+
? "recorded-untracked"
|
|
2376
|
+
: "needs-review";
|
|
2377
|
+
const result = makeResult(approvalGate, status);
|
|
2378
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2379
|
+
persistGateState(projectRoot, result);
|
|
2380
|
+
return result;
|
|
1874
2381
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
2382
|
+
catch (error) {
|
|
2383
|
+
cleanupSharedCheckout();
|
|
2384
|
+
throw error;
|
|
1877
2385
|
}
|
|
1878
|
-
const status = approvalGate.satisfied
|
|
1879
|
-
? "recorded-untracked"
|
|
1880
|
-
: "needs-review";
|
|
1881
|
-
const result = makeResult(approvalGate, status);
|
|
1882
|
-
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1883
|
-
persistGateState(projectRoot, result);
|
|
1884
|
-
return result;
|
|
1885
2386
|
}, projectRoot).catch((error) => {
|
|
1886
2387
|
if (isStateLockHeldError(error)) {
|
|
1887
2388
|
throw new Error(`Review gate ${gateId} is already active for this worktree. Wait for the owner to finish, then retry with --resume ${gateId}; the lock acquisition bound was not increased.`, { cause: error });
|
|
@@ -2243,6 +2744,8 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2243
2744
|
.option("--effort <effort>", "Review effort (medium|high)")
|
|
2244
2745
|
.option("--resume <gate-id>", "Resume a persisted review gate")
|
|
2245
2746
|
.option("--reviewer <name>", "Reviewer id for `review log`")
|
|
2747
|
+
.option("--reason <text>", "Maintainer reason for `review override`")
|
|
2748
|
+
.option("--confirm", "Confirm a `review override` (required)")
|
|
2246
2749
|
.option("--target-kind <kind>", "blueprint | pull-request")
|
|
2247
2750
|
.option("--target-id <id>", "Target slug / PR number / stable review target id")
|
|
2248
2751
|
.option("--artifact <path>", "Committed review transcript/artifact path relative to blueprint (required for promotion-counted approvals)")
|
|
@@ -2290,6 +2793,7 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2290
2793
|
? undefined
|
|
2291
2794
|
: reviewGateNonConvergenceHint(slug, {
|
|
2292
2795
|
gateId: result.gateId,
|
|
2796
|
+
status: result.status,
|
|
2293
2797
|
...(latestFailure?.failureCode
|
|
2294
2798
|
? { failureCode: latestFailure.failureCode }
|
|
2295
2799
|
: {}),
|
|
@@ -2300,6 +2804,9 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2300
2804
|
`${result.status}: ${result.message}`,
|
|
2301
2805
|
`gate: ${result.gateId}`,
|
|
2302
2806
|
`state: ${result.statePath}`,
|
|
2807
|
+
...(latestFailure?.providerErrorDetail
|
|
2808
|
+
? [`provider error: ${latestFailure.providerErrorDetail}`]
|
|
2809
|
+
: []),
|
|
2303
2810
|
...(hint ? [hint] : []),
|
|
2304
2811
|
].join("\n"));
|
|
2305
2812
|
return;
|
|
@@ -2318,6 +2825,16 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2318
2825
|
: `Logged review ${entry.id} -> ${entry.blueprintSlug}`);
|
|
2319
2826
|
return;
|
|
2320
2827
|
}
|
|
2828
|
+
case "override": {
|
|
2829
|
+
const slug = args[0];
|
|
2830
|
+
if (!slug)
|
|
2831
|
+
throw new Error("Usage: wp review override <blueprint-slug> --reason <text> --confirm [--purpose plan|delivery] [--target <git-ref>]");
|
|
2832
|
+
const entry = await runReviewOverride(projectRoot, slug, options);
|
|
2833
|
+
console.log(options.json
|
|
2834
|
+
? JSON.stringify(entry, null, 2)
|
|
2835
|
+
: `Recorded maintainer override ${entry.id} -> ${entry.blueprintSlug}`);
|
|
2836
|
+
return;
|
|
2837
|
+
}
|
|
2321
2838
|
case "read": {
|
|
2322
2839
|
const slug = args[0];
|
|
2323
2840
|
if (!slug)
|
|
@@ -2359,7 +2876,7 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2359
2876
|
}
|
|
2360
2877
|
}
|
|
2361
2878
|
default:
|
|
2362
|
-
throw new Error("Unknown review subcommand. Use one of: gate, run, log, read, scoreboard, availability, doctor");
|
|
2879
|
+
throw new Error("Unknown review subcommand. Use one of: gate, run, log, override, read, scoreboard, availability, doctor");
|
|
2363
2880
|
}
|
|
2364
2881
|
});
|
|
2365
2882
|
}
|