@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
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import matter from "gray-matter";
|
|
8
8
|
import { planFrontmatterSchema, taskStatusSchema } from "#core/schema";
|
|
9
|
+
import { buildTaskHeadingRegex } from "#markdown/task-heading";
|
|
9
10
|
function parseWithSchema(markdown, schema) {
|
|
10
11
|
const { data: rawData, content } = matter(markdown);
|
|
11
12
|
const data = schema.parse(rawData);
|
|
@@ -74,14 +75,109 @@ export function serializeBlueprint(blueprint) {
|
|
|
74
75
|
delete data.completed_at;
|
|
75
76
|
}
|
|
76
77
|
const { tasks: _tasks, task_statuses: _task_statuses, ...cleanedData } = data;
|
|
77
|
-
|
|
78
|
+
const serialized = quoteGeneratedProgressScalar(matter.stringify(content, cleanedData), blueprint.progress);
|
|
79
|
+
return preserveOriginalFrontmatterMultiline(preserveOriginalFrontmatterScalars(serialized, blueprint.raw), blueprint.raw);
|
|
78
80
|
}
|
|
81
|
+
// Frontmatter keys serializeBlueprint intentionally rewrites; every other
|
|
82
|
+
// scalar must survive a write byte-for-byte.
|
|
83
|
+
const SERIALIZE_OWNED_FRONTMATTER_KEYS = new Set([
|
|
84
|
+
"status",
|
|
85
|
+
"complexity",
|
|
86
|
+
"last_updated",
|
|
87
|
+
"progress",
|
|
88
|
+
"completed_at",
|
|
89
|
+
]);
|
|
90
|
+
const FRONTMATTER_SCALAR_LINE = /^([A-Za-z0-9_]+):[ \t]+\S.*$/;
|
|
79
91
|
function quoteGeneratedProgressScalar(serialized, progress) {
|
|
80
92
|
if (!progress)
|
|
81
93
|
return serialized;
|
|
82
94
|
const escaped = progress.replace(/'/g, "''");
|
|
83
95
|
return serialized.replace(/^progress: .+$/m, `progress: '${escaped}'`);
|
|
84
96
|
}
|
|
97
|
+
// `matter.stringify` re-dumps the whole frontmatter YAML, which canonicalizes
|
|
98
|
+
// scalar quoting (e.g. `title: "X"` -> `title: X`, `owner: "claude"` ->
|
|
99
|
+
// `owner: claude`). Those bytes feed the plan-subject digest, so a cosmetic
|
|
100
|
+
// re-quote on a write silently invalidates a recorded review approval. Restore
|
|
101
|
+
// the ORIGINAL single-line scalar verbatim for every field this serializer does
|
|
102
|
+
// not own, so a status/progress write never disturbs `title`/`owner`/`created`
|
|
103
|
+
// etc. Multi-line/array fields are restored separately by
|
|
104
|
+
// preserveOriginalFrontmatterMultiline. Generalizes the earlier progress-only patch.
|
|
105
|
+
function preserveOriginalFrontmatterScalars(serialized, originalRaw) {
|
|
106
|
+
const originalFrontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(originalRaw)?.[1] ?? "";
|
|
107
|
+
const originalScalars = new Map();
|
|
108
|
+
for (const line of originalFrontmatter.split("\n")) {
|
|
109
|
+
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
110
|
+
if (match && !SERIALIZE_OWNED_FRONTMATTER_KEYS.has(match[1])) {
|
|
111
|
+
originalScalars.set(match[1], line);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (originalScalars.size === 0)
|
|
115
|
+
return serialized;
|
|
116
|
+
const block = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/.exec(serialized);
|
|
117
|
+
if (!block)
|
|
118
|
+
return serialized;
|
|
119
|
+
const rebuilt = block[2]
|
|
120
|
+
.split("\n")
|
|
121
|
+
.map((line) => {
|
|
122
|
+
const match = FRONTMATTER_SCALAR_LINE.exec(line);
|
|
123
|
+
return (match && originalScalars.get(match[1])) || line;
|
|
124
|
+
})
|
|
125
|
+
.join("\n");
|
|
126
|
+
return `${serialized.slice(0, block.index)}${block[1]}${rebuilt}${block[3]}${serialized.slice(block.index + block[0].length)}`;
|
|
127
|
+
}
|
|
128
|
+
// Same residual as scalars, for multi-line blocks (YAML arrays/maps under a
|
|
129
|
+
// key with no inline value). `matter.stringify` re-dumps arrays and drops
|
|
130
|
+
// item quotes (`- "review"` -> `- review`). MCP put and the CLI disagree on
|
|
131
|
+
// that quoting; restore the ORIGINAL multi-line block for every non-owned key.
|
|
132
|
+
// Allow optional CR on the key line so CRLF checkouts still match.
|
|
133
|
+
const FRONTMATTER_MULTILINE_KEY = /^([A-Za-z0-9_]+):[ \t]*\r?$/;
|
|
134
|
+
function preserveOriginalFrontmatterMultiline(serialized, originalRaw) {
|
|
135
|
+
const originalFrontmatter = /^---\r?\n([\s\S]*?)\r?\n---/.exec(originalRaw)?.[1] ?? "";
|
|
136
|
+
const originalBlocks = new Map();
|
|
137
|
+
const lines = originalFrontmatter.split("\n");
|
|
138
|
+
for (let index = 0; index < lines.length;) {
|
|
139
|
+
const match = FRONTMATTER_MULTILINE_KEY.exec(lines[index]);
|
|
140
|
+
if (!match) {
|
|
141
|
+
index += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
const key = match[1];
|
|
145
|
+
if (SERIALIZE_OWNED_FRONTMATTER_KEYS.has(key)) {
|
|
146
|
+
index += 1;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
// Strip a trailing CR so joined blocks stay LF-normalized like the rest of the pipeline.
|
|
150
|
+
const blockLines = [lines[index].replace(/\r$/, "")];
|
|
151
|
+
index += 1;
|
|
152
|
+
while (index < lines.length && /^[ \t]/.test(lines[index])) {
|
|
153
|
+
blockLines.push(lines[index].replace(/\r$/, ""));
|
|
154
|
+
index += 1;
|
|
155
|
+
}
|
|
156
|
+
if (blockLines.length > 1) {
|
|
157
|
+
originalBlocks.set(key, blockLines.join("\n"));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (originalBlocks.size === 0)
|
|
161
|
+
return serialized;
|
|
162
|
+
const wrapper = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/.exec(serialized);
|
|
163
|
+
if (!wrapper)
|
|
164
|
+
return serialized;
|
|
165
|
+
let body = wrapper[2];
|
|
166
|
+
for (const [key, originalBlock] of originalBlocks) {
|
|
167
|
+
// Match the serialized multi-line block. A trailing newline is optional when
|
|
168
|
+
// the block is the last key in the frontmatter body (wrapper[2] does not
|
|
169
|
+
// include the newline before `---`).
|
|
170
|
+
const pattern = new RegExp(String.raw `^${key}:[ \t]*\n(?:[ \t][^\n]*\n?)*`, "mu");
|
|
171
|
+
if (!pattern.test(body))
|
|
172
|
+
continue;
|
|
173
|
+
// Replacer function: avoid `$` re-interpretation of originalBlock contents,
|
|
174
|
+
// and only re-append a trailing newline when the match consumed one — otherwise
|
|
175
|
+
// a last-key `tags:` block gains a blank line before `---` and the plan digest
|
|
176
|
+
// flips on the first lifecycle write after MCP put (B1).
|
|
177
|
+
body = body.replace(pattern, (matched) => matched.endsWith("\n") ? `${originalBlock}\n` : originalBlock);
|
|
178
|
+
}
|
|
179
|
+
return `${serialized.slice(0, wrapper.index)}${wrapper[1]}${body}${wrapper[3]}${serialized.slice(wrapper.index + wrapper[0].length)}`;
|
|
180
|
+
}
|
|
85
181
|
function extractCheckboxStatus(section) {
|
|
86
182
|
const checkboxRegex = /^- \[([ x])\]/gm;
|
|
87
183
|
const matches = Array.from(section.matchAll(checkboxRegex));
|
|
@@ -136,7 +232,10 @@ function validateTaskFormat(content) {
|
|
|
136
232
|
}
|
|
137
233
|
}
|
|
138
234
|
function extractTasks(content) {
|
|
139
|
-
|
|
235
|
+
// Built from #markdown/task-heading's shared pattern (also consumed by
|
|
236
|
+
// #core/schema's taskIdSchema) so the reader and the MCP put-task writer
|
|
237
|
+
// cannot drift apart on what counts as a valid task ID.
|
|
238
|
+
const taskRegex = buildTaskHeadingRegex();
|
|
140
239
|
const matches = Array.from(content.matchAll(taskRegex));
|
|
141
240
|
validateTaskFormat(content);
|
|
142
241
|
return matches.map((match, index) => {
|
|
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|
|
11
11
|
*/
|
|
12
12
|
import { executionBackendSchema } from "#types/execution-backend.js";
|
|
13
13
|
export { executionBackendSchema } from "#types/execution-backend.js";
|
|
14
|
+
import { TASK_ID_PATTERN as TASK_HEADING_TASK_ID_PATTERN } from "#markdown/task-heading";
|
|
14
15
|
/**
|
|
15
16
|
* Valid plan status values.
|
|
16
17
|
* Maps to plan lifecycle: draft/planned/parked → in-progress → completed/archived.
|
|
@@ -58,6 +59,31 @@ export const crossRepoDependencySchema = z.object({
|
|
|
58
59
|
slug: z.string().min(1),
|
|
59
60
|
require_status: planStatusSchema.optional(),
|
|
60
61
|
});
|
|
62
|
+
/**
|
|
63
|
+
* Task-ID validator for MCP writer boundaries (e.g. `wp_blueprint_put`).
|
|
64
|
+
*
|
|
65
|
+
* Built from `#markdown/task-heading`'s `TASK_ID_PATTERN` — the existing
|
|
66
|
+
* single source of truth for the numeric-dotted task-ID shape (e.g. "1.1",
|
|
67
|
+
* "2.3.1") already consumed by `#core/parser`'s `extractTasks()` (via
|
|
68
|
+
* `buildTaskHeadingRegex()`) and by `verification.ts` / `markdown/helpers.ts`.
|
|
69
|
+
* Validating writer-supplied IDs against the same pattern the reader uses
|
|
70
|
+
* means the writer (MCP `put`) and the reader (parser) cannot drift apart —
|
|
71
|
+
* a non-numeric-dotted ID like "task-1" is rejected before it ever reaches
|
|
72
|
+
* markdown, instead of being silently unparseable afterward.
|
|
73
|
+
*/
|
|
74
|
+
const TASK_ID_ANCHORED_PATTERN = new RegExp(`^${TASK_HEADING_TASK_ID_PATTERN}$`);
|
|
75
|
+
export const taskIdSchema = z
|
|
76
|
+
.string()
|
|
77
|
+
.min(1)
|
|
78
|
+
.superRefine((value, ctx) => {
|
|
79
|
+
if (!TASK_ID_ANCHORED_PATTERN.test(value)) {
|
|
80
|
+
ctx.addIssue({
|
|
81
|
+
code: z.ZodIssueCode.custom,
|
|
82
|
+
message: `Task IDs must be numeric dotted (e.g. "1.1", "2.3.1") — the blueprint parser only ` +
|
|
83
|
+
`recognizes "#### Task <numeric-dotted-id>:" headers. Use "1.1", got "${value}".`,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
});
|
|
61
87
|
/**
|
|
62
88
|
* Plan frontmatter schema.
|
|
63
89
|
*
|
|
@@ -3,3 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import type { CriteriaResult } from "#core/types";
|
|
5
5
|
export declare function checkAcceptanceCriteria(markdown: string): CriteriaResult;
|
|
6
|
+
/**
|
|
7
|
+
* Count checked acceptance-criteria checkboxes that are NOT explained by an
|
|
8
|
+
* explicit `**Status:** done`/`dropped` line on their containing task.
|
|
9
|
+
*
|
|
10
|
+
* `completeTask()` (see `markdown/helpers.ts`) checks every checkbox inside
|
|
11
|
+
* a task's own section when that task is marked done — which is expected,
|
|
12
|
+
* honest progress tracking during `draft` (pretool-guard's `blueprint-first`
|
|
13
|
+
* validator explicitly allows implementing source while a blueprint is
|
|
14
|
+
* still draft). A checked box that is NOT inside a done/dropped task section
|
|
15
|
+
* — e.g. the top-level `## Approvals` checklist, or a task still `todo` with
|
|
16
|
+
* pre-checked boxes — still signals an unreviewed completion claim and must
|
|
17
|
+
* keep failing the draft anti-masquerade guard.
|
|
18
|
+
*/
|
|
19
|
+
export declare function countUnexplainedCheckedCriteria(markdown: string): number;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Check acceptance criteria completion in plan markdown.
|
|
3
3
|
*/
|
|
4
|
+
const CHECKBOX_REGEX = /^- \[([ x])\]/gm;
|
|
5
|
+
const TASK_HEADER_REGEX = /^####\s+(?:\[[^\]]+\]\s+)?Task\s+\d+(?:\.\d+)+:.*$/gm;
|
|
6
|
+
const TASK_DONE_OR_DROPPED_REGEX = /^\*\*Status:\*\*\s*(done|dropped)\s*$/im;
|
|
7
|
+
const SECTION_BOUNDARY_REGEX = /\n##\s|\n---\n/;
|
|
4
8
|
export function checkAcceptanceCriteria(markdown) {
|
|
5
|
-
const
|
|
6
|
-
const matches = Array.from(markdown.matchAll(criteriaRegex));
|
|
9
|
+
const matches = Array.from(markdown.matchAll(CHECKBOX_REGEX));
|
|
7
10
|
const total = matches.length;
|
|
8
11
|
const checked = matches.filter((m) => m[1] === "x").length;
|
|
9
12
|
return {
|
|
@@ -12,3 +15,39 @@ export function checkAcceptanceCriteria(markdown) {
|
|
|
12
15
|
allChecked: total === 0 || checked === total,
|
|
13
16
|
};
|
|
14
17
|
}
|
|
18
|
+
function countChecked(text) {
|
|
19
|
+
return Array.from(text.matchAll(CHECKBOX_REGEX)).filter((m) => m[1] === "x").length;
|
|
20
|
+
}
|
|
21
|
+
function taskSectionEnd(markdown, start, nextTaskStart) {
|
|
22
|
+
const rest = markdown.slice(start);
|
|
23
|
+
const boundaryMatch = rest.match(SECTION_BOUNDARY_REGEX);
|
|
24
|
+
const boundaryIndex = boundaryMatch?.index === undefined ? markdown.length : start + boundaryMatch.index + 1;
|
|
25
|
+
return Math.min(nextTaskStart, boundaryIndex);
|
|
26
|
+
}
|
|
27
|
+
function sumCheckedInDoneOrDroppedTasks(markdown) {
|
|
28
|
+
const headers = Array.from(markdown.matchAll(TASK_HEADER_REGEX));
|
|
29
|
+
return headers.reduce((sum, header, index) => {
|
|
30
|
+
const start = header.index ?? 0;
|
|
31
|
+
const nextStart = headers[index + 1]?.index ?? markdown.length;
|
|
32
|
+
const section = markdown.slice(start, taskSectionEnd(markdown, start, nextStart));
|
|
33
|
+
return TASK_DONE_OR_DROPPED_REGEX.test(section) ? sum + countChecked(section) : sum;
|
|
34
|
+
}, 0);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Count checked acceptance-criteria checkboxes that are NOT explained by an
|
|
38
|
+
* explicit `**Status:** done`/`dropped` line on their containing task.
|
|
39
|
+
*
|
|
40
|
+
* `completeTask()` (see `markdown/helpers.ts`) checks every checkbox inside
|
|
41
|
+
* a task's own section when that task is marked done — which is expected,
|
|
42
|
+
* honest progress tracking during `draft` (pretool-guard's `blueprint-first`
|
|
43
|
+
* validator explicitly allows implementing source while a blueprint is
|
|
44
|
+
* still draft). A checked box that is NOT inside a done/dropped task section
|
|
45
|
+
* — e.g. the top-level `## Approvals` checklist, or a task still `todo` with
|
|
46
|
+
* pre-checked boxes — still signals an unreviewed completion claim and must
|
|
47
|
+
* keep failing the draft anti-masquerade guard.
|
|
48
|
+
*/
|
|
49
|
+
export function countUnexplainedCheckedCriteria(markdown) {
|
|
50
|
+
const { checked } = checkAcceptanceCriteria(markdown);
|
|
51
|
+
const explained = sumCheckedInDoneOrDroppedTasks(markdown);
|
|
52
|
+
return Math.max(0, checked - explained);
|
|
53
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Validate plan state consistency with folder structure and checkbox status.
|
|
3
3
|
*/
|
|
4
4
|
import { planStatusSchema } from "#core/schema";
|
|
5
|
-
import { checkAcceptanceCriteria } from "./criteria.js";
|
|
5
|
+
import { checkAcceptanceCriteria, countUnexplainedCheckedCriteria } from "./criteria.js";
|
|
6
6
|
/**
|
|
7
7
|
* Validate plan is in completed/ folder if status is completed.
|
|
8
8
|
*/
|
|
@@ -22,7 +22,9 @@ function validateCompletedFolder(status, isInCompleted, criteria) {
|
|
|
22
22
|
if (status === "completed" && !criteria.allChecked) {
|
|
23
23
|
return {
|
|
24
24
|
valid: false,
|
|
25
|
-
error: `Plan has status: completed but only ${criteria.checked}/${criteria.total} acceptance
|
|
25
|
+
error: `Plan has status: completed but only ${criteria.checked}/${criteria.total} acceptance ` +
|
|
26
|
+
`criteria are checked. Check the remaining boxes once that work is genuinely done, or ` +
|
|
27
|
+
`set status back to in-progress until it is.`,
|
|
26
28
|
};
|
|
27
29
|
}
|
|
28
30
|
return { valid: true };
|
|
@@ -30,7 +32,7 @@ function validateCompletedFolder(status, isInCompleted, criteria) {
|
|
|
30
32
|
/**
|
|
31
33
|
* Validate plan is in draft/ folder if status is draft.
|
|
32
34
|
*/
|
|
33
|
-
function validateDraftFolder(status, isInDraft,
|
|
35
|
+
function validateDraftFolder(status, isInDraft, markdown) {
|
|
34
36
|
if (status === "draft" && !isInDraft) {
|
|
35
37
|
return {
|
|
36
38
|
valid: false,
|
|
@@ -43,11 +45,20 @@ function validateDraftFolder(status, isInDraft, criteria) {
|
|
|
43
45
|
error: `Plan is in draft/ folder but has status: ${status} (expected status: draft)`,
|
|
44
46
|
};
|
|
45
47
|
}
|
|
46
|
-
if (status === "draft"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
if (status === "draft") {
|
|
49
|
+
const unexplained = countUnexplainedCheckedCriteria(markdown);
|
|
50
|
+
if (unexplained > 0) {
|
|
51
|
+
return {
|
|
52
|
+
valid: false,
|
|
53
|
+
error: `Plan has status: draft but ${unexplained} acceptance criteria are checked outside ` +
|
|
54
|
+
`a task marked **Status:** done/dropped (expected 0). Marking a task's own status ` +
|
|
55
|
+
`done/dropped legitimately checks its boxes during draft implementation; a checked ` +
|
|
56
|
+
`box elsewhere (e.g. the Approvals checklist, or a still-todo task) claims unreviewed ` +
|
|
57
|
+
`completion. Fix by either setting the owning task's **Status:** to done/dropped if ` +
|
|
58
|
+
`the work is genuinely finished, unchecking the box if it isn't, or promoting the ` +
|
|
59
|
+
`blueprint with \`wp blueprint promote <slug>\` once it has been reviewed.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
63
|
return { valid: true };
|
|
53
64
|
}
|
|
@@ -72,11 +83,11 @@ function validateParkedFolder(status, isInParked) {
|
|
|
72
83
|
/**
|
|
73
84
|
* Validate state without path information.
|
|
74
85
|
*/
|
|
75
|
-
function validateStateOnly(status, criteria) {
|
|
86
|
+
function validateStateOnly(status, markdown, criteria) {
|
|
76
87
|
if (status === "completed" && !criteria.allChecked) {
|
|
77
88
|
return { valid: false, error: "Plan completed but criteria not met" };
|
|
78
89
|
}
|
|
79
|
-
if (status === "draft" &&
|
|
90
|
+
if (status === "draft" && countUnexplainedCheckedCriteria(markdown) > 0) {
|
|
80
91
|
return { valid: false, error: "Plan draft but criteria checked" };
|
|
81
92
|
}
|
|
82
93
|
return { valid: true };
|
|
@@ -84,14 +95,14 @@ function validateStateOnly(status, criteria) {
|
|
|
84
95
|
/**
|
|
85
96
|
* Validate state with path information.
|
|
86
97
|
*/
|
|
87
|
-
function validateStateWithPath(status, filePath, criteria) {
|
|
98
|
+
function validateStateWithPath(status, filePath, markdown, criteria) {
|
|
88
99
|
const isInCompleted = filePath.includes("/completed/") || filePath.includes("completed/");
|
|
89
100
|
const isInDraft = filePath.includes("/draft/") || filePath.includes("draft/");
|
|
90
101
|
const isInParked = filePath.includes("/parked/") || filePath.includes("parked/");
|
|
91
102
|
const completedResult = validateCompletedFolder(status, isInCompleted, criteria);
|
|
92
103
|
if (!completedResult.valid)
|
|
93
104
|
return completedResult;
|
|
94
|
-
const draftResult = validateDraftFolder(status, isInDraft,
|
|
105
|
+
const draftResult = validateDraftFolder(status, isInDraft, markdown);
|
|
95
106
|
if (!draftResult.valid)
|
|
96
107
|
return draftResult;
|
|
97
108
|
const parkedResult = validateParkedFolder(status, isInParked);
|
|
@@ -116,7 +127,7 @@ export function validatePlanState(markdown, filePath) {
|
|
|
116
127
|
}
|
|
117
128
|
const criteria = checkAcceptanceCriteria(markdown);
|
|
118
129
|
if (!filePath) {
|
|
119
|
-
return validateStateOnly(status, criteria);
|
|
130
|
+
return validateStateOnly(status, markdown, criteria);
|
|
120
131
|
}
|
|
121
|
-
return validateStateWithPath(status, filePath, criteria);
|
|
132
|
+
return validateStateWithPath(status, filePath, markdown, criteria);
|
|
122
133
|
}
|
|
@@ -4,8 +4,10 @@ import { findPackageAsset } from "#utils/package-assets.js";
|
|
|
4
4
|
const PACKAGED_MIGRATIONS_RELATIVE_DIR = "dist/esm/blueprint/db/migrations";
|
|
5
5
|
const SOURCE_MIGRATIONS_RELATIVE_DIR = "src/blueprint/db/migrations";
|
|
6
6
|
function resolveMigrationsDir() {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
// Source checkouts may rebuild dist concurrently in another test project.
|
|
8
|
+
// Prefer immutable authoring SQL there; published packages fall back to dist.
|
|
9
|
+
const found = findPackageAsset(SOURCE_MIGRATIONS_RELATIVE_DIR) ??
|
|
10
|
+
findPackageAsset(PACKAGED_MIGRATIONS_RELATIVE_DIR);
|
|
9
11
|
if (found)
|
|
10
12
|
return found;
|
|
11
13
|
throw new Error(`Missing blueprint migrations at ${PACKAGED_MIGRATIONS_RELATIVE_DIR} or ${SOURCE_MIGRATIONS_RELATIVE_DIR}. ` +
|
|
@@ -43,6 +43,7 @@ export declare function countDistinctLogBackedApprovals(file: string, approvals:
|
|
|
43
43
|
export interface ApprovalGateEvaluation {
|
|
44
44
|
distinct: number;
|
|
45
45
|
openCodeGoModelReviewers: string[];
|
|
46
|
+
overrideReviewers: string[];
|
|
46
47
|
satisfied: boolean;
|
|
47
48
|
strongReviewers: string[];
|
|
48
49
|
}
|
|
@@ -59,5 +60,24 @@ export declare function validateApprovalGate(file: string, frontmatter: Lifecycl
|
|
|
59
60
|
completedApprovalGate?: boolean;
|
|
60
61
|
inProgressApprovalGate?: boolean;
|
|
61
62
|
}): BlueprintAuditIssue[];
|
|
63
|
+
/**
|
|
64
|
+
* Write-path mirror of {@link validateApprovalGate}'s `completed` branch.
|
|
65
|
+
*
|
|
66
|
+
* There is no single completion chokepoint: this helper is called from all
|
|
67
|
+
* three write-path owners that can set `status: completed` — the MCP transition
|
|
68
|
+
* funnel `applyLocalBlueprintTransition` (finalize/promote/transition), the CLI
|
|
69
|
+
* `promoteBlueprintLocked` (which does not traverse that funnel), and the
|
|
70
|
+
* execution engine's `finalize` intent via `applyBlueprintLifecycleToFile`
|
|
71
|
+
* (exec/resume/stop/move). Calling it before each write guarantees a blueprint
|
|
72
|
+
* can only reach `completed` when it carries the same plan-purpose approval CI's
|
|
73
|
+
* `wp audit blueprint-lifecycle` requires afterward. It delegates to
|
|
74
|
+
* {@link validateApprovalGate} verbatim — including the pre-rollout grandfather
|
|
75
|
+
* ({@link predatesCompletedApprovalGate}) and the v2 `review-events.jsonl`
|
|
76
|
+
* ledger — so the write-path and the audit-path can never diverge and a
|
|
77
|
+
* finalized blueprint can never fail its own lifecycle audit.
|
|
78
|
+
*
|
|
79
|
+
* @throws Error when no plan-purpose approval satisfies the promotion reviewer mix.
|
|
80
|
+
*/
|
|
81
|
+
export declare function assertCompletedApprovalGate(file: string, markdown: string): void;
|
|
62
82
|
export declare function runBlueprintAudit(options: RunBlueprintAuditOptions): Promise<BlueprintAuditResult>;
|
|
63
83
|
export {};
|
|
@@ -12,7 +12,9 @@ import { scanBlueprintDirectory } from "#service/scanner";
|
|
|
12
12
|
import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
13
13
|
import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
|
|
14
14
|
import { relativeBlueprintSlug } from "#lifecycle/local.js";
|
|
15
|
-
import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, } from "#lifecycle/review-provenance.js";
|
|
15
|
+
import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, stableSlugForBlueprintFile, } from "#lifecycle/review-provenance.js";
|
|
16
|
+
import { isBlueprintExemptFromRollout, ROLLOUT_DATE } from "#lifecycle/rollout-anchor.js";
|
|
17
|
+
import { MAINTAINER_OVERRIDE_REVIEWER } from "#review/events.js";
|
|
16
18
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
17
19
|
function isBlueprintOverview(file) {
|
|
18
20
|
const normalized = file.replace(/\\/g, "/");
|
|
@@ -50,14 +52,25 @@ function readLifecycleAuditFrontmatter(raw) {
|
|
|
50
52
|
* blueprints that predate those changed-state gates.
|
|
51
53
|
*/
|
|
52
54
|
const APPROVAL_GATED_STATUSES = new Set(["planned"]);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
55
|
+
/**
|
|
56
|
+
* True when this blueprint predates the completed-approval gate and is exempt.
|
|
57
|
+
*
|
|
58
|
+
* Inside this repository the rollout anchor commit decides and the
|
|
59
|
+
* author-supplied `created` is ignored, which closes a real bypass: `created`
|
|
60
|
+
* is written by the same author the gate constrains, so an old date used to
|
|
61
|
+
* disable the gate outright -- on the CI audit path and on the unconditional
|
|
62
|
+
* `assertCompletedApprovalGate` write path, letting a blueprint reach
|
|
63
|
+
* `completed` with no approval at all. Outside this repository the anchor
|
|
64
|
+
* cannot answer the question and the legacy date compare still applies. See
|
|
65
|
+
* `#lifecycle/rollout-anchor.js` for the full resolution order.
|
|
66
|
+
*/
|
|
67
|
+
function predatesCompletedApprovalGate(file, created) {
|
|
68
|
+
return isBlueprintExemptFromRollout({
|
|
69
|
+
repoRoot: findGitRoot(file),
|
|
70
|
+
slug: stableSlugForBlueprintFile(file),
|
|
71
|
+
created,
|
|
72
|
+
rolloutDate: ROLLOUT_DATE,
|
|
73
|
+
});
|
|
61
74
|
}
|
|
62
75
|
/**
|
|
63
76
|
* Promotion gate: a blueprint past `draft` must carry either one provenance-backed
|
|
@@ -94,11 +107,13 @@ function approvalReviewerKey(reviewer) {
|
|
|
94
107
|
return parts.at(-1) ?? normalized;
|
|
95
108
|
}
|
|
96
109
|
function provenanceBackedApprovalReviewers(file, approvals, options = {}) {
|
|
97
|
-
const
|
|
98
|
-
|
|
110
|
+
const purposes = options.purpose
|
|
111
|
+
? [options.purpose]
|
|
112
|
+
: ["plan", "delivery"];
|
|
113
|
+
const legacy = purposes.includes("plan")
|
|
99
114
|
? collectTrackedReviewProvenance(file, approvals, options).map((record) => record.reviewer)
|
|
100
115
|
: [];
|
|
101
|
-
const current = collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer);
|
|
116
|
+
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
102
117
|
return new Set([...legacy, ...current]);
|
|
103
118
|
}
|
|
104
119
|
export function countDistinctProvenanceBackedApprovals(file, approvals) {
|
|
@@ -116,15 +131,24 @@ export function evaluateApprovalReviewers(reviewerIds) {
|
|
|
116
131
|
const openCodeGoModelReviewers = reviewers
|
|
117
132
|
.map(approvalReviewerKey)
|
|
118
133
|
.filter((reviewer) => OPENCODE_GO_MODEL_APPROVAL_REVIEWERS.has(reviewer));
|
|
134
|
+
const overrideReviewers = reviewers
|
|
135
|
+
.map(approvalReviewerKey)
|
|
136
|
+
.filter((reviewer) => reviewer === MAINTAINER_OVERRIDE_REVIEWER);
|
|
119
137
|
return {
|
|
120
138
|
distinct: reviewers.length,
|
|
121
139
|
openCodeGoModelReviewers: [...new Set(openCodeGoModelReviewers)].sort(),
|
|
122
|
-
|
|
140
|
+
overrideReviewers: [...new Set(overrideReviewers)].sort(),
|
|
141
|
+
satisfied: strongReviewers.length >= 1 ||
|
|
142
|
+
new Set(openCodeGoModelReviewers).size >= 1 ||
|
|
143
|
+
new Set(overrideReviewers).size >= 1,
|
|
123
144
|
strongReviewers: [...new Set(strongReviewers)].sort(),
|
|
124
145
|
};
|
|
125
146
|
}
|
|
126
147
|
export function formatApprovalGateRequirement(evaluation) {
|
|
127
|
-
|
|
148
|
+
const override = evaluation.overrideReviewers.length >= 1
|
|
149
|
+
? ` + ${evaluation.overrideReviewers.length} maintainer-override(s)`
|
|
150
|
+
: "";
|
|
151
|
+
return `need 1 provenance-backed Claude/Codex/Grok or OpenCode-Go model approval (or a recorded maintainer-override); currently have ${evaluation.strongReviewers.length} Claude/Codex/Grok approval(s), ${evaluation.openCodeGoModelReviewers.length} OpenCode-Go model approval(s)${override}, ${evaluation.distinct} distinct provenance-backed approval(s) total`;
|
|
128
152
|
}
|
|
129
153
|
export function validateApprovalGate(file, frontmatter, options = {}) {
|
|
130
154
|
if (frontmatter.type !== "blueprint")
|
|
@@ -132,7 +156,7 @@ export function validateApprovalGate(file, frontmatter, options = {}) {
|
|
|
132
156
|
const status = typeof frontmatter.status === "string" ? frontmatter.status : "";
|
|
133
157
|
if (status === "completed" &&
|
|
134
158
|
options.completedApprovalGate === true &&
|
|
135
|
-
predatesCompletedApprovalGate(frontmatter.created)) {
|
|
159
|
+
predatesCompletedApprovalGate(file, frontmatter.created)) {
|
|
136
160
|
return [];
|
|
137
161
|
}
|
|
138
162
|
const gateApplies = APPROVAL_GATED_STATUSES.has(status) ||
|
|
@@ -162,6 +186,31 @@ export function validateApprovalGate(file, frontmatter, options = {}) {
|
|
|
162
186
|
message: `Review evidence integrity: ${issue}`,
|
|
163
187
|
}));
|
|
164
188
|
}
|
|
189
|
+
/**
|
|
190
|
+
* Write-path mirror of {@link validateApprovalGate}'s `completed` branch.
|
|
191
|
+
*
|
|
192
|
+
* There is no single completion chokepoint: this helper is called from all
|
|
193
|
+
* three write-path owners that can set `status: completed` — the MCP transition
|
|
194
|
+
* funnel `applyLocalBlueprintTransition` (finalize/promote/transition), the CLI
|
|
195
|
+
* `promoteBlueprintLocked` (which does not traverse that funnel), and the
|
|
196
|
+
* execution engine's `finalize` intent via `applyBlueprintLifecycleToFile`
|
|
197
|
+
* (exec/resume/stop/move). Calling it before each write guarantees a blueprint
|
|
198
|
+
* can only reach `completed` when it carries the same plan-purpose approval CI's
|
|
199
|
+
* `wp audit blueprint-lifecycle` requires afterward. It delegates to
|
|
200
|
+
* {@link validateApprovalGate} verbatim — including the pre-rollout grandfather
|
|
201
|
+
* ({@link predatesCompletedApprovalGate}) and the v2 `review-events.jsonl`
|
|
202
|
+
* ledger — so the write-path and the audit-path can never diverge and a
|
|
203
|
+
* finalized blueprint can never fail its own lifecycle audit.
|
|
204
|
+
*
|
|
205
|
+
* @throws Error when no plan-purpose approval satisfies the promotion reviewer mix.
|
|
206
|
+
*/
|
|
207
|
+
export function assertCompletedApprovalGate(file, markdown) {
|
|
208
|
+
const frontmatter = readLifecycleAuditFrontmatter(markdown);
|
|
209
|
+
const errors = validateApprovalGate(file, { ...frontmatter, status: "completed" }, { completedApprovalGate: true }).filter((issue) => issue.level === "error");
|
|
210
|
+
if (errors.length > 0) {
|
|
211
|
+
throw new Error(errors.map((issue) => issue.message).join("; "));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
165
214
|
function countTaskHeadings(raw) {
|
|
166
215
|
return raw.match(/^####\s+(?:\[[^\]]+\]\s+)?Task\s+/gm)?.length ?? 0;
|
|
167
216
|
}
|
|
@@ -226,7 +275,13 @@ function validateTaskState(blueprint) {
|
|
|
226
275
|
}
|
|
227
276
|
return issues;
|
|
228
277
|
}
|
|
229
|
-
function validateBlueprintSlugUniqueness(blueprints
|
|
278
|
+
function validateBlueprintSlugUniqueness(blueprints,
|
|
279
|
+
// When auditing a changed-only subset, `stagedSet` restricts reporting to
|
|
280
|
+
// duplicates a CHANGED blueprint participates in, and attributes the error to
|
|
281
|
+
// that changed file so a scope-filtering caller still surfaces it. A
|
|
282
|
+
// pre-existing duplicate among only-untouched blueprints is left for the
|
|
283
|
+
// full-corpus (CI) pass, not the changed-only hook.
|
|
284
|
+
scope) {
|
|
230
285
|
const grouped = new Map();
|
|
231
286
|
for (const blueprint of blueprints) {
|
|
232
287
|
const normalizedSlug = relativeBlueprintSlug(blueprint.slug);
|
|
@@ -239,12 +294,18 @@ function validateBlueprintSlugUniqueness(blueprints) {
|
|
|
239
294
|
}
|
|
240
295
|
}
|
|
241
296
|
const issues = [];
|
|
297
|
+
const stagedSet = scope?.stagedSet ?? null;
|
|
242
298
|
for (const [normalizedSlug, entries] of grouped) {
|
|
243
299
|
const uniqueLifecycleSlugs = new Set(entries.map((entry) => entry.slug));
|
|
244
300
|
if (uniqueLifecycleSlugs.size <= 1)
|
|
245
301
|
continue;
|
|
302
|
+
const changedEntry = stagedSet
|
|
303
|
+
? entries.find((entry) => stagedSet.has(normalizePath(path.relative(scope.projectRoot, entry.file))))
|
|
304
|
+
: undefined;
|
|
305
|
+
if (stagedSet && !changedEntry)
|
|
306
|
+
continue;
|
|
246
307
|
issues.push({
|
|
247
|
-
file: entries[0]?.file,
|
|
308
|
+
file: (changedEntry ?? entries[0])?.file,
|
|
248
309
|
level: "error",
|
|
249
310
|
message: `Blueprint slug "${normalizedSlug}" appears in multiple lifecycle locations: ${Array.from(uniqueLifecycleSlugs)
|
|
250
311
|
.toSorted()
|
|
@@ -620,10 +681,16 @@ export async function runBlueprintAudit(options) {
|
|
|
620
681
|
baseDir: resolveBlueprintRoot(options.projectRoot),
|
|
621
682
|
includeSpecialFolders: true,
|
|
622
683
|
});
|
|
623
|
-
const
|
|
684
|
+
const stagedSet = options.all || !options.stagedFiles ? null : new Set(options.stagedFiles.map(normalizePath));
|
|
685
|
+
const blueprintFiles = stagedSet === null
|
|
624
686
|
? scanned
|
|
625
|
-
: scanned.filter((entry) =>
|
|
626
|
-
|
|
687
|
+
: scanned.filter((entry) => stagedSet.has(normalizePath(path.relative(options.projectRoot, entry.path))));
|
|
688
|
+
// Slug uniqueness is a corpus-wide invariant, so validate it over ALL scanned
|
|
689
|
+
// blueprints even in changed-only mode — otherwise a changed blueprint that
|
|
690
|
+
// duplicates an *untouched* lifecycle slug would only be compared with itself
|
|
691
|
+
// and the error would vanish. Only the expensive per-file audit below is
|
|
692
|
+
// scoped to the changed subset.
|
|
693
|
+
issues.push(...validateBlueprintSlugUniqueness(scanned, { projectRoot: options.projectRoot, stagedSet }));
|
|
627
694
|
for (const entry of blueprintFiles) {
|
|
628
695
|
issues.push(...(await auditBlueprintFile(entry.path, entry.slug, options)));
|
|
629
696
|
}
|