@webpresso/agent-kit 0.21.4 → 0.23.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 (194) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +93 -66
  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/plan-refine/SKILL.md +5 -4
  13. package/catalog/agent/skills/pll/SKILL.md +1 -0
  14. package/catalog/base-kit/.github/workflows/ci.webpresso.yml.tmpl +33 -0
  15. package/catalog/base-kit/commitlint.config.ts.tmpl +1 -3
  16. package/catalog/base-kit/e2e/fixtures/smoke.html.tmpl +13 -0
  17. package/catalog/base-kit/e2e/smoke.spec.ts.tmpl +13 -0
  18. package/catalog/base-kit/oxlint.config.ts.tmpl +26 -0
  19. package/catalog/base-kit/playwright.config.ts.tmpl +10 -0
  20. package/catalog/base-kit/src/quality-sample.test.ts.tmpl +19 -0
  21. package/catalog/base-kit/src/quality-sample.ts.tmpl +11 -0
  22. package/catalog/base-kit/stryker.config.ts.tmpl +14 -0
  23. package/catalog/base-kit/tsconfig.json.tmpl +9 -0
  24. package/catalog/base-kit/vitest.config.ts.tmpl +10 -0
  25. package/catalog/docs/templates/adr.md +1 -1
  26. package/catalog/docs/templates/blueprint.md +2 -0
  27. package/catalog/docs/templates/blueprint.yaml +16 -15
  28. package/catalog/docs/templates/guide.md +1 -1
  29. package/catalog/docs/templates/postmortem.md +1 -1
  30. package/catalog/docs/templates/research.md +1 -1
  31. package/catalog/docs/templates/runbook.md +1 -1
  32. package/catalog/docs/templates/system.md +12 -3
  33. package/catalog/docs/templates/tech-debt.md +1 -0
  34. package/commands/blueprint.md +10 -12
  35. package/dist/esm/audit/blueprint-db-consistency.d.ts +1 -1
  36. package/dist/esm/audit/blueprint-db-consistency.js +6 -8
  37. package/dist/esm/audit/blueprint-lifecycle-sql.js +10 -3
  38. package/dist/esm/audit/cloudflare-deploy-contract.d.ts +3 -0
  39. package/dist/esm/audit/cloudflare-deploy-contract.js +64 -0
  40. package/dist/esm/audit/no-legacy-cli-bin.d.ts +3 -0
  41. package/dist/esm/audit/no-legacy-cli-bin.js +100 -0
  42. package/dist/esm/audit/package-surface.js +14 -1
  43. package/dist/esm/audit/repo-guardrails.js +40 -13
  44. package/dist/esm/audit/resolve-audit-script.d.ts +24 -0
  45. package/dist/esm/audit/resolve-audit-script.js +27 -0
  46. package/dist/esm/audit/roadmap-links.js +23 -10
  47. package/dist/esm/blueprint/core/schema.d.ts +8 -8
  48. package/dist/esm/blueprint/core/schema.js +2 -2
  49. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  50. package/dist/esm/blueprint/db/ingester.js +18 -10
  51. package/dist/esm/blueprint/index.d.ts +0 -1
  52. package/dist/esm/blueprint/index.js +0 -2
  53. package/dist/esm/blueprint/lifecycle/audit.js +9 -2
  54. package/dist/esm/blueprint/lifecycle/local.js +15 -4
  55. package/dist/esm/blueprint/local.d.ts +0 -3
  56. package/dist/esm/blueprint/local.js +0 -2
  57. package/dist/esm/blueprint/service/BlueprintCreationService.js +16 -8
  58. package/dist/esm/blueprint/service/BlueprintService.js +37 -19
  59. package/dist/esm/blueprint/service/scanner.js +73 -9
  60. package/dist/esm/blueprint/tracked-document/schema.d.ts +2 -2
  61. package/dist/esm/blueprint/utils/document-paths.d.ts +23 -0
  62. package/dist/esm/blueprint/utils/document-paths.js +91 -0
  63. package/dist/esm/blueprint/utils/package-assets.d.ts +11 -0
  64. package/dist/esm/blueprint/utils/package-assets.js +33 -4
  65. package/dist/esm/build/package-manifest.js +7 -0
  66. package/dist/esm/build/release-policy.d.ts +27 -0
  67. package/dist/esm/build/release-policy.js +29 -0
  68. package/dist/esm/build/runtime-targets.d.ts +13 -0
  69. package/dist/esm/build/runtime-targets.js +48 -0
  70. package/dist/esm/build/sync-catalog-doc-templates.d.ts +23 -0
  71. package/dist/esm/build/sync-catalog-doc-templates.js +93 -0
  72. package/dist/esm/cli/auto-update/detect-pm.d.ts +15 -0
  73. package/dist/esm/cli/auto-update/detect-pm.js +24 -9
  74. package/dist/esm/cli/auto-update/skip.js +9 -1
  75. package/dist/esm/cli/bundle/agent-command-inventory.d.ts +120 -0
  76. package/dist/esm/cli/bundle/agent-command-inventory.js +100 -0
  77. package/dist/esm/cli/bundle/index.d.ts +17 -0
  78. package/dist/esm/cli/bundle/index.js +15 -0
  79. package/dist/esm/cli/cli.d.ts +1 -1
  80. package/dist/esm/cli/cli.js +49 -5
  81. package/dist/esm/cli/commands/audit-core.d.ts +1 -1
  82. package/dist/esm/cli/commands/audit.js +4 -7
  83. package/dist/esm/cli/commands/blueprint/router.js +16 -10
  84. package/dist/esm/cli/commands/blueprint/template-resolver.js +8 -4
  85. package/dist/esm/cli/commands/hook.d.ts +8 -0
  86. package/dist/esm/cli/commands/hook.js +47 -0
  87. package/dist/esm/cli/commands/init/host-visibility.js +4 -2
  88. package/dist/esm/cli/commands/init/index.js +80 -7
  89. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -0
  90. package/dist/esm/cli/commands/init/scaffold-base-kit.js +142 -7
  91. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/codex-ownership.js +9 -1
  92. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +130 -20
  93. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +65 -0
  94. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +64 -0
  95. package/dist/esm/cli/commands/package-manager.d.ts +15 -0
  96. package/dist/esm/cli/commands/package-manager.js +42 -0
  97. package/dist/esm/cli/commands/test.d.ts +1 -0
  98. package/dist/esm/cli/commands/test.js +2 -1
  99. package/dist/esm/cli/commands/typecheck.js +10 -19
  100. package/dist/esm/cli/package-scripts.d.ts +12 -0
  101. package/dist/esm/cli/package-scripts.js +59 -0
  102. package/dist/esm/cli/utils.js +3 -22
  103. package/dist/esm/cli/wp-extensions.d.ts +14 -0
  104. package/dist/esm/cli/wp-extensions.js +34 -0
  105. package/dist/esm/config/docs-lint/schemas/common.d.ts +1 -1
  106. package/dist/esm/config/docs-lint/schemas/implementation-plan.d.ts +2 -2
  107. package/dist/esm/config/docs-lint/schemas/parent-roadmap.d.ts +1 -1
  108. package/dist/esm/config/stryker/index.d.ts +85 -0
  109. package/dist/esm/config/stryker/index.js +31 -0
  110. package/dist/esm/e2e/command-builder.js +35 -7
  111. package/dist/esm/e2e/config.d.ts +56 -0
  112. package/dist/esm/e2e/config.js +114 -0
  113. package/dist/esm/e2e/execution.js +8 -0
  114. package/dist/esm/e2e/run-planner.js +2 -0
  115. package/dist/esm/e2e/types.d.ts +3 -0
  116. package/dist/esm/format/index.js +5 -1
  117. package/dist/esm/hooks/guard-switch/index.d.ts +1 -1
  118. package/dist/esm/hooks/guard-switch/index.js +22 -14
  119. package/dist/esm/hooks/post-tool/lint-after-edit.d.ts +1 -0
  120. package/dist/esm/hooks/post-tool/lint-after-edit.js +5 -2
  121. package/dist/esm/hooks/pretool-guard/validators/file-conventions.js +1 -1
  122. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.d.ts +6 -0
  123. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +27 -2
  124. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +2 -1
  125. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +59 -34
  126. package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -3
  127. package/dist/esm/hooks/shared/routing-block.js +18 -4
  128. package/dist/esm/hooks/shared/validators/blueprint.js +3 -0
  129. package/dist/esm/hooks/stop/qa-changed-files.d.ts +1 -0
  130. package/dist/esm/hooks/stop/qa-changed-files.js +5 -2
  131. package/dist/esm/lint/index.js +3 -1
  132. package/dist/esm/mcp/auto-discover.d.ts +2 -0
  133. package/dist/esm/mcp/auto-discover.js +14 -6
  134. package/dist/esm/mcp/blueprint-server.js +379 -80
  135. package/dist/esm/mcp/cli.js +21 -0
  136. package/dist/esm/mcp/runners/test.js +15 -0
  137. package/dist/esm/mcp/server.d.ts +7 -0
  138. package/dist/esm/mcp/server.js +16 -27
  139. package/dist/esm/mcp/tools/_registry.d.ts +3 -0
  140. package/dist/esm/mcp/tools/_registry.js +21 -0
  141. package/dist/esm/mcp/tools/audit.d.ts +1 -0
  142. package/dist/esm/mcp/tools/audit.js +13 -8
  143. package/dist/esm/mcp/tools/typecheck.js +4 -2
  144. package/dist/esm/mutation/affected.d.ts +9 -0
  145. package/dist/esm/mutation/affected.js +36 -0
  146. package/dist/esm/package.json +8 -0
  147. package/dist/esm/runtime/package-version.d.ts +2 -0
  148. package/dist/esm/runtime/package-version.js +43 -0
  149. package/dist/esm/test/command-builder.d.ts +4 -0
  150. package/dist/esm/test/command-builder.js +28 -3
  151. package/dist/esm/test-helpers/hermetic-env.d.ts +25 -0
  152. package/dist/esm/test-helpers/hermetic-env.js +31 -0
  153. package/dist/esm/tool-runtime/index.d.ts +5 -0
  154. package/dist/esm/tool-runtime/index.js +24 -0
  155. package/dist/esm/tool-runtime/resolve-runner.d.ts +16 -0
  156. package/dist/esm/tool-runtime/resolve-runner.js +42 -0
  157. package/dist/esm/typecheck/index.js +4 -2
  158. package/dist/esm/wp-extension/index.d.ts +50 -0
  159. package/dist/esm/wp-extension/index.js +268 -0
  160. package/package.json +75 -46
  161. package/skills/plan-refine/SKILL.md +5 -4
  162. package/skills/pll/SKILL.md +1 -0
  163. package/dist/esm/blueprint/dag/cycle-detector.d.ts +0 -12
  164. package/dist/esm/blueprint/dag/cycle-detector.js +0 -46
  165. package/dist/esm/blueprint/dag/executor.d.ts +0 -140
  166. package/dist/esm/blueprint/dag/executor.js +0 -292
  167. package/dist/esm/blueprint/dag/index.d.ts +0 -20
  168. package/dist/esm/blueprint/dag/index.js +0 -17
  169. package/dist/esm/blueprint/dag/interfaces.d.ts +0 -56
  170. package/dist/esm/blueprint/dag/interfaces.js +0 -13
  171. package/dist/esm/blueprint/dag/local/independence.d.ts +0 -107
  172. package/dist/esm/blueprint/dag/local/independence.js +0 -231
  173. package/dist/esm/blueprint/dag/local/index.d.ts +0 -14
  174. package/dist/esm/blueprint/dag/local/index.js +0 -14
  175. package/dist/esm/blueprint/dag/local/package-graph.d.ts +0 -66
  176. package/dist/esm/blueprint/dag/local/package-graph.js +0 -148
  177. package/dist/esm/blueprint/dag/plan-parser.d.ts +0 -54
  178. package/dist/esm/blueprint/dag/plan-parser.js +0 -236
  179. package/dist/esm/blueprint/dag/task-graph-algorithms.d.ts +0 -13
  180. package/dist/esm/blueprint/dag/task-graph-algorithms.js +0 -236
  181. package/dist/esm/blueprint/dag/task-graph.d.ts +0 -171
  182. package/dist/esm/blueprint/dag/task-graph.js +0 -370
  183. package/dist/esm/blueprint/dag/types.d.ts +0 -17
  184. package/dist/esm/blueprint/dag/types.js +0 -2
  185. package/dist/esm/blueprint/graph/index.d.ts +0 -5
  186. package/dist/esm/blueprint/graph/index.js +0 -5
  187. package/dist/esm/blueprint/graph/mermaid-parser.d.ts +0 -3
  188. package/dist/esm/blueprint/graph/mermaid-parser.js +0 -93
  189. package/dist/esm/blueprint/graph/mermaid-serializer.d.ts +0 -3
  190. package/dist/esm/blueprint/graph/mermaid-serializer.js +0 -20
  191. package/dist/esm/blueprint/graph/schema.d.ts +0 -89
  192. package/dist/esm/blueprint/graph/schema.js +0 -104
  193. package/dist/esm/blueprint/graph/task-graph-adapter.d.ts +0 -6
  194. package/dist/esm/blueprint/graph/task-graph-adapter.js +0 -30
@@ -18,10 +18,12 @@ import path from 'node:path';
18
18
  import matter from 'gray-matter';
19
19
  import { z } from 'zod';
20
20
  import { parseBlueprint } from '#core/parser';
21
+ import { setBlueprintFrontmatterFields } from '#lifecycle/engine';
21
22
  import { openDb } from '#db/connection.js';
22
23
  import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
23
24
  import { findTemplate } from '#db/templates.js';
24
25
  import { resolveBlueprintRoot } from '#utils/blueprint-root.js';
26
+ import { getBlueprintDocumentPaths } from '#utils/document-paths.js';
25
27
  import { evidenceListSchema, canonicalizeEvidenceList } from '#evidence.js';
26
28
  import { checkFreshness, readCurrentHead, readProjectionMetadata } from '#freshness.js';
27
29
  import { applyVerification, assertAllTasksHaveCanonicalPassingEvidence, readTaskVerification, } from '#verification.js';
@@ -93,6 +95,13 @@ async function resolveSyncAdapter(cwd) {
93
95
  };
94
96
  }
95
97
  const DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS = 5_000;
98
+ function todayIsoDate() {
99
+ return new Date().toISOString().split('T')[0] ?? new Date().toISOString();
100
+ }
101
+ function formatBlueprintProgress(totalTasks, doneTasks, blockedTasks) {
102
+ const percent = totalTasks === 0 ? 0 : Math.round((doneTasks / totalTasks) * 100);
103
+ return `${percent}% (${doneTasks}/${totalTasks} tasks done, ${blockedTasks} blocked, updated ${todayIsoDate()})`;
104
+ }
96
105
  function readPlatformMutationTimeoutMs() {
97
106
  const parsed = Number.parseInt(process.env['WP_BLUEPRINT_PLATFORM_MUTATION_TIMEOUT_MS'] ??
98
107
  String(DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS), 10);
@@ -242,11 +251,25 @@ function openDbRW(cwd) {
242
251
  async function reIngest(cwd) {
243
252
  await reIngestProjection(cwd);
244
253
  }
254
+ async function persistBlueprintMarkdown(input) {
255
+ const { projectCwd, slug, blueprintPath, markdown } = input;
256
+ mkdirSync(path.dirname(blueprintPath), { recursive: true });
257
+ parseBlueprint(markdown, slug);
258
+ writeFileSync(blueprintPath, markdown, 'utf8');
259
+ await reIngest(projectCwd);
260
+ const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
261
+ if (!refreshed.blueprint) {
262
+ throw new Error(`Blueprint "${slug}" did not appear in the projection after write`);
263
+ }
264
+ return refreshed.blueprint;
265
+ }
245
266
  function findBlueprintDir(blueprintRoot, slug, states) {
246
267
  for (const state of states) {
247
- const d = path.join(blueprintRoot, state, slug);
248
- if (existsSync(d))
249
- 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 };
250
273
  }
251
274
  return null;
252
275
  }
@@ -537,7 +560,7 @@ async function handleNew(cwd, raw) {
537
560
  }
538
561
  const b = bytes(template);
539
562
  const slug = titleToSlug(title);
540
- const targetPath = path.join(resolveBlueprintRoot(cwd), 'draft', slug, '_overview.md');
563
+ const targetPath = getBlueprintDocumentPaths(resolveBlueprintRoot(cwd), 'draft', slug).flat;
541
564
  // Platform-first path: push event to register the blueprint before returning the scaffold.
542
565
  // Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
543
566
  const adapter = await resolveSyncAdapter(cwd);
@@ -866,7 +889,7 @@ async function handleTaskVerify(projectResolver, cwd, raw) {
866
889
  if (!found) {
867
890
  return err('wp_blueprint_task_verify failed', `Blueprint "${slug}" not found in any state directory`);
868
891
  }
869
- const filePath = path.join(found.dir, '_overview.md');
892
+ const filePath = found.path;
870
893
  if (!existsSync(filePath)) {
871
894
  return err('wp_blueprint_task_verify failed', `Blueprint overview not found at ${filePath}`);
872
895
  }
@@ -961,12 +984,8 @@ async function handlePromote(projectResolver, cwd, raw) {
961
984
  const found = findBlueprintDir(root, slug, ALL_STATES);
962
985
  if (!found)
963
986
  return err('wp_blueprint_promote failed', `Blueprint "${slug}" not found in any state directory`);
964
- const { dir: currentDir, state: currentState } = found;
965
- const overviewPath = path.join(currentDir, '_overview.md');
966
- const ts = readVt(projectCwd);
967
- const mtime = existsSync(overviewPath) ? statSync(overviewPath).mtimeMs : 0;
968
- if ((ts[slug] ?? 0) < mtime)
969
- return err('wp_blueprint_promote refused', `Blueprint "${slug}" not validated since last write. Run wp_blueprint_validate first.`);
987
+ const { state: currentState } = found;
988
+ const overviewPath = found.path;
970
989
  if (to_state === 'completed') {
971
990
  try {
972
991
  assertBlueprintCanComplete(overviewPath, slug);
@@ -999,40 +1018,34 @@ async function handlePromote(projectResolver, cwd, raw) {
999
1018
  catch (e) {
1000
1019
  return err('wp_blueprint_promote failed', toStr(e));
1001
1020
  }
1002
- const { renameSync } = await import('node:fs');
1003
- const destDir = path.join(root, to_state, slug);
1004
- mkdirSync(path.dirname(destDir), { recursive: true });
1005
1021
  try {
1006
- renameSync(currentDir, destDir);
1022
+ const transitioned = await applyLocalBlueprintTransition({
1023
+ found,
1024
+ projectCwd,
1025
+ slug,
1026
+ to_state,
1027
+ });
1028
+ const payload = {
1029
+ summary: `Blueprint "${slug}" promoted from "${currentState}" to "${to_state}"`,
1030
+ slug,
1031
+ from_state: currentState,
1032
+ to_state,
1033
+ new_path: transitioned.overviewPath,
1034
+ status: transitioned.blueprint.status,
1035
+ content_hash: transitioned.blueprint.content_hash,
1036
+ revision: transitioned.blueprint.content_hash,
1037
+ ingested_at: transitioned.blueprint.ingested_at,
1038
+ failures: [],
1039
+ bytes: 0,
1040
+ tokensSaved: 0,
1041
+ };
1042
+ if (currentState === 'draft' && to_state === 'planned')
1043
+ appendHint(payload, projectCwd, 'PLAN_REFINE');
1044
+ return finishPayload(payload);
1007
1045
  }
1008
1046
  catch (e) {
1009
- return err('wp_blueprint_promote failed', `Directory move error: ${toStr(e)}`);
1010
- }
1011
- const destOverview = path.join(destDir, '_overview.md');
1012
- if (existsSync(destOverview)) {
1013
- const fm = matter(readFileSync(destOverview, 'utf8'));
1014
- fm.data['status'] = to_state;
1015
- writeFileSync(destOverview, matter.stringify(fm.content, fm.data), 'utf8');
1016
- }
1017
- try {
1018
- await reIngest(projectCwd);
1019
- }
1020
- catch {
1021
- /* non-fatal */
1047
+ return err('wp_blueprint_promote failed', toStr(e));
1022
1048
  }
1023
- const payload = {
1024
- summary: `Blueprint "${slug}" promoted from "${currentState}" to "${to_state}"`,
1025
- slug,
1026
- from_state: currentState,
1027
- to_state,
1028
- new_path: destOverview,
1029
- failures: [],
1030
- bytes: 0,
1031
- tokensSaved: 0,
1032
- };
1033
- if (currentState === 'draft' && to_state === 'planned')
1034
- appendHint(payload, projectCwd, 'PLAN_REFINE');
1035
- return finishPayload(payload);
1036
1049
  }
1037
1050
  const finalizeSchema = z.object({ project_id: z.string().optional(), slug: z.string() });
1038
1051
  async function handleFinalize(projectResolver, cwd, raw) {
@@ -1075,7 +1088,7 @@ async function handleFinalize(projectResolver, cwd, raw) {
1075
1088
  return err('wp_blueprint_finalize failed', `Blueprint "${slug}" not found`);
1076
1089
  }
1077
1090
  try {
1078
- assertBlueprintCanComplete(path.join(found.dir, '_overview.md'), slug);
1091
+ assertBlueprintCanComplete(found.path, slug);
1079
1092
  }
1080
1093
  catch (error) {
1081
1094
  return err('wp_blueprint_finalize refused', toStr(error));
@@ -1102,39 +1115,32 @@ async function handleFinalize(projectResolver, cwd, raw) {
1102
1115
  catch (e) {
1103
1116
  return err('wp_blueprint_finalize failed', toStr(e));
1104
1117
  }
1105
- const { renameSync } = await import('node:fs');
1106
- const destDir = path.join(root, 'completed', slug);
1107
- mkdirSync(path.dirname(destDir), { recursive: true });
1108
1118
  try {
1109
- renameSync(found.dir, destDir);
1119
+ const transitioned = await applyLocalBlueprintTransition({
1120
+ found,
1121
+ projectCwd,
1122
+ slug,
1123
+ to_state: 'completed',
1124
+ });
1125
+ const payload = {
1126
+ summary: `Blueprint "${slug}" finalized and moved to completed`,
1127
+ slug,
1128
+ new_path: transitioned.overviewPath,
1129
+ status: transitioned.blueprint.status,
1130
+ content_hash: transitioned.blueprint.content_hash,
1131
+ revision: transitioned.blueprint.content_hash,
1132
+ ingested_at: transitioned.blueprint.ingested_at,
1133
+ failures: [],
1134
+ bytes: 0,
1135
+ tokensSaved: 0,
1136
+ };
1137
+ if (hasRecentAuditFinding(projectCwd))
1138
+ appendHint(payload, projectCwd, 'AUDIT_FIX');
1139
+ return finishPayload(payload);
1110
1140
  }
1111
1141
  catch (e) {
1112
- return err('wp_blueprint_finalize failed', `Directory move error: ${toStr(e)}`);
1113
- }
1114
- const destOverview = path.join(destDir, '_overview.md');
1115
- if (existsSync(destOverview)) {
1116
- const fm = matter(readFileSync(destOverview, 'utf8'));
1117
- fm.data['status'] = 'completed';
1118
- fm.data['completed_at'] = new Date().toISOString().split('T')[0] ?? '';
1119
- writeFileSync(destOverview, matter.stringify(fm.content, fm.data), 'utf8');
1120
- }
1121
- try {
1122
- await reIngest(projectCwd);
1123
- }
1124
- catch {
1125
- /* non-fatal */
1142
+ return err('wp_blueprint_finalize failed', toStr(e));
1126
1143
  }
1127
- const payload = {
1128
- summary: `Blueprint "${slug}" finalized and moved to completed`,
1129
- slug,
1130
- new_path: destOverview,
1131
- failures: [],
1132
- bytes: 0,
1133
- tokensSaved: 0,
1134
- };
1135
- if (hasRecentAuditFinding(projectCwd))
1136
- appendHint(payload, projectCwd, 'AUDIT_FIX');
1137
- return finishPayload(payload);
1138
1144
  }
1139
1145
  function assertBlueprintCanComplete(overviewPath, slug) {
1140
1146
  const markdown = readFileSync(overviewPath, 'utf8');
@@ -1698,6 +1704,245 @@ const createSchema = MutationTarget.extend({
1698
1704
  request_id: z.string().min(1).optional(),
1699
1705
  head_at_ingest: z.string().nullable().optional(),
1700
1706
  });
1707
+ const putTaskSchema = z.object({
1708
+ id: z.string().min(1),
1709
+ title: z.string().min(1),
1710
+ status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'dropped']).default('todo'),
1711
+ wave: z.string().optional(),
1712
+ lane: z.string().optional(),
1713
+ description: z.string().optional(),
1714
+ acceptance: z.array(z.string().min(1)).min(1),
1715
+ });
1716
+ const putDocumentSchema = z.object({
1717
+ type: z.literal('blueprint').default('blueprint'),
1718
+ title: z.string().min(1),
1719
+ status: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
1720
+ complexity: z.enum(['XS', 'S', 'M', 'L', 'XL']),
1721
+ owner: z.string().min(1),
1722
+ created: z.string().min(1),
1723
+ last_updated: z.string().min(1),
1724
+ progress: z.string().optional(),
1725
+ tags: z.array(z.string()).optional(),
1726
+ product_wedge_anchor: z.object({
1727
+ stage_outcome: z.string().min(1),
1728
+ consuming_surface: z.string().min(1),
1729
+ new_user_visible_capability: z.string().min(1),
1730
+ }),
1731
+ summary: z.string().min(1),
1732
+ tasks: z.array(putTaskSchema).min(1),
1733
+ });
1734
+ const putSchema = MutationTarget.extend({
1735
+ slug: z.string().min(1),
1736
+ document: putDocumentSchema,
1737
+ request_id: z.string().min(1).optional(),
1738
+ head_at_ingest: z.string().nullable().optional(),
1739
+ });
1740
+ function renderBlueprintMarkdownFromDocument(slug, document) {
1741
+ const frontmatter = [
1742
+ '---',
1743
+ `type: ${document.type}`,
1744
+ `title: ${document.title}`,
1745
+ `status: ${document.status}`,
1746
+ `complexity: ${document.complexity}`,
1747
+ `owner: ${document.owner}`,
1748
+ `created: '${document.created}'`,
1749
+ `last_updated: '${document.last_updated}'`,
1750
+ ...(document.progress ? [`progress: ${JSON.stringify(document.progress)}`] : []),
1751
+ ...(document.tags && document.tags.length > 0
1752
+ ? ['tags:', ...document.tags.map((tag) => ` - ${tag}`)]
1753
+ : []),
1754
+ '---',
1755
+ '',
1756
+ ];
1757
+ const sections = [
1758
+ `# ${document.title}`,
1759
+ '',
1760
+ '## Product wedge anchor',
1761
+ '',
1762
+ `- **Stage outcome:** ${document.product_wedge_anchor.stage_outcome}`,
1763
+ `- **Consuming surface:** ${document.product_wedge_anchor.consuming_surface}`,
1764
+ `- **New user-visible capability:** ${document.product_wedge_anchor.new_user_visible_capability}`,
1765
+ '',
1766
+ '## Summary',
1767
+ '',
1768
+ document.summary,
1769
+ '',
1770
+ ];
1771
+ const taskBlocks = document.tasks.flatMap((task) => [
1772
+ `#### Task ${task.id}: ${task.title}`,
1773
+ '',
1774
+ `**Status:** ${task.status}`,
1775
+ ...(task.wave ? [`**Wave:** ${task.wave}`] : []),
1776
+ ...(task.lane ? [`**Lane:** ${task.lane}`] : []),
1777
+ ...(task.description ? ['', task.description] : []),
1778
+ '',
1779
+ '**Acceptance:**',
1780
+ ...task.acceptance.map((item) => `- [ ] ${item}`),
1781
+ '',
1782
+ ]);
1783
+ return [...frontmatter, ...sections, ...taskBlocks].join('\n').trimEnd() + '\n';
1784
+ }
1785
+ async function handleBlueprintPut(projectResolver, cwd, raw) {
1786
+ const p = putSchema.safeParse(raw);
1787
+ if (!p.success)
1788
+ return err('wp_blueprint_put validation error', p.error.message);
1789
+ const { project_id, slug, document, request_id, head_at_ingest } = p.data;
1790
+ const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
1791
+ if ('content' in resolvedProject)
1792
+ return resolvedProject;
1793
+ const projectCwd = resolvedProject.cwd;
1794
+ await ensureProjectionReady(projectCwd);
1795
+ const freshnessFailure = validateMutationFreshnessToken(projectCwd, head_at_ingest, 'wp_blueprint_put', 'wp_blueprint_get');
1796
+ if (freshnessFailure)
1797
+ return freshnessFailure;
1798
+ const payloadHash = hashMutationPayload({ slug, document });
1799
+ const replay = request_id !== undefined
1800
+ ? readMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash)
1801
+ : null;
1802
+ if (replay)
1803
+ return replay;
1804
+ const root = resolveBlueprintRoot(projectCwd);
1805
+ const found = findBlueprintDir(root, slug, ALL_STATES);
1806
+ if (found && found.state !== document.status) {
1807
+ return err('wp_blueprint_put refused', `Blueprint "${slug}" currently lives in "${found.state}" and cannot be rewritten as "${document.status}" without a lifecycle transition.`);
1808
+ }
1809
+ if (!found && document.status !== 'draft') {
1810
+ return err('wp_blueprint_put refused', `New blueprint "${slug}" must start in "draft"; use wp_blueprint_transition for later lifecycle moves.`);
1811
+ }
1812
+ const overviewPath = found
1813
+ ? found.path
1814
+ : getBlueprintDocumentPaths(root, document.status, slug).flat;
1815
+ try {
1816
+ const markdown = renderBlueprintMarkdownFromDocument(slug, document);
1817
+ const blueprint = await persistBlueprintMarkdown({
1818
+ projectCwd,
1819
+ slug,
1820
+ blueprintPath: overviewPath,
1821
+ markdown,
1822
+ });
1823
+ const payload = {
1824
+ summary: `Blueprint "${slug}" written to ${overviewPath}`,
1825
+ slug,
1826
+ path: overviewPath,
1827
+ status: blueprint.status,
1828
+ content_hash: blueprint.content_hash,
1829
+ ingested_at: blueprint.ingested_at,
1830
+ revision: blueprint.content_hash,
1831
+ idempotent: false,
1832
+ failures: [],
1833
+ next_action: makeNextAction('verify_task', 'Blueprint written. Next: validate or transition the latest revision token through the structured surface.'),
1834
+ project_id: resolvedProject.project_id ?? projectCwd,
1835
+ };
1836
+ if (request_id !== undefined) {
1837
+ recordMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash, payload);
1838
+ }
1839
+ return finishPayload(payload);
1840
+ }
1841
+ catch (e) {
1842
+ return err('wp_blueprint_put failed', toStr(e));
1843
+ }
1844
+ }
1845
+ const transitionSchema = MutationTarget.extend({
1846
+ slug: z.string().min(1),
1847
+ to_state: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
1848
+ expected_version: z.string().min(1),
1849
+ });
1850
+ async function handleBlueprintTransition(projectResolver, cwd, raw) {
1851
+ const p = transitionSchema.safeParse(raw);
1852
+ if (!p.success)
1853
+ return err('wp_blueprint_transition validation error', p.error.message);
1854
+ const { project_id, slug, to_state, expected_version } = p.data;
1855
+ const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
1856
+ if ('content' in resolvedProject)
1857
+ return resolvedProject;
1858
+ const projectCwd = resolvedProject.cwd;
1859
+ await ensureProjectionReady(projectCwd);
1860
+ const target = dbPath(projectCwd);
1861
+ if (!existsSync(target))
1862
+ return err('wp_blueprint_transition failed', 'Blueprint DB not found');
1863
+ const current = getCurrentProjectBlueprint(projectCwd, slug);
1864
+ if (!current.blueprint) {
1865
+ return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found`);
1866
+ }
1867
+ if (current.blueprint.content_hash !== expected_version) {
1868
+ return jsonContent({
1869
+ summary: `wp_blueprint_transition rejected a stale blueprint revision`,
1870
+ failures: [
1871
+ `expected_version "${expected_version}" does not match current content hash "${current.blueprint.content_hash}"`,
1872
+ ],
1873
+ error: 'stale_blueprint_revision',
1874
+ next_action: makeNextAction('reingest_project', 'Call wp_blueprint_get again to fetch the latest content_hash before retrying the transition.'),
1875
+ bytes: 0,
1876
+ tokensSaved: 0,
1877
+ }, true);
1878
+ }
1879
+ const root = resolveBlueprintRoot(projectCwd);
1880
+ const found = findBlueprintDir(root, slug, ALL_STATES);
1881
+ if (!found)
1882
+ return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found on disk`);
1883
+ try {
1884
+ const refreshed = await applyLocalBlueprintTransition({
1885
+ found,
1886
+ projectCwd,
1887
+ slug,
1888
+ to_state,
1889
+ });
1890
+ return finishPayload({
1891
+ summary: `Blueprint "${slug}" transitioned to ${to_state}`,
1892
+ slug,
1893
+ old_status: current.blueprint.status,
1894
+ new_status: to_state,
1895
+ status: refreshed.blueprint.status,
1896
+ content_hash: refreshed.blueprint.content_hash,
1897
+ revision: refreshed.blueprint.content_hash,
1898
+ ingested_at: refreshed.blueprint.ingested_at,
1899
+ failures: [],
1900
+ project_id: resolvedProject.project_id ?? projectCwd,
1901
+ });
1902
+ }
1903
+ catch (e) {
1904
+ return err('wp_blueprint_transition failed', toStr(e));
1905
+ }
1906
+ }
1907
+ async function applyLocalBlueprintTransition(input) {
1908
+ const { projectCwd, slug, to_state, found } = input;
1909
+ const root = resolveBlueprintRoot(projectCwd);
1910
+ const overviewPath = found.path;
1911
+ const parsed = runValidate(overviewPath);
1912
+ if (!parsed.valid) {
1913
+ throw new Error(parsed.gaps.join('; '));
1914
+ }
1915
+ const markdown = readFileSync(overviewPath, 'utf8');
1916
+ const currentBlueprint = parseBlueprint(markdown, slug);
1917
+ const updated = setBlueprintFrontmatterFields(markdown, {
1918
+ status: to_state,
1919
+ last_updated: todayIsoDate(),
1920
+ completed_at: to_state === 'completed' ? todayIsoDate() : undefined,
1921
+ progress: formatBlueprintProgress(currentBlueprint.tasks.length, currentBlueprint.tasks.filter((task) => task.status === 'done').length, currentBlueprint.tasks.filter((task) => task.status === 'blocked').length),
1922
+ });
1923
+ parseBlueprint(updated, slug);
1924
+ const destination = getBlueprintDocumentPaths(root, to_state, slug);
1925
+ mkdirSync(path.dirname(found.shape === 'flat' ? destination.flat : destination.directory), {
1926
+ recursive: true,
1927
+ });
1928
+ let finalOverviewPath = overviewPath;
1929
+ if (found.state !== to_state) {
1930
+ const { renameSync } = await import('node:fs');
1931
+ renameSync(found.shape === 'flat' ? overviewPath : found.dir, found.shape === 'flat' ? destination.flat : destination.directory);
1932
+ finalOverviewPath = found.shape === 'flat' ? destination.flat : destination.folder;
1933
+ }
1934
+ writeFileSync(finalOverviewPath, updated, 'utf8');
1935
+ await reIngest(projectCwd);
1936
+ const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
1937
+ if (!refreshed.blueprint) {
1938
+ throw new Error(`Blueprint "${slug}" did not appear in the projection after transition`);
1939
+ }
1940
+ return {
1941
+ blueprint: refreshed.blueprint,
1942
+ overviewPath: finalOverviewPath,
1943
+ fromState: found.state,
1944
+ };
1945
+ }
1701
1946
  async function handleBlueprintCreate(projectResolver, cwd, raw) {
1702
1947
  const p = createSchema.safeParse(raw);
1703
1948
  if (!p.success)
@@ -1720,17 +1965,19 @@ async function handleBlueprintCreate(projectResolver, cwd, raw) {
1720
1965
  const today = new Date().toISOString().split('T')[0] ?? '';
1721
1966
  const slug = titleToSlug(title);
1722
1967
  const root = resolveBlueprintRoot(projectCwd);
1723
- const targetDir = path.join(root, 'draft', slug);
1724
- const overviewPath = path.join(targetDir, '_overview.md');
1968
+ const overviewPath = getBlueprintDocumentPaths(root, 'draft', slug).flat;
1725
1969
  try {
1726
- mkdirSync(targetDir, { recursive: true });
1970
+ mkdirSync(path.dirname(overviewPath), { recursive: true });
1727
1971
  const content = BLUEPRINT_TEMPLATE.replace(/{TITLE}/g, title)
1728
1972
  .replace(/{COMPLEXITY}/g, complexity)
1729
1973
  .replace(/{DATE}/g, today)
1730
1974
  .replace('{GOAL}', goal);
1731
- writeFileSync(overviewPath, content, 'utf8');
1732
- // Re-ingest so the DB reflects the new blueprint
1733
- await reIngest(projectCwd);
1975
+ await persistBlueprintMarkdown({
1976
+ projectCwd,
1977
+ slug,
1978
+ blueprintPath: overviewPath,
1979
+ markdown: content,
1980
+ });
1734
1981
  const b = bytes(content);
1735
1982
  const payload = {
1736
1983
  summary: `Blueprint "${slug}" created at ${overviewPath}`,
@@ -1788,12 +2035,64 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
1788
2035
  },
1789
2036
  required: ['title', 'goal_prompt'],
1790
2037
  }, undefined, (r) => handleNew(cwd, r), { title: 'Blueprint New', readOnlyHint: true, openWorldHint: false });
1791
- 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 });
2038
+ registrar.registerTool('wp_blueprint_put', 'Create or replace a blueprint from a structured whole-document payload. Renders markdown, validates it, re-ingests the projection, and returns revision metadata.', {
2039
+ type: 'object',
2040
+ properties: {
2041
+ project_id: { type: 'string' },
2042
+ slug: { type: 'string' },
2043
+ document: { type: 'object' },
2044
+ request_id: { type: 'string' },
2045
+ head_at_ingest: { type: ['string', 'null'] },
2046
+ },
2047
+ required: ['project_id', 'slug', 'document'],
2048
+ }, {
2049
+ ...summaryEnvelopeOutputSchema,
2050
+ properties: {
2051
+ ...summaryEnvelopeOutputSchema.properties,
2052
+ slug: { type: 'string' },
2053
+ path: { type: 'string' },
2054
+ status: { type: 'string' },
2055
+ content_hash: { type: 'string' },
2056
+ ingested_at: { type: 'number' },
2057
+ revision: { type: 'string' },
2058
+ idempotent: { type: 'boolean' },
2059
+ project_id: { type: 'string' },
2060
+ next_action: nextActionOutputSchema,
2061
+ },
2062
+ }, (r) => handleBlueprintPut(projectResolver, cwd, r), { title: 'Blueprint Put', destructiveHint: false, openWorldHint: false });
2063
+ registrar.registerTool('wp_blueprint_transition', 'Transition a blueprint lifecycle state using an expected revision token. Revalidates the latest markdown, rewrites frontmatter atomically, re-ingests the projection, and returns updated revision metadata.', {
2064
+ type: 'object',
2065
+ properties: {
2066
+ project_id: { type: 'string' },
2067
+ slug: { type: 'string' },
2068
+ to_state: {
2069
+ type: 'string',
2070
+ enum: ['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived'],
2071
+ },
2072
+ expected_version: { type: 'string' },
2073
+ },
2074
+ required: ['project_id', 'slug', 'to_state', 'expected_version'],
2075
+ }, {
2076
+ ...summaryEnvelopeOutputSchema,
2077
+ properties: {
2078
+ ...summaryEnvelopeOutputSchema.properties,
2079
+ slug: { type: 'string' },
2080
+ old_status: { type: 'string' },
2081
+ new_status: { type: 'string' },
2082
+ status: { type: 'string' },
2083
+ content_hash: { type: 'string' },
2084
+ revision: { type: 'string' },
2085
+ ingested_at: { type: 'number' },
2086
+ project_id: { type: 'string' },
2087
+ next_action: nextActionOutputSchema,
2088
+ },
2089
+ }, (r) => handleBlueprintTransition(projectResolver, cwd, r), { title: 'Blueprint Transition', destructiveHint: 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 });
1792
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 }.', {
1793
2092
  type: 'object',
1794
2093
  properties: { blueprint: { type: 'string' }, project_id: { type: 'string' } },
1795
2094
  }, undefined, (r) => handleTaskNext(projectResolver, cwd, r), { title: 'Blueprint Task Next', readOnlyHint: true, openWorldHint: false });
1796
- 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 }.', {
1797
2096
  type: 'object',
1798
2097
  properties: {
1799
2098
  project_id: { type: 'string' },
@@ -1911,7 +2210,7 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
1911
2210
  },
1912
2211
  required: [...summaryEnvelopeOutputSchema.required, 'chunks', 'total_bytes', 'project_id'],
1913
2212
  }, (r) => handleBlueprintContext(projectResolver, cwd, r), { title: 'Blueprint Context', readOnlyHint: true, openWorldHint: false });
1914
- 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 }.', {
1915
2214
  type: 'object',
1916
2215
  properties: {
1917
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.