@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,135 @@
|
|
|
1
|
+
import { constants } from "node:fs";
|
|
2
|
+
import { access, lstat, mkdir, readdir, rename, rmdir } from "node:fs/promises";
|
|
3
|
+
import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
|
|
4
|
+
import { resolveBlueprintFile } from "#lifecycle/local.js";
|
|
5
|
+
import { BLUEPRINT_STATUSES, getBlueprintDocumentPaths, isBlueprintSlugSegment, isBlueprintStatus, } from "#utils/document-paths.js";
|
|
6
|
+
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
7
|
+
async function exists(filePath) {
|
|
8
|
+
try {
|
|
9
|
+
await access(filePath, constants.F_OK);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function assertRegularFile(filePath) {
|
|
17
|
+
const stats = await lstat(filePath);
|
|
18
|
+
if (!stats.isFile() || stats.isSymbolicLink()) {
|
|
19
|
+
throw new Error(`Blueprint canonicalization requires a regular file: ${filePath}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
async function directCanonicalizeLocation(projectRoot, slug) {
|
|
23
|
+
const root = resolveBlueprintRoot(projectRoot);
|
|
24
|
+
const segments = slug.split("/").filter((segment) => segment.length > 0);
|
|
25
|
+
const scopedState = isBlueprintStatus(segments[0]) ? segments[0] : null;
|
|
26
|
+
const slugSegments = scopedState === null ? segments : segments.slice(1);
|
|
27
|
+
const stableSlug = slugSegments.join("/");
|
|
28
|
+
// Validate before any filesystem probe: an unvalidated slug such as
|
|
29
|
+
// "../outside" would otherwise resolve paths outside the blueprint root.
|
|
30
|
+
if (!stableSlug || !slugSegments.every((segment) => isBlueprintSlugSegment(segment))) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const states = scopedState === null ? BLUEPRINT_STATUSES : [scopedState];
|
|
34
|
+
for (const state of states) {
|
|
35
|
+
const paths = getBlueprintDocumentPaths(root, state, stableSlug);
|
|
36
|
+
const [flatExists, folderExists] = await Promise.all([
|
|
37
|
+
exists(paths.flat),
|
|
38
|
+
exists(paths.folder),
|
|
39
|
+
]);
|
|
40
|
+
if (flatExists && folderExists) {
|
|
41
|
+
await assertRegularFile(paths.flat);
|
|
42
|
+
await assertRegularFile(paths.folder);
|
|
43
|
+
throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${paths.folder}`);
|
|
44
|
+
}
|
|
45
|
+
if (flatExists) {
|
|
46
|
+
return {
|
|
47
|
+
folderDirectory: paths.directory,
|
|
48
|
+
folderPath: paths.folder,
|
|
49
|
+
path: paths.flat,
|
|
50
|
+
shape: "flat",
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (folderExists) {
|
|
54
|
+
return {
|
|
55
|
+
folderDirectory: paths.directory,
|
|
56
|
+
folderPath: paths.folder,
|
|
57
|
+
path: paths.folder,
|
|
58
|
+
shape: "folder",
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const GOVERNANCE_ENTRY_NAMES = new Set(["review-events.jsonl", "reviews.md", "review-artifacts"]);
|
|
65
|
+
async function assertUsableTargetDirectory(directory) {
|
|
66
|
+
try {
|
|
67
|
+
const stats = await lstat(directory);
|
|
68
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
69
|
+
throw new Error(`Blueprint canonicalization target is not a real directory: ${directory}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const code = error.code;
|
|
74
|
+
if (code !== "ENOENT")
|
|
75
|
+
throw error;
|
|
76
|
+
await mkdir(directory, { recursive: true });
|
|
77
|
+
return "created";
|
|
78
|
+
}
|
|
79
|
+
// Adopting a directory that already holds governance files would turn
|
|
80
|
+
// previously unattributed evidence into authoritative folder evidence the
|
|
81
|
+
// moment the overview lands next to it. Refuse; the operator must review
|
|
82
|
+
// and remove (or intentionally commit) that evidence first. Compare
|
|
83
|
+
// case-folded: on case-insensitive filesystems "Reviews.md" aliases the
|
|
84
|
+
// authoritative lowercase path.
|
|
85
|
+
const adopted = (await readdir(directory)).filter((entry) => GOVERNANCE_ENTRY_NAMES.has(entry.toLowerCase()));
|
|
86
|
+
if (adopted.length > 0) {
|
|
87
|
+
throw new Error(`Blueprint canonicalization refused to adopt a directory containing review evidence: ${directory} (${adopted.join(", ")})`);
|
|
88
|
+
}
|
|
89
|
+
return "existing";
|
|
90
|
+
}
|
|
91
|
+
async function removeCreatedDirectoryIfEmpty(directory, created) {
|
|
92
|
+
if (!created)
|
|
93
|
+
return;
|
|
94
|
+
try {
|
|
95
|
+
const entries = await readdir(directory);
|
|
96
|
+
if (entries.length === 0)
|
|
97
|
+
await rmdir(directory);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Best-effort cleanup only. A concurrent caller may have completed the move.
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
export async function canonicalizeBlueprint(projectRoot, slug) {
|
|
104
|
+
const location = (await directCanonicalizeLocation(projectRoot, slug)) ??
|
|
105
|
+
(await resolveBlueprintFile(projectRoot, slug));
|
|
106
|
+
const from = location.path;
|
|
107
|
+
const to = location.folderPath;
|
|
108
|
+
if (location.shape === "folder") {
|
|
109
|
+
await assertRegularFile(to);
|
|
110
|
+
return { from, to, moved: false };
|
|
111
|
+
}
|
|
112
|
+
await assertRegularFile(from);
|
|
113
|
+
const directoryState = await assertUsableTargetDirectory(location.folderDirectory);
|
|
114
|
+
const createdDirectory = directoryState === "created";
|
|
115
|
+
try {
|
|
116
|
+
if (await exists(to)) {
|
|
117
|
+
await assertRegularFile(to);
|
|
118
|
+
if (await exists(from)) {
|
|
119
|
+
throw new Error(`Blueprint canonicalization refused to overwrite existing overview: ${to}`);
|
|
120
|
+
}
|
|
121
|
+
return { from, to, moved: false };
|
|
122
|
+
}
|
|
123
|
+
await rename(from, to);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
if (error.code === "ENOENT" && (await exists(to))) {
|
|
127
|
+
await assertRegularFile(to);
|
|
128
|
+
return { from, to, moved: false };
|
|
129
|
+
}
|
|
130
|
+
await removeCreatedDirectoryIfEmpty(location.folderDirectory, createdDirectory);
|
|
131
|
+
throw error;
|
|
132
|
+
}
|
|
133
|
+
refreshBlueprintReadmeIndex(projectRoot);
|
|
134
|
+
return { from, to, moved: true };
|
|
135
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BlueprintLifecycleIntent, BlueprintLifecycleResult } from "./engine.js";
|
|
2
|
-
|
|
2
|
+
import { type BlueprintDocumentIdentity } from "#utils/document-paths.js";
|
|
3
|
+
export interface ResolvedBlueprintFile extends BlueprintDocumentIdentity {
|
|
3
4
|
path: string;
|
|
4
5
|
slug: string;
|
|
5
6
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { mkdir, readFile, readdir, rename, rmdir, writeFile } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { refreshBlueprintReadmeIndex } from "#audit/blueprint-readme-drift";
|
|
4
|
+
import { assertCompletedApprovalGate } from "#lifecycle/audit";
|
|
4
5
|
import { applyBlueprintLifecycle } from "#lifecycle/engine";
|
|
5
6
|
import { scanBlueprintDirectory } from "#service/scanner";
|
|
6
7
|
import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
7
|
-
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
8
|
+
import { getBlueprintDocumentPaths, resolveBlueprintDocumentIdentity, } from "#utils/document-paths.js";
|
|
8
9
|
const BLUEPRINT_SLUG_SEGMENT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
9
10
|
function isStatusSegment(segment) {
|
|
10
11
|
return (segment === "draft" ||
|
|
@@ -57,7 +58,15 @@ export async function resolveBlueprintFile(projectRoot, slug) {
|
|
|
57
58
|
.map((entry) => entry.slug)
|
|
58
59
|
.join(", ")}`);
|
|
59
60
|
}
|
|
60
|
-
|
|
61
|
+
const identity = resolveBlueprintDocumentIdentity(baseDir, match.path);
|
|
62
|
+
if (!identity) {
|
|
63
|
+
throw new Error(`Blueprint ${match.slug} resolved to an invalid document path: ${match.path}`);
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
...identity,
|
|
67
|
+
path: identity.documentPath,
|
|
68
|
+
slug: match.slug,
|
|
69
|
+
};
|
|
61
70
|
}
|
|
62
71
|
async function tryRemoveEmptyParent(dir) {
|
|
63
72
|
try {
|
|
@@ -75,8 +84,15 @@ export async function applyBlueprintLifecycleToFile(projectRoot, slug, intent) {
|
|
|
75
84
|
const location = await resolveBlueprintFile(projectRoot, slug);
|
|
76
85
|
const raw = await readFile(location.path, "utf-8");
|
|
77
86
|
const mutation = applyBlueprintLifecycle(raw, location.slug, intent);
|
|
87
|
+
// Completion parity: the execution engine's `finalize` intent (exec/resume/
|
|
88
|
+
// stop/move flows) also writes `status: completed`. Gate it with the same
|
|
89
|
+
// plan-approval check CI enforces so this path cannot diverge from the audit
|
|
90
|
+
// either — matching the MCP funnel and CLI completion owners.
|
|
91
|
+
if (mutation.targetStatus === "completed") {
|
|
92
|
+
assertCompletedApprovalGate(location.path, raw);
|
|
93
|
+
}
|
|
78
94
|
const relativeSlug = relativeBlueprintSlug(location.slug);
|
|
79
|
-
const isFlatFile =
|
|
95
|
+
const isFlatFile = location.shape === "flat";
|
|
80
96
|
const sourceDir = path.dirname(location.path);
|
|
81
97
|
const targetDocumentPaths = getBlueprintDocumentPaths(baseDir, mutation.targetStatus, relativeSlug);
|
|
82
98
|
const targetDir = targetDocumentPaths.directory;
|
|
@@ -36,6 +36,12 @@ export interface TrackedReviewProvenanceOptions {
|
|
|
36
36
|
readonly targetHash?: string;
|
|
37
37
|
}
|
|
38
38
|
export declare function normalizeTrackedReviewApprovals(approvals: unknown): TrackedReviewApproval[];
|
|
39
|
+
/**
|
|
40
|
+
* Slug for a blueprint file, invariant across lifecycle moves
|
|
41
|
+
* (`draft/ → planned/ → completed/`) and across both the folder
|
|
42
|
+
* (`<slug>/_overview.md`) and legacy flat (`<slug>.md`) shapes.
|
|
43
|
+
*/
|
|
44
|
+
export declare function stableSlugForBlueprintFile(file: string): string;
|
|
39
45
|
/**
|
|
40
46
|
* Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
|
|
41
47
|
* if missing (lifecycle move draft→planned before the promote commit), resolve
|
|
@@ -3,7 +3,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
|
|
5
5
|
import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
|
|
6
|
-
import { createPlanSubjectDigestForContent, normalizePlanSubjectContent } from "#review/subject.js";
|
|
6
|
+
import { createDeliverySubjectAtRef, createLegacyPlanSubjectDigestForContent, createPlanSubjectDigestForContent, createPreTagsStripPlanSubjectDigestForContent, normalizePlanSubjectContent, } from "#review/subject.js";
|
|
7
7
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
8
8
|
const REVIEW_ENTRY_MARKER = "<!-- wp:review-entry ";
|
|
9
9
|
function normalizePath(file) {
|
|
@@ -44,12 +44,21 @@ function findGitRoot(file) {
|
|
|
44
44
|
current = parent;
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Slug for a blueprint file, invariant across lifecycle moves
|
|
49
|
+
* (`draft/ → planned/ → completed/`) and across both the folder
|
|
50
|
+
* (`<slug>/_overview.md`) and legacy flat (`<slug>.md`) shapes.
|
|
51
|
+
*/
|
|
52
|
+
export function stableSlugForBlueprintFile(file) {
|
|
48
53
|
const segments = normalizePath(file).split("/");
|
|
49
54
|
const lifecycleIndex = segments.findIndex((segment) => new Set(["draft", "planned", "parked", "in-progress", "completed", "archived"]).has(segment));
|
|
50
55
|
if (lifecycleIndex >= 0 && segments.at(-1) === "_overview.md") {
|
|
51
56
|
return segments.slice(lifecycleIndex + 1, -1).join("/");
|
|
52
57
|
}
|
|
58
|
+
const basename = segments.at(-1) ?? "";
|
|
59
|
+
if (lifecycleIndex >= 0 && basename.endsWith(".md")) {
|
|
60
|
+
return basename.slice(0, -3);
|
|
61
|
+
}
|
|
53
62
|
return path.basename(path.dirname(file));
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
@@ -83,7 +92,7 @@ export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
|
|
|
83
92
|
timeout: 5_000,
|
|
84
93
|
});
|
|
85
94
|
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
86
|
-
const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}
|
|
95
|
+
const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}(?:/_overview\\.md|\\.md)$`, "u");
|
|
87
96
|
const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
|
|
88
97
|
if (hits.length !== 1)
|
|
89
98
|
return null;
|
|
@@ -135,21 +144,68 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
135
144
|
// event still counts only when its subjectDigest matches the digest of the
|
|
136
145
|
// exact working bytes being committed and its artifact hash-verifies on disk —
|
|
137
146
|
// content-addressing keeps this sound before the commit exists.
|
|
138
|
-
function collectWorkingV2ReviewProvenance(file, issues) {
|
|
147
|
+
function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
139
148
|
try {
|
|
149
|
+
const repoRoot = findGitRoot(file);
|
|
150
|
+
if (!repoRoot)
|
|
151
|
+
return [];
|
|
140
152
|
const slug = stableSlugForBlueprintFile(file);
|
|
141
153
|
const working = readWorkingReviewEvents(file, slug);
|
|
142
154
|
if (working.events.length === 0)
|
|
143
155
|
return [];
|
|
144
156
|
issues?.push(...working.issues);
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
// Events whose recorded `reviewedCommit` no longer resolves (typically
|
|
158
|
+
// orphaned by a rebase). Collected here rather than mutating
|
|
159
|
+
// `working.compromisedIds`, which is a ReadonlySet by contract, then merged
|
|
160
|
+
// into the set handed to evaluateReviewEvents below.
|
|
161
|
+
const unresolvableEventIds = new Set();
|
|
162
|
+
const subjects = purpose === "plan"
|
|
163
|
+
? [
|
|
164
|
+
...new Map([
|
|
165
|
+
createPlanSubjectDigestForContent(slug, readFileSync(file)),
|
|
166
|
+
createPreTagsStripPlanSubjectDigestForContent(slug, readFileSync(file)),
|
|
167
|
+
createLegacyPlanSubjectDigestForContent(slug, readFileSync(file)),
|
|
168
|
+
].map((digest) => [digest, { scheme: "wp-blueprint-v1", digest }])).values(),
|
|
169
|
+
]
|
|
170
|
+
: [
|
|
171
|
+
...new Map(working.events
|
|
172
|
+
.filter((event) => event.purpose === "delivery")
|
|
173
|
+
.flatMap((event) => {
|
|
174
|
+
// Scope subject-construction failure to THIS event. A rebase
|
|
175
|
+
// rewrites commit SHAs, so a previously-recorded
|
|
176
|
+
// `reviewedCommit` can become unresolvable; letting that throw
|
|
177
|
+
// would escape to the outer catch and discard every approval,
|
|
178
|
+
// including valid ones for commits that still resolve. Mirror
|
|
179
|
+
// the ref-path's markCompromisedArtifactEvents: record the
|
|
180
|
+
// issue, mark the event compromised, and drop only it.
|
|
181
|
+
let subject;
|
|
182
|
+
try {
|
|
183
|
+
subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, normalizePath(path.relative(repoRoot, file)));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
issues?.push(`Working delivery review subject unavailable for ${event.id} at ${event.reviewedCommit}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
|
+
// Compromised (not merely skipped) so the event can never
|
|
188
|
+
// grant approval while still participating in reviewer
|
|
189
|
+
// supersession — see evaluateReviewEvents.
|
|
190
|
+
unresolvableEventIds.add(event.id);
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
if (subject.digest !== event.subjectDigest) {
|
|
194
|
+
issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
return [[subject.digest, subject]];
|
|
198
|
+
})).values(),
|
|
199
|
+
];
|
|
200
|
+
return subjects
|
|
201
|
+
.flatMap((subject) => evaluateReviewEvents(working.events, {
|
|
202
|
+
purpose,
|
|
148
203
|
blueprintSlug: slug,
|
|
149
|
-
subjectScheme:
|
|
150
|
-
subjectDigest: digest,
|
|
151
|
-
},
|
|
152
|
-
|
|
204
|
+
subjectScheme: subject.scheme,
|
|
205
|
+
subjectDigest: subject.digest,
|
|
206
|
+
}, unresolvableEventIds.size === 0
|
|
207
|
+
? working.compromisedIds
|
|
208
|
+
: new Set([...working.compromisedIds, ...unresolvableEventIds])).approvals)
|
|
153
209
|
.filter((event) => event.artifactPath)
|
|
154
210
|
.map((event) => ({
|
|
155
211
|
reviewer: event.reviewer,
|
|
@@ -166,7 +222,7 @@ export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
|
|
|
166
222
|
const repoRoot = findGitRoot(file);
|
|
167
223
|
if (!repoRoot)
|
|
168
224
|
return [];
|
|
169
|
-
const workingApprovals =
|
|
225
|
+
const workingApprovals = collectWorkingV2ReviewProvenance(file, purpose, issues);
|
|
170
226
|
if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
|
|
171
227
|
return workingApprovals;
|
|
172
228
|
try {
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rollout grandfathering, decided from git history instead of frontmatter.
|
|
3
|
+
*
|
|
4
|
+
* Two gates used to grant themselves an exemption by reading a blueprint's own
|
|
5
|
+
* `created:` frontmatter and comparing it to a hardcoded rollout date. That
|
|
6
|
+
* value is written by the same author the gate is meant to constrain, so
|
|
7
|
+
* `created: "2020-01-01"` disabled the check outright — on the CI audit path and
|
|
8
|
+
* (for the approval gate) on the `assertCompletedApprovalGate` write path, which
|
|
9
|
+
* would let a blueprint reach `completed` carrying no approval at all.
|
|
10
|
+
*
|
|
11
|
+
* Presence in a fixed commit's tree is instead a structural fact about history:
|
|
12
|
+
* no file content can add a slug to a tree that was written months ago.
|
|
13
|
+
*
|
|
14
|
+
* Matching on **slug** rather than path is deliberate. Blueprints legitimately
|
|
15
|
+
* move `draft/ → planned/ → completed/`, so a path-based check would mistake a
|
|
16
|
+
* lifecycle move for a new blueprint, while `git log --follow` would introduce a
|
|
17
|
+
* rename-similarity heuristic. The slug is invariant across those moves — the
|
|
18
|
+
* repo already enforces one blueprint = one slug = one `bp/<slug>` branch.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* The commit that introduced both the completed-approval gate and the Trust
|
|
22
|
+
* Dossier requirement ("fix: preserve pre-contract blueprint rollouts",
|
|
23
|
+
* 2026-07-12). Blueprints already tracked at this commit predate both contracts.
|
|
24
|
+
*/
|
|
25
|
+
export declare const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
|
|
26
|
+
/** The date that anchor landed, used only by the legacy fallback below. */
|
|
27
|
+
export declare const ROLLOUT_DATE = "2026-07-12";
|
|
28
|
+
/**
|
|
29
|
+
* Outcome of consulting the anchor, cached per (repoRoot, anchorCommit).
|
|
30
|
+
*
|
|
31
|
+
* - `not-applicable`: the anchor commit is not in this repository at all. The
|
|
32
|
+
* rollout it marks is an agent-kit history event, so in a consumer repo (which
|
|
33
|
+
* has its own unrelated history) the "predates the rollout" question is
|
|
34
|
+
* vacuous. Those repos keep their prior behaviour: nothing is retroactively
|
|
35
|
+
* gated. The security property is unaffected where it matters, because in
|
|
36
|
+
* agent-kit the anchor is an ancestor of `main`. Note a shallow clone can
|
|
37
|
+
* also lack it; that case is detected separately and fails closed rather
|
|
38
|
+
* than falling back, so truncated history cannot reopen the bypass.
|
|
39
|
+
* - `slugs`: the anchor resolved; membership decides the exemption.
|
|
40
|
+
* - `unreadable`: the anchor exists but could not be listed, OR it is absent
|
|
41
|
+
* from a shallow clone where truncated history could be hiding it. Both are
|
|
42
|
+
* inconclusive rather than different-repo signals, so both fail **closed**.
|
|
43
|
+
* Memoised for the process lifetime, so a transient git failure stays
|
|
44
|
+
* fail-closed for the rest of the run -- acceptable for short-lived CLI and
|
|
45
|
+
* audit processes, and the safe direction if it ever is not.
|
|
46
|
+
*/
|
|
47
|
+
type AnchorLookup = {
|
|
48
|
+
readonly kind: "not-applicable";
|
|
49
|
+
} | {
|
|
50
|
+
readonly kind: "slugs";
|
|
51
|
+
readonly slugs: ReadonlySet<string>;
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: "unreadable";
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* Slugs tracked under `blueprints/` at `anchorCommit`, or `null` when the anchor
|
|
57
|
+
* cannot be read. Memoised per (repoRoot, anchorCommit).
|
|
58
|
+
*
|
|
59
|
+
* Callers must treat `null` as "grandfather nobody" — see
|
|
60
|
+
* {@link isBlueprintExemptFromRollout}, which fails closed.
|
|
61
|
+
*/
|
|
62
|
+
export declare function blueprintSlugsAtRolloutAnchor(repoRoot: string, anchorCommit?: string): AnchorLookup;
|
|
63
|
+
/**
|
|
64
|
+
* True when this blueprint is exempt from a rollout-gated contract.
|
|
65
|
+
*
|
|
66
|
+
* Resolution order, deliberately:
|
|
67
|
+
*
|
|
68
|
+
* 1. **Anchor is present in this repository** (the agent-kit repo, where these
|
|
69
|
+
* contracts actually rolled out and where CI and the promote/finalize write
|
|
70
|
+
* path enforce them): membership in the anchor tree decides, and the
|
|
71
|
+
* author-supplied `created` is ignored entirely. This is what closes the
|
|
72
|
+
* forgery — a blueprint cannot add its own slug to a months-old tree.
|
|
73
|
+
* 2. **Anchor absent from a full (non-shallow) clone, or no git repository at
|
|
74
|
+
* all**: the rollout is an agent-kit history event that never happened here,
|
|
75
|
+
* so the anchor cannot answer the question. Fall back to the previous
|
|
76
|
+
* `created` compare. This is unchanged prior behaviour for consumer repos and
|
|
77
|
+
* non-git contexts, which is deliberate: those contexts have no such
|
|
78
|
+
* protection today, so tightening them is a separate decision, and gating
|
|
79
|
+
* them here would be unsatisfiable anyway because every accepted form of
|
|
80
|
+
* approval provenance is git-tracked.
|
|
81
|
+
* 3. **Inconclusive**: the anchor exists but cannot be listed, or it is absent
|
|
82
|
+
* from a *shallow* clone where truncated history could be hiding it. Fails
|
|
83
|
+
* **closed** — the gate applies. Without the shallow check, `git clone
|
|
84
|
+
* --depth 1` of this very repository would drop to the forgeable date
|
|
85
|
+
* fallback and reopen the bypass on the local promote/finalize write path.
|
|
86
|
+
*
|
|
87
|
+
* Net effect: strictly hardens the gate where it is enforced, and regresses
|
|
88
|
+
* nothing anywhere else.
|
|
89
|
+
*/
|
|
90
|
+
export declare function isBlueprintExemptFromRollout(input: {
|
|
91
|
+
readonly repoRoot: string | null | undefined;
|
|
92
|
+
readonly slug: string;
|
|
93
|
+
readonly created: unknown;
|
|
94
|
+
readonly rolloutDate: string;
|
|
95
|
+
readonly anchorCommit?: string;
|
|
96
|
+
}): boolean;
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Rollout grandfathering, decided from git history instead of frontmatter.
|
|
4
|
+
*
|
|
5
|
+
* Two gates used to grant themselves an exemption by reading a blueprint's own
|
|
6
|
+
* `created:` frontmatter and comparing it to a hardcoded rollout date. That
|
|
7
|
+
* value is written by the same author the gate is meant to constrain, so
|
|
8
|
+
* `created: "2020-01-01"` disabled the check outright — on the CI audit path and
|
|
9
|
+
* (for the approval gate) on the `assertCompletedApprovalGate` write path, which
|
|
10
|
+
* would let a blueprint reach `completed` carrying no approval at all.
|
|
11
|
+
*
|
|
12
|
+
* Presence in a fixed commit's tree is instead a structural fact about history:
|
|
13
|
+
* no file content can add a slug to a tree that was written months ago.
|
|
14
|
+
*
|
|
15
|
+
* Matching on **slug** rather than path is deliberate. Blueprints legitimately
|
|
16
|
+
* move `draft/ → planned/ → completed/`, so a path-based check would mistake a
|
|
17
|
+
* lifecycle move for a new blueprint, while `git log --follow` would introduce a
|
|
18
|
+
* rename-similarity heuristic. The slug is invariant across those moves — the
|
|
19
|
+
* repo already enforces one blueprint = one slug = one `bp/<slug>` branch.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The commit that introduced both the completed-approval gate and the Trust
|
|
23
|
+
* Dossier requirement ("fix: preserve pre-contract blueprint rollouts",
|
|
24
|
+
* 2026-07-12). Blueprints already tracked at this commit predate both contracts.
|
|
25
|
+
*/
|
|
26
|
+
export const ROLLOUT_ANCHOR_COMMIT = "c6ccb5f31cbecde3be4dbd6d3b7e67918006d22e";
|
|
27
|
+
/** The date that anchor landed, used only by the legacy fallback below. */
|
|
28
|
+
export const ROLLOUT_DATE = "2026-07-12";
|
|
29
|
+
const anchorSlugCache = new Map();
|
|
30
|
+
function anchorCommitExists(repoRoot, anchorCommit) {
|
|
31
|
+
try {
|
|
32
|
+
execFileSync("git", ["cat-file", "-e", `${anchorCommit}^{commit}`], {
|
|
33
|
+
cwd: repoRoot,
|
|
34
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
35
|
+
});
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A shallow clone (`git clone --depth 1`) can be missing the anchor purely
|
|
44
|
+
* because history was truncated, which is indistinguishable from "different
|
|
45
|
+
* repository" by anchor lookup alone. Detecting shallowness lets us tell the two
|
|
46
|
+
* apart instead of assuming the benign one.
|
|
47
|
+
*/
|
|
48
|
+
function isShallowRepository(repoRoot) {
|
|
49
|
+
try {
|
|
50
|
+
const out = execFileSync("git", ["rev-parse", "--is-shallow-repository"], {
|
|
51
|
+
cwd: repoRoot,
|
|
52
|
+
encoding: "utf8",
|
|
53
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
54
|
+
});
|
|
55
|
+
// A path that is not a git repository at all answers with a non-zero exit,
|
|
56
|
+
// handled below. Anything else that is not a clean "false" is treated as
|
|
57
|
+
// truncated, so an odd repository shape fails closed rather than falling
|
|
58
|
+
// back to the forgeable date.
|
|
59
|
+
return out.trim() !== "false";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Not a git repository (or git is unusable here). This is the "no history to
|
|
63
|
+
// consult" case, not a truncated-history case: the caller falls back, which
|
|
64
|
+
// preserves prior behaviour for non-git contexts where the gate would be
|
|
65
|
+
// unsatisfiable anyway.
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Slugs tracked under `blueprints/` at `anchorCommit`, or `null` when the anchor
|
|
71
|
+
* cannot be read. Memoised per (repoRoot, anchorCommit).
|
|
72
|
+
*
|
|
73
|
+
* Callers must treat `null` as "grandfather nobody" — see
|
|
74
|
+
* {@link isBlueprintExemptFromRollout}, which fails closed.
|
|
75
|
+
*/
|
|
76
|
+
export function blueprintSlugsAtRolloutAnchor(repoRoot, anchorCommit = ROLLOUT_ANCHOR_COMMIT) {
|
|
77
|
+
const cacheKey = `${anchorCommit}:${repoRoot}`;
|
|
78
|
+
const cached = anchorSlugCache.get(cacheKey);
|
|
79
|
+
if (cached !== undefined)
|
|
80
|
+
return cached;
|
|
81
|
+
const result = (() => {
|
|
82
|
+
if (!anchorCommitExists(repoRoot, anchorCommit)) {
|
|
83
|
+
// Absent because this is a different repository (fine -- the rollout never
|
|
84
|
+
// happened here) or because history was truncated (NOT fine -- that would
|
|
85
|
+
// silently reopen the forged-date bypass). Only the former may fall back.
|
|
86
|
+
return isShallowRepository(repoRoot) ? { kind: "unreadable" } : { kind: "not-applicable" };
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
const stdout = execFileSync("git", ["ls-tree", "-r", "--name-only", anchorCommit, "blueprints/"], { cwd: repoRoot, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
90
|
+
const found = new Set();
|
|
91
|
+
for (const line of stdout.split("\n")) {
|
|
92
|
+
const match = /^blueprints\/[^/]+\/(.+)$/u.exec(line.trim());
|
|
93
|
+
if (!match)
|
|
94
|
+
continue;
|
|
95
|
+
const rest = match[1] ?? "";
|
|
96
|
+
// Folder shape `<slug>/_overview.md`, or legacy flat `<slug>.md`.
|
|
97
|
+
const slug = rest.includes("/")
|
|
98
|
+
? rest.slice(0, rest.indexOf("/"))
|
|
99
|
+
: rest.endsWith(".md")
|
|
100
|
+
? rest.slice(0, -3)
|
|
101
|
+
: "";
|
|
102
|
+
if (slug)
|
|
103
|
+
found.add(slug);
|
|
104
|
+
}
|
|
105
|
+
return { kind: "slugs", slugs: found };
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return { kind: "unreadable" };
|
|
109
|
+
}
|
|
110
|
+
})();
|
|
111
|
+
anchorSlugCache.set(cacheKey, result);
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
/** Legacy fallback: the author-supplied `created` date compare. */
|
|
115
|
+
function createdPredatesRollout(created, rolloutDate) {
|
|
116
|
+
const date = created instanceof Date
|
|
117
|
+
? created.toISOString().slice(0, 10)
|
|
118
|
+
: typeof created === "string" && /^\d{4}-\d{2}-\d{2}$/u.test(created)
|
|
119
|
+
? created
|
|
120
|
+
: null;
|
|
121
|
+
return date !== null && date < rolloutDate;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* True when this blueprint is exempt from a rollout-gated contract.
|
|
125
|
+
*
|
|
126
|
+
* Resolution order, deliberately:
|
|
127
|
+
*
|
|
128
|
+
* 1. **Anchor is present in this repository** (the agent-kit repo, where these
|
|
129
|
+
* contracts actually rolled out and where CI and the promote/finalize write
|
|
130
|
+
* path enforce them): membership in the anchor tree decides, and the
|
|
131
|
+
* author-supplied `created` is ignored entirely. This is what closes the
|
|
132
|
+
* forgery — a blueprint cannot add its own slug to a months-old tree.
|
|
133
|
+
* 2. **Anchor absent from a full (non-shallow) clone, or no git repository at
|
|
134
|
+
* all**: the rollout is an agent-kit history event that never happened here,
|
|
135
|
+
* so the anchor cannot answer the question. Fall back to the previous
|
|
136
|
+
* `created` compare. This is unchanged prior behaviour for consumer repos and
|
|
137
|
+
* non-git contexts, which is deliberate: those contexts have no such
|
|
138
|
+
* protection today, so tightening them is a separate decision, and gating
|
|
139
|
+
* them here would be unsatisfiable anyway because every accepted form of
|
|
140
|
+
* approval provenance is git-tracked.
|
|
141
|
+
* 3. **Inconclusive**: the anchor exists but cannot be listed, or it is absent
|
|
142
|
+
* from a *shallow* clone where truncated history could be hiding it. Fails
|
|
143
|
+
* **closed** — the gate applies. Without the shallow check, `git clone
|
|
144
|
+
* --depth 1` of this very repository would drop to the forgeable date
|
|
145
|
+
* fallback and reopen the bypass on the local promote/finalize write path.
|
|
146
|
+
*
|
|
147
|
+
* Net effect: strictly hardens the gate where it is enforced, and regresses
|
|
148
|
+
* nothing anywhere else.
|
|
149
|
+
*/
|
|
150
|
+
export function isBlueprintExemptFromRollout(input) {
|
|
151
|
+
const { repoRoot, slug, created, rolloutDate } = input;
|
|
152
|
+
const anchorCommit = input.anchorCommit ?? ROLLOUT_ANCHOR_COMMIT;
|
|
153
|
+
if (repoRoot) {
|
|
154
|
+
const lookup = blueprintSlugsAtRolloutAnchor(repoRoot, anchorCommit);
|
|
155
|
+
if (lookup.kind === "slugs")
|
|
156
|
+
return slug.length > 0 && lookup.slugs.has(slug);
|
|
157
|
+
if (lookup.kind === "unreadable")
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
return createdPredatesRollout(created, rolloutDate);
|
|
161
|
+
}
|
|
@@ -48,6 +48,7 @@ export declare class BlueprintCreationService {
|
|
|
48
48
|
resolveDraftSlug(input: CreateBlueprintDraftInput): string;
|
|
49
49
|
compileDraft(input: CreateBlueprintDraftInput): Promise<CompiledBlueprintDraft>;
|
|
50
50
|
create(input: CreateBlueprintDraftInput): Promise<CreatedBlueprintDraft>;
|
|
51
|
+
private reserveDraftSlugFolder;
|
|
51
52
|
private resolveCollisionSafeSlug;
|
|
52
53
|
}
|
|
53
54
|
export {};
|