@webpresso/agent-kit 3.1.30 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -15
- package/bin/_run.js +183 -20
- package/catalog/AGENTS.md.tpl +3 -3
- package/catalog/agent/agents/implementer.md +45 -0
- package/catalog/agent/rules/parallel-orchestration-quota.md +100 -0
- package/catalog/agent/rules/pre-implementation.md +28 -1
- package/catalog/agent/rules/supported-agent-clis.md +9 -0
- package/catalog/agent/rules/typescript-version.md +50 -0
- package/catalog/agent/skills/codex/SKILL.md +25 -1
- package/catalog/agent/skills/verify/SKILL.md +4 -2
- package/catalog/docs/templates/blueprint.md +1 -1
- package/dist/esm/audit/agents.js +1 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +12 -0
- package/dist/esm/audit/blueprint-lifecycle-sql.js +23 -2
- package/dist/esm/audit/blueprint-pr-coverage.js +4 -0
- package/dist/esm/audit/blueprint-trust.js +14 -10
- package/dist/esm/audit/changeset-evidence.d.ts +23 -0
- package/dist/esm/audit/changeset-evidence.js +30 -0
- package/dist/esm/audit/changeset-required.d.ts +10 -0
- package/dist/esm/audit/changeset-required.js +73 -0
- package/dist/esm/audit/ci-guardrails-detection.d.ts +27 -0
- package/dist/esm/audit/ci-guardrails-detection.js +68 -0
- package/dist/esm/audit/ci-guardrails-wiring.d.ts +22 -0
- package/dist/esm/audit/ci-guardrails-wiring.js +80 -0
- package/dist/esm/audit/ci-test-perf.js +30 -0
- package/dist/esm/audit/command-surface.js +4 -2
- package/dist/esm/audit/host-substitution-risk.d.ts +2 -0
- package/dist/esm/audit/host-substitution-risk.js +256 -0
- package/dist/esm/audit/registry.d.ts +4 -4
- package/dist/esm/audit/registry.js +13 -1
- package/dist/esm/audit/toolchain-isolation.js +1 -2
- package/dist/esm/audit/typescript-version.d.ts +2 -0
- package/dist/esm/audit/typescript-version.js +123 -0
- package/dist/esm/blueprint/core/parser.js +101 -2
- package/dist/esm/blueprint/core/schema.d.ts +1 -0
- package/dist/esm/blueprint/core/schema.js +26 -0
- package/dist/esm/blueprint/core/validation/criteria.d.ts +14 -0
- package/dist/esm/blueprint/core/validation/criteria.js +41 -2
- package/dist/esm/blueprint/core/validation/state.js +25 -14
- package/dist/esm/blueprint/db/migrations/run.js +4 -2
- package/dist/esm/blueprint/lifecycle/audit.d.ts +20 -0
- package/dist/esm/blueprint/lifecycle/audit.js +87 -20
- package/dist/esm/blueprint/lifecycle/canonicalize.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/canonicalize.js +135 -0
- package/dist/esm/blueprint/lifecycle/local.d.ts +2 -1
- package/dist/esm/blueprint/lifecycle/local.js +19 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.d.ts +6 -0
- package/dist/esm/blueprint/lifecycle/review-provenance.js +68 -12
- package/dist/esm/blueprint/lifecycle/rollout-anchor.d.ts +97 -0
- package/dist/esm/blueprint/lifecycle/rollout-anchor.js +161 -0
- package/dist/esm/blueprint/service/BlueprintCreationService.d.ts +1 -0
- package/dist/esm/blueprint/service/BlueprintCreationService.js +53 -4
- package/dist/esm/blueprint/service/blueprint-tech-debt-links.js +16 -2
- package/dist/esm/blueprint/sync/client.d.ts +2 -1
- package/dist/esm/blueprint/sync/client.js +1 -1
- package/dist/esm/blueprint/trust/command-runner.d.ts +26 -0
- package/dist/esm/blueprint/trust/command-runner.js +148 -0
- package/dist/esm/blueprint/trust/dossier.d.ts +1 -0
- package/dist/esm/blueprint/trust/dossier.js +40 -6
- package/dist/esm/blueprint/trust/gates.d.ts +7 -0
- package/dist/esm/blueprint/trust/gates.js +20 -0
- package/dist/esm/blueprint/trust/promotion.d.ts +8 -0
- package/dist/esm/blueprint/trust/promotion.js +74 -112
- package/dist/esm/blueprint/trust/scaffold.d.ts +19 -0
- package/dist/esm/blueprint/trust/scaffold.js +64 -0
- package/dist/esm/blueprint/trust/validator.d.ts +1 -0
- package/dist/esm/blueprint/trust/validator.js +7 -1
- package/dist/esm/blueprint/utils/archive.js +27 -5
- package/dist/esm/blueprint/utils/document-paths.d.ts +18 -0
- package/dist/esm/blueprint/utils/document-paths.js +45 -12
- package/dist/esm/build/cli-mcp-parity.js +15 -0
- package/dist/esm/cli/bundle/commands/blueprint.d.ts +1 -0
- package/dist/esm/cli/bundle/commands/blueprint.js +1 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +43 -0
- package/dist/esm/cli/commands/agent-launch.d.ts +8 -1
- package/dist/esm/cli/commands/agent-launch.js +93 -4
- package/dist/esm/cli/commands/audit-core.d.ts +1 -1
- package/dist/esm/cli/commands/audit-core.js +8 -1
- package/dist/esm/cli/commands/audit.js +21 -5
- package/dist/esm/cli/commands/blueprint/execution.js +18 -6
- package/dist/esm/cli/commands/blueprint/mutations.js +59 -40
- package/dist/esm/cli/commands/blueprint/router-dispatch.d.ts +2 -1
- package/dist/esm/cli/commands/blueprint/router-dispatch.js +20 -1
- package/dist/esm/cli/commands/blueprint/router-output.d.ts +21 -0
- package/dist/esm/cli/commands/blueprint/router-output.js +48 -17
- package/dist/esm/cli/commands/blueprint/router.d.ts +7 -0
- package/dist/esm/cli/commands/blueprint/router.js +13 -1
- package/dist/esm/cli/commands/ci-preflight.d.ts +13 -2
- package/dist/esm/cli/commands/ci-preflight.js +30 -6
- package/dist/esm/cli/commands/compile.js +5 -31
- package/dist/esm/cli/commands/dash/delivery-status.d.ts +1 -0
- package/dist/esm/cli/commands/dash/delivery-status.js +11 -4
- package/dist/esm/cli/commands/doctor.js +5 -0
- package/dist/esm/cli/commands/hook.d.ts +5 -0
- package/dist/esm/cli/commands/hook.js +15 -9
- package/dist/esm/cli/commands/init/config.js +2 -2
- package/dist/esm/cli/commands/init/convergence-apply.d.ts +88 -0
- package/dist/esm/cli/commands/init/convergence-apply.js +325 -0
- package/dist/esm/cli/commands/init/convergence-state.d.ts +13 -0
- package/dist/esm/cli/commands/init/convergence-state.js +54 -0
- package/dist/esm/cli/commands/init/convergence.d.ts +23 -0
- package/dist/esm/cli/commands/init/convergence.js +57 -0
- package/dist/esm/cli/commands/init/git-convergence.d.ts +19 -0
- package/dist/esm/cli/commands/init/git-convergence.js +168 -0
- package/dist/esm/cli/commands/init/gitignore-patcher.js +2 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.d.ts +52 -0
- package/dist/esm/cli/commands/init/grok-privacy-smoke.js +91 -0
- package/dist/esm/cli/commands/init/host-native-proof.d.ts +18 -0
- package/dist/esm/cli/commands/init/host-native-proof.js +88 -0
- package/dist/esm/cli/commands/init/host-visibility.d.ts +2 -2
- package/dist/esm/cli/commands/init/host-visibility.js +94 -5
- package/dist/esm/cli/commands/init/index.d.ts +3 -0
- package/dist/esm/cli/commands/init/index.js +326 -173
- package/dist/esm/cli/commands/init/mcp-spec.d.ts +57 -0
- package/dist/esm/cli/commands/init/mcp-spec.js +273 -0
- package/dist/esm/cli/commands/init/merge.d.ts +1 -0
- package/dist/esm/cli/commands/init/merge.js +30 -1
- package/dist/esm/cli/commands/init/package-root.d.ts +7 -0
- package/dist/esm/cli/commands/init/package-root.js +10 -2
- package/dist/esm/cli/commands/init/plugin-cache-prune.d.ts +0 -3
- package/dist/esm/cli/commands/init/plugin-cache-prune.js +0 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.d.ts +1 -1
- package/dist/esm/cli/commands/init/scaffold-agents-md.js +9 -3
- package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +20 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +61 -5
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.d.ts +14 -8
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/emitters/grok.js +41 -24
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +2 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +54 -24
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.d.ts +12 -31
- package/dist/esm/cli/commands/init/scaffolders/codex-mcp/index.js +288 -202
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.d.ts +22 -0
- package/dist/esm/cli/commands/init/scaffolders/file-transaction.js +230 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.d.ts +38 -0
- package/dist/esm/cli/commands/init/scaffolders/grok-mcp/index.js +73 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.d.ts +21 -0
- package/dist/esm/cli/commands/init/scaffolders/opencode-plugin/index.js +559 -63
- package/dist/esm/cli/commands/init/untracked-collision.d.ts +14 -0
- package/dist/esm/cli/commands/init/untracked-collision.js +42 -0
- package/dist/esm/cli/commands/opencode-probe.d.ts +107 -0
- package/dist/esm/cli/commands/opencode-probe.js +323 -0
- package/dist/esm/cli/commands/opencode-rotate.d.ts +72 -0
- package/dist/esm/cli/commands/opencode-rotate.js +147 -0
- package/dist/esm/cli/commands/public-readiness.d.ts +11 -0
- package/dist/esm/cli/commands/public-readiness.js +42 -0
- package/dist/esm/cli/commands/quality-runner.js +3 -3
- package/dist/esm/cli/commands/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +751 -209
- package/dist/esm/cli/commands/run-wait.d.ts +2 -0
- package/dist/esm/cli/commands/run-wait.js +122 -0
- package/dist/esm/cli/commands/rust-check.d.ts +24 -0
- package/dist/esm/cli/commands/rust-check.js +110 -0
- package/dist/esm/cli/commands/setup-help.js +1 -1
- package/dist/esm/cli/commands/setup-repair-help.js +1 -1
- package/dist/esm/cli/commands/typecheck.d.ts +1 -0
- package/dist/esm/cli/commands/typecheck.js +30 -1
- package/dist/esm/cli/commands/worktree/router-dispatch.d.ts +6 -0
- package/dist/esm/cli/commands/worktree/router-dispatch.js +126 -9
- package/dist/esm/cli/commands/worktree/router.js +10 -0
- package/dist/esm/cli/direct-provider-launch.js +9 -5
- package/dist/esm/cli/optional-tool-freshness.d.ts +27 -0
- package/dist/esm/cli/optional-tool-freshness.js +219 -9
- package/dist/esm/daemon/domains/git-actions.d.ts +29 -0
- package/dist/esm/daemon/domains/git-actions.js +136 -0
- package/dist/esm/daemon/domains/run-registry-interop.d.ts +16 -0
- package/dist/esm/daemon/domains/run-registry-interop.js +45 -0
- package/dist/esm/daemon/domains/sessions-search.d.ts +11 -0
- package/dist/esm/daemon/domains/sessions-search.js +29 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.d.ts +5 -0
- package/dist/esm/daemon/handlers/blueprints-handlers.js +36 -0
- package/dist/esm/daemon/handlers/deploy-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/deploy-handlers.js +5 -0
- package/dist/esm/daemon/handlers/fleet-handlers.d.ts +15 -0
- package/dist/esm/daemon/handlers/fleet-handlers.js +91 -0
- package/dist/esm/daemon/handlers/git-handlers.d.ts +6 -0
- package/dist/esm/daemon/handlers/git-handlers.js +27 -0
- package/dist/esm/daemon/handlers/product.d.ts +7 -0
- package/dist/esm/daemon/handlers/product.js +10 -119
- package/dist/esm/daemon/handlers/sessions-handlers.d.ts +12 -0
- package/dist/esm/daemon/handlers/sessions-handlers.js +31 -0
- package/dist/esm/daemon/handlers/shared.d.ts +5 -0
- package/dist/esm/daemon/handlers/shared.js +16 -0
- package/dist/esm/daemon/methods.d.ts +8 -1
- package/dist/esm/daemon/methods.js +7 -0
- package/dist/esm/daemon/protocol/contract.d.ts +43 -0
- package/dist/esm/daemon/protocol/contract.js +26 -0
- package/dist/esm/daemon/protocol/index.d.ts +1 -1
- package/dist/esm/daemon/protocol/index.js +1 -1
- package/dist/esm/hooks/__conformance__/matrix.d.ts +6 -0
- package/dist/esm/hooks/__conformance__/matrix.js +137 -0
- package/dist/esm/hooks/conformance/batch.js +5 -0
- package/dist/esm/hooks/doctor.d.ts +5 -3
- package/dist/esm/hooks/doctor.js +67 -16
- package/dist/esm/hooks/permission-request/index.js +9 -3
- package/dist/esm/hooks/permission-request/policy.d.ts +9 -0
- package/dist/esm/hooks/permission-request/policy.js +13 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.d.ts +9 -0
- package/dist/esm/hooks/pretool-guard/dev-routing.js +41 -5
- package/dist/esm/hooks/pretool-guard/logger.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/logger.js +16 -0
- package/dist/esm/hooks/pretool-guard/runner.d.ts +6 -0
- package/dist/esm/hooks/pretool-guard/runner.js +68 -16
- package/dist/esm/hooks/pretool-guard/validators/blueprint.js +30 -10
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +54 -0
- package/dist/esm/hooks/pretool-guard/validators/index.d.ts +7 -0
- package/dist/esm/hooks/pretool-guard/validators/index.js +21 -0
- package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +19 -0
- package/dist/esm/hooks/pretool-guard/validators/path-contract.js +25 -8
- package/dist/esm/hooks/pretool-guard/validators/worktree-discipline.js +6 -1
- package/dist/esm/hooks/shared/guard-outcome.d.ts +149 -0
- package/dist/esm/hooks/shared/guard-outcome.js +215 -0
- package/dist/esm/hooks/shared/hook-bootstrap.d.ts +26 -0
- package/dist/esm/hooks/shared/hook-bootstrap.js +64 -1
- package/dist/esm/hooks/shared/types.d.ts +8 -5
- package/dist/esm/hooks/shared/types.js +50 -9
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +11 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +92 -0
- package/dist/esm/mcp/blueprint/_shared/lifecycle.js +14 -1
- package/dist/esm/mcp/blueprint/handlers/document-mutations.d.ts +5 -0
- package/dist/esm/mcp/blueprint/handlers/document-mutations.js +109 -11
- package/dist/esm/mcp/blueprint/handlers/review-log.js +8 -1
- package/dist/esm/mcp/blueprint/handlers/task-advance.js +113 -54
- package/dist/esm/mcp/blueprint/handlers/task-verify.js +39 -19
- package/dist/esm/mcp/blueprint/registration.js +3 -2
- package/dist/esm/mcp/server.d.ts +1 -0
- package/dist/esm/mcp/server.js +27 -0
- package/dist/esm/mcp/tools/_names.d.ts +1 -1
- package/dist/esm/mcp/tools/_names.js +5 -0
- package/dist/esm/mcp/tools/_registry.js +10 -0
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/_shared/result.js +44 -1
- package/dist/esm/mcp/tools/audit.d.ts +5 -1
- package/dist/esm/mcp/tools/audit.js +42 -0
- package/dist/esm/mcp/tools/audits.d.ts +4 -0
- package/dist/esm/mcp/tools/ci-preflight.d.ts +50 -0
- package/dist/esm/mcp/tools/ci-preflight.js +183 -0
- package/dist/esm/mcp/tools/pr-upsert.js +39 -1
- package/dist/esm/mcp/tools/review-run.d.ts +28 -0
- package/dist/esm/mcp/tools/review-run.js +216 -0
- package/dist/esm/mcp/tools/run-wait.d.ts +86 -0
- package/dist/esm/mcp/tools/run-wait.js +593 -0
- package/dist/esm/mcp/tools/session-fetch-and-index.d.ts +1 -1
- package/dist/esm/mcp/tools/session-id.d.ts +3 -0
- package/dist/esm/mcp/tools/session-id.js +45 -0
- package/dist/esm/mcp/tools/session-info.d.ts +3 -0
- package/dist/esm/mcp/tools/session-info.js +49 -0
- package/dist/esm/output-transforms/audit.d.ts +23 -0
- package/dist/esm/output-transforms/audit.js +47 -0
- package/dist/esm/output-transforms/index.js +2 -0
- package/dist/esm/package.json +2 -0
- package/dist/esm/platform/client.d.ts +7 -1
- package/dist/esm/platform/github-contract-source.d.ts +2 -1
- package/dist/esm/pr-description/contract.js +24 -4
- package/dist/esm/review/authority.js +27 -14
- package/dist/esm/review/availability.d.ts +33 -0
- package/dist/esm/review/availability.js +122 -20
- package/dist/esm/review/delivery-verifier.d.ts +27 -0
- package/dist/esm/review/delivery-verifier.js +115 -0
- package/dist/esm/review/events.d.ts +3 -2
- package/dist/esm/review/events.js +23 -3
- package/dist/esm/review/execution/adapters.d.ts +11 -1
- package/dist/esm/review/execution/adapters.js +36 -10
- package/dist/esm/review/execution/artifacts.d.ts +1 -1
- package/dist/esm/review/execution/output-policy.d.ts +2 -0
- package/dist/esm/review/execution/output-policy.js +12 -0
- package/dist/esm/review/execution/review-checkout.js +121 -21
- package/dist/esm/review/execution/supervisor.js +36 -4
- package/dist/esm/review/execution/types.d.ts +9 -0
- package/dist/esm/review/opencode-account.d.ts +24 -0
- package/dist/esm/review/opencode-account.js +68 -0
- package/dist/esm/review/opencode-auth.d.ts +33 -0
- package/dist/esm/review/opencode-auth.js +79 -0
- package/dist/esm/review/subject.d.ts +25 -0
- package/dist/esm/review/subject.js +116 -26
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- package/dist/esm/rust/affected-manifests.d.ts +38 -0
- package/dist/esm/rust/affected-manifests.js +69 -0
- package/dist/esm/session-memory/fetch-index.d.ts +1 -1
- package/dist/esm/status/snapshot.js +32 -25
- package/dist/esm/test/duration-sequencer.d.ts +20 -0
- package/dist/esm/test/duration-sequencer.js +54 -0
- package/dist/esm/test/shard-durations.json +922 -0
- package/dist/esm/test/worker-budget.d.ts +6 -0
- package/dist/esm/test/worker-budget.js +59 -1
- package/dist/esm/test-helpers/global-setup.d.ts +1 -2
- package/dist/esm/test-helpers/global-setup.js +79 -32
- package/dist/esm/typecheck/planner.d.ts +9 -1
- package/dist/esm/typecheck/planner.js +31 -0
- package/dist/esm/ultragoal/service.d.ts +7 -0
- package/dist/esm/ultragoal/service.js +42 -2
- package/dist/esm/utils/write-json-file.js +3 -1
- package/dist/esm/worktrees/codex-project-trust.d.ts +15 -0
- package/dist/esm/worktrees/codex-project-trust.js +56 -0
- package/dist/esm/worktrees/orphan-scan.d.ts +50 -0
- package/dist/esm/worktrees/orphan-scan.js +211 -0
- package/dist/esm/worktrees/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +18 -13
|
@@ -54,11 +54,103 @@ export function findTestFiles(sourceFile, projectDir) {
|
|
|
54
54
|
const pattern = join(dir, `${base}.{test,integration.test}{.ts,.tsx}`);
|
|
55
55
|
return globSync(pattern, { cwd: projectDir, withFileTypes: false });
|
|
56
56
|
}
|
|
57
|
+
/**
|
|
58
|
+
* Source files whose tests are NOT colocated.
|
|
59
|
+
*
|
|
60
|
+
* `findTestFiles` maps a changed file to `<same-dir>/<same-basename>.test.ts`.
|
|
61
|
+
* That is right for the common case and wrong for a few "contract" files whose
|
|
62
|
+
* dependent assertions deliberately live elsewhere — a registry that generates
|
|
63
|
+
* docs, a template rendered under a byte budget, and so on. Those tests then
|
|
64
|
+
* stay invisible to `--affected`, so `wp ci-preflight` (whose every stage runs
|
|
65
|
+
* `--affected --branch`) can report "all required checks passed" while they are
|
|
66
|
+
* genuinely failing.
|
|
67
|
+
*
|
|
68
|
+
* Observed 2026-07-24: adding MCP tools broke `scaffold-agents-md`,
|
|
69
|
+
* `package-manifest`, `reference-parity-tool-surface`, and `session-docs`.
|
|
70
|
+
* `wp ci-preflight` was green locally AND the pre-push hook passed; CI caught
|
|
71
|
+
* all of it. Three PRs landed red before the pattern was understood.
|
|
72
|
+
*
|
|
73
|
+
* Patterns are matched against repo-relative POSIX paths. Listed tests are
|
|
74
|
+
* existence-filtered by the caller, so an entry that does not apply to a
|
|
75
|
+
* consumer repo is simply inert.
|
|
76
|
+
*/
|
|
77
|
+
const CROSS_CUTTING_TEST_DEPENDENTS = [
|
|
78
|
+
{
|
|
79
|
+
// The compiled MCP tool registry feeds the generated AGENTS.md tool list
|
|
80
|
+
// (and its byte budget), the packed README session-memory contract, and
|
|
81
|
+
// reference-parity classification. None are colocated with the registry.
|
|
82
|
+
when: /^src\/mcp\/tools\/_(names|registry)\.ts$/u,
|
|
83
|
+
tests: [
|
|
84
|
+
"src/cli/commands/init/scaffold-agents-md.test.ts",
|
|
85
|
+
"src/build/package-manifest.test.ts",
|
|
86
|
+
"src/__integration__/reference-parity-tool-surface.integration.test.ts",
|
|
87
|
+
"src/mcp/tools/session-docs.test.ts",
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
// The AGENTS.md template renders into the scaffold byte-budget assertion.
|
|
92
|
+
when: /^catalog\/AGENTS\.md\.tpl$/u,
|
|
93
|
+
tests: ["src/cli/commands/init/scaffold-agents-md.test.ts"],
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
// README and the session-memory guide are pinned to the registered
|
|
97
|
+
// session-memory tool set from both directions.
|
|
98
|
+
when: /^(README\.md|docs\/guides\/session-memory\.md)$/u,
|
|
99
|
+
tests: ["src/mcp/tools/session-docs.test.ts", "src/build/package-manifest.test.ts"],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
// blueprint-server.ts owns the MCP architecture contract (tool naming,
|
|
103
|
+
// handler wiring, transport shape). Its architecture test is not colocated
|
|
104
|
+
// under the same basename, so --affected would otherwise skip it.
|
|
105
|
+
when: /^src\/mcp\/blueprint-server\./u,
|
|
106
|
+
tests: ["src/mcp/blueprint-server.test-architecture.test.ts"],
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
// Package surface / publishability contracts live in the audit package,
|
|
110
|
+
// not next to package.json or the manifest builder.
|
|
111
|
+
when: /^(package\.json|src\/build\/package-manifest\.ts)$/u,
|
|
112
|
+
tests: ["src/audit/package-surface.test.ts"],
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
// Marketplace and plugin.json validation is a build-side contract test.
|
|
116
|
+
when: /^\.claude-plugin\/(marketplace|plugin)\.json$/u,
|
|
117
|
+
tests: ["src/build/validate-marketplace.test.ts"],
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
/**
|
|
121
|
+
* Every test path the cross-cutting map can yield.
|
|
122
|
+
*
|
|
123
|
+
* Exported so a guard test can assert these still exist on disk. Without that
|
|
124
|
+
* guard a rename would keep the unit tests green — they only compare strings —
|
|
125
|
+
* while the caller's existence filter silently dropped the path, reinstating
|
|
126
|
+
* the very under-selection this map exists to prevent.
|
|
127
|
+
*/
|
|
128
|
+
export function crossCuttingTestDependentPaths() {
|
|
129
|
+
const paths = new Set();
|
|
130
|
+
for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
|
|
131
|
+
for (const test of rule.tests)
|
|
132
|
+
paths.add(test);
|
|
133
|
+
}
|
|
134
|
+
return [...paths];
|
|
135
|
+
}
|
|
136
|
+
/** Non-colocated tests a changed file is known to affect. */
|
|
137
|
+
export function findCrossCuttingTestFiles(sourceFile) {
|
|
138
|
+
const normalized = sourceFile.replaceAll("\\", "/");
|
|
139
|
+
const matched = new Set();
|
|
140
|
+
for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
|
|
141
|
+
if (rule.when.test(normalized))
|
|
142
|
+
for (const test of rule.tests)
|
|
143
|
+
matched.add(test);
|
|
144
|
+
}
|
|
145
|
+
return [...matched];
|
|
146
|
+
}
|
|
57
147
|
export function discoverTestFiles(changedFiles, projectDir) {
|
|
58
148
|
const testFiles = new Set();
|
|
59
149
|
for (const file of changedFiles) {
|
|
60
150
|
for (const testFile of findTestFiles(file, projectDir))
|
|
61
151
|
testFiles.add(testFile);
|
|
152
|
+
for (const testFile of findCrossCuttingTestFiles(file))
|
|
153
|
+
testFiles.add(testFile);
|
|
62
154
|
}
|
|
63
155
|
return [...testFiles];
|
|
64
156
|
}
|
|
@@ -3,7 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import { parseBlueprint } from "#core/parser";
|
|
5
5
|
import { setBlueprintFrontmatterFields } from "#lifecycle/engine";
|
|
6
|
-
import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
|
|
6
|
+
import { assertCompletedApprovalGate, evaluateApprovalGate, formatApprovalGateRequirement, } from "#lifecycle/audit";
|
|
7
7
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
8
8
|
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
9
9
|
import { assertAllTasksHaveCanonicalPassingEvidence } from "#verification.js";
|
|
@@ -70,6 +70,19 @@ export async function applyLocalBlueprintTransition(input) {
|
|
|
70
70
|
throw new Error(`Cannot promote "${slug}" to ${to_state}: frontmatter \`approvals:\` does not satisfy the promotion reviewer mix (${formatApprovalGateRequirement(approvalEvaluation)}). Record approvals, commit the matching review ledger (e.g. reviews.md), and include tracked review transcript/artifact files referenced by each structured review entry — see catalog/agent/rules/pre-implementation.md.`);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
|
+
// Completion parity for the MCP lifecycle funnel: finalize, promote, and
|
|
74
|
+
// transition all route here, so gate `completed` writes on the same
|
|
75
|
+
// plan-purpose approval CI's `wp audit blueprint-lifecycle` enforces
|
|
76
|
+
// afterward. Reads the raw on-disk markdown (not the trust-gated copy) to
|
|
77
|
+
// mirror the audit's frontmatter parsing exactly.
|
|
78
|
+
if (to_state === "completed") {
|
|
79
|
+
try {
|
|
80
|
+
assertCompletedApprovalGate(overviewPath, markdown);
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
throw new Error(`Cannot complete "${slug}": ${error instanceof Error ? error.message : String(error)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
const updated = setBlueprintFrontmatterFields(trustedMarkdown, {
|
|
74
87
|
status: to_state,
|
|
75
88
|
last_updated: todayIsoDate(),
|
|
@@ -2,14 +2,18 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import matter from "gray-matter";
|
|
5
|
+
import { parseBlueprint } from "#core/parser";
|
|
6
|
+
import { taskIdSchema } from "#core/schema";
|
|
5
7
|
import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
|
|
8
|
+
import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
|
|
6
9
|
import { BlueprintCreationService } from "#service/BlueprintCreationService";
|
|
7
10
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
8
|
-
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
11
|
+
import { getBlueprintDocumentPaths, isBlueprintSlugSegment, } from "#utils/document-paths.js";
|
|
9
12
|
import { makeNextAction } from "#next-action.js";
|
|
10
13
|
import { ensureProjectionReady } from "#projection-ready.js";
|
|
11
14
|
import { applyPromotionTrustGate } from "#trust/promotion.js";
|
|
12
15
|
import { validateBlueprintTrust } from "#trust/validator.js";
|
|
16
|
+
import { applyVerification, readTaskVerification } from "#verification.js";
|
|
13
17
|
import { dbPath, persistBlueprintMarkdown, reIngest } from "#mcp/blueprint/_shared/db";
|
|
14
18
|
import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
|
|
15
19
|
import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
|
|
@@ -37,7 +41,7 @@ const createSchema = MutationTarget.extend({
|
|
|
37
41
|
head_at_ingest: z.string().nullable().optional(),
|
|
38
42
|
});
|
|
39
43
|
const putTaskSchema = z.object({
|
|
40
|
-
id:
|
|
44
|
+
id: taskIdSchema,
|
|
41
45
|
title: z.string().min(1),
|
|
42
46
|
status: z.enum(["todo", "in-progress", "blocked", "done", "dropped"]).default("todo"),
|
|
43
47
|
wave: z.string().optional(),
|
|
@@ -69,6 +73,12 @@ const putPromotionGateSchema = z.object({
|
|
|
69
73
|
command: z.string().min(1),
|
|
70
74
|
expected_outcome: z.string().min(1),
|
|
71
75
|
last_result: z.string().min(1),
|
|
76
|
+
// Explicit per-gate pre-implementation deferral. When set, `wp blueprint
|
|
77
|
+
// promote` skips executing/existence-checking this gate's command entirely
|
|
78
|
+
// and records a `deferred: ...` Last result instead. See
|
|
79
|
+
// src/blueprint/trust/promotion.ts and catalog/agent/rules
|
|
80
|
+
// /pre-implementation.md's Promotion gate section.
|
|
81
|
+
defer: z.literal("pre-implementation").optional(),
|
|
72
82
|
});
|
|
73
83
|
const putTrustDossierSchema = z.object({
|
|
74
84
|
readiness: putTrustReadinessSchema,
|
|
@@ -177,6 +187,11 @@ const putTrustDossierJsonSchema = {
|
|
|
177
187
|
command: { type: "string", description: "wp or ./bin/wp facade command" },
|
|
178
188
|
expected_outcome: { type: "string" },
|
|
179
189
|
last_result: { type: "string" },
|
|
190
|
+
defer: {
|
|
191
|
+
type: "string",
|
|
192
|
+
const: "pre-implementation",
|
|
193
|
+
description: "Explicit opt-in: skip executing/existence-checking this gate at promote time",
|
|
194
|
+
},
|
|
180
195
|
},
|
|
181
196
|
required: ["gate", "command", "expected_outcome", "last_result"],
|
|
182
197
|
additionalProperties: false,
|
|
@@ -320,12 +335,7 @@ function renderTrustDossierMarkdown(dossier) {
|
|
|
320
335
|
"",
|
|
321
336
|
"### Promotion Gates",
|
|
322
337
|
"",
|
|
323
|
-
...
|
|
324
|
-
gate.gate,
|
|
325
|
-
gate.command,
|
|
326
|
-
gate.expected_outcome,
|
|
327
|
-
gate.last_result,
|
|
328
|
-
])),
|
|
338
|
+
...renderPromotionGatesTable(dossier.promotion_gates),
|
|
329
339
|
"",
|
|
330
340
|
"### Residual Unknowns",
|
|
331
341
|
"",
|
|
@@ -333,6 +343,23 @@ function renderTrustDossierMarkdown(dossier) {
|
|
|
333
343
|
"",
|
|
334
344
|
];
|
|
335
345
|
}
|
|
346
|
+
// Only emits the (opt-in) Defer column when at least one gate uses it, so
|
|
347
|
+
// the common no-defer case keeps rendering the legacy 4-column table exactly
|
|
348
|
+
// as before -- matching src/blueprint/trust/dossier.ts's parser, which
|
|
349
|
+
// accepts either width.
|
|
350
|
+
function renderPromotionGatesTable(gates) {
|
|
351
|
+
const hasDefer = gates.some((gate) => gate.defer !== undefined);
|
|
352
|
+
if (!hasDefer) {
|
|
353
|
+
return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result"], gates.map((gate) => [gate.gate, gate.command, gate.expected_outcome, gate.last_result]));
|
|
354
|
+
}
|
|
355
|
+
return renderMarkdownTable(["Gate", "Command", "Expected outcome", "Last result", "Defer"], gates.map((gate) => [
|
|
356
|
+
gate.gate,
|
|
357
|
+
gate.command,
|
|
358
|
+
gate.expected_outcome,
|
|
359
|
+
gate.last_result,
|
|
360
|
+
gate.defer ?? "",
|
|
361
|
+
]));
|
|
362
|
+
}
|
|
336
363
|
function normalizeResidualUnknowns(value) {
|
|
337
364
|
if (value === undefined)
|
|
338
365
|
return "None.";
|
|
@@ -353,6 +380,52 @@ function renderMarkdownTable(headers, rows) {
|
|
|
353
380
|
function markdownTableCell(value) {
|
|
354
381
|
return value.replace(/\s*\n\s*/gu, " ").trim();
|
|
355
382
|
}
|
|
383
|
+
/**
|
|
384
|
+
* wp_blueprint_put's task schema has no field for the `webpresso-evidence-v1`
|
|
385
|
+
* verification block that wp_blueprint_task_verify writes — evidence is an
|
|
386
|
+
* append-only trust artifact that callers must never freely reconstruct (see
|
|
387
|
+
* catalog/agent/rules/pre-implementation.md). A naive re-render of the whole
|
|
388
|
+
* document therefore silently discards any `**Verification:**` block already
|
|
389
|
+
* recorded for a `done` task.
|
|
390
|
+
*
|
|
391
|
+
* This reads the exact evidence already on disk for every task that has a
|
|
392
|
+
* block, and re-applies it verbatim onto the freshly rendered markdown when
|
|
393
|
+
* that task is still present in the payload with status "done" — the caller
|
|
394
|
+
* never supplies or fabricates evidence content. When the payload cannot
|
|
395
|
+
* represent a protected task (dropped, or moved off "done"), this fails
|
|
396
|
+
* closed with an actionable error instead of destroying the evidence.
|
|
397
|
+
*/
|
|
398
|
+
function preserveExistingTaskVerification(existingMarkdown, slug, document, renderedMarkdown) {
|
|
399
|
+
const existingBlueprint = parseBlueprint(existingMarkdown, slug);
|
|
400
|
+
const protectedTasks = existingBlueprint.tasks
|
|
401
|
+
.map((task) => ({ id: task.id, evidence: readTaskVerification(existingMarkdown, task.id) }))
|
|
402
|
+
.filter((entry) => entry.evidence !== null);
|
|
403
|
+
let markdown = renderedMarkdown;
|
|
404
|
+
for (const protectedTask of protectedTasks) {
|
|
405
|
+
const { id, evidence } = protectedTask;
|
|
406
|
+
const submittedTask = document.tasks.find((task) => task.id === id);
|
|
407
|
+
if (!submittedTask || submittedTask.status !== "done") {
|
|
408
|
+
return { error: describeEvidenceLossRefusal(id, submittedTask?.status) };
|
|
409
|
+
}
|
|
410
|
+
const applied = applyVerification(markdown, id, evidence);
|
|
411
|
+
if (!applied.ok) {
|
|
412
|
+
return {
|
|
413
|
+
error: `wp_blueprint_put failed to re-apply preserved verification evidence for task ${id}: ${applied.failures.join("; ")}`,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
markdown = applied.markdown;
|
|
417
|
+
}
|
|
418
|
+
return { markdown };
|
|
419
|
+
}
|
|
420
|
+
function describeEvidenceLossRefusal(taskId, submittedStatus) {
|
|
421
|
+
const disposition = submittedStatus === undefined
|
|
422
|
+
? `the submitted document does not include task ${taskId}`
|
|
423
|
+
: `the submitted document sets task ${taskId}'s status to "${submittedStatus}"`;
|
|
424
|
+
return (`wp_blueprint_put refused: task ${taskId} already has verification evidence recorded by ` +
|
|
425
|
+
`wp_blueprint_task_verify, but ${disposition} — wp_blueprint_put cannot represent or discard ` +
|
|
426
|
+
`that evidence. Keep task ${taskId} present with status "done" in the payload; put will carry ` +
|
|
427
|
+
`the recorded evidence forward automatically.`);
|
|
428
|
+
}
|
|
356
429
|
function validatePutTrustDossier(repoRoot, file, markdown) {
|
|
357
430
|
const validated = validateBlueprintTrust({
|
|
358
431
|
repoRoot,
|
|
@@ -374,6 +447,13 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
|
374
447
|
if (!p.success)
|
|
375
448
|
return err("wp_blueprint_put validation error", p.error.message);
|
|
376
449
|
const { project_id, slug, document, request_id, head_at_ingest } = p.data;
|
|
450
|
+
// Validate the slug against the canonical segment pattern before ANY
|
|
451
|
+
// routing or filesystem probe: an unvalidated slug such as "../../outside"
|
|
452
|
+
// would otherwise construct paths outside the blueprint root.
|
|
453
|
+
const slugSegments = slug.split("/");
|
|
454
|
+
if (slugSegments.length === 0 || !slugSegments.every((s) => isBlueprintSlugSegment(s))) {
|
|
455
|
+
return err("wp_blueprint_put validation error", `Invalid blueprint slug "${slug}": every segment must match lowercase [a-z0-9] with single hyphens.`);
|
|
456
|
+
}
|
|
377
457
|
const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
|
|
378
458
|
if ("content" in resolvedProject)
|
|
379
459
|
return resolvedProject;
|
|
@@ -408,16 +488,34 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
|
408
488
|
if (found && found.state !== document.status) {
|
|
409
489
|
return err("wp_blueprint_put refused", `Blueprint "${slug}" currently lives in "${found.state}" and cannot be rewritten as "${document.status}" without a lifecycle transition.`);
|
|
410
490
|
}
|
|
491
|
+
// Failure atomicity: render and validate against the canonical folder
|
|
492
|
+
// path FIRST, and only canonicalize an existing flat document after every
|
|
493
|
+
// validation has passed. A validation failure must leave the flat file
|
|
494
|
+
// untouched; a persistence failure after conversion leaves the
|
|
495
|
+
// byte-identical document at the canonical folder path (retry-safe).
|
|
411
496
|
const overviewPath = found
|
|
412
|
-
? found.
|
|
413
|
-
|
|
414
|
-
|
|
497
|
+
? found.shape === "flat"
|
|
498
|
+
? getBlueprintDocumentPaths(root, found.state, slug).folder
|
|
499
|
+
: found.path
|
|
500
|
+
: getBlueprintDocumentPaths(root, document.status, slug).folder;
|
|
501
|
+
let markdown = renderBlueprintMarkdownFromDocument(slug, document);
|
|
502
|
+
if (found) {
|
|
503
|
+
const existingMarkdown = readFileSync(found.path, "utf8");
|
|
504
|
+
const preserved = preserveExistingTaskVerification(existingMarkdown, slug, document, markdown);
|
|
505
|
+
if ("error" in preserved) {
|
|
506
|
+
return err("wp_blueprint_put refused", preserved.error);
|
|
507
|
+
}
|
|
508
|
+
markdown = preserved.markdown;
|
|
509
|
+
}
|
|
415
510
|
if (document.trust_dossier) {
|
|
416
511
|
const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown);
|
|
417
512
|
if (trustDossierFailure) {
|
|
418
513
|
return err("wp_blueprint_put validation error", trustDossierFailure);
|
|
419
514
|
}
|
|
420
515
|
}
|
|
516
|
+
if (found && found.shape === "flat") {
|
|
517
|
+
await canonicalizeBlueprint(writeCwd, slug);
|
|
518
|
+
}
|
|
421
519
|
const blueprint = await persistBlueprintMarkdown({
|
|
422
520
|
projectCwd: writeCwd,
|
|
423
521
|
slug,
|
|
@@ -4,7 +4,7 @@ import matter from "gray-matter";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { logReviewEntry, REVIEWER_IDS } from "#cli/commands/review.js";
|
|
6
6
|
import { isPolicyApprovalVerdict } from "#review/verdict.js";
|
|
7
|
-
import { isBlueprintSlugSegment } from "#utils/document-paths.js";
|
|
7
|
+
import { folderBlueprintRequiredMessage, isBlueprintSlugSegment } from "#utils/document-paths.js";
|
|
8
8
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
9
9
|
import { writeFileAtomic } from "#shared-utils/write-json-file.js";
|
|
10
10
|
import { err, finishPayload } from "#mcp/blueprint/_shared/errors";
|
|
@@ -98,6 +98,13 @@ export async function handleReviewLog(projectResolver, cwd, raw) {
|
|
|
98
98
|
const found = findBlueprintDir(root, slug, ALL_BLUEPRINT_STATES);
|
|
99
99
|
if (!found)
|
|
100
100
|
return err("wp_blueprint_review_log failed", `Blueprint "${slug}" not found in any state directory`);
|
|
101
|
+
const shapeFailure = folderBlueprintRequiredMessage({
|
|
102
|
+
documentPath: found.path,
|
|
103
|
+
lifecycleSlug: `${found.state}/${slug}`,
|
|
104
|
+
shape: found.shape,
|
|
105
|
+
});
|
|
106
|
+
if (shapeFailure)
|
|
107
|
+
return err("wp_blueprint_review_log failed", shapeFailure);
|
|
101
108
|
const blueprintDir = realpathSync(path.dirname(found.path));
|
|
102
109
|
const artifactAbsolute = path.resolve(blueprintDir, normalizedArtifactPath);
|
|
103
110
|
if (!artifactAbsolute.startsWith(`${blueprintDir}${path.sep}`)) {
|
|
@@ -8,12 +8,53 @@ import { dbPath, reIngest } from "#mcp/blueprint/_shared/db";
|
|
|
8
8
|
import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
|
|
9
9
|
import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
|
|
10
10
|
import { toStr } from "#mcp/blueprint/_shared/payload";
|
|
11
|
-
import { resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
11
|
+
import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
12
12
|
import { resolveSyncAdapter, runPlatformMutationSync } from "#mcp/blueprint/_shared/sync";
|
|
13
13
|
import { writeFileAtomic } from "#shared-utils/write-json-file.js";
|
|
14
14
|
import { escapeRegex } from "#utils/string";
|
|
15
|
+
import { withMarkdownWriteLock } from "#db/paths.js";
|
|
16
|
+
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
17
|
+
const ALL_STATES = ["draft", "planned", "in-progress", "parked", "archived", "completed"];
|
|
18
|
+
/**
|
|
19
|
+
* Matches the canonical task-heading grammar `parser.ts`'s `extractTasks`
|
|
20
|
+
* uses (`^####\s+(?:\[lane\]\s+)?Task\s+<id>:`), not a looser ad hoc pattern.
|
|
21
|
+
* The id is anchored on a trailing colon so "1.1" cannot match "1.10", and
|
|
22
|
+
* an optional `[lane]` prefix is accepted the same way the canonical parser
|
|
23
|
+
* does.
|
|
24
|
+
*/
|
|
25
|
+
function buildTaskHeadingPattern(taskId) {
|
|
26
|
+
return new RegExp(`^####\\s+(?:\\[[^\\]]+\\]\\s+)?Task\\s+${escapeRegex(taskId)}:`, "u");
|
|
27
|
+
}
|
|
28
|
+
function findTaskStatusLine(markdown, taskId) {
|
|
29
|
+
const lines = markdown.split("\n");
|
|
30
|
+
const headingPattern = buildTaskHeadingPattern(taskId);
|
|
31
|
+
let inBlock = false;
|
|
32
|
+
for (let i = 0; i < lines.length; i++) {
|
|
33
|
+
const line = lines[i] ?? "";
|
|
34
|
+
if (headingPattern.test(line))
|
|
35
|
+
inBlock = true;
|
|
36
|
+
else if (inBlock && /^####\s+/u.test(line))
|
|
37
|
+
break;
|
|
38
|
+
else if (inBlock) {
|
|
39
|
+
// Preserve any inline metadata sharing the status line (the canonical
|
|
40
|
+
// format allows `**Status:** done | **Depends:** Task 1.1`) instead of
|
|
41
|
+
// overwriting the whole line and discarding it.
|
|
42
|
+
const match = line.match(/^(\*\*Status:\*\*\s*)(\S+)(.*)$/u);
|
|
43
|
+
if (match) {
|
|
44
|
+
return {
|
|
45
|
+
lineIndex: i,
|
|
46
|
+
prefix: match[1] ?? "",
|
|
47
|
+
status: match[2] ?? "",
|
|
48
|
+
suffix: match[3] ?? "",
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
15
55
|
const advanceSchema = z.object({
|
|
16
56
|
project_id: z.string(),
|
|
57
|
+
slug: z.string(),
|
|
17
58
|
task_id: z.string(),
|
|
18
59
|
to: z.enum(["todo", "in-progress", "blocked", "done", "dropped"]),
|
|
19
60
|
request_id: z.string().min(1).optional(),
|
|
@@ -23,7 +64,7 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
|
|
|
23
64
|
const p = advanceSchema.safeParse(raw);
|
|
24
65
|
if (!p.success)
|
|
25
66
|
return err("wp_blueprint_task_advance validation error", p.error.message);
|
|
26
|
-
const { project_id, task_id, to, request_id, head_at_ingest } = p.data;
|
|
67
|
+
const { project_id, slug, task_id, to, request_id, head_at_ingest } = p.data;
|
|
27
68
|
const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
|
|
28
69
|
if ("content" in resolvedProject)
|
|
29
70
|
return resolvedProject;
|
|
@@ -32,7 +73,7 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
|
|
|
32
73
|
const freshnessFailure = validateMutationFreshnessToken(projectCwd, head_at_ingest, "wp_blueprint_task_advance", "wp_blueprint_list");
|
|
33
74
|
if (freshnessFailure)
|
|
34
75
|
return freshnessFailure;
|
|
35
|
-
const payloadHash = hashMutationPayload({ task_id, to });
|
|
76
|
+
const payloadHash = hashMutationPayload({ slug, task_id, to });
|
|
36
77
|
const replay = request_id !== undefined
|
|
37
78
|
? readMutationReplay(projectCwd, "wp_blueprint_task_advance", request_id, payloadHash)
|
|
38
79
|
: null;
|
|
@@ -54,70 +95,88 @@ export async function handleTaskAdvance(projectResolver, cwd, raw) {
|
|
|
54
95
|
return err("wp_blueprint_task_advance failed", "Blueprint DB not found");
|
|
55
96
|
try {
|
|
56
97
|
const conn = openDb(target);
|
|
57
|
-
let oldStatus = null;
|
|
58
|
-
let filePath = null;
|
|
59
|
-
let blueprintSlug = null;
|
|
60
98
|
try {
|
|
61
99
|
const row = conn.db
|
|
62
|
-
.prepare("SELECT status
|
|
63
|
-
.get(task_id);
|
|
100
|
+
.prepare("SELECT status FROM tasks WHERE task_id = ? AND blueprint_slug = ? LIMIT 1")
|
|
101
|
+
.get(task_id, slug);
|
|
64
102
|
if (!row)
|
|
65
|
-
return err("wp_blueprint_task_advance failed", `Task "${task_id}" not found in
|
|
66
|
-
oldStatus = row.status;
|
|
67
|
-
blueprintSlug = row.blueprint_slug;
|
|
68
|
-
const bp = conn.db
|
|
69
|
-
.prepare("SELECT file_path FROM blueprints WHERE slug = ?")
|
|
70
|
-
.get(row.blueprint_slug);
|
|
71
|
-
if (bp?.file_path)
|
|
72
|
-
filePath = bp.file_path;
|
|
103
|
+
return err("wp_blueprint_task_advance failed", `Task "${task_id}" not found in blueprint "${slug}"`);
|
|
73
104
|
}
|
|
74
105
|
finally {
|
|
75
106
|
conn.close();
|
|
76
107
|
}
|
|
108
|
+
// Derive the authoritative fromStatus from a live read of the blueprint's
|
|
109
|
+
// on-disk markdown, not the SQLite projection above (which is only
|
|
110
|
+
// re-ingested asynchronously and can be stale relative to the file --
|
|
111
|
+
// e.g. after a blueprint move/promotion changed its lifecycle directory).
|
|
112
|
+
// Fail closed here rather than proceeding with a guessed value, or later
|
|
113
|
+
// silently reporting success when the local write never happens.
|
|
114
|
+
const root = resolveBlueprintRoot(projectCwd);
|
|
115
|
+
const foundBeforePush = findBlueprintDir(root, slug, ALL_STATES);
|
|
116
|
+
if (!foundBeforePush || !existsSync(foundBeforePush.path)) {
|
|
117
|
+
return err("wp_blueprint_task_advance failed", `Blueprint "${slug}" overview file not found`);
|
|
118
|
+
}
|
|
119
|
+
const preStatus = findTaskStatusLine(readFileSync(foundBeforePush.path, "utf8"), task_id);
|
|
120
|
+
if (!preStatus) {
|
|
121
|
+
return err("wp_blueprint_task_advance failed", `Task "${task_id}" has no **Status:** line in blueprint "${slug}"`);
|
|
122
|
+
}
|
|
123
|
+
const oldStatus = preStatus.status;
|
|
77
124
|
// Platform-first path: push event + pull fresh replica before local update.
|
|
78
125
|
// Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
|
|
79
126
|
const adapter = await resolveSyncAdapter(projectCwd);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
try {
|
|
128
|
+
await runPlatformMutationSync(adapter, {
|
|
129
|
+
label: "wp_blueprint_task_advance",
|
|
130
|
+
event: {
|
|
131
|
+
eventId: randomUUID(),
|
|
132
|
+
repoId: process.env["WP_BLUEPRINT_PLATFORM_REPO_ID"] ?? "local",
|
|
133
|
+
occurredAt: new Date().toISOString(),
|
|
134
|
+
type: "task.status_changed",
|
|
135
|
+
payload: {
|
|
88
136
|
type: "task.status_changed",
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
fromStatus: oldStatus,
|
|
94
|
-
toStatus: to,
|
|
95
|
-
},
|
|
137
|
+
blueprintSlug: slug,
|
|
138
|
+
taskId: task_id,
|
|
139
|
+
fromStatus: oldStatus,
|
|
140
|
+
toStatus: to,
|
|
96
141
|
},
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
return err("wp_blueprint_task_advance failed", toStr(e));
|
|
102
|
-
}
|
|
142
|
+
},
|
|
143
|
+
ensureFreshSlug: slug,
|
|
144
|
+
});
|
|
103
145
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
146
|
+
catch (e) {
|
|
147
|
+
return err("wp_blueprint_task_advance failed", toStr(e));
|
|
148
|
+
}
|
|
149
|
+
// Always update local markdown + SQLite. Re-resolve the path live INSIDE
|
|
150
|
+
// the repo-scoped write lock: a concurrent promotion can rename the
|
|
151
|
+
// blueprint's directory between the pre-push read above and this write.
|
|
152
|
+
// A missing blueprint or status line here is a genuine race, not
|
|
153
|
+
// something to paper over as success -- `wrote` makes that failure
|
|
154
|
+
// observable to the caller instead of silently no-op'ing.
|
|
155
|
+
let wrote = false;
|
|
156
|
+
await withMarkdownWriteLock(projectCwd, () => {
|
|
157
|
+
const liveFound = findBlueprintDir(root, slug, ALL_STATES);
|
|
158
|
+
const livePath = liveFound?.path;
|
|
159
|
+
if (!livePath || !existsSync(livePath))
|
|
160
|
+
return;
|
|
161
|
+
const markdown = readFileSync(livePath, "utf8");
|
|
162
|
+
const live = findTaskStatusLine(markdown, task_id);
|
|
163
|
+
if (!live)
|
|
164
|
+
return;
|
|
165
|
+
const lines = markdown.split("\n");
|
|
166
|
+
lines[live.lineIndex] = `${live.prefix}${to}${live.suffix}`;
|
|
167
|
+
writeFileAtomic(livePath, lines.join("\n"), "utf8");
|
|
168
|
+
wrote = true;
|
|
169
|
+
});
|
|
170
|
+
if (!wrote) {
|
|
171
|
+
// The platform mutation above already succeeded by this point (or was
|
|
172
|
+
// a no-op if platform sync is disabled) -- this failure therefore
|
|
173
|
+
// means platform and local state have genuinely diverged, not just
|
|
174
|
+
// that the local write was skipped harmlessly. There is no automatic
|
|
175
|
+
// reconciliation path today (that requires the platform-first
|
|
176
|
+
// mutation reconciliation work tracked separately, not built here);
|
|
177
|
+
// surface the divergence explicitly rather than a generic error so an
|
|
178
|
+
// operator knows this needs manual attention, not just a retry.
|
|
179
|
+
return err("wp_blueprint_task_advance failed", `Task "${task_id}" in blueprint "${slug}" could not be updated locally after the platform mutation already succeeded (blueprint or status line changed concurrently). Platform and local state have diverged and require manual reconciliation -- retrying this call alone will not fix it if the blueprint moved again.`);
|
|
121
180
|
}
|
|
122
181
|
try {
|
|
123
182
|
await reIngest(projectCwd);
|