@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
@@ -1,7 +1,8 @@
1
1
  import { execFileSync } from 'node:child_process';
2
- import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, } from 'node:fs';
2
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, } from 'node:fs';
3
3
  import { tmpdir } from 'node:os';
4
4
  import { basename, dirname, join, relative, resolve } from 'node:path';
5
+ import { AGENT_KIT_TARBALL_SIZE_BUDGET_BYTES, AGENT_KIT_TARBALL_UNPACKED_SIZE_BUDGET_BYTES, evaluateAgentKitTarballSizeBudget, } from '#build/runtime-surface-policy.js';
5
6
  const DEFAULT_ALLOWED_PUBLIC_PACKAGES = [
6
7
  '@webpresso/webpresso',
7
8
  '@webpresso/agent-kit',
@@ -179,7 +180,10 @@ export function stagePublishableTarballSurface(rootDirectory, destinationDirecto
179
180
  const packages = discoverPublishablePackages(root);
180
181
  let fileCount = 0;
181
182
  for (const candidate of packages) {
182
- const packedFiles = readPackedFiles(candidate.packageRoot);
183
+ const packEntry = readPackedEntry(candidate.packageRoot);
184
+ const packedFiles = Array.isArray(packEntry.files)
185
+ ? packEntry.files.filter((item) => Boolean(item?.path))
186
+ : [];
183
187
  stagePackedFiles(root, destinationRoot, candidate, packedFiles);
184
188
  fileCount += packedFiles.length;
185
189
  }
@@ -244,9 +248,9 @@ function auditPackedTarballSurface(root, contract, violations) {
244
248
  ];
245
249
  let checked = 0;
246
250
  for (const candidate of packages) {
247
- let packedFiles;
251
+ let packEntry;
248
252
  try {
249
- packedFiles = readPackedFiles(candidate.packageRoot);
253
+ packEntry = readPackedEntry(candidate.packageRoot);
250
254
  }
251
255
  catch (error) {
252
256
  violations.push({
@@ -256,6 +260,9 @@ function auditPackedTarballSurface(root, contract, violations) {
256
260
  checked += 1;
257
261
  continue;
258
262
  }
263
+ const packedFiles = Array.isArray(packEntry.files)
264
+ ? packEntry.files.filter((item) => Boolean(item?.path))
265
+ : [];
259
266
  checked += packedFiles.length;
260
267
  for (const packedFile of packedFiles) {
261
268
  const repoRelative = packageFileToRepoRelative(root, candidate, packedFile.path);
@@ -267,11 +274,72 @@ function auditPackedTarballSurface(root, contract, violations) {
267
274
  });
268
275
  }
269
276
  }
277
+ checked += auditAgentKitNativeRuntimeSurface(root, candidate, packEntry, packedFiles, violations);
270
278
  checked += auditPackedTarballContent(root, candidate, packedFiles, forbiddenContentRules, allowedContentRules, allowedPathRules, deepScanExcludedPathPrefixes, violations);
271
279
  checked += auditPackedTarballSecrets(root, candidate, packedFiles, forbiddenPathRules, allowedPathRules, allowedSecretlintMessageIds, deepScanExcludedPathPrefixes, violations);
272
280
  }
273
281
  return checked;
274
282
  }
283
+ function auditAgentKitNativeRuntimeSurface(root, candidate, packEntry, packedFiles, violations) {
284
+ if (candidate.name !== '@webpresso/agent-kit')
285
+ return 0;
286
+ const manifestPath = join(candidate.packageRoot, 'bin', 'runtime-manifest.json');
287
+ if (!existsSync(manifestPath)) {
288
+ violations.push({
289
+ file: relativePath(root, join(candidate.packageRoot, 'bin', 'runtime-manifest.json')),
290
+ message: 'Native runtime manifest is missing from the publishable package surface',
291
+ });
292
+ return 1;
293
+ }
294
+ const packedPaths = new Set(packedFiles.map((file) => file.path));
295
+ const requiredPackedPaths = new Set(['bin/runtime-manifest.json', 'bin/wp']);
296
+ const deniedPackedPrefixes = ['bin/runtime/', 'dist/runtime/', 'dist/runtime-packages/'];
297
+ for (const requiredPath of requiredPackedPaths) {
298
+ if (packedPaths.has(requiredPath))
299
+ continue;
300
+ violations.push({
301
+ file: relativePath(root, join(candidate.packageRoot, requiredPath)),
302
+ message: `Publishable tarball is missing required native runtime artifact ${requiredPath}`,
303
+ });
304
+ }
305
+ for (const packedPath of packedPaths) {
306
+ if (!deniedPackedPrefixes.some((prefix) => packedPath.startsWith(prefix)))
307
+ continue;
308
+ violations.push({
309
+ file: relativePath(root, join(candidate.packageRoot, packedPath)),
310
+ message: `Publishable tarball contains denied native runtime payload ${packedPath}`,
311
+ });
312
+ }
313
+ const stagedLauncherPath = join(candidate.packageRoot, 'bin', 'wp');
314
+ if (!existsSync(stagedLauncherPath)) {
315
+ violations.push({
316
+ file: relativePath(root, stagedLauncherPath),
317
+ message: 'Publishable native launcher bin/wp is missing',
318
+ });
319
+ }
320
+ else if (lstatSync(stagedLauncherPath).isSymbolicLink()) {
321
+ violations.push({
322
+ file: relativePath(root, stagedLauncherPath),
323
+ message: 'Publishable native launcher bin/wp must be a real file, not a symlink',
324
+ });
325
+ }
326
+ else if (!isRootWpDispatcher(stagedLauncherPath)) {
327
+ violations.push({
328
+ file: relativePath(root, stagedLauncherPath),
329
+ message: 'Publishable native launcher bin/wp must be the cross-platform JS dispatcher, not a native binary',
330
+ });
331
+ }
332
+ const sizeBudget = evaluateAgentKitTarballSizeBudget(packEntry);
333
+ if (!sizeBudget.sizeOk || !sizeBudget.unpackedOk) {
334
+ violations.push({
335
+ file: relativePath(root, candidate.packageFile),
336
+ message: `Publishable tarball exceeds native-runtime size budget: size=${sizeBudget.size}/` +
337
+ `${AGENT_KIT_TARBALL_SIZE_BUDGET_BYTES}, unpacked=${sizeBudget.unpackedSize}/` +
338
+ `${AGENT_KIT_TARBALL_UNPACKED_SIZE_BUDGET_BYTES}`,
339
+ });
340
+ }
341
+ return requiredPackedPaths.size + 2 + packedFiles.length;
342
+ }
275
343
  function auditPackedTarballContent(root, candidate, packedFiles, forbiddenRules, allowedRules, allowedPathRules, deepScanExcludedPathPrefixes, violations) {
276
344
  let checked = 0;
277
345
  for (const packedFile of packedFiles) {
@@ -297,6 +365,10 @@ function auditPackedTarballContent(root, candidate, packedFiles, forbiddenRules,
297
365
  }
298
366
  return checked;
299
367
  }
368
+ function isRootWpDispatcher(path) {
369
+ const text = readPackedText(path);
370
+ return Boolean(text?.startsWith('#!/usr/bin/env node') && text.includes("runNamedBin('wp')"));
371
+ }
300
372
  function auditPackedTarballSecrets(root, candidate, packedFiles, forbiddenPathRules, allowedPathRules, allowedMessageIds, deepScanExcludedPathPrefixes, violations) {
301
373
  const packageRelativeRoot = relativePath(root, candidate.packageRoot);
302
374
  const secretlintCandidates = packedFiles.filter((packedFile) => {
@@ -366,7 +438,7 @@ function discoverPublishablePackages(root) {
366
438
  }
367
439
  return packages;
368
440
  }
369
- function readPackedFiles(packageRoot) {
441
+ function readPackedEntry(packageRoot) {
370
442
  const raw = execFileSync('npm', ['pack', '--dry-run', '--json'], {
371
443
  cwd: packageRoot,
372
444
  encoding: 'utf8',
@@ -374,11 +446,8 @@ function readPackedFiles(packageRoot) {
374
446
  });
375
447
  const entries = JSON.parse(raw);
376
448
  if (!Array.isArray(entries) || entries.length === 0)
377
- return [];
378
- const first = entries[0];
379
- return Array.isArray(first.files)
380
- ? first.files.filter((item) => Boolean(item?.path))
381
- : [];
449
+ return {};
450
+ return entries[0] ?? {};
382
451
  }
383
452
  function stagePackedFiles(root, destinationRoot, candidate, packedFiles) {
384
453
  const packageRelativeRoot = relativePath(root, candidate.packageRoot);
@@ -21,7 +21,7 @@ export interface DocsFrontmatterOptions {
21
21
  export interface BlueprintLifecycleOptions {
22
22
  blueprintsRoot?: string;
23
23
  statuses?: readonly string[];
24
- includeLegacyOmx?: boolean;
24
+ includeOmxPlans?: boolean;
25
25
  }
26
26
  export interface CommitMessageOptions {
27
27
  allowedTypes?: readonly string[];
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
2
2
  import { join, relative, resolve, sep } from 'node:path';
3
3
  import matter from 'gray-matter';
4
+ import { z } from 'zod';
4
5
  import { blueprintDerivedHandoffSchema } from '#execution/types';
5
6
  import { BLUEPRINT_OVERVIEW_FILENAME, isBlueprintSupportingMarkdownRelativePath, parseBlueprintDocumentRelativePath, } from '#utils/document-paths.js';
6
7
  import { validateLoreTrailers } from './commit-message-lore.js';
@@ -51,6 +52,13 @@ const ABSOLUTE_FILE_REFERENCE_PATTERN = /^(?:\/|[A-Za-z]:[\\/]|file:\/\/)/i;
51
52
  const LEGACY_CROSS_REPO_LABEL_PATTERN = /^cross-repo:/i;
52
53
  const GITHUB_REPO_PATTERN = /^[^/\s]+\/[^/\s]+$/;
53
54
  const BLUEPRINT_SLUG_PATTERN = /^[A-Za-z0-9._-]+$/;
55
+ const BLUEPRINT_COMPLEXITIES = ['XS', 'S', 'M', 'L', 'XL'];
56
+ const blueprintLifecycleRequiredFrontmatterSchema = z.object({
57
+ title: z.string().trim().min(1),
58
+ owner: z.string().trim().min(1),
59
+ complexity: z.enum(BLUEPRINT_COMPLEXITIES),
60
+ last_updated: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
61
+ });
54
62
  export function auditCatalogDrift(rootDirectory = process.cwd(), options = {}) {
55
63
  const root = resolve(rootDirectory);
56
64
  const workspacePath = resolve(root, options.workspaceFile ?? 'pnpm-workspace.yaml');
@@ -261,6 +269,38 @@ export function auditBlueprintLifecycle(rootDirectory = process.cwd(), options =
261
269
  message: `Blueprint status must match folder (${status})`,
262
270
  });
263
271
  }
272
+ if (status !== 'draft') {
273
+ const frontmatterValidation = blueprintLifecycleRequiredFrontmatterSchema.safeParse({
274
+ title: typeof frontmatter.title === 'string'
275
+ ? frontmatter.title
276
+ : String(frontmatter.title ?? ''),
277
+ owner: typeof frontmatter.owner === 'string'
278
+ ? frontmatter.owner
279
+ : String(frontmatter.owner ?? ''),
280
+ complexity: typeof frontmatter.complexity === 'string'
281
+ ? frontmatter.complexity
282
+ : String(frontmatter.complexity ?? ''),
283
+ last_updated: frontmatter.last_updated instanceof Date
284
+ ? (frontmatter.last_updated.toISOString().split('T')[0] ?? '')
285
+ : typeof frontmatter.last_updated === 'string'
286
+ ? frontmatter.last_updated
287
+ : String(frontmatter.last_updated ?? ''),
288
+ });
289
+ if (!frontmatterValidation.success) {
290
+ for (const issue of frontmatterValidation.error.issues) {
291
+ const field = String(issue.path[0] ?? '');
292
+ const message = field === 'complexity'
293
+ ? `Blueprint complexity must be one of ${BLUEPRINT_COMPLEXITIES.join(', ')}`
294
+ : field === 'last_updated'
295
+ ? 'Blueprint last_updated must be a YYYY-MM-DD date'
296
+ : `Blueprint is missing required frontmatter field: ${field}`;
297
+ violations.push({
298
+ file: relativePath(root, canonicalPath),
299
+ message,
300
+ });
301
+ }
302
+ }
303
+ }
264
304
  violations.push(...validateBlueprintLinkingFrontmatter({
265
305
  file: relativePath(root, canonicalPath),
266
306
  frontmatter,
@@ -268,8 +308,8 @@ export function auditBlueprintLifecycle(rootDirectory = process.cwd(), options =
268
308
  }));
269
309
  }
270
310
  }
271
- if (options.includeLegacyOmx === true) {
272
- const legacy = auditLegacyOmxPlans(root);
311
+ if (options.includeOmxPlans === true) {
312
+ const legacy = auditOmxPlanHandoffs(root);
273
313
  checked += legacy.checked;
274
314
  violations.push(...legacy.violations);
275
315
  }
@@ -527,7 +567,7 @@ function applyDocsFrontmatterFix(markdown, options) {
527
567
  return markdown;
528
568
  return `${markdown.slice(0, end)}\n${lines.join('\n')}${markdown.slice(end)}`;
529
569
  }
530
- function auditLegacyOmxPlans(root) {
570
+ function auditOmxPlanHandoffs(root) {
531
571
  const plansRoot = join(root, '.omx', 'plans');
532
572
  const violations = [];
533
573
  let checked = 0;
@@ -11,9 +11,8 @@
11
11
  * 3. Items that have never been reviewed (last_reviewed IS NULL)
12
12
  * AND were created more than 90 days ago.
13
13
  */
14
- import path from 'node:path';
15
14
  import { existsSync } from 'node:fs';
16
- const DB_PATH = path.join('.agent', '.blueprints.db');
15
+ import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
17
16
  export async function auditTechDebtCadence(cwd) {
18
17
  if (!process.env['WP_USE_SQL_AUDITS']) {
19
18
  return {
@@ -23,7 +22,7 @@ export async function auditTechDebtCadence(cwd) {
23
22
  violations: [],
24
23
  };
25
24
  }
26
- const dbFile = path.join(cwd, DB_PATH);
25
+ const dbFile = resolveBlueprintProjectionDbPath(cwd);
27
26
  if (!existsSync(dbFile)) {
28
27
  return {
29
28
  ok: true,
@@ -21,9 +21,7 @@ export function auditToolchainIsolation(root) {
21
21
  const packagePaths = findPackageJsonFiles(root);
22
22
  const violations = [];
23
23
  // Per-repo runtime exemptions: dependency names the repo declares as
24
- // legitimate app-specific runtimes (e.g. `tsx` for a Pulumi program's TS
25
- // loader, `@playwright/test` imported by e2e specs) rather than generic
26
- // toolchain. Mechanism here; data lives in the consumer's `.webpressorc.json`.
24
+ // legitimate app-specific runtimes rather than generic toolchain.
27
25
  const allowDependencies = new Set(readConfig(root)?.audit?.toolchainIsolation?.allowDependencies ?? []);
28
26
  for (const packagePath of packagePaths) {
29
27
  const pkg = readPackageJson(packagePath);
@@ -122,6 +120,7 @@ function shouldSkipDirectory(name) {
122
120
  '.omx',
123
121
  '.omc',
124
122
  '.codex',
123
+ '.windsurf',
125
124
  // Gitignored Claude Code agent surface — agent worktree scratch under
126
125
  // .claude/worktrees/* carries vendored package manifests that are not the
127
126
  // repo's own packages; walking it produces false positives on local dev
@@ -85,7 +85,7 @@ function extractCheckboxStatus(section) {
85
85
  status = 'done';
86
86
  }
87
87
  else if (checked > 0) {
88
- status = 'in_progress';
88
+ status = 'in-progress';
89
89
  }
90
90
  }
91
91
  return {
@@ -97,7 +97,8 @@ function extractExplicitTaskStatus(section) {
97
97
  const statusMatch = section.match(/\*\*Status:\*\*\s*(.+)/i);
98
98
  if (!statusMatch?.[1])
99
99
  return undefined;
100
- const parsed = taskStatusSchema.safeParse(statusMatch[1].trim());
100
+ const normalizedStatus = statusMatch[1].trim().replace(/\bin_progress\b/gi, 'in-progress');
101
+ const parsed = taskStatusSchema.safeParse(normalizedStatus);
101
102
  if (!parsed.success) {
102
103
  throw new Error(`Invalid task status "${statusMatch[1].trim()}". Valid statuses: ${taskStatusSchema.options.join(', ')}`);
103
104
  }
@@ -39,9 +39,10 @@ export declare const lifecycleBlueprintStatusSchema: z.ZodEnum<{
39
39
  */
40
40
  export declare const taskStatusSchema: z.ZodEnum<{
41
41
  blocked: "blocked";
42
- done: "done";
42
+ "in-progress": "in-progress";
43
43
  todo: "todo";
44
- in_progress: "in_progress";
44
+ done: "done";
45
+ dropped: "dropped";
45
46
  }>;
46
47
  /**
47
48
  * Valid complexity values using t-shirt sizing.
@@ -37,7 +37,7 @@ export const lifecycleBlueprintStatusSchema = z.enum([
37
37
  /**
38
38
  * Canonical task statuses for blueprint lifecycle management.
39
39
  */
40
- export const taskStatusSchema = z.enum(['todo', 'in_progress', 'blocked', 'done']);
40
+ export const taskStatusSchema = z.enum(['todo', 'in-progress', 'blocked', 'done', 'dropped']);
41
41
  /**
42
42
  * Valid complexity values using t-shirt sizing.
43
43
  */
@@ -15,15 +15,14 @@
15
15
  */
16
16
  import { createHash } from 'node:crypto';
17
17
  import { existsSync } from 'node:fs';
18
- import path from 'node:path';
18
+ import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
19
19
  import { Database } from '#db/sqlite.js';
20
20
  import { bothSidesAllowlistEntries } from './resolver.js';
21
21
  // ---------------------------------------------------------------------------
22
22
  // Main audit
23
23
  // ---------------------------------------------------------------------------
24
- const DB_PATH = path.join('.agent', '.blueprints.db');
25
24
  export async function auditCrossRepoCorrelation(cwd, _dryRun) {
26
- const dbFile = path.join(cwd, DB_PATH);
25
+ const dbFile = resolveBlueprintProjectionDbPath(cwd);
27
26
  if (!existsSync(dbFile)) {
28
27
  // No DB — nothing to audit
29
28
  return { pass: true, leaks: [], missingAllowlists: [] };
@@ -130,7 +129,7 @@ function runAudit(db) {
130
129
  * It must be invoked explicitly via `wp fix cross-repo-leak <slug>`.
131
130
  */
132
131
  export async function fixCrossRepoLeak(cwd, blueprintSlug) {
133
- const dbFile = path.join(cwd, DB_PATH);
132
+ const dbFile = resolveBlueprintProjectionDbPath(cwd);
134
133
  if (!existsSync(dbFile)) {
135
134
  return { fixed: false, reason: 'DB file not found' };
136
135
  }
@@ -1,15 +1,14 @@
1
1
  import { existsSync, mkdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { openDb } from './connection.js';
4
+ import { pruneProjectionArtifacts } from './gc.js';
4
5
  import { ingestAll } from './ingester.js';
5
- import { migrateLegacyAgentDb } from './legacy-migration.js';
6
6
  import { resolveBlueprintProjectionDbPath, withProjectionDbWriteLock } from './paths.js';
7
7
  import { recordProjectionMetadata } from '#freshness.js';
8
8
  export async function coldStartIfNeeded(cwd) {
9
9
  const start = Date.now();
10
- // F12/R10/E12: detect+migrate legacy `.agent/.blueprints.db` once per repo.
11
- migrateLegacyAgentDb(cwd);
12
10
  const target = resolveBlueprintProjectionDbPath(cwd);
11
+ pruneProjectionArtifacts({ preserveDbPath: target });
13
12
  if (existsSync(target)) {
14
13
  return { rebuilt: false, blueprintsCount: 0, techDebtCount: 0, durationMs: 0 };
15
14
  }
@@ -27,8 +27,8 @@ export declare const blueprintComplexitySchema: z.ZodEnum<{
27
27
  export declare const taskStatusSchema: z.ZodEnum<{
28
28
  blocked: "blocked";
29
29
  "in-progress": "in-progress";
30
- done: "done";
31
30
  todo: "todo";
31
+ done: "done";
32
32
  dropped: "dropped";
33
33
  }>;
34
34
  export declare const techDebtStatusSchema: z.ZodEnum<{
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Ephemeral in-memory blueprint projection.
3
+ *
4
+ * Builds a throwaway SQLite projection of the repo's blueprint markdown in
5
+ * `:memory:` and returns the open connection. The caller runs its queries and
6
+ * `close()`s the connection — nothing is written to disk.
7
+ *
8
+ * This is the data source for the `blueprint-lifecycle` audit: the verdict is a
9
+ * pure function of `markdown@HEAD`, identical in CLI / MCP / `wp doctor` / CI,
10
+ * with zero dependency on any persistent per-worktree projection. The schema is
11
+ * single-sourced through `openDb` → `runMigrations` (same as the persistent
12
+ * store), so the two can never drift.
13
+ *
14
+ * Deliberately does NOT take `withProjectionDbWriteLock`, `mkdirSync`, or
15
+ * `recordProjectionMetadata` — those belong to the persistent `reIngestProjection`
16
+ * / `coldStartIfNeeded` paths. An in-memory DB has no file, no lock, no metadata.
17
+ */
18
+ import { type DbConnection } from './connection.js';
19
+ /**
20
+ * Parse `blueprints/` (+ `tech-debt/`) markdown under `cwd` into a fresh
21
+ * in-memory SQLite projection. Caller owns the returned connection and must
22
+ * `close()` it (typically in a `finally`).
23
+ */
24
+ export declare function buildEphemeralProjection(cwd: string): Promise<DbConnection>;
25
+ //# sourceMappingURL=ephemeral-projection.d.ts.map
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Ephemeral in-memory blueprint projection.
3
+ *
4
+ * Builds a throwaway SQLite projection of the repo's blueprint markdown in
5
+ * `:memory:` and returns the open connection. The caller runs its queries and
6
+ * `close()`s the connection — nothing is written to disk.
7
+ *
8
+ * This is the data source for the `blueprint-lifecycle` audit: the verdict is a
9
+ * pure function of `markdown@HEAD`, identical in CLI / MCP / `wp doctor` / CI,
10
+ * with zero dependency on any persistent per-worktree projection. The schema is
11
+ * single-sourced through `openDb` → `runMigrations` (same as the persistent
12
+ * store), so the two can never drift.
13
+ *
14
+ * Deliberately does NOT take `withProjectionDbWriteLock`, `mkdirSync`, or
15
+ * `recordProjectionMetadata` — those belong to the persistent `reIngestProjection`
16
+ * / `coldStartIfNeeded` paths. An in-memory DB has no file, no lock, no metadata.
17
+ */
18
+ import { openDb } from './connection.js';
19
+ import { ingestAll } from './ingester.js';
20
+ /**
21
+ * Parse `blueprints/` (+ `tech-debt/`) markdown under `cwd` into a fresh
22
+ * in-memory SQLite projection. Caller owns the returned connection and must
23
+ * `close()` it (typically in a `finally`).
24
+ */
25
+ export async function buildEphemeralProjection(cwd) {
26
+ const conn = openDb(':memory:');
27
+ try {
28
+ await ingestAll({ db: conn.db, cwd });
29
+ }
30
+ catch (error) {
31
+ conn.close();
32
+ throw error;
33
+ }
34
+ return conn;
35
+ }
36
+ //# sourceMappingURL=ephemeral-projection.js.map
@@ -0,0 +1,11 @@
1
+ export interface PruneProjectionArtifactsOptions {
2
+ readonly now?: number;
3
+ readonly preserveDbPath?: string;
4
+ readonly stateRoot?: string;
5
+ readonly ttlMs?: number;
6
+ }
7
+ export interface PruneProjectionArtifactsResult {
8
+ readonly pruned: number;
9
+ }
10
+ export declare function pruneProjectionArtifacts(options?: PruneProjectionArtifactsOptions): PruneProjectionArtifactsResult;
11
+ //# sourceMappingURL=gc.d.ts.map
@@ -0,0 +1,55 @@
1
+ import { existsSync, readdirSync, rmSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { readProjectionMetadata } from '#freshness.js';
4
+ import { getStateRoot } from '#paths/state-root.js';
5
+ const DEFAULT_PROJECTION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
6
+ const LEGACY_WORKTREE_SEGMENT = `${path.sep}worktree${path.sep}`;
7
+ function safeListDirs(root) {
8
+ if (!existsSync(root))
9
+ return [];
10
+ return readdirSync(root, { withFileTypes: true })
11
+ .filter((entry) => entry.isDirectory())
12
+ .map((entry) => path.join(root, entry.name));
13
+ }
14
+ function removeProjection(dbPath) {
15
+ let pruned = 0;
16
+ for (const target of [dbPath, `${dbPath}.meta.json`]) {
17
+ if (!existsSync(target))
18
+ continue;
19
+ rmSync(target, { force: true });
20
+ pruned += 1;
21
+ }
22
+ return pruned;
23
+ }
24
+ function shouldPruneRepoScopedProjection(dbPath, now, ttlMs) {
25
+ const metadata = readProjectionMetadata(dbPath);
26
+ if (!metadata)
27
+ return false;
28
+ if (metadata.worktree_path && !existsSync(metadata.worktree_path))
29
+ return true;
30
+ return now - metadata.ingested_at > ttlMs;
31
+ }
32
+ export function pruneProjectionArtifacts(options = {}) {
33
+ const stateRoot = options.stateRoot ?? getStateRoot();
34
+ const now = options.now ?? Date.now();
35
+ const ttlMs = options.ttlMs ?? DEFAULT_PROJECTION_TTL_MS;
36
+ const preserveDbPath = options.preserveDbPath;
37
+ let pruned = 0;
38
+ for (const repoRoot of safeListDirs(stateRoot)) {
39
+ const repoScopedDb = path.join(repoRoot, 'blueprints', 'blueprints.db');
40
+ if (repoScopedDb !== preserveDbPath && shouldPruneRepoScopedProjection(repoScopedDb, now, ttlMs)) {
41
+ pruned += removeProjection(repoScopedDb);
42
+ }
43
+ const legacyWorktreeRoot = path.join(repoRoot, 'worktree');
44
+ for (const worktreeDir of safeListDirs(legacyWorktreeRoot)) {
45
+ const legacyDb = path.join(worktreeDir, 'blueprints', 'blueprints.db');
46
+ if (legacyDb === preserveDbPath)
47
+ continue;
48
+ if (!legacyDb.includes(LEGACY_WORKTREE_SEGMENT))
49
+ continue;
50
+ pruned += removeProjection(legacyDb);
51
+ }
52
+ }
53
+ return { pruned };
54
+ }
55
+ //# sourceMappingURL=gc.js.map
@@ -57,6 +57,13 @@ function upsertBlueprint(db, filePath, blueprintRoot) {
57
57
  const content = readFileSync(filePath, 'utf8');
58
58
  const slug = deriveSlugFromBlueprintPath(filePath, blueprintRoot);
59
59
  const parsed = parseBlueprintForDb(content, filePath, slug);
60
+ // progress_pct: honest done-only roll-up over parsed tasks (single source of
61
+ // truth = the task checkboxes/status, never a hand-entered frontmatter field).
62
+ // `null` when there are no tasks (prose-completed plans, parent-roadmaps) so
63
+ // the "completed but < 100%" audit check skips them rather than flagging 0%.
64
+ const totalTaskCount = parsed.tasks.length;
65
+ const doneTaskCount = parsed.tasks.filter((task) => task.status === 'done').length;
66
+ const progressPct = totalTaskCount === 0 ? null : Math.round((doneTaskCount / totalTaskCount) * 100);
60
67
  const now = Date.now();
61
68
  const upsertBp = db.prepare(`INSERT INTO blueprints
62
69
  (slug, title, status, complexity, owner, created, last_updated, completed_at,
@@ -101,7 +108,7 @@ function upsertBlueprint(db, filePath, blueprintRoot) {
101
108
  const insertEdge = db.prepare(`INSERT INTO edge_cases (blueprint_slug, edge_id, severity, description, mitigation)
102
109
  VALUES (?, ?, ?, ?, ?)`);
103
110
  db.transaction(() => {
104
- upsertBp.run(parsed.slug, parsed.title, parsed.status, parsed.complexity, parsed.owner, parsed.created, parsed.lastUpdated, parsed.completedAt, null, // progress_pct
111
+ upsertBp.run(parsed.slug, parsed.title, parsed.status, parsed.complexity, parsed.owner, parsed.created, parsed.lastUpdated, parsed.completedAt, progressPct, // progress_pct (terminal-task roll-up from tasks)
105
112
  null, // progress_text
106
113
  parsed.filePath, parsed.byteSize, parsed.contentHash, now, parsed.organization, parsed.visibility);
107
114
  // Clear and reinsert all related data
@@ -207,10 +214,41 @@ export async function ingestBlueprints(opts) {
207
214
  baseDir: blueprintRoot,
208
215
  includeSpecialFolders: true,
209
216
  }).map((entry) => entry.path);
217
+ const filesBySlug = new Map();
218
+ for (const filePath of files) {
219
+ try {
220
+ const slug = deriveSlugFromBlueprintPath(filePath, blueprintRoot);
221
+ const existing = filesBySlug.get(slug);
222
+ if (existing) {
223
+ existing.push(filePath);
224
+ }
225
+ else {
226
+ filesBySlug.set(slug, [filePath]);
227
+ }
228
+ }
229
+ catch (err) {
230
+ const msg = `[ingester] Blueprint failed: ${filePath}: ${String(err)}`;
231
+ process.stderr.write(msg + '\n');
232
+ errors.push(msg);
233
+ }
234
+ }
235
+ const duplicateSlugs = new Set();
236
+ for (const [slug, slugFiles] of filesBySlug) {
237
+ if (slugFiles.length <= 1) {
238
+ continue;
239
+ }
240
+ duplicateSlugs.add(slug);
241
+ const msg = `[ingester] Blueprint failed: duplicate slug "${slug}" appears in multiple blueprint documents: ${slugFiles.join(', ')}`;
242
+ process.stderr.write(msg + '\n');
243
+ errors.push(msg);
244
+ }
210
245
  for (const filePath of files) {
211
246
  try {
212
247
  const content = readFileSync(filePath, 'utf8');
213
248
  const slug = deriveSlugFromBlueprintPath(filePath, blueprintRoot);
249
+ if (duplicateSlugs.has(slug)) {
250
+ continue;
251
+ }
214
252
  const newHash = createHash('sha256').update(content).digest('hex');
215
253
  if (!dryRun) {
216
254
  const existing = existingBlueprintHash(db, slug);
@@ -1,8 +1,10 @@
1
1
  import { readdirSync, readFileSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
- const MIGRATIONS_DIR = __dirname;
3
+ import { resolvePackageAssetPreferred } from '#utils/package-assets.js';
4
+ const MIGRATIONS_DIR = resolvePackageAssetPreferred([
5
+ 'src/blueprint/db/migrations',
6
+ 'dist/esm/blueprint/db/migrations',
7
+ ]);
6
8
  function ensureSchemaVersionTable(db) {
7
9
  db.exec('CREATE TABLE IF NOT EXISTS schema_version (version INTEGER PRIMARY KEY, applied_at TEXT)');
8
10
  }