@webpresso/agent-kit 0.21.5 → 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 (130) 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 +64 -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 +56 -0
  82. package/dist/esm/e2e/config.js +114 -0
  83. package/dist/esm/e2e/execution.js +4 -0
  84. package/dist/esm/e2e/run-planner.js +1 -0
  85. package/dist/esm/e2e/types.d.ts +2 -0
  86. package/dist/esm/format/index.js +1 -3
  87. package/dist/esm/hooks/guard-switch/index.d.ts +1 -1
  88. package/dist/esm/hooks/guard-switch/index.js +22 -14
  89. package/dist/esm/hooks/post-tool/lint-after-edit.d.ts +1 -0
  90. package/dist/esm/hooks/post-tool/lint-after-edit.js +5 -2
  91. package/dist/esm/hooks/pretool-guard/validators/file-conventions.js +1 -1
  92. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.d.ts +6 -0
  93. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +27 -2
  94. package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +2 -1
  95. package/dist/esm/hooks/pretool-guard/validators/path-contract.js +59 -34
  96. package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -3
  97. package/dist/esm/hooks/shared/routing-block.js +18 -4
  98. package/dist/esm/hooks/shared/validators/blueprint.js +3 -0
  99. package/dist/esm/hooks/stop/qa-changed-files.d.ts +1 -0
  100. package/dist/esm/hooks/stop/qa-changed-files.js +5 -2
  101. package/dist/esm/lint/index.js +1 -1
  102. package/dist/esm/mcp/auto-discover.d.ts +2 -0
  103. package/dist/esm/mcp/auto-discover.js +14 -6
  104. package/dist/esm/mcp/blueprint-server.js +30 -26
  105. package/dist/esm/mcp/cli.js +21 -0
  106. package/dist/esm/mcp/runners/test.js +15 -0
  107. package/dist/esm/mcp/server.d.ts +7 -0
  108. package/dist/esm/mcp/server.js +16 -27
  109. package/dist/esm/mcp/tools/_registry.d.ts +3 -0
  110. package/dist/esm/mcp/tools/_registry.js +21 -0
  111. package/dist/esm/mcp/tools/audit.d.ts +1 -0
  112. package/dist/esm/mcp/tools/audit.js +11 -0
  113. package/dist/esm/mcp/tools/e2e.d.ts +1 -1
  114. package/dist/esm/mcp/tools/typecheck.js +4 -2
  115. package/dist/esm/mutation/affected.d.ts +9 -0
  116. package/dist/esm/mutation/affected.js +36 -0
  117. package/dist/esm/package.json +5 -0
  118. package/dist/esm/runtime/package-version.d.ts +2 -0
  119. package/dist/esm/runtime/package-version.js +43 -0
  120. package/dist/esm/test/command-builder.d.ts +3 -0
  121. package/dist/esm/test/command-builder.js +22 -3
  122. package/dist/esm/tool-runtime/index.d.ts +2 -2
  123. package/dist/esm/tool-runtime/index.js +2 -1
  124. package/dist/esm/tool-runtime/resolve-runner.d.ts +3 -0
  125. package/dist/esm/tool-runtime/resolve-runner.js +7 -5
  126. package/dist/esm/typecheck/index.js +4 -2
  127. package/dist/esm/wp-extension/index.d.ts +50 -0
  128. package/dist/esm/wp-extension/index.js +268 -0
  129. package/package.json +67 -31
  130. package/skills/pll/SKILL.md +1 -0
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env bun
2
- export {};
2
+ export declare function main(): Promise<void>;
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1,18 +1,26 @@
1
1
  #!/usr/bin/env bun
2
2
  import { runHook } from '#hooks/shared/hook-bootstrap';
3
+ import { realpathSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
3
5
  import { setGuardEnabled } from './state.js';
4
- runHook((input) => {
5
- const normalized = (input.prompt ?? '').toLowerCase().trim();
6
- if (normalized === 'guard off') {
7
- setGuardEnabled(false);
8
- console.error('🛡️ Guard disabled — pretool validators will be skipped');
9
- process.exit(2);
10
- }
11
- if (normalized === 'guard on') {
12
- setGuardEnabled(true);
13
- console.error('🛡️ Guard enabled — pretool validators active');
14
- process.exit(2);
15
- }
16
- return null;
17
- }, () => '{}');
6
+ export async function main() {
7
+ runHook((input) => {
8
+ const normalized = (input.prompt ?? '').toLowerCase().trim();
9
+ if (normalized === 'guard off') {
10
+ setGuardEnabled(false);
11
+ console.error('🛡️ Guard disabled — pretool validators will be skipped');
12
+ process.exit(2);
13
+ }
14
+ if (normalized === 'guard on') {
15
+ setGuardEnabled(true);
16
+ console.error('🛡️ Guard enabled — pretool validators active');
17
+ process.exit(2);
18
+ }
19
+ return null;
20
+ }, () => '{}');
21
+ }
22
+ if (process.argv[1] &&
23
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
24
+ void main();
25
+ }
18
26
  //# sourceMappingURL=index.js.map
@@ -14,4 +14,5 @@ export declare function shouldLintFile(input: ToolInput): boolean;
14
14
  */
15
15
  export declare function lintFile(filePath: string, _projectDir: string): boolean;
16
16
  export declare function processPostToolUse(input: ToolInput, projectDir: string): boolean;
17
+ export declare function main(): Promise<void>;
17
18
  //# sourceMappingURL=lint-after-edit.d.ts.map
@@ -46,12 +46,15 @@ export function processPostToolUse(input, projectDir) {
46
46
  const filePath = input.tool_input.file_path;
47
47
  return lintFile(filePath, projectDir);
48
48
  }
49
- if (process.argv[1] &&
50
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
49
+ export async function main() {
51
50
  runHook((input) => {
52
51
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
53
52
  processPostToolUse(input, projectDir);
54
53
  return null;
55
54
  }, () => '{}');
56
55
  }
56
+ if (process.argv[1] &&
57
+ realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
58
+ void main();
59
+ }
57
60
  //# sourceMappingURL=lint-after-edit.js.map
@@ -46,7 +46,7 @@ export function validateFileConventions(input) {
46
46
  if (nonCanonicalPlanningViolation) {
47
47
  return { validator: 'file-conventions', passed: false, message: nonCanonicalPlanningViolation };
48
48
  }
49
- const blueprintPathViolation = getBlueprintPathViolation(normalized);
49
+ const blueprintPathViolation = getBlueprintPathViolation(normalized, undefined, input.cwd);
50
50
  if (blueprintPathViolation) {
51
51
  return { validator: 'file-conventions', passed: false, message: blueprintPathViolation };
52
52
  }
@@ -29,6 +29,11 @@ interface BlockedScriptSpec {
29
29
  category: CommandCategory;
30
30
  suggestion: string;
31
31
  }
32
+ interface BlockedRawNodeModulesToolSpec {
33
+ modulePath: string;
34
+ category: CommandCategory;
35
+ suggestion: string;
36
+ }
32
37
  interface RedirectOptions {
33
38
  mcpReady?: boolean;
34
39
  mcp?: MCPRedirectConfig;
@@ -39,6 +44,7 @@ export declare const AUDIT_MODE_ENV = "FORBIDDEN_COMMANDS_AUDIT";
39
44
  export declare const DOCS_REF = "AGENTS.md \"Forbidden Commands (CRITICAL)\" section";
40
45
  export declare const BLOCKED_TOOLS: BlockedToolSpec[];
41
46
  export declare const BLOCKED_SCRIPTS: BlockedScriptSpec[];
47
+ export declare const BLOCKED_RAW_NODE_MODULE_TOOLS: BlockedRawNodeModulesToolSpec[];
42
48
  export declare function generateRules(): CommandRule[];
43
49
  export declare const COMMAND_RULES: CommandRule[];
44
50
  export declare const SUGGESTION_MODIFIERS: SuggestionModifier[];
@@ -19,8 +19,18 @@ const TYPECHECK_HINT = 'wp_typecheck MCP tool with package/file scope';
19
19
  const E2E_HINT = 'wp_e2e MCP tool';
20
20
  const ENV_HINT = 'Use the repo-approved environment wrapper for secret-bearing commands';
21
21
  const TASK_TARGET_HINT = 'Use the repo-approved vp facade or MCP tool instead of raw execution';
22
- const EXEC_RUNNERS = ['vp exec'];
23
- const DIRECT_RUNNERS = ['vp'];
22
+ const EXEC_RUNNERS = [
23
+ 'vp exec',
24
+ 'pnpm exec',
25
+ 'npm exec',
26
+ 'npm exec --',
27
+ 'npx',
28
+ 'pnpx',
29
+ 'yarn exec',
30
+ 'yarn dlx',
31
+ 'bunx',
32
+ ];
33
+ const DIRECT_RUNNERS = ['vp', 'pnpm', 'yarn', 'yarnpkg'];
24
34
  const SCRIPT_RUNNERS = ['vp run', 'vp', 'pnpm', 'pnpm run', 'just'];
25
35
  export const BLOCKED_TOOLS = [
26
36
  {
@@ -59,6 +69,11 @@ export const BLOCKED_SCRIPTS = [
59
69
  { script: 'e2e', category: 'e2e', suggestion: E2E_HINT },
60
70
  { script: 'qa', category: 'unknown', suggestion: QA_HINT },
61
71
  ];
72
+ export const BLOCKED_RAW_NODE_MODULE_TOOLS = [
73
+ { modulePath: 'vitest/vitest.mjs', category: 'test', suggestion: TEST_HINT },
74
+ { modulePath: 'typescript/bin/tsc', category: 'typecheck', suggestion: TYPECHECK_HINT },
75
+ { modulePath: 'oxlint/bin/oxlint', category: 'lint', suggestion: LINT_HINT },
76
+ ];
62
77
  function escapeRegex(s) {
63
78
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
64
79
  }
@@ -66,6 +81,9 @@ function buildToolPattern(prefix, tool) {
66
81
  const escaped = prefix ? `${escapeRegex(prefix)} ${escapeRegex(tool)}` : escapeRegex(tool);
67
82
  return new RegExp(`^${escaped}(\\s|$)`);
68
83
  }
84
+ function buildRawNodeModulesToolPattern(modulePath) {
85
+ return new RegExp(`^node\\s+(?:\\.\\/)?node_modules\\/${escapeRegex(modulePath)}(?:\\s|$)`);
86
+ }
69
87
  export function generateRules() {
70
88
  const rules = [];
71
89
  for (const spec of BLOCKED_TOOLS) {
@@ -104,6 +122,13 @@ export function generateRules() {
104
122
  });
105
123
  }
106
124
  }
125
+ for (const spec of BLOCKED_RAW_NODE_MODULE_TOOLS) {
126
+ rules.push({
127
+ pattern: buildRawNodeModulesToolPattern(spec.modulePath),
128
+ category: spec.category,
129
+ suggestion: spec.suggestion,
130
+ });
131
+ }
107
132
  rules.push({
108
133
  pattern: /^vp exec markdownlint-cli2\b/,
109
134
  category: 'unknown',
@@ -11,5 +11,6 @@ export declare function getNonCanonicalPlanningPathViolation(filePath: string, b
11
11
  * accepted blueprints root layout (or the explicitly provided root).
12
12
  */
13
13
  export declare function isCanonicalBlueprintOverviewPath(filePath: string, blueprintsRoot?: string): boolean;
14
- export declare function getBlueprintPathViolation(filePath: string, blueprintsRoot?: string): string | null;
14
+ export declare function isCanonicalBlueprintDocumentPath(filePath: string, blueprintsRoot?: string): boolean;
15
+ export declare function getBlueprintPathViolation(filePath: string, blueprintsRoot?: string, cwd?: string): string | null;
15
16
  //# sourceMappingURL=path-contract.d.ts.map
@@ -1,16 +1,10 @@
1
+ import { existsSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { BLUEPRINT_OVERVIEW_FILENAME, getBlueprintAlternateDocumentPath, isBlueprintSupportingMarkdownRelativePath, isBlueprintStatus, parseBlueprintDocumentRelativePath, } from '#utils/document-paths.js';
1
4
  export const BLUEPRINTS_ROOT = 'webpresso/blueprints';
2
5
  const DEFAULT_BLUEPRINTS_ROOT = 'blueprints';
3
6
  export const TECH_DEBT_ROOT = 'webpresso/tech-debt';
4
7
  const DEFAULT_TECH_DEBT_ROOT = 'tech-debt';
5
- const BLUEPRINT_STATUSES = new Set([
6
- 'draft',
7
- 'planned',
8
- 'parked',
9
- 'in-progress',
10
- 'completed',
11
- 'archived',
12
- ]);
13
- const KEBAB_CASE_SEGMENT = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
8
  // Both canonical blueprint-root layouts accepted by default.
15
9
  const CANONICAL_BLUEPRINTS_ROOTS = [BLUEPRINTS_ROOT, DEFAULT_BLUEPRINTS_ROOT];
16
10
  const CANONICAL_TECH_DEBT_ROOTS = [TECH_DEBT_ROOT, DEFAULT_TECH_DEBT_ROOT];
@@ -20,6 +14,16 @@ function normalizePlanningPath(filePath) {
20
14
  function matchesRoot(normalized, root) {
21
15
  return normalized === root || normalized.startsWith(`${root}/`);
22
16
  }
17
+ function stripBlueprintRoot(normalized, roots) {
18
+ for (const root of roots) {
19
+ if (normalized === root)
20
+ return { relativePath: '', root };
21
+ if (normalized.startsWith(`${root}/`)) {
22
+ return { relativePath: normalized.slice(root.length + 1), root };
23
+ }
24
+ }
25
+ return null;
26
+ }
23
27
  /**
24
28
  * Returns true if the path is under any accepted blueprints root.
25
29
  * Pass `blueprintsRoot` to restrict to a single configured root.
@@ -66,39 +70,60 @@ export function getNonCanonicalPlanningPathViolation(filePath, blueprintsRoot, t
66
70
  * accepted blueprints root layout (or the explicitly provided root).
67
71
  */
68
72
  export function isCanonicalBlueprintOverviewPath(filePath, blueprintsRoot) {
73
+ const parsed = getCanonicalBlueprintDocument(filePath, blueprintsRoot);
74
+ return parsed?.shape === 'folder';
75
+ }
76
+ export function isCanonicalBlueprintDocumentPath(filePath, blueprintsRoot) {
77
+ return getCanonicalBlueprintDocument(filePath, blueprintsRoot) !== null;
78
+ }
79
+ function getCanonicalBlueprintDocument(filePath, blueprintsRoot) {
69
80
  const normalized = normalizePlanningPath(filePath);
70
81
  const roots = blueprintsRoot ? [blueprintsRoot] : CANONICAL_BLUEPRINTS_ROOTS;
71
- return roots.some((root) => {
72
- const rootParts = root.split('/');
73
- const parts = normalized.split('/');
74
- const n = rootParts.length;
75
- return (parts.length === n + 3 &&
76
- parts.slice(0, n).join('/') === root &&
77
- BLUEPRINT_STATUSES.has(parts[n] ?? '') &&
78
- KEBAB_CASE_SEGMENT.test(parts[n + 1] ?? '') &&
79
- parts[n + 2] === '_overview.md');
80
- });
82
+ const stripped = stripBlueprintRoot(normalized, roots);
83
+ return stripped ? parseBlueprintDocumentRelativePath(stripped.relativePath) : null;
81
84
  }
82
- export function getBlueprintPathViolation(filePath, blueprintsRoot) {
85
+ export function getBlueprintPathViolation(filePath, blueprintsRoot, cwd = process.cwd()) {
83
86
  const normalized = normalizePlanningPath(filePath);
84
87
  if (!isBlueprintPath(normalized, blueprintsRoot))
85
88
  return null;
86
- if (normalized.endsWith('/_overview.md') &&
87
- !isCanonicalBlueprintOverviewPath(normalized, blueprintsRoot)) {
88
- const root = blueprintsRoot ?? BLUEPRINTS_ROOT;
89
- return `Blueprint overview files must live at ${root}/<status>/<slug>/_overview.md. Got: ${normalized}`;
90
- }
91
89
  const roots = blueprintsRoot ? [blueprintsRoot] : CANONICAL_BLUEPRINTS_ROOTS;
92
- for (const root of roots) {
93
- const rootParts = root.split('/');
94
- const parts = normalized.split('/');
95
- const n = rootParts.length;
96
- if (parts.length === n + 2 &&
97
- parts.slice(0, n).join('/') === root &&
98
- BLUEPRINT_STATUSES.has(parts[n] ?? '') &&
99
- normalized.endsWith('.md')) {
100
- return `Blueprint markdown files cannot live directly under a status directory. Move this file to ${root}/${parts[n]}/<slug>/_overview.md or place supporting docs inside ${root}/${parts[n]}/<slug>/. Got: ${normalized}`;
90
+ const stripped = stripBlueprintRoot(normalized, roots);
91
+ if (!stripped) {
92
+ return null;
93
+ }
94
+ const parsed = parseBlueprintDocumentRelativePath(stripped.relativePath);
95
+ if (parsed) {
96
+ const blueprintRoot = path.isAbsolute(filePath)
97
+ ? path.join(path.parse(filePath).root, stripped.root)
98
+ : path.join(cwd, stripped.root);
99
+ const currentPath = path.isAbsolute(filePath) ? filePath : path.join(cwd, normalized);
100
+ const alternate = getBlueprintAlternateDocumentPath(blueprintRoot, currentPath);
101
+ if (alternate && existsSync(alternate)) {
102
+ return `Blueprint slug "${parsed.state}/${parsed.slug}" cannot exist in both flat and folder forms. Remove either ${path.relative(cwd, filePath).replace(/\\/g, '/')} or ${path.relative(cwd, alternate).replace(/\\/g, '/')}.`;
101
103
  }
104
+ return null;
105
+ }
106
+ const parts = stripped.relativePath.split('/').filter((segment) => segment.length > 0);
107
+ const [state, slug, doc] = parts;
108
+ const root = stripped.root;
109
+ if (parts.length === 2 &&
110
+ typeof doc === 'undefined' &&
111
+ typeof slug === 'string' &&
112
+ slug.endsWith('.md')) {
113
+ return `Blueprint markdown under ${root}/<status>/ must be either <slug>.md or <slug>/${BLUEPRINT_OVERVIEW_FILENAME}. Got: ${normalized}`;
114
+ }
115
+ if (parts.length === 3 && doc === BLUEPRINT_OVERVIEW_FILENAME) {
116
+ return `Blueprint overview files must live at ${root}/<status>/<slug>/${BLUEPRINT_OVERVIEW_FILENAME}. Got: ${normalized}`;
117
+ }
118
+ if (parts.length === 3 && isBlueprintSupportingMarkdownRelativePath(stripped.relativePath)) {
119
+ const canonicalOverviewPath = path.join(cwd, root, state ?? '', slug ?? '', BLUEPRINT_OVERVIEW_FILENAME);
120
+ if (!existsSync(canonicalOverviewPath)) {
121
+ return `Supporting blueprint markdown requires ${root}/${state}/${slug}/${BLUEPRINT_OVERVIEW_FILENAME}. Got: ${normalized}`;
122
+ }
123
+ return null;
124
+ }
125
+ if (parts.length >= 3 && isBlueprintStatus(state)) {
126
+ return `Blueprint markdown must use one of ${root}/<status>/<slug>.md or ${root}/<status>/<slug>/${BLUEPRINT_OVERVIEW_FILENAME}. Supporting markdown is only allowed beside ${BLUEPRINT_OVERVIEW_FILENAME}. Got: ${normalized}`;
102
127
  }
103
128
  return null;
104
129
  }
@@ -1,6 +1,6 @@
1
1
  import jsYaml from 'js-yaml';
2
2
  import { getContent, getFilePath } from '#hooks/shared/types';
3
- import { getNonCanonicalPlanningPathViolation, isBlueprintPath } from './path-contract.js';
3
+ import { getNonCanonicalPlanningPathViolation, isBlueprintPath, isCanonicalBlueprintDocumentPath, } from './path-contract.js';
4
4
  import { createSkipResult } from './skip-result.js';
5
5
  // Keep aligned with webpresso/blueprint planStatusSchema + plan type enum.
6
6
  const VALID_TYPES = ['blueprint', 'parent-roadmap'];
@@ -10,8 +10,8 @@ function shouldValidatePath(filePath) {
10
10
  const normalized = filePath.startsWith('/') ? filePath.slice(1) : filePath;
11
11
  const nonCanonicalPlanningPath = getNonCanonicalPlanningPathViolation(normalized);
12
12
  const currentPath = isBlueprintPath(normalized);
13
- const isOverviewFile = normalized.endsWith('/README.md') || normalized.endsWith('/_overview.md');
14
- return !nonCanonicalPlanningPath && currentPath && isOverviewFile;
13
+ const isCanonicalBlueprintDoc = isCanonicalBlueprintDocumentPath(normalized);
14
+ return !nonCanonicalPlanningPath && currentPath && isCanonicalBlueprintDoc;
15
15
  }
16
16
  export function extractFrontmatterBlock(content) {
17
17
  const match = content.match(/^---\n([\s\S]*?)\n---/);
@@ -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