@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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +2 -3
- package/README.md +2 -2
- package/bin/_run.js +6 -0
- package/bin/wp +5 -0
- package/catalog/base-kit/.github/actions/setup-webpresso/action.yml.tmpl +21 -0
- package/catalog/base-kit/.github/workflows/{ci.webpresso.yml.tmpl → ci.yml.tmpl} +17 -7
- package/catalog/base-kit/tsconfig.json.tmpl +1 -1
- package/catalog/docs/templates/blueprint.yaml +1 -1
- package/dist/esm/audit/_budgets.d.ts +9 -1
- package/dist/esm/audit/_budgets.js +8 -1
- package/dist/esm/audit/blueprint-db-consistency.js +2 -2
- package/dist/esm/audit/blueprint-lifecycle-sql.d.ts +17 -7
- package/dist/esm/audit/blueprint-lifecycle-sql.js +298 -48
- package/dist/esm/audit/blueprint-readme-drift.d.ts +6 -0
- package/dist/esm/audit/blueprint-readme-drift.js +110 -0
- package/dist/esm/audit/no-first-party-mjs.js +5 -4
- package/dist/esm/audit/package-surface.js +79 -10
- package/dist/esm/audit/repo-guardrails.d.ts +1 -1
- package/dist/esm/audit/repo-guardrails.js +43 -3
- package/dist/esm/audit/tech-debt-cadence.js +2 -3
- package/dist/esm/audit/toolchain-isolation.js +2 -3
- package/dist/esm/blueprint/core/parser.js +3 -2
- package/dist/esm/blueprint/core/schema.d.ts +3 -2
- package/dist/esm/blueprint/core/schema.js +1 -1
- package/dist/esm/blueprint/cross-repo/audit.js +3 -4
- package/dist/esm/blueprint/db/cold-start.js +2 -3
- package/dist/esm/blueprint/db/enums.d.ts +1 -1
- package/dist/esm/blueprint/db/ephemeral-projection.d.ts +25 -0
- package/dist/esm/blueprint/db/ephemeral-projection.js +36 -0
- package/dist/esm/blueprint/db/gc.d.ts +11 -0
- package/dist/esm/blueprint/db/gc.js +55 -0
- package/dist/esm/blueprint/db/ingester.js +39 -1
- package/dist/esm/blueprint/db/migrations/run.js +5 -3
- package/dist/esm/blueprint/db/paths.d.ts +13 -24
- package/dist/esm/blueprint/db/paths.js +25 -33
- package/dist/esm/blueprint/execution/progress-bridge.js +5 -4
- package/dist/esm/blueprint/freshness.d.ts +2 -0
- package/dist/esm/blueprint/freshness.js +3 -1
- package/dist/esm/blueprint/lifecycle/audit.js +6 -6
- package/dist/esm/blueprint/lifecycle/engine.d.ts +1 -1
- package/dist/esm/blueprint/lifecycle/engine.js +13 -9
- package/dist/esm/blueprint/lifecycle/transition-matrix.d.ts +5 -0
- package/dist/esm/blueprint/lifecycle/transition-matrix.js +20 -0
- package/dist/esm/blueprint/markdown/helpers.d.ts +1 -1
- package/dist/esm/blueprint/projection-ready.js +2 -0
- package/dist/esm/blueprint/service/BlueprintService.js +1 -1
- package/dist/esm/blueprint/service/blueprint-records.js +1 -1
- package/dist/esm/blueprint/tracked-document/parser.js +1 -1
- package/dist/esm/blueprint/utils/archive.d.ts +2 -2
- package/dist/esm/blueprint/utils/archive.js +5 -2
- package/dist/esm/blueprint/utils/package-assets.d.ts +13 -0
- package/dist/esm/blueprint/utils/package-assets.js +38 -6
- package/dist/esm/build/normalize-tsconfig-json-exports.d.ts +13 -0
- package/dist/esm/build/normalize-tsconfig-json-exports.js +39 -0
- package/dist/esm/build/package-manifest.js +12 -4
- package/dist/esm/build/release-policy.d.ts +9 -18
- package/dist/esm/build/release-policy.js +10 -19
- package/dist/esm/build/runtime-surface-policy.d.ts +14 -0
- package/dist/esm/build/runtime-surface-policy.js +13 -0
- package/dist/esm/cli/commands/audit-core.d.ts +2 -2
- package/dist/esm/cli/commands/audit.js +7 -3
- package/dist/esm/cli/commands/blueprint/db-commands.js +0 -3
- package/dist/esm/cli/commands/blueprint/mutations.d.ts +3 -2
- package/dist/esm/cli/commands/blueprint/mutations.js +45 -39
- package/dist/esm/cli/commands/blueprint/router-output.js +2 -2
- package/dist/esm/cli/commands/doctor.d.ts +1 -1
- package/dist/esm/cli/commands/doctor.js +4 -5
- package/dist/esm/cli/commands/init/config.d.ts +6 -10
- package/dist/esm/cli/commands/init/config.js +36 -20
- package/dist/esm/cli/commands/init/gitignore-patcher.js +0 -1
- package/dist/esm/cli/commands/init/index.d.ts +8 -1
- package/dist/esm/cli/commands/init/index.js +17 -19
- package/dist/esm/cli/commands/init/package-root.d.ts +20 -0
- package/dist/esm/cli/commands/init/package-root.js +110 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +5 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.d.ts +3 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +8 -24
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +9 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +79 -1
- package/dist/esm/cli/commands/init/scaffolders/claude-rules/index.js +2 -12
- package/dist/esm/cli/commands/init/scaffolders/subagents/index.js +2 -12
- package/dist/esm/config/tsconfig/cloudflare.json +1 -1
- package/dist/esm/config/tsconfig/library.json +1 -1
- package/dist/esm/config/tsconfig/react-library.json +3 -2
- package/dist/esm/config/tsconfig/react-router.json +1 -1
- package/dist/esm/dev/restore-dev-links/index.js +3 -4
- package/dist/esm/docs-linter/blueprint-plan.js +46 -4
- package/dist/esm/hooks/check-dev-link/index.js +3 -4
- package/dist/esm/hooks/doctor.d.ts +11 -0
- package/dist/esm/hooks/doctor.js +174 -30
- package/dist/esm/hooks/guard-switch/index.js +3 -5
- package/dist/esm/hooks/post-tool/lint-after-edit.js +4 -5
- package/dist/esm/hooks/pretool-guard/index.js +2 -4
- package/dist/esm/hooks/pretool-guard/runner.js +2 -4
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +47 -6
- package/dist/esm/hooks/sessionstart/index.js +3 -4
- package/dist/esm/hooks/shared/direct-entrypoint.d.ts +10 -0
- package/dist/esm/hooks/shared/direct-entrypoint.js +21 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +3 -5
- package/dist/esm/hooks/test-quality-check.js +3 -4
- package/dist/esm/mcp/blueprint-server.js +26 -3
- package/dist/esm/mcp/cli.js +2 -6
- package/dist/esm/mcp/server.d.ts +2 -0
- package/dist/esm/mcp/server.js +18 -3
- package/dist/esm/mcp/tools/_shared/audit-kinds.d.ts +1 -1
- package/dist/esm/mcp/tools/_shared/audit-kinds.js +1 -0
- package/dist/esm/mcp/tools/audit.d.ts +2 -1
- package/dist/esm/mcp/tools/audit.js +13 -3
- package/dist/esm/package.json +2 -0
- package/package.json +24 -15
- package/tsconfig/cloudflare.json +1 -1
- package/tsconfig/library.json +1 -1
- package/tsconfig/react-library.json +3 -2
- package/tsconfig/react-router.json +1 -1
- package/dist/esm/blueprint/db/legacy-migration.d.ts +0 -41
- 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
|
|
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
|
|
251
|
+
let packEntry;
|
|
248
252
|
try {
|
|
249
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
272
|
-
const legacy =
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
42
|
+
"in-progress": "in-progress";
|
|
43
43
|
todo: "todo";
|
|
44
|
-
|
|
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', '
|
|
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
|
|
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 =
|
|
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 =
|
|
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,
|
|
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 {
|
|
4
|
-
const
|
|
5
|
-
|
|
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
|
}
|