@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
|
@@ -67,10 +67,14 @@ export function updateBlockedReason(content, taskId, reason) {
|
|
|
67
67
|
const trimmedReason = reason.trim();
|
|
68
68
|
const blockedPattern = /\n?\*\*Blocked:\*\*\s*.*(?:\n+)?/i;
|
|
69
69
|
if (!trimmedReason) {
|
|
70
|
-
|
|
70
|
+
// Preserve the section's trailing whitespace: trimEnd on the whole
|
|
71
|
+
// section would also delete the blank line separating this task from the
|
|
72
|
+
// next heading, changing bytes the writer does not own.
|
|
73
|
+
const trailingWhitespace = section.slice(section.trimEnd().length);
|
|
74
|
+
return (section
|
|
71
75
|
.replace(blockedPattern, "\n")
|
|
72
76
|
.replace(/\n{3,}/g, "\n\n")
|
|
73
|
-
.trimEnd();
|
|
77
|
+
.trimEnd() + trailingWhitespace);
|
|
74
78
|
}
|
|
75
79
|
const blockedLine = `**Blocked:** ${trimmedReason}`;
|
|
76
80
|
if (/\*\*Blocked:\*\*/i.test(section)) {
|
|
@@ -48,6 +48,7 @@ export declare class BlueprintCreationService {
|
|
|
48
48
|
resolveDraftSlug(input: CreateBlueprintDraftInput): string;
|
|
49
49
|
compileDraft(input: CreateBlueprintDraftInput): Promise<CompiledBlueprintDraft>;
|
|
50
50
|
create(input: CreateBlueprintDraftInput): Promise<CreatedBlueprintDraft>;
|
|
51
|
+
private reserveDraftSlugFolder;
|
|
51
52
|
private resolveCollisionSafeSlug;
|
|
52
53
|
}
|
|
53
54
|
export {};
|
|
@@ -42,12 +42,38 @@ function sentenceCase(value) {
|
|
|
42
42
|
}
|
|
43
43
|
return value.slice(0, 1).toUpperCase() + value.slice(1);
|
|
44
44
|
}
|
|
45
|
+
// A slug becomes a branch name, worktree directory, and repeated CLI argument,
|
|
46
|
+
// so an unbounded goal string must not produce an unbounded slug. Cap it at a
|
|
47
|
+
// word AND a character ceiling (whichever binds first), truncating on a word
|
|
48
|
+
// boundary. The full goal survives as the human-readable title; only the slug
|
|
49
|
+
// is bounded. Collision suffixing runs AFTER this cap, so `-2` is never counted
|
|
50
|
+
// against the ceiling.
|
|
51
|
+
const MAX_SLUG_WORDS = 6;
|
|
52
|
+
const MAX_SLUG_CHARS = 50;
|
|
53
|
+
// Bound on atomic slug-folder reservation retries under concurrent creation.
|
|
54
|
+
const MAX_SLUG_RESERVATION_ATTEMPTS = 50;
|
|
55
|
+
function capSlug(slug) {
|
|
56
|
+
const [firstWord, ...rest] = slug.split("-").filter(Boolean).slice(0, MAX_SLUG_WORDS);
|
|
57
|
+
if (firstWord === undefined)
|
|
58
|
+
return slug;
|
|
59
|
+
let capped = firstWord;
|
|
60
|
+
for (const word of rest) {
|
|
61
|
+
const candidate = `${capped}-${word}`;
|
|
62
|
+
if (candidate.length > MAX_SLUG_CHARS)
|
|
63
|
+
break;
|
|
64
|
+
capped = candidate;
|
|
65
|
+
}
|
|
66
|
+
// A single first word longer than the ceiling is hard-trimmed; drop any
|
|
67
|
+
// trailing hyphen the trim could expose.
|
|
68
|
+
return capped.slice(0, MAX_SLUG_CHARS).replace(/-+$/u, "");
|
|
69
|
+
}
|
|
45
70
|
function deriveSlug(goal) {
|
|
46
|
-
|
|
71
|
+
const slug = goal
|
|
47
72
|
.toLowerCase()
|
|
48
73
|
.replace(/[^a-z0-9]+/g, "-")
|
|
49
74
|
.replace(/^-+|-+$/g, "")
|
|
50
75
|
.replace(/-{2,}/g, "-");
|
|
76
|
+
return capSlug(slug);
|
|
51
77
|
}
|
|
52
78
|
function assertGoalProducesUsableSlug(goal, slug) {
|
|
53
79
|
if (slug.length === 0) {
|
|
@@ -223,11 +249,16 @@ export class BlueprintCreationService {
|
|
|
223
249
|
async create(input) {
|
|
224
250
|
// H-009: refuse primary-like roots so drafts never leak onto main checkouts.
|
|
225
251
|
assertDraftAuthoringAllowed(this.projectRoot);
|
|
226
|
-
const draft = await this.compileDraft(input);
|
|
227
252
|
const draftRoot = path.join(this.blueprintsRoot, "draft");
|
|
228
|
-
const finalPath = draft.outputPath;
|
|
229
253
|
await mkdir(draftRoot, { recursive: true });
|
|
230
|
-
|
|
254
|
+
// Atomically RESERVE the slug folder. Two concurrent creations (CLI *and*
|
|
255
|
+
// MCP) can each resolve the same collision-safe slug before either writes;
|
|
256
|
+
// a plain recursive mkdir + rename would then let the later writer silently
|
|
257
|
+
// overwrite the earlier draft. `mkdir(..., { recursive: false })` is atomic:
|
|
258
|
+
// exactly one racer creates the folder, the loser gets EEXIST and re-resolves
|
|
259
|
+
// to the next `-N` suffix (compileDraft re-scans and sees the reservation).
|
|
260
|
+
const draft = await this.reserveDraftSlugFolder(input);
|
|
261
|
+
const finalPath = draft.outputPath;
|
|
231
262
|
const tempDir = await mkdtemp(path.join(draftRoot, `${draft.slug}.tmp-`));
|
|
232
263
|
const tempPath = path.join(tempDir, `${draft.slug}.md`);
|
|
233
264
|
try {
|
|
@@ -252,6 +283,24 @@ export class BlueprintCreationService {
|
|
|
252
283
|
throw error;
|
|
253
284
|
}
|
|
254
285
|
}
|
|
286
|
+
// Compile a draft and atomically claim its slug folder, retrying with the next
|
|
287
|
+
// collision-safe slug when a concurrent creator wins the folder first.
|
|
288
|
+
async reserveDraftSlugFolder(input) {
|
|
289
|
+
for (let attempt = 0; attempt < MAX_SLUG_RESERVATION_ATTEMPTS; attempt += 1) {
|
|
290
|
+
const candidate = await this.compileDraft(input);
|
|
291
|
+
try {
|
|
292
|
+
await mkdir(path.dirname(candidate.outputPath), { recursive: false });
|
|
293
|
+
return candidate;
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
// Folder already reserved by a concurrent creator — re-resolve the slug.
|
|
297
|
+
if (error instanceof Error && error.code === "EEXIST")
|
|
298
|
+
continue;
|
|
299
|
+
throw error;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
throw new Error(`Could not reserve a unique blueprint slug after ${MAX_SLUG_RESERVATION_ATTEMPTS} attempts.`);
|
|
303
|
+
}
|
|
255
304
|
resolveCollisionSafeSlug(baseSlug) {
|
|
256
305
|
const blueprintsRoot = this.blueprintsRoot;
|
|
257
306
|
const existing = new Set(scanBlueprintDirectory({
|
|
@@ -1,15 +1,29 @@
|
|
|
1
1
|
import matter from "gray-matter";
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
+
import { getBlueprintDocumentCandidates } from "#utils/document-paths.js";
|
|
4
5
|
import { resolveTechDebtRoot } from "#utils/tech-debt-root";
|
|
6
|
+
async function resolveBlueprintDocumentPath(baseDir, bpSlug) {
|
|
7
|
+
const candidates = getBlueprintDocumentCandidates(baseDir, bpSlug);
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
try {
|
|
10
|
+
await fs.access(candidate);
|
|
11
|
+
return candidate;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// Keep trying the other canonical shape.
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return candidates.at(-1) ?? path.join(baseDir, bpSlug, "_overview.md");
|
|
18
|
+
}
|
|
5
19
|
export async function linkBlueprintToTechDebt(baseDir, projectPath, bpSlug, tdSlug) {
|
|
6
|
-
const blueprintPath =
|
|
20
|
+
const blueprintPath = await resolveBlueprintDocumentPath(baseDir, bpSlug);
|
|
7
21
|
await updateLinkedSlugs(blueprintPath, "linked_tech_debt_slugs", (linked) => [...linked, tdSlug]);
|
|
8
22
|
const tdPath = path.join(resolveTechDebtRoot(projectPath), tdSlug, "README.md");
|
|
9
23
|
await updateLinkedSlugs(tdPath, "linked_blueprints", (linked) => [...linked, bpSlug]);
|
|
10
24
|
}
|
|
11
25
|
export async function unlinkBlueprintFromTechDebt(baseDir, projectPath, bpSlug, tdSlug) {
|
|
12
|
-
const blueprintPath =
|
|
26
|
+
const blueprintPath = await resolveBlueprintDocumentPath(baseDir, bpSlug);
|
|
13
27
|
await updateLinkedSlugs(blueprintPath, "linked_tech_debt_slugs", (linked) => linked.filter((slug) => slug !== tdSlug));
|
|
14
28
|
const tdPath = path.join(resolveTechDebtRoot(projectPath), tdSlug, "README.md");
|
|
15
29
|
try {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
export declare const ALLOWED_WP_COMMAND_TIMEOUT_MS = 30000;
|
|
3
|
+
export declare const ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT = 500;
|
|
4
|
+
export type AllowedWpCommandOutcome = "passed" | "failed" | "timed-out";
|
|
5
|
+
export interface AllowedWpCommandResult {
|
|
6
|
+
readonly command: string;
|
|
7
|
+
readonly outcome: AllowedWpCommandOutcome;
|
|
8
|
+
readonly durationMs: number;
|
|
9
|
+
readonly exitCode: number | null;
|
|
10
|
+
readonly signal?: string;
|
|
11
|
+
readonly stdout: string;
|
|
12
|
+
readonly stderr: string;
|
|
13
|
+
readonly logPath?: string;
|
|
14
|
+
readonly failureCode?: string;
|
|
15
|
+
readonly errorMessage?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface ExecuteAllowedWpCommandInput {
|
|
18
|
+
readonly command: string;
|
|
19
|
+
readonly executionRoot: string;
|
|
20
|
+
readonly logRoot: string;
|
|
21
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
22
|
+
readonly spawn?: typeof spawnSync;
|
|
23
|
+
}
|
|
24
|
+
export declare function executeAllowedWpCommand(input: ExecuteAllowedWpCommandInput): AllowedWpCommandResult;
|
|
25
|
+
export declare function tailBounded(text: string): string;
|
|
26
|
+
export declare function relativeCommandLogPath(root: string, logPath: string | undefined): string | undefined;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { parseAllowedWpCommand } from "./gates.js";
|
|
5
|
+
import { resolvePackageAssetPreferred } from "#utils/package-assets.js";
|
|
6
|
+
export const ALLOWED_WP_COMMAND_TIMEOUT_MS = 30_000;
|
|
7
|
+
export const ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT = 500;
|
|
8
|
+
export function executeAllowedWpCommand(input) {
|
|
9
|
+
const started = Date.now();
|
|
10
|
+
let invocation;
|
|
11
|
+
try {
|
|
12
|
+
const argv = parseAllowedWpCommand(input.command);
|
|
13
|
+
const [binary, ...args] = argv;
|
|
14
|
+
if (binary === undefined)
|
|
15
|
+
throw new Error(`Promotion gate command is empty: ${input.command}`);
|
|
16
|
+
invocation = resolveAllowedWpInvocation(input.executionRoot, binary, args, input.env);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
20
|
+
const result = {
|
|
21
|
+
command: input.command,
|
|
22
|
+
outcome: "failed",
|
|
23
|
+
durationMs: Date.now() - started,
|
|
24
|
+
exitCode: null,
|
|
25
|
+
stdout: "",
|
|
26
|
+
stderr: message,
|
|
27
|
+
failureCode: "command-policy-rejected",
|
|
28
|
+
errorMessage: message,
|
|
29
|
+
};
|
|
30
|
+
return {
|
|
31
|
+
...result,
|
|
32
|
+
logPath: writeAllowedWpCommandLog(input.logRoot, input.command, null, result),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const spawn = input.spawn ?? spawnSync;
|
|
36
|
+
const result = spawn(invocation.command, invocation.args, {
|
|
37
|
+
cwd: input.executionRoot,
|
|
38
|
+
encoding: "utf8",
|
|
39
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
40
|
+
timeout: ALLOWED_WP_COMMAND_TIMEOUT_MS,
|
|
41
|
+
env: invocation.env,
|
|
42
|
+
});
|
|
43
|
+
const durationMs = Date.now() - started;
|
|
44
|
+
const stdout = typeof result.stdout === "string" ? result.stdout : "";
|
|
45
|
+
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
46
|
+
const timedOut = result.error ? errorCode(result.error) === "ETIMEDOUT" : false;
|
|
47
|
+
const output = {
|
|
48
|
+
command: input.command,
|
|
49
|
+
outcome: result.status === 0 && !result.error && result.signal === null
|
|
50
|
+
? "passed"
|
|
51
|
+
: timedOut
|
|
52
|
+
? "timed-out"
|
|
53
|
+
: "failed",
|
|
54
|
+
durationMs,
|
|
55
|
+
exitCode: typeof result.status === "number" ? result.status : null,
|
|
56
|
+
...(result.signal ? { signal: result.signal } : {}),
|
|
57
|
+
stdout,
|
|
58
|
+
stderr,
|
|
59
|
+
...(timedOut ? { failureCode: "command-timed-out" } : {}),
|
|
60
|
+
...(result.error ? { errorMessage: result.error.message } : {}),
|
|
61
|
+
};
|
|
62
|
+
if (output.outcome === "passed")
|
|
63
|
+
return output;
|
|
64
|
+
return {
|
|
65
|
+
...output,
|
|
66
|
+
logPath: writeAllowedWpCommandLog(input.logRoot, input.command, invocation, output),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function tailBounded(text) {
|
|
70
|
+
if (text.length <= ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT)
|
|
71
|
+
return text.trim();
|
|
72
|
+
return text.slice(-ALLOWED_WP_COMMAND_STDIO_TAIL_LIMIT).trim();
|
|
73
|
+
}
|
|
74
|
+
export function relativeCommandLogPath(root, logPath) {
|
|
75
|
+
if (!logPath)
|
|
76
|
+
return undefined;
|
|
77
|
+
const rel = relative(root, logPath).replace(/\\/gu, "/");
|
|
78
|
+
return rel === "" || rel.startsWith("../") ? logPath : rel;
|
|
79
|
+
}
|
|
80
|
+
function resolveAllowedWpInvocation(executionRoot, binary, args, extraEnv) {
|
|
81
|
+
const baseEnv = { ...process.env, ...extraEnv };
|
|
82
|
+
if (binary === "./bin/wp" || binary === "wp") {
|
|
83
|
+
const localLauncher = resolveRepoWpLauncher(executionRoot);
|
|
84
|
+
if (localLauncher !== null) {
|
|
85
|
+
return {
|
|
86
|
+
command: localLauncher,
|
|
87
|
+
args: [...args],
|
|
88
|
+
env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
if (binary === "./bin/wp") {
|
|
92
|
+
return {
|
|
93
|
+
command: join(executionRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp"),
|
|
94
|
+
args: [...args],
|
|
95
|
+
env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const packagedLauncher = resolvePackagedWpLauncher();
|
|
99
|
+
return {
|
|
100
|
+
command: process.execPath,
|
|
101
|
+
args: [packagedLauncher, ...args],
|
|
102
|
+
env: baseEnv,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
command: binary,
|
|
107
|
+
args: [...args],
|
|
108
|
+
env: { ...baseEnv, PATH: `${executionRoot}/bin:${baseEnv["PATH"] ?? ""}` },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function resolveRepoWpLauncher(repoRoot) {
|
|
112
|
+
const launcher = join(repoRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp");
|
|
113
|
+
return existsSync(launcher) ? launcher : null;
|
|
114
|
+
}
|
|
115
|
+
function resolvePackagedWpLauncher() {
|
|
116
|
+
const launcher = resolvePackageAssetPreferred([
|
|
117
|
+
process.platform === "win32" ? "bin/wp.cmd" : "bin/wp",
|
|
118
|
+
"bin/wp",
|
|
119
|
+
]);
|
|
120
|
+
if (!existsSync(launcher)) {
|
|
121
|
+
throw new Error(`Promotion gate failed: packaged wp launcher is unavailable at ${launcher}`);
|
|
122
|
+
}
|
|
123
|
+
return launcher;
|
|
124
|
+
}
|
|
125
|
+
function writeAllowedWpCommandLog(logRoot, command, invocation, result) {
|
|
126
|
+
mkdirSync(logRoot, { recursive: true });
|
|
127
|
+
const logPath = join(logRoot, `${Date.now()}-wp-command.log`);
|
|
128
|
+
writeFileSync(logPath, [
|
|
129
|
+
`command: ${command}`,
|
|
130
|
+
`invocation: ${invocation ? `${invocation.command} ${invocation.args.join(" ")}` : "not-run"}`,
|
|
131
|
+
`outcome: ${result.outcome}`,
|
|
132
|
+
`exit: ${result.exitCode ?? "null"}`,
|
|
133
|
+
`signal: ${result.signal ?? "null"}`,
|
|
134
|
+
`timeout_ms: ${ALLOWED_WP_COMMAND_TIMEOUT_MS}`,
|
|
135
|
+
`error: ${result.errorMessage ?? "none"}`,
|
|
136
|
+
"--- stderr ---",
|
|
137
|
+
result.stderr,
|
|
138
|
+
"--- stdout ---",
|
|
139
|
+
result.stdout,
|
|
140
|
+
"",
|
|
141
|
+
].join("\n"), "utf8");
|
|
142
|
+
return logPath;
|
|
143
|
+
}
|
|
144
|
+
function errorCode(error) {
|
|
145
|
+
return "code" in error && typeof error.code === "string"
|
|
146
|
+
? error.code
|
|
147
|
+
: null;
|
|
148
|
+
}
|
|
@@ -1 +1,8 @@
|
|
|
1
1
|
export declare function parseAllowedWpCommand(command: string): string[];
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the `--file` targets from an argv already parsed by
|
|
4
|
+
* `parseAllowedWpCommand`. Returns an empty array for any non-`wp test` argv.
|
|
5
|
+
* `parseAllowedWpCommand` guarantees `wp test` args are only `--file <target>`
|
|
6
|
+
* pairs, so no shell/flag validation is needed here.
|
|
7
|
+
*/
|
|
8
|
+
export declare function wpTestFileTargets(argv: readonly string[]): string[];
|
|
@@ -54,3 +54,23 @@ export function parseAllowedWpCommand(command) {
|
|
|
54
54
|
}
|
|
55
55
|
throw new Error(`Unsupported promotion gate wp subcommand: ${sub ?? ""}`);
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Extracts the `--file` targets from an argv already parsed by
|
|
59
|
+
* `parseAllowedWpCommand`. Returns an empty array for any non-`wp test` argv.
|
|
60
|
+
* `parseAllowedWpCommand` guarantees `wp test` args are only `--file <target>`
|
|
61
|
+
* pairs, so no shell/flag validation is needed here.
|
|
62
|
+
*/
|
|
63
|
+
export function wpTestFileTargets(argv) {
|
|
64
|
+
if (argv[1] !== "test")
|
|
65
|
+
return [];
|
|
66
|
+
const targets = [];
|
|
67
|
+
const rest = argv.slice(2);
|
|
68
|
+
for (let i = 0; i < rest.length; i++) {
|
|
69
|
+
const target = rest[i + 1];
|
|
70
|
+
if (rest[i] === "--file" && target !== undefined) {
|
|
71
|
+
targets.push(target);
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return targets;
|
|
76
|
+
}
|
|
@@ -7,7 +7,15 @@ export type PromotionTrustInput = {
|
|
|
7
7
|
markdown: string;
|
|
8
8
|
now?: Date;
|
|
9
9
|
};
|
|
10
|
+
export type PromotionTrustProof = {
|
|
11
|
+
readonly file: string;
|
|
12
|
+
readonly sourceMarkdown: string;
|
|
13
|
+
readonly verifiedHead: string;
|
|
14
|
+
readonly trustedMarkdown: string;
|
|
15
|
+
};
|
|
16
|
+
export declare function preparePromotionTrustGate(input: PromotionTrustInput): PromotionTrustProof;
|
|
10
17
|
export declare function applyPromotionTrustGate(input: PromotionTrustInput): string;
|
|
18
|
+
export declare function assertPromotionTrustProofCurrent(input: Pick<PromotionTrustInput, "repoRoot" | "file" | "markdown">, proof: PromotionTrustProof): void;
|
|
11
19
|
export declare function runPromotionCommand(repoRoot: string, command: string, deps?: {
|
|
12
20
|
spawn?: typeof spawnSync;
|
|
13
21
|
}): void;
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { validateBlueprintTrust } from "./validator.js";
|
|
5
5
|
import { parseTrustDossier } from "./dossier.js";
|
|
6
|
-
import { parseAllowedWpCommand } from "./gates.js";
|
|
7
|
-
import {
|
|
6
|
+
import { parseAllowedWpCommand, wpTestFileTargets } from "./gates.js";
|
|
7
|
+
import { ALLOWED_WP_COMMAND_TIMEOUT_MS, executeAllowedWpCommand, tailBounded, } from "./command-runner.js";
|
|
8
8
|
export { parseAllowedWpCommand };
|
|
9
|
-
|
|
10
|
-
const PROMOTION_GATE_STDIO_TAIL_LIMIT = 500;
|
|
11
|
-
export function applyPromotionTrustGate(input) {
|
|
9
|
+
export function preparePromotionTrustGate(input) {
|
|
12
10
|
const now = (input.now ?? new Date()).toISOString();
|
|
13
11
|
const syntacticMarkdown = upsertReadinessValue(upsertReadinessValue(input.markdown, "verified-at", now), "verified-head", "0123456789abcdef0123456789abcdef01234567");
|
|
14
12
|
const parsedBeforeHead = parseTrustDossier(syntacticMarkdown);
|
|
@@ -30,6 +28,16 @@ export function applyPromotionTrustGate(input) {
|
|
|
30
28
|
throw new Error(`Blueprint trust gate failed: ${preflight.violations.map((v) => `${v.section}: ${v.message}`).join("; ")}`);
|
|
31
29
|
const parsed = parseTrustDossier(markdown);
|
|
32
30
|
for (const gate of parsed.dossier?.gates ?? []) {
|
|
31
|
+
const deferredTarget = firstMissingTestTarget(input.repoRoot, gate.command);
|
|
32
|
+
if (deferredTarget !== undefined) {
|
|
33
|
+
// A `wp test --file <path>` gate whose target file does not exist yet is
|
|
34
|
+
// recorded as an honest non-`pass` result instead of aborting the whole
|
|
35
|
+
// promotion. `deferred` is a distinct failing state: it is only tolerated
|
|
36
|
+
// by the post-run validation below (allowDeferredGates), and the strict
|
|
37
|
+
// whole-repo completion audit still rejects it, so it can never merge.
|
|
38
|
+
markdown = updateGateLastResult(markdown, gate.gate, `deferred: test file not yet created (${deferredTarget})`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
33
41
|
runPromotionCommand(input.repoRoot, gate.command);
|
|
34
42
|
markdown = updateGateLastResult(markdown, gate.gate, `pass at ${now}`);
|
|
35
43
|
}
|
|
@@ -40,10 +48,27 @@ export function applyPromotionTrustGate(input) {
|
|
|
40
48
|
markdown,
|
|
41
49
|
promotionCandidate: true,
|
|
42
50
|
scanTaskAmbiguity: true,
|
|
51
|
+
allowDeferredGates: true,
|
|
43
52
|
});
|
|
44
53
|
if (!validated.ok)
|
|
45
54
|
throw new Error(`Blueprint trust gate failed: ${validated.violations.map((v) => `${v.section}: ${v.message}`).join("; ")}`);
|
|
46
|
-
return
|
|
55
|
+
return {
|
|
56
|
+
file: input.file,
|
|
57
|
+
sourceMarkdown: input.markdown,
|
|
58
|
+
verifiedHead: head,
|
|
59
|
+
trustedMarkdown: markdown,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export function applyPromotionTrustGate(input) {
|
|
63
|
+
return preparePromotionTrustGate(input).trustedMarkdown;
|
|
64
|
+
}
|
|
65
|
+
export function assertPromotionTrustProofCurrent(input, proof) {
|
|
66
|
+
if (input.file !== proof.file || input.markdown !== proof.sourceMarkdown) {
|
|
67
|
+
throw new Error("Blueprint trust gate invalidated: the blueprint changed after external gates ran; retry promotion");
|
|
68
|
+
}
|
|
69
|
+
if (readHead(input.repoRoot) !== proof.verifiedHead) {
|
|
70
|
+
throw new Error("Blueprint trust gate invalidated: git HEAD changed after external gates ran; retry promotion");
|
|
71
|
+
}
|
|
47
72
|
}
|
|
48
73
|
function readHead(repoRoot) {
|
|
49
74
|
const testHead = process.env["WP_BLUEPRINT_TRUST_GATE_TEST_HEAD"];
|
|
@@ -61,123 +86,48 @@ function readHead(repoRoot) {
|
|
|
61
86
|
throw new Error("Blueprint trust gate failed: git HEAD is unavailable");
|
|
62
87
|
}
|
|
63
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Returns the first `wp test --file` target of `command` that does not exist on
|
|
91
|
+
* disk (resolved against `repoRoot`), or undefined when the gate is not a
|
|
92
|
+
* `wp test --file` gate or all its targets exist. A missing target is
|
|
93
|
+
* indistinguishable at `existsSync` time from "renamed / wrong path / deleted",
|
|
94
|
+
* so callers must treat it as a non-`pass` deferral, never a success.
|
|
95
|
+
*/
|
|
96
|
+
function firstMissingTestTarget(repoRoot, command) {
|
|
97
|
+
const targets = wpTestFileTargets(parseAllowedWpCommand(command));
|
|
98
|
+
return targets.find((target) => !existsSync(join(repoRoot, target)));
|
|
99
|
+
}
|
|
64
100
|
export function runPromotionCommand(repoRoot, command, deps = {}) {
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const spawn = deps.spawn ?? spawnSync;
|
|
71
|
-
const result = spawn(invocation.command, invocation.args, {
|
|
72
|
-
cwd: repoRoot,
|
|
73
|
-
encoding: "utf8",
|
|
74
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
75
|
-
timeout: PROMOTION_GATE_TIMEOUT_MS,
|
|
76
|
-
env: invocation.env,
|
|
101
|
+
const result = executeAllowedWpCommand({
|
|
102
|
+
command,
|
|
103
|
+
executionRoot: repoRoot,
|
|
104
|
+
logRoot: join(repoRoot, ".webpresso", "logs", "promotion-gates"),
|
|
105
|
+
...(deps.spawn ? { spawn: deps.spawn } : {}),
|
|
77
106
|
});
|
|
78
|
-
if (result.
|
|
107
|
+
if (result.outcome === "passed")
|
|
79
108
|
return;
|
|
80
|
-
|
|
81
|
-
const stderr = typeof result.stderr === "string" ? result.stderr : "";
|
|
82
|
-
const logPath = writePromotionGateLog(repoRoot, command, invocation, stdout, stderr, result);
|
|
83
|
-
throw new Error(formatPromotionGateFailure(command, stdout, stderr, result, logPath));
|
|
109
|
+
throw new Error(formatPromotionGateFailure(result));
|
|
84
110
|
}
|
|
85
|
-
function
|
|
86
|
-
if (binary === "./bin/wp" || binary === "wp") {
|
|
87
|
-
const localLauncher = resolveRepoWpLauncher(repoRoot);
|
|
88
|
-
if (localLauncher !== null) {
|
|
89
|
-
return {
|
|
90
|
-
command: localLauncher,
|
|
91
|
-
args: [...args],
|
|
92
|
-
env: { ...process.env, PATH: `${repoRoot}/bin:${process.env["PATH"] ?? ""}` },
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
if (binary === "./bin/wp") {
|
|
96
|
-
return {
|
|
97
|
-
command: join(repoRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp"),
|
|
98
|
-
args: [...args],
|
|
99
|
-
env: { ...process.env, PATH: `${repoRoot}/bin:${process.env["PATH"] ?? ""}` },
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
const packagedLauncher = resolvePackagedWpLauncher();
|
|
103
|
-
return {
|
|
104
|
-
command: process.execPath,
|
|
105
|
-
args: [packagedLauncher, ...args],
|
|
106
|
-
env: { ...process.env },
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
return {
|
|
110
|
-
command: binary,
|
|
111
|
-
args: [...args],
|
|
112
|
-
env: { ...process.env, PATH: `${repoRoot}/bin:${process.env["PATH"] ?? ""}` },
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
function resolveRepoWpLauncher(repoRoot) {
|
|
116
|
-
const launcher = join(repoRoot, "bin", process.platform === "win32" ? "wp.cmd" : "wp");
|
|
117
|
-
return existsSync(launcher) ? launcher : null;
|
|
118
|
-
}
|
|
119
|
-
function resolvePackagedWpLauncher() {
|
|
120
|
-
const launcher = resolvePackageAssetPreferred([
|
|
121
|
-
process.platform === "win32" ? "bin/wp.cmd" : "bin/wp",
|
|
122
|
-
"bin/wp",
|
|
123
|
-
]);
|
|
124
|
-
if (!existsSync(launcher)) {
|
|
125
|
-
throw new Error(`Promotion gate failed: packaged wp launcher is unavailable at ${launcher}`);
|
|
126
|
-
}
|
|
127
|
-
return launcher;
|
|
128
|
-
}
|
|
129
|
-
function formatPromotionGateFailure(command, stdout, stderr, result, logPath) {
|
|
111
|
+
function formatPromotionGateFailure(result) {
|
|
130
112
|
const details = [];
|
|
131
|
-
if (typeof result.
|
|
132
|
-
details.push(`exit=${result.
|
|
113
|
+
if (typeof result.exitCode === "number")
|
|
114
|
+
details.push(`exit=${result.exitCode}`);
|
|
133
115
|
if (result.signal)
|
|
134
116
|
details.push(`signal=${result.signal}`);
|
|
135
|
-
if (result.
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
typeof result.error.code === "string"
|
|
140
|
-
? result.error.code
|
|
141
|
-
: null;
|
|
142
|
-
details.push(code ? `spawn_error=${code}` : `spawn_error=${result.error.message}`);
|
|
143
|
-
if (code === "ETIMEDOUT")
|
|
144
|
-
details.push(`timeout=${PROMOTION_GATE_TIMEOUT_MS}ms`);
|
|
117
|
+
if (result.errorMessage) {
|
|
118
|
+
details.push(`spawn_error=${result.failureCode ?? result.errorMessage}`);
|
|
119
|
+
if (result.outcome === "timed-out")
|
|
120
|
+
details.push(`timeout=${ALLOWED_WP_COMMAND_TIMEOUT_MS}ms`);
|
|
145
121
|
}
|
|
146
|
-
const stderrTail = tailBounded(stderr);
|
|
147
|
-
const stdoutTail = tailBounded(stdout);
|
|
122
|
+
const stderrTail = tailBounded(result.stderr);
|
|
123
|
+
const stdoutTail = tailBounded(result.stdout);
|
|
148
124
|
if (stderrTail.length > 0)
|
|
149
125
|
details.push(`stderr_tail=${JSON.stringify(stderrTail)}`);
|
|
150
126
|
if (stdoutTail.length > 0)
|
|
151
127
|
details.push(`stdout_tail=${JSON.stringify(stdoutTail)}`);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
}
|
|
155
|
-
function tailBounded(text) {
|
|
156
|
-
if (text.length <= PROMOTION_GATE_STDIO_TAIL_LIMIT)
|
|
157
|
-
return text.trim();
|
|
158
|
-
return text.slice(-PROMOTION_GATE_STDIO_TAIL_LIMIT).trim();
|
|
159
|
-
}
|
|
160
|
-
function writePromotionGateLog(repoRoot, command, invocation, stdout, stderr, result) {
|
|
161
|
-
const logsDir = join(repoRoot, ".webpresso", "logs", "promotion-gates");
|
|
162
|
-
mkdirSync(logsDir, { recursive: true });
|
|
163
|
-
const logPath = join(logsDir, `${Date.now()}-promotion-gate.log`);
|
|
164
|
-
const errorBlock = result.error
|
|
165
|
-
? `error: ${result.error.name}: ${result.error.message}\n`
|
|
166
|
-
: "error: none\n";
|
|
167
|
-
writeFileSync(logPath, [
|
|
168
|
-
`command: ${command}`,
|
|
169
|
-
`invocation: ${invocation.command} ${invocation.args.join(" ")}`,
|
|
170
|
-
`exit: ${result.status ?? "null"}`,
|
|
171
|
-
`signal: ${result.signal ?? "null"}`,
|
|
172
|
-
`timeout_ms: ${PROMOTION_GATE_TIMEOUT_MS}`,
|
|
173
|
-
errorBlock.trimEnd(),
|
|
174
|
-
"--- stderr ---",
|
|
175
|
-
stderr,
|
|
176
|
-
"--- stdout ---",
|
|
177
|
-
stdout,
|
|
178
|
-
"",
|
|
179
|
-
].join("\n"), "utf8");
|
|
180
|
-
return logPath;
|
|
128
|
+
if (result.logPath)
|
|
129
|
+
details.push(`log=${result.logPath}`);
|
|
130
|
+
return `Promotion gate failed (${result.command}): ${details.join("; ")}`;
|
|
181
131
|
}
|
|
182
132
|
function upsertReadinessValue(markdown, key, value) {
|
|
183
133
|
const re = new RegExp(`^- ${key}: .*$`, "mu");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Trust Dossier authoring scaffold.
|
|
3
|
+
*
|
|
4
|
+
* `dossier.ts` / `evidence.ts` / `gates.ts` / `ambiguity.ts` define strict
|
|
5
|
+
* validation for a blueprint's `## Trust Dossier` section, but nothing
|
|
6
|
+
* generates a starting template. `TRUST_DOSSIER_SCAFFOLD` is a complete,
|
|
7
|
+
* already-valid worked example — proven by `scaffold.test.ts` to pass
|
|
8
|
+
* `validateBlueprintTrust` under both the strict configuration
|
|
9
|
+
* `wp audit blueprint-trust` uses and the draft-preflight configuration
|
|
10
|
+
* `wp_blueprint_put` / `preparePromotionTrustGate` use — so an author can
|
|
11
|
+
* paste it in and edit the example values instead of hand-deriving the
|
|
12
|
+
* contract from source.
|
|
13
|
+
*
|
|
14
|
+
* `verified-at` / `verified-head` stay as inert, obviously-fake placeholders:
|
|
15
|
+
* `preparePromotionTrustGate` (./promotion.ts) overwrites both with the real
|
|
16
|
+
* timestamp/HEAD on every promotion attempt, so there is no need for this
|
|
17
|
+
* pure, dependency-free module to shell out to git.
|
|
18
|
+
*/
|
|
19
|
+
export declare const TRUST_DOSSIER_SCAFFOLD: string;
|