@webpresso/agent-kit 0.21.5 → 0.24.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.
Files changed (132) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +87 -124
  4. package/bin/_run.js +143 -1
  5. package/bin/runtime-manifest.json +40 -0
  6. package/catalog/AGENTS.md.tpl +7 -6
  7. package/catalog/agent/commands/plan-refine.md +3 -3
  8. package/catalog/agent/commands/pll.md +2 -0
  9. package/catalog/agent/guides/parallel-execution.md +2 -0
  10. package/catalog/agent/rules/extraction-parity.md +27 -1
  11. package/catalog/agent/rules/public-package-safety.md +24 -1
  12. package/catalog/agent/skills/pll/SKILL.md +1 -0
  13. package/catalog/base-kit/.github/workflows/ci.webpresso.yml.tmpl +33 -0
  14. package/catalog/base-kit/stryker.config.ts.tmpl +2 -2
  15. package/catalog/docs/templates/blueprint.md +1 -0
  16. package/catalog/docs/templates/blueprint.yaml +10 -12
  17. package/commands/blueprint.md +8 -43
  18. package/dist/esm/audit/blueprint-db-consistency.d.ts +1 -1
  19. package/dist/esm/audit/blueprint-db-consistency.js +6 -8
  20. package/dist/esm/audit/blueprint-lifecycle-sql.js +10 -3
  21. package/dist/esm/audit/cloudflare-deploy-contract.d.ts +3 -0
  22. package/dist/esm/audit/cloudflare-deploy-contract.js +80 -0
  23. package/dist/esm/audit/no-legacy-cli-bin.d.ts +3 -0
  24. package/dist/esm/audit/no-legacy-cli-bin.js +100 -0
  25. package/dist/esm/audit/package-surface.js +14 -1
  26. package/dist/esm/audit/repo-guardrails.js +40 -13
  27. package/dist/esm/audit/roadmap-links.js +23 -10
  28. package/dist/esm/blueprint/core/schema.d.ts +8 -8
  29. package/dist/esm/blueprint/core/schema.js +2 -2
  30. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  31. package/dist/esm/blueprint/db/ingester.js +18 -10
  32. package/dist/esm/blueprint/lifecycle/audit.js +9 -2
  33. package/dist/esm/blueprint/lifecycle/local.js +15 -4
  34. package/dist/esm/blueprint/service/BlueprintCreationService.js +11 -6
  35. package/dist/esm/blueprint/service/BlueprintService.js +37 -19
  36. package/dist/esm/blueprint/service/scanner.js +73 -9
  37. package/dist/esm/blueprint/tracked-document/schema.d.ts +2 -2
  38. package/dist/esm/blueprint/utils/document-paths.d.ts +23 -0
  39. package/dist/esm/blueprint/utils/document-paths.js +91 -0
  40. package/dist/esm/build/package-manifest.js +7 -0
  41. package/dist/esm/build/release-policy.d.ts +27 -0
  42. package/dist/esm/build/release-policy.js +29 -0
  43. package/dist/esm/build/runtime-targets.d.ts +13 -0
  44. package/dist/esm/build/runtime-targets.js +48 -0
  45. package/dist/esm/cli/auto-update/detect-pm.d.ts +15 -0
  46. package/dist/esm/cli/auto-update/detect-pm.js +24 -9
  47. package/dist/esm/cli/auto-update/skip.js +9 -1
  48. package/dist/esm/cli/bundle/agent-command-inventory.d.ts +120 -0
  49. package/dist/esm/cli/bundle/agent-command-inventory.js +100 -0
  50. package/dist/esm/cli/bundle/index.d.ts +17 -0
  51. package/dist/esm/cli/bundle/index.js +15 -0
  52. package/dist/esm/cli/cli.d.ts +1 -1
  53. package/dist/esm/cli/cli.js +49 -5
  54. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  55. package/dist/esm/cli/commands/audit.js +2 -0
  56. package/dist/esm/cli/commands/blueprint/router.js +11 -8
  57. package/dist/esm/cli/commands/hook.d.ts +8 -0
  58. package/dist/esm/cli/commands/hook.js +47 -0
  59. package/dist/esm/cli/commands/init/index.js +35 -1
  60. package/dist/esm/cli/commands/init/scaffold-base-kit.js +1 -1
  61. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/codex-ownership.js +9 -1
  62. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +130 -20
  63. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +65 -0
  64. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +64 -0
  65. package/dist/esm/cli/commands/package-manager.d.ts +15 -0
  66. package/dist/esm/cli/commands/package-manager.js +42 -0
  67. package/dist/esm/cli/commands/test.d.ts +1 -0
  68. package/dist/esm/cli/commands/test.js +2 -1
  69. package/dist/esm/cli/commands/typecheck.js +5 -20
  70. package/dist/esm/cli/package-scripts.d.ts +12 -0
  71. package/dist/esm/cli/package-scripts.js +59 -0
  72. package/dist/esm/cli/utils.js +3 -22
  73. package/dist/esm/cli/wp-extensions.d.ts +14 -0
  74. package/dist/esm/cli/wp-extensions.js +34 -0
  75. package/dist/esm/config/docs-lint/schemas/common.d.ts +1 -1
  76. package/dist/esm/config/docs-lint/schemas/implementation-plan.d.ts +2 -2
  77. package/dist/esm/config/docs-lint/schemas/parent-roadmap.d.ts +1 -1
  78. package/dist/esm/config/stryker/index.d.ts +85 -0
  79. package/dist/esm/config/stryker/index.js +31 -0
  80. package/dist/esm/e2e/command-builder.js +11 -2
  81. package/dist/esm/e2e/config.d.ts +65 -0
  82. package/dist/esm/e2e/config.js +126 -0
  83. package/dist/esm/e2e/execution.js +4 -0
  84. package/dist/esm/e2e/load-host-adapter.d.ts +6 -1
  85. package/dist/esm/e2e/load-host-adapter.js +27 -9
  86. package/dist/esm/e2e/run-planner.js +1 -0
  87. package/dist/esm/e2e/types.d.ts +2 -0
  88. package/dist/esm/format/index.js +1 -3
  89. package/dist/esm/hooks/guard-switch/index.d.ts +1 -1
  90. package/dist/esm/hooks/guard-switch/index.js +22 -14
  91. package/dist/esm/hooks/post-tool/lint-after-edit.d.ts +1 -0
  92. package/dist/esm/hooks/post-tool/lint-after-edit.js +5 -2
  93. package/dist/esm/hooks/pretool-guard/validators/file-conventions.js +1 -1
  94. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.d.ts +6 -0
  95. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +27 -2
  96. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +2 -1
  97. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +59 -34
  98. package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -3
  99. package/dist/esm/hooks/shared/routing-block.js +18 -4
  100. package/dist/esm/hooks/shared/validators/blueprint.js +3 -0
  101. package/dist/esm/hooks/stop/qa-changed-files.d.ts +1 -0
  102. package/dist/esm/hooks/stop/qa-changed-files.js +5 -2
  103. package/dist/esm/lint/index.js +1 -1
  104. package/dist/esm/mcp/auto-discover.d.ts +2 -0
  105. package/dist/esm/mcp/auto-discover.js +14 -6
  106. package/dist/esm/mcp/blueprint-server.js +30 -26
  107. package/dist/esm/mcp/cli.js +21 -0
  108. package/dist/esm/mcp/runners/test.js +15 -0
  109. package/dist/esm/mcp/server.d.ts +7 -0
  110. package/dist/esm/mcp/server.js +16 -27
  111. package/dist/esm/mcp/tools/_registry.d.ts +3 -0
  112. package/dist/esm/mcp/tools/_registry.js +21 -0
  113. package/dist/esm/mcp/tools/audit.d.ts +1 -0
  114. package/dist/esm/mcp/tools/audit.js +11 -0
  115. package/dist/esm/mcp/tools/e2e.d.ts +1 -1
  116. package/dist/esm/mcp/tools/typecheck.js +4 -2
  117. package/dist/esm/mutation/affected.d.ts +9 -0
  118. package/dist/esm/mutation/affected.js +36 -0
  119. package/dist/esm/package.json +5 -0
  120. package/dist/esm/runtime/package-version.d.ts +2 -0
  121. package/dist/esm/runtime/package-version.js +43 -0
  122. package/dist/esm/test/command-builder.d.ts +3 -0
  123. package/dist/esm/test/command-builder.js +22 -3
  124. package/dist/esm/tool-runtime/index.d.ts +2 -2
  125. package/dist/esm/tool-runtime/index.js +2 -1
  126. package/dist/esm/tool-runtime/resolve-runner.d.ts +3 -0
  127. package/dist/esm/tool-runtime/resolve-runner.js +7 -5
  128. package/dist/esm/typecheck/index.js +4 -2
  129. package/dist/esm/wp-extension/index.d.ts +50 -0
  130. package/dist/esm/wp-extension/index.js +268 -0
  131. package/package.json +67 -31
  132. package/skills/pll/SKILL.md +1 -0
@@ -8,6 +8,7 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
8
8
  <description>
9
9
  Use the wp_* MCP tools for all test, lint, typecheck, qa, audit, local CI act,
10
10
  and Cloudflare Worker tail operations.
11
+ If a wp_* MCP tool is stale or unavailable, use the matching wp CLI command.
11
12
  If context-mode plugin routing is present, let it own ctx_* data-processing nudges.
12
13
  These tools return structured, summary-first results and keep output concise.
13
14
  </description>
@@ -55,7 +56,7 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
55
56
  <tool name="wp_test">
56
57
  <category>dev-workflow</category>
57
58
  <trigger>running tests, verifying test suite, check if tests pass</trigger>
58
- <forbidden>just test, pnpm test, vitest</forbidden>
59
+ <forbidden>just test, pnpm test, vitest, npx vitest, npm exec -- vitest, yarn vitest, bunx vitest, node ./node_modules/vitest/vitest.mjs</forbidden>
59
60
  </tool>
60
61
  <tool name="wp_e2e">
61
62
  <category>dev-workflow</category>
@@ -65,12 +66,12 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
65
66
  <tool name="wp_lint">
66
67
  <category>dev-workflow</category>
67
68
  <trigger>linting, code style checks, lint errors</trigger>
68
- <forbidden>just lint, oxlint</forbidden>
69
+ <forbidden>just lint, oxlint, node ./node_modules/oxlint/bin/oxlint</forbidden>
69
70
  </tool>
70
71
  <tool name="wp_typecheck">
71
72
  <category>dev-workflow</category>
72
73
  <trigger>type checking, TypeScript errors, type errors</trigger>
73
- <forbidden>tsc</forbidden>
74
+ <forbidden>tsc, node ./node_modules/typescript/bin/tsc</forbidden>
74
75
  </tool>
75
76
  <tool name="wp_qa">
76
77
  <category>dev-workflow</category>
@@ -102,6 +103,11 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
102
103
  <rule>Context-mode owns ctx_* routing when that plugin is installed.</rule>
103
104
  </ownership_boundary>
104
105
 
106
+ <hook_diagnostics>
107
+ <rule>Prefer wp hook &lt;name&gt; over direct wp-&lt;hook-bin&gt; calls when a wp hook command exists.</rule>
108
+ <rule>Direct wp-* hook bins remain generated-hook runtime internals, not recommended agent diagnostics.</rule>
109
+ </hook_diagnostics>
110
+
105
111
  <package_guidance>
106
112
  <rule>Consumers add @webpresso/agent-kit and import config helpers through @webpresso/agent-kit/* subpath exports such as @webpresso/agent-kit/oxlint, @webpresso/agent-kit/vitest/node, @webpresso/agent-kit/test-preset, @webpresso/agent-kit/e2e-preset, @webpresso/agent-kit/tsconfig/base.json, @webpresso/agent-kit/docs-lint, @webpresso/agent-kit/stryker, @webpresso/agent-kit/launch, and @webpresso/agent-kit/workers-test.</rule>
107
113
  <rule>Do not recommend adding retired split agent config packages for consumer projects; keep wp_* MCP tool names and wp-* hook bin names unchanged.</rule>
@@ -114,9 +120,16 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
114
120
  <command>just qa</command>
115
121
  <command>just lint-md</command>
116
122
  <command>vitest</command>
123
+ <command>npx vitest</command>
124
+ <command>npm exec -- vitest</command>
125
+ <command>yarn vitest</command>
126
+ <command>bunx vitest</command>
127
+ <command>node ./node_modules/vitest/vitest.mjs</command>
117
128
  <command>oxlint</command>
129
+ <command>node ./node_modules/oxlint/bin/oxlint</command>
118
130
  <command>markdownlint-cli2</command>
119
131
  <command>tsc</command>
132
+ <command>node ./node_modules/typescript/bin/tsc</command>
120
133
  <command>act</command>
121
134
  <command>vp exec act</command>
122
135
  <command>pnpm exec act</command>
@@ -133,7 +146,8 @@ export const WP_ROUTING_BLOCK = `<wp_routing>
133
146
  </output_format>
134
147
 
135
148
  <fallback>
136
- When MCP tools are unavailable, use just recipes directly and keep output brief.
149
+ When MCP tools are unavailable or stale, use the matching wp CLI command and keep output brief.
150
+ Do not fall through to raw tool bins under node_modules when a wp wrapper exists.
137
151
  .omx is runtime/state only; it is not a direct hook surface.
138
152
  </fallback>
139
153
  </wp_routing>`;
@@ -26,6 +26,9 @@ export function shouldSkipFile(filePath) {
26
26
  if (!filePath)
27
27
  return false;
28
28
  const normalized = filePath.startsWith('/') ? filePath.slice(1) : filePath;
29
+ if (normalized.startsWith('blueprints/') || normalized.startsWith('webpresso/blueprints/')) {
30
+ return false;
31
+ }
29
32
  return SKIP_PATTERNS.some((pattern) => pattern.test(normalized));
30
33
  }
31
34
  export function getSkipReason(filePath) {
@@ -11,4 +11,5 @@ export type StopHookResult = {
11
11
  systemMessage: string;
12
12
  };
13
13
  export declare function formatStopHookOutput(result: StopHookResult): string;
14
+ export declare function main(): Promise<void>;
14
15
  //# sourceMappingURL=qa-changed-files.d.ts.map
@@ -85,12 +85,15 @@ export function runQaChecks(qaFiles, projectDir) {
85
85
  export function formatStopHookOutput(result) {
86
86
  return JSON.stringify(result);
87
87
  }
88
- if (process.argv[1] &&
89
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
88
+ export async function main() {
90
89
  runHook(
91
90
  // `Stop` is latency-sensitive and user-visible. Until webpresso grows a
92
91
  // deferred execution plane, broad typecheck/test sweeps stay off the hot
93
92
  // path instead of shelling synchronously at turn end.
94
93
  (_input) => null, formatStopHookOutput);
95
94
  }
95
+ if (process.argv[1] &&
96
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
97
+ void main();
98
+ }
96
99
  //# sourceMappingURL=qa-changed-files.js.map
@@ -93,7 +93,7 @@ export async function runLint(options = {}) {
93
93
  else {
94
94
  lintArgs.push('.');
95
95
  }
96
- const resolution = getManagedRunner('vp', { filterOutput: false });
96
+ const resolution = getManagedRunner('vp', { outputPolicy: 'structured' });
97
97
  const vpOutcome = await runCommand(resolution.command, [...resolution.args, ...lintArgs], runOptions);
98
98
  if (isRunFailure(vpOutcome)) {
99
99
  return {
@@ -61,5 +61,7 @@ export interface ToolDescriptor {
61
61
  export interface ToolRegistrar {
62
62
  registerTool(name: string, description: string, jsonSchema: Record<string, unknown>, outputSchema: Record<string, unknown> | undefined, handler: ToolHandler, annotations?: ToolAnnotations): void;
63
63
  }
64
+ export declare function registerToolDescriptor(server: ToolRegistrar, descriptor: ToolDescriptor): ToolDescriptor;
65
+ export declare function registerToolDescriptors(server: ToolRegistrar, descriptors: readonly ToolDescriptor[]): ToolDescriptor[];
64
66
  export declare function discoverTools(server: ToolRegistrar, toolsDir: string): Promise<ToolDescriptor[]>;
65
67
  //# sourceMappingURL=auto-discover.d.ts.map
@@ -15,9 +15,20 @@ import { extname, join } from 'node:path';
15
15
  import { pathToFileURL } from 'node:url';
16
16
  import { z } from 'zod';
17
17
  import { zodToJsonSchema } from 'zod-to-json-schema';
18
+ export function registerToolDescriptor(server, descriptor) {
19
+ const jsonSchema = toJsonSchema(descriptor.inputSchema);
20
+ const outputSchema = descriptor.outputSchema ? toJsonSchema(descriptor.outputSchema) : undefined;
21
+ server.registerTool(descriptor.name, descriptor.description, jsonSchema, outputSchema, descriptor.handler, descriptor.annotations);
22
+ return descriptor;
23
+ }
24
+ export function registerToolDescriptors(server, descriptors) {
25
+ return descriptors.map((descriptor) => registerToolDescriptor(server, descriptor));
26
+ }
18
27
  const SKIP_SUFFIXES = ['.test.ts', '.test.js', '.integration.test.ts', '.integration.test.js'];
19
28
  const SUPPORTED_EXTENSIONS = new Set(['.ts', '.js', '.mjs', '.cjs']);
20
29
  function shouldSkip(file) {
30
+ if (file.startsWith('_'))
31
+ return true;
21
32
  if (file.endsWith('.d.ts') || file.endsWith('.d.ts.map'))
22
33
  return true;
23
34
  if (file.endsWith('.js.map') || file.endsWith('.ts.map'))
@@ -82,7 +93,7 @@ function toJsonSchema(schema) {
82
93
  }
83
94
  export async function discoverTools(server, toolsDir) {
84
95
  const entries = await readdir(toolsDir, { withFileTypes: true });
85
- const registered = [];
96
+ const loaded = [];
86
97
  for (const entry of entries) {
87
98
  if (!entry.isFile())
88
99
  continue;
@@ -98,11 +109,8 @@ export async function discoverTools(server, toolsDir) {
98
109
  if (typeof descriptor.name !== 'string' || typeof descriptor.handler !== 'function') {
99
110
  throw new Error(`Tool file ${fullPath} default export is malformed (missing name or handler)`);
100
111
  }
101
- const jsonSchema = toJsonSchema(descriptor.inputSchema);
102
- const outputSchema = descriptor.outputSchema ? toJsonSchema(descriptor.outputSchema) : undefined;
103
- server.registerTool(descriptor.name, descriptor.description, jsonSchema, outputSchema, descriptor.handler, descriptor.annotations);
104
- registered.push(descriptor);
112
+ loaded.push(descriptor);
105
113
  }
106
- return registered;
114
+ return registerToolDescriptors(server, loaded);
107
115
  }
108
116
  //# sourceMappingURL=auto-discover.js.map
@@ -23,6 +23,7 @@ import { openDb } from '#db/connection.js';
23
23
  import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
24
24
  import { findTemplate } from '#db/templates.js';
25
25
  import { resolveBlueprintRoot } from '#utils/blueprint-root.js';
26
+ import { getBlueprintDocumentPaths } from '#utils/document-paths.js';
26
27
  import { evidenceListSchema, canonicalizeEvidenceList } from '#evidence.js';
27
28
  import { checkFreshness, readCurrentHead, readProjectionMetadata } from '#freshness.js';
28
29
  import { applyVerification, assertAllTasksHaveCanonicalPassingEvidence, readTaskVerification, } from '#verification.js';
@@ -251,10 +252,10 @@ async function reIngest(cwd) {
251
252
  await reIngestProjection(cwd);
252
253
  }
253
254
  async function persistBlueprintMarkdown(input) {
254
- const { projectCwd, slug, overviewPath, markdown } = input;
255
- mkdirSync(path.dirname(overviewPath), { recursive: true });
255
+ const { projectCwd, slug, blueprintPath, markdown } = input;
256
+ mkdirSync(path.dirname(blueprintPath), { recursive: true });
256
257
  parseBlueprint(markdown, slug);
257
- writeFileSync(overviewPath, markdown, 'utf8');
258
+ writeFileSync(blueprintPath, markdown, 'utf8');
258
259
  await reIngest(projectCwd);
259
260
  const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
260
261
  if (!refreshed.blueprint) {
@@ -264,9 +265,11 @@ async function persistBlueprintMarkdown(input) {
264
265
  }
265
266
  function findBlueprintDir(blueprintRoot, slug, states) {
266
267
  for (const state of states) {
267
- const d = path.join(blueprintRoot, state, slug);
268
- if (existsSync(d))
269
- return { dir: d, state };
268
+ const paths = getBlueprintDocumentPaths(blueprintRoot, state, slug);
269
+ if (existsSync(paths.flat))
270
+ return { dir: path.dirname(paths.flat), path: paths.flat, shape: 'flat', state };
271
+ if (existsSync(paths.directory))
272
+ return { dir: paths.directory, path: paths.folder, shape: 'folder', state };
270
273
  }
271
274
  return null;
272
275
  }
@@ -557,7 +560,7 @@ async function handleNew(cwd, raw) {
557
560
  }
558
561
  const b = bytes(template);
559
562
  const slug = titleToSlug(title);
560
- const targetPath = path.join(resolveBlueprintRoot(cwd), 'draft', slug, '_overview.md');
563
+ const targetPath = getBlueprintDocumentPaths(resolveBlueprintRoot(cwd), 'draft', slug).flat;
561
564
  // Platform-first path: push event to register the blueprint before returning the scaffold.
562
565
  // Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
563
566
  const adapter = await resolveSyncAdapter(cwd);
@@ -886,7 +889,7 @@ async function handleTaskVerify(projectResolver, cwd, raw) {
886
889
  if (!found) {
887
890
  return err('wp_blueprint_task_verify failed', `Blueprint "${slug}" not found in any state directory`);
888
891
  }
889
- const filePath = path.join(found.dir, '_overview.md');
892
+ const filePath = found.path;
890
893
  if (!existsSync(filePath)) {
891
894
  return err('wp_blueprint_task_verify failed', `Blueprint overview not found at ${filePath}`);
892
895
  }
@@ -981,8 +984,8 @@ async function handlePromote(projectResolver, cwd, raw) {
981
984
  const found = findBlueprintDir(root, slug, ALL_STATES);
982
985
  if (!found)
983
986
  return err('wp_blueprint_promote failed', `Blueprint "${slug}" not found in any state directory`);
984
- const { dir: currentDir, state: currentState } = found;
985
- const overviewPath = path.join(currentDir, '_overview.md');
987
+ const { state: currentState } = found;
988
+ const overviewPath = found.path;
986
989
  if (to_state === 'completed') {
987
990
  try {
988
991
  assertBlueprintCanComplete(overviewPath, slug);
@@ -1085,7 +1088,7 @@ async function handleFinalize(projectResolver, cwd, raw) {
1085
1088
  return err('wp_blueprint_finalize failed', `Blueprint "${slug}" not found`);
1086
1089
  }
1087
1090
  try {
1088
- assertBlueprintCanComplete(path.join(found.dir, '_overview.md'), slug);
1091
+ assertBlueprintCanComplete(found.path, slug);
1089
1092
  }
1090
1093
  catch (error) {
1091
1094
  return err('wp_blueprint_finalize refused', toStr(error));
@@ -1807,14 +1810,14 @@ async function handleBlueprintPut(projectResolver, cwd, raw) {
1807
1810
  return err('wp_blueprint_put refused', `New blueprint "${slug}" must start in "draft"; use wp_blueprint_transition for later lifecycle moves.`);
1808
1811
  }
1809
1812
  const overviewPath = found
1810
- ? path.join(found.dir, '_overview.md')
1811
- : path.join(root, document.status, slug, '_overview.md');
1813
+ ? found.path
1814
+ : getBlueprintDocumentPaths(root, document.status, slug).flat;
1812
1815
  try {
1813
1816
  const markdown = renderBlueprintMarkdownFromDocument(slug, document);
1814
1817
  const blueprint = await persistBlueprintMarkdown({
1815
1818
  projectCwd,
1816
1819
  slug,
1817
- overviewPath,
1820
+ blueprintPath: overviewPath,
1818
1821
  markdown,
1819
1822
  });
1820
1823
  const payload = {
@@ -1904,7 +1907,7 @@ async function handleBlueprintTransition(projectResolver, cwd, raw) {
1904
1907
  async function applyLocalBlueprintTransition(input) {
1905
1908
  const { projectCwd, slug, to_state, found } = input;
1906
1909
  const root = resolveBlueprintRoot(projectCwd);
1907
- const overviewPath = path.join(found.dir, '_overview.md');
1910
+ const overviewPath = found.path;
1908
1911
  const parsed = runValidate(overviewPath);
1909
1912
  if (!parsed.valid) {
1910
1913
  throw new Error(parsed.gaps.join('; '));
@@ -1918,13 +1921,15 @@ async function applyLocalBlueprintTransition(input) {
1918
1921
  progress: formatBlueprintProgress(currentBlueprint.tasks.length, currentBlueprint.tasks.filter((task) => task.status === 'done').length, currentBlueprint.tasks.filter((task) => task.status === 'blocked').length),
1919
1922
  });
1920
1923
  parseBlueprint(updated, slug);
1921
- const destDir = path.join(root, to_state, slug);
1922
- mkdirSync(path.dirname(destDir), { recursive: true });
1924
+ const destination = getBlueprintDocumentPaths(root, to_state, slug);
1925
+ mkdirSync(path.dirname(found.shape === 'flat' ? destination.flat : destination.directory), {
1926
+ recursive: true,
1927
+ });
1923
1928
  let finalOverviewPath = overviewPath;
1924
1929
  if (found.state !== to_state) {
1925
1930
  const { renameSync } = await import('node:fs');
1926
- renameSync(found.dir, destDir);
1927
- finalOverviewPath = path.join(destDir, '_overview.md');
1931
+ renameSync(found.shape === 'flat' ? overviewPath : found.dir, found.shape === 'flat' ? destination.flat : destination.directory);
1932
+ finalOverviewPath = found.shape === 'flat' ? destination.flat : destination.folder;
1928
1933
  }
1929
1934
  writeFileSync(finalOverviewPath, updated, 'utf8');
1930
1935
  await reIngest(projectCwd);
@@ -1960,10 +1965,9 @@ async function handleBlueprintCreate(projectResolver, cwd, raw) {
1960
1965
  const today = new Date().toISOString().split('T')[0] ?? '';
1961
1966
  const slug = titleToSlug(title);
1962
1967
  const root = resolveBlueprintRoot(projectCwd);
1963
- const targetDir = path.join(root, 'draft', slug);
1964
- const overviewPath = path.join(targetDir, '_overview.md');
1968
+ const overviewPath = getBlueprintDocumentPaths(root, 'draft', slug).flat;
1965
1969
  try {
1966
- mkdirSync(targetDir, { recursive: true });
1970
+ mkdirSync(path.dirname(overviewPath), { recursive: true });
1967
1971
  const content = BLUEPRINT_TEMPLATE.replace(/{TITLE}/g, title)
1968
1972
  .replace(/{COMPLEXITY}/g, complexity)
1969
1973
  .replace(/{DATE}/g, today)
@@ -1971,7 +1975,7 @@ async function handleBlueprintCreate(projectResolver, cwd, raw) {
1971
1975
  await persistBlueprintMarkdown({
1972
1976
  projectCwd,
1973
1977
  slug,
1974
- overviewPath,
1978
+ blueprintPath: overviewPath,
1975
1979
  markdown: content,
1976
1980
  });
1977
1981
  const b = bytes(content);
@@ -2083,12 +2087,12 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
2083
2087
  next_action: nextActionOutputSchema,
2084
2088
  },
2085
2089
  }, (r) => handleBlueprintTransition(projectResolver, cwd, r), { title: 'Blueprint Transition', destructiveHint: false, openWorldHint: false });
2086
- registrar.registerTool('wp_blueprint_validate', 'Validate _overview.md structure. Returns { valid, gaps }. Must pass before wp_blueprint_promote.', { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, undefined, (r) => handleValidate(cwd, r), { title: 'Blueprint Validate', readOnlyHint: false, openWorldHint: false });
2090
+ registrar.registerTool('wp_blueprint_validate', 'Validate canonical blueprint markdown structure (`<slug>.md` or `<slug>/_overview.md`). Returns { valid, gaps }. Must pass before wp_blueprint_promote.', { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, undefined, (r) => handleValidate(cwd, r), { title: 'Blueprint Validate', readOnlyHint: false, openWorldHint: false });
2087
2091
  registrar.registerTool('wp_blueprint_task_next', 'Return the next ready task (all deps done). Accepts optional project_id for nested-workspace disambiguation. Returns { summary, task }.', {
2088
2092
  type: 'object',
2089
2093
  properties: { blueprint: { type: 'string' }, project_id: { type: 'string' } },
2090
2094
  }, undefined, (r) => handleTaskNext(projectResolver, cwd, r), { title: 'Blueprint Task Next', readOnlyHint: true, openWorldHint: false });
2091
- registrar.registerTool('wp_blueprint_task_advance', 'Advance task status. Edits _overview.md 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 }.', {
2095
+ 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 }.', {
2092
2096
  type: 'object',
2093
2097
  properties: {
2094
2098
  project_id: { type: 'string' },
@@ -2206,7 +2210,7 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
2206
2210
  },
2207
2211
  required: [...summaryEnvelopeOutputSchema.required, 'chunks', 'total_bytes', 'project_id'],
2208
2212
  }, (r) => handleBlueprintContext(projectResolver, cwd, r), { title: 'Blueprint Context', readOnlyHint: true, openWorldHint: false });
2209
- registrar.registerTool('wp_blueprint_create', 'Create a new blueprint markdown under blueprints/draft/<slug>/_overview.md and re-ingest. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_projects/wp_blueprint_list to reject stale writes. Returns { slug, path, next_action, idempotent }.', {
2213
+ registrar.registerTool('wp_blueprint_create', 'Create a new blueprint markdown under blueprints/draft/<slug>.md by default (folder-shaped `<slug>/_overview.md` remains supported elsewhere) and re-ingest. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_projects/wp_blueprint_list to reject stale writes. Returns { slug, path, next_action, idempotent }.', {
2210
2214
  type: 'object',
2211
2215
  properties: {
2212
2216
  project_id: { type: 'string' },
@@ -12,18 +12,26 @@ import { createServer } from './server.js';
12
12
  export async function runStdioServer() {
13
13
  const server = await createServer();
14
14
  const transport = new StdioServerTransport();
15
+ const settle = Promise.withResolvers();
15
16
  let shuttingDown = false;
16
17
  const shutdown = async () => {
17
18
  if (shuttingDown)
18
19
  return;
19
20
  shuttingDown = true;
20
21
  deleteSentinel();
22
+ try {
23
+ await transport.close();
24
+ }
25
+ catch {
26
+ /* ignore transport close errors during shutdown */
27
+ }
21
28
  try {
22
29
  await server.close();
23
30
  }
24
31
  catch {
25
32
  /* ignore close errors during shutdown */
26
33
  }
34
+ settle.resolve();
27
35
  };
28
36
  process.on('SIGINT', () => {
29
37
  void shutdown().then(() => process.exit(0));
@@ -31,8 +39,21 @@ export async function runStdioServer() {
31
39
  process.on('SIGTERM', () => {
32
40
  void shutdown().then(() => process.exit(0));
33
41
  });
42
+ process.stdin.on('end', () => {
43
+ void shutdown();
44
+ });
45
+ process.stdin.on('close', () => {
46
+ void shutdown();
47
+ });
48
+ transport.onclose = () => {
49
+ void shutdown();
50
+ };
51
+ transport.onerror = () => {
52
+ void shutdown();
53
+ };
34
54
  await server.connect(transport);
35
55
  writeSentinel();
56
+ await settle.promise;
36
57
  }
37
58
  import { realpathSync } from 'node:fs';
38
59
  import { fileURLToPath } from 'node:url';
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, statSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { globSync } from 'glob';
4
+ import { getPackageScript, isRecursiveWpScript, packageUsesVitest } from '#cli/package-scripts.js';
4
5
  import { isRunFailure, runCommand as runSharedCommand } from '#mcp/tools/_shared/run-command';
5
6
  // Keep the runner's own deadline comfortably below common MCP client call
6
7
  // ceilings so slow suites fail fast with a structured `timedOut` payload
@@ -55,6 +56,14 @@ export async function runTests(input) {
55
56
  if (workspaceShardRuns && workspaceShardRuns.length > 0) {
56
57
  return runScopedSequence(cwd, workspaceShardRuns, input, workspaceSharding);
57
58
  }
59
+ if (shouldBypassWorkspaceTestScript(cwd)) {
60
+ const result = await runCommand('vp', ['exec', '--', 'vitest', 'run', '--reporter=json', '--no-color'], {
61
+ ...input,
62
+ cwd,
63
+ timeoutMs: commandTimeoutMs,
64
+ });
65
+ return withFailureScope(result, 'workspace vitest command');
66
+ }
58
67
  const result = await runCommand('vp', ['run', 'test'], {
59
68
  ...input,
60
69
  cwd,
@@ -222,6 +231,12 @@ function hasRootVitestTestScript(cwd) {
222
231
  const testScript = scripts.test;
223
232
  return typeof testScript === 'string' && /\bvitest\b/.test(testScript);
224
233
  }
234
+ function shouldBypassWorkspaceTestScript(cwd) {
235
+ const testScript = getPackageScript(cwd, 'test');
236
+ if (!testScript || !isRecursiveWpScript(testScript, 'test'))
237
+ return false;
238
+ return packageUsesVitest(cwd);
239
+ }
225
240
  function discoverVitestFiles(cwd) {
226
241
  return globSync(VITEST_DEFAULT_INCLUDE, {
227
242
  cwd,
@@ -7,6 +7,7 @@
7
7
  * — no edits required here.
8
8
  */
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ export type ToolLoadMode = 'filesystem' | 'registry';
10
11
  export interface CreateServerOptions {
11
12
  /**
12
13
  * Directory to scan for tool descriptors. Defaults to `./tools` relative to
@@ -14,6 +15,12 @@ export interface CreateServerOptions {
14
15
  * `vp run build`.
15
16
  */
16
17
  toolsDir?: string;
18
+ /**
19
+ * Tool loading strategy. Use `registry` for compiled runtime execution where
20
+ * runtime directory scans are unsafe, and `filesystem` for dev/test disk
21
+ * discovery.
22
+ */
23
+ toolLoadMode?: ToolLoadMode;
17
24
  /**
18
25
  * Repo working directory passed through to the blueprint structured-store
19
26
  * registrar (Task 2.1). Defaults to `process.cwd()`. Tests inject a tmpdir.
@@ -8,42 +8,25 @@
8
8
  */
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
10
  import { CallToolRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, RootsListChangedNotificationSchema, } from '@modelcontextprotocol/sdk/types.js';
11
- import { existsSync, readFileSync } from 'node:fs';
12
11
  import { dirname, join } from 'node:path';
13
12
  import { fileURLToPath } from 'node:url';
14
- import { discoverTools, } from './auto-discover.js';
13
+ import { discoverTools, registerToolDescriptors, } from './auto-discover.js';
15
14
  import { registerBlueprintServer } from './blueprint-server.js';
15
+ import { COMPILED_TOOL_REGISTRY } from './tools/_registry.js';
16
+ import { readOwnedPackageVersion } from '#runtime/package-version.js';
16
17
  const SERVER_NAME = 'webpresso';
17
- // Walk upward from this module's location until we find package.json. Works
18
- // for both src/mcp/server.ts (dev) and dist/esm/mcp/server.js (built) without
19
- // hardcoding asymmetric `../../..` counts.
20
- const MAX_UPWARD_LEVELS = 8;
21
- function readPackageVersion() {
22
- let dir = dirname(fileURLToPath(import.meta.url));
23
- for (let i = 0; i < MAX_UPWARD_LEVELS; i++) {
24
- const candidate = join(dir, 'package.json');
25
- if (existsSync(candidate)) {
26
- const pkg = JSON.parse(readFileSync(candidate, 'utf-8'));
27
- // Some build outputs emit a marker `package.json` (e.g. `{ "type": "module" }`)
28
- // without a version. Skip those and keep walking so we land on the real
29
- // package root, instead of throwing the moment we see any package.json.
30
- if (typeof pkg.version === 'string' && pkg.version.length > 0)
31
- return pkg.version;
32
- }
33
- const parent = dirname(dir);
34
- if (parent === dir)
35
- break;
36
- dir = parent;
37
- }
38
- throw new Error('Cannot locate a versioned package.json relative to webpresso MCP server module');
39
- }
40
- const SERVER_VERSION = readPackageVersion();
18
+ const SERVER_VERSION = readOwnedPackageVersion(import.meta.url);
41
19
  function defaultToolsDir() {
42
20
  // import.meta.url resolves to either src/mcp/server.ts (dev/test via vitest)
43
21
  // or dist/esm/mcp/server.js (built). The tools directory is colocated.
44
22
  const here = dirname(fileURLToPath(import.meta.url));
45
23
  return join(here, 'tools');
46
24
  }
25
+ function resolveDefaultToolLoadMode() {
26
+ return process.env.WP_MCP_TOOL_MODE === 'registry' || process.env.WP_COMPILED_RUNTIME === '1'
27
+ ? 'registry'
28
+ : 'filesystem';
29
+ }
47
30
  export async function createServer(options = {}) {
48
31
  const server = new Server({ name: SERVER_NAME, version: SERVER_VERSION }, {
49
32
  capabilities: {
@@ -60,7 +43,13 @@ export async function createServer(options = {}) {
60
43
  tools.set(name, { name, description, inputSchema, outputSchema, handler, annotations });
61
44
  },
62
45
  };
63
- await discoverTools(registrar, options.toolsDir ?? defaultToolsDir());
46
+ const toolLoadMode = options.toolLoadMode ?? resolveDefaultToolLoadMode();
47
+ if (toolLoadMode === 'registry') {
48
+ registerToolDescriptors(registrar, COMPILED_TOOL_REGISTRY);
49
+ }
50
+ else {
51
+ await discoverTools(registrar, options.toolsDir ?? defaultToolsDir());
52
+ }
64
53
  // Task 2.1: register the blueprint structured-store tools AFTER auto-discover
65
54
  // so any tool-name collision surfaces here as a thrown error rather than
66
55
  // silent shadowing. Roots are looked up lazily via `server.listRoots()`; the
@@ -0,0 +1,3 @@
1
+ import type { ToolDescriptor } from '#mcp/auto-discover';
2
+ export declare const COMPILED_TOOL_REGISTRY: readonly ToolDescriptor[];
3
+ //# sourceMappingURL=_registry.d.ts.map
@@ -0,0 +1,21 @@
1
+ import audit from './audit.js';
2
+ import ciAct from './ci-act.js';
3
+ import e2e from './e2e.js';
4
+ import format from './format.js';
5
+ import lint from './lint.js';
6
+ import qa from './qa.js';
7
+ import test from './test.js';
8
+ import typecheck from './typecheck.js';
9
+ import workerTail from './worker-tail.js';
10
+ export const COMPILED_TOOL_REGISTRY = [
11
+ audit,
12
+ ciAct,
13
+ e2e,
14
+ format,
15
+ lint,
16
+ qa,
17
+ test,
18
+ typecheck,
19
+ workerTail,
20
+ ];
21
+ //# sourceMappingURL=_registry.js.map
@@ -32,6 +32,7 @@ declare const inputSchema: z.ZodObject<{
32
32
  "catalog-drift": "catalog-drift";
33
33
  "package-surface": "package-surface";
34
34
  "architecture-drift": "architecture-drift";
35
+ "cloudflare-deploy-contract": "cloudflare-deploy-contract";
35
36
  "absolute-path-policy": "absolute-path-policy";
36
37
  "ai-contracts": "ai-contracts";
37
38
  }>;
@@ -28,6 +28,7 @@ const KINDS = [
28
28
  'docs-frontmatter',
29
29
  'blueprint-lifecycle',
30
30
  'architecture-drift',
31
+ 'cloudflare-deploy-contract',
31
32
  'absolute-path-policy',
32
33
  'roadmap-links',
33
34
  'bundle-budget',
@@ -155,6 +156,16 @@ async function dispatch(input) {
155
156
  details: auditResult,
156
157
  };
157
158
  }
159
+ case 'cloudflare-deploy-contract': {
160
+ const { auditCloudflareDeployContract } = await import('#audit/cloudflare-deploy-contract');
161
+ const auditResult = await auditCloudflareDeployContract(input.cwd ?? input.directory ?? process.cwd());
162
+ return {
163
+ passed: auditResult.ok,
164
+ summary: summarizeRepoAudit(kind, auditResult),
165
+ kind,
166
+ details: auditResult,
167
+ };
168
+ }
158
169
  case 'absolute-path-policy': {
159
170
  const { auditAbsolutePathPolicy } = await import('#audit/absolute-path-policy');
160
171
  const auditResult = auditAbsolutePathPolicy(input.cwd ?? input.directory ?? process.cwd());
@@ -12,8 +12,8 @@ declare const inputSchema: z.ZodObject<{
12
12
  suite: z.ZodOptional<z.ZodString>;
13
13
  runner: z.ZodOptional<z.ZodEnum<{
14
14
  command: "command";
15
- vitest: "vitest";
16
15
  playwright: "playwright";
16
+ vitest: "vitest";
17
17
  }>>;
18
18
  config: z.ZodOptional<z.ZodString>;
19
19
  files: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
@@ -14,6 +14,7 @@ import { join } from 'node:path';
14
14
  import { globSync } from 'glob';
15
15
  import { z } from 'zod';
16
16
  import { applyOutputTransform } from '#output-transforms/index';
17
+ import { getManagedRunner } from '#tool-runtime';
17
18
  import { resolveProjectRoot } from './_shared/project-root.js';
18
19
  import { createSummaryOutputSchema, createSummaryResult } from './_shared/result.js';
19
20
  import { isRunFailure, runCommand } from './_shared/run-command.js';
@@ -143,11 +144,12 @@ const tool = {
143
144
  // current resolution treats each entry as a relative path either way.
144
145
  const workspaceGlobs = targets ? readWorkspaceGlobs(cwd) : null;
145
146
  const runs = [];
147
+ const resolution = getManagedRunner('tsc', { outputPolicy: 'structured' });
146
148
  if (targets) {
147
149
  for (const pkg of targets) {
148
150
  const resolvedTarget = resolveTypecheckTarget(cwd, pkg, workspaceGlobs);
149
151
  const tsconfig = join(resolvedTarget, 'tsconfig.json');
150
- const outcome = await runCommand('tsc', ['--noEmit', '-p', tsconfig], runOptions);
152
+ const outcome = await runCommand(resolution.command, [...resolution.args, '--noEmit', '-p', tsconfig], runOptions);
151
153
  if (isRunFailure(outcome)) {
152
154
  throw outcome.error;
153
155
  }
@@ -155,7 +157,7 @@ const tool = {
155
157
  }
156
158
  }
157
159
  else {
158
- const outcome = await runCommand('tsc', ['--noEmit'], runOptions);
160
+ const outcome = await runCommand(resolution.command, [...resolution.args, '--noEmit'], runOptions);
159
161
  if (isRunFailure(outcome)) {
160
162
  throw outcome.error;
161
163
  }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Run Stryker only on packages changed vs. the base branch.
3
+ * Returns 0 on success, 1 if any package fails its break threshold.
4
+ *
5
+ * Reads GITHUB_BASE_REF (set by GitHub Actions on pull_request events) to
6
+ * determine the base branch; falls back to "main".
7
+ */
8
+ export declare function runAffectedMutation(): 0 | 1;
9
+ //# sourceMappingURL=affected.d.ts.map