@webpresso/agent-kit 0.21.4 → 0.23.0
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 +1 -1
- package/README.md +93 -66
- package/bin/_run.js +143 -1
- package/bin/runtime-manifest.json +40 -0
- package/catalog/AGENTS.md.tpl +7 -6
- package/catalog/agent/commands/plan-refine.md +3 -3
- package/catalog/agent/commands/pll.md +2 -0
- package/catalog/agent/guides/parallel-execution.md +2 -0
- package/catalog/agent/rules/extraction-parity.md +27 -1
- package/catalog/agent/rules/public-package-safety.md +24 -1
- package/catalog/agent/skills/plan-refine/SKILL.md +5 -4
- package/catalog/agent/skills/pll/SKILL.md +1 -0
- package/catalog/base-kit/.github/workflows/ci.webpresso.yml.tmpl +33 -0
- package/catalog/base-kit/commitlint.config.ts.tmpl +1 -3
- package/catalog/base-kit/e2e/fixtures/smoke.html.tmpl +13 -0
- package/catalog/base-kit/e2e/smoke.spec.ts.tmpl +13 -0
- package/catalog/base-kit/oxlint.config.ts.tmpl +26 -0
- package/catalog/base-kit/playwright.config.ts.tmpl +10 -0
- package/catalog/base-kit/src/quality-sample.test.ts.tmpl +19 -0
- package/catalog/base-kit/src/quality-sample.ts.tmpl +11 -0
- package/catalog/base-kit/stryker.config.ts.tmpl +14 -0
- package/catalog/base-kit/tsconfig.json.tmpl +9 -0
- package/catalog/base-kit/vitest.config.ts.tmpl +10 -0
- package/catalog/docs/templates/adr.md +1 -1
- package/catalog/docs/templates/blueprint.md +2 -0
- package/catalog/docs/templates/blueprint.yaml +16 -15
- package/catalog/docs/templates/guide.md +1 -1
- package/catalog/docs/templates/postmortem.md +1 -1
- package/catalog/docs/templates/research.md +1 -1
- package/catalog/docs/templates/runbook.md +1 -1
- package/catalog/docs/templates/system.md +12 -3
- package/catalog/docs/templates/tech-debt.md +1 -0
- package/commands/blueprint.md +10 -12
- package/dist/esm/audit/blueprint-db-consistency.d.ts +1 -1
- package/dist/esm/audit/blueprint-db-consistency.js +6 -8
- package/dist/esm/audit/blueprint-lifecycle-sql.js +10 -3
- package/dist/esm/audit/cloudflare-deploy-contract.d.ts +3 -0
- package/dist/esm/audit/cloudflare-deploy-contract.js +64 -0
- package/dist/esm/audit/no-legacy-cli-bin.d.ts +3 -0
- package/dist/esm/audit/no-legacy-cli-bin.js +100 -0
- package/dist/esm/audit/package-surface.js +14 -1
- package/dist/esm/audit/repo-guardrails.js +40 -13
- package/dist/esm/audit/resolve-audit-script.d.ts +24 -0
- package/dist/esm/audit/resolve-audit-script.js +27 -0
- package/dist/esm/audit/roadmap-links.js +23 -10
- package/dist/esm/blueprint/core/schema.d.ts +8 -8
- package/dist/esm/blueprint/core/schema.js +2 -2
- package/dist/esm/blueprint/db/enums.d.ts +1 -1
- package/dist/esm/blueprint/db/ingester.js +18 -10
- package/dist/esm/blueprint/index.d.ts +0 -1
- package/dist/esm/blueprint/index.js +0 -2
- package/dist/esm/blueprint/lifecycle/audit.js +9 -2
- package/dist/esm/blueprint/lifecycle/local.js +15 -4
- package/dist/esm/blueprint/local.d.ts +0 -3
- package/dist/esm/blueprint/local.js +0 -2
- package/dist/esm/blueprint/service/BlueprintCreationService.js +16 -8
- package/dist/esm/blueprint/service/BlueprintService.js +37 -19
- package/dist/esm/blueprint/service/scanner.js +73 -9
- package/dist/esm/blueprint/tracked-document/schema.d.ts +2 -2
- package/dist/esm/blueprint/utils/document-paths.d.ts +23 -0
- package/dist/esm/blueprint/utils/document-paths.js +91 -0
- package/dist/esm/blueprint/utils/package-assets.d.ts +11 -0
- package/dist/esm/blueprint/utils/package-assets.js +33 -4
- package/dist/esm/build/package-manifest.js +7 -0
- package/dist/esm/build/release-policy.d.ts +27 -0
- package/dist/esm/build/release-policy.js +29 -0
- package/dist/esm/build/runtime-targets.d.ts +13 -0
- package/dist/esm/build/runtime-targets.js +48 -0
- package/dist/esm/build/sync-catalog-doc-templates.d.ts +23 -0
- package/dist/esm/build/sync-catalog-doc-templates.js +93 -0
- package/dist/esm/cli/auto-update/detect-pm.d.ts +15 -0
- package/dist/esm/cli/auto-update/detect-pm.js +24 -9
- package/dist/esm/cli/auto-update/skip.js +9 -1
- package/dist/esm/cli/bundle/agent-command-inventory.d.ts +120 -0
- package/dist/esm/cli/bundle/agent-command-inventory.js +100 -0
- package/dist/esm/cli/bundle/index.d.ts +17 -0
- package/dist/esm/cli/bundle/index.js +15 -0
- package/dist/esm/cli/cli.d.ts +1 -1
- package/dist/esm/cli/cli.js +49 -5
- package/dist/esm/cli/commands/audit-core.d.ts +1 -1
- package/dist/esm/cli/commands/audit.js +4 -7
- package/dist/esm/cli/commands/blueprint/router.js +16 -10
- package/dist/esm/cli/commands/blueprint/template-resolver.js +8 -4
- package/dist/esm/cli/commands/hook.d.ts +8 -0
- package/dist/esm/cli/commands/hook.js +47 -0
- package/dist/esm/cli/commands/init/host-visibility.js +4 -2
- package/dist/esm/cli/commands/init/index.js +80 -7
- package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -0
- package/dist/esm/cli/commands/init/scaffold-base-kit.js +142 -7
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/codex-ownership.js +9 -1
- package/dist/esm/cli/commands/init/scaffolders/agent-hooks/index.js +130 -20
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.d.ts +65 -0
- package/dist/esm/cli/commands/init/scaffolders/agent-kit-global/index.js +64 -0
- package/dist/esm/cli/commands/package-manager.d.ts +15 -0
- package/dist/esm/cli/commands/package-manager.js +42 -0
- package/dist/esm/cli/commands/test.d.ts +1 -0
- package/dist/esm/cli/commands/test.js +2 -1
- package/dist/esm/cli/commands/typecheck.js +10 -19
- package/dist/esm/cli/package-scripts.d.ts +12 -0
- package/dist/esm/cli/package-scripts.js +59 -0
- package/dist/esm/cli/utils.js +3 -22
- package/dist/esm/cli/wp-extensions.d.ts +14 -0
- package/dist/esm/cli/wp-extensions.js +34 -0
- package/dist/esm/config/docs-lint/schemas/common.d.ts +1 -1
- package/dist/esm/config/docs-lint/schemas/implementation-plan.d.ts +2 -2
- package/dist/esm/config/docs-lint/schemas/parent-roadmap.d.ts +1 -1
- package/dist/esm/config/stryker/index.d.ts +85 -0
- package/dist/esm/config/stryker/index.js +31 -0
- package/dist/esm/e2e/command-builder.js +35 -7
- package/dist/esm/e2e/config.d.ts +56 -0
- package/dist/esm/e2e/config.js +114 -0
- package/dist/esm/e2e/execution.js +8 -0
- package/dist/esm/e2e/run-planner.js +2 -0
- package/dist/esm/e2e/types.d.ts +3 -0
- package/dist/esm/format/index.js +5 -1
- package/dist/esm/hooks/guard-switch/index.d.ts +1 -1
- package/dist/esm/hooks/guard-switch/index.js +22 -14
- package/dist/esm/hooks/post-tool/lint-after-edit.d.ts +1 -0
- package/dist/esm/hooks/post-tool/lint-after-edit.js +5 -2
- package/dist/esm/hooks/pretool-guard/validators/file-conventions.js +1 -1
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.d.ts +6 -0
- package/dist/esm/hooks/pretool-guard/validators/forbidden-commands.js +27 -2
- package/dist/esm/hooks/pretool-guard/validators/path-contract.d.ts +2 -1
- package/dist/esm/hooks/pretool-guard/validators/path-contract.js +59 -34
- package/dist/esm/hooks/pretool-guard/validators/plan-frontmatter.js +3 -3
- package/dist/esm/hooks/shared/routing-block.js +18 -4
- package/dist/esm/hooks/shared/validators/blueprint.js +3 -0
- package/dist/esm/hooks/stop/qa-changed-files.d.ts +1 -0
- package/dist/esm/hooks/stop/qa-changed-files.js +5 -2
- package/dist/esm/lint/index.js +3 -1
- package/dist/esm/mcp/auto-discover.d.ts +2 -0
- package/dist/esm/mcp/auto-discover.js +14 -6
- package/dist/esm/mcp/blueprint-server.js +379 -80
- package/dist/esm/mcp/cli.js +21 -0
- package/dist/esm/mcp/runners/test.js +15 -0
- package/dist/esm/mcp/server.d.ts +7 -0
- package/dist/esm/mcp/server.js +16 -27
- package/dist/esm/mcp/tools/_registry.d.ts +3 -0
- package/dist/esm/mcp/tools/_registry.js +21 -0
- package/dist/esm/mcp/tools/audit.d.ts +1 -0
- package/dist/esm/mcp/tools/audit.js +13 -8
- package/dist/esm/mcp/tools/typecheck.js +4 -2
- package/dist/esm/mutation/affected.d.ts +9 -0
- package/dist/esm/mutation/affected.js +36 -0
- package/dist/esm/package.json +8 -0
- package/dist/esm/runtime/package-version.d.ts +2 -0
- package/dist/esm/runtime/package-version.js +43 -0
- package/dist/esm/test/command-builder.d.ts +4 -0
- package/dist/esm/test/command-builder.js +28 -3
- package/dist/esm/test-helpers/hermetic-env.d.ts +25 -0
- package/dist/esm/test-helpers/hermetic-env.js +31 -0
- package/dist/esm/tool-runtime/index.d.ts +5 -0
- package/dist/esm/tool-runtime/index.js +24 -0
- package/dist/esm/tool-runtime/resolve-runner.d.ts +16 -0
- package/dist/esm/tool-runtime/resolve-runner.js +42 -0
- package/dist/esm/typecheck/index.js +4 -2
- package/dist/esm/wp-extension/index.d.ts +50 -0
- package/dist/esm/wp-extension/index.js +268 -0
- package/package.json +75 -46
- package/skills/plan-refine/SKILL.md +5 -4
- package/skills/pll/SKILL.md +1 -0
- package/dist/esm/blueprint/dag/cycle-detector.d.ts +0 -12
- package/dist/esm/blueprint/dag/cycle-detector.js +0 -46
- package/dist/esm/blueprint/dag/executor.d.ts +0 -140
- package/dist/esm/blueprint/dag/executor.js +0 -292
- package/dist/esm/blueprint/dag/index.d.ts +0 -20
- package/dist/esm/blueprint/dag/index.js +0 -17
- package/dist/esm/blueprint/dag/interfaces.d.ts +0 -56
- package/dist/esm/blueprint/dag/interfaces.js +0 -13
- package/dist/esm/blueprint/dag/local/independence.d.ts +0 -107
- package/dist/esm/blueprint/dag/local/independence.js +0 -231
- package/dist/esm/blueprint/dag/local/index.d.ts +0 -14
- package/dist/esm/blueprint/dag/local/index.js +0 -14
- package/dist/esm/blueprint/dag/local/package-graph.d.ts +0 -66
- package/dist/esm/blueprint/dag/local/package-graph.js +0 -148
- package/dist/esm/blueprint/dag/plan-parser.d.ts +0 -54
- package/dist/esm/blueprint/dag/plan-parser.js +0 -236
- package/dist/esm/blueprint/dag/task-graph-algorithms.d.ts +0 -13
- package/dist/esm/blueprint/dag/task-graph-algorithms.js +0 -236
- package/dist/esm/blueprint/dag/task-graph.d.ts +0 -171
- package/dist/esm/blueprint/dag/task-graph.js +0 -370
- package/dist/esm/blueprint/dag/types.d.ts +0 -17
- package/dist/esm/blueprint/dag/types.js +0 -2
- package/dist/esm/blueprint/graph/index.d.ts +0 -5
- package/dist/esm/blueprint/graph/index.js +0 -5
- package/dist/esm/blueprint/graph/mermaid-parser.d.ts +0 -3
- package/dist/esm/blueprint/graph/mermaid-parser.js +0 -93
- package/dist/esm/blueprint/graph/mermaid-serializer.d.ts +0 -3
- package/dist/esm/blueprint/graph/mermaid-serializer.js +0 -20
- package/dist/esm/blueprint/graph/schema.d.ts +0 -89
- package/dist/esm/blueprint/graph/schema.js +0 -104
- package/dist/esm/blueprint/graph/task-graph-adapter.d.ts +0 -6
- package/dist/esm/blueprint/graph/task-graph-adapter.js +0 -30
|
@@ -18,10 +18,12 @@ import path from 'node:path';
|
|
|
18
18
|
import matter from 'gray-matter';
|
|
19
19
|
import { z } from 'zod';
|
|
20
20
|
import { parseBlueprint } from '#core/parser';
|
|
21
|
+
import { setBlueprintFrontmatterFields } from '#lifecycle/engine';
|
|
21
22
|
import { openDb } from '#db/connection.js';
|
|
22
23
|
import { resolveBlueprintProjectionDbPath } from '#db/paths.js';
|
|
23
24
|
import { findTemplate } from '#db/templates.js';
|
|
24
25
|
import { resolveBlueprintRoot } from '#utils/blueprint-root.js';
|
|
26
|
+
import { getBlueprintDocumentPaths } from '#utils/document-paths.js';
|
|
25
27
|
import { evidenceListSchema, canonicalizeEvidenceList } from '#evidence.js';
|
|
26
28
|
import { checkFreshness, readCurrentHead, readProjectionMetadata } from '#freshness.js';
|
|
27
29
|
import { applyVerification, assertAllTasksHaveCanonicalPassingEvidence, readTaskVerification, } from '#verification.js';
|
|
@@ -93,6 +95,13 @@ async function resolveSyncAdapter(cwd) {
|
|
|
93
95
|
};
|
|
94
96
|
}
|
|
95
97
|
const DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS = 5_000;
|
|
98
|
+
function todayIsoDate() {
|
|
99
|
+
return new Date().toISOString().split('T')[0] ?? new Date().toISOString();
|
|
100
|
+
}
|
|
101
|
+
function formatBlueprintProgress(totalTasks, doneTasks, blockedTasks) {
|
|
102
|
+
const percent = totalTasks === 0 ? 0 : Math.round((doneTasks / totalTasks) * 100);
|
|
103
|
+
return `${percent}% (${doneTasks}/${totalTasks} tasks done, ${blockedTasks} blocked, updated ${todayIsoDate()})`;
|
|
104
|
+
}
|
|
96
105
|
function readPlatformMutationTimeoutMs() {
|
|
97
106
|
const parsed = Number.parseInt(process.env['WP_BLUEPRINT_PLATFORM_MUTATION_TIMEOUT_MS'] ??
|
|
98
107
|
String(DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS), 10);
|
|
@@ -242,11 +251,25 @@ function openDbRW(cwd) {
|
|
|
242
251
|
async function reIngest(cwd) {
|
|
243
252
|
await reIngestProjection(cwd);
|
|
244
253
|
}
|
|
254
|
+
async function persistBlueprintMarkdown(input) {
|
|
255
|
+
const { projectCwd, slug, blueprintPath, markdown } = input;
|
|
256
|
+
mkdirSync(path.dirname(blueprintPath), { recursive: true });
|
|
257
|
+
parseBlueprint(markdown, slug);
|
|
258
|
+
writeFileSync(blueprintPath, markdown, 'utf8');
|
|
259
|
+
await reIngest(projectCwd);
|
|
260
|
+
const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
|
|
261
|
+
if (!refreshed.blueprint) {
|
|
262
|
+
throw new Error(`Blueprint "${slug}" did not appear in the projection after write`);
|
|
263
|
+
}
|
|
264
|
+
return refreshed.blueprint;
|
|
265
|
+
}
|
|
245
266
|
function findBlueprintDir(blueprintRoot, slug, states) {
|
|
246
267
|
for (const state of states) {
|
|
247
|
-
const
|
|
248
|
-
if (existsSync(
|
|
249
|
-
return { dir:
|
|
268
|
+
const paths = getBlueprintDocumentPaths(blueprintRoot, state, slug);
|
|
269
|
+
if (existsSync(paths.flat))
|
|
270
|
+
return { dir: path.dirname(paths.flat), path: paths.flat, shape: 'flat', state };
|
|
271
|
+
if (existsSync(paths.directory))
|
|
272
|
+
return { dir: paths.directory, path: paths.folder, shape: 'folder', state };
|
|
250
273
|
}
|
|
251
274
|
return null;
|
|
252
275
|
}
|
|
@@ -537,7 +560,7 @@ async function handleNew(cwd, raw) {
|
|
|
537
560
|
}
|
|
538
561
|
const b = bytes(template);
|
|
539
562
|
const slug = titleToSlug(title);
|
|
540
|
-
const targetPath =
|
|
563
|
+
const targetPath = getBlueprintDocumentPaths(resolveBlueprintRoot(cwd), 'draft', slug).flat;
|
|
541
564
|
// Platform-first path: push event to register the blueprint before returning the scaffold.
|
|
542
565
|
// Iron rule: resolveSyncAdapter() returns null when WP_BLUEPRINT_PLATFORM_DISABLED=1.
|
|
543
566
|
const adapter = await resolveSyncAdapter(cwd);
|
|
@@ -866,7 +889,7 @@ async function handleTaskVerify(projectResolver, cwd, raw) {
|
|
|
866
889
|
if (!found) {
|
|
867
890
|
return err('wp_blueprint_task_verify failed', `Blueprint "${slug}" not found in any state directory`);
|
|
868
891
|
}
|
|
869
|
-
const filePath =
|
|
892
|
+
const filePath = found.path;
|
|
870
893
|
if (!existsSync(filePath)) {
|
|
871
894
|
return err('wp_blueprint_task_verify failed', `Blueprint overview not found at ${filePath}`);
|
|
872
895
|
}
|
|
@@ -961,12 +984,8 @@ async function handlePromote(projectResolver, cwd, raw) {
|
|
|
961
984
|
const found = findBlueprintDir(root, slug, ALL_STATES);
|
|
962
985
|
if (!found)
|
|
963
986
|
return err('wp_blueprint_promote failed', `Blueprint "${slug}" not found in any state directory`);
|
|
964
|
-
const {
|
|
965
|
-
const overviewPath = path
|
|
966
|
-
const ts = readVt(projectCwd);
|
|
967
|
-
const mtime = existsSync(overviewPath) ? statSync(overviewPath).mtimeMs : 0;
|
|
968
|
-
if ((ts[slug] ?? 0) < mtime)
|
|
969
|
-
return err('wp_blueprint_promote refused', `Blueprint "${slug}" not validated since last write. Run wp_blueprint_validate first.`);
|
|
987
|
+
const { state: currentState } = found;
|
|
988
|
+
const overviewPath = found.path;
|
|
970
989
|
if (to_state === 'completed') {
|
|
971
990
|
try {
|
|
972
991
|
assertBlueprintCanComplete(overviewPath, slug);
|
|
@@ -999,40 +1018,34 @@ async function handlePromote(projectResolver, cwd, raw) {
|
|
|
999
1018
|
catch (e) {
|
|
1000
1019
|
return err('wp_blueprint_promote failed', toStr(e));
|
|
1001
1020
|
}
|
|
1002
|
-
const { renameSync } = await import('node:fs');
|
|
1003
|
-
const destDir = path.join(root, to_state, slug);
|
|
1004
|
-
mkdirSync(path.dirname(destDir), { recursive: true });
|
|
1005
1021
|
try {
|
|
1006
|
-
|
|
1022
|
+
const transitioned = await applyLocalBlueprintTransition({
|
|
1023
|
+
found,
|
|
1024
|
+
projectCwd,
|
|
1025
|
+
slug,
|
|
1026
|
+
to_state,
|
|
1027
|
+
});
|
|
1028
|
+
const payload = {
|
|
1029
|
+
summary: `Blueprint "${slug}" promoted from "${currentState}" to "${to_state}"`,
|
|
1030
|
+
slug,
|
|
1031
|
+
from_state: currentState,
|
|
1032
|
+
to_state,
|
|
1033
|
+
new_path: transitioned.overviewPath,
|
|
1034
|
+
status: transitioned.blueprint.status,
|
|
1035
|
+
content_hash: transitioned.blueprint.content_hash,
|
|
1036
|
+
revision: transitioned.blueprint.content_hash,
|
|
1037
|
+
ingested_at: transitioned.blueprint.ingested_at,
|
|
1038
|
+
failures: [],
|
|
1039
|
+
bytes: 0,
|
|
1040
|
+
tokensSaved: 0,
|
|
1041
|
+
};
|
|
1042
|
+
if (currentState === 'draft' && to_state === 'planned')
|
|
1043
|
+
appendHint(payload, projectCwd, 'PLAN_REFINE');
|
|
1044
|
+
return finishPayload(payload);
|
|
1007
1045
|
}
|
|
1008
1046
|
catch (e) {
|
|
1009
|
-
return err('wp_blueprint_promote failed',
|
|
1010
|
-
}
|
|
1011
|
-
const destOverview = path.join(destDir, '_overview.md');
|
|
1012
|
-
if (existsSync(destOverview)) {
|
|
1013
|
-
const fm = matter(readFileSync(destOverview, 'utf8'));
|
|
1014
|
-
fm.data['status'] = to_state;
|
|
1015
|
-
writeFileSync(destOverview, matter.stringify(fm.content, fm.data), 'utf8');
|
|
1016
|
-
}
|
|
1017
|
-
try {
|
|
1018
|
-
await reIngest(projectCwd);
|
|
1019
|
-
}
|
|
1020
|
-
catch {
|
|
1021
|
-
/* non-fatal */
|
|
1047
|
+
return err('wp_blueprint_promote failed', toStr(e));
|
|
1022
1048
|
}
|
|
1023
|
-
const payload = {
|
|
1024
|
-
summary: `Blueprint "${slug}" promoted from "${currentState}" to "${to_state}"`,
|
|
1025
|
-
slug,
|
|
1026
|
-
from_state: currentState,
|
|
1027
|
-
to_state,
|
|
1028
|
-
new_path: destOverview,
|
|
1029
|
-
failures: [],
|
|
1030
|
-
bytes: 0,
|
|
1031
|
-
tokensSaved: 0,
|
|
1032
|
-
};
|
|
1033
|
-
if (currentState === 'draft' && to_state === 'planned')
|
|
1034
|
-
appendHint(payload, projectCwd, 'PLAN_REFINE');
|
|
1035
|
-
return finishPayload(payload);
|
|
1036
1049
|
}
|
|
1037
1050
|
const finalizeSchema = z.object({ project_id: z.string().optional(), slug: z.string() });
|
|
1038
1051
|
async function handleFinalize(projectResolver, cwd, raw) {
|
|
@@ -1075,7 +1088,7 @@ async function handleFinalize(projectResolver, cwd, raw) {
|
|
|
1075
1088
|
return err('wp_blueprint_finalize failed', `Blueprint "${slug}" not found`);
|
|
1076
1089
|
}
|
|
1077
1090
|
try {
|
|
1078
|
-
assertBlueprintCanComplete(
|
|
1091
|
+
assertBlueprintCanComplete(found.path, slug);
|
|
1079
1092
|
}
|
|
1080
1093
|
catch (error) {
|
|
1081
1094
|
return err('wp_blueprint_finalize refused', toStr(error));
|
|
@@ -1102,39 +1115,32 @@ async function handleFinalize(projectResolver, cwd, raw) {
|
|
|
1102
1115
|
catch (e) {
|
|
1103
1116
|
return err('wp_blueprint_finalize failed', toStr(e));
|
|
1104
1117
|
}
|
|
1105
|
-
const { renameSync } = await import('node:fs');
|
|
1106
|
-
const destDir = path.join(root, 'completed', slug);
|
|
1107
|
-
mkdirSync(path.dirname(destDir), { recursive: true });
|
|
1108
1118
|
try {
|
|
1109
|
-
|
|
1119
|
+
const transitioned = await applyLocalBlueprintTransition({
|
|
1120
|
+
found,
|
|
1121
|
+
projectCwd,
|
|
1122
|
+
slug,
|
|
1123
|
+
to_state: 'completed',
|
|
1124
|
+
});
|
|
1125
|
+
const payload = {
|
|
1126
|
+
summary: `Blueprint "${slug}" finalized and moved to completed`,
|
|
1127
|
+
slug,
|
|
1128
|
+
new_path: transitioned.overviewPath,
|
|
1129
|
+
status: transitioned.blueprint.status,
|
|
1130
|
+
content_hash: transitioned.blueprint.content_hash,
|
|
1131
|
+
revision: transitioned.blueprint.content_hash,
|
|
1132
|
+
ingested_at: transitioned.blueprint.ingested_at,
|
|
1133
|
+
failures: [],
|
|
1134
|
+
bytes: 0,
|
|
1135
|
+
tokensSaved: 0,
|
|
1136
|
+
};
|
|
1137
|
+
if (hasRecentAuditFinding(projectCwd))
|
|
1138
|
+
appendHint(payload, projectCwd, 'AUDIT_FIX');
|
|
1139
|
+
return finishPayload(payload);
|
|
1110
1140
|
}
|
|
1111
1141
|
catch (e) {
|
|
1112
|
-
return err('wp_blueprint_finalize failed',
|
|
1113
|
-
}
|
|
1114
|
-
const destOverview = path.join(destDir, '_overview.md');
|
|
1115
|
-
if (existsSync(destOverview)) {
|
|
1116
|
-
const fm = matter(readFileSync(destOverview, 'utf8'));
|
|
1117
|
-
fm.data['status'] = 'completed';
|
|
1118
|
-
fm.data['completed_at'] = new Date().toISOString().split('T')[0] ?? '';
|
|
1119
|
-
writeFileSync(destOverview, matter.stringify(fm.content, fm.data), 'utf8');
|
|
1120
|
-
}
|
|
1121
|
-
try {
|
|
1122
|
-
await reIngest(projectCwd);
|
|
1123
|
-
}
|
|
1124
|
-
catch {
|
|
1125
|
-
/* non-fatal */
|
|
1142
|
+
return err('wp_blueprint_finalize failed', toStr(e));
|
|
1126
1143
|
}
|
|
1127
|
-
const payload = {
|
|
1128
|
-
summary: `Blueprint "${slug}" finalized and moved to completed`,
|
|
1129
|
-
slug,
|
|
1130
|
-
new_path: destOverview,
|
|
1131
|
-
failures: [],
|
|
1132
|
-
bytes: 0,
|
|
1133
|
-
tokensSaved: 0,
|
|
1134
|
-
};
|
|
1135
|
-
if (hasRecentAuditFinding(projectCwd))
|
|
1136
|
-
appendHint(payload, projectCwd, 'AUDIT_FIX');
|
|
1137
|
-
return finishPayload(payload);
|
|
1138
1144
|
}
|
|
1139
1145
|
function assertBlueprintCanComplete(overviewPath, slug) {
|
|
1140
1146
|
const markdown = readFileSync(overviewPath, 'utf8');
|
|
@@ -1698,6 +1704,245 @@ const createSchema = MutationTarget.extend({
|
|
|
1698
1704
|
request_id: z.string().min(1).optional(),
|
|
1699
1705
|
head_at_ingest: z.string().nullable().optional(),
|
|
1700
1706
|
});
|
|
1707
|
+
const putTaskSchema = z.object({
|
|
1708
|
+
id: z.string().min(1),
|
|
1709
|
+
title: z.string().min(1),
|
|
1710
|
+
status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'dropped']).default('todo'),
|
|
1711
|
+
wave: z.string().optional(),
|
|
1712
|
+
lane: z.string().optional(),
|
|
1713
|
+
description: z.string().optional(),
|
|
1714
|
+
acceptance: z.array(z.string().min(1)).min(1),
|
|
1715
|
+
});
|
|
1716
|
+
const putDocumentSchema = z.object({
|
|
1717
|
+
type: z.literal('blueprint').default('blueprint'),
|
|
1718
|
+
title: z.string().min(1),
|
|
1719
|
+
status: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
|
|
1720
|
+
complexity: z.enum(['XS', 'S', 'M', 'L', 'XL']),
|
|
1721
|
+
owner: z.string().min(1),
|
|
1722
|
+
created: z.string().min(1),
|
|
1723
|
+
last_updated: z.string().min(1),
|
|
1724
|
+
progress: z.string().optional(),
|
|
1725
|
+
tags: z.array(z.string()).optional(),
|
|
1726
|
+
product_wedge_anchor: z.object({
|
|
1727
|
+
stage_outcome: z.string().min(1),
|
|
1728
|
+
consuming_surface: z.string().min(1),
|
|
1729
|
+
new_user_visible_capability: z.string().min(1),
|
|
1730
|
+
}),
|
|
1731
|
+
summary: z.string().min(1),
|
|
1732
|
+
tasks: z.array(putTaskSchema).min(1),
|
|
1733
|
+
});
|
|
1734
|
+
const putSchema = MutationTarget.extend({
|
|
1735
|
+
slug: z.string().min(1),
|
|
1736
|
+
document: putDocumentSchema,
|
|
1737
|
+
request_id: z.string().min(1).optional(),
|
|
1738
|
+
head_at_ingest: z.string().nullable().optional(),
|
|
1739
|
+
});
|
|
1740
|
+
function renderBlueprintMarkdownFromDocument(slug, document) {
|
|
1741
|
+
const frontmatter = [
|
|
1742
|
+
'---',
|
|
1743
|
+
`type: ${document.type}`,
|
|
1744
|
+
`title: ${document.title}`,
|
|
1745
|
+
`status: ${document.status}`,
|
|
1746
|
+
`complexity: ${document.complexity}`,
|
|
1747
|
+
`owner: ${document.owner}`,
|
|
1748
|
+
`created: '${document.created}'`,
|
|
1749
|
+
`last_updated: '${document.last_updated}'`,
|
|
1750
|
+
...(document.progress ? [`progress: ${JSON.stringify(document.progress)}`] : []),
|
|
1751
|
+
...(document.tags && document.tags.length > 0
|
|
1752
|
+
? ['tags:', ...document.tags.map((tag) => ` - ${tag}`)]
|
|
1753
|
+
: []),
|
|
1754
|
+
'---',
|
|
1755
|
+
'',
|
|
1756
|
+
];
|
|
1757
|
+
const sections = [
|
|
1758
|
+
`# ${document.title}`,
|
|
1759
|
+
'',
|
|
1760
|
+
'## Product wedge anchor',
|
|
1761
|
+
'',
|
|
1762
|
+
`- **Stage outcome:** ${document.product_wedge_anchor.stage_outcome}`,
|
|
1763
|
+
`- **Consuming surface:** ${document.product_wedge_anchor.consuming_surface}`,
|
|
1764
|
+
`- **New user-visible capability:** ${document.product_wedge_anchor.new_user_visible_capability}`,
|
|
1765
|
+
'',
|
|
1766
|
+
'## Summary',
|
|
1767
|
+
'',
|
|
1768
|
+
document.summary,
|
|
1769
|
+
'',
|
|
1770
|
+
];
|
|
1771
|
+
const taskBlocks = document.tasks.flatMap((task) => [
|
|
1772
|
+
`#### Task ${task.id}: ${task.title}`,
|
|
1773
|
+
'',
|
|
1774
|
+
`**Status:** ${task.status}`,
|
|
1775
|
+
...(task.wave ? [`**Wave:** ${task.wave}`] : []),
|
|
1776
|
+
...(task.lane ? [`**Lane:** ${task.lane}`] : []),
|
|
1777
|
+
...(task.description ? ['', task.description] : []),
|
|
1778
|
+
'',
|
|
1779
|
+
'**Acceptance:**',
|
|
1780
|
+
...task.acceptance.map((item) => `- [ ] ${item}`),
|
|
1781
|
+
'',
|
|
1782
|
+
]);
|
|
1783
|
+
return [...frontmatter, ...sections, ...taskBlocks].join('\n').trimEnd() + '\n';
|
|
1784
|
+
}
|
|
1785
|
+
async function handleBlueprintPut(projectResolver, cwd, raw) {
|
|
1786
|
+
const p = putSchema.safeParse(raw);
|
|
1787
|
+
if (!p.success)
|
|
1788
|
+
return err('wp_blueprint_put validation error', p.error.message);
|
|
1789
|
+
const { project_id, slug, document, request_id, head_at_ingest } = p.data;
|
|
1790
|
+
const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
|
|
1791
|
+
if ('content' in resolvedProject)
|
|
1792
|
+
return resolvedProject;
|
|
1793
|
+
const projectCwd = resolvedProject.cwd;
|
|
1794
|
+
await ensureProjectionReady(projectCwd);
|
|
1795
|
+
const freshnessFailure = validateMutationFreshnessToken(projectCwd, head_at_ingest, 'wp_blueprint_put', 'wp_blueprint_get');
|
|
1796
|
+
if (freshnessFailure)
|
|
1797
|
+
return freshnessFailure;
|
|
1798
|
+
const payloadHash = hashMutationPayload({ slug, document });
|
|
1799
|
+
const replay = request_id !== undefined
|
|
1800
|
+
? readMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash)
|
|
1801
|
+
: null;
|
|
1802
|
+
if (replay)
|
|
1803
|
+
return replay;
|
|
1804
|
+
const root = resolveBlueprintRoot(projectCwd);
|
|
1805
|
+
const found = findBlueprintDir(root, slug, ALL_STATES);
|
|
1806
|
+
if (found && found.state !== document.status) {
|
|
1807
|
+
return err('wp_blueprint_put refused', `Blueprint "${slug}" currently lives in "${found.state}" and cannot be rewritten as "${document.status}" without a lifecycle transition.`);
|
|
1808
|
+
}
|
|
1809
|
+
if (!found && document.status !== 'draft') {
|
|
1810
|
+
return err('wp_blueprint_put refused', `New blueprint "${slug}" must start in "draft"; use wp_blueprint_transition for later lifecycle moves.`);
|
|
1811
|
+
}
|
|
1812
|
+
const overviewPath = found
|
|
1813
|
+
? found.path
|
|
1814
|
+
: getBlueprintDocumentPaths(root, document.status, slug).flat;
|
|
1815
|
+
try {
|
|
1816
|
+
const markdown = renderBlueprintMarkdownFromDocument(slug, document);
|
|
1817
|
+
const blueprint = await persistBlueprintMarkdown({
|
|
1818
|
+
projectCwd,
|
|
1819
|
+
slug,
|
|
1820
|
+
blueprintPath: overviewPath,
|
|
1821
|
+
markdown,
|
|
1822
|
+
});
|
|
1823
|
+
const payload = {
|
|
1824
|
+
summary: `Blueprint "${slug}" written to ${overviewPath}`,
|
|
1825
|
+
slug,
|
|
1826
|
+
path: overviewPath,
|
|
1827
|
+
status: blueprint.status,
|
|
1828
|
+
content_hash: blueprint.content_hash,
|
|
1829
|
+
ingested_at: blueprint.ingested_at,
|
|
1830
|
+
revision: blueprint.content_hash,
|
|
1831
|
+
idempotent: false,
|
|
1832
|
+
failures: [],
|
|
1833
|
+
next_action: makeNextAction('verify_task', 'Blueprint written. Next: validate or transition the latest revision token through the structured surface.'),
|
|
1834
|
+
project_id: resolvedProject.project_id ?? projectCwd,
|
|
1835
|
+
};
|
|
1836
|
+
if (request_id !== undefined) {
|
|
1837
|
+
recordMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash, payload);
|
|
1838
|
+
}
|
|
1839
|
+
return finishPayload(payload);
|
|
1840
|
+
}
|
|
1841
|
+
catch (e) {
|
|
1842
|
+
return err('wp_blueprint_put failed', toStr(e));
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
const transitionSchema = MutationTarget.extend({
|
|
1846
|
+
slug: z.string().min(1),
|
|
1847
|
+
to_state: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
|
|
1848
|
+
expected_version: z.string().min(1),
|
|
1849
|
+
});
|
|
1850
|
+
async function handleBlueprintTransition(projectResolver, cwd, raw) {
|
|
1851
|
+
const p = transitionSchema.safeParse(raw);
|
|
1852
|
+
if (!p.success)
|
|
1853
|
+
return err('wp_blueprint_transition validation error', p.error.message);
|
|
1854
|
+
const { project_id, slug, to_state, expected_version } = p.data;
|
|
1855
|
+
const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
|
|
1856
|
+
if ('content' in resolvedProject)
|
|
1857
|
+
return resolvedProject;
|
|
1858
|
+
const projectCwd = resolvedProject.cwd;
|
|
1859
|
+
await ensureProjectionReady(projectCwd);
|
|
1860
|
+
const target = dbPath(projectCwd);
|
|
1861
|
+
if (!existsSync(target))
|
|
1862
|
+
return err('wp_blueprint_transition failed', 'Blueprint DB not found');
|
|
1863
|
+
const current = getCurrentProjectBlueprint(projectCwd, slug);
|
|
1864
|
+
if (!current.blueprint) {
|
|
1865
|
+
return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found`);
|
|
1866
|
+
}
|
|
1867
|
+
if (current.blueprint.content_hash !== expected_version) {
|
|
1868
|
+
return jsonContent({
|
|
1869
|
+
summary: `wp_blueprint_transition rejected a stale blueprint revision`,
|
|
1870
|
+
failures: [
|
|
1871
|
+
`expected_version "${expected_version}" does not match current content hash "${current.blueprint.content_hash}"`,
|
|
1872
|
+
],
|
|
1873
|
+
error: 'stale_blueprint_revision',
|
|
1874
|
+
next_action: makeNextAction('reingest_project', 'Call wp_blueprint_get again to fetch the latest content_hash before retrying the transition.'),
|
|
1875
|
+
bytes: 0,
|
|
1876
|
+
tokensSaved: 0,
|
|
1877
|
+
}, true);
|
|
1878
|
+
}
|
|
1879
|
+
const root = resolveBlueprintRoot(projectCwd);
|
|
1880
|
+
const found = findBlueprintDir(root, slug, ALL_STATES);
|
|
1881
|
+
if (!found)
|
|
1882
|
+
return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found on disk`);
|
|
1883
|
+
try {
|
|
1884
|
+
const refreshed = await applyLocalBlueprintTransition({
|
|
1885
|
+
found,
|
|
1886
|
+
projectCwd,
|
|
1887
|
+
slug,
|
|
1888
|
+
to_state,
|
|
1889
|
+
});
|
|
1890
|
+
return finishPayload({
|
|
1891
|
+
summary: `Blueprint "${slug}" transitioned to ${to_state}`,
|
|
1892
|
+
slug,
|
|
1893
|
+
old_status: current.blueprint.status,
|
|
1894
|
+
new_status: to_state,
|
|
1895
|
+
status: refreshed.blueprint.status,
|
|
1896
|
+
content_hash: refreshed.blueprint.content_hash,
|
|
1897
|
+
revision: refreshed.blueprint.content_hash,
|
|
1898
|
+
ingested_at: refreshed.blueprint.ingested_at,
|
|
1899
|
+
failures: [],
|
|
1900
|
+
project_id: resolvedProject.project_id ?? projectCwd,
|
|
1901
|
+
});
|
|
1902
|
+
}
|
|
1903
|
+
catch (e) {
|
|
1904
|
+
return err('wp_blueprint_transition failed', toStr(e));
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
async function applyLocalBlueprintTransition(input) {
|
|
1908
|
+
const { projectCwd, slug, to_state, found } = input;
|
|
1909
|
+
const root = resolveBlueprintRoot(projectCwd);
|
|
1910
|
+
const overviewPath = found.path;
|
|
1911
|
+
const parsed = runValidate(overviewPath);
|
|
1912
|
+
if (!parsed.valid) {
|
|
1913
|
+
throw new Error(parsed.gaps.join('; '));
|
|
1914
|
+
}
|
|
1915
|
+
const markdown = readFileSync(overviewPath, 'utf8');
|
|
1916
|
+
const currentBlueprint = parseBlueprint(markdown, slug);
|
|
1917
|
+
const updated = setBlueprintFrontmatterFields(markdown, {
|
|
1918
|
+
status: to_state,
|
|
1919
|
+
last_updated: todayIsoDate(),
|
|
1920
|
+
completed_at: to_state === 'completed' ? todayIsoDate() : undefined,
|
|
1921
|
+
progress: formatBlueprintProgress(currentBlueprint.tasks.length, currentBlueprint.tasks.filter((task) => task.status === 'done').length, currentBlueprint.tasks.filter((task) => task.status === 'blocked').length),
|
|
1922
|
+
});
|
|
1923
|
+
parseBlueprint(updated, slug);
|
|
1924
|
+
const destination = getBlueprintDocumentPaths(root, to_state, slug);
|
|
1925
|
+
mkdirSync(path.dirname(found.shape === 'flat' ? destination.flat : destination.directory), {
|
|
1926
|
+
recursive: true,
|
|
1927
|
+
});
|
|
1928
|
+
let finalOverviewPath = overviewPath;
|
|
1929
|
+
if (found.state !== to_state) {
|
|
1930
|
+
const { renameSync } = await import('node:fs');
|
|
1931
|
+
renameSync(found.shape === 'flat' ? overviewPath : found.dir, found.shape === 'flat' ? destination.flat : destination.directory);
|
|
1932
|
+
finalOverviewPath = found.shape === 'flat' ? destination.flat : destination.folder;
|
|
1933
|
+
}
|
|
1934
|
+
writeFileSync(finalOverviewPath, updated, 'utf8');
|
|
1935
|
+
await reIngest(projectCwd);
|
|
1936
|
+
const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
|
|
1937
|
+
if (!refreshed.blueprint) {
|
|
1938
|
+
throw new Error(`Blueprint "${slug}" did not appear in the projection after transition`);
|
|
1939
|
+
}
|
|
1940
|
+
return {
|
|
1941
|
+
blueprint: refreshed.blueprint,
|
|
1942
|
+
overviewPath: finalOverviewPath,
|
|
1943
|
+
fromState: found.state,
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1701
1946
|
async function handleBlueprintCreate(projectResolver, cwd, raw) {
|
|
1702
1947
|
const p = createSchema.safeParse(raw);
|
|
1703
1948
|
if (!p.success)
|
|
@@ -1720,17 +1965,19 @@ async function handleBlueprintCreate(projectResolver, cwd, raw) {
|
|
|
1720
1965
|
const today = new Date().toISOString().split('T')[0] ?? '';
|
|
1721
1966
|
const slug = titleToSlug(title);
|
|
1722
1967
|
const root = resolveBlueprintRoot(projectCwd);
|
|
1723
|
-
const
|
|
1724
|
-
const overviewPath = path.join(targetDir, '_overview.md');
|
|
1968
|
+
const overviewPath = getBlueprintDocumentPaths(root, 'draft', slug).flat;
|
|
1725
1969
|
try {
|
|
1726
|
-
mkdirSync(
|
|
1970
|
+
mkdirSync(path.dirname(overviewPath), { recursive: true });
|
|
1727
1971
|
const content = BLUEPRINT_TEMPLATE.replace(/{TITLE}/g, title)
|
|
1728
1972
|
.replace(/{COMPLEXITY}/g, complexity)
|
|
1729
1973
|
.replace(/{DATE}/g, today)
|
|
1730
1974
|
.replace('{GOAL}', goal);
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1975
|
+
await persistBlueprintMarkdown({
|
|
1976
|
+
projectCwd,
|
|
1977
|
+
slug,
|
|
1978
|
+
blueprintPath: overviewPath,
|
|
1979
|
+
markdown: content,
|
|
1980
|
+
});
|
|
1734
1981
|
const b = bytes(content);
|
|
1735
1982
|
const payload = {
|
|
1736
1983
|
summary: `Blueprint "${slug}" created at ${overviewPath}`,
|
|
@@ -1788,12 +2035,64 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
1788
2035
|
},
|
|
1789
2036
|
required: ['title', 'goal_prompt'],
|
|
1790
2037
|
}, undefined, (r) => handleNew(cwd, r), { title: 'Blueprint New', readOnlyHint: true, openWorldHint: false });
|
|
1791
|
-
registrar.registerTool('
|
|
2038
|
+
registrar.registerTool('wp_blueprint_put', 'Create or replace a blueprint from a structured whole-document payload. Renders markdown, validates it, re-ingests the projection, and returns revision metadata.', {
|
|
2039
|
+
type: 'object',
|
|
2040
|
+
properties: {
|
|
2041
|
+
project_id: { type: 'string' },
|
|
2042
|
+
slug: { type: 'string' },
|
|
2043
|
+
document: { type: 'object' },
|
|
2044
|
+
request_id: { type: 'string' },
|
|
2045
|
+
head_at_ingest: { type: ['string', 'null'] },
|
|
2046
|
+
},
|
|
2047
|
+
required: ['project_id', 'slug', 'document'],
|
|
2048
|
+
}, {
|
|
2049
|
+
...summaryEnvelopeOutputSchema,
|
|
2050
|
+
properties: {
|
|
2051
|
+
...summaryEnvelopeOutputSchema.properties,
|
|
2052
|
+
slug: { type: 'string' },
|
|
2053
|
+
path: { type: 'string' },
|
|
2054
|
+
status: { type: 'string' },
|
|
2055
|
+
content_hash: { type: 'string' },
|
|
2056
|
+
ingested_at: { type: 'number' },
|
|
2057
|
+
revision: { type: 'string' },
|
|
2058
|
+
idempotent: { type: 'boolean' },
|
|
2059
|
+
project_id: { type: 'string' },
|
|
2060
|
+
next_action: nextActionOutputSchema,
|
|
2061
|
+
},
|
|
2062
|
+
}, (r) => handleBlueprintPut(projectResolver, cwd, r), { title: 'Blueprint Put', destructiveHint: false, openWorldHint: false });
|
|
2063
|
+
registrar.registerTool('wp_blueprint_transition', 'Transition a blueprint lifecycle state using an expected revision token. Revalidates the latest markdown, rewrites frontmatter atomically, re-ingests the projection, and returns updated revision metadata.', {
|
|
2064
|
+
type: 'object',
|
|
2065
|
+
properties: {
|
|
2066
|
+
project_id: { type: 'string' },
|
|
2067
|
+
slug: { type: 'string' },
|
|
2068
|
+
to_state: {
|
|
2069
|
+
type: 'string',
|
|
2070
|
+
enum: ['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived'],
|
|
2071
|
+
},
|
|
2072
|
+
expected_version: { type: 'string' },
|
|
2073
|
+
},
|
|
2074
|
+
required: ['project_id', 'slug', 'to_state', 'expected_version'],
|
|
2075
|
+
}, {
|
|
2076
|
+
...summaryEnvelopeOutputSchema,
|
|
2077
|
+
properties: {
|
|
2078
|
+
...summaryEnvelopeOutputSchema.properties,
|
|
2079
|
+
slug: { type: 'string' },
|
|
2080
|
+
old_status: { type: 'string' },
|
|
2081
|
+
new_status: { type: 'string' },
|
|
2082
|
+
status: { type: 'string' },
|
|
2083
|
+
content_hash: { type: 'string' },
|
|
2084
|
+
revision: { type: 'string' },
|
|
2085
|
+
ingested_at: { type: 'number' },
|
|
2086
|
+
project_id: { type: 'string' },
|
|
2087
|
+
next_action: nextActionOutputSchema,
|
|
2088
|
+
},
|
|
2089
|
+
}, (r) => handleBlueprintTransition(projectResolver, cwd, r), { title: 'Blueprint Transition', destructiveHint: false, openWorldHint: false });
|
|
2090
|
+
registrar.registerTool('wp_blueprint_validate', 'Validate canonical blueprint markdown structure (`<slug>.md` or `<slug>/_overview.md`). Returns { valid, gaps }. Must pass before wp_blueprint_promote.', { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] }, undefined, (r) => handleValidate(cwd, r), { title: 'Blueprint Validate', readOnlyHint: false, openWorldHint: false });
|
|
1792
2091
|
registrar.registerTool('wp_blueprint_task_next', 'Return the next ready task (all deps done). Accepts optional project_id for nested-workspace disambiguation. Returns { summary, task }.', {
|
|
1793
2092
|
type: 'object',
|
|
1794
2093
|
properties: { blueprint: { type: 'string' }, project_id: { type: 'string' } },
|
|
1795
2094
|
}, undefined, (r) => handleTaskNext(projectResolver, cwd, r), { title: 'Blueprint Task Next', readOnlyHint: true, openWorldHint: false });
|
|
1796
|
-
registrar.registerTool('wp_blueprint_task_advance', 'Advance task status. Edits
|
|
2095
|
+
registrar.registerTool('wp_blueprint_task_advance', 'Advance task status. Edits the canonical blueprint markdown and re-syncs DB. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_get/wp_blueprint_list to reject stale writes. Returns { summary, old_status, new_status, idempotent }.', {
|
|
1797
2096
|
type: 'object',
|
|
1798
2097
|
properties: {
|
|
1799
2098
|
project_id: { type: 'string' },
|
|
@@ -1911,7 +2210,7 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
|
|
|
1911
2210
|
},
|
|
1912
2211
|
required: [...summaryEnvelopeOutputSchema.required, 'chunks', 'total_bytes', 'project_id'],
|
|
1913
2212
|
}, (r) => handleBlueprintContext(projectResolver, cwd, r), { title: 'Blueprint Context', readOnlyHint: true, openWorldHint: false });
|
|
1914
|
-
registrar.registerTool('wp_blueprint_create', 'Create a new blueprint markdown under blueprints/draft/<slug>/_overview.md and re-ingest. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_projects/wp_blueprint_list to reject stale writes. Returns { slug, path, next_action, idempotent }.', {
|
|
2213
|
+
registrar.registerTool('wp_blueprint_create', 'Create a new blueprint markdown under blueprints/draft/<slug>.md by default (folder-shaped `<slug>/_overview.md` remains supported elsewhere) and re-ingest. Accepts optional request_id for idempotent retries and optional head_at_ingest from wp_blueprint_projects/wp_blueprint_list to reject stale writes. Returns { slug, path, next_action, idempotent }.', {
|
|
1915
2214
|
type: 'object',
|
|
1916
2215
|
properties: {
|
|
1917
2216
|
project_id: { type: 'string' },
|
package/dist/esm/mcp/cli.js
CHANGED
|
@@ -12,18 +12,26 @@ import { createServer } from './server.js';
|
|
|
12
12
|
export async function runStdioServer() {
|
|
13
13
|
const server = await createServer();
|
|
14
14
|
const transport = new StdioServerTransport();
|
|
15
|
+
const settle = Promise.withResolvers();
|
|
15
16
|
let shuttingDown = false;
|
|
16
17
|
const shutdown = async () => {
|
|
17
18
|
if (shuttingDown)
|
|
18
19
|
return;
|
|
19
20
|
shuttingDown = true;
|
|
20
21
|
deleteSentinel();
|
|
22
|
+
try {
|
|
23
|
+
await transport.close();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
/* ignore transport close errors during shutdown */
|
|
27
|
+
}
|
|
21
28
|
try {
|
|
22
29
|
await server.close();
|
|
23
30
|
}
|
|
24
31
|
catch {
|
|
25
32
|
/* ignore close errors during shutdown */
|
|
26
33
|
}
|
|
34
|
+
settle.resolve();
|
|
27
35
|
};
|
|
28
36
|
process.on('SIGINT', () => {
|
|
29
37
|
void shutdown().then(() => process.exit(0));
|
|
@@ -31,8 +39,21 @@ export async function runStdioServer() {
|
|
|
31
39
|
process.on('SIGTERM', () => {
|
|
32
40
|
void shutdown().then(() => process.exit(0));
|
|
33
41
|
});
|
|
42
|
+
process.stdin.on('end', () => {
|
|
43
|
+
void shutdown();
|
|
44
|
+
});
|
|
45
|
+
process.stdin.on('close', () => {
|
|
46
|
+
void shutdown();
|
|
47
|
+
});
|
|
48
|
+
transport.onclose = () => {
|
|
49
|
+
void shutdown();
|
|
50
|
+
};
|
|
51
|
+
transport.onerror = () => {
|
|
52
|
+
void shutdown();
|
|
53
|
+
};
|
|
34
54
|
await server.connect(transport);
|
|
35
55
|
writeSentinel();
|
|
56
|
+
await settle.promise;
|
|
36
57
|
}
|
|
37
58
|
import { realpathSync } from 'node:fs';
|
|
38
59
|
import { fileURLToPath } from 'node:url';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { globSync } from 'glob';
|
|
4
|
+
import { getPackageScript, isRecursiveWpScript, packageUsesVitest } from '#cli/package-scripts.js';
|
|
4
5
|
import { isRunFailure, runCommand as runSharedCommand } from '#mcp/tools/_shared/run-command';
|
|
5
6
|
// Keep the runner's own deadline comfortably below common MCP client call
|
|
6
7
|
// ceilings so slow suites fail fast with a structured `timedOut` payload
|
|
@@ -55,6 +56,14 @@ export async function runTests(input) {
|
|
|
55
56
|
if (workspaceShardRuns && workspaceShardRuns.length > 0) {
|
|
56
57
|
return runScopedSequence(cwd, workspaceShardRuns, input, workspaceSharding);
|
|
57
58
|
}
|
|
59
|
+
if (shouldBypassWorkspaceTestScript(cwd)) {
|
|
60
|
+
const result = await runCommand('vp', ['exec', '--', 'vitest', 'run', '--reporter=json', '--no-color'], {
|
|
61
|
+
...input,
|
|
62
|
+
cwd,
|
|
63
|
+
timeoutMs: commandTimeoutMs,
|
|
64
|
+
});
|
|
65
|
+
return withFailureScope(result, 'workspace vitest command');
|
|
66
|
+
}
|
|
58
67
|
const result = await runCommand('vp', ['run', 'test'], {
|
|
59
68
|
...input,
|
|
60
69
|
cwd,
|
|
@@ -222,6 +231,12 @@ function hasRootVitestTestScript(cwd) {
|
|
|
222
231
|
const testScript = scripts.test;
|
|
223
232
|
return typeof testScript === 'string' && /\bvitest\b/.test(testScript);
|
|
224
233
|
}
|
|
234
|
+
function shouldBypassWorkspaceTestScript(cwd) {
|
|
235
|
+
const testScript = getPackageScript(cwd, 'test');
|
|
236
|
+
if (!testScript || !isRecursiveWpScript(testScript, 'test'))
|
|
237
|
+
return false;
|
|
238
|
+
return packageUsesVitest(cwd);
|
|
239
|
+
}
|
|
225
240
|
function discoverVitestFiles(cwd) {
|
|
226
241
|
return globSync(VITEST_DEFAULT_INCLUDE, {
|
|
227
242
|
cwd,
|
package/dist/esm/mcp/server.d.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* — no edits required here.
|
|
8
8
|
*/
|
|
9
9
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
10
|
+
export type ToolLoadMode = 'filesystem' | 'registry';
|
|
10
11
|
export interface CreateServerOptions {
|
|
11
12
|
/**
|
|
12
13
|
* Directory to scan for tool descriptors. Defaults to `./tools` relative to
|
|
@@ -14,6 +15,12 @@ export interface CreateServerOptions {
|
|
|
14
15
|
* `vp run build`.
|
|
15
16
|
*/
|
|
16
17
|
toolsDir?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Tool loading strategy. Use `registry` for compiled runtime execution where
|
|
20
|
+
* runtime directory scans are unsafe, and `filesystem` for dev/test disk
|
|
21
|
+
* discovery.
|
|
22
|
+
*/
|
|
23
|
+
toolLoadMode?: ToolLoadMode;
|
|
17
24
|
/**
|
|
18
25
|
* Repo working directory passed through to the blueprint structured-store
|
|
19
26
|
* registrar (Task 2.1). Defaults to `process.cwd()`. Tests inject a tmpdir.
|