@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,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { getStateRoot } from "#paths/state-root.js";
|
|
3
|
+
import { isOpencodeAccountSuppressed, } from "./availability.js";
|
|
4
|
+
const OPENCODE_GO_KEY_PATTERN = /^OPENCODE_GO_KEY_([A-Za-z0-9]+)$/;
|
|
5
|
+
/**
|
|
6
|
+
* Discover opencode-go accounts from Doppler-sourced secrets shaped
|
|
7
|
+
* `OPENCODE_GO_KEY_<ID>`. Sorted by id ascending, de-duplicated by id.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveOpencodeAccounts(secrets) {
|
|
10
|
+
const byId = new Map();
|
|
11
|
+
for (const [secretName, key] of Object.entries(secrets)) {
|
|
12
|
+
if (!key || key.trim().length === 0)
|
|
13
|
+
continue;
|
|
14
|
+
const match = OPENCODE_GO_KEY_PATTERN.exec(secretName);
|
|
15
|
+
if (!match?.[1])
|
|
16
|
+
continue;
|
|
17
|
+
const id = match[1].toLowerCase();
|
|
18
|
+
if (!byId.has(id))
|
|
19
|
+
byId.set(id, { id, secretName, key });
|
|
20
|
+
}
|
|
21
|
+
return [...byId.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
22
|
+
}
|
|
23
|
+
export function opencodeAccountCanonicalId(id) {
|
|
24
|
+
return `opencode-go:${id}`;
|
|
25
|
+
}
|
|
26
|
+
export function opencodeAccountDataDir(id, stateRoot) {
|
|
27
|
+
return path.join(stateRoot ?? getStateRoot(), "opencode-accounts", id);
|
|
28
|
+
}
|
|
29
|
+
function suppressionExpiryMs(state, canonicalId) {
|
|
30
|
+
const record = state.accounts[canonicalId];
|
|
31
|
+
if (record === undefined || record.lastFailureAt === undefined)
|
|
32
|
+
return undefined;
|
|
33
|
+
const failureAt = Date.parse(record.lastFailureAt);
|
|
34
|
+
if (!Number.isFinite(failureAt))
|
|
35
|
+
return undefined;
|
|
36
|
+
return failureAt + (record.retryAfterMs ?? 0);
|
|
37
|
+
}
|
|
38
|
+
function pickSoonestExpiring(accounts, state, nowMs) {
|
|
39
|
+
const withExpiry = accounts.map((account) => ({
|
|
40
|
+
account,
|
|
41
|
+
expiresAt: suppressionExpiryMs(state, opencodeAccountCanonicalId(account.id)) ??
|
|
42
|
+
Number.POSITIVE_INFINITY,
|
|
43
|
+
}));
|
|
44
|
+
// `accounts` is non-empty whenever this is called, so a no-initial-value
|
|
45
|
+
// reduce is safe and keeps the accumulator type undefined-free.
|
|
46
|
+
const soonest = withExpiry.reduce((min, candidate) => candidate.expiresAt < min.expiresAt ? candidate : min);
|
|
47
|
+
const waitMs = Number.isFinite(soonest.expiresAt)
|
|
48
|
+
? Math.max(0, soonest.expiresAt - nowMs)
|
|
49
|
+
: undefined;
|
|
50
|
+
return {
|
|
51
|
+
account: soonest.account,
|
|
52
|
+
cooled: true,
|
|
53
|
+
...(waitMs === undefined ? {} : { waitMs }),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Select the account to use for the next opencode-go invocation. Callers must
|
|
58
|
+
* prune expired suppressions in `state` before calling this (via
|
|
59
|
+
* `pruneExpiredOpencodeAccountSuppressions`).
|
|
60
|
+
*/
|
|
61
|
+
export function selectActiveOpencodeAccount(accounts, state, nowMs = Date.now()) {
|
|
62
|
+
if (accounts.length === 0)
|
|
63
|
+
return undefined;
|
|
64
|
+
const available = accounts.find((account) => !isOpencodeAccountSuppressed(state, opencodeAccountCanonicalId(account.id)));
|
|
65
|
+
if (available)
|
|
66
|
+
return { account: available, cooled: false };
|
|
67
|
+
return pickSoonestExpiring(accounts, state, nowMs);
|
|
68
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve opencode's `auth.json` path from an XDG-style data-home env var.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the resolver in `src/cli/commands/dash/opencode-db.ts`
|
|
5
|
+
* (`opencodeDatabasePath`), but targets `opencode/auth.json` instead of
|
|
6
|
+
* `opencode/opencode.db`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function opencodeAuthPath(env?: Readonly<Record<string, string | undefined>>, home?: string): string;
|
|
9
|
+
export interface MaterializeOpencodeGoAuthOptions {
|
|
10
|
+
readonly dataDir?: string;
|
|
11
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
12
|
+
readonly home?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface MaterializeOpencodeGoAuthResult {
|
|
15
|
+
readonly path: string;
|
|
16
|
+
readonly wrote: boolean;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Read the `opencode-go` API key currently on disk, or `undefined` when the
|
|
20
|
+
* store is missing, corrupt, or holds no `opencode-go` entry.
|
|
21
|
+
*
|
|
22
|
+
* Callers use this to identify which account is currently materialized by
|
|
23
|
+
* matching the value against their own resolved account list — that keeps the
|
|
24
|
+
* "which account is active" question answerable without persisting extra state.
|
|
25
|
+
* Never throws.
|
|
26
|
+
*/
|
|
27
|
+
export declare function readOpencodeGoKey(options?: MaterializeOpencodeGoAuthOptions): string | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* Merge an `opencode-go` API-key entry into opencode's `auth.json`, preserving
|
|
30
|
+
* every other provider entry already on disk. Never throws on a missing,
|
|
31
|
+
* unreadable, or corrupt existing file — that state is treated as `{}`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function materializeOpencodeGoAuth(key: string, options?: MaterializeOpencodeGoAuthOptions): MaterializeOpencodeGoAuthResult;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { writeJsonFileAtomic } from "#shared-utils/write-json-file.js";
|
|
5
|
+
/**
|
|
6
|
+
* Resolve opencode's `auth.json` path from an XDG-style data-home env var.
|
|
7
|
+
*
|
|
8
|
+
* Mirrors the resolver in `src/cli/commands/dash/opencode-db.ts`
|
|
9
|
+
* (`opencodeDatabasePath`), but targets `opencode/auth.json` instead of
|
|
10
|
+
* `opencode/opencode.db`.
|
|
11
|
+
*/
|
|
12
|
+
export function opencodeAuthPath(env = process.env, home = homedir()) {
|
|
13
|
+
const dataHome = env["XDG_DATA_HOME"]?.trim() || join(home, ".local", "share");
|
|
14
|
+
return join(dataHome, "opencode", "auth.json");
|
|
15
|
+
}
|
|
16
|
+
function resolveAuthPath(options) {
|
|
17
|
+
if (options.dataDir !== undefined) {
|
|
18
|
+
return join(options.dataDir, "opencode", "auth.json");
|
|
19
|
+
}
|
|
20
|
+
return opencodeAuthPath(options.env, options.home);
|
|
21
|
+
}
|
|
22
|
+
function readExistingAuth(path) {
|
|
23
|
+
let raw;
|
|
24
|
+
try {
|
|
25
|
+
raw = readFileSync(path, "utf8");
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const parsed = JSON.parse(raw);
|
|
32
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
33
|
+
return parsed;
|
|
34
|
+
}
|
|
35
|
+
return {};
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isSameOpencodeGoEntry(entry, key) {
|
|
42
|
+
if (entry === null || typeof entry !== "object")
|
|
43
|
+
return false;
|
|
44
|
+
const candidate = entry;
|
|
45
|
+
return candidate["type"] === "api" && candidate["key"] === key;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read the `opencode-go` API key currently on disk, or `undefined` when the
|
|
49
|
+
* store is missing, corrupt, or holds no `opencode-go` entry.
|
|
50
|
+
*
|
|
51
|
+
* Callers use this to identify which account is currently materialized by
|
|
52
|
+
* matching the value against their own resolved account list — that keeps the
|
|
53
|
+
* "which account is active" question answerable without persisting extra state.
|
|
54
|
+
* Never throws.
|
|
55
|
+
*/
|
|
56
|
+
export function readOpencodeGoKey(options = {}) {
|
|
57
|
+
const entry = readExistingAuth(resolveAuthPath(options))["opencode-go"];
|
|
58
|
+
if (entry === null || typeof entry !== "object")
|
|
59
|
+
return undefined;
|
|
60
|
+
const candidate = entry;
|
|
61
|
+
return typeof candidate["key"] === "string" ? candidate["key"] : undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merge an `opencode-go` API-key entry into opencode's `auth.json`, preserving
|
|
65
|
+
* every other provider entry already on disk. Never throws on a missing,
|
|
66
|
+
* unreadable, or corrupt existing file — that state is treated as `{}`.
|
|
67
|
+
*/
|
|
68
|
+
export function materializeOpencodeGoAuth(key, options = {}) {
|
|
69
|
+
const path = resolveAuthPath(options);
|
|
70
|
+
const existing = readExistingAuth(path);
|
|
71
|
+
if (isSameOpencodeGoEntry(existing["opencode-go"], key)) {
|
|
72
|
+
return { path, wrote: false };
|
|
73
|
+
}
|
|
74
|
+
const entry = { type: "api", key };
|
|
75
|
+
const next = { ...existing, "opencode-go": entry };
|
|
76
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
77
|
+
writeJsonFileAtomic(path, next, { writeFileOptions: { mode: 0o600 } });
|
|
78
|
+
return { path, wrote: true };
|
|
79
|
+
}
|
|
@@ -12,7 +12,19 @@ interface TreeEntry {
|
|
|
12
12
|
readonly path: Buffer;
|
|
13
13
|
}
|
|
14
14
|
export declare function resolveReviewCommit(cwd: string, ref: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Repo-relative paths with uncommitted changes (staged, unstaged, or
|
|
17
|
+
* untracked) that are NOT review evidence (reviews.md / review-events.jsonl /
|
|
18
|
+
* review-artifacts/*.md — see isReviewEvidenceRelativePath). A `wp-git-tree-v1`
|
|
19
|
+
* subject digest is computed purely from the committed tree at a ref, so any
|
|
20
|
+
* non-evidence path here means the working tree diverges from what that
|
|
21
|
+
* digest — and any approval bound to it — actually covers. Review evidence is
|
|
22
|
+
* excluded because the gate's own artifact/ledger writes routinely leave the
|
|
23
|
+
* tree dirty between reruns without representing unreviewed content.
|
|
24
|
+
*/
|
|
25
|
+
export declare function findUnreviewedWorkingTreeChanges(cwd: string): readonly string[];
|
|
15
26
|
declare function isExcludedReviewEnvelope(entry: TreeEntry, blueprintRoot: Buffer): boolean;
|
|
27
|
+
export declare function isReviewEvidenceRelativePath(value: string): boolean;
|
|
16
28
|
declare function isReviewEvidencePath(entry: TreeEntry): boolean;
|
|
17
29
|
declare function updateField(hash: Hash, value: Buffer | string): void;
|
|
18
30
|
export declare function createDeliverySubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
@@ -31,9 +43,22 @@ export declare function createPlanSubjectDigestForContent(blueprintSlug: string,
|
|
|
31
43
|
* `normalizePlanSubjectContent`.
|
|
32
44
|
*/
|
|
33
45
|
export declare function legacyNormalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
46
|
+
/**
|
|
47
|
+
* Digest-compatibility variant for review events recorded after the lifecycle
|
|
48
|
+
* stripped-key extension but before the advisory `tags` strip. Verification
|
|
49
|
+
* accepts a match against this tier so existing plan approvals on tagged
|
|
50
|
+
* blueprints are not stranded by the tags-strip rollout.
|
|
51
|
+
*/
|
|
52
|
+
export declare function preTagsStripNormalizePlanSubjectContent(content: Buffer | string): Buffer;
|
|
34
53
|
export declare function createPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
35
54
|
/** Plan subject digested with the pre-extension normalization (see above). */
|
|
36
55
|
export declare function createLegacyPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
56
|
+
/** Plan subject digested without the advisory `tags` strip (pre-tags-strip tier). */
|
|
57
|
+
export declare function createPreTagsStripPlanSubjectAtRef(cwd: string, ref: string, blueprintSlug: string, blueprintPath?: string): ReviewSubject;
|
|
58
|
+
/** Working-ledger digest for the pre-tags-strip compatibility tier. */
|
|
59
|
+
export declare function createPreTagsStripPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
|
|
60
|
+
/** Working-ledger digest for the pre-extension legacy compatibility tier. */
|
|
61
|
+
export declare function createLegacyPlanSubjectDigestForContent(blueprintSlug: string, content: Buffer | string): string;
|
|
37
62
|
export declare const reviewSubjectInternals: {
|
|
38
63
|
isExcludedReviewEnvelope: typeof isExcludedReviewEnvelope;
|
|
39
64
|
isReviewEvidencePath: typeof isReviewEvidencePath;
|
|
@@ -39,6 +39,43 @@ export function resolveReviewCommit(cwd, ref) {
|
|
|
39
39
|
}
|
|
40
40
|
return commit;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Repo-relative paths with uncommitted changes (staged, unstaged, or
|
|
44
|
+
* untracked) that are NOT review evidence (reviews.md / review-events.jsonl /
|
|
45
|
+
* review-artifacts/*.md — see isReviewEvidenceRelativePath). A `wp-git-tree-v1`
|
|
46
|
+
* subject digest is computed purely from the committed tree at a ref, so any
|
|
47
|
+
* non-evidence path here means the working tree diverges from what that
|
|
48
|
+
* digest — and any approval bound to it — actually covers. Review evidence is
|
|
49
|
+
* excluded because the gate's own artifact/ledger writes routinely leave the
|
|
50
|
+
* tree dirty between reruns without representing unreviewed content.
|
|
51
|
+
*/
|
|
52
|
+
export function findUnreviewedWorkingTreeChanges(cwd) {
|
|
53
|
+
const output = gitBuffer(cwd, [
|
|
54
|
+
"status",
|
|
55
|
+
"--porcelain=v1",
|
|
56
|
+
"--untracked-files=all",
|
|
57
|
+
"--ignored=no",
|
|
58
|
+
"-z",
|
|
59
|
+
]).toString("utf8");
|
|
60
|
+
const records = output.split("\0").filter((record) => record.length > 0);
|
|
61
|
+
const paths = new Set();
|
|
62
|
+
for (let index = 0; index < records.length; index += 1) {
|
|
63
|
+
const record = records[index];
|
|
64
|
+
const statusCode = record.slice(0, 2);
|
|
65
|
+
paths.add(record.slice(3));
|
|
66
|
+
if (/[RC]/u.test(statusCode)) {
|
|
67
|
+
// Rename/copy porcelain records are followed by the original path.
|
|
68
|
+
const originalPath = records[index + 1];
|
|
69
|
+
if (originalPath !== undefined) {
|
|
70
|
+
paths.add(originalPath);
|
|
71
|
+
index += 1;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return [...paths]
|
|
76
|
+
.filter((relativePath) => !isReviewEvidenceRelativePath(relativePath))
|
|
77
|
+
.toSorted();
|
|
78
|
+
}
|
|
42
79
|
function parseTree(cwd, commit) {
|
|
43
80
|
const output = gitBuffer(cwd, ["ls-tree", "-r", "-z", "--full-tree", commit]);
|
|
44
81
|
const entries = [];
|
|
@@ -74,31 +111,40 @@ function parseTree(cwd, commit) {
|
|
|
74
111
|
throw new Error("Truncated git ls-tree output.");
|
|
75
112
|
return entries.sort((left, right) => Buffer.compare(left.path, right.path));
|
|
76
113
|
}
|
|
77
|
-
function
|
|
114
|
+
function isRegularBlob(entry) {
|
|
115
|
+
return entry.type === "blob" && /^100(?:644|755)$/u.test(entry.mode.toString("ascii"));
|
|
116
|
+
}
|
|
117
|
+
function findBlueprintDocument(entries, blueprintSlug, blueprintPath) {
|
|
78
118
|
if (blueprintPath) {
|
|
79
119
|
const normalized = blueprintPath.replaceAll("\\", "/");
|
|
80
120
|
const target = Buffer.from(normalized, "utf8");
|
|
81
|
-
const matches = entries.filter((entry) => entry.path.equals(target));
|
|
82
|
-
if (matches.length === 1 && normalized.endsWith("
|
|
83
|
-
return
|
|
121
|
+
const matches = entries.filter((entry) => entry.path.equals(target) && isRegularBlob(entry));
|
|
122
|
+
if (matches.length === 1 && normalized.endsWith(".md")) {
|
|
123
|
+
return matches[0];
|
|
84
124
|
}
|
|
85
125
|
}
|
|
86
|
-
const
|
|
126
|
+
const folderSuffix = `/${blueprintSlug}/_overview.md`;
|
|
127
|
+
const flatSuffix = `/${blueprintSlug}.md`;
|
|
87
128
|
const matches = entries.filter((entry) => {
|
|
88
129
|
const normalized = entry.path.toString("utf8");
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
130
|
+
const suffix = normalized.endsWith(folderSuffix)
|
|
131
|
+
? folderSuffix
|
|
132
|
+
: normalized.endsWith(flatSuffix)
|
|
133
|
+
? flatSuffix
|
|
134
|
+
: undefined;
|
|
135
|
+
const lifecycle = suffix ? normalized.slice(0, -suffix.length).split("/").at(-1) : undefined;
|
|
136
|
+
return isRegularBlob(entry) && BLUEPRINT_STATUSES.has(lifecycle ?? "");
|
|
93
137
|
});
|
|
94
138
|
if (matches.length !== 1) {
|
|
95
139
|
throw new Error(`Expected one tracked blueprint ${blueprintSlug} at target commit, found ${matches.length}.`);
|
|
96
140
|
}
|
|
97
|
-
|
|
98
|
-
return overview.subarray(0, overview.length - Buffer.byteLength("_overview.md"));
|
|
141
|
+
return matches[0];
|
|
99
142
|
}
|
|
100
|
-
function
|
|
101
|
-
|
|
143
|
+
function blueprintEvidenceRoot(documentPath) {
|
|
144
|
+
const value = documentPath.toString("utf8");
|
|
145
|
+
if (!value.endsWith("/_overview.md"))
|
|
146
|
+
return null;
|
|
147
|
+
return documentPath.subarray(0, documentPath.length - Buffer.byteLength("_overview.md"));
|
|
102
148
|
}
|
|
103
149
|
function isExcludedReviewEnvelope(entry, blueprintRoot) {
|
|
104
150
|
if (!isRegularBlob(entry) ||
|
|
@@ -114,10 +160,10 @@ function isExcludedReviewEnvelope(entry, blueprintRoot) {
|
|
|
114
160
|
const artifactName = relative.slice(prefix.length);
|
|
115
161
|
return artifactName.length > 3 && !artifactName.includes("/") && artifactName.endsWith(".md");
|
|
116
162
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
// String-based counterpart of isReviewEvidencePath, shared with callers that
|
|
164
|
+
// only have a repo-relative path (no TreeEntry) — currently the working-tree
|
|
165
|
+
// divergence check in findUnreviewedWorkingTreeChanges below.
|
|
166
|
+
export function isReviewEvidenceRelativePath(value) {
|
|
121
167
|
if (!/^blueprints\/(?:[^/]+\/)*[^/]+\//u.test(value))
|
|
122
168
|
return false;
|
|
123
169
|
if (/(?:^|\/)reviews\.md$/u.test(value) || /(?:^|\/)review-events\.jsonl$/u.test(value)) {
|
|
@@ -125,6 +171,11 @@ function isReviewEvidencePath(entry) {
|
|
|
125
171
|
}
|
|
126
172
|
return /(?:^|\/)review-artifacts\/[^/]+\.md$/u.test(value);
|
|
127
173
|
}
|
|
174
|
+
function isReviewEvidencePath(entry) {
|
|
175
|
+
if (!isRegularBlob(entry))
|
|
176
|
+
return false;
|
|
177
|
+
return isReviewEvidenceRelativePath(entry.path.toString("utf8"));
|
|
178
|
+
}
|
|
128
179
|
function loadBlobContents(cwd, entries) {
|
|
129
180
|
const blobs = entries.filter((entry) => entry.type === "blob");
|
|
130
181
|
if (blobs.length === 0)
|
|
@@ -195,11 +246,13 @@ function readTreeAtRef(cwd, ref) {
|
|
|
195
246
|
}
|
|
196
247
|
export function createDeliverySubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
197
248
|
const { commit, objectFormat, entries: allEntries } = readTreeAtRef(cwd, ref);
|
|
198
|
-
const
|
|
249
|
+
const document = findBlueprintDocument(allEntries, blueprintSlug, blueprintPath);
|
|
250
|
+
const blueprintRoot = blueprintEvidenceRoot(document.path);
|
|
199
251
|
// Exclude review evidence for the owning blueprint AND for every other blueprint
|
|
200
252
|
// (the same global exclusion source snapshots use): recording evidence under an
|
|
201
253
|
// unrelated blueprint must not invalidate an otherwise-identical delivery review.
|
|
202
|
-
const entries = allEntries.filter((entry) => !isExcludedReviewEnvelope(entry, blueprintRoot) &&
|
|
254
|
+
const entries = allEntries.filter((entry) => (blueprintRoot === null || !isExcludedReviewEnvelope(entry, blueprintRoot)) &&
|
|
255
|
+
!isReviewEvidencePath(entry));
|
|
203
256
|
return {
|
|
204
257
|
scheme: "wp-git-tree-v1",
|
|
205
258
|
digest: hashTreeEntries(cwd, entries, objectFormat, DELIVERY_DOMAIN),
|
|
@@ -259,26 +312,32 @@ function neutralizePromotionGateResults(text) {
|
|
|
259
312
|
// continuation lines). Each mirrors exactly one lifecycle writer:
|
|
260
313
|
// `completed_at` (upsertCompletedAt) and the `wp blueprint start` /
|
|
261
314
|
// progress-stamp fields (setBlueprintFrontmatterFields lifecycle intents).
|
|
262
|
-
|
|
315
|
+
// Lifecycle writers only — the pre-tags-strip normalization (compat tier).
|
|
316
|
+
const PLAN_LIFECYCLE_STRIPPED_KEYS_CORE = [
|
|
263
317
|
"completed_at",
|
|
264
318
|
"last_updated",
|
|
265
319
|
"progress",
|
|
266
320
|
"worktree_owner_id",
|
|
267
321
|
"worktree_owner_branch",
|
|
268
322
|
];
|
|
323
|
+
// Current plan-subject strip set: core lifecycle keys plus advisory `tags`.
|
|
324
|
+
// Tags are not plan substance; MCP put and CLI serialize disagree on array-item
|
|
325
|
+
// quoting (`- "x"` vs `- x`). Approvals recorded before the tags strip must
|
|
326
|
+
// still verify via `preTagsStripNormalizePlanSubjectContent`.
|
|
327
|
+
const PLAN_LIFECYCLE_STRIPPED_KEYS = [...PLAN_LIFECYCLE_STRIPPED_KEYS_CORE, "tags"];
|
|
269
328
|
// Neutralize the lifecycle-owned frontmatter fields (`status:` value plus the
|
|
270
|
-
// stripped keys above) INSIDE the leading frontmatter block only. A body line
|
|
329
|
+
// stripped keys above, including advisory `tags`) INSIDE the leading frontmatter block only. A body line
|
|
271
330
|
// that merely looks like one of these (e.g. task text reading
|
|
272
331
|
// "completed_at: must remain unset until release") is plan substance and must
|
|
273
332
|
// keep affecting the digest. Stripping removes the key line and any indented
|
|
274
333
|
// continuation lines so folded (`>-`) values normalize away too.
|
|
275
|
-
function neutralizeLifecycleFrontmatter(text) {
|
|
334
|
+
function neutralizeLifecycleFrontmatter(text, strippedKeys = PLAN_LIFECYCLE_STRIPPED_KEYS) {
|
|
276
335
|
const match = /^---\r?\n[\s\S]*?\r?\n---(?:\r?\n|$)/u.exec(text);
|
|
277
336
|
if (!match)
|
|
278
337
|
return text;
|
|
279
338
|
let frontmatter = match[0];
|
|
280
339
|
frontmatter = frontmatter.replace(/^(status:\s*)(['"]?)[^'"\r\n]+?(['"]?)(\s*)$/m, `$1${PLAN_STATUS_PLACEHOLDER}$4`);
|
|
281
|
-
for (const key of
|
|
340
|
+
for (const key of strippedKeys) {
|
|
282
341
|
frontmatter = frontmatter.replace(new RegExp(String.raw `^${key}:[^\n]*\n?(?:[ \t]+[^\n]*\n?)*`, "gmu"), "");
|
|
283
342
|
}
|
|
284
343
|
return `${frontmatter}${text.slice(match[0].length)}`;
|
|
@@ -410,12 +469,25 @@ export function legacyNormalizePlanSubjectContent(content) {
|
|
|
410
469
|
text = neutralizePromotionGateResults(text);
|
|
411
470
|
return Buffer.from(text, "utf8");
|
|
412
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Digest-compatibility variant for review events recorded after the lifecycle
|
|
474
|
+
* stripped-key extension but before the advisory `tags` strip. Verification
|
|
475
|
+
* accepts a match against this tier so existing plan approvals on tagged
|
|
476
|
+
* blueprints are not stranded by the tags-strip rollout.
|
|
477
|
+
*/
|
|
478
|
+
export function preTagsStripNormalizePlanSubjectContent(content) {
|
|
479
|
+
let text = typeof content === "string" ? content : content.toString("utf8");
|
|
480
|
+
text = neutralizeLifecycleFrontmatter(text, PLAN_LIFECYCLE_STRIPPED_KEYS_CORE);
|
|
481
|
+
text = neutralizeReadinessStamps(text);
|
|
482
|
+
text = neutralizePromotionGateResults(text);
|
|
483
|
+
text = neutralizeTaskExecutionState(text);
|
|
484
|
+
return Buffer.from(text, "utf8");
|
|
485
|
+
}
|
|
413
486
|
function planSubjectAtRef(cwd, ref, blueprintSlug, normalize, blueprintPath) {
|
|
414
487
|
const commit = resolveReviewCommit(cwd, ref);
|
|
415
488
|
const entries = parseTree(cwd, commit);
|
|
416
|
-
const
|
|
417
|
-
const overviewPath =
|
|
418
|
-
const overview = entries.find((entry) => entry.path.equals(overviewPath));
|
|
489
|
+
const overview = findBlueprintDocument(entries, blueprintSlug, blueprintPath);
|
|
490
|
+
const overviewPath = overview.path;
|
|
419
491
|
if (!overview || overview.type !== "blob") {
|
|
420
492
|
throw new Error("Tracked blueprint overview is not a blob.");
|
|
421
493
|
}
|
|
@@ -439,6 +511,24 @@ export function createPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
|
439
511
|
export function createLegacyPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
440
512
|
return planSubjectAtRef(cwd, ref, blueprintSlug, legacyNormalizePlanSubjectContent, blueprintPath);
|
|
441
513
|
}
|
|
514
|
+
/** Plan subject digested without the advisory `tags` strip (pre-tags-strip tier). */
|
|
515
|
+
export function createPreTagsStripPlanSubjectAtRef(cwd, ref, blueprintSlug, blueprintPath) {
|
|
516
|
+
return planSubjectAtRef(cwd, ref, blueprintSlug, preTagsStripNormalizePlanSubjectContent, blueprintPath);
|
|
517
|
+
}
|
|
518
|
+
/** Working-ledger digest for the pre-tags-strip compatibility tier. */
|
|
519
|
+
export function createPreTagsStripPlanSubjectDigestForContent(blueprintSlug, content) {
|
|
520
|
+
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
521
|
+
updateField(hash, blueprintSlug);
|
|
522
|
+
updateField(hash, preTagsStripNormalizePlanSubjectContent(content));
|
|
523
|
+
return hash.digest("hex");
|
|
524
|
+
}
|
|
525
|
+
/** Working-ledger digest for the pre-extension legacy compatibility tier. */
|
|
526
|
+
export function createLegacyPlanSubjectDigestForContent(blueprintSlug, content) {
|
|
527
|
+
const hash = createHash("sha256").update(PLAN_DOMAIN);
|
|
528
|
+
updateField(hash, blueprintSlug);
|
|
529
|
+
updateField(hash, legacyNormalizePlanSubjectContent(content));
|
|
530
|
+
return hash.digest("hex");
|
|
531
|
+
}
|
|
442
532
|
export const reviewSubjectInternals = {
|
|
443
533
|
isExcludedReviewEnvelope,
|
|
444
534
|
isReviewEvidencePath,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type OpencodeAccount } from "#review/opencode-account.js";
|
|
2
|
+
import { resolveRuntimeEnvironment } from "./executor.js";
|
|
3
|
+
export type OpencodeAccountsResolution = {
|
|
4
|
+
readonly ok: true;
|
|
5
|
+
readonly accounts: readonly OpencodeAccount[];
|
|
6
|
+
} | {
|
|
7
|
+
readonly ok: false;
|
|
8
|
+
readonly reason: string;
|
|
9
|
+
};
|
|
10
|
+
export interface ResolveOpencodeAccountsGracefullyInput {
|
|
11
|
+
readonly cwd: string;
|
|
12
|
+
/**
|
|
13
|
+
* Secret profile to resolve through — REQUIRED, never optional.
|
|
14
|
+
*
|
|
15
|
+
* Without it `resolveRuntimeEnvironment` passes no environment selector and
|
|
16
|
+
* the secret manager falls back to its own ambient, path-keyed config
|
|
17
|
+
* (Doppler's per-directory scopes, Infisical's discovered `.infisical.json`).
|
|
18
|
+
* That resolves only in a checkout that happens to sit under the scoped path,
|
|
19
|
+
* so discovery silently yields zero accounts in every managed worktree, CI
|
|
20
|
+
* clone, and fresh checkout — a failure mode that is invisible because callers
|
|
21
|
+
* degrade gracefully. Keeping this required makes that class of bug a compile
|
|
22
|
+
* error instead of a silent runtime degradation. Callers pass
|
|
23
|
+
* `resolveAgentSecretProfile(env)`.
|
|
24
|
+
*/
|
|
25
|
+
readonly secretProfile: string;
|
|
26
|
+
readonly resolveRuntimeEnvironment?: typeof resolveRuntimeEnvironment;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
|
|
30
|
+
* On any resolution failure (missing profile, secret-manager error, empty
|
|
31
|
+
* result), returns a failure reason derived only from the error's own
|
|
32
|
+
* message/type — never from a secret value.
|
|
33
|
+
*/
|
|
34
|
+
export declare function resolveOpencodeAccountsGracefully(input: ResolveOpencodeAccountsGracefullyInput): OpencodeAccountsResolution;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { resolveOpencodeAccounts } from "#review/opencode-account.js";
|
|
2
|
+
import { resolveRuntimeEnvironment } from "./executor.js";
|
|
3
|
+
function describeError(error) {
|
|
4
|
+
if (error instanceof Error)
|
|
5
|
+
return `${error.name}: ${error.message}`;
|
|
6
|
+
return typeof error === "string" ? error : "unknown error";
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Resolve `OPENCODE_GO_KEY_*` secrets into accounts without ever throwing.
|
|
10
|
+
* On any resolution failure (missing profile, secret-manager error, empty
|
|
11
|
+
* result), returns a failure reason derived only from the error's own
|
|
12
|
+
* message/type — never from a secret value.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveOpencodeAccountsGracefully(input) {
|
|
15
|
+
const resolveEnv = input.resolveRuntimeEnvironment ?? resolveRuntimeEnvironment;
|
|
16
|
+
let secrets;
|
|
17
|
+
try {
|
|
18
|
+
secrets = resolveEnv({
|
|
19
|
+
cwd: input.cwd,
|
|
20
|
+
profile: "secrets-only",
|
|
21
|
+
secretProfile: input.secretProfile,
|
|
22
|
+
environment: input.secretProfile,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return { ok: false, reason: describeError(error) };
|
|
27
|
+
}
|
|
28
|
+
const accounts = resolveOpencodeAccounts(secrets);
|
|
29
|
+
if (accounts.length === 0) {
|
|
30
|
+
return { ok: false, reason: "no OPENCODE_GO_KEY_* secrets found" };
|
|
31
|
+
}
|
|
32
|
+
return { ok: true, accounts };
|
|
33
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps changed repo paths to the tracked Rust crate(s) they belong to.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free so it is unit-testable without shelling out to
|
|
5
|
+
* `cargo` or `git` — see `#cli/commands/rust-check.js` for the CLI command
|
|
6
|
+
* that spawns `cargo check`/`cargo test` for whatever this resolves.
|
|
7
|
+
*/
|
|
8
|
+
export interface TrackedRustCrate {
|
|
9
|
+
/** Crate directory relative to the repo root, e.g. "native/dash-tui". */
|
|
10
|
+
readonly root: string;
|
|
11
|
+
/** Manifest path relative to the repo root. */
|
|
12
|
+
readonly manifest: string;
|
|
13
|
+
/** `cargo` args (after `cargo`) that prove this crate's tests still pass. */
|
|
14
|
+
readonly testArgs: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The Rust crates `wp ci-preflight` proves compile + pass on an affected
|
|
18
|
+
* change. Mirrors exactly what `.github/workflows/ci.agent-kit.yml` (native
|
|
19
|
+
* session-memory + dash-tui jobs) and `.github/workflows/desktop-perf.yml`
|
|
20
|
+
* (desktop `src-tauri`) already build and test in CI — no new cargo
|
|
21
|
+
* invocation shape is invented here.
|
|
22
|
+
*
|
|
23
|
+
* Deliberately EXCLUDES the `spikes/**` crates (`spikes/pty-spawn-transport/
|
|
24
|
+
* rust`, `spikes/desktop-pty-loadtest/rust`,
|
|
25
|
+
* `spikes/desktop-pty-loadtest/src-tauri`): no CI workflow builds or tests
|
|
26
|
+
* them today — they are scratch/prototype crates with no compile guarantee.
|
|
27
|
+
* Gating pre-push on them would fail closed on code nobody has ever promised
|
|
28
|
+
* compiles. Revisit this list if a spike graduates to a tracked crate.
|
|
29
|
+
*/
|
|
30
|
+
export declare const TRACKED_RUST_CRATES: readonly TrackedRustCrate[];
|
|
31
|
+
/**
|
|
32
|
+
* Returns the sorted, deduped list of tracked-crate manifest paths touched by
|
|
33
|
+
* `files`. Rust files under an untracked crate (e.g. `spikes/**`) or outside
|
|
34
|
+
* any crate are ignored, not escalated to "affects everything" — each
|
|
35
|
+
* manifest maps back to `TRACKED_RUST_CRATES` for its `testArgs`.
|
|
36
|
+
*/
|
|
37
|
+
export declare function resolveAffectedRustManifests(files: readonly string[]): readonly string[];
|
|
38
|
+
export declare function trackedCrateForManifest(manifest: string): TrackedRustCrate | undefined;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maps changed repo paths to the tracked Rust crate(s) they belong to.
|
|
3
|
+
*
|
|
4
|
+
* Pure and dependency-free so it is unit-testable without shelling out to
|
|
5
|
+
* `cargo` or `git` — see `#cli/commands/rust-check.js` for the CLI command
|
|
6
|
+
* that spawns `cargo check`/`cargo test` for whatever this resolves.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* The Rust crates `wp ci-preflight` proves compile + pass on an affected
|
|
10
|
+
* change. Mirrors exactly what `.github/workflows/ci.agent-kit.yml` (native
|
|
11
|
+
* session-memory + dash-tui jobs) and `.github/workflows/desktop-perf.yml`
|
|
12
|
+
* (desktop `src-tauri`) already build and test in CI — no new cargo
|
|
13
|
+
* invocation shape is invented here.
|
|
14
|
+
*
|
|
15
|
+
* Deliberately EXCLUDES the `spikes/**` crates (`spikes/pty-spawn-transport/
|
|
16
|
+
* rust`, `spikes/desktop-pty-loadtest/rust`,
|
|
17
|
+
* `spikes/desktop-pty-loadtest/src-tauri`): no CI workflow builds or tests
|
|
18
|
+
* them today — they are scratch/prototype crates with no compile guarantee.
|
|
19
|
+
* Gating pre-push on them would fail closed on code nobody has ever promised
|
|
20
|
+
* compiles. Revisit this list if a spike graduates to a tracked crate.
|
|
21
|
+
*/
|
|
22
|
+
export const TRACKED_RUST_CRATES = [
|
|
23
|
+
{
|
|
24
|
+
root: "native/session-memory-engine",
|
|
25
|
+
manifest: "native/session-memory-engine/Cargo.toml",
|
|
26
|
+
testArgs: ["test", "--manifest-path", "native/session-memory-engine/Cargo.toml", "--all"],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
root: "native/dash-tui",
|
|
30
|
+
manifest: "native/dash-tui/Cargo.toml",
|
|
31
|
+
testArgs: ["test", "--manifest-path", "native/dash-tui/Cargo.toml"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
root: "apps/desktop/src-tauri",
|
|
35
|
+
manifest: "apps/desktop/src-tauri/Cargo.toml",
|
|
36
|
+
testArgs: ["test", "--manifest-path", "apps/desktop/src-tauri/Cargo.toml", "--lib"],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
function isRustSignalFile(file) {
|
|
40
|
+
return (file.endsWith(".rs") ||
|
|
41
|
+
file === "Cargo.toml" ||
|
|
42
|
+
file.endsWith("/Cargo.toml") ||
|
|
43
|
+
file === "Cargo.lock" ||
|
|
44
|
+
file.endsWith("/Cargo.lock"));
|
|
45
|
+
}
|
|
46
|
+
function owningTrackedCrate(file) {
|
|
47
|
+
return TRACKED_RUST_CRATES.find((candidate) => file === candidate.root || file.startsWith(`${candidate.root}/`));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Returns the sorted, deduped list of tracked-crate manifest paths touched by
|
|
51
|
+
* `files`. Rust files under an untracked crate (e.g. `spikes/**`) or outside
|
|
52
|
+
* any crate are ignored, not escalated to "affects everything" — each
|
|
53
|
+
* manifest maps back to `TRACKED_RUST_CRATES` for its `testArgs`.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveAffectedRustManifests(files) {
|
|
56
|
+
const manifests = new Set();
|
|
57
|
+
for (const rawFile of files) {
|
|
58
|
+
const file = rawFile.replaceAll("\\", "/");
|
|
59
|
+
if (!isRustSignalFile(file))
|
|
60
|
+
continue;
|
|
61
|
+
const crate = owningTrackedCrate(file);
|
|
62
|
+
if (crate)
|
|
63
|
+
manifests.add(crate.manifest);
|
|
64
|
+
}
|
|
65
|
+
return [...manifests].sort();
|
|
66
|
+
}
|
|
67
|
+
export function trackedCrateForManifest(manifest) {
|
|
68
|
+
return TRACKED_RUST_CRATES.find((candidate) => candidate.manifest === manifest);
|
|
69
|
+
}
|
|
@@ -20,7 +20,7 @@ export interface FetchAndIndexOptions {
|
|
|
20
20
|
signal?: AbortSignal;
|
|
21
21
|
}
|
|
22
22
|
export declare function fetchAndIndex(options: FetchAndIndexOptions, deps?: {
|
|
23
|
-
readonly fetchImpl?:
|
|
23
|
+
readonly fetchImpl?: (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
24
24
|
/** Internal/test-only escape hatch for loopback fixtures; never exposed in CLI/MCP input. */
|
|
25
25
|
readonly allowedHosts?: readonly string[];
|
|
26
26
|
}): Promise<SessionMemoryChunk[]>;
|