@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
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ConvergeUnit } from "./convergence.js";
|
|
2
|
+
export type CollisionResolution = "none" | "tracked" | "removed-identical" | "removed-owned";
|
|
3
|
+
export interface CollisionResult {
|
|
4
|
+
readonly ok: boolean;
|
|
5
|
+
readonly resolution?: CollisionResolution;
|
|
6
|
+
readonly error?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface ResolveCollisionOptions {
|
|
9
|
+
readonly unit: ConvergeUnit;
|
|
10
|
+
readonly desiredContent: string | Uint8Array;
|
|
11
|
+
/** Compatibility input only. Unknown content remains protected when true. */
|
|
12
|
+
readonly force?: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolveUntrackedCollision(options: ResolveCollisionOptions): CollisionResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, lstatSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
const GIT_TIMEOUT_MS = 15_000;
|
|
4
|
+
const OWNERSHIP_MARKER = /^\uFEFF?[ \t]*(?:#|\/\/|\/\*|<!--|")?[ \t]*(?:>>> managed by webpresso\b|Generated by webpresso\b)/iu;
|
|
5
|
+
function git(repoRoot, args) {
|
|
6
|
+
const result = spawnSync("git", [...args], {
|
|
7
|
+
cwd: repoRoot,
|
|
8
|
+
encoding: "utf8",
|
|
9
|
+
timeout: GIT_TIMEOUT_MS,
|
|
10
|
+
maxBuffer: 1024 * 1024,
|
|
11
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12
|
+
});
|
|
13
|
+
return { ok: result.status === 0 && !result.error };
|
|
14
|
+
}
|
|
15
|
+
export function resolveUntrackedCollision(options) {
|
|
16
|
+
const { unit } = options;
|
|
17
|
+
if (!existsSync(unit.targetPath))
|
|
18
|
+
return { ok: true, resolution: "none" };
|
|
19
|
+
if (git(unit.repoRoot, ["ls-files", "--error-unmatch", "--", unit.gitPath]).ok) {
|
|
20
|
+
return { ok: true, resolution: "tracked" };
|
|
21
|
+
}
|
|
22
|
+
if (!lstatSync(unit.targetPath).isFile()) {
|
|
23
|
+
return {
|
|
24
|
+
ok: false,
|
|
25
|
+
error: `Untracked collision at ${unit.gitPath} is not a regular file; move it aside and rerun wp setup.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
const current = readFileSync(unit.targetPath);
|
|
29
|
+
if (current.equals(Buffer.from(options.desiredContent))) {
|
|
30
|
+
rmSync(unit.targetPath);
|
|
31
|
+
return { ok: true, resolution: "removed-identical" };
|
|
32
|
+
}
|
|
33
|
+
if (OWNERSHIP_MARKER.test(current.toString("utf8"))) {
|
|
34
|
+
rmSync(unit.targetPath);
|
|
35
|
+
return { ok: true, resolution: "removed-owned" };
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
error: `Unknown untracked content blocks ${unit.gitPath}; it was preserved${options.force ? " despite --force" : ""}. ` +
|
|
40
|
+
"Move or commit the file, then rerun wp setup.",
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { type OpencodeAccountAvailabilityEvent } from "#review/availability.js";
|
|
2
|
+
import type { ReviewLimitWindow } from "#review/execution/types.js";
|
|
3
|
+
import { type OpencodeAccount, type OpencodeAccountSelection } from "#review/opencode-account.js";
|
|
4
|
+
/** Set to `1`/`true` to launch without probing account health at all. */
|
|
5
|
+
export declare const OPENCODE_SKIP_PROBE_ENV = "WP_OPENCODE_SKIP_PROBE";
|
|
6
|
+
/**
|
|
7
|
+
* Wall-clock bound for a single account probe.
|
|
8
|
+
*
|
|
9
|
+
* Measured live on 2026-07-24 against three real OpenCode Go accounts
|
|
10
|
+
* (`opencode` 1.18.4, model `opencode-go/deepseek-v4-flash`, trivial prompt):
|
|
11
|
+
*
|
|
12
|
+
* | run | capped `a` | capped `o` | healthy `z` |
|
|
13
|
+
* | --- | --------------- | --------------- | -------------------------- |
|
|
14
|
+
* | repo cwd, default tools | 6.0s to first limit line | ~10s | 50.9s to completion |
|
|
15
|
+
* | temp cwd, default tools | 11.6s | 20.7s | 28.2s to first assistant text |
|
|
16
|
+
* | temp cwd, no-tools agent | 13.9s | 11.8s | 18.6s to first assistant text |
|
|
17
|
+
*
|
|
18
|
+
* `opencode --version` alone costs ~5.7s on the same machine, so roughly a third
|
|
19
|
+
* of the floor is binary startup and cannot be optimised away here. The probe
|
|
20
|
+
* therefore runs in a fresh temp cwd (a repo cwd made opencode snapshot the
|
|
21
|
+
* worktree, which alone cost ~23s) with a no-tools agent (which shrinks the
|
|
22
|
+
* system prompt and saved a further ~10s).
|
|
23
|
+
*
|
|
24
|
+
* 45s is ~2.4x the slowest observed healthy completion (18.6s) and ~2.2x the
|
|
25
|
+
* slowest observed limit detection (20.7s). The headroom covers a cold provider
|
|
26
|
+
* catalog fetch on a slow network; it is not a workaround for a slow probe.
|
|
27
|
+
* Hitting it is not a failure to be silenced by raising the bound — it degrades
|
|
28
|
+
* to `inconclusive` and the launch proceeds on the already-selected account.
|
|
29
|
+
*
|
|
30
|
+
* A capped account is detected by *streaming* the output rather than waiting for
|
|
31
|
+
* exit: opencode retries a usage-limit stream error and does not exit on it, so
|
|
32
|
+
* a blocking `spawnSync` sat at the full 60s bound in every measured run even
|
|
33
|
+
* though the limit was already on stderr after ~6s.
|
|
34
|
+
*/
|
|
35
|
+
export declare const OPENCODE_PROBE_TIMEOUT_MS = 45000;
|
|
36
|
+
/**
|
|
37
|
+
* How long a successful probe is trusted before the account is probed again.
|
|
38
|
+
*
|
|
39
|
+
* A probe costs 12-28s (see `OPENCODE_PROBE_TIMEOUT_MS`), so probing a healthy
|
|
40
|
+
* account on every launch would make `wp opencode` permanently ~19s slower. A
|
|
41
|
+
* *capped* account never needs a re-probe — its cooldown record already
|
|
42
|
+
* suppresses it for the real reset window — so only the healthy verdict is
|
|
43
|
+
* memoised.
|
|
44
|
+
*
|
|
45
|
+
* Measured end to end on 2026-07-24 with three real accounts (two capped):
|
|
46
|
+
* a cold launch that rotated twice took 40.6s; the immediately following launch
|
|
47
|
+
* took 476ms, because the two capped accounts were suppressed by their recorded
|
|
48
|
+
* windows and the healthy one was covered by this marker.
|
|
49
|
+
*/
|
|
50
|
+
export declare const OPENCODE_PROBE_HEALTHY_TTL_MS: number;
|
|
51
|
+
export type OpencodeProbeResult =
|
|
52
|
+
/** The account answered, so it has budget. */
|
|
53
|
+
{
|
|
54
|
+
readonly kind: "healthy";
|
|
55
|
+
}
|
|
56
|
+
/** The provider reported a usage limit; `retryAfterMs` is already defaulted. */
|
|
57
|
+
| {
|
|
58
|
+
readonly kind: "limited";
|
|
59
|
+
readonly limitWindow: ReviewLimitWindow;
|
|
60
|
+
readonly retryAfterMs: number;
|
|
61
|
+
readonly windowParsed: boolean;
|
|
62
|
+
}
|
|
63
|
+
/** Nothing could be concluded; the caller must launch on this account anyway. */
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: "inconclusive";
|
|
66
|
+
readonly reason: string;
|
|
67
|
+
};
|
|
68
|
+
export interface OpencodeProbeRunnerInput {
|
|
69
|
+
readonly dataDir: string;
|
|
70
|
+
readonly timeoutMs: number;
|
|
71
|
+
}
|
|
72
|
+
export type OpencodeProbeRunner = (input: OpencodeProbeRunnerInput) => Promise<OpencodeProbeResult>;
|
|
73
|
+
export declare function isOpencodeProbeEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
74
|
+
export interface ProbeOpencodeAccountDeps {
|
|
75
|
+
readonly run?: OpencodeProbeRunner;
|
|
76
|
+
readonly materialize?: (key: string, dataDir: string) => void;
|
|
77
|
+
readonly now?: () => number;
|
|
78
|
+
readonly timeoutMs?: number;
|
|
79
|
+
readonly healthyTtlMs?: number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Probe one account's OpenCode Go credential, memoising a healthy verdict.
|
|
83
|
+
*
|
|
84
|
+
* Never throws: every failure path resolves to `inconclusive` so a credential
|
|
85
|
+
* check can never stop the TUI from opening.
|
|
86
|
+
*/
|
|
87
|
+
export declare function probeOpencodeAccount(account: OpencodeAccount, deps?: ProbeOpencodeAccountDeps): Promise<OpencodeProbeResult>;
|
|
88
|
+
export interface OpencodeProbeRotationDeps {
|
|
89
|
+
readonly probe?: (account: OpencodeAccount) => Promise<OpencodeProbeResult>;
|
|
90
|
+
readonly recordCooldown?: (canonicalId: string, event: OpencodeAccountAvailabilityEvent) => Promise<unknown>;
|
|
91
|
+
readonly selectFrom?: (accounts: readonly OpencodeAccount[], excludedIds: ReadonlySet<string>) => OpencodeAccountSelection | undefined;
|
|
92
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
93
|
+
}
|
|
94
|
+
export interface OpencodeProbeRotation {
|
|
95
|
+
readonly selection: OpencodeAccountSelection;
|
|
96
|
+
/** Human-readable lines describing what the probe did, in print order. */
|
|
97
|
+
readonly lines: readonly string[];
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Probe the selected account and rotate past capped ones, bounded by the number
|
|
101
|
+
* of configured accounts.
|
|
102
|
+
*
|
|
103
|
+
* Returns the account to launch on plus the lines to print. Never throws and
|
|
104
|
+
* never returns nothing: an inconclusive probe keeps the incoming selection, so
|
|
105
|
+
* the worst case is exactly today's behaviour plus one log line.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveHealthyOpencodeAccount(accounts: readonly OpencodeAccount[], initial: OpencodeAccountSelection, deps?: OpencodeProbeRotationDeps): Promise<OpencodeProbeRotation>;
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { formatOpencodeResetWait, OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS, } from "#cli/commands/opencode-rotate.js";
|
|
6
|
+
import { loadOpencodeAccountAvailability, pruneExpiredOpencodeAccountSuppressions, recordOpencodeAccountAvailabilityWithLock, } from "#review/availability.js";
|
|
7
|
+
import { opencodeUsageLimit } from "#review/execution/adapters.js";
|
|
8
|
+
import { opencodeAccountCanonicalId, opencodeAccountDataDir, selectActiveOpencodeAccount, } from "#review/opencode-account.js";
|
|
9
|
+
import { materializeOpencodeGoAuth } from "#review/opencode-auth.js";
|
|
10
|
+
/** Set to `1`/`true` to launch without probing account health at all. */
|
|
11
|
+
export const OPENCODE_SKIP_PROBE_ENV = "WP_OPENCODE_SKIP_PROBE";
|
|
12
|
+
/**
|
|
13
|
+
* Wall-clock bound for a single account probe.
|
|
14
|
+
*
|
|
15
|
+
* Measured live on 2026-07-24 against three real OpenCode Go accounts
|
|
16
|
+
* (`opencode` 1.18.4, model `opencode-go/deepseek-v4-flash`, trivial prompt):
|
|
17
|
+
*
|
|
18
|
+
* | run | capped `a` | capped `o` | healthy `z` |
|
|
19
|
+
* | --- | --------------- | --------------- | -------------------------- |
|
|
20
|
+
* | repo cwd, default tools | 6.0s to first limit line | ~10s | 50.9s to completion |
|
|
21
|
+
* | temp cwd, default tools | 11.6s | 20.7s | 28.2s to first assistant text |
|
|
22
|
+
* | temp cwd, no-tools agent | 13.9s | 11.8s | 18.6s to first assistant text |
|
|
23
|
+
*
|
|
24
|
+
* `opencode --version` alone costs ~5.7s on the same machine, so roughly a third
|
|
25
|
+
* of the floor is binary startup and cannot be optimised away here. The probe
|
|
26
|
+
* therefore runs in a fresh temp cwd (a repo cwd made opencode snapshot the
|
|
27
|
+
* worktree, which alone cost ~23s) with a no-tools agent (which shrinks the
|
|
28
|
+
* system prompt and saved a further ~10s).
|
|
29
|
+
*
|
|
30
|
+
* 45s is ~2.4x the slowest observed healthy completion (18.6s) and ~2.2x the
|
|
31
|
+
* slowest observed limit detection (20.7s). The headroom covers a cold provider
|
|
32
|
+
* catalog fetch on a slow network; it is not a workaround for a slow probe.
|
|
33
|
+
* Hitting it is not a failure to be silenced by raising the bound — it degrades
|
|
34
|
+
* to `inconclusive` and the launch proceeds on the already-selected account.
|
|
35
|
+
*
|
|
36
|
+
* A capped account is detected by *streaming* the output rather than waiting for
|
|
37
|
+
* exit: opencode retries a usage-limit stream error and does not exit on it, so
|
|
38
|
+
* a blocking `spawnSync` sat at the full 60s bound in every measured run even
|
|
39
|
+
* though the limit was already on stderr after ~6s.
|
|
40
|
+
*/
|
|
41
|
+
export const OPENCODE_PROBE_TIMEOUT_MS = 45_000;
|
|
42
|
+
/**
|
|
43
|
+
* How long a successful probe is trusted before the account is probed again.
|
|
44
|
+
*
|
|
45
|
+
* A probe costs 12-28s (see `OPENCODE_PROBE_TIMEOUT_MS`), so probing a healthy
|
|
46
|
+
* account on every launch would make `wp opencode` permanently ~19s slower. A
|
|
47
|
+
* *capped* account never needs a re-probe — its cooldown record already
|
|
48
|
+
* suppresses it for the real reset window — so only the healthy verdict is
|
|
49
|
+
* memoised.
|
|
50
|
+
*
|
|
51
|
+
* Measured end to end on 2026-07-24 with three real accounts (two capped):
|
|
52
|
+
* a cold launch that rotated twice took 40.6s; the immediately following launch
|
|
53
|
+
* took 476ms, because the two capped accounts were suppressed by their recorded
|
|
54
|
+
* windows and the healthy one was covered by this marker.
|
|
55
|
+
*/
|
|
56
|
+
export const OPENCODE_PROBE_HEALTHY_TTL_MS = 30 * 60 * 1_000;
|
|
57
|
+
/**
|
|
58
|
+
* Cheapest OpenCode Go model at the time of writing. The gateway caps by
|
|
59
|
+
* account, not by model, so any model reveals the cap; this one is picked purely
|
|
60
|
+
* to minimise probe cost. If the id ever disappears from the catalog the probe
|
|
61
|
+
* fails as `inconclusive` and the launch proceeds unchanged, which is the same
|
|
62
|
+
* safe degradation as a spawn failure.
|
|
63
|
+
*/
|
|
64
|
+
const OPENCODE_PROBE_MODEL = "opencode-go/deepseek-v4-flash";
|
|
65
|
+
const OPENCODE_PROBE_AGENT = "wp-health-probe";
|
|
66
|
+
const OPENCODE_PROBE_PROMPT = "hi";
|
|
67
|
+
const OPENCODE_PROBE_PERMISSIONS = { "*": "deny" };
|
|
68
|
+
const OPENCODE_PROBE_CONFIG = JSON.stringify({
|
|
69
|
+
$schema: "https://opencode.ai/config.json",
|
|
70
|
+
share: "disabled",
|
|
71
|
+
permission: OPENCODE_PROBE_PERMISSIONS,
|
|
72
|
+
agent: {
|
|
73
|
+
[OPENCODE_PROBE_AGENT]: {
|
|
74
|
+
description: "Minimal OpenCode Go credential health probe.",
|
|
75
|
+
mode: "primary",
|
|
76
|
+
permission: OPENCODE_PROBE_PERMISSIONS,
|
|
77
|
+
tools: { "*": false },
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
/** Rolling capture bound: the limit text always arrives in the newest output. */
|
|
82
|
+
const MAX_CAPTURE_BYTES = 64 * 1_024;
|
|
83
|
+
/** A rendered assistant token or a finished step proves the account answered. */
|
|
84
|
+
const HEALTHY_EVENT_PATTERN = /"type"\s*:\s*"(?:text|step_finish)"/u;
|
|
85
|
+
const HEALTH_MARKER_FILE = "wp-probe-health.json";
|
|
86
|
+
export function isOpencodeProbeEnabled(env = process.env) {
|
|
87
|
+
const raw = env[OPENCODE_SKIP_PROBE_ENV]?.trim().toLowerCase();
|
|
88
|
+
return raw !== "1" && raw !== "true";
|
|
89
|
+
}
|
|
90
|
+
function healthMarkerPath(dataDir) {
|
|
91
|
+
return join(dataDir, HEALTH_MARKER_FILE);
|
|
92
|
+
}
|
|
93
|
+
function readHealthMarker(dataDir) {
|
|
94
|
+
let parsed;
|
|
95
|
+
try {
|
|
96
|
+
parsed = JSON.parse(readFileSync(healthMarkerPath(dataDir), "utf8"));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
if (parsed === null || typeof parsed !== "object")
|
|
102
|
+
return undefined;
|
|
103
|
+
const okAt = parsed["okAt"];
|
|
104
|
+
return typeof okAt === "number" && Number.isFinite(okAt) ? okAt : undefined;
|
|
105
|
+
}
|
|
106
|
+
function writeHealthMarker(dataDir, atMs) {
|
|
107
|
+
try {
|
|
108
|
+
mkdirSync(dataDir, { recursive: true });
|
|
109
|
+
writeFileSync(healthMarkerPath(dataDir), JSON.stringify({ okAt: atMs }), {
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
mode: 0o600,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// Best-effort memoisation: a missing marker only costs another probe.
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function limitedFromEvent(message) {
|
|
119
|
+
const event = opencodeUsageLimit(message);
|
|
120
|
+
if (event === null || event.kind !== "failure")
|
|
121
|
+
return null;
|
|
122
|
+
const parsed = event.retryAfterMs;
|
|
123
|
+
return {
|
|
124
|
+
kind: "limited",
|
|
125
|
+
limitWindow: event.limitWindow ?? "unknown",
|
|
126
|
+
retryAfterMs: parsed ?? OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS,
|
|
127
|
+
windowParsed: parsed !== undefined,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function probeArgs() {
|
|
131
|
+
return [
|
|
132
|
+
"run",
|
|
133
|
+
"--pure",
|
|
134
|
+
"--print-logs",
|
|
135
|
+
"--log-level",
|
|
136
|
+
"ERROR",
|
|
137
|
+
"--format",
|
|
138
|
+
"json",
|
|
139
|
+
"--model",
|
|
140
|
+
OPENCODE_PROBE_MODEL,
|
|
141
|
+
"--agent",
|
|
142
|
+
OPENCODE_PROBE_AGENT,
|
|
143
|
+
OPENCODE_PROBE_PROMPT,
|
|
144
|
+
];
|
|
145
|
+
}
|
|
146
|
+
function classifyChunk(captured) {
|
|
147
|
+
return (limitedFromEvent(captured) ??
|
|
148
|
+
(HEALTHY_EVENT_PATTERN.test(captured) ? { kind: "healthy" } : null));
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Spawn a bounded `opencode run` against one account's isolated credential
|
|
152
|
+
* store and classify the first conclusive thing it says.
|
|
153
|
+
*
|
|
154
|
+
* There is no OpenCode Go HTTP endpoint anywhere in this codebase — the review
|
|
155
|
+
* adapter also reaches the provider only by spawning this binary — so a
|
|
156
|
+
* subprocess is the only available probe vehicle. Per-account isolation is the
|
|
157
|
+
* same `XDG_DATA_HOME` trick the review adapter uses, so a probe can never race
|
|
158
|
+
* a live interactive TUI's global `auth.json`.
|
|
159
|
+
*/
|
|
160
|
+
function spawnOpencodeProbe(input) {
|
|
161
|
+
return new Promise((resolve) => {
|
|
162
|
+
let probeCwd;
|
|
163
|
+
try {
|
|
164
|
+
probeCwd = mkdtempSync(join(tmpdir(), "wp-opencode-probe-"));
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
resolve({ kind: "inconclusive", reason: describeError(error) });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
runProbeChild(probeCwd, input, resolve);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function runProbeChild(probeCwd, input, resolve) {
|
|
174
|
+
const child = spawn("opencode", [...probeArgs()], {
|
|
175
|
+
cwd: probeCwd,
|
|
176
|
+
env: {
|
|
177
|
+
...process.env,
|
|
178
|
+
XDG_DATA_HOME: input.dataDir,
|
|
179
|
+
OPENCODE_CONFIG_CONTENT: OPENCODE_PROBE_CONFIG,
|
|
180
|
+
OPENCODE_DISABLE_AUTOUPDATE: "true",
|
|
181
|
+
OPENCODE_DISABLE_PRUNE: "true",
|
|
182
|
+
},
|
|
183
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
184
|
+
});
|
|
185
|
+
let settled = false;
|
|
186
|
+
let timer;
|
|
187
|
+
const finish = (result) => {
|
|
188
|
+
if (settled)
|
|
189
|
+
return;
|
|
190
|
+
settled = true;
|
|
191
|
+
if (timer)
|
|
192
|
+
clearTimeout(timer);
|
|
193
|
+
child.kill("SIGKILL");
|
|
194
|
+
try {
|
|
195
|
+
rmSync(probeCwd, { recursive: true, force: true });
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Best-effort temp cleanup; the OS reclaims it either way.
|
|
199
|
+
}
|
|
200
|
+
resolve(result);
|
|
201
|
+
};
|
|
202
|
+
timer = setTimeout(() => finish({ kind: "inconclusive", reason: `probe timed out after ${input.timeoutMs}ms` }), input.timeoutMs);
|
|
203
|
+
let captured = "";
|
|
204
|
+
const onChunk = (chunk) => {
|
|
205
|
+
captured = (captured + chunk.toString("utf8")).slice(-MAX_CAPTURE_BYTES);
|
|
206
|
+
const result = classifyChunk(captured);
|
|
207
|
+
if (result)
|
|
208
|
+
finish(result);
|
|
209
|
+
};
|
|
210
|
+
child.stdout.on("data", onChunk);
|
|
211
|
+
child.stderr.on("data", onChunk);
|
|
212
|
+
child.on("error", (error) => finish({ kind: "inconclusive", reason: error.message }));
|
|
213
|
+
child.on("close", (code) => finish(code === 0
|
|
214
|
+
? { kind: "healthy" }
|
|
215
|
+
: { kind: "inconclusive", reason: `opencode exited with code ${code ?? "signal"}` }));
|
|
216
|
+
}
|
|
217
|
+
function describeError(error) {
|
|
218
|
+
return error instanceof Error ? error.message : String(error);
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Probe one account's OpenCode Go credential, memoising a healthy verdict.
|
|
222
|
+
*
|
|
223
|
+
* Never throws: every failure path resolves to `inconclusive` so a credential
|
|
224
|
+
* check can never stop the TUI from opening.
|
|
225
|
+
*/
|
|
226
|
+
export async function probeOpencodeAccount(account, deps = {}) {
|
|
227
|
+
const now = (deps.now ?? Date.now)();
|
|
228
|
+
const dataDir = opencodeAccountDataDir(account.id);
|
|
229
|
+
const ttl = deps.healthyTtlMs ?? OPENCODE_PROBE_HEALTHY_TTL_MS;
|
|
230
|
+
const okAt = readHealthMarker(dataDir);
|
|
231
|
+
if (okAt !== undefined && now - okAt >= 0 && now - okAt < ttl)
|
|
232
|
+
return { kind: "healthy" };
|
|
233
|
+
try {
|
|
234
|
+
(deps.materialize ?? defaultMaterialize)(account.key, dataDir);
|
|
235
|
+
}
|
|
236
|
+
catch (error) {
|
|
237
|
+
return { kind: "inconclusive", reason: describeError(error) };
|
|
238
|
+
}
|
|
239
|
+
const result = await (deps.run ?? spawnOpencodeProbe)({
|
|
240
|
+
dataDir,
|
|
241
|
+
timeoutMs: deps.timeoutMs ?? OPENCODE_PROBE_TIMEOUT_MS,
|
|
242
|
+
});
|
|
243
|
+
if (result.kind === "healthy")
|
|
244
|
+
writeHealthMarker(dataDir, now);
|
|
245
|
+
return result;
|
|
246
|
+
}
|
|
247
|
+
function defaultMaterialize(key, dataDir) {
|
|
248
|
+
materializeOpencodeGoAuth(key, { dataDir });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Select the account to launch on, excluding `excludedIds`.
|
|
252
|
+
*
|
|
253
|
+
* Excluded ids are the accounts this run just cooled. They are filtered here
|
|
254
|
+
* rather than relied upon coming back suppressed from the store — the same
|
|
255
|
+
* read-after-write independence `selectNextAccount` in `opencode-rotate.ts`
|
|
256
|
+
* keeps, so rotation stays correct even when the cooldown write is contended.
|
|
257
|
+
*/
|
|
258
|
+
function defaultSelectFrom(accounts, excludedIds) {
|
|
259
|
+
const pool = accounts.filter((account) => !excludedIds.has(account.id));
|
|
260
|
+
if (pool.length === 0)
|
|
261
|
+
return undefined;
|
|
262
|
+
const pruned = pruneExpiredOpencodeAccountSuppressions(loadOpencodeAccountAvailability());
|
|
263
|
+
return selectActiveOpencodeAccount(pool, pruned.state);
|
|
264
|
+
}
|
|
265
|
+
function coolLimitedAccount(accountId, result, deps) {
|
|
266
|
+
const canonicalId = opencodeAccountCanonicalId(accountId);
|
|
267
|
+
// Same event shape `--wp-rotate` and the review gate record, so all three
|
|
268
|
+
// writers produce identical, mutually-readable cooldown records. Unlike a
|
|
269
|
+
// manual rotation this one carries the provider's REAL reset window.
|
|
270
|
+
const event = {
|
|
271
|
+
failureCode: "opencode-go-usage-limit",
|
|
272
|
+
terminalClassification: "provider-limit-reached",
|
|
273
|
+
limitWindow: result.limitWindow,
|
|
274
|
+
retryAfterMs: result.retryAfterMs,
|
|
275
|
+
};
|
|
276
|
+
return (deps.recordCooldown ?? recordOpencodeAccountAvailabilityWithLock)(canonicalId, event);
|
|
277
|
+
}
|
|
278
|
+
function allCappedLines(selection) {
|
|
279
|
+
return [
|
|
280
|
+
`wp opencode: every OpenCode Go account probed as capped; launching on the soonest-expiring one (${selection.account.id})${formatOpencodeResetWait(selection.waitMs)}. Add another OPENCODE_GO_KEY_<ID> to widen the pool.`,
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Probe the selected account and rotate past capped ones, bounded by the number
|
|
285
|
+
* of configured accounts.
|
|
286
|
+
*
|
|
287
|
+
* Returns the account to launch on plus the lines to print. Never throws and
|
|
288
|
+
* never returns nothing: an inconclusive probe keeps the incoming selection, so
|
|
289
|
+
* the worst case is exactly today's behaviour plus one log line.
|
|
290
|
+
*/
|
|
291
|
+
export async function resolveHealthyOpencodeAccount(accounts, initial, deps = {}) {
|
|
292
|
+
if (!isOpencodeProbeEnabled(deps.env ?? process.env))
|
|
293
|
+
return { selection: initial, lines: [] };
|
|
294
|
+
// `cooled` means every account is already known-capped: probing cannot find a
|
|
295
|
+
// better one and would cost a full timeout, so keep the existing behaviour.
|
|
296
|
+
if (initial.cooled)
|
|
297
|
+
return { selection: initial, lines: [] };
|
|
298
|
+
const probe = deps.probe ?? ((account) => probeOpencodeAccount(account));
|
|
299
|
+
const selectFrom = deps.selectFrom ?? defaultSelectFrom;
|
|
300
|
+
const excluded = new Set();
|
|
301
|
+
const lines = [];
|
|
302
|
+
let current = initial;
|
|
303
|
+
for (let attempt = 0; attempt < accounts.length; attempt += 1) {
|
|
304
|
+
const result = await probe(current.account);
|
|
305
|
+
if (result.kind === "healthy")
|
|
306
|
+
return { selection: current, lines };
|
|
307
|
+
if (result.kind === "inconclusive") {
|
|
308
|
+
lines.push(`wp opencode: health probe for opencode-go:${current.account.id} was inconclusive (${result.reason}); launching on it anyway.`);
|
|
309
|
+
return { selection: current, lines };
|
|
310
|
+
}
|
|
311
|
+
await coolLimitedAccount(current.account.id, result, deps);
|
|
312
|
+
excluded.add(current.account.id);
|
|
313
|
+
const next = selectFrom(accounts, excluded);
|
|
314
|
+
if (!next) {
|
|
315
|
+
const fallback = selectFrom(accounts, new Set()) ?? current;
|
|
316
|
+
lines.push(...allCappedLines(fallback));
|
|
317
|
+
return { selection: fallback, lines };
|
|
318
|
+
}
|
|
319
|
+
lines.push(`wp opencode: probe found opencode-go:${current.account.id} capped${formatOpencodeResetWait(result.retryAfterMs)}, rotated to opencode-go:${next.account.id}`);
|
|
320
|
+
current = next;
|
|
321
|
+
}
|
|
322
|
+
return { selection: current, lines };
|
|
323
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { resolveOpencodeAccountsGracefully } from "#runtime/opencode-account-materializer.js";
|
|
2
|
+
/** The `--wp-rotate` flag, namespaced like `--wp-permission-mode`. */
|
|
3
|
+
export declare const OPENCODE_ROTATE_FLAG = "--wp-rotate";
|
|
4
|
+
export type OpencodeRotateOutcome =
|
|
5
|
+
/** The previously-active account was cooled and a different one is now active. */
|
|
6
|
+
{
|
|
7
|
+
readonly kind: "rotated";
|
|
8
|
+
readonly cooledAccountId: string;
|
|
9
|
+
readonly activeAccountId: string;
|
|
10
|
+
readonly activeIsCooled: boolean;
|
|
11
|
+
readonly waitMs?: number;
|
|
12
|
+
}
|
|
13
|
+
/** The on-disk key matched no known account, so nothing was cooled. */
|
|
14
|
+
| {
|
|
15
|
+
readonly kind: "reselected";
|
|
16
|
+
readonly activeAccountId: string;
|
|
17
|
+
readonly activeIsCooled: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Fewer than two accounts are provisioned — there is nowhere to rotate to. */
|
|
20
|
+
| {
|
|
21
|
+
readonly kind: "single-account";
|
|
22
|
+
}
|
|
23
|
+
/** Secrets could not be resolved; the caller must still launch normally. */
|
|
24
|
+
| {
|
|
25
|
+
readonly kind: "unavailable";
|
|
26
|
+
readonly reason: string;
|
|
27
|
+
};
|
|
28
|
+
export interface OpencodeRotateDeps {
|
|
29
|
+
readonly resolveOpencodeAccounts?: typeof resolveOpencodeAccountsGracefully;
|
|
30
|
+
readonly readActiveKey?: () => string | undefined;
|
|
31
|
+
readonly materializeGlobalOpencodeAuth?: (key: string) => void;
|
|
32
|
+
readonly recordCooldown?: (canonicalId: string) => Promise<unknown>;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Cooldown to record when an account is known to be capped but the real reset
|
|
36
|
+
* window is unknown: it assumes the shortest real OpenCode Go window (5 hours).
|
|
37
|
+
*
|
|
38
|
+
* A manual `--wp-rotate` has no provider response at all, so it always lands
|
|
39
|
+
* here. The launch-time health probe (`#cli/commands/opencode-probe.js`) uses it
|
|
40
|
+
* only as a fallback, because that path *does* have a provider response and
|
|
41
|
+
* normally records the true parsed window instead.
|
|
42
|
+
*
|
|
43
|
+
* Recording no window at all would leave the record permanently suppressed —
|
|
44
|
+
* `suppressionExpired` only clears a record that carries `retryAfterMs` — so
|
|
45
|
+
* every rotated account would be cooled forever and selection would degrade to
|
|
46
|
+
* "first account" again. A too-short guess is self-correcting: the account is
|
|
47
|
+
* retried, and if it is still capped the gate (or the next probe/rotation)
|
|
48
|
+
* simply cools it again.
|
|
49
|
+
*/
|
|
50
|
+
export declare const OPENCODE_UNKNOWN_LIMIT_COOLDOWN_MS: number;
|
|
51
|
+
/**
|
|
52
|
+
* Cool the currently-materialized opencode-go account and materialize the next
|
|
53
|
+
* usable one into the global `auth.json`.
|
|
54
|
+
*
|
|
55
|
+
* The active account is identified by reading the global store back and matching
|
|
56
|
+
* its key against the resolved accounts — `wp` wrote that value itself, so this
|
|
57
|
+
* stays correct without persisting a separate "last launched" record. When the
|
|
58
|
+
* key matches nothing (a hand-edited store, or a fresh machine), the rotation
|
|
59
|
+
* degrades to a plain reselect and cools nothing, because cooling an account we
|
|
60
|
+
* cannot identify would suppress the wrong one.
|
|
61
|
+
*
|
|
62
|
+
* Never throws: the caller launches the TUI regardless of the outcome.
|
|
63
|
+
*/
|
|
64
|
+
export declare function rotateActiveOpencodeAccount(cwd: string, secretProfile: string, deps?: OpencodeRotateDeps): Promise<OpencodeRotateOutcome>;
|
|
65
|
+
/**
|
|
66
|
+
* Render a cooldown remainder as a short ` (resets in ~Xd)` suffix, or the empty
|
|
67
|
+
* string when the window is unknown. Shared with the launch-time health probe so
|
|
68
|
+
* rotation and probe output cannot drift apart.
|
|
69
|
+
*/
|
|
70
|
+
export declare function formatOpencodeResetWait(waitMs: number | undefined): string;
|
|
71
|
+
/** Human-readable lines describing a rotation outcome, in print order. */
|
|
72
|
+
export declare function describeOpencodeRotate(outcome: OpencodeRotateOutcome): readonly string[];
|