@webpresso/agent-kit 3.1.28 → 3.2.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.
Potentially problematic release.
This version of @webpresso/agent-kit might be problematic. Click here for more details.
- package/README.md +53 -15
- package/bin/_run.js +179 -19
- package/catalog/AGENTS.md.tpl +6 -6
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/ci-cost-local-first.md +139 -0
- package/catalog/agent/rules/ci-test-perf.md +5 -1
- package/catalog/agent/rules/cmd-execution.md +9 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +30 -2
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/test-scan-perf.md +80 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/rules/workflow-skills-routing.md +4 -2
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/fix/SKILL.md +3 -0
- package/catalog/agent/skills/verify/SKILL.md +22 -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 +72 -7
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +90 -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 +11 -1
- package/dist/esm/audit/roadmap-links.js +6 -0
- package/dist/esm/audit/test-scan-perf.d.ts +18 -0
- package/dist/esm/audit/test-scan-perf.js +275 -0
- 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 +43 -1
- 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 +65 -10
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- package/dist/esm/blueprint/lifecycle/engine.d.ts +9 -0
- package/dist/esm/blueprint/lifecycle/engine.js +53 -6
- 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 +118 -12
- package/dist/esm/blueprint/markdown/helpers.js +6 -2
- 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/trust/command-runner.d.ts +26 -0
- package/dist/esm/blueprint/trust/command-runner.js +148 -0
- 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 +62 -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 +10 -0
- package/dist/esm/ci/act-replay.js +110 -7
- package/dist/esm/ci/act-runner.d.ts +4 -0
- package/dist/esm/ci/act-runner.js +85 -26
- package/dist/esm/ci/act-worktree-git.d.ts +11 -0
- package/dist/esm/ci/act-worktree-git.js +219 -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 +34 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +6 -1
- package/dist/esm/cli/commands/agent-launch.js +82 -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 +20 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +83 -47
- 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 +40 -3
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +6 -3
- package/dist/esm/cli/commands/ci.d.ts +8 -1
- package/dist/esm/cli/commands/ci.js +80 -21
- 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/dash/keymap.js +1 -0
- package/dist/esm/cli/commands/dash/launch-preferences.d.ts +15 -0
- package/dist/esm/cli/commands/dash/launch-preferences.js +46 -17
- package/dist/esm/cli/commands/dash/plan-once-label.d.ts +7 -0
- package/dist/esm/cli/commands/dash/plan-once-label.js +5 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.d.ts +67 -0
- package/dist/esm/cli/commands/dash/plan-once-membership.js +151 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.d.ts +143 -0
- package/dist/esm/cli/commands/dash/plan-once-merge.js +238 -0
- package/dist/esm/cli/commands/dash/plan-once.d.ts +22 -2
- package/dist/esm/cli/commands/dash/plan-once.js +41 -4
- package/dist/esm/cli/commands/dash/tui/attention-hud.d.ts +9 -0
- package/dist/esm/cli/commands/dash/tui/attention-hud.js +9 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/chat-bridge.js +73 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.d.ts +47 -0
- package/dist/esm/cli/commands/dash/tui/chat-mode.js +98 -0
- package/dist/esm/cli/commands/dash/tui/frame.d.ts +3 -0
- package/dist/esm/cli/commands/dash/tui/frame.js +3 -1
- package/dist/esm/cli/commands/dash/tui/palette.d.ts +1 -1
- package/dist/esm/cli/commands/dash/tui/palette.js +14 -0
- package/dist/esm/cli/commands/dash/tui/runtime.js +239 -4
- package/dist/esm/cli/commands/dash/tui/state.d.ts +41 -0
- package/dist/esm/cli/commands/dash/tui/state.js +257 -4
- package/dist/esm/cli/commands/doctor.js +5 -0
- package/dist/esm/cli/commands/format.js +0 -2
- 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.d.ts +0 -1
- package/dist/esm/cli/commands/init/config.js +3 -6
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +24 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +221 -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 +1 -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/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 +11 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +56 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.d.ts +5 -6
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/claude.js +6 -13
- 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 +51 -23
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.d.ts +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/ir.js +0 -2
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.d.ts +6 -3
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/merge.js +39 -26
- 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 +297 -173
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +7 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +58 -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/logs.d.ts +15 -0
- package/dist/esm/cli/commands/logs.js +192 -3
- package/dist/esm/cli/commands/opencode-rotate.d.ts +49 -0
- package/dist/esm/cli/commands/opencode-rotate.js +135 -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-log-store.d.ts +1 -0
- package/dist/esm/cli/commands/quality-log-store.js +187 -51
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +734 -217
- 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/secrets.d.ts +1 -0
- package/dist/esm/cli/commands/secrets.js +4 -1
- 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/worktree/router-dispatch.js +13 -8
- package/dist/esm/cli/direct-provider-launch.js +31 -3
- 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 +15 -0
- package/dist/esm/hooks/doctor.js +79 -8
- 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 +58 -4
- 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/mcp-redirect.d.ts +0 -1
- package/dist/esm/hooks/pretool-guard/validators/mcp-redirect.js +8 -5
- 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.d.ts +9 -7
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +126 -14
- 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 +23 -8
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +74 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +78 -4
- 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 +40 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- package/dist/esm/mcp/tools/ci-act.js +9 -11
- 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-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/paths/state-root.js +3 -0
- package/dist/esm/review/authority.js +30 -7
- package/dist/esm/review/availability.d.ts +22 -0
- package/dist/esm/review/availability.js +123 -23
- 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.js +26 -9
- 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 +87 -6
- package/dist/esm/review/execution/supervisor.js +5 -0
- package/dist/esm/review/execution/types.d.ts +8 -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 +27 -0
- package/dist/esm/review/subject.js +179 -32
- package/dist/esm/runtime/executor.d.ts +1 -0
- package/dist/esm/runtime/executor.js +2 -1
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/secret-gate/runner.d.ts +1 -0
- package/dist/esm/secret-gate/runner.js +4 -1
- package/dist/esm/status/snapshot.js +30 -19
- 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 -1
- package/dist/esm/test-helpers/global-setup.js +68 -24
- 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/current.d.ts +1 -0
- package/dist/esm/worktrees/current.js +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.d.ts +1 -0
- package/dist/esm/worktrees/project-agent-surfaces.js +2 -1
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -15
|
@@ -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 {};
|
|
@@ -13,6 +13,7 @@ import { resolveBlueprintRoot } from "#utils/blueprint-root";
|
|
|
13
13
|
import { parseBlueprintDocumentRelativePath } from "#utils/document-paths.js";
|
|
14
14
|
import { relativeBlueprintSlug } from "#lifecycle/local.js";
|
|
15
15
|
import { collectV2ReviewProvenance, collectTrackedReviewProvenance, normalizeTrackedReviewApprovals, } from "#lifecycle/review-provenance.js";
|
|
16
|
+
import { MAINTAINER_OVERRIDE_REVIEWER } from "#review/events.js";
|
|
16
17
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
17
18
|
function isBlueprintOverview(file) {
|
|
18
19
|
const normalized = file.replace(/\\/g, "/");
|
|
@@ -94,11 +95,13 @@ function approvalReviewerKey(reviewer) {
|
|
|
94
95
|
return parts.at(-1) ?? normalized;
|
|
95
96
|
}
|
|
96
97
|
function provenanceBackedApprovalReviewers(file, approvals, options = {}) {
|
|
97
|
-
const
|
|
98
|
-
|
|
98
|
+
const purposes = options.purpose
|
|
99
|
+
? [options.purpose]
|
|
100
|
+
: ["plan", "delivery"];
|
|
101
|
+
const legacy = purposes.includes("plan")
|
|
99
102
|
? collectTrackedReviewProvenance(file, approvals, options).map((record) => record.reviewer)
|
|
100
103
|
: [];
|
|
101
|
-
const current = collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer);
|
|
104
|
+
const current = purposes.flatMap((purpose) => collectV2ReviewProvenance(file, purpose, "HEAD", options.issuesSink).map((record) => record.reviewer));
|
|
102
105
|
return new Set([...legacy, ...current]);
|
|
103
106
|
}
|
|
104
107
|
export function countDistinctProvenanceBackedApprovals(file, approvals) {
|
|
@@ -116,15 +119,24 @@ export function evaluateApprovalReviewers(reviewerIds) {
|
|
|
116
119
|
const openCodeGoModelReviewers = reviewers
|
|
117
120
|
.map(approvalReviewerKey)
|
|
118
121
|
.filter((reviewer) => OPENCODE_GO_MODEL_APPROVAL_REVIEWERS.has(reviewer));
|
|
122
|
+
const overrideReviewers = reviewers
|
|
123
|
+
.map(approvalReviewerKey)
|
|
124
|
+
.filter((reviewer) => reviewer === MAINTAINER_OVERRIDE_REVIEWER);
|
|
119
125
|
return {
|
|
120
126
|
distinct: reviewers.length,
|
|
121
127
|
openCodeGoModelReviewers: [...new Set(openCodeGoModelReviewers)].sort(),
|
|
122
|
-
|
|
128
|
+
overrideReviewers: [...new Set(overrideReviewers)].sort(),
|
|
129
|
+
satisfied: strongReviewers.length >= 1 ||
|
|
130
|
+
new Set(openCodeGoModelReviewers).size >= 1 ||
|
|
131
|
+
new Set(overrideReviewers).size >= 1,
|
|
123
132
|
strongReviewers: [...new Set(strongReviewers)].sort(),
|
|
124
133
|
};
|
|
125
134
|
}
|
|
126
135
|
export function formatApprovalGateRequirement(evaluation) {
|
|
127
|
-
|
|
136
|
+
const override = evaluation.overrideReviewers.length >= 1
|
|
137
|
+
? ` + ${evaluation.overrideReviewers.length} maintainer-override(s)`
|
|
138
|
+
: "";
|
|
139
|
+
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
140
|
}
|
|
129
141
|
export function validateApprovalGate(file, frontmatter, options = {}) {
|
|
130
142
|
if (frontmatter.type !== "blueprint")
|
|
@@ -162,6 +174,31 @@ export function validateApprovalGate(file, frontmatter, options = {}) {
|
|
|
162
174
|
message: `Review evidence integrity: ${issue}`,
|
|
163
175
|
}));
|
|
164
176
|
}
|
|
177
|
+
/**
|
|
178
|
+
* Write-path mirror of {@link validateApprovalGate}'s `completed` branch.
|
|
179
|
+
*
|
|
180
|
+
* There is no single completion chokepoint: this helper is called from all
|
|
181
|
+
* three write-path owners that can set `status: completed` — the MCP transition
|
|
182
|
+
* funnel `applyLocalBlueprintTransition` (finalize/promote/transition), the CLI
|
|
183
|
+
* `promoteBlueprintLocked` (which does not traverse that funnel), and the
|
|
184
|
+
* execution engine's `finalize` intent via `applyBlueprintLifecycleToFile`
|
|
185
|
+
* (exec/resume/stop/move). Calling it before each write guarantees a blueprint
|
|
186
|
+
* can only reach `completed` when it carries the same plan-purpose approval CI's
|
|
187
|
+
* `wp audit blueprint-lifecycle` requires afterward. It delegates to
|
|
188
|
+
* {@link validateApprovalGate} verbatim — including the pre-rollout grandfather
|
|
189
|
+
* ({@link predatesCompletedApprovalGate}) and the v2 `review-events.jsonl`
|
|
190
|
+
* ledger — so the write-path and the audit-path can never diverge and a
|
|
191
|
+
* finalized blueprint can never fail its own lifecycle audit.
|
|
192
|
+
*
|
|
193
|
+
* @throws Error when no plan-purpose approval satisfies the promotion reviewer mix.
|
|
194
|
+
*/
|
|
195
|
+
export function assertCompletedApprovalGate(file, markdown) {
|
|
196
|
+
const frontmatter = readLifecycleAuditFrontmatter(markdown);
|
|
197
|
+
const errors = validateApprovalGate(file, { ...frontmatter, status: "completed" }, { completedApprovalGate: true }).filter((issue) => issue.level === "error");
|
|
198
|
+
if (errors.length > 0) {
|
|
199
|
+
throw new Error(errors.map((issue) => issue.message).join("; "));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
165
202
|
function countTaskHeadings(raw) {
|
|
166
203
|
return raw.match(/^####\s+(?:\[[^\]]+\]\s+)?Task\s+/gm)?.length ?? 0;
|
|
167
204
|
}
|
|
@@ -226,7 +263,13 @@ function validateTaskState(blueprint) {
|
|
|
226
263
|
}
|
|
227
264
|
return issues;
|
|
228
265
|
}
|
|
229
|
-
function validateBlueprintSlugUniqueness(blueprints
|
|
266
|
+
function validateBlueprintSlugUniqueness(blueprints,
|
|
267
|
+
// When auditing a changed-only subset, `stagedSet` restricts reporting to
|
|
268
|
+
// duplicates a CHANGED blueprint participates in, and attributes the error to
|
|
269
|
+
// that changed file so a scope-filtering caller still surfaces it. A
|
|
270
|
+
// pre-existing duplicate among only-untouched blueprints is left for the
|
|
271
|
+
// full-corpus (CI) pass, not the changed-only hook.
|
|
272
|
+
scope) {
|
|
230
273
|
const grouped = new Map();
|
|
231
274
|
for (const blueprint of blueprints) {
|
|
232
275
|
const normalizedSlug = relativeBlueprintSlug(blueprint.slug);
|
|
@@ -239,12 +282,18 @@ function validateBlueprintSlugUniqueness(blueprints) {
|
|
|
239
282
|
}
|
|
240
283
|
}
|
|
241
284
|
const issues = [];
|
|
285
|
+
const stagedSet = scope?.stagedSet ?? null;
|
|
242
286
|
for (const [normalizedSlug, entries] of grouped) {
|
|
243
287
|
const uniqueLifecycleSlugs = new Set(entries.map((entry) => entry.slug));
|
|
244
288
|
if (uniqueLifecycleSlugs.size <= 1)
|
|
245
289
|
continue;
|
|
290
|
+
const changedEntry = stagedSet
|
|
291
|
+
? entries.find((entry) => stagedSet.has(normalizePath(path.relative(scope.projectRoot, entry.file))))
|
|
292
|
+
: undefined;
|
|
293
|
+
if (stagedSet && !changedEntry)
|
|
294
|
+
continue;
|
|
246
295
|
issues.push({
|
|
247
|
-
file: entries[0]?.file,
|
|
296
|
+
file: (changedEntry ?? entries[0])?.file,
|
|
248
297
|
level: "error",
|
|
249
298
|
message: `Blueprint slug "${normalizedSlug}" appears in multiple lifecycle locations: ${Array.from(uniqueLifecycleSlugs)
|
|
250
299
|
.toSorted()
|
|
@@ -620,10 +669,16 @@ export async function runBlueprintAudit(options) {
|
|
|
620
669
|
baseDir: resolveBlueprintRoot(options.projectRoot),
|
|
621
670
|
includeSpecialFolders: true,
|
|
622
671
|
});
|
|
623
|
-
const
|
|
672
|
+
const stagedSet = options.all || !options.stagedFiles ? null : new Set(options.stagedFiles.map(normalizePath));
|
|
673
|
+
const blueprintFiles = stagedSet === null
|
|
624
674
|
? scanned
|
|
625
|
-
: scanned.filter((entry) =>
|
|
626
|
-
|
|
675
|
+
: scanned.filter((entry) => stagedSet.has(normalizePath(path.relative(options.projectRoot, entry.path))));
|
|
676
|
+
// Slug uniqueness is a corpus-wide invariant, so validate it over ALL scanned
|
|
677
|
+
// blueprints even in changed-only mode — otherwise a changed blueprint that
|
|
678
|
+
// duplicates an *untouched* lifecycle slug would only be compared with itself
|
|
679
|
+
// and the error would vanish. Only the expensive per-file audit below is
|
|
680
|
+
// scoped to the changed subset.
|
|
681
|
+
issues.push(...validateBlueprintSlugUniqueness(scanned, { projectRoot: options.projectRoot, stagedSet }));
|
|
627
682
|
for (const entry of blueprintFiles) {
|
|
628
683
|
issues.push(...(await auditBlueprintFile(entry.path, entry.slug, options)));
|
|
629
684
|
}
|
|
@@ -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
|
+
}
|
|
@@ -35,5 +35,14 @@ export interface BlueprintLifecycleResult {
|
|
|
35
35
|
progress: string;
|
|
36
36
|
targetStatus: LifecycleBlueprintStatus;
|
|
37
37
|
}
|
|
38
|
+
/**
|
|
39
|
+
* Update only the named frontmatter keys, byte-preserving every other line.
|
|
40
|
+
* Review authority binds plan approvals to normalized blueprint bytes
|
|
41
|
+
* (`normalizePlanSubjectContent`), so lifecycle writers must never re-serialize
|
|
42
|
+
* fields they do not own — a whole-document YAML round-trip (quote-style or
|
|
43
|
+
* folding changes to an untouched `title:`) would strand committed approvals
|
|
44
|
+
* mid-lifecycle. Falls back to a gray-matter round-trip only when the document
|
|
45
|
+
* has no frontmatter block at all.
|
|
46
|
+
*/
|
|
38
47
|
export declare function setBlueprintFrontmatterFields(markdown: string, updates: Record<string, string | string[] | undefined>): string;
|
|
39
48
|
export declare function applyBlueprintLifecycle(markdown: string, slug: string, intent: BlueprintLifecycleIntent): BlueprintLifecycleResult;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import matter from "gray-matter";
|
|
2
|
+
import { stringify as stringifyYaml } from "yaml";
|
|
2
3
|
import { parseBlueprint } from "#core/parser";
|
|
3
4
|
import { lifecycleBlueprintStatusSchema } from "#core/schema";
|
|
4
5
|
import { updateBlockedReason, updateTaskStatus } from "#markdown/helpers";
|
|
@@ -6,17 +7,63 @@ import { applyVerification, assertAllTasksHaveCanonicalPassingEvidence } from "#
|
|
|
6
7
|
function todayIsoDate() {
|
|
7
8
|
return new Date().toISOString().split("T")[0] ?? new Date().toISOString();
|
|
8
9
|
}
|
|
10
|
+
function serializeFrontmatterEntry(key, value) {
|
|
11
|
+
// lineWidth: 0 disables folding so long scalars stay on one line. gray-matter
|
|
12
|
+
// parses frontmatter with js-yaml, which resolves plain date-like scalars
|
|
13
|
+
// ("2026-07-21") into Date objects — quote a string whenever gray-matter
|
|
14
|
+
// would not round-trip its plain form back to the same string.
|
|
15
|
+
const plain = stringifyYaml({ [key]: value }, { lineWidth: 0 }).trimEnd();
|
|
16
|
+
if (typeof value === "string" && matter(`---\n${plain}\n---\n`).data[key] !== value) {
|
|
17
|
+
return stringifyYaml({ [key]: value }, { defaultKeyType: "PLAIN", defaultStringType: "QUOTE_DOUBLE", lineWidth: 0 })
|
|
18
|
+
.trimEnd()
|
|
19
|
+
.split("\n");
|
|
20
|
+
}
|
|
21
|
+
return plain.split("\n");
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Update only the named frontmatter keys, byte-preserving every other line.
|
|
25
|
+
* Review authority binds plan approvals to normalized blueprint bytes
|
|
26
|
+
* (`normalizePlanSubjectContent`), so lifecycle writers must never re-serialize
|
|
27
|
+
* fields they do not own — a whole-document YAML round-trip (quote-style or
|
|
28
|
+
* folding changes to an untouched `title:`) would strand committed approvals
|
|
29
|
+
* mid-lifecycle. Falls back to a gray-matter round-trip only when the document
|
|
30
|
+
* has no frontmatter block at all.
|
|
31
|
+
*/
|
|
9
32
|
export function setBlueprintFrontmatterFields(markdown, updates) {
|
|
10
|
-
const
|
|
33
|
+
const match = /^(---\r?\n)([\s\S]*?)(\r?\n---(?:\r?\n|$))/u.exec(markdown);
|
|
34
|
+
if (!match) {
|
|
35
|
+
const parsed = matter(markdown);
|
|
36
|
+
for (const [key, value] of Object.entries(updates)) {
|
|
37
|
+
if (value === undefined || value === "") {
|
|
38
|
+
delete parsed.data[key];
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
parsed.data[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return matter.stringify(parsed.content, parsed.data);
|
|
45
|
+
}
|
|
46
|
+
const lines = (match[2] ?? "").split("\n");
|
|
11
47
|
for (const [key, value] of Object.entries(updates)) {
|
|
12
|
-
|
|
13
|
-
|
|
48
|
+
const keyPattern = new RegExp(String.raw `^${key}:`, "u");
|
|
49
|
+
const start = lines.findIndex((line) => keyPattern.test(line));
|
|
50
|
+
let removeCount = 0;
|
|
51
|
+
if (start !== -1) {
|
|
52
|
+
removeCount = 1;
|
|
53
|
+
while (start + removeCount < lines.length &&
|
|
54
|
+
/^[ \t]/u.test(lines[start + removeCount] ?? "")) {
|
|
55
|
+
removeCount += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const replacement = value === undefined || value === "" ? [] : serializeFrontmatterEntry(key, value);
|
|
59
|
+
if (start !== -1) {
|
|
60
|
+
lines.splice(start, removeCount, ...replacement);
|
|
14
61
|
}
|
|
15
|
-
else {
|
|
16
|
-
|
|
62
|
+
else if (replacement.length > 0) {
|
|
63
|
+
lines.push(...replacement);
|
|
17
64
|
}
|
|
18
65
|
}
|
|
19
|
-
return
|
|
66
|
+
return `${match[1]}${lines.join("\n")}${match[3]}${markdown.slice(match[0].length)}`;
|
|
20
67
|
}
|
|
21
68
|
function assertExecutableStatus(status) {
|
|
22
69
|
const parsed = lifecycleBlueprintStatusSchema.safeParse(status);
|
|
@@ -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
|
+
* Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
|
|
41
|
+
* if missing (lifecycle move draft→planned before the promote commit), resolve
|
|
42
|
+
* by stable slug under any lifecycle directory at that ref.
|
|
43
|
+
*/
|
|
44
|
+
export declare function readBlueprintOverviewAtRef(repoRoot: string, ref: string, file: string): Buffer | null;
|
|
39
45
|
export declare function collectV2ReviewProvenance(file: string, purpose: ReviewEvent["purpose"], ref?: string, issues?: string[]): readonly ValidTrackedReview[];
|
|
40
46
|
export declare function auditReviewEventAppendOnly(cwd: string, baseRef: string, targetRef?: string): readonly string[];
|
|
41
47
|
export declare function parseTrackedReviewRecords(markdown: string): TrackedReviewRecord[];
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { execFileSync } from "node:child_process";
|
|
2
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { readReviewAuthorityAtRef } from "#review/authority.js";
|
|
5
|
-
import { parseReviewEventLog } from "#review/events.js";
|
|
6
|
-
import { normalizePlanSubjectContent } from "#review/subject.js";
|
|
4
|
+
import { readReviewAuthorityAtRef, readWorkingReviewEvents } from "#review/authority.js";
|
|
5
|
+
import { evaluateReviewEvents, parseReviewEventLog } from "#review/events.js";
|
|
6
|
+
import { createDeliverySubjectAtRef, createPlanSubjectDigestForContent, 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) {
|
|
@@ -50,8 +50,59 @@ function stableSlugForBlueprintFile(file) {
|
|
|
50
50
|
if (lifecycleIndex >= 0 && segments.at(-1) === "_overview.md") {
|
|
51
51
|
return segments.slice(lifecycleIndex + 1, -1).join("/");
|
|
52
52
|
}
|
|
53
|
+
const basename = segments.at(-1) ?? "";
|
|
54
|
+
if (lifecycleIndex >= 0 && basename.endsWith(".md")) {
|
|
55
|
+
return basename.slice(0, -3);
|
|
56
|
+
}
|
|
53
57
|
return path.basename(path.dirname(file));
|
|
54
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Read `_overview.md` for a blueprint at `ref`. Prefer the working-tree path;
|
|
61
|
+
* if missing (lifecycle move draft→planned before the promote commit), resolve
|
|
62
|
+
* by stable slug under any lifecycle directory at that ref.
|
|
63
|
+
*/
|
|
64
|
+
export function readBlueprintOverviewAtRef(repoRoot, ref, file) {
|
|
65
|
+
const relative = normalizePath(path.relative(repoRoot, file));
|
|
66
|
+
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
67
|
+
return null;
|
|
68
|
+
try {
|
|
69
|
+
return execFileSync("git", ["show", `${ref}:${relative}`], {
|
|
70
|
+
cwd: repoRoot,
|
|
71
|
+
encoding: "buffer",
|
|
72
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
73
|
+
timeout: 5_000,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// Path may have moved across lifecycle dirs (draft→planned) while the
|
|
78
|
+
// promote commit is still uncommitted. Stable slug is the authority key.
|
|
79
|
+
const slug = stableSlugForBlueprintFile(file);
|
|
80
|
+
if (!slug || slug.includes(".."))
|
|
81
|
+
return null;
|
|
82
|
+
try {
|
|
83
|
+
const names = execFileSync("git", ["ls-tree", "-r", "--name-only", ref], {
|
|
84
|
+
cwd: repoRoot,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
87
|
+
timeout: 5_000,
|
|
88
|
+
});
|
|
89
|
+
const escaped = slug.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
90
|
+
const pattern = new RegExp(`(?:^|/)(?:draft|planned|parked|in-progress|completed|archived)/${escaped}(?:/_overview\\.md|\\.md)$`, "u");
|
|
91
|
+
const hits = names.split("\n").filter((name) => pattern.test(normalizePath(name)));
|
|
92
|
+
if (hits.length !== 1)
|
|
93
|
+
return null;
|
|
94
|
+
return execFileSync("git", ["show", `${ref}:${hits[0]}`], {
|
|
95
|
+
cwd: repoRoot,
|
|
96
|
+
encoding: "buffer",
|
|
97
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
98
|
+
timeout: 5_000,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
55
106
|
function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
56
107
|
const relative = path.relative(repoRoot, file);
|
|
57
108
|
if (relative.startsWith("..") || path.isAbsolute(relative))
|
|
@@ -63,12 +114,11 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
63
114
|
// in the working tree before the promotion commit exists, and that
|
|
64
115
|
// window must not strip a legitimately promoted blueprint of its plan
|
|
65
116
|
// authority. Substantive edits still differ after normalization.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
});
|
|
117
|
+
// Also resolve by stable slug when the lifecycle path changed (draft→planned)
|
|
118
|
+
// so pre-commit on a rename does not falsely drop plan authority.
|
|
119
|
+
const atRef = readBlueprintOverviewAtRef(repoRoot, ref, file);
|
|
120
|
+
if (!atRef)
|
|
121
|
+
return false;
|
|
72
122
|
const working = readFileSync(file);
|
|
73
123
|
return normalizePlanSubjectContent(working).equals(normalizePlanSubjectContent(atRef));
|
|
74
124
|
}
|
|
@@ -84,27 +134,83 @@ function workingContentMatchesRef(repoRoot, file, purpose, ref) {
|
|
|
84
134
|
return false;
|
|
85
135
|
}
|
|
86
136
|
}
|
|
137
|
+
// Working-ledger acceptance: pre-commit validates a blueprint whose new review
|
|
138
|
+
// events are staged alongside it and therefore not yet at any git ref. An
|
|
139
|
+
// event still counts only when its subjectDigest matches the digest of the
|
|
140
|
+
// exact working bytes being committed and its artifact hash-verifies on disk —
|
|
141
|
+
// content-addressing keeps this sound before the commit exists.
|
|
142
|
+
function collectWorkingV2ReviewProvenance(file, purpose, issues) {
|
|
143
|
+
try {
|
|
144
|
+
const repoRoot = findGitRoot(file);
|
|
145
|
+
if (!repoRoot)
|
|
146
|
+
return [];
|
|
147
|
+
const slug = stableSlugForBlueprintFile(file);
|
|
148
|
+
const working = readWorkingReviewEvents(file, slug);
|
|
149
|
+
if (working.events.length === 0)
|
|
150
|
+
return [];
|
|
151
|
+
issues?.push(...working.issues);
|
|
152
|
+
const subjects = purpose === "plan"
|
|
153
|
+
? [
|
|
154
|
+
{
|
|
155
|
+
scheme: "wp-blueprint-v1",
|
|
156
|
+
digest: createPlanSubjectDigestForContent(slug, readFileSync(file)),
|
|
157
|
+
},
|
|
158
|
+
]
|
|
159
|
+
: [
|
|
160
|
+
...new Map(working.events
|
|
161
|
+
.filter((event) => event.purpose === "delivery")
|
|
162
|
+
.flatMap((event) => {
|
|
163
|
+
const subject = createDeliverySubjectAtRef(repoRoot, event.reviewedCommit, slug, normalizePath(path.relative(repoRoot, file)));
|
|
164
|
+
if (subject.digest !== event.subjectDigest) {
|
|
165
|
+
issues?.push(`Working delivery review subject mismatch for ${event.id}: expected ${subject.digest}, found ${event.subjectDigest}.`);
|
|
166
|
+
return [];
|
|
167
|
+
}
|
|
168
|
+
return [[subject.digest, subject]];
|
|
169
|
+
})).values(),
|
|
170
|
+
];
|
|
171
|
+
return subjects
|
|
172
|
+
.flatMap((subject) => evaluateReviewEvents(working.events, {
|
|
173
|
+
purpose,
|
|
174
|
+
blueprintSlug: slug,
|
|
175
|
+
subjectScheme: subject.scheme,
|
|
176
|
+
subjectDigest: subject.digest,
|
|
177
|
+
}, working.compromisedIds).approvals)
|
|
178
|
+
.filter((event) => event.artifactPath)
|
|
179
|
+
.map((event) => ({
|
|
180
|
+
reviewer: event.reviewer,
|
|
181
|
+
artifact: event.artifactPath,
|
|
182
|
+
targetHash: event.subjectDigest,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
issues?.push(`Working review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
}
|
|
87
190
|
export function collectV2ReviewProvenance(file, purpose, ref = "HEAD", issues) {
|
|
88
191
|
const repoRoot = findGitRoot(file);
|
|
89
192
|
if (!repoRoot)
|
|
90
193
|
return [];
|
|
194
|
+
const workingApprovals = collectWorkingV2ReviewProvenance(file, purpose, issues);
|
|
91
195
|
if (!workingContentMatchesRef(repoRoot, file, purpose, ref))
|
|
92
|
-
return
|
|
196
|
+
return workingApprovals;
|
|
93
197
|
try {
|
|
94
198
|
const authority = readReviewAuthorityAtRef(repoRoot, ref, stableSlugForBlueprintFile(file), purpose);
|
|
95
199
|
issues?.push(...authority.issues);
|
|
96
|
-
|
|
200
|
+
const refApprovals = authority.approvals.map((event) => ({
|
|
97
201
|
reviewer: event.reviewer,
|
|
98
202
|
artifact: event.artifactPath,
|
|
99
203
|
targetHash: event.subjectDigest,
|
|
100
204
|
}));
|
|
205
|
+
const seen = new Set(refApprovals.map((approval) => approval.reviewer));
|
|
206
|
+
return [...refApprovals, ...workingApprovals.filter((entry) => !seen.has(entry.reviewer))];
|
|
101
207
|
}
|
|
102
208
|
catch (error) {
|
|
103
209
|
// Container-level failure (unparseable ledger, duplicate ids, cross-slug
|
|
104
210
|
// events, unresolved commit): fail closed for the whole file, but surface
|
|
105
211
|
// the reason instead of silently reporting zero authority.
|
|
106
212
|
issues?.push(`Review authority unavailable for ${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
107
|
-
return
|
|
213
|
+
return workingApprovals;
|
|
108
214
|
}
|
|
109
215
|
}
|
|
110
216
|
function reviewEventLogsAtRef(cwd, ref) {
|