@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
@@ -9,12 +9,10 @@
9
9
  *
10
10
  * We adopt the **two-lock** policy:
11
11
  *
12
- * 1. **Projection DB lock — `'worktree'` scope.**
13
- * The SQLite file at `getSurfacePath('blueprints/blueprints.db', 'worktree', cwd)`
14
- * is a per-worktree derived artifact. Concurrent writers in the **same**
15
- * worktree (cold-start + mutation re-ingest, two ingest paths, etc.) must
16
- * serialize against the projection. Cross-worktree writers target distinct
17
- * DB files, so they do not need this lock.
12
+ * 1. **Projection DB lock — `'repo'` scope.**
13
+ * The SQLite file at `getSurfacePath('blueprints/blueprints.db', 'repo', cwd)`
14
+ * is a per-repo derived artifact shared by worktrees of the same repository.
15
+ * Concurrent writers therefore serialize at repo scope.
18
16
  *
19
17
  * 2. **Markdown-mutation lock — `'repo'` scope.**
20
18
  * The `blueprints/` markdown directory is git-tracked and shared across
@@ -31,36 +29,27 @@
31
29
  * raise `LockTimeoutError` on failure. Read-only paths may proceed without a
32
30
  * lock (they take a consistent SQLite snapshot regardless).
33
31
  *
34
- * ## Legacy fallback
32
+ * ## Non-git fallback
35
33
  *
36
34
  * Non-git temp repos (most tests, ad-hoc directories) cannot resolve a repo
37
- * key. For those we keep the historical `<cwd>/.agent/.blueprints.db` layout
38
- * so existing fixtures and bootstrap flows continue to work.
39
- *
40
- * For git repos that still carry a stray `.agent/.blueprints.db` from a
41
- * previous webpresso version, see `legacy-migration.ts`.
35
+ * key. Those callers use a deterministic user-state path keyed by the absolute
36
+ * cwd rather than writing legacy `.agent/.blueprints.db` artifacts into the
37
+ * repo itself.
42
38
  */
43
- export declare const LEGACY_AGENT_DIR = ".agent";
44
- export declare const LEGACY_DB_FILENAME = ".blueprints.db";
45
- export declare const LEGACY_LOCK_FILENAME = ".blueprints.lock";
46
39
  export declare class LockTimeoutError extends Error {
47
40
  readonly lockPath: string;
48
41
  readonly nextAction: 'reingest_project';
49
42
  constructor(lockPath: string, cause?: unknown);
50
43
  }
51
44
  /**
52
- * Resolve the worktree-scoped projection DB path.
45
+ * Resolve the repo-scoped projection DB path.
53
46
  *
54
- * In a git repo: `<state-root>/<repoKey>/worktree/<wtKey>/blueprints/blueprints.db`.
55
- * Outside a git repo: legacy `<cwd>/.agent/.blueprints.db` (no isolation).
47
+ * In a git repo: `<state-root>/<repoKey>/blueprints/blueprints.db`.
48
+ * Outside a git repo: deterministic user-state fallback keyed by absolute cwd.
56
49
  */
57
50
  export declare function resolveBlueprintProjectionDbPath(cwd: string): string;
58
51
  /**
59
- * Resolve the worktree-scoped lock file for the projection DB.
60
- *
61
- * Lives next to the DB so a single `mkdir -p` covers both. Cross-worktree
62
- * writers do not contend on this lock; see `resolveBlueprintMarkdownLockPath`
63
- * for the cross-worktree case.
52
+ * Resolve the repo-scoped lock file for the projection DB.
64
53
  */
65
54
  export declare function resolveBlueprintProjectionDbLockPath(cwd: string): string;
66
55
  /**
@@ -77,7 +66,7 @@ export interface AcquireLockOptions {
77
66
  readonly staleMs?: number;
78
67
  }
79
68
  /**
80
- * Acquire the worktree-scoped projection-DB write lock.
69
+ * Acquire the repo-scoped projection-DB write lock.
81
70
  *
82
71
  * Throws `LockTimeoutError` on failure — there is no silent "proceeds anyway"
83
72
  * escape. Read-only callers should not use this helper.
@@ -9,12 +9,10 @@
9
9
  *
10
10
  * We adopt the **two-lock** policy:
11
11
  *
12
- * 1. **Projection DB lock — `'worktree'` scope.**
13
- * The SQLite file at `getSurfacePath('blueprints/blueprints.db', 'worktree', cwd)`
14
- * is a per-worktree derived artifact. Concurrent writers in the **same**
15
- * worktree (cold-start + mutation re-ingest, two ingest paths, etc.) must
16
- * serialize against the projection. Cross-worktree writers target distinct
17
- * DB files, so they do not need this lock.
12
+ * 1. **Projection DB lock — `'repo'` scope.**
13
+ * The SQLite file at `getSurfacePath('blueprints/blueprints.db', 'repo', cwd)`
14
+ * is a per-repo derived artifact shared by worktrees of the same repository.
15
+ * Concurrent writers therefore serialize at repo scope.
18
16
  *
19
17
  * 2. **Markdown-mutation lock — `'repo'` scope.**
20
18
  * The `blueprints/` markdown directory is git-tracked and shared across
@@ -31,22 +29,18 @@
31
29
  * raise `LockTimeoutError` on failure. Read-only paths may proceed without a
32
30
  * lock (they take a consistent SQLite snapshot regardless).
33
31
  *
34
- * ## Legacy fallback
32
+ * ## Non-git fallback
35
33
  *
36
34
  * Non-git temp repos (most tests, ad-hoc directories) cannot resolve a repo
37
- * key. For those we keep the historical `<cwd>/.agent/.blueprints.db` layout
38
- * so existing fixtures and bootstrap flows continue to work.
39
- *
40
- * For git repos that still carry a stray `.agent/.blueprints.db` from a
41
- * previous webpresso version, see `legacy-migration.ts`.
35
+ * key. Those callers use a deterministic user-state path keyed by the absolute
36
+ * cwd rather than writing legacy `.agent/.blueprints.db` artifacts into the
37
+ * repo itself.
42
38
  */
43
- import { mkdirSync } from 'node:fs';
39
+ import { createHash } from 'node:crypto';
40
+ import { mkdirSync, realpathSync } from 'node:fs';
44
41
  import path from 'node:path';
45
42
  import lockfile from 'proper-lockfile';
46
- import { getSurfacePath, NotInGitRepoError } from '#paths/state-root.js';
47
- export const LEGACY_AGENT_DIR = '.agent';
48
- export const LEGACY_DB_FILENAME = '.blueprints.db';
49
- export const LEGACY_LOCK_FILENAME = '.blueprints.lock';
43
+ import { getStateRoot, getSurfacePath, NotInGitRepoError } from '#paths/state-root.js';
50
44
  const SURFACE_DB = 'blueprints/blueprints.db';
51
45
  const SURFACE_DB_LOCK = 'blueprints/blueprints.db.lock';
52
46
  const SURFACE_MARKDOWN_LOCK = 'blueprints/markdown.lock';
@@ -64,39 +58,37 @@ export class LockTimeoutError extends Error {
64
58
  }
65
59
  }
66
60
  }
67
- function legacyAgentPath(cwd, filename) {
68
- return path.join(cwd, LEGACY_AGENT_DIR, filename);
61
+ function nonGitStatePath(cwd, filename) {
62
+ const absoluteCwd = realpathSync(cwd);
63
+ const cwdKey = createHash('sha256').update(absoluteCwd).digest('hex').slice(0, 16);
64
+ return path.join(getStateRoot(), 'non-git', cwdKey, filename);
69
65
  }
70
66
  /**
71
- * Resolve the worktree-scoped projection DB path.
67
+ * Resolve the repo-scoped projection DB path.
72
68
  *
73
- * In a git repo: `<state-root>/<repoKey>/worktree/<wtKey>/blueprints/blueprints.db`.
74
- * Outside a git repo: legacy `<cwd>/.agent/.blueprints.db` (no isolation).
69
+ * In a git repo: `<state-root>/<repoKey>/blueprints/blueprints.db`.
70
+ * Outside a git repo: deterministic user-state fallback keyed by absolute cwd.
75
71
  */
76
72
  export function resolveBlueprintProjectionDbPath(cwd) {
77
73
  try {
78
- return getSurfacePath(SURFACE_DB, 'worktree', cwd);
74
+ return getSurfacePath(SURFACE_DB, 'repo', cwd);
79
75
  }
80
76
  catch (err) {
81
77
  if (err instanceof NotInGitRepoError)
82
- return legacyAgentPath(cwd, LEGACY_DB_FILENAME);
78
+ return nonGitStatePath(cwd, '.blueprints.db');
83
79
  throw err;
84
80
  }
85
81
  }
86
82
  /**
87
- * Resolve the worktree-scoped lock file for the projection DB.
88
- *
89
- * Lives next to the DB so a single `mkdir -p` covers both. Cross-worktree
90
- * writers do not contend on this lock; see `resolveBlueprintMarkdownLockPath`
91
- * for the cross-worktree case.
83
+ * Resolve the repo-scoped lock file for the projection DB.
92
84
  */
93
85
  export function resolveBlueprintProjectionDbLockPath(cwd) {
94
86
  try {
95
- return getSurfacePath(SURFACE_DB_LOCK, 'worktree', cwd);
87
+ return getSurfacePath(SURFACE_DB_LOCK, 'repo', cwd);
96
88
  }
97
89
  catch (err) {
98
90
  if (err instanceof NotInGitRepoError)
99
- return legacyAgentPath(cwd, LEGACY_LOCK_FILENAME);
91
+ return nonGitStatePath(cwd, '.blueprints.lock');
100
92
  throw err;
101
93
  }
102
94
  }
@@ -112,7 +104,7 @@ export function resolveBlueprintMarkdownLockPath(cwd) {
112
104
  }
113
105
  catch (err) {
114
106
  if (err instanceof NotInGitRepoError) {
115
- return legacyAgentPath(cwd, '.blueprints.markdown.lock');
107
+ return nonGitStatePath(cwd, '.blueprints.markdown.lock');
116
108
  }
117
109
  throw err;
118
110
  }
@@ -145,7 +137,7 @@ async function acquireWriteLockAt(lockPath, opts) {
145
137
  }
146
138
  }
147
139
  /**
148
- * Acquire the worktree-scoped projection-DB write lock.
140
+ * Acquire the repo-scoped projection-DB write lock.
149
141
  *
150
142
  * Throws `LockTimeoutError` on failure — there is no silent "proceeds anyway"
151
143
  * escape. Read-only callers should not use this helper.
@@ -72,7 +72,7 @@ function applyProjectedIntent(taskStatuses, intent) {
72
72
  return;
73
73
  }
74
74
  const nextStatus = intent.type === 'task_start'
75
- ? 'in_progress'
75
+ ? 'in-progress'
76
76
  : intent.type === 'task_verify'
77
77
  ? 'done'
78
78
  : intent.type === 'task_block'
@@ -222,11 +222,11 @@ function shouldFinalizeBlueprint(markdown, blueprint, snapshot) {
222
222
  blueprint.status === 'completed' ||
223
223
  blueprint.status === 'archived' ||
224
224
  blueprint.tasks.length === 0 ||
225
- !blueprint.tasks.every((task) => task.status === 'done')) {
225
+ !blueprint.tasks.every((task) => task.status === 'done' || task.status === 'dropped')) {
226
226
  return false;
227
227
  }
228
228
  try {
229
- assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.map((task) => task.id));
229
+ assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.filter((task) => task.status === 'done').map((task) => task.id));
230
230
  return true;
231
231
  }
232
232
  catch {
@@ -270,6 +270,7 @@ export function applyRuntimeProgressSnapshot(markdown, slug, input) {
270
270
  }
271
271
  if ((snapshot.status === 'blocked' || snapshot.status === 'failed') &&
272
272
  task.status !== 'done' &&
273
+ task.status !== 'dropped' &&
273
274
  task.status !== 'blocked') {
274
275
  nextMarkdown = applyIntent(nextMarkdown, slug, appliedTransitions, {
275
276
  type: 'task_block',
@@ -278,7 +279,7 @@ export function applyRuntimeProgressSnapshot(markdown, slug, input) {
278
279
  });
279
280
  nextBlueprint = parseBlueprint(nextMarkdown, slug);
280
281
  }
281
- if (snapshot.status === 'completed' && task.status !== 'done') {
282
+ if (snapshot.status === 'completed' && task.status !== 'done' && task.status !== 'dropped') {
282
283
  const verifyIntent = taskVerifyIntent(snapshot.taskId, snapshot.evidence);
283
284
  nextMarkdown = applyIntent(nextMarkdown, slug, appliedTransitions, verifyIntent ?? {
284
285
  type: 'task_block',
@@ -39,6 +39,8 @@ export interface ProjectionMetadata {
39
39
  readonly head_at_ingest: string | null;
40
40
  /** Epoch milliseconds at which the projection was last written. */
41
41
  readonly ingested_at: number;
42
+ /** Absolute worktree path that produced this projection, when known. */
43
+ readonly worktree_path?: string;
42
44
  }
43
45
  export type FreshnessResult = {
44
46
  readonly ok: true;
@@ -51,7 +51,8 @@ function isProjectionMetadata(value) {
51
51
  const obj = value;
52
52
  const headOk = obj.head_at_ingest === null || typeof obj.head_at_ingest === 'string';
53
53
  const tsOk = typeof obj.ingested_at === 'number' && Number.isFinite(obj.ingested_at);
54
- return headOk && tsOk;
54
+ const worktreeOk = obj.worktree_path === undefined || typeof obj.worktree_path === 'string';
55
+ return headOk && tsOk && worktreeOk;
55
56
  }
56
57
  // ---------------------------------------------------------------------------
57
58
  // Public API
@@ -85,6 +86,7 @@ export function recordProjectionMetadata(input) {
85
86
  const metadata = {
86
87
  head_at_ingest: readCurrentHead(input.cwd),
87
88
  ingested_at: input.ingestedAt,
89
+ worktree_path: input.cwd,
88
90
  };
89
91
  writeFileSync(sidecarPath(input.dbPath), JSON.stringify(metadata, null, 2) + '\n', 'utf8');
90
92
  return metadata;
@@ -141,11 +141,11 @@ function validateBlueprintEngineSemantics(file, blueprint) {
141
141
  const issues = [];
142
142
  if (blueprint.status === 'completed') {
143
143
  for (const task of blueprint.tasks) {
144
- if (task.status !== 'done') {
144
+ if (task.status !== 'done' && task.status !== 'dropped') {
145
145
  issues.push({
146
146
  file,
147
147
  level: 'error',
148
- message: `Blueprint status is completed but task ${task.id} is "${task.status}" (expected "done").`,
148
+ message: `Blueprint status is completed but task ${task.id} is "${task.status}" (expected "done" or "dropped").`,
149
149
  });
150
150
  }
151
151
  }
@@ -186,7 +186,7 @@ function validateExecutionMetadataTruth(file, blueprint) {
186
186
  });
187
187
  }
188
188
  if (metadata.status === 'completed') {
189
- const incompleteTasks = blueprint.tasks.filter((task) => task.status !== 'done');
189
+ const incompleteTasks = blueprint.tasks.filter((task) => task.status !== 'done' && task.status !== 'dropped');
190
190
  if (blueprint.status !== 'completed') {
191
191
  issues.push({
192
192
  file,
@@ -228,16 +228,16 @@ function validateExecutionMetadataTruth(file, blueprint) {
228
228
  }
229
229
  if ((metadata.status === 'blocked' || metadata.status === 'failed') &&
230
230
  blueprint.tasks.length > 0 &&
231
- blueprint.tasks.every((task) => task.status === 'done')) {
231
+ blueprint.tasks.every((task) => task.status === 'done' || task.status === 'dropped')) {
232
232
  issues.push({
233
233
  file,
234
234
  level: 'error',
235
- message: `Blueprint execution is ${metadata.status} but every task is marked done; failed or blocked runtime work must not appear completed.`,
235
+ message: `Blueprint execution is ${metadata.status} but every task is marked done/dropped; failed or blocked runtime work must not appear completed.`,
236
236
  });
237
237
  }
238
238
  return issues;
239
239
  }
240
- const lifecycleTaskStatuses = new Set(['todo', 'in_progress', 'blocked', 'done']);
240
+ const lifecycleTaskStatuses = new Set(['todo', 'in-progress', 'blocked', 'done', 'dropped']);
241
241
  function validateBlueprintPlacement(file, blueprint) {
242
242
  const issues = [];
243
243
  const normalized = normalizePath(file);
@@ -1,7 +1,7 @@
1
1
  import type { Blueprint } from '#core/parser';
2
2
  import type { LifecycleBlueprintStatus } from '#core/schema';
3
3
  import type { Evidence } from '#evidence.js';
4
- export type LifecycleTaskStatus = 'todo' | 'in_progress' | 'blocked' | 'done';
4
+ export type LifecycleTaskStatus = 'todo' | 'in-progress' | 'blocked' | 'done' | 'dropped';
5
5
  export type BlueprintLifecycleIntent = {
6
6
  type: 'start';
7
7
  } | {
@@ -27,7 +27,7 @@ function assertExecutableStatus(status) {
27
27
  }
28
28
  function formatProgress(blueprint) {
29
29
  const total = blueprint.tasks.length;
30
- const done = blueprint.tasks.filter((task) => task.status === 'done').length;
30
+ const done = blueprint.tasks.filter((task) => task.status === 'done' || task.status === 'dropped').length;
31
31
  const blocked = blueprint.tasks.filter((task) => task.status === 'blocked').length;
32
32
  const percent = total === 0 ? 0 : Math.round((done / total) * 100);
33
33
  return `${percent}% (${done}/${total} tasks done, ${blocked} blocked, updated ${todayIsoDate()})`;
@@ -46,29 +46,33 @@ function assertTaskExists(blueprint, taskId) {
46
46
  return task;
47
47
  }
48
48
  function assertTaskDoneRequirements(markdown, blueprint) {
49
+ const tasksRequiringEvidence = [];
49
50
  for (const task of blueprint.tasks) {
50
- if (task.status !== 'done') {
51
+ if (task.status !== 'done' && task.status !== 'dropped') {
51
52
  throw new Error(`Blueprint ${blueprint.name} cannot finalize: Task ${task.id} is ${task.status}`);
52
53
  }
53
54
  const { checked, total } = task.acceptanceCriteria;
54
- if (total > 0 && checked !== total) {
55
+ if (task.status === 'done' && total > 0 && checked !== total) {
55
56
  throw new Error(`Blueprint ${blueprint.name} cannot finalize: Task ${task.id} has ${checked}/${total} acceptance criteria checked`);
56
57
  }
58
+ if (task.status === 'done') {
59
+ tasksRequiringEvidence.push(task.id);
60
+ }
57
61
  }
58
- assertAllTasksHaveCanonicalPassingEvidence(markdown, blueprint.tasks.map((task) => task.id));
62
+ assertAllTasksHaveCanonicalPassingEvidence(markdown, tasksRequiringEvidence);
59
63
  }
60
64
  function applyTaskIntent(markdown, blueprint, intent) {
61
65
  const task = assertTaskExists(blueprint, intent.taskId);
62
66
  switch (intent.type) {
63
67
  case 'task_start': {
64
- if (task.status === 'done') {
65
- throw new Error(`Task ${task.id} is already done`);
68
+ if (task.status === 'done' || task.status === 'dropped') {
69
+ throw new Error(`Task ${task.id} is already ${task.status}`);
66
70
  }
67
- return updateBlockedReason(updateTaskStatus(markdown, task.id, 'in_progress'), task.id, '');
71
+ return updateBlockedReason(updateTaskStatus(markdown, task.id, 'in-progress'), task.id, '');
68
72
  }
69
73
  case 'task_block': {
70
- if (task.status === 'done') {
71
- throw new Error(`Task ${task.id} is already done`);
74
+ if (task.status === 'done' || task.status === 'dropped') {
75
+ throw new Error(`Task ${task.id} is already ${task.status}`);
72
76
  }
73
77
  const reason = intent.reason.trim();
74
78
  if (!reason) {
@@ -0,0 +1,5 @@
1
+ import type { LifecycleBlueprintStatus } from '#core/schema.js';
2
+ export declare function parseLifecycleBlueprintStatus(value: string): LifecycleBlueprintStatus | null;
3
+ export declare function getLegalLifecycleTargets(from: LifecycleBlueprintStatus): readonly LifecycleBlueprintStatus[];
4
+ export declare function isLegalLifecycleTransition(from: LifecycleBlueprintStatus, to: LifecycleBlueprintStatus): boolean;
5
+ //# sourceMappingURL=transition-matrix.d.ts.map
@@ -0,0 +1,20 @@
1
+ import { lifecycleBlueprintStatusSchema } from '#core/schema.js';
2
+ const LEGAL_TRANSITIONS = {
3
+ draft: ['planned', 'archived'],
4
+ planned: ['in-progress', 'parked', 'archived'],
5
+ 'in-progress': ['completed', 'parked', 'archived'],
6
+ parked: ['in-progress', 'planned', 'archived'],
7
+ completed: ['in-progress', 'archived'],
8
+ archived: [],
9
+ };
10
+ export function parseLifecycleBlueprintStatus(value) {
11
+ const parsed = lifecycleBlueprintStatusSchema.safeParse(value);
12
+ return parsed.success ? parsed.data : null;
13
+ }
14
+ export function getLegalLifecycleTargets(from) {
15
+ return LEGAL_TRANSITIONS[from];
16
+ }
17
+ export function isLegalLifecycleTransition(from, to) {
18
+ return getLegalLifecycleTargets(from).includes(to);
19
+ }
20
+ //# sourceMappingURL=transition-matrix.js.map
@@ -13,5 +13,5 @@ export declare function checkFirstCheckbox(content: string, taskId: string): str
13
13
  export declare function checkAllCheckboxes(content: string, taskId: string): string;
14
14
  export declare function completeTask(content: string, taskId: string): string;
15
15
  export declare function updateBlockedReason(content: string, taskId: string, reason: string): string;
16
- export declare function updateTaskStatus(content: string, taskId: string, status: 'todo' | 'in_progress' | 'blocked' | 'done'): string;
16
+ export declare function updateTaskStatus(content: string, taskId: string, status: 'todo' | 'in-progress' | 'blocked' | 'done' | 'dropped'): string;
17
17
  //# sourceMappingURL=helpers.d.ts.map
@@ -2,11 +2,13 @@ import { mkdirSync } from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { coldStartIfNeeded } from '#db/cold-start.js';
4
4
  import { openDb } from '#db/connection.js';
5
+ import { pruneProjectionArtifacts } from '#db/gc.js';
5
6
  import { ingestAll } from '#db/ingester.js';
6
7
  import { resolveBlueprintProjectionDbPath, withProjectionDbWriteLock } from '#db/paths.js';
7
8
  import { recordProjectionMetadata } from './freshness.js';
8
9
  export async function reIngestProjection(cwd) {
9
10
  const target = resolveBlueprintProjectionDbPath(cwd);
11
+ pruneProjectionArtifacts({ preserveDbPath: target });
10
12
  await withProjectionDbWriteLock(cwd, async () => {
11
13
  mkdirSync(path.dirname(target), { recursive: true });
12
14
  const conn = openDb(target);
@@ -49,7 +49,7 @@ export class BlueprintService extends TrackedDocumentService {
49
49
  }
50
50
  parseSummary(content, slug) {
51
51
  const plan = parseBlueprint(content, slug);
52
- const doneCount = plan.tasks.filter((t) => t.status === 'done').length;
52
+ const doneCount = plan.tasks.filter((t) => t.status === 'done' || t.status === 'dropped').length;
53
53
  return {
54
54
  name: plan.name,
55
55
  title: plan.title,
@@ -9,7 +9,7 @@ export async function toBlueprintRecord(filePath, slug, group) {
9
9
  const planStatus = isBlueprintStatus(plan.status) ? plan.status : 'draft';
10
10
  const lastUpdated = plan.lastUpdated ? new Date(plan.lastUpdated) : new Date();
11
11
  const freshness = calculateFreshness(lastUpdated, planStatus);
12
- const tasksCompleted = plan.tasks.filter((task) => task.status === 'done').length;
12
+ const tasksCompleted = plan.tasks.filter((task) => task.status === 'done' || task.status === 'dropped').length;
13
13
  return {
14
14
  name: slug,
15
15
  title: extractTitle(plan.raw) ?? slug,
@@ -42,7 +42,7 @@ export function extractCheckboxStatus(section) {
42
42
  status = 'done';
43
43
  }
44
44
  else if (checked > 0) {
45
- status = 'in_progress';
45
+ status = 'in-progress';
46
46
  }
47
47
  }
48
48
  return { total, checked, status };
@@ -18,8 +18,8 @@ export interface ValidationResult {
18
18
  * Validates that all tasks in a plan are complete.
19
19
  *
20
20
  * A task is considered complete when:
21
- * - status === 'done'
22
- * - All acceptance criteria checkboxes are checked
21
+ * - status === 'done' or 'dropped'
22
+ * - If status === 'done', all acceptance criteria checkboxes are checked
23
23
  *
24
24
  * @param plan - The plan to validate
25
25
  * @returns Validation result with details of incomplete tasks
@@ -11,8 +11,8 @@ import { resolveBlueprintRoot } from './blueprint-root.js';
11
11
  * Validates that all tasks in a plan are complete.
12
12
  *
13
13
  * A task is considered complete when:
14
- * - status === 'done'
15
- * - All acceptance criteria checkboxes are checked
14
+ * - status === 'done' or 'dropped'
15
+ * - If status === 'done', all acceptance criteria checkboxes are checked
16
16
  *
17
17
  * @param plan - The plan to validate
18
18
  * @returns Validation result with details of incomplete tasks
@@ -59,6 +59,9 @@ function findIncompleteTasks(tasks) {
59
59
  * @returns True if task is complete
60
60
  */
61
61
  function isTaskComplete(task) {
62
+ if (task.status === 'dropped') {
63
+ return true;
64
+ }
62
65
  // Task must have status 'done'
63
66
  if (task.status !== 'done') {
64
67
  return false;
@@ -1,3 +1,15 @@
1
+ interface FindPackageAssetOptions {
2
+ readonly moduleUrl?: string;
3
+ readonly cwd?: string;
4
+ readonly execPath?: string;
5
+ readonly argv0?: string;
6
+ readonly argv1?: string;
7
+ }
8
+ /**
9
+ * Walk up from this file's location looking for `relativeFromRoot`. Returns the
10
+ * first existing match, or `null` if none is found within the ancestor budget.
11
+ */
12
+ export declare function findPackageAsset(relativeFromRoot: string, options?: FindPackageAssetOptions): string | null;
1
13
  /**
2
14
  * Walk up from this file's location until the given path (relative to the
3
15
  * package root) is found. Works whether running from src/ or dist/esm/.
@@ -14,4 +26,5 @@ export declare function resolvePackageAsset(relativeFromRoot: string): string;
14
26
  * exist, matching `resolvePackageAsset`'s last-resort behavior.
15
27
  */
16
28
  export declare function resolvePackageAssetPreferred(candidates: readonly string[]): string;
29
+ export {};
17
30
  //# sourceMappingURL=package-assets.d.ts.map
@@ -1,11 +1,22 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import path from 'node:path';
3
- /**
4
- * Walk up from this file's location looking for `relativeFromRoot`. Returns the
5
- * first existing match, or `null` if none is found within the ancestor budget.
6
- */
7
- function findPackageAsset(relativeFromRoot) {
8
- let dir = path.dirname(new URL(import.meta.url).pathname);
3
+ import { fileURLToPath } from 'node:url';
4
+ function isBunVirtualPath(filePath) {
5
+ return filePath === '/$bunfs/root' || filePath.startsWith('/$bunfs/root/');
6
+ }
7
+ function modulePathFromUrl(moduleUrl) {
8
+ try {
9
+ return fileURLToPath(moduleUrl);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ }
15
+ function isUsableStartPath(filePath) {
16
+ return typeof filePath === 'string' && filePath.length > 0 && !isBunVirtualPath(filePath);
17
+ }
18
+ function findFromStartPath(startPath, relativeFromRoot) {
19
+ let dir = path.dirname(startPath);
9
20
  for (let i = 0; i < 8; i++) {
10
21
  const candidate = path.join(dir, relativeFromRoot);
11
22
  if (existsSync(candidate))
@@ -17,6 +28,27 @@ function findPackageAsset(relativeFromRoot) {
17
28
  }
18
29
  return null;
19
30
  }
31
+ /**
32
+ * Walk up from this file's location looking for `relativeFromRoot`. Returns the
33
+ * first existing match, or `null` if none is found within the ancestor budget.
34
+ */
35
+ export function findPackageAsset(relativeFromRoot, options = {}) {
36
+ const starts = [
37
+ modulePathFromUrl(options.moduleUrl ?? import.meta.url),
38
+ options.argv1 ?? process.argv[1],
39
+ options.execPath ?? process.execPath,
40
+ options.argv0 ?? process.argv[0],
41
+ path.join(options.cwd ?? process.cwd(), 'package.json'),
42
+ ];
43
+ for (const start of starts) {
44
+ if (!isUsableStartPath(start))
45
+ continue;
46
+ const found = findFromStartPath(start, relativeFromRoot);
47
+ if (found)
48
+ return found;
49
+ }
50
+ return null;
51
+ }
20
52
  /**
21
53
  * Walk up from this file's location until the given path (relative to the
22
54
  * package root) is found. Works whether running from src/ or dist/esm/.
@@ -0,0 +1,13 @@
1
+ type ExportEntry = string | {
2
+ import?: string | {
3
+ default?: string;
4
+ types?: string;
5
+ };
6
+ default?: string;
7
+ };
8
+ type PackageManifest = {
9
+ exports?: Record<string, ExportEntry>;
10
+ };
11
+ export declare function normalizeTsconfigJsonExports(manifest: PackageManifest): PackageManifest;
12
+ export {};
13
+ //# sourceMappingURL=normalize-tsconfig-json-exports.d.ts.map
@@ -0,0 +1,39 @@
1
+ import { readFileSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ const TSCONFIG_EXPORT_PREFIX = './tsconfig/';
4
+ export function normalizeTsconfigJsonExports(manifest) {
5
+ if (!manifest.exports)
6
+ return manifest;
7
+ let changed = false;
8
+ const normalizedExports = { ...manifest.exports };
9
+ for (const [subpath, entry] of Object.entries(manifest.exports)) {
10
+ if (!subpath.startsWith(TSCONFIG_EXPORT_PREFIX) || !subpath.endsWith('.json'))
11
+ continue;
12
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
13
+ continue;
14
+ if (typeof entry.default === 'string')
15
+ continue;
16
+ const importDefault = typeof entry.import === 'string'
17
+ ? entry.import
18
+ : entry.import && typeof entry.import === 'object'
19
+ ? entry.import.default
20
+ : undefined;
21
+ if (typeof importDefault !== 'string')
22
+ continue;
23
+ normalizedExports[subpath] = {
24
+ ...entry,
25
+ default: importDefault,
26
+ };
27
+ changed = true;
28
+ }
29
+ return changed ? { ...manifest, exports: normalizedExports } : manifest;
30
+ }
31
+ if (import.meta.main) {
32
+ const packageJsonPath = join(process.cwd(), 'package.json');
33
+ const manifest = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
34
+ const normalized = normalizeTsconfigJsonExports(manifest);
35
+ if (normalized !== manifest) {
36
+ writeFileSync(packageJsonPath, `${JSON.stringify(normalized, null, 2)}\n`, 'utf8');
37
+ }
38
+ }
39
+ //# sourceMappingURL=normalize-tsconfig-json-exports.js.map