@webpresso/agent-kit 3.1.30 → 3.3.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/README.md +53 -15
- package/bin/_run.js +183 -20
- package/catalog/AGENTS.md.tpl +3 -3
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +28 -1
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/verify/SKILL.md +4 -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 +4 -0
- package/dist/esm/audit/blueprint-trust.js +14 -10
- package/dist/esm/audit/changeset-evidence.d.ts +23 -0
- package/dist/esm/audit/changeset-evidence.js +30 -0
- package/dist/esm/audit/changeset-required.d.ts +10 -0
- package/dist/esm/audit/changeset-required.js +73 -0
- package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
- package/dist/esm/audit/ci-guardrails-detection.js +68 -0
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +80 -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 +13 -1
- 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 +101 -2
- package/dist/esm/blueprint/core/schema.d.ts +1 -0
- package/dist/esm/blueprint/core/schema.js +26 -0
- 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 +87 -20
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- 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 +68 -12
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
- 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/sync/client.d.ts +2 -1
- package/dist/esm/blueprint/sync/client.js +1 -1
- 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/dossier.d.ts +1 -0
- package/dist/esm/blueprint/trust/dossier.js +40 -6
- 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 +74 -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 +15 -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 +43 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
- package/dist/esm/cli/commands/agent-launch.js +93 -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 +21 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
- 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 +13 -1
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +30 -6
- 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/doctor.js +5 -0
- 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.js +2 -2
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +325 -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 +3 -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/package-root.d.ts +7 -0
- package/dist/esm/cli/commands/init/package-root.js +10 -2
- 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 +20 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
- 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 +54 -24
- 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 +288 -202
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -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/opencode-probe.d.ts +107 -0
- package/dist/esm/cli/commands/opencode-probe.js +323 -0
- package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
- package/dist/esm/cli/commands/opencode-rotate.js +147 -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-runner.js +3 -3
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +751 -209
- 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/rust-check.d.ts +24 -0
- package/dist/esm/cli/commands/rust-check.js +110 -0
- 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/typecheck.d.ts +1 -0
- package/dist/esm/cli/commands/typecheck.js +30 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
- package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
- package/dist/esm/cli/commands/worktree/router.js +10 -0
- package/dist/esm/cli/direct-provider-launch.js +9 -5
- 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 +5 -3
- package/dist/esm/hooks/doctor.js +67 -16
- 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 +54 -0
- 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/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.js +6 -1
- 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 +50 -9
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
- 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 +42 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- 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-fetch-and-index.d.ts +1 -1
- 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/package.json +2 -0
- package/dist/esm/platform/client.d.ts +7 -1
- package/dist/esm/platform/github-contract-source.d.ts +2 -1
- package/dist/esm/pr-description/contract.js +24 -4
- package/dist/esm/review/authority.js +27 -14
- package/dist/esm/review/availability.d.ts +33 -0
- package/dist/esm/review/availability.js +122 -20
- 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.d.ts +11 -1
- package/dist/esm/review/execution/adapters.js +36 -10
- package/dist/esm/review/execution/artifacts.d.ts +1 -1
- 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 +121 -21
- package/dist/esm/review/execution/supervisor.js +36 -4
- package/dist/esm/review/execution/types.d.ts +9 -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 +25 -0
- package/dist/esm/review/subject.js +116 -26
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/rust/affected-manifests.d.ts +38 -0
- package/dist/esm/rust/affected-manifests.js +69 -0
- package/dist/esm/session-memory/fetch-index.d.ts +1 -1
- package/dist/esm/status/snapshot.js +32 -25
- 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 -2
- package/dist/esm/test-helpers/global-setup.js +79 -32
- package/dist/esm/typecheck/planner.d.ts +9 -1
- package/dist/esm/typecheck/planner.js +31 -0
- 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/orphan-scan.d.ts +50 -0
- package/dist/esm/worktrees/orphan-scan.js +211 -0
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -13
|
@@ -10,17 +10,23 @@ import { relativeBlueprintSlug, resolveBlueprintFile } from "#lifecycle/local.js
|
|
|
10
10
|
import { readBlueprintOverviewAtRef } from "#lifecycle/review-provenance.js";
|
|
11
11
|
import { scanBlueprintDirectory } from "#service/scanner.js";
|
|
12
12
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
13
|
+
import { assertFolderBlueprintIdentity } from "#utils/document-paths.js";
|
|
13
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";
|
|
14
15
|
import { REVIEW_OUTPUT_MAX_BYTES, REVIEW_OUTPUT_TOO_LARGE, applyReviewOutputPolicy, } from "#review/execution/output-policy.js";
|
|
15
16
|
import { removeOwnedRunningReviewState } from "#review/execution/artifacts.js";
|
|
16
17
|
import { createReviewTargetCheckout, } from "#review/execution/review-checkout.js";
|
|
17
18
|
import { loadReviewProcessRecords, readReviewProcessIdentity, removeReviewProcessRecord, } from "#review/execution/process-registry.js";
|
|
18
|
-
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";
|
|
19
24
|
import { evaluateApprovalGate, evaluateApprovalReviewers, formatApprovalGateRequirement, } from "#lifecycle/audit";
|
|
20
25
|
import { assertReviewRefUnchanged, readReviewAuthorityAtRef, readWorkingReviewEvents, } from "#review/authority.js";
|
|
21
|
-
import { appendReviewEvent, evaluateReviewEvents } from "#review/events.js";
|
|
26
|
+
import { appendReviewEvent, evaluateReviewEvents, MAINTAINER_OVERRIDE_REVIEWER, parseReviewEventLog, } from "#review/events.js";
|
|
22
27
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
23
|
-
import {
|
|
28
|
+
import { verificationArtifactSection, verificationPromptSummary, verifyDeliveryPromotionGates, } from "#review/delivery-verifier.js";
|
|
29
|
+
import { createDeliverySubjectAtRef, createPlanSubjectAtRef, findUnreviewedWorkingTreeChanges, } from "#review/subject.js";
|
|
24
30
|
import { runCommand } from "#mcp/tools/_shared/run-command.js";
|
|
25
31
|
import { NotInGitRepoError, withLock } from "#paths/state-root.js";
|
|
26
32
|
import { writeFileAtomic, writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
|
|
@@ -149,6 +155,18 @@ function defaultReviewModel(provider) {
|
|
|
149
155
|
return GROK_DEFAULT_REVIEW_MODEL;
|
|
150
156
|
return undefined;
|
|
151
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
|
+
}
|
|
152
170
|
function classificationForFailureCode(code) {
|
|
153
171
|
if (!code)
|
|
154
172
|
return undefined;
|
|
@@ -189,9 +207,10 @@ export async function runReviewRunCommand(projectRoot, options, deps = {}) {
|
|
|
189
207
|
if (!options.artifactRoot)
|
|
190
208
|
throw new Error("Missing required --artifact-root.");
|
|
191
209
|
const provider = parseReviewProvider(options.provider);
|
|
192
|
-
const
|
|
193
|
-
if (!
|
|
210
|
+
const requestedModel = options.model?.trim() || defaultReviewModel(provider);
|
|
211
|
+
if (!requestedModel)
|
|
194
212
|
throw new Error(`Missing required --model for ${provider}.`);
|
|
213
|
+
const model = resolveReviewModel(provider, requestedModel);
|
|
195
214
|
const effort = parseReviewEffort(options.effort);
|
|
196
215
|
const stage = parseReviewStage(options.stage);
|
|
197
216
|
const idleSeconds = options.idleSeconds === undefined ? 180 : Number(options.idleSeconds);
|
|
@@ -354,6 +373,13 @@ function reviewsPathForBlueprint(blueprintPath) {
|
|
|
354
373
|
}
|
|
355
374
|
return path.join(path.dirname(blueprintPath), "reviews.md");
|
|
356
375
|
}
|
|
376
|
+
function assertReviewEvidenceCapable(location) {
|
|
377
|
+
assertFolderBlueprintIdentity({
|
|
378
|
+
documentPath: location.path,
|
|
379
|
+
lifecycleSlug: location.slug,
|
|
380
|
+
shape: location.shape,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
357
383
|
function parseLegacyTableEntries(markdown, blueprintSlug, blueprintPath, reviewsPath) {
|
|
358
384
|
const entries = [];
|
|
359
385
|
for (const line of markdown.split("\n")) {
|
|
@@ -688,6 +714,7 @@ async function resolveReviewBlueprint(projectRoot, slug) {
|
|
|
688
714
|
}
|
|
689
715
|
export async function logReviewEntry(projectRoot, slug, input) {
|
|
690
716
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
717
|
+
assertReviewEvidenceCapable(location);
|
|
691
718
|
const reviewsPath = reviewsPathForBlueprint(location.path);
|
|
692
719
|
const timestamp = new Date().toISOString();
|
|
693
720
|
const reviewer = parseReviewerId(input.reviewer);
|
|
@@ -749,6 +776,7 @@ export async function logReviewEntry(projectRoot, slug, input) {
|
|
|
749
776
|
}
|
|
750
777
|
export async function logReviewEvent(projectRoot, slug, input) {
|
|
751
778
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
779
|
+
assertReviewEvidenceCapable(location);
|
|
752
780
|
const stableSlug = relativeBlueprintSlug(location.slug);
|
|
753
781
|
const purpose = normalizeReviewPurpose(input.purpose);
|
|
754
782
|
const targetSha = resolveImmutableSha(projectRoot, input.target);
|
|
@@ -810,6 +838,127 @@ export async function logReviewEvent(projectRoot, slug, input) {
|
|
|
810
838
|
updateReviewCache(projectRoot, scanAllReviewLedgers(projectRoot));
|
|
811
839
|
return event;
|
|
812
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
|
+
}
|
|
813
962
|
function resolveImmutableSha(projectRoot, target) {
|
|
814
963
|
let resolved;
|
|
815
964
|
try {
|
|
@@ -946,8 +1095,15 @@ function dedupeGateAttempts(attempts) {
|
|
|
946
1095
|
return result;
|
|
947
1096
|
}
|
|
948
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
|
+
}
|
|
949
1105
|
return {
|
|
950
|
-
provider
|
|
1106
|
+
provider,
|
|
951
1107
|
...(event.model ? { model: event.model } : {}),
|
|
952
1108
|
...(event.effort ? { effort: event.effort } : {}),
|
|
953
1109
|
reviewer: event.reviewer,
|
|
@@ -999,6 +1155,19 @@ const reviewGateAttemptSchema = z.object({
|
|
|
999
1155
|
retryAfterMs: z.number().optional(),
|
|
1000
1156
|
artifact: z.string().optional(),
|
|
1001
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
|
+
});
|
|
1002
1171
|
const reviewGateStateSchema = z
|
|
1003
1172
|
.object({
|
|
1004
1173
|
schemaVersion: z.literal(2),
|
|
@@ -1011,10 +1180,18 @@ const reviewGateStateSchema = z
|
|
|
1011
1180
|
targetSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u),
|
|
1012
1181
|
baseSha: z.string().regex(/^(?:[0-9a-f]{40}|[0-9a-f]{64})$/u),
|
|
1013
1182
|
promptHash: z.string().length(64),
|
|
1014
|
-
status: z.enum([
|
|
1183
|
+
status: z.enum([
|
|
1184
|
+
"approved",
|
|
1185
|
+
"needs-review",
|
|
1186
|
+
"rejected",
|
|
1187
|
+
"scope-exhausted",
|
|
1188
|
+
"recorded-untracked",
|
|
1189
|
+
"verification-failed",
|
|
1190
|
+
]),
|
|
1015
1191
|
approvalGate: z.object({
|
|
1016
1192
|
distinct: z.number(),
|
|
1017
1193
|
openCodeGoModelReviewers: z.array(z.string()),
|
|
1194
|
+
overrideReviewers: z.array(z.string()),
|
|
1018
1195
|
satisfied: z.boolean(),
|
|
1019
1196
|
strongReviewers: z.array(z.string()),
|
|
1020
1197
|
}),
|
|
@@ -1036,6 +1213,7 @@ const reviewGateStateSchema = z
|
|
|
1036
1213
|
// Optional so resuming a gate state persisted before issue tracking existed
|
|
1037
1214
|
// still validates under .strict().
|
|
1038
1215
|
issues: z.array(z.string()).optional(),
|
|
1216
|
+
verification: reviewGateVerificationSchema.optional(),
|
|
1039
1217
|
message: z.string(),
|
|
1040
1218
|
})
|
|
1041
1219
|
.strict();
|
|
@@ -1077,6 +1255,16 @@ function buildGatePrompt(input) {
|
|
|
1077
1255
|
`Review purpose: ${input.purpose}`,
|
|
1078
1256
|
`Reviewed content subject: ${input.subject.scheme}:${input.subject.digest}`,
|
|
1079
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
|
+
: []),
|
|
1080
1268
|
"Inspect the target SHA and blueprint directly. Focus on correctness, security, concurrency, recovery, tests, and scope compliance.",
|
|
1081
1269
|
"End with exactly one standalone verdict line: APPROVED, APPROVE-WITH-NITS, or BLOCKED.",
|
|
1082
1270
|
"APPROVE-WITH-NITS satisfies the gate only when every finding is non-blocking.",
|
|
@@ -1168,7 +1356,10 @@ function chooseOpenCodeModels(catalog, explicitModel) {
|
|
|
1168
1356
|
return selected;
|
|
1169
1357
|
}
|
|
1170
1358
|
function isProviderWideOpenCodeGateResult(result) {
|
|
1171
|
-
|
|
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";
|
|
1172
1363
|
}
|
|
1173
1364
|
function contaminatedReviewResult(result, changes) {
|
|
1174
1365
|
return {
|
|
@@ -1226,12 +1417,18 @@ function gateFailureFromSummary(summary) {
|
|
|
1226
1417
|
verdict: "no-verdict",
|
|
1227
1418
|
terminalClassification,
|
|
1228
1419
|
...(summary.failureCode ? { failureCode: summary.failureCode } : {}),
|
|
1420
|
+
...(summary.providerErrorDetail ? { providerErrorDetail: summary.providerErrorDetail } : {}),
|
|
1229
1421
|
...(summary.limitWindow ? { limitWindow: summary.limitWindow } : {}),
|
|
1230
1422
|
...(summary.retryAfterMs === undefined ? {} : { retryAfterMs: summary.retryAfterMs }),
|
|
1231
1423
|
};
|
|
1232
1424
|
}
|
|
1233
|
-
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) {
|
|
1234
1430
|
let checkout;
|
|
1431
|
+
const existingCheckout = getSharedCheckout?.();
|
|
1235
1432
|
const ownsCheckout = existingCheckout === undefined;
|
|
1236
1433
|
try {
|
|
1237
1434
|
checkout = existingCheckout ?? createTargetCheckout(input.projectRoot, input.targetSha);
|
|
@@ -1246,7 +1443,8 @@ export async function invokeProvider(input, executeReview = runReviewExecution,
|
|
|
1246
1443
|
// The attempt already fails closed as an isolation failure.
|
|
1247
1444
|
}
|
|
1248
1445
|
}
|
|
1249
|
-
const
|
|
1446
|
+
const requestedModel = input.model ?? defaultReviewModel(input.provider) ?? "unresolved";
|
|
1447
|
+
const model = resolveReviewModel(input.provider, requestedModel);
|
|
1250
1448
|
return {
|
|
1251
1449
|
output: "Unable to create the disposable review checkout; review was not started.",
|
|
1252
1450
|
model,
|
|
@@ -1280,6 +1478,7 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1280
1478
|
cwd: input.executionCwd,
|
|
1281
1479
|
registryRoot: input.projectRoot,
|
|
1282
1480
|
...(input.signal ? { signal: input.signal } : {}),
|
|
1481
|
+
...(input.authDataDir ? { authDataDir: input.authDataDir } : {}),
|
|
1283
1482
|
};
|
|
1284
1483
|
async function probeProvider(args) {
|
|
1285
1484
|
const probe = await executeReview({
|
|
@@ -1302,7 +1501,7 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1302
1501
|
};
|
|
1303
1502
|
}
|
|
1304
1503
|
if (input.provider === "claude") {
|
|
1305
|
-
const model = input.model ?? defaultReviewModel("claude");
|
|
1504
|
+
const model = resolveReviewModel("claude", input.model ?? defaultReviewModel("claude"));
|
|
1306
1505
|
const probeFailure = await probeProvider({ provider: "claude", model, reviewer: "claude" });
|
|
1307
1506
|
if (probeFailure)
|
|
1308
1507
|
return probeFailure;
|
|
@@ -1442,6 +1641,101 @@ async function invokeProviderInCheckout(input, executeReview = runReviewExecutio
|
|
|
1442
1641
|
: gateFailureFromSummary(result)),
|
|
1443
1642
|
};
|
|
1444
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
|
+
}
|
|
1445
1739
|
function currentReviewHost(env = process.env) {
|
|
1446
1740
|
const host = env["WP_REVIEW_HOST"] ?? env["WP_HOOK_HOST"];
|
|
1447
1741
|
return host === "claude" || host === "codex" || host === "opencode" || host === "grok"
|
|
@@ -1524,11 +1818,65 @@ function reviewArtifactRelativePath(input) {
|
|
|
1524
1818
|
return path.posix.join(artifactDirectory, filename);
|
|
1525
1819
|
}
|
|
1526
1820
|
function reviewGateEventId(input) {
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
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
|
+
// A recorded rejection's `reviewedCommit` can be orphaned by a later rebase.
|
|
1857
|
+
// Rebuilding its subject then throws, and this loop had no guard, so the
|
|
1858
|
+
// failure propagated to the caller and took down the whole rejection
|
|
1859
|
+
// lineage. Skip only the unrebuildable event: the remaining rejections still
|
|
1860
|
+
// count toward the scope-exhaustion circuit breaker, which is the
|
|
1861
|
+
// fail-closed direction.
|
|
1862
|
+
let canonical;
|
|
1863
|
+
try {
|
|
1864
|
+
canonical =
|
|
1865
|
+
input.purpose === "plan"
|
|
1866
|
+
? createPlanSubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug)
|
|
1867
|
+
: createDeliverySubjectAtRef(input.projectRoot, event.reviewedCommit, input.stableSlug);
|
|
1868
|
+
}
|
|
1869
|
+
catch {
|
|
1870
|
+
continue;
|
|
1871
|
+
}
|
|
1872
|
+
if (!bySubject.has(canonical.digest)) {
|
|
1873
|
+
bySubject.set(canonical.digest, {
|
|
1874
|
+
artifactPath: event.artifactPath,
|
|
1875
|
+
subjectDigest: canonical.digest,
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
return [...bySubject.values()];
|
|
1532
1880
|
}
|
|
1533
1881
|
/**
|
|
1534
1882
|
* Sanctioned fallback when `wp review gate` does not reach "approved" — points
|
|
@@ -1537,13 +1885,46 @@ function reviewGateEventId(input) {
|
|
|
1537
1885
|
* workaround. See catalog/agent/rules/pre-implementation.md.
|
|
1538
1886
|
*/
|
|
1539
1887
|
export function reviewGateNonConvergenceHint(slug, context = {}) {
|
|
1888
|
+
if (context.status === "scope-exhausted") {
|
|
1889
|
+
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.`;
|
|
1890
|
+
}
|
|
1540
1891
|
if (context.failureCode === "provider-event-too-large" && context.gateId) {
|
|
1541
1892
|
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.`;
|
|
1542
1893
|
}
|
|
1543
1894
|
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.`;
|
|
1544
1895
|
}
|
|
1896
|
+
/**
|
|
1897
|
+
* Guards the invariant this file must never silently violate: a
|
|
1898
|
+
* `wp-git-tree-v1` delivery subject digest is computed purely from the
|
|
1899
|
+
* committed tree at `targetSha` (see createDeliverySubjectAtRef). When that
|
|
1900
|
+
* target is the currently checked-out HEAD — the default and overwhelmingly
|
|
1901
|
+
* common case — any uncommitted non-evidence change in the working tree is
|
|
1902
|
+
* content nobody has reviewed yet. Reporting a "satisfied" verdict in that
|
|
1903
|
+
* state (whether by reusing a prior approval for `targetSha` or by running a
|
|
1904
|
+
* fresh review whose prompt still only sees `targetSha`'s committed content)
|
|
1905
|
+
* would silently ship edits that were never reviewed. Fail closed instead.
|
|
1906
|
+
*
|
|
1907
|
+
* Plan purpose is intentionally exempt: its `wp-blueprint-v1` digest is
|
|
1908
|
+
* scoped to a single file and already neutralizes lifecycle bookkeeping by
|
|
1909
|
+
* design (see normalizePlanSubjectContent), so uncommitted lifecycle moves
|
|
1910
|
+
* are expected to keep reusing an existing plan approval.
|
|
1911
|
+
*/
|
|
1912
|
+
function assertWorkingTreeMatchesDeliveryTarget(projectRoot, purpose, targetSha, baseSha) {
|
|
1913
|
+
if (purpose !== "delivery" || targetSha !== baseSha)
|
|
1914
|
+
return;
|
|
1915
|
+
const gitRoot = resolveReviewGitToplevel(projectRoot);
|
|
1916
|
+
const divergentPaths = findUnreviewedWorkingTreeChanges(gitRoot);
|
|
1917
|
+
if (divergentPaths.length === 0)
|
|
1918
|
+
return;
|
|
1919
|
+
throw new Error(`Review target ${targetSha} is the current HEAD, but the working tree has uncommitted ` +
|
|
1920
|
+
`changes not reflected in that reviewed commit: ${divergentPaths.join(", ")}. A ` +
|
|
1921
|
+
`wp-git-tree-v1 approval bound to ${targetSha} does not cover these edits. Commit (or ` +
|
|
1922
|
+
`discard) the changes so the reviewed content matches what will actually ship, then ` +
|
|
1923
|
+
`re-run "wp review gate".`);
|
|
1924
|
+
}
|
|
1545
1925
|
export async function runReviewGate(projectRoot, slug, input, dependencies = { invokeProvider }) {
|
|
1546
1926
|
const location = await resolveReviewBlueprint(projectRoot, slug);
|
|
1927
|
+
assertReviewEvidenceCapable(location);
|
|
1547
1928
|
if (lstatSync(location.path).isSymbolicLink()) {
|
|
1548
1929
|
throw new Error(`Blueprint overview must not be a symlink: ${location.path}`);
|
|
1549
1930
|
}
|
|
@@ -1557,6 +1938,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1557
1938
|
const subject = purpose === "plan"
|
|
1558
1939
|
? createPlanSubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath)
|
|
1559
1940
|
: createDeliverySubjectAtRef(projectRoot, targetSha, stableSlug, repositoryBlueprintPath);
|
|
1941
|
+
assertWorkingTreeMatchesDeliveryTarget(projectRoot, purpose, targetSha, baseSha);
|
|
1560
1942
|
const promptHash = hashPrompt({
|
|
1561
1943
|
blueprintContent,
|
|
1562
1944
|
blueprintSlug: stableSlug,
|
|
@@ -1575,13 +1957,16 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1575
1957
|
}
|
|
1576
1958
|
const explicitProvider = normalizeGateProvider(input.provider);
|
|
1577
1959
|
const effort = normalizeGateEffort(input.effort);
|
|
1578
|
-
const
|
|
1960
|
+
const renderPrompt = (verificationSummary, priorRejections) => buildGatePrompt({
|
|
1579
1961
|
blueprintPath: projectBlueprintPath,
|
|
1580
1962
|
blueprintSlug: stableSlug,
|
|
1581
1963
|
purpose,
|
|
1582
1964
|
subject,
|
|
1583
1965
|
targetSha,
|
|
1966
|
+
...(verificationSummary ? { verificationSummary } : {}),
|
|
1967
|
+
...(priorRejections ? { priorRejections } : {}),
|
|
1584
1968
|
});
|
|
1969
|
+
let prompt = renderPrompt();
|
|
1585
1970
|
return withLock("worktree", async () => {
|
|
1586
1971
|
const resumed = loadGateState(projectRoot, gateId, {
|
|
1587
1972
|
purpose,
|
|
@@ -1594,17 +1979,32 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1594
1979
|
resumed.subjectDigest !== subject.digest)) {
|
|
1595
1980
|
throw new Error(`Gate ${gateId} does not match target ${targetSha} and prompt ${promptHash}.`);
|
|
1596
1981
|
}
|
|
1597
|
-
let reusedEvents = [];
|
|
1598
1982
|
const gateIssues = [];
|
|
1983
|
+
let lineageFailure;
|
|
1984
|
+
let rejectedSubjects = [];
|
|
1985
|
+
let reusedApprovals = [];
|
|
1599
1986
|
try {
|
|
1600
1987
|
const reusedAuthority = readReviewAuthorityAtRef(projectRoot, targetSha, stableSlug, purpose);
|
|
1601
|
-
|
|
1988
|
+
reusedApprovals = reusedAuthority.approvals;
|
|
1602
1989
|
gateIssues.push(...reusedAuthority.issues);
|
|
1990
|
+
const formalRejectionIds = new Set(reusedAuthority.events.filter(isFormalGateRejection).map((event) => event.id));
|
|
1991
|
+
const compromisedRejection = reusedAuthority.issues.find((issue) => [...formalRejectionIds].some((eventId) => issue.includes(`(${eventId})`)));
|
|
1992
|
+
if (compromisedRejection) {
|
|
1993
|
+
lineageFailure = `Formal rejection evidence failed integrity verification: ${compromisedRejection}`;
|
|
1994
|
+
}
|
|
1995
|
+
else {
|
|
1996
|
+
rejectedSubjects = rejectedReviewSubjects({
|
|
1997
|
+
events: reusedAuthority.events,
|
|
1998
|
+
projectRoot,
|
|
1999
|
+
purpose,
|
|
2000
|
+
stableSlug,
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
1603
2003
|
}
|
|
1604
|
-
catch {
|
|
1605
|
-
|
|
2004
|
+
catch (error) {
|
|
2005
|
+
lineageFailure = `Review lineage authority is unreadable: ${error instanceof Error ? error.message : String(error)}`;
|
|
2006
|
+
gateIssues.push(lineageFailure);
|
|
1606
2007
|
}
|
|
1607
|
-
const reusedApprovals = reusedEvents;
|
|
1608
2008
|
const statePath = reviewGateStatePath(projectRoot, gateId);
|
|
1609
2009
|
const reusedSummary = reusedApprovals.map((entry) => ({
|
|
1610
2010
|
reviewer: entry.reviewer,
|
|
@@ -1614,7 +2014,9 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1614
2014
|
targetHash: entry.subjectDigest,
|
|
1615
2015
|
rev: entry.id,
|
|
1616
2016
|
}));
|
|
1617
|
-
const workingAtStart =
|
|
2017
|
+
const workingAtStart = lineageFailure
|
|
2018
|
+
? { events: [], compromisedIds: new Set(), issues: [] }
|
|
2019
|
+
: readWorkingReviewEvents(location.path, stableSlug);
|
|
1618
2020
|
gateIssues.push(...workingAtStart.issues);
|
|
1619
2021
|
const durableGateEvents = workingAtStart.events.filter((event) => event.gateId === gateId &&
|
|
1620
2022
|
event.promptHash === promptHash &&
|
|
@@ -1627,6 +2029,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1627
2029
|
...(resumed?.attempts ?? []),
|
|
1628
2030
|
...durableGateEvents.map(gateAttemptFromEvent),
|
|
1629
2031
|
]);
|
|
2032
|
+
let verification;
|
|
1630
2033
|
let publicationProgress = resumed?.publicationProgress ??
|
|
1631
2034
|
(durableGateEvents.length > 0
|
|
1632
2035
|
? { artifact: true, ledger: true, frontmatter: false }
|
|
@@ -1645,7 +2048,7 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1645
2048
|
}, working.compromisedIds);
|
|
1646
2049
|
return evaluateApprovalReviewers(evaluation.approvals.map((entry) => entry.reviewer));
|
|
1647
2050
|
};
|
|
1648
|
-
const makeResult = (approvalGate, status) => ({
|
|
2051
|
+
const makeResult = (approvalGate, status, messageOverride) => ({
|
|
1649
2052
|
schemaVersion: 2,
|
|
1650
2053
|
gateId,
|
|
1651
2054
|
purpose,
|
|
@@ -1663,222 +2066,343 @@ export async function runReviewGate(projectRoot, slug, input, dependencies = { i
|
|
|
1663
2066
|
publicationProgress,
|
|
1664
2067
|
reusedApprovals: reusedSummary,
|
|
1665
2068
|
...(gateIssues.length > 0 ? { issues: [...gateIssues] } : {}),
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
2069
|
+
...(verification ? { verification } : {}),
|
|
2070
|
+
message: messageOverride ??
|
|
2071
|
+
`${approvalGate.satisfied
|
|
2072
|
+
? `Approval gate satisfied (${formatApprovalGateRequirement(approvalGate)}).`
|
|
2073
|
+
: `Approval gate not satisfied (${formatApprovalGateRequirement(approvalGate)}).`}${gateIssues.length > 0 ? ` Warnings: ${gateIssues.join("; ")}` : ""}`,
|
|
1669
2074
|
});
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
const result = makeResult(approvalGate, reusedApprovals.length > 0 ? "approved" : "recorded-untracked");
|
|
1673
|
-
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1674
|
-
persistGateState(projectRoot, result);
|
|
1675
|
-
return result;
|
|
1676
|
-
}
|
|
1677
|
-
if (attempts.some((attempt) => attempt.status === "complete" && attempt.verdict === "reject")) {
|
|
1678
|
-
const result = makeResult(approvalGate, "rejected");
|
|
2075
|
+
if (lineageFailure) {
|
|
2076
|
+
const result = makeResult(evaluateApprovalReviewers([]), "verification-failed", `${lineageFailure} No review provider was invoked.`);
|
|
1679
2077
|
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1680
2078
|
persistGateState(projectRoot, result);
|
|
1681
2079
|
return result;
|
|
1682
2080
|
}
|
|
1683
|
-
const artifactDirectory = path.join(path.dirname(location.path), "review-artifacts");
|
|
1684
|
-
if (existsSync(artifactDirectory) && lstatSync(artifactDirectory).isSymbolicLink()) {
|
|
1685
|
-
throw new Error(`Review artifact directory must not be a symlink: ${artifactDirectory}`);
|
|
1686
|
-
}
|
|
1687
|
-
const availabilityPrune = pruneExpiredAvailabilitySuppressions(loadReviewAvailability(projectRoot));
|
|
1688
|
-
if (availabilityPrune.pruned.length > 0) {
|
|
1689
|
-
saveReviewAvailability(projectRoot, availabilityPrune.state);
|
|
1690
|
-
}
|
|
1691
|
-
const availability = availabilityPrune.state;
|
|
1692
|
-
const providerQueue = selectGateProviders({
|
|
1693
|
-
availability,
|
|
1694
|
-
...(explicitProvider ? { explicitProvider } : {}),
|
|
1695
|
-
});
|
|
1696
|
-
const usedReviewers = new Set([
|
|
1697
|
-
...attempts
|
|
1698
|
-
.filter((attempt) => !isRetryableGateAttempt(attempt))
|
|
1699
|
-
.map((attempt) => attempt.reviewer)
|
|
1700
|
-
.filter((value) => Boolean(value)),
|
|
1701
|
-
...suppressedReviewers(availability),
|
|
1702
|
-
]);
|
|
1703
2081
|
const checkoutFactory = dependencies.createTargetCheckout ?? createReviewTargetCheckout;
|
|
1704
2082
|
let sharedCheckout;
|
|
1705
2083
|
let checkoutSetupFailed = false;
|
|
1706
|
-
|
|
2084
|
+
// Deferred: a review checkout costs a full `git archive` + tar tree copy
|
|
2085
|
+
// plus status fingerprints, so it must not be created on gate calls that
|
|
2086
|
+
// never need one (plan-purpose reuse of an existing approval, terminal
|
|
2087
|
+
// rejections, exhausted scope). A setup failure is remembered so provider
|
|
2088
|
+
// attempts still surface the same isolation-failure evidence.
|
|
2089
|
+
const ensureSharedCheckout = () => {
|
|
2090
|
+
if (sharedCheckout !== undefined || checkoutSetupFailed)
|
|
2091
|
+
return sharedCheckout;
|
|
1707
2092
|
try {
|
|
1708
2093
|
sharedCheckout = checkoutFactory(projectRoot, targetSha);
|
|
1709
2094
|
}
|
|
1710
2095
|
catch {
|
|
1711
2096
|
checkoutSetupFailed = true;
|
|
1712
2097
|
}
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
2098
|
+
return sharedCheckout;
|
|
2099
|
+
};
|
|
2100
|
+
const createCheckoutForAttempt = () => {
|
|
2101
|
+
// Only reachable when ensureSharedCheckout() already failed — a live
|
|
2102
|
+
// shared checkout is always passed to the attempt as existingCheckout.
|
|
2103
|
+
throw new Error("review checkout setup failed");
|
|
2104
|
+
};
|
|
2105
|
+
let sharedCheckoutCleaned = false;
|
|
2106
|
+
const cleanupSharedCheckout = () => {
|
|
2107
|
+
if (sharedCheckoutCleaned)
|
|
2108
|
+
return;
|
|
2109
|
+
sharedCheckoutCleaned = true;
|
|
2110
|
+
sharedCheckout?.cleanup();
|
|
2111
|
+
};
|
|
1719
2112
|
try {
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
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
|
-
|
|
2113
|
+
// Only the delivery purpose runs checkout-backed gate verification
|
|
2114
|
+
// (verifyDeliveryPromotionGates is a no-op for every other purpose).
|
|
2115
|
+
const deliveryCheckout = purpose === "delivery" ? ensureSharedCheckout() : undefined;
|
|
2116
|
+
if (deliveryCheckout) {
|
|
2117
|
+
verification = verifyDeliveryPromotionGates({
|
|
2118
|
+
purpose,
|
|
2119
|
+
blueprintContent,
|
|
2120
|
+
projectRoot,
|
|
2121
|
+
gateId,
|
|
2122
|
+
checkout: deliveryCheckout,
|
|
2123
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
2124
|
+
});
|
|
2125
|
+
const summary = verificationPromptSummary(verification);
|
|
2126
|
+
if (summary) {
|
|
2127
|
+
prompt = renderPrompt(summary);
|
|
2128
|
+
}
|
|
2129
|
+
if (verification?.status === "failed") {
|
|
2130
|
+
const result = makeResult(currentApprovalGate(), "verification-failed");
|
|
2131
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2132
|
+
persistGateState(projectRoot, result);
|
|
2133
|
+
cleanupSharedCheckout();
|
|
2134
|
+
return result;
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
let approvalGate = currentApprovalGate();
|
|
2138
|
+
if (approvalGate.satisfied) {
|
|
2139
|
+
const result = makeResult(approvalGate, reusedApprovals.length > 0 ? "approved" : "recorded-untracked");
|
|
2140
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2141
|
+
persistGateState(projectRoot, result);
|
|
2142
|
+
cleanupSharedCheckout();
|
|
2143
|
+
return result;
|
|
2144
|
+
}
|
|
2145
|
+
if (attempts.some((attempt) => attempt.status === "complete" && attempt.verdict === "reject")) {
|
|
2146
|
+
const result = makeResult(approvalGate, "rejected");
|
|
2147
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2148
|
+
persistGateState(projectRoot, result);
|
|
2149
|
+
cleanupSharedCheckout();
|
|
2150
|
+
return result;
|
|
2151
|
+
}
|
|
2152
|
+
const currentSubjectRejection = rejectedSubjects.find((entry) => entry.subjectDigest === subject.digest);
|
|
2153
|
+
if (currentSubjectRejection) {
|
|
2154
|
+
const result = makeResult(approvalGate, "rejected", `This semantic review subject was already rejected (${currentSubjectRejection.artifactPath}). No review provider was invoked.`);
|
|
2155
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2156
|
+
persistGateState(projectRoot, result);
|
|
2157
|
+
cleanupSharedCheckout();
|
|
2158
|
+
return result;
|
|
2159
|
+
}
|
|
2160
|
+
if (rejectedSubjects.length >= 2) {
|
|
2161
|
+
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.");
|
|
2162
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2163
|
+
persistGateState(projectRoot, result);
|
|
2164
|
+
cleanupSharedCheckout();
|
|
2165
|
+
return result;
|
|
2166
|
+
}
|
|
2167
|
+
if (rejectedSubjects.length > 0) {
|
|
2168
|
+
prompt = renderPrompt(verification ? verificationPromptSummary(verification) : undefined, rejectedSubjects.slice(0, 2));
|
|
2169
|
+
}
|
|
2170
|
+
const artifactDirectory = path.join(path.dirname(location.path), "review-artifacts");
|
|
2171
|
+
if (existsSync(artifactDirectory) && lstatSync(artifactDirectory).isSymbolicLink()) {
|
|
2172
|
+
throw new Error(`Review artifact directory must not be a symlink: ${artifactDirectory}`);
|
|
2173
|
+
}
|
|
2174
|
+
const availabilityPrune = pruneExpiredAvailabilitySuppressions(loadReviewAvailability(projectRoot));
|
|
2175
|
+
if (availabilityPrune.pruned.length > 0) {
|
|
2176
|
+
saveReviewAvailability(projectRoot, availabilityPrune.state);
|
|
2177
|
+
}
|
|
2178
|
+
const availability = availabilityPrune.state;
|
|
2179
|
+
const providerQueue = selectGateProviders({
|
|
2180
|
+
availability,
|
|
2181
|
+
...(explicitProvider ? { explicitProvider } : {}),
|
|
2182
|
+
});
|
|
2183
|
+
const usedReviewers = new Set([
|
|
2184
|
+
...attempts
|
|
2185
|
+
.filter((attempt) => !isRetryableGateAttempt(attempt))
|
|
2186
|
+
.map((attempt) => attempt.reviewer)
|
|
2187
|
+
.filter((value) => Boolean(value)),
|
|
2188
|
+
...suppressedReviewers(availability),
|
|
2189
|
+
]);
|
|
2190
|
+
// Prepare opencode-go multi-account failover only when opencode is
|
|
2191
|
+
// actually queued (avoids a needless secret fetch otherwise). Resolves
|
|
2192
|
+
// Doppler accounts, materializes the freshest non-cooled account's
|
|
2193
|
+
// isolated auth store, and hands the loop its XDG_DATA_HOME. Degrades to
|
|
2194
|
+
// undefined (today's global-auth behavior) when unprovisioned.
|
|
2195
|
+
const opencodeFailover = providerQueue.includes("opencode")
|
|
2196
|
+
? createOpencodeFailoverController({
|
|
2197
|
+
projectRoot,
|
|
2198
|
+
resolveAccounts: dependencies.resolveOpencodeAccounts ?? resolveOpencodeAccountsGracefully,
|
|
2199
|
+
prepareCredential: dependencies.prepareOpencodeCredential ?? prepareOpencodeCredentialDefault,
|
|
2200
|
+
})
|
|
2201
|
+
: undefined;
|
|
2202
|
+
try {
|
|
2203
|
+
reviewAttempts: for (const provider of providerQueue) {
|
|
2204
|
+
const maxAttempts = provider === "claude" ||
|
|
2205
|
+
provider === "codex" ||
|
|
2206
|
+
provider === "grok" ||
|
|
2207
|
+
(provider === explicitProvider && input.model)
|
|
2208
|
+
? 1
|
|
2209
|
+
: OPENCODE_FAMILY_ORDER.length;
|
|
2210
|
+
for (let index = 0; index < maxAttempts && !approvalGate.satisfied; index += 1) {
|
|
2211
|
+
if (dependencies.signal?.aborted)
|
|
2212
|
+
break reviewAttempts;
|
|
2213
|
+
const opencodeAuthDataDir = provider === "opencode" ? opencodeFailover?.authDataDir() : undefined;
|
|
2214
|
+
const providerResult = enforceReviewPublicationOutput(await (dependencies.invokeProvider ?? invokeProvider)({
|
|
2215
|
+
provider,
|
|
1749
2216
|
projectRoot,
|
|
1750
|
-
|
|
1751
|
-
|
|
2217
|
+
prompt,
|
|
2218
|
+
effort,
|
|
2219
|
+
...(provider === explicitProvider && input.model ? { model: input.model } : {}),
|
|
2220
|
+
excludedReviewers: usedReviewers,
|
|
2221
|
+
targetSha,
|
|
2222
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
2223
|
+
...(opencodeAuthDataDir ? { authDataDir: opencodeAuthDataDir } : {}),
|
|
2224
|
+
}, dependencies.runReviewExecution, dependencies.runCatalogCommand, createCheckoutForAttempt, ensureSharedCheckout));
|
|
2225
|
+
if (providerResult.reviewer !== "opencode")
|
|
2226
|
+
usedReviewers.add(providerResult.reviewer);
|
|
2227
|
+
const providerStage = gateAttemptStage(providerResult.stage ?? "review");
|
|
2228
|
+
let attemptOrdinal = nextGateAttemptOrdinal(attempts, provider, providerResult.model, providerStage);
|
|
2229
|
+
let artifactRelative;
|
|
2230
|
+
let artifactAbsolute;
|
|
2231
|
+
let rev;
|
|
2232
|
+
while (true) {
|
|
2233
|
+
artifactRelative = reviewArtifactRelativePath({
|
|
2234
|
+
projectRoot,
|
|
2235
|
+
blueprintPath: location.path,
|
|
2236
|
+
gateId,
|
|
2237
|
+
provider,
|
|
2238
|
+
reviewer: providerResult.reviewer,
|
|
2239
|
+
stage: providerStage,
|
|
2240
|
+
ordinal: attemptOrdinal,
|
|
2241
|
+
});
|
|
2242
|
+
artifactAbsolute = safeArtifactPath(location.path, artifactRelative);
|
|
2243
|
+
rev = reviewGateEventId({
|
|
2244
|
+
gateId,
|
|
2245
|
+
provider,
|
|
2246
|
+
model: providerResult.model,
|
|
2247
|
+
stage: providerStage,
|
|
2248
|
+
ordinal: attemptOrdinal,
|
|
2249
|
+
});
|
|
2250
|
+
if (!existsSync(artifactAbsolute) &&
|
|
2251
|
+
!gateArtifactPaths.has(artifactRelative) &&
|
|
2252
|
+
!gateEventIds.has(rev)) {
|
|
2253
|
+
break;
|
|
2254
|
+
}
|
|
2255
|
+
attemptOrdinal += 1;
|
|
2256
|
+
}
|
|
2257
|
+
const persistedOrdinal = providerStage === "approval-gate" && attemptOrdinal === 1
|
|
2258
|
+
? undefined
|
|
2259
|
+
: attemptOrdinal;
|
|
2260
|
+
const attempt = {
|
|
2261
|
+
provider,
|
|
2262
|
+
model: providerResult.model,
|
|
2263
|
+
effort,
|
|
2264
|
+
reviewer: providerResult.reviewer,
|
|
2265
|
+
promptHash,
|
|
2266
|
+
...(persistedOrdinal === undefined ? {} : { ordinal: persistedOrdinal }),
|
|
2267
|
+
stage: providerStage,
|
|
2268
|
+
status: providerResult.status,
|
|
2269
|
+
verdict: providerResult.verdict,
|
|
2270
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2271
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2272
|
+
...(providerResult.providerErrorDetail
|
|
2273
|
+
? { providerErrorDetail: providerResult.providerErrorDetail }
|
|
2274
|
+
: {}),
|
|
2275
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2276
|
+
...(providerResult.retryAfterMs === undefined
|
|
2277
|
+
? {}
|
|
2278
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2279
|
+
artifact: artifactRelative,
|
|
2280
|
+
};
|
|
2281
|
+
attempts = dedupeGateAttempts([...attempts, attempt]);
|
|
2282
|
+
recordReviewAvailability(projectRoot, {
|
|
1752
2283
|
provider,
|
|
1753
2284
|
reviewer: providerResult.reviewer,
|
|
2285
|
+
model: providerResult.model,
|
|
2286
|
+
effort,
|
|
1754
2287
|
stage: providerStage,
|
|
1755
|
-
|
|
2288
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2289
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2290
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2291
|
+
...(providerResult.retryAfterMs === undefined
|
|
2292
|
+
? {}
|
|
2293
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1756
2294
|
});
|
|
1757
|
-
|
|
1758
|
-
|
|
2295
|
+
const artifactContent = Buffer.from(`${verificationArtifactSection(verification)}${providerResult.output}`, "utf8");
|
|
2296
|
+
const event = {
|
|
2297
|
+
schemaVersion: 2,
|
|
2298
|
+
id: rev,
|
|
2299
|
+
purpose,
|
|
2300
|
+
blueprintSlug: stableSlug,
|
|
2301
|
+
subjectScheme: subject.scheme,
|
|
2302
|
+
subjectDigest: subject.digest,
|
|
2303
|
+
reviewedCommit: targetSha,
|
|
1759
2304
|
gateId,
|
|
2305
|
+
promptHash,
|
|
2306
|
+
reviewer: providerResult.reviewer,
|
|
1760
2307
|
provider,
|
|
1761
2308
|
model: providerResult.model,
|
|
2309
|
+
effort,
|
|
1762
2310
|
stage: providerStage,
|
|
1763
|
-
|
|
2311
|
+
...(persistedOrdinal === undefined ? {} : { attemptOrdinal: persistedOrdinal }),
|
|
2312
|
+
status: providerResult.status,
|
|
2313
|
+
verdict: providerResult.verdict,
|
|
2314
|
+
terminalClassification: providerResult.terminalClassification,
|
|
2315
|
+
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
2316
|
+
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
2317
|
+
...(providerResult.retryAfterMs === undefined
|
|
2318
|
+
? {}
|
|
2319
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2320
|
+
timestamp: new Date().toISOString(),
|
|
2321
|
+
artifactPath: artifactRelative,
|
|
2322
|
+
artifactSha256: createHash("sha256").update(artifactContent).digest("hex"),
|
|
2323
|
+
};
|
|
2324
|
+
appendReviewEvent({
|
|
2325
|
+
ledgerPath: path.join(path.dirname(location.path), "review-events.jsonl"),
|
|
2326
|
+
event,
|
|
2327
|
+
artifact: { path: artifactAbsolute, content: artifactContent },
|
|
1764
2328
|
});
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
2329
|
+
gateEventIds.add(rev);
|
|
2330
|
+
gateArtifactPaths.add(artifactRelative);
|
|
2331
|
+
publicationProgress = { artifact: true, ledger: true, frontmatter: false };
|
|
2332
|
+
approvalGate = currentApprovalGate();
|
|
2333
|
+
const interimStatus = providerResult.verdict === "reject"
|
|
2334
|
+
? "rejected"
|
|
2335
|
+
: approvalGate.satisfied
|
|
2336
|
+
? "recorded-untracked"
|
|
2337
|
+
: "needs-review";
|
|
2338
|
+
persistGateState(projectRoot, makeResult(approvalGate, interimStatus));
|
|
2339
|
+
if (providerResult.verdict === "reject") {
|
|
2340
|
+
return makeResult(approvalGate, "rejected");
|
|
2341
|
+
}
|
|
2342
|
+
if (providerResult.failureCode === "review-worktree-contaminated" ||
|
|
2343
|
+
providerResult.failureCode === "review-isolation-failure") {
|
|
2344
|
+
break reviewAttempts;
|
|
2345
|
+
}
|
|
2346
|
+
if (dependencies.signal?.aborted)
|
|
2347
|
+
break reviewAttempts;
|
|
2348
|
+
if (provider === "opencode" &&
|
|
2349
|
+
opencodeFailover &&
|
|
2350
|
+
providerResult.terminalClassification === "provider-limit-reached" &&
|
|
2351
|
+
providerResult.failureCode === "opencode-go-usage-limit") {
|
|
2352
|
+
const outcome = await opencodeFailover.onUsageLimit({
|
|
2353
|
+
effort,
|
|
2354
|
+
stage: providerStage,
|
|
2355
|
+
...(providerResult.limitWindow
|
|
2356
|
+
? { limitWindow: providerResult.limitWindow }
|
|
2357
|
+
: {}),
|
|
2358
|
+
...(providerResult.retryAfterMs === undefined
|
|
2359
|
+
? {}
|
|
2360
|
+
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
2361
|
+
});
|
|
2362
|
+
if (outcome === "retry") {
|
|
2363
|
+
// The family failed only because its ACCOUNT was capped. Make it
|
|
2364
|
+
// eligible again and retry under the next account without
|
|
2365
|
+
// consuming a family-rotation slot.
|
|
2366
|
+
usedReviewers.delete(providerResult.reviewer);
|
|
2367
|
+
index -= 1;
|
|
2368
|
+
continue;
|
|
2369
|
+
}
|
|
1768
2370
|
break;
|
|
1769
2371
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1787
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1788
|
-
...(providerResult.retryAfterMs === undefined
|
|
1789
|
-
? {}
|
|
1790
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1791
|
-
artifact: artifactRelative,
|
|
1792
|
-
};
|
|
1793
|
-
attempts = dedupeGateAttempts([...attempts, attempt]);
|
|
1794
|
-
recordReviewAvailability(projectRoot, {
|
|
1795
|
-
provider,
|
|
1796
|
-
reviewer: providerResult.reviewer,
|
|
1797
|
-
model: providerResult.model,
|
|
1798
|
-
effort,
|
|
1799
|
-
stage: providerStage,
|
|
1800
|
-
terminalClassification: providerResult.terminalClassification,
|
|
1801
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1802
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1803
|
-
...(providerResult.retryAfterMs === undefined
|
|
1804
|
-
? {}
|
|
1805
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1806
|
-
});
|
|
1807
|
-
const artifactContent = Buffer.from(providerResult.output, "utf8");
|
|
1808
|
-
const event = {
|
|
1809
|
-
schemaVersion: 2,
|
|
1810
|
-
id: rev,
|
|
1811
|
-
purpose,
|
|
1812
|
-
blueprintSlug: stableSlug,
|
|
1813
|
-
subjectScheme: subject.scheme,
|
|
1814
|
-
subjectDigest: subject.digest,
|
|
1815
|
-
reviewedCommit: targetSha,
|
|
1816
|
-
gateId,
|
|
1817
|
-
promptHash,
|
|
1818
|
-
reviewer: providerResult.reviewer,
|
|
1819
|
-
provider,
|
|
1820
|
-
model: providerResult.model,
|
|
1821
|
-
effort,
|
|
1822
|
-
stage: providerStage,
|
|
1823
|
-
...(persistedOrdinal === undefined ? {} : { attemptOrdinal: persistedOrdinal }),
|
|
1824
|
-
status: providerResult.status,
|
|
1825
|
-
verdict: providerResult.verdict,
|
|
1826
|
-
terminalClassification: providerResult.terminalClassification,
|
|
1827
|
-
...(providerResult.failureCode ? { failureCode: providerResult.failureCode } : {}),
|
|
1828
|
-
...(providerResult.limitWindow ? { limitWindow: providerResult.limitWindow } : {}),
|
|
1829
|
-
...(providerResult.retryAfterMs === undefined
|
|
1830
|
-
? {}
|
|
1831
|
-
: { retryAfterMs: providerResult.retryAfterMs }),
|
|
1832
|
-
timestamp: new Date().toISOString(),
|
|
1833
|
-
artifactPath: artifactRelative,
|
|
1834
|
-
artifactSha256: createHash("sha256").update(artifactContent).digest("hex"),
|
|
1835
|
-
};
|
|
1836
|
-
appendReviewEvent({
|
|
1837
|
-
ledgerPath: path.join(path.dirname(location.path), "review-events.jsonl"),
|
|
1838
|
-
event,
|
|
1839
|
-
artifact: { path: artifactAbsolute, content: artifactContent },
|
|
1840
|
-
});
|
|
1841
|
-
gateEventIds.add(rev);
|
|
1842
|
-
gateArtifactPaths.add(artifactRelative);
|
|
1843
|
-
publicationProgress = { artifact: true, ledger: true, frontmatter: false };
|
|
1844
|
-
approvalGate = currentApprovalGate();
|
|
1845
|
-
const interimStatus = providerResult.verdict === "reject"
|
|
1846
|
-
? "rejected"
|
|
1847
|
-
: approvalGate.satisfied
|
|
1848
|
-
? "recorded-untracked"
|
|
1849
|
-
: "needs-review";
|
|
1850
|
-
persistGateState(projectRoot, makeResult(approvalGate, interimStatus));
|
|
1851
|
-
if (providerResult.verdict === "reject") {
|
|
1852
|
-
return makeResult(approvalGate, "rejected");
|
|
1853
|
-
}
|
|
1854
|
-
if (providerResult.failureCode === "review-worktree-contaminated" ||
|
|
1855
|
-
providerResult.failureCode === "review-isolation-failure") {
|
|
1856
|
-
break reviewAttempts;
|
|
1857
|
-
}
|
|
1858
|
-
if (dependencies.signal?.aborted)
|
|
1859
|
-
break reviewAttempts;
|
|
1860
|
-
if (providerResult.terminalClassification === "provider-limit-reached" &&
|
|
1861
|
-
(provider !== "opencode" || isProviderWideOpenCodeGateResult(providerResult))) {
|
|
1862
|
-
break;
|
|
2372
|
+
// OpenCode family-wide stop: `isProviderWideOpenCodeGateResult`
|
|
2373
|
+
// is exactly `reviewer === "opencode"`, so the final branch below
|
|
2374
|
+
// already covers provider-limit-reached for that case. Keep the
|
|
2375
|
+
// provider-limit break only for non-opencode providers here.
|
|
2376
|
+
if (providerResult.terminalClassification === "provider-limit-reached" &&
|
|
2377
|
+
provider !== "opencode") {
|
|
2378
|
+
break;
|
|
2379
|
+
}
|
|
2380
|
+
if ((provider === "claude" || provider === "codex") &&
|
|
2381
|
+
providerResult.status === "complete")
|
|
2382
|
+
break;
|
|
2383
|
+
// Also stops the opencode family on any result from the
|
|
2384
|
+
// family-agnostic "opencode" reviewer (complete, limit, or
|
|
2385
|
+
// other failure) once per-account failover has already run.
|
|
2386
|
+
if (provider === "opencode" && isProviderWideOpenCodeGateResult(providerResult))
|
|
2387
|
+
break;
|
|
1863
2388
|
}
|
|
1864
|
-
if ((provider === "claude" || provider === "codex") &&
|
|
1865
|
-
providerResult.status === "complete")
|
|
1866
|
-
break;
|
|
1867
|
-
if (provider === "opencode" && providerResult.reviewer === "opencode")
|
|
1868
|
-
break;
|
|
1869
2389
|
}
|
|
1870
2390
|
}
|
|
2391
|
+
finally {
|
|
2392
|
+
cleanupSharedCheckout();
|
|
2393
|
+
}
|
|
2394
|
+
const status = approvalGate.satisfied
|
|
2395
|
+
? "recorded-untracked"
|
|
2396
|
+
: "needs-review";
|
|
2397
|
+
const result = makeResult(approvalGate, status);
|
|
2398
|
+
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
2399
|
+
persistGateState(projectRoot, result);
|
|
2400
|
+
return result;
|
|
1871
2401
|
}
|
|
1872
|
-
|
|
1873
|
-
|
|
2402
|
+
catch (error) {
|
|
2403
|
+
cleanupSharedCheckout();
|
|
2404
|
+
throw error;
|
|
1874
2405
|
}
|
|
1875
|
-
const status = approvalGate.satisfied
|
|
1876
|
-
? "recorded-untracked"
|
|
1877
|
-
: "needs-review";
|
|
1878
|
-
const result = makeResult(approvalGate, status);
|
|
1879
|
-
assertReviewRefUnchanged(projectRoot, input.target ?? "HEAD", targetSha);
|
|
1880
|
-
persistGateState(projectRoot, result);
|
|
1881
|
-
return result;
|
|
1882
2406
|
}, projectRoot).catch((error) => {
|
|
1883
2407
|
if (isStateLockHeldError(error)) {
|
|
1884
2408
|
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 });
|
|
@@ -2240,6 +2764,8 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2240
2764
|
.option("--effort <effort>", "Review effort (medium|high)")
|
|
2241
2765
|
.option("--resume <gate-id>", "Resume a persisted review gate")
|
|
2242
2766
|
.option("--reviewer <name>", "Reviewer id for `review log`")
|
|
2767
|
+
.option("--reason <text>", "Maintainer reason for `review override`")
|
|
2768
|
+
.option("--confirm", "Confirm a `review override` (required)")
|
|
2243
2769
|
.option("--target-kind <kind>", "blueprint | pull-request")
|
|
2244
2770
|
.option("--target-id <id>", "Target slug / PR number / stable review target id")
|
|
2245
2771
|
.option("--artifact <path>", "Committed review transcript/artifact path relative to blueprint (required for promotion-counted approvals)")
|
|
@@ -2273,6 +2799,8 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2273
2799
|
const slug = args[0];
|
|
2274
2800
|
if (!slug)
|
|
2275
2801
|
throw new Error("Usage: wp review gate <blueprint-slug> [--purpose plan|delivery] [--target <git-ref>] [--provider claude|codex|opencode|grok] [--model <model-id>] [--effort medium|high] [--resume <gate-id>] [--json]");
|
|
2802
|
+
if (options.targetId !== undefined || options.targetKind !== undefined)
|
|
2803
|
+
throw new Error("wp review gate does not accept --target-id/--target-kind: the blueprint slug is positional (wp review gate <blueprint-slug>). --target-id/--target-kind apply only to `wp review log`.");
|
|
2276
2804
|
const interrupt = createReviewInterruptScope();
|
|
2277
2805
|
try {
|
|
2278
2806
|
const result = await runReviewGate(projectRoot, slug, options, {
|
|
@@ -2287,6 +2815,7 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2287
2815
|
? undefined
|
|
2288
2816
|
: reviewGateNonConvergenceHint(slug, {
|
|
2289
2817
|
gateId: result.gateId,
|
|
2818
|
+
status: result.status,
|
|
2290
2819
|
...(latestFailure?.failureCode
|
|
2291
2820
|
? { failureCode: latestFailure.failureCode }
|
|
2292
2821
|
: {}),
|
|
@@ -2297,6 +2826,9 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2297
2826
|
`${result.status}: ${result.message}`,
|
|
2298
2827
|
`gate: ${result.gateId}`,
|
|
2299
2828
|
`state: ${result.statePath}`,
|
|
2829
|
+
...(latestFailure?.providerErrorDetail
|
|
2830
|
+
? [`provider error: ${latestFailure.providerErrorDetail}`]
|
|
2831
|
+
: []),
|
|
2300
2832
|
...(hint ? [hint] : []),
|
|
2301
2833
|
].join("\n"));
|
|
2302
2834
|
return;
|
|
@@ -2315,6 +2847,16 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2315
2847
|
: `Logged review ${entry.id} -> ${entry.blueprintSlug}`);
|
|
2316
2848
|
return;
|
|
2317
2849
|
}
|
|
2850
|
+
case "override": {
|
|
2851
|
+
const slug = args[0];
|
|
2852
|
+
if (!slug)
|
|
2853
|
+
throw new Error("Usage: wp review override <blueprint-slug> --reason <text> --confirm [--purpose plan|delivery] [--target <git-ref>]");
|
|
2854
|
+
const entry = await runReviewOverride(projectRoot, slug, options);
|
|
2855
|
+
console.log(options.json
|
|
2856
|
+
? JSON.stringify(entry, null, 2)
|
|
2857
|
+
: `Recorded maintainer override ${entry.id} -> ${entry.blueprintSlug}`);
|
|
2858
|
+
return;
|
|
2859
|
+
}
|
|
2318
2860
|
case "read": {
|
|
2319
2861
|
const slug = args[0];
|
|
2320
2862
|
if (!slug)
|
|
@@ -2356,7 +2898,7 @@ export function registerReviewCommand(cli, deps = {}) {
|
|
|
2356
2898
|
}
|
|
2357
2899
|
}
|
|
2358
2900
|
default:
|
|
2359
|
-
throw new Error("Unknown review subcommand. Use one of: gate, run, log, read, scoreboard, availability, doctor");
|
|
2901
|
+
throw new Error("Unknown review subcommand. Use one of: gate, run, log, override, read, scoreboard, availability, doctor");
|
|
2360
2902
|
}
|
|
2361
2903
|
});
|
|
2362
2904
|
}
|