@webpresso/agent-kit 3.1.30 → 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 +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/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 +8 -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 +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/local.d.ts +2 -1
- package/dist/esm/blueprint/lifecycle/local.js +19 -3
- package/dist/esm/blueprint/lifecycle/review-provenance.js +36 -11
- 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/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 +17 -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 +6 -3
- 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 +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/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/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 +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/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/review.d.ts +35 -3
- package/dist/esm/cli/commands/review.js +729 -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/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 +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 +3 -3
- package/dist/esm/hooks/doctor.js +30 -15
- 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/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 +4 -1
- package/dist/esm/mcp/tools/audit.js +30 -0
- package/dist/esm/mcp/tools/audits.d.ts +3 -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-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/review/authority.js +13 -5
- package/dist/esm/review/availability.d.ts +22 -0
- package/dist/esm/review/availability.js +107 -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.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 +12 -0
- package/dist/esm/review/subject.js +74 -22
- package/dist/esm/runtime/opencode-account-materializer.d.ts +34 -0
- package/dist/esm/runtime/opencode-account-materializer.js +33 -0
- 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/registry.d.ts +8 -0
- package/dist/esm/worktrees/registry.js +18 -3
- package/package.json +16 -13
|
@@ -54,11 +54,85 @@ 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
|
+
/**
|
|
103
|
+
* Every test path the cross-cutting map can yield.
|
|
104
|
+
*
|
|
105
|
+
* Exported so a guard test can assert these still exist on disk. Without that
|
|
106
|
+
* guard a rename would keep the unit tests green — they only compare strings —
|
|
107
|
+
* while the caller's existence filter silently dropped the path, reinstating
|
|
108
|
+
* the very under-selection this map exists to prevent.
|
|
109
|
+
*/
|
|
110
|
+
export function crossCuttingTestDependentPaths() {
|
|
111
|
+
const paths = new Set();
|
|
112
|
+
for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
|
|
113
|
+
for (const test of rule.tests)
|
|
114
|
+
paths.add(test);
|
|
115
|
+
}
|
|
116
|
+
return [...paths];
|
|
117
|
+
}
|
|
118
|
+
/** Non-colocated tests a changed file is known to affect. */
|
|
119
|
+
export function findCrossCuttingTestFiles(sourceFile) {
|
|
120
|
+
const normalized = sourceFile.replaceAll("\\", "/");
|
|
121
|
+
const matched = new Set();
|
|
122
|
+
for (const rule of CROSS_CUTTING_TEST_DEPENDENTS) {
|
|
123
|
+
if (rule.when.test(normalized))
|
|
124
|
+
for (const test of rule.tests)
|
|
125
|
+
matched.add(test);
|
|
126
|
+
}
|
|
127
|
+
return [...matched];
|
|
128
|
+
}
|
|
57
129
|
export function discoverTestFiles(changedFiles, projectDir) {
|
|
58
130
|
const testFiles = new Set();
|
|
59
131
|
for (const file of changedFiles) {
|
|
60
132
|
for (const testFile of findTestFiles(file, projectDir))
|
|
61
133
|
testFiles.add(testFile);
|
|
134
|
+
for (const testFile of findCrossCuttingTestFiles(file))
|
|
135
|
+
testFiles.add(testFile);
|
|
62
136
|
}
|
|
63
137
|
return [...testFiles];
|
|
64
138
|
}
|
|
@@ -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,17 @@ 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";
|
|
5
6
|
import { evaluateApprovalGate, formatApprovalGateRequirement } from "#lifecycle/audit";
|
|
7
|
+
import { canonicalizeBlueprint } from "#lifecycle/canonicalize.js";
|
|
6
8
|
import { BlueprintCreationService } from "#service/BlueprintCreationService";
|
|
7
9
|
import { resolveBlueprintRoot } from "#utils/blueprint-root.js";
|
|
8
|
-
import { getBlueprintDocumentPaths } from "#utils/document-paths.js";
|
|
10
|
+
import { getBlueprintDocumentPaths, isBlueprintSlugSegment, } from "#utils/document-paths.js";
|
|
9
11
|
import { makeNextAction } from "#next-action.js";
|
|
10
12
|
import { ensureProjectionReady } from "#projection-ready.js";
|
|
11
13
|
import { applyPromotionTrustGate } from "#trust/promotion.js";
|
|
12
14
|
import { validateBlueprintTrust } from "#trust/validator.js";
|
|
15
|
+
import { applyVerification, readTaskVerification } from "#verification.js";
|
|
13
16
|
import { dbPath, persistBlueprintMarkdown, reIngest } from "#mcp/blueprint/_shared/db";
|
|
14
17
|
import { err, finishPayload, jsonContent } from "#mcp/blueprint/_shared/errors";
|
|
15
18
|
import { hashMutationPayload, readMutationReplay, recordMutationReplay, validateMutationFreshnessToken, } from "#mcp/blueprint/_shared/freshness";
|
|
@@ -353,6 +356,52 @@ function renderMarkdownTable(headers, rows) {
|
|
|
353
356
|
function markdownTableCell(value) {
|
|
354
357
|
return value.replace(/\s*\n\s*/gu, " ").trim();
|
|
355
358
|
}
|
|
359
|
+
/**
|
|
360
|
+
* wp_blueprint_put's task schema has no field for the `webpresso-evidence-v1`
|
|
361
|
+
* verification block that wp_blueprint_task_verify writes — evidence is an
|
|
362
|
+
* append-only trust artifact that callers must never freely reconstruct (see
|
|
363
|
+
* catalog/agent/rules/pre-implementation.md). A naive re-render of the whole
|
|
364
|
+
* document therefore silently discards any `**Verification:**` block already
|
|
365
|
+
* recorded for a `done` task.
|
|
366
|
+
*
|
|
367
|
+
* This reads the exact evidence already on disk for every task that has a
|
|
368
|
+
* block, and re-applies it verbatim onto the freshly rendered markdown when
|
|
369
|
+
* that task is still present in the payload with status "done" — the caller
|
|
370
|
+
* never supplies or fabricates evidence content. When the payload cannot
|
|
371
|
+
* represent a protected task (dropped, or moved off "done"), this fails
|
|
372
|
+
* closed with an actionable error instead of destroying the evidence.
|
|
373
|
+
*/
|
|
374
|
+
function preserveExistingTaskVerification(existingMarkdown, slug, document, renderedMarkdown) {
|
|
375
|
+
const existingBlueprint = parseBlueprint(existingMarkdown, slug);
|
|
376
|
+
const protectedTasks = existingBlueprint.tasks
|
|
377
|
+
.map((task) => ({ id: task.id, evidence: readTaskVerification(existingMarkdown, task.id) }))
|
|
378
|
+
.filter((entry) => entry.evidence !== null);
|
|
379
|
+
let markdown = renderedMarkdown;
|
|
380
|
+
for (const protectedTask of protectedTasks) {
|
|
381
|
+
const { id, evidence } = protectedTask;
|
|
382
|
+
const submittedTask = document.tasks.find((task) => task.id === id);
|
|
383
|
+
if (!submittedTask || submittedTask.status !== "done") {
|
|
384
|
+
return { error: describeEvidenceLossRefusal(id, submittedTask?.status) };
|
|
385
|
+
}
|
|
386
|
+
const applied = applyVerification(markdown, id, evidence);
|
|
387
|
+
if (!applied.ok) {
|
|
388
|
+
return {
|
|
389
|
+
error: `wp_blueprint_put failed to re-apply preserved verification evidence for task ${id}: ${applied.failures.join("; ")}`,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
markdown = applied.markdown;
|
|
393
|
+
}
|
|
394
|
+
return { markdown };
|
|
395
|
+
}
|
|
396
|
+
function describeEvidenceLossRefusal(taskId, submittedStatus) {
|
|
397
|
+
const disposition = submittedStatus === undefined
|
|
398
|
+
? `the submitted document does not include task ${taskId}`
|
|
399
|
+
: `the submitted document sets task ${taskId}'s status to "${submittedStatus}"`;
|
|
400
|
+
return (`wp_blueprint_put refused: task ${taskId} already has verification evidence recorded by ` +
|
|
401
|
+
`wp_blueprint_task_verify, but ${disposition} — wp_blueprint_put cannot represent or discard ` +
|
|
402
|
+
`that evidence. Keep task ${taskId} present with status "done" in the payload; put will carry ` +
|
|
403
|
+
`the recorded evidence forward automatically.`);
|
|
404
|
+
}
|
|
356
405
|
function validatePutTrustDossier(repoRoot, file, markdown) {
|
|
357
406
|
const validated = validateBlueprintTrust({
|
|
358
407
|
repoRoot,
|
|
@@ -374,6 +423,13 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
|
374
423
|
if (!p.success)
|
|
375
424
|
return err("wp_blueprint_put validation error", p.error.message);
|
|
376
425
|
const { project_id, slug, document, request_id, head_at_ingest } = p.data;
|
|
426
|
+
// Validate the slug against the canonical segment pattern before ANY
|
|
427
|
+
// routing or filesystem probe: an unvalidated slug such as "../../outside"
|
|
428
|
+
// would otherwise construct paths outside the blueprint root.
|
|
429
|
+
const slugSegments = slug.split("/");
|
|
430
|
+
if (slugSegments.length === 0 || !slugSegments.every((s) => isBlueprintSlugSegment(s))) {
|
|
431
|
+
return err("wp_blueprint_put validation error", `Invalid blueprint slug "${slug}": every segment must match lowercase [a-z0-9] with single hyphens.`);
|
|
432
|
+
}
|
|
377
433
|
const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
|
|
378
434
|
if ("content" in resolvedProject)
|
|
379
435
|
return resolvedProject;
|
|
@@ -408,16 +464,34 @@ export async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
|
408
464
|
if (found && found.state !== document.status) {
|
|
409
465
|
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
466
|
}
|
|
467
|
+
// Failure atomicity: render and validate against the canonical folder
|
|
468
|
+
// path FIRST, and only canonicalize an existing flat document after every
|
|
469
|
+
// validation has passed. A validation failure must leave the flat file
|
|
470
|
+
// untouched; a persistence failure after conversion leaves the
|
|
471
|
+
// byte-identical document at the canonical folder path (retry-safe).
|
|
411
472
|
const overviewPath = found
|
|
412
|
-
? found.
|
|
413
|
-
|
|
414
|
-
|
|
473
|
+
? found.shape === "flat"
|
|
474
|
+
? getBlueprintDocumentPaths(root, found.state, slug).folder
|
|
475
|
+
: found.path
|
|
476
|
+
: getBlueprintDocumentPaths(root, document.status, slug).folder;
|
|
477
|
+
let markdown = renderBlueprintMarkdownFromDocument(slug, document);
|
|
478
|
+
if (found) {
|
|
479
|
+
const existingMarkdown = readFileSync(found.path, "utf8");
|
|
480
|
+
const preserved = preserveExistingTaskVerification(existingMarkdown, slug, document, markdown);
|
|
481
|
+
if ("error" in preserved) {
|
|
482
|
+
return err("wp_blueprint_put refused", preserved.error);
|
|
483
|
+
}
|
|
484
|
+
markdown = preserved.markdown;
|
|
485
|
+
}
|
|
415
486
|
if (document.trust_dossier) {
|
|
416
487
|
const trustDossierFailure = validatePutTrustDossier(writeCwd, overviewPath, markdown);
|
|
417
488
|
if (trustDossierFailure) {
|
|
418
489
|
return err("wp_blueprint_put validation error", trustDossierFailure);
|
|
419
490
|
}
|
|
420
491
|
}
|
|
492
|
+
if (found && found.shape === "flat") {
|
|
493
|
+
await canonicalizeBlueprint(writeCwd, slug);
|
|
494
|
+
}
|
|
421
495
|
const blueprint = await persistBlueprintMarkdown({
|
|
422
496
|
projectCwd: writeCwd,
|
|
423
497
|
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);
|
|
@@ -10,8 +10,9 @@ import { hashMutationPayload, readMutationReplay, recordMutationReplay, validate
|
|
|
10
10
|
import { bytes } from "#mcp/blueprint/_shared/payload";
|
|
11
11
|
import { findBlueprintDir, resolveToolProject } from "#mcp/blueprint/_shared/project";
|
|
12
12
|
import { handleTaskNext } from "#mcp/blueprint/handlers/task-next";
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
13
|
+
import { readTaskVerification } from "#verification.js";
|
|
14
|
+
import { applyBlueprintLifecycle } from "#lifecycle/engine.js";
|
|
15
|
+
import { applyBlueprintLifecycleToFile } from "#lifecycle/local.js";
|
|
15
16
|
const ALL_STATES = ["draft", "planned", "in-progress", "parked", "archived", "completed"];
|
|
16
17
|
// Task 3.2 — wp_blueprint_task_verify
|
|
17
18
|
const taskVerifySchema = z.object({
|
|
@@ -52,13 +53,31 @@ export async function handleTaskVerify(projectResolver, cwd, raw) {
|
|
|
52
53
|
return err("wp_blueprint_task_verify failed", `Blueprint overview not found at ${filePath}`);
|
|
53
54
|
}
|
|
54
55
|
const markdownBefore = readFileSync(filePath, "utf8");
|
|
55
|
-
// Idempotency check: if the task already has the same canonical evidence block, skip write.
|
|
56
|
-
// We check this BEFORE calling applyVerification to avoid whitespace normalization drift
|
|
57
|
-
// making `result.markdown !== markdownBefore` always true even for identical evidence.
|
|
58
56
|
const incomingCanonical = canonicalizeEvidenceList(evidence);
|
|
59
57
|
const existingEvidence = readTaskVerification(markdownBefore, task_id);
|
|
58
|
+
let result;
|
|
59
|
+
try {
|
|
60
|
+
result = applyBlueprintLifecycle(markdownBefore, slug, {
|
|
61
|
+
type: "task_verify",
|
|
62
|
+
taskId: task_id,
|
|
63
|
+
evidence,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
return jsonContent({
|
|
68
|
+
summary: "Verification failed",
|
|
69
|
+
failures: [error instanceof Error ? error.message : String(error)],
|
|
70
|
+
next_action: makeNextAction("verify_task", error instanceof Error ? error.message : String(error)),
|
|
71
|
+
bytes: 0,
|
|
72
|
+
tokensSaved: 0,
|
|
73
|
+
}, true);
|
|
74
|
+
}
|
|
75
|
+
// Idempotency includes all lifecycle-owned fields. Reapplying identical
|
|
76
|
+
// evidence repairs stale progress/last_updated metadata once, then becomes a
|
|
77
|
+
// byte-for-byte no-op.
|
|
60
78
|
if (existingEvidence !== null &&
|
|
61
|
-
canonicalizeEvidenceList(existingEvidence) === incomingCanonical
|
|
79
|
+
canonicalizeEvidenceList(existingEvidence) === incomingCanonical &&
|
|
80
|
+
result.markdown === markdownBefore) {
|
|
62
81
|
const nextPayload = parseStructuredJson(await handleTaskNext(projectResolver, projectCwd, {
|
|
63
82
|
blueprint: slug,
|
|
64
83
|
project_id: resolvedProject.project_id ?? projectCwd,
|
|
@@ -82,19 +101,20 @@ export async function handleTaskVerify(projectResolver, cwd, raw) {
|
|
|
82
101
|
}
|
|
83
102
|
return finishPayload(payload);
|
|
84
103
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
// Write the mutated markdown back to disk via the file-aware lifecycle
|
|
105
|
+
// wrapper, not a raw write to the pre-mutation path: the pure engine call
|
|
106
|
+
// above can change the blueprint's frontmatter `status` (e.g. draft ->
|
|
107
|
+
// in-progress on the first task mutation), and that status change must be
|
|
108
|
+
// paired with the matching physical folder move -- the same contract the
|
|
109
|
+
// CLI router and BlueprintService already honor via
|
|
110
|
+
// applyBlueprintLifecycleToFile. Writing to the original `filePath`
|
|
111
|
+
// unconditionally would leave folder and status disagreeing whenever the
|
|
112
|
+
// blueprint started outside in-progress/.
|
|
113
|
+
await applyBlueprintLifecycleToFile(projectCwd, slug, {
|
|
114
|
+
type: "task_verify",
|
|
115
|
+
taskId: task_id,
|
|
116
|
+
evidence,
|
|
117
|
+
});
|
|
98
118
|
// Re-ingest so the DB projection reflects the completed status
|
|
99
119
|
try {
|
|
100
120
|
await reIngest(projectCwd);
|
|
@@ -107,16 +107,17 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
107
107
|
type: "object",
|
|
108
108
|
properties: { blueprint: { type: "string" }, project_id: { type: "string" } },
|
|
109
109
|
}, undefined, (r) => handleTaskNext(projectResolver, cwd, r), { title: "Blueprint Task Next", readOnlyHint: true, openWorldHint: false });
|
|
110
|
-
registrar.registerTool("wp_blueprint_task_advance", "Advance task status. Edits the canonical blueprint markdown and re-syncs DB. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_get/wp_blueprint_list to reject stale writes. Returns { summary, old_status, new_status, idempotent }.", {
|
|
110
|
+
registrar.registerTool("wp_blueprint_task_advance", "Advance task status within a specific blueprint. Requires slug to scope the mutation — task ids (e.g. 2.1) are not unique across blueprints in a project, so slug prevents advancing the wrong blueprint. Edits the canonical blueprint markdown and re-syncs DB. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_get/wp_blueprint_list to reject stale writes. Returns { summary, old_status, new_status, idempotent }.", {
|
|
111
111
|
type: "object",
|
|
112
112
|
properties: {
|
|
113
113
|
project_id: { type: "string" },
|
|
114
|
+
slug: { type: "string" },
|
|
114
115
|
task_id: { type: "string" },
|
|
115
116
|
to: { type: "string", enum: ["todo", "in-progress", "blocked", "done", "dropped"] },
|
|
116
117
|
request_id: { type: "string" },
|
|
117
118
|
head_at_ingest: { type: ["string", "null"] },
|
|
118
119
|
},
|
|
119
|
-
required: ["project_id", "task_id", "to"],
|
|
120
|
+
required: ["project_id", "slug", "task_id", "to"],
|
|
120
121
|
}, {
|
|
121
122
|
...summaryEnvelopeOutputSchema,
|
|
122
123
|
properties: {
|
package/dist/esm/mcp/server.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* — no edits required here.
|
|
8
8
|
*/
|
|
9
9
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
+
export declare function resolveMcpToolAllowlist(raw?: string | undefined): ReadonlySet<string> | null;
|
|
10
11
|
export declare function isBunSingleFileModuleUrl(moduleUrl: string): boolean;
|
|
11
12
|
export type ToolLoadMode = "filesystem" | "registry";
|
|
12
13
|
export declare function resolveDefaultToolLoadMode(moduleUrl?: string): ToolLoadMode;
|
package/dist/esm/mcp/server.js
CHANGED
|
@@ -16,6 +16,24 @@ import { COMPILED_TOOL_REGISTRY } from "./tools/_registry.js";
|
|
|
16
16
|
import { readOwnedPackageVersion } from "#runtime/package-version.js";
|
|
17
17
|
const SERVER_NAME = "webpresso";
|
|
18
18
|
const SERVER_VERSION = readOwnedPackageVersion(import.meta.url);
|
|
19
|
+
const MCP_TOOL_ALLOWLIST_ENV = "WP_MCP_TOOL_ALLOWLIST";
|
|
20
|
+
export function resolveMcpToolAllowlist(raw = process.env[MCP_TOOL_ALLOWLIST_ENV]) {
|
|
21
|
+
if (raw === undefined)
|
|
22
|
+
return null;
|
|
23
|
+
const names = raw
|
|
24
|
+
.split(",")
|
|
25
|
+
.map((name) => name.trim())
|
|
26
|
+
.filter(Boolean);
|
|
27
|
+
if (names.length === 0 || new Set(names).size !== names.length) {
|
|
28
|
+
throw new Error(`${MCP_TOOL_ALLOWLIST_ENV} must contain a non-empty, duplicate-free comma-separated tool list`);
|
|
29
|
+
}
|
|
30
|
+
for (const name of names) {
|
|
31
|
+
if (!/^wp_[a-z0-9_]+$/u.test(name)) {
|
|
32
|
+
throw new Error(`${MCP_TOOL_ALLOWLIST_ENV} contains invalid tool name: ${name}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return new Set(names);
|
|
36
|
+
}
|
|
19
37
|
function modulePathFromUrl(moduleUrl) {
|
|
20
38
|
try {
|
|
21
39
|
return fileURLToPath(moduleUrl);
|
|
@@ -53,8 +71,11 @@ export async function createServer(options = {}) {
|
|
|
53
71
|
},
|
|
54
72
|
});
|
|
55
73
|
const tools = new Map();
|
|
74
|
+
const allowlist = resolveMcpToolAllowlist();
|
|
56
75
|
const registrar = {
|
|
57
76
|
registerTool(name, description, inputSchema, outputSchema, handler, annotations) {
|
|
77
|
+
if (allowlist !== null && !allowlist.has(name))
|
|
78
|
+
return;
|
|
58
79
|
tools.set(name, { name, description, inputSchema, outputSchema, handler, annotations });
|
|
59
80
|
},
|
|
60
81
|
};
|
|
@@ -90,6 +111,12 @@ export async function createServer(options = {}) {
|
|
|
90
111
|
}
|
|
91
112
|
},
|
|
92
113
|
});
|
|
114
|
+
if (allowlist !== null) {
|
|
115
|
+
const missing = [...allowlist].filter((name) => !tools.has(name));
|
|
116
|
+
if (missing.length > 0) {
|
|
117
|
+
throw new Error(`${MCP_TOOL_ALLOWLIST_ENV} names unavailable tools: ${missing.sort().join(", ")}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
93
120
|
// Empty prompts/resources/resource-templates handlers, registered exactly
|
|
94
121
|
// the same way other MCP servers do. Several MCP clients —
|
|
95
122
|
// including Claude Code 2.1.x and OpenCode — call listPrompts() or
|