@webpresso/agent-kit 0.28.0 → 0.29.1

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 (117) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/README.md +2 -2
  4. package/bin/_run.js +6 -0
  5. package/bin/wp +5 -0
  6. package/catalog/base-kit/.github/actions/setup-webpresso/action.yml.tmpl +21 -0
  7. package/catalog/base-kit/.github/workflows/{ci.webpresso.yml.tmpl → ci.yml.tmpl} +17 -7
  8. package/catalog/base-kit/tsconfig.json.tmpl +1 -1
  9. package/catalog/docs/templates/blueprint.yaml +1 -1
  10. package/dist/esm/audit/_budgets.d.ts +9 -1
  11. package/dist/esm/audit/_budgets.js +8 -1
  12. package/dist/esm/audit/blueprint-db-consistency.js +2 -2
  13. package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +17 -7
  14. package/dist/esm/audit/blueprint-lifecycle-sql.js +298 -48
  15. package/dist/esm/audit/blueprint-readme-drift.d.ts +6 -0
  16. package/dist/esm/audit/blueprint-readme-drift.js +110 -0
  17. package/dist/esm/audit/no-first-party-mjs.js +5 -4
  18. package/dist/esm/audit/package-surface.js +79 -10
  19. package/dist/esm/audit/repo-guardrails.d.ts +1 -1
  20. package/dist/esm/audit/repo-guardrails.js +43 -3
  21. package/dist/esm/audit/tech-debt-cadence.js +2 -3
  22. package/dist/esm/audit/toolchain-isolation.js +2 -3
  23. package/dist/esm/blueprint/core/parser.js +3 -2
  24. package/dist/esm/blueprint/core/schema.d.ts +3 -2
  25. package/dist/esm/blueprint/core/schema.js +1 -1
  26. package/dist/esm/blueprint/cross-repo/audit.js +3 -4
  27. package/dist/esm/blueprint/db/cold-start.js +2 -3
  28. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  29. package/dist/esm/blueprint/db/ephemeral-projection.d.ts +25 -0
  30. package/dist/esm/blueprint/db/ephemeral-projection.js +36 -0
  31. package/dist/esm/blueprint/db/gc.d.ts +11 -0
  32. package/dist/esm/blueprint/db/gc.js +55 -0
  33. package/dist/esm/blueprint/db/ingester.js +39 -1
  34. package/dist/esm/blueprint/db/migrations/run.js +5 -3
  35. package/dist/esm/blueprint/db/paths.d.ts +13 -24
  36. package/dist/esm/blueprint/db/paths.js +25 -33
  37. package/dist/esm/blueprint/execution/progress-bridge.js +5 -4
  38. package/dist/esm/blueprint/freshness.d.ts +2 -0
  39. package/dist/esm/blueprint/freshness.js +3 -1
  40. package/dist/esm/blueprint/lifecycle/audit.js +6 -6
  41. package/dist/esm/blueprint/lifecycle/engine.d.ts +1 -1
  42. package/dist/esm/blueprint/lifecycle/engine.js +13 -9
  43. package/dist/esm/blueprint/lifecycle/transition-matrix.d.ts +5 -0
  44. package/dist/esm/blueprint/lifecycle/transition-matrix.js +20 -0
  45. package/dist/esm/blueprint/markdown/helpers.d.ts +1 -1
  46. package/dist/esm/blueprint/projection-ready.js +2 -0
  47. package/dist/esm/blueprint/service/BlueprintService.js +1 -1
  48. package/dist/esm/blueprint/service/blueprint-records.js +1 -1
  49. package/dist/esm/blueprint/tracked-document/parser.js +1 -1
  50. package/dist/esm/blueprint/utils/archive.d.ts +2 -2
  51. package/dist/esm/blueprint/utils/archive.js +5 -2
  52. package/dist/esm/blueprint/utils/package-assets.d.ts +13 -0
  53. package/dist/esm/blueprint/utils/package-assets.js +38 -6
  54. package/dist/esm/build/normalize-tsconfig-json-exports.d.ts +13 -0
  55. package/dist/esm/build/normalize-tsconfig-json-exports.js +39 -0
  56. package/dist/esm/build/package-manifest.js +12 -4
  57. package/dist/esm/build/release-policy.d.ts +9 -18
  58. package/dist/esm/build/release-policy.js +10 -19
  59. package/dist/esm/build/runtime-surface-policy.d.ts +14 -0
  60. package/dist/esm/build/runtime-surface-policy.js +13 -0
  61. package/dist/esm/cli/commands/audit-core.d.ts +2 -2
  62. package/dist/esm/cli/commands/audit.js +7 -3
  63. package/dist/esm/cli/commands/blueprint/db-commands.js +0 -3
  64. package/dist/esm/cli/commands/blueprint/mutations.d.ts +3 -2
  65. package/dist/esm/cli/commands/blueprint/mutations.js +45 -39
  66. package/dist/esm/cli/commands/blueprint/router-output.js +2 -2
  67. package/dist/esm/cli/commands/doctor.d.ts +1 -1
  68. package/dist/esm/cli/commands/doctor.js +4 -5
  69. package/dist/esm/cli/commands/init/config.d.ts +6 -10
  70. package/dist/esm/cli/commands/init/config.js +36 -20
  71. package/dist/esm/cli/commands/init/gitignore-patcher.js +0 -1
  72. package/dist/esm/cli/commands/init/index.d.ts +8 -1
  73. package/dist/esm/cli/commands/init/index.js +17 -19
  74. package/dist/esm/cli/commands/init/package-root.d.ts +20 -0
  75. package/dist/esm/cli/commands/init/package-root.js +110 -0
  76. package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
  77. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +3 -0
  78. package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +8 -24
  79. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +9 -0
  80. package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +79 -1
  81. package/dist/esm/cli/commands/init/scaffolders/claude-rules/index.js +2 -12
  82. package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +2 -12
  83. package/dist/esm/config/tsconfig/cloudflare.json +1 -1
  84. package/dist/esm/config/tsconfig/library.json +1 -1
  85. package/dist/esm/config/tsconfig/react-library.json +3 -2
  86. package/dist/esm/config/tsconfig/react-router.json +1 -1
  87. package/dist/esm/dev/restore-dev-links/index.js +3 -4
  88. package/dist/esm/docs-linter/blueprint-plan.js +46 -4
  89. package/dist/esm/hooks/check-dev-link/index.js +3 -4
  90. package/dist/esm/hooks/doctor.d.ts +11 -0
  91. package/dist/esm/hooks/doctor.js +174 -30
  92. package/dist/esm/hooks/guard-switch/index.js +3 -5
  93. package/dist/esm/hooks/post-tool/lint-after-edit.js +4 -5
  94. package/dist/esm/hooks/pretool-guard/index.js +2 -4
  95. package/dist/esm/hooks/pretool-guard/runner.js +2 -4
  96. package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +47 -6
  97. package/dist/esm/hooks/sessionstart/index.js +3 -4
  98. package/dist/esm/hooks/shared/direct-entrypoint.d.ts +10 -0
  99. package/dist/esm/hooks/shared/direct-entrypoint.js +21 -0
  100. package/dist/esm/hooks/stop/qa-changed-files.js +3 -5
  101. package/dist/esm/hooks/test-quality-check.js +3 -4
  102. package/dist/esm/mcp/blueprint-server.js +26 -3
  103. package/dist/esm/mcp/cli.js +2 -6
  104. package/dist/esm/mcp/server.d.ts +2 -0
  105. package/dist/esm/mcp/server.js +18 -3
  106. package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
  107. package/dist/esm/mcp/tools/_shared/audit-kinds.js +1 -0
  108. package/dist/esm/mcp/tools/audit.d.ts +2 -1
  109. package/dist/esm/mcp/tools/audit.js +13 -3
  110. package/dist/esm/package.json +2 -0
  111. package/package.json +24 -15
  112. package/tsconfig/cloudflare.json +1 -1
  113. package/tsconfig/library.json +1 -1
  114. package/tsconfig/react-library.json +3 -2
  115. package/tsconfig/react-router.json +1 -1
  116. package/dist/esm/blueprint/db/legacy-migration.d.ts +0 -41
  117. package/dist/esm/blueprint/db/legacy-migration.js +0 -122
@@ -28,8 +28,19 @@ export interface RunHooksDoctorOptions {
28
28
  /** Override the working directory used to detect RTK marker files. Defaults to process.cwd(). */
29
29
  cwd?: string;
30
30
  }
31
+ export interface ResolvePackageRootForRuntimeOptions {
32
+ readonly moduleUrl?: string;
33
+ readonly execPath?: string;
34
+ readonly argv0?: string;
35
+ readonly argv1?: string;
36
+ readonly pathEnv?: string;
37
+ readonly pathExtEnv?: string;
38
+ readonly platform?: NodeJS.Platform;
39
+ }
40
+ export declare function resolvePackageRootForRuntime(options?: ResolvePackageRootForRuntimeOptions): string | null;
31
41
  export declare function findOwningPackageRoot(startDir: string): string | null;
32
42
  export declare function checkRtkOnPath(cwd?: string): Promise<DoctorCheck | null>;
43
+ export declare function checkNativePluginRuntime(): DoctorCheck;
33
44
  /**
34
45
  * Verify the consumer's `.claude/settings.json` carries the managed agent-kit
35
46
  * hook launchers. Since the hooks are single-sourced there (not in the plugin
@@ -9,11 +9,11 @@
9
9
  * - MCP server starts and responds to tools/list (soft-fail)
10
10
  * - installed host CLIs (Codex/OpenCode/Claude) can see the expected surfaces
11
11
  */
12
- import { accessSync, constants, readFileSync, statSync } from 'node:fs';
12
+ import { accessSync, constants, lstatSync, readFileSync, statSync } from 'node:fs';
13
13
  import { spawn } from 'node:child_process';
14
14
  import { platform } from 'node:os';
15
- import { dirname, join, resolve } from 'node:path';
16
- import { fileURLToPath } from 'node:url';
15
+ import { join, resolve } from 'node:path';
16
+ import { findAgentKitPackageRoot, resolveAgentKitPackageRoot, } from '#cli/commands/init/package-root';
17
17
  import { STATE_FILE_RELATIVE_PATH, readDevLinkState } from '#dev/dev-link-state';
18
18
  import { detectDevLinkBreakage, formatBreakageMessage } from '#hooks/check-dev-link/index';
19
19
  import { isMcpReady } from './shared/mcp-sentinel.js';
@@ -30,35 +30,13 @@ const HOOK_BINS = [
30
30
  { name: 'test-quality-check', binName: 'wp-test-quality-check', checkStdin: false },
31
31
  ];
32
32
  function resolvePackageRoot() {
33
- return findOwningPackageRoot(dirname(fileURLToPath(import.meta.url)));
33
+ return resolvePackageRootForRuntime();
34
34
  }
35
- export function findOwningPackageRoot(startDir) {
36
- let dir = startDir;
37
- let fallback = null;
38
- while (dir !== dirname(dir)) {
39
- if (tryAccess(join(dir, 'package.json'))) {
40
- if (fallback === null)
41
- fallback = dir;
42
- if (isOwningPackageRoot(dir))
43
- return dir;
44
- }
45
- dir = dirname(dir);
46
- }
47
- return fallback;
35
+ export function resolvePackageRootForRuntime(options = {}) {
36
+ return resolveAgentKitPackageRoot(options);
48
37
  }
49
- function isOwningPackageRoot(dir) {
50
- try {
51
- const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'));
52
- if (typeof pkg.bin?.['wp'] === 'string')
53
- return true;
54
- }
55
- catch {
56
- // Ignore malformed package.json here and fall back to structural markers below.
57
- }
58
- return (tryAccess(join(dir, '.claude-plugin', 'plugin.json')) ||
59
- tryAccess(join(dir, 'bin', 'wp.js')) ||
60
- tryAccess(join(dir, 'src', 'cli', 'cli.ts')) ||
61
- tryAccess(join(dir, 'dist', 'esm', 'cli', 'cli.js')));
38
+ export function findOwningPackageRoot(startDir) {
39
+ return findAgentKitPackageRoot(startDir);
62
40
  }
63
41
  function resolveHookBin(binName) {
64
42
  try {
@@ -371,6 +349,171 @@ function checkPluginJson() {
371
349
  return { ok: false, detail: `failed to read plugin.json: ${String(err)}` };
372
350
  }
373
351
  }
352
+ function formatNativeRuntimeDetail(status) {
353
+ return [
354
+ `launchMode=${status.launchMode}`,
355
+ status.targetId ? `targetId=${status.targetId}` : null,
356
+ status.manifestPath ? `manifest=${status.manifestPath}` : null,
357
+ status.stagedBinPath ? `stagedBin=${status.stagedBinPath}` : null,
358
+ status.runtimeTargetPath ? `targetBin=${status.runtimeTargetPath}` : null,
359
+ status.reason ? `reason=${status.reason}` : null,
360
+ ]
361
+ .filter((value) => value !== null)
362
+ .join(', ');
363
+ }
364
+ export function checkNativePluginRuntime() {
365
+ const root = resolvePluginRoot();
366
+ if (!root) {
367
+ return {
368
+ name: 'native plugin runtime',
369
+ ok: false,
370
+ detail: formatNativeRuntimeDetail({
371
+ launchMode: 'missing',
372
+ reason: 'plugin root not found',
373
+ }),
374
+ };
375
+ }
376
+ const pluginJsonPath = join(root, '.claude-plugin', 'plugin.json');
377
+ const manifestPath = join(root, 'bin', 'runtime-manifest.json');
378
+ const stagedBinPath = join(root, 'bin', 'wp');
379
+ if (!tryAccess(pluginJsonPath)) {
380
+ return {
381
+ name: 'native plugin runtime',
382
+ ok: false,
383
+ detail: formatNativeRuntimeDetail({
384
+ launchMode: 'missing',
385
+ manifestPath,
386
+ stagedBinPath,
387
+ reason: 'plugin manifest missing',
388
+ }),
389
+ };
390
+ }
391
+ try {
392
+ const pluginManifest = JSON.parse(readFileSync(pluginJsonPath, 'utf-8'));
393
+ const server = pluginManifest.mcpServers?.webpresso;
394
+ const launchMode = server?.command === '${CLAUDE_PLUGIN_ROOT}/bin/wp' &&
395
+ Array.isArray(server.args) &&
396
+ server.args.length === 1 &&
397
+ server.args[0] === 'mcp'
398
+ ? 'native'
399
+ : server?.command === 'node' ||
400
+ (server?.args ?? []).some((arg) => arg.endsWith('wp.js'))
401
+ ? 'legacy-js'
402
+ : server
403
+ ? 'custom'
404
+ : 'missing';
405
+ if (!tryAccess(manifestPath)) {
406
+ return {
407
+ name: 'native plugin runtime',
408
+ ok: false,
409
+ detail: formatNativeRuntimeDetail({
410
+ launchMode,
411
+ manifestPath,
412
+ stagedBinPath,
413
+ reason: 'runtime manifest missing',
414
+ }),
415
+ };
416
+ }
417
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
418
+ const target = manifest.targets?.find((candidate) => candidate.os === process.platform && candidate.cpu === process.arch);
419
+ const targetId = target?.id;
420
+ const targetFilename = target?.os === 'win32' ? `${manifest.binaryName ?? 'wp'}.exe` : manifest.binaryName ?? 'wp';
421
+ const runtimeTargetPath = targetId
422
+ ? join(root, 'bin', 'runtime', targetId, targetFilename)
423
+ : undefined;
424
+ if (!targetId || !runtimeTargetPath) {
425
+ return {
426
+ name: 'native plugin runtime',
427
+ ok: false,
428
+ detail: formatNativeRuntimeDetail({
429
+ launchMode,
430
+ manifestPath,
431
+ stagedBinPath,
432
+ reason: `no runtime target for ${process.platform}/${process.arch}`,
433
+ }),
434
+ };
435
+ }
436
+ if (!tryAccess(stagedBinPath)) {
437
+ return {
438
+ name: 'native plugin runtime',
439
+ ok: false,
440
+ detail: formatNativeRuntimeDetail({
441
+ launchMode,
442
+ targetId,
443
+ manifestPath,
444
+ stagedBinPath,
445
+ runtimeTargetPath,
446
+ reason: 'staged native launcher missing',
447
+ }),
448
+ };
449
+ }
450
+ if (lstatSync(stagedBinPath).isSymbolicLink()) {
451
+ return {
452
+ name: 'native plugin runtime',
453
+ ok: false,
454
+ detail: formatNativeRuntimeDetail({
455
+ launchMode,
456
+ targetId,
457
+ manifestPath,
458
+ stagedBinPath,
459
+ runtimeTargetPath,
460
+ reason: 'staged native launcher is a symlink',
461
+ }),
462
+ };
463
+ }
464
+ if (!tryAccess(runtimeTargetPath)) {
465
+ return {
466
+ name: 'native plugin runtime',
467
+ ok: false,
468
+ detail: formatNativeRuntimeDetail({
469
+ launchMode,
470
+ targetId,
471
+ manifestPath,
472
+ stagedBinPath,
473
+ runtimeTargetPath,
474
+ reason: 'target runtime binary missing',
475
+ }),
476
+ };
477
+ }
478
+ if (launchMode !== 'native') {
479
+ return {
480
+ name: 'native plugin runtime',
481
+ ok: false,
482
+ detail: formatNativeRuntimeDetail({
483
+ launchMode,
484
+ targetId,
485
+ manifestPath,
486
+ stagedBinPath,
487
+ runtimeTargetPath,
488
+ reason: 'plugin manifest is not using the native launcher',
489
+ }),
490
+ };
491
+ }
492
+ return {
493
+ name: 'native plugin runtime',
494
+ ok: true,
495
+ detail: formatNativeRuntimeDetail({
496
+ launchMode,
497
+ targetId,
498
+ manifestPath,
499
+ stagedBinPath,
500
+ runtimeTargetPath,
501
+ }),
502
+ };
503
+ }
504
+ catch (error) {
505
+ return {
506
+ name: 'native plugin runtime',
507
+ ok: false,
508
+ detail: formatNativeRuntimeDetail({
509
+ launchMode: 'missing',
510
+ manifestPath,
511
+ stagedBinPath,
512
+ reason: error instanceof Error ? error.message : String(error),
513
+ }),
514
+ };
515
+ }
516
+ }
374
517
  async function checkMcpServer() {
375
518
  if (isMcpReady()) {
376
519
  return { ok: true, detail: 'MCP server already running (sentinel found)', skipped: true };
@@ -643,6 +786,7 @@ export async function runHooksDoctor(opts = {}) {
643
786
  }
644
787
  checks.push(checkConsumerCodexHookPaths(opts.cwd));
645
788
  checks.push({ name: 'plugin.json integrity', ...checkPluginJson() });
789
+ checks.push({ advisory: true, ...checkNativePluginRuntime() });
646
790
  checks.push({
647
791
  name: 'managed hooks installed (.claude/settings.json)',
648
792
  advisory: true,
@@ -1,10 +1,9 @@
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';
5
3
  import { setGuardEnabled } from './state.js';
4
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
6
5
  export async function main() {
7
- runHook((input) => {
6
+ await runHook((input) => {
8
7
  const normalized = (input.prompt ?? '').toLowerCase().trim();
9
8
  if (normalized === 'guard off') {
10
9
  setGuardEnabled(false);
@@ -19,8 +18,7 @@ export async function main() {
19
18
  return null;
20
19
  }, () => '{}');
21
20
  }
22
- if (process.argv[1] &&
23
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
21
+ if (isDirectEntrypoint(import.meta.url)) {
24
22
  void main();
25
23
  }
26
24
  //# sourceMappingURL=index.js.map
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env bun
2
- import { existsSync, realpathSync } from 'node:fs';
2
+ import { existsSync } from 'node:fs';
3
3
  import { extname } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
4
  import { runHook } from '#hooks/shared/hook-bootstrap';
6
5
  import { getFilePath } from '#hooks/shared/types';
6
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
7
7
  export const LINTABLE_EXTENSIONS = ['.ts', '.tsx', '.js', '.jsx', '.json', '.css'];
8
8
  export const SKIP_PATTERNS = [
9
9
  /\/node_modules\//,
@@ -47,14 +47,13 @@ export function processPostToolUse(input, projectDir) {
47
47
  return lintFile(filePath, projectDir);
48
48
  }
49
49
  export async function main() {
50
- runHook((input) => {
50
+ await runHook((input) => {
51
51
  const projectDir = process.env.CLAUDE_PROJECT_DIR || process.cwd();
52
52
  processPostToolUse(input, projectDir);
53
53
  return null;
54
54
  }, () => '{}');
55
55
  }
56
- if (process.argv[1] &&
57
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
56
+ if (isDirectEntrypoint(import.meta.url)) {
58
57
  void main();
59
58
  }
60
59
  //# sourceMappingURL=lint-after-edit.js.map
@@ -1,11 +1,9 @@
1
1
  #!/usr/bin/env bun
2
- import { realpathSync } from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
2
  import { main as runMain } from './runner.js';
3
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
5
4
  export { getTarget, getToolType, handleParseError, logValidationResult, main, processValidation, runAllValidators, } from './runner.js';
6
5
  export { VALIDATORS } from './validators/index.js';
7
- if (process.argv[1] &&
8
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
6
+ if (isDirectEntrypoint(import.meta.url)) {
9
7
  runMain();
10
8
  }
11
9
  //# sourceMappingURL=index.js.map
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { realpathSync } from 'node:fs';
3
- import { fileURLToPath } from 'node:url';
4
2
  import { isGuardEnabled } from '#hooks/guard-switch/state';
5
3
  import { readStdinJson, suppressStderr } from '#hooks/shared/hook-bootstrap';
6
4
  import { getCommand, getFilePath, isBashInput, parseToolInput } from '#hooks/shared/types';
7
5
  import { logRun } from './logger.js';
8
6
  import { extractRoutableCommandsFromToolInput, routeCommand } from './dev-routing.js';
9
7
  import { VALIDATORS } from './validators/index.js';
8
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
10
9
  const RED = '\x1b[31m';
11
10
  const YELLOW = '\x1b[33m';
12
11
  const DIM = '\x1b[2m';
@@ -147,8 +146,7 @@ export async function main() {
147
146
  handleParseError(error, inputJson);
148
147
  }
149
148
  }
150
- if (process.argv[1] &&
151
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
149
+ if (isDirectEntrypoint(import.meta.url)) {
152
150
  main();
153
151
  }
154
152
  //# sourceMappingURL=runner.js.map
@@ -1,4 +1,5 @@
1
1
  import { readConfig } from '#cli/commands/init/config';
2
+ import { getLegalLifecycleTargets, isLegalLifecycleTransition, parseLifecycleBlueprintStatus, } from '#lifecycle/transition-matrix.js';
2
3
  import { getCommand, isBashInput } from '#hooks/shared/types';
3
4
  import { AUDIT_KINDS } from '#mcp/tools/_shared/audit-kinds';
4
5
  import { createSkipResult } from './skip-result.js';
@@ -9,7 +10,12 @@ export const AUDIT_MODE_ENV = 'FORBIDDEN_COMMANDS_AUDIT';
9
10
  export const DOCS_REF = 'AGENTS.md "Forbidden Commands (CRITICAL)" section';
10
11
  const DB_HINT = 'Use the database MCP/tooling entrypoint instead of direct CLI execution';
11
12
  const BLUEPRINT_HINT = 'wp blueprint new|list|audit — use wp_blueprint MCP tool for lifecycle transitions';
12
- const BLUEPRINT_LIFECYCLE_DIRS = '(draft|planned|in-progress|completed|archived)';
13
+ const BLUEPRINT_LIFECYCLE_DIRS = '(draft|planned|in-progress|parked|completed|archived)';
14
+ const BLUEPRINT_GIT_MV_RULE = {
15
+ pattern: /^git\s+mv\b/,
16
+ category: 'blueprint',
17
+ suggestion: BLUEPRINT_HINT,
18
+ };
13
19
  const LINT_BASE = 'wp_lint MCP tool with package/file scope';
14
20
  const LINT_HINT = `${LINT_BASE} [--fix] [--fix-unsafe]`;
15
21
  const FORMAT_HINT = 'wp_format MCP tool';
@@ -142,11 +148,6 @@ export function generateRules() {
142
148
  pattern: new RegExp(`^mkdir\\b.*blueprints\\/${BLUEPRINT_LIFECYCLE_DIRS}`),
143
149
  category: 'blueprint',
144
150
  suggestion: BLUEPRINT_HINT,
145
- }, {
146
- // git mv is an alternative to bare mv for tracked files — same bypass vector.
147
- pattern: new RegExp(`^git\\s+mv\\b.*blueprints\\/${BLUEPRINT_LIFECYCLE_DIRS}`),
148
- category: 'blueprint',
149
- suggestion: BLUEPRINT_HINT,
150
151
  }, { pattern: /^doppler run/, category: 'unknown', suggestion: ENV_HINT }, { pattern: /^DATABASE_URL=/, category: 'unknown', suggestion: ENV_HINT }, { pattern: /^vp exec\b/, category: 'unknown', suggestion: TASK_TARGET_HINT }, { pattern: /^vp run\b/, category: 'unknown', suggestion: TASK_TARGET_HINT });
151
152
  return rules;
152
153
  }
@@ -342,6 +343,34 @@ export function splitTopLevelCommands(command) {
342
343
  segments.push(last);
343
344
  return segments;
344
345
  }
346
+ function splitShellArgs(command) {
347
+ return command.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
348
+ }
349
+ function unquoteShellArg(arg) {
350
+ return arg.replace(/^['"]|['"]$/g, '');
351
+ }
352
+ function extractBlueprintLifecycleStatusFromPathArg(arg) {
353
+ const normalized = unquoteShellArg(arg).replace(/\\/g, '/');
354
+ const match = normalized.match(/(?:^|\/)blueprints\/(draft|planned|in-progress|parked|completed|archived)(?:\/|$)/);
355
+ return parseLifecycleBlueprintStatus(match?.[1] ?? '');
356
+ }
357
+ function findIllegalBlueprintGitMv(command) {
358
+ for (const segment of splitTopLevelCommands(command.trim())) {
359
+ if (!/^git\s+mv\b/.test(segment))
360
+ continue;
361
+ const args = splitShellArgs(segment);
362
+ if (args.length < 4)
363
+ continue;
364
+ const from = extractBlueprintLifecycleStatusFromPathArg(args[2] ?? '');
365
+ const to = extractBlueprintLifecycleStatusFromPathArg(args[3] ?? '');
366
+ if (!from || !to)
367
+ continue;
368
+ if (isLegalLifecycleTransition(from, to))
369
+ return null;
370
+ return { segment, from, to };
371
+ }
372
+ return null;
373
+ }
345
374
  export function findMatchingRule(command) {
346
375
  for (const variant of getCommandVariants(command)) {
347
376
  const rule = COMMAND_RULES.find((r) => r.pattern.test(variant));
@@ -505,6 +534,18 @@ export function validateForbiddenCommands(input) {
505
534
  const command = getCommand(input);
506
535
  if (!command)
507
536
  return createSkipResult(VALIDATOR_NAME, 'No command found');
537
+ const illegalBlueprintGitMv = findIllegalBlueprintGitMv(command);
538
+ if (illegalBlueprintGitMv) {
539
+ const decoratedRule = {
540
+ ...BLUEPRINT_GIT_MV_RULE,
541
+ suggestion: `${BLUEPRINT_HINT}. Illegal transition ${illegalBlueprintGitMv.from} → ` +
542
+ `${illegalBlueprintGitMv.to}; legal targets: ` +
543
+ `${getLegalLifecycleTargets(illegalBlueprintGitMv.from).join(', ') || '(none)'}`,
544
+ };
545
+ if (process.env[AUDIT_MODE_ENV] === '1')
546
+ return createAuditResult(illegalBlueprintGitMv.segment, decoratedRule);
547
+ return createBlockedResult(illegalBlueprintGitMv.segment, decoratedRule);
548
+ }
508
549
  const rule = findMatchingRule(command);
509
550
  if (rule) {
510
551
  if (process.env[AUDIT_MODE_ENV] === '1')
@@ -15,12 +15,12 @@
15
15
  * Always emits — never returns null. WP_ROUTING_BLOCK is always prepended.
16
16
  * If `.agent/routing.md` exists and is non-empty, it is appended after the block.
17
17
  */
18
- import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs';
19
- import { fileURLToPath } from 'node:url';
18
+ import { existsSync, readFileSync, statSync } from 'node:fs';
20
19
  import { homedir } from 'node:os';
21
20
  import { join } from 'node:path';
22
21
  import { WP_ROUTING_BLOCK } from '#hooks/shared/routing-block';
23
22
  import { readUpdateBanner } from './update-banner.js';
23
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
24
24
  export { WP_ROUTING_BLOCK };
25
25
  export const MAX_BYTES = 200 * 1024;
26
26
  export const TRUNCATION_NOTICE = '\n\n[truncated: file exceeded 200KB limit]';
@@ -115,8 +115,7 @@ export async function main() {
115
115
  }
116
116
  process.exit(0);
117
117
  }
118
- if (process.argv[1] &&
119
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
118
+ if (isDirectEntrypoint(import.meta.url)) {
120
119
  void main();
121
120
  }
122
121
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Return true when a module is being executed directly rather than imported.
3
+ *
4
+ * Bun single-file executables expose virtual `/$bunfs/...` paths while loading
5
+ * bundled modules. Those paths are not real filesystem entries, so direct
6
+ * `realpathSync` checks must degrade instead of throwing during native runtime
7
+ * imports.
8
+ */
9
+ export declare function isDirectEntrypoint(moduleUrl: string, argvPath?: string | undefined): boolean;
10
+ //# sourceMappingURL=direct-entrypoint.d.ts.map
@@ -0,0 +1,21 @@
1
+ import { realpathSync } from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ /**
4
+ * Return true when a module is being executed directly rather than imported.
5
+ *
6
+ * Bun single-file executables expose virtual `/$bunfs/...` paths while loading
7
+ * bundled modules. Those paths are not real filesystem entries, so direct
8
+ * `realpathSync` checks must degrade instead of throwing during native runtime
9
+ * imports.
10
+ */
11
+ export function isDirectEntrypoint(moduleUrl, argvPath = process.argv[1]) {
12
+ if (!argvPath)
13
+ return false;
14
+ try {
15
+ return realpathSync(fileURLToPath(moduleUrl)) === realpathSync(argvPath);
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ //# sourceMappingURL=direct-entrypoint.js.map
@@ -1,11 +1,10 @@
1
1
  #!/usr/bin/env bun
2
2
  import { globSync } from 'glob';
3
3
  import { execSync } from 'node:child_process';
4
- import { realpathSync } from 'node:fs';
5
4
  import { basename, dirname, extname, join } from 'node:path';
6
- import { fileURLToPath } from 'node:url';
7
5
  import { runHook } from '#hooks/shared/hook-bootstrap';
8
6
  import { isLintableFile, isSkippedPath } from '#hooks/post-tool/lint-after-edit';
7
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
9
8
  const TYPECHECKABLE_EXTENSIONS = new Set(['.ts', '.tsx']);
10
9
  export function getChangedFiles(projectDir) {
11
10
  const unstaged = execSync('git diff --name-only', { cwd: projectDir, encoding: 'utf-8' }).trim();
@@ -86,14 +85,13 @@ export function formatStopHookOutput(result) {
86
85
  return JSON.stringify(result);
87
86
  }
88
87
  export async function main() {
89
- runHook(
88
+ await runHook(
90
89
  // `Stop` is latency-sensitive and user-visible. Until webpresso grows a
91
90
  // deferred execution plane, broad typecheck/test sweeps stay off the hot
92
91
  // path instead of shelling synchronously at turn end.
93
92
  (_input) => null, formatStopHookOutput);
94
93
  }
95
- if (process.argv[1] &&
96
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
94
+ if (isDirectEntrypoint(import.meta.url)) {
97
95
  void main();
98
96
  }
99
97
  //# sourceMappingURL=qa-changed-files.js.map
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import { resolveActiveWorktreeRoot } from './shared/worktree-root.js';
3
- import { readFileSync, realpathSync } from 'node:fs';
4
- import { fileURLToPath } from 'node:url';
3
+ import { readFileSync } from 'node:fs';
5
4
  import { isAbsolute, join } from 'node:path';
5
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
6
6
  import { findMutationGamingPatterns, findTautologicalAssertions, MUTATION_GAMING_PATTERNS, TAUTOLOGICAL_PATTERNS, } from './pretool-guard/validators/test-quality.js';
7
7
  const testFileRegex = /\.test\.(ts|tsx|js|jsx)$/;
8
8
  export function getTestQualityCheckCwd() {
@@ -54,8 +54,7 @@ export function runTestQualityCheck(argv = process.argv.slice(2), cwd = getTestQ
54
54
  process.exit(1);
55
55
  }
56
56
  }
57
- if (process.argv[1] &&
58
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1])) {
57
+ if (isDirectEntrypoint(import.meta.url)) {
59
58
  runTestQualityCheck();
60
59
  }
61
60
  //# sourceMappingURL=test-quality-check.js.map
@@ -461,11 +461,23 @@ function runValidate(filePath) {
461
461
  catch (e) {
462
462
  return { valid: false, gaps: [`Frontmatter parse error: ${toStr(e)}`] };
463
463
  }
464
- for (const f of ['type', 'title', 'status', 'complexity', 'owner']) {
464
+ const blueprintStatus = String(fm.data['status'] ?? '').trim();
465
+ const requiredFields = blueprintStatus === 'draft'
466
+ ? ['type', 'status']
467
+ : ['type', 'title', 'status', 'complexity', 'owner', 'last_updated'];
468
+ for (const f of requiredFields) {
465
469
  const v = fm.data[f];
466
470
  if (!v || String(v).trim() === '')
467
471
  gaps.push(`Missing or empty frontmatter field: ${f}`);
468
472
  }
473
+ const complexity = String(fm.data['complexity'] ?? '').trim();
474
+ if (complexity && !['XS', 'S', 'M', 'L', 'XL'].includes(complexity)) {
475
+ gaps.push(`Invalid frontmatter field: complexity (${complexity})`);
476
+ }
477
+ const lastUpdated = String(fm.data['last_updated'] ?? '').trim();
478
+ if (lastUpdated && !/^\d{4}-\d{2}-\d{2}$/.test(lastUpdated.replace(/^['"]|['"]$/g, ''))) {
479
+ gaps.push(`Invalid frontmatter field: last_updated (${lastUpdated})`);
480
+ }
469
481
  const body = fm.content;
470
482
  const taskHeaderRegex = /^####\s+(?:\[[^\]]+\]\s+)?Task\s+\S/m;
471
483
  if (!taskHeaderRegex.test(body))
@@ -1145,12 +1157,12 @@ async function handleFinalize(projectResolver, cwd, raw) {
1145
1157
  function assertBlueprintCanComplete(overviewPath, slug) {
1146
1158
  const markdown = readFileSync(overviewPath, 'utf8');
1147
1159
  const blueprint = parseBlueprint(markdown, slug);
1148
- const unfinished = blueprint.tasks.filter((task) => task.status !== 'done');
1160
+ const unfinished = blueprint.tasks.filter((task) => task.status !== 'done' && task.status !== 'dropped');
1149
1161
  if (unfinished.length > 0) {
1150
1162
  const list = unfinished.map((task) => `${task.id} (${task.status})`).join(', ');
1151
1163
  throw new Error(`Cannot complete "${slug}": the following tasks are not done: ${list}`);
1152
1164
  }
1153
- assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.map((task) => task.id));
1165
+ assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status !== 'dropped').map((task) => task.id));
1154
1166
  }
1155
1167
  const depgraphSchema = z.object({ from: z.string() });
1156
1168
  async function handleDepgraph(cwd, raw) {
@@ -1880,6 +1892,17 @@ async function handleBlueprintTransition(projectResolver, cwd, raw) {
1880
1892
  const found = findBlueprintDir(root, slug, ALL_STATES);
1881
1893
  if (!found)
1882
1894
  return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found on disk`);
1895
+ // Transitioning to `completed` must satisfy the same open-task gate as
1896
+ // finalize/promote — otherwise this path is a hole that completes a blueprint
1897
+ // with unfinished work. (terminal = done ∪ dropped, see assertBlueprintCanComplete.)
1898
+ if (to_state === 'completed') {
1899
+ try {
1900
+ assertBlueprintCanComplete(found.path, slug);
1901
+ }
1902
+ catch (error) {
1903
+ return err('wp_blueprint_transition failed', error instanceof Error ? error.message : String(error));
1904
+ }
1905
+ }
1883
1906
  try {
1884
1907
  const refreshed = await applyLocalBlueprintTransition({
1885
1908
  found,
@@ -7,6 +7,7 @@
7
7
  * `dist/esm/mcp/tools/*.js` (post-build) or `src/mcp/tools/*.ts` (dev).
8
8
  */
9
9
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { isDirectEntrypoint } from '#hooks/shared/direct-entrypoint';
10
11
  import { deleteSentinel, writeSentinel } from '#hooks/shared/mcp-sentinel';
11
12
  import { createServer } from './server.js';
12
13
  export async function runStdioServer() {
@@ -55,12 +56,7 @@ export async function runStdioServer() {
55
56
  writeSentinel();
56
57
  await settle.promise;
57
58
  }
58
- import { realpathSync } from 'node:fs';
59
- import { fileURLToPath } from 'node:url';
60
- const isDirectInvocation = typeof process !== 'undefined' &&
61
- process.argv[1] !== undefined &&
62
- realpathSync(fileURLToPath(import.meta.url)) === realpathSync(process.argv[1]);
63
- if (isDirectInvocation) {
59
+ if (isDirectEntrypoint(import.meta.url)) {
64
60
  runStdioServer().catch((err) => {
65
61
  process.stderr.write(`wp mcp: ${err instanceof Error ? err.message : String(err)}\n`);
66
62
  process.exit(1);
@@ -7,7 +7,9 @@
7
7
  * — no edits required here.
8
8
  */
9
9
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
10
+ export declare function isBunSingleFileModuleUrl(moduleUrl: string): boolean;
10
11
  export type ToolLoadMode = 'filesystem' | 'registry';
12
+ export declare function resolveDefaultToolLoadMode(moduleUrl?: string): ToolLoadMode;
11
13
  export interface CreateServerOptions {
12
14
  /**
13
15
  * Directory to scan for tool descriptors. Defaults to `./tools` relative to