@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
|
@@ -9,12 +9,10 @@
|
|
|
9
9
|
*
|
|
10
10
|
* We adopt the **two-lock** policy:
|
|
11
11
|
*
|
|
12
|
-
* 1. **Projection DB lock — `'
|
|
13
|
-
* The SQLite file at `getSurfacePath('blueprints/blueprints.db', '
|
|
14
|
-
* is a per-
|
|
15
|
-
*
|
|
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
|
-
* ##
|
|
32
|
+
* ## Non-git fallback
|
|
35
33
|
*
|
|
36
34
|
* Non-git temp repos (most tests, ad-hoc directories) cannot resolve a repo
|
|
37
|
-
* key.
|
|
38
|
-
*
|
|
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
|
|
45
|
+
* Resolve the repo-scoped projection DB path.
|
|
53
46
|
*
|
|
54
|
-
* In a git repo: `<state-root>/<repoKey>/
|
|
55
|
-
* Outside a git repo:
|
|
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
|
|
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
|
|
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 — `'
|
|
13
|
-
* The SQLite file at `getSurfacePath('blueprints/blueprints.db', '
|
|
14
|
-
* is a per-
|
|
15
|
-
*
|
|
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
|
-
* ##
|
|
32
|
+
* ## Non-git fallback
|
|
35
33
|
*
|
|
36
34
|
* Non-git temp repos (most tests, ad-hoc directories) cannot resolve a repo
|
|
37
|
-
* key.
|
|
38
|
-
*
|
|
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 {
|
|
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
|
|
68
|
-
|
|
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
|
|
67
|
+
* Resolve the repo-scoped projection DB path.
|
|
72
68
|
*
|
|
73
|
-
* In a git repo: `<state-root>/<repoKey>/
|
|
74
|
-
* Outside a git repo:
|
|
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, '
|
|
74
|
+
return getSurfacePath(SURFACE_DB, 'repo', cwd);
|
|
79
75
|
}
|
|
80
76
|
catch (err) {
|
|
81
77
|
if (err instanceof NotInGitRepoError)
|
|
82
|
-
return
|
|
78
|
+
return nonGitStatePath(cwd, '.blueprints.db');
|
|
83
79
|
throw err;
|
|
84
80
|
}
|
|
85
81
|
}
|
|
86
82
|
/**
|
|
87
|
-
* Resolve the
|
|
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, '
|
|
87
|
+
return getSurfacePath(SURFACE_DB_LOCK, 'repo', cwd);
|
|
96
88
|
}
|
|
97
89
|
catch (err) {
|
|
98
90
|
if (err instanceof NotInGitRepoError)
|
|
99
|
-
return
|
|
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
|
|
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
|
|
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
|
-
? '
|
|
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
|
-
|
|
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', '
|
|
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' | '
|
|
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,
|
|
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
|
|
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, '
|
|
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
|
|
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' | '
|
|
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,
|
|
@@ -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
|
-
* -
|
|
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
|
-
* -
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
function
|
|
8
|
-
|
|
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
|