@webpresso/agent-kit 0.21.3 → 0.21.5

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 (99) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +105 -41
  4. package/catalog/AGENTS.md.tpl +3 -1
  5. package/catalog/agent/rules/changeset-release.md +13 -16
  6. package/catalog/agent/skills/plan-refine/SKILL.md +5 -4
  7. package/catalog/base-kit/commitlint.config.ts.tmpl +1 -3
  8. package/catalog/base-kit/e2e/fixtures/smoke.html.tmpl +13 -0
  9. package/catalog/base-kit/e2e/smoke.spec.ts.tmpl +13 -0
  10. package/catalog/base-kit/oxlint.config.ts.tmpl +26 -0
  11. package/catalog/base-kit/playwright.config.ts.tmpl +10 -0
  12. package/catalog/base-kit/src/quality-sample.test.ts.tmpl +19 -0
  13. package/catalog/base-kit/src/quality-sample.ts.tmpl +11 -0
  14. package/catalog/base-kit/stryker.config.ts.tmpl +14 -0
  15. package/catalog/base-kit/tsconfig.json.tmpl +9 -0
  16. package/catalog/base-kit/vitest.config.ts.tmpl +10 -0
  17. package/catalog/docs/templates/adr.md +1 -1
  18. package/catalog/docs/templates/blueprint.md +1 -0
  19. package/catalog/docs/templates/blueprint.yaml +6 -3
  20. package/catalog/docs/templates/guide.md +1 -1
  21. package/catalog/docs/templates/postmortem.md +1 -1
  22. package/catalog/docs/templates/research.md +1 -1
  23. package/catalog/docs/templates/runbook.md +1 -1
  24. package/catalog/docs/templates/system.md +12 -3
  25. package/catalog/docs/templates/tech-debt.md +1 -0
  26. package/commands/blueprint.md +37 -4
  27. package/dist/esm/audit/resolve-audit-script.d.ts +24 -0
  28. package/dist/esm/audit/resolve-audit-script.js +27 -0
  29. package/dist/esm/blueprint/db/enums.d.ts +1 -1
  30. package/dist/esm/blueprint/index.d.ts +0 -1
  31. package/dist/esm/blueprint/index.js +0 -2
  32. package/dist/esm/blueprint/local.d.ts +0 -3
  33. package/dist/esm/blueprint/local.js +0 -2
  34. package/dist/esm/blueprint/service/BlueprintCreationService.js +5 -2
  35. package/dist/esm/blueprint/utils/package-assets.d.ts +11 -0
  36. package/dist/esm/blueprint/utils/package-assets.js +33 -4
  37. package/dist/esm/build/sync-catalog-doc-templates.d.ts +23 -0
  38. package/dist/esm/build/sync-catalog-doc-templates.js +93 -0
  39. package/dist/esm/cli/commands/audit.js +2 -7
  40. package/dist/esm/cli/commands/blueprint/router.js +5 -2
  41. package/dist/esm/cli/commands/blueprint/template-resolver.js +8 -4
  42. package/dist/esm/cli/commands/init/host-visibility.js +4 -2
  43. package/dist/esm/cli/commands/init/index.js +46 -7
  44. package/dist/esm/cli/commands/init/scaffold-base-kit.d.ts +12 -0
  45. package/dist/esm/cli/commands/init/scaffold-base-kit.js +141 -6
  46. package/dist/esm/cli/commands/typecheck.js +10 -4
  47. package/dist/esm/e2e/command-builder.js +26 -7
  48. package/dist/esm/e2e/execution.js +4 -0
  49. package/dist/esm/e2e/run-planner.js +1 -0
  50. package/dist/esm/e2e/types.d.ts +1 -0
  51. package/dist/esm/format/index.js +7 -1
  52. package/dist/esm/lint/index.js +3 -1
  53. package/dist/esm/mcp/blueprint-server.js +361 -66
  54. package/dist/esm/mcp/tools/audit.js +2 -8
  55. package/dist/esm/mcp/tools/e2e.d.ts +1 -1
  56. package/dist/esm/package.json +3 -0
  57. package/dist/esm/secret-gate/runner.js +4 -0
  58. package/dist/esm/test/command-builder.d.ts +1 -0
  59. package/dist/esm/test/command-builder.js +8 -2
  60. package/dist/esm/test-helpers/hermetic-env.d.ts +25 -0
  61. package/dist/esm/test-helpers/hermetic-env.js +31 -0
  62. package/dist/esm/tool-runtime/index.d.ts +5 -0
  63. package/dist/esm/tool-runtime/index.js +23 -0
  64. package/dist/esm/tool-runtime/resolve-runner.d.ts +13 -0
  65. package/dist/esm/tool-runtime/resolve-runner.js +40 -0
  66. package/package.json +12 -18
  67. package/skills/plan-refine/SKILL.md +5 -4
  68. package/dist/esm/blueprint/dag/cycle-detector.d.ts +0 -12
  69. package/dist/esm/blueprint/dag/cycle-detector.js +0 -46
  70. package/dist/esm/blueprint/dag/executor.d.ts +0 -140
  71. package/dist/esm/blueprint/dag/executor.js +0 -292
  72. package/dist/esm/blueprint/dag/index.d.ts +0 -20
  73. package/dist/esm/blueprint/dag/index.js +0 -17
  74. package/dist/esm/blueprint/dag/interfaces.d.ts +0 -56
  75. package/dist/esm/blueprint/dag/interfaces.js +0 -13
  76. package/dist/esm/blueprint/dag/local/independence.d.ts +0 -107
  77. package/dist/esm/blueprint/dag/local/independence.js +0 -231
  78. package/dist/esm/blueprint/dag/local/index.d.ts +0 -14
  79. package/dist/esm/blueprint/dag/local/index.js +0 -14
  80. package/dist/esm/blueprint/dag/local/package-graph.d.ts +0 -66
  81. package/dist/esm/blueprint/dag/local/package-graph.js +0 -148
  82. package/dist/esm/blueprint/dag/plan-parser.d.ts +0 -54
  83. package/dist/esm/blueprint/dag/plan-parser.js +0 -236
  84. package/dist/esm/blueprint/dag/task-graph-algorithms.d.ts +0 -13
  85. package/dist/esm/blueprint/dag/task-graph-algorithms.js +0 -236
  86. package/dist/esm/blueprint/dag/task-graph.d.ts +0 -171
  87. package/dist/esm/blueprint/dag/task-graph.js +0 -370
  88. package/dist/esm/blueprint/dag/types.d.ts +0 -17
  89. package/dist/esm/blueprint/dag/types.js +0 -2
  90. package/dist/esm/blueprint/graph/index.d.ts +0 -5
  91. package/dist/esm/blueprint/graph/index.js +0 -5
  92. package/dist/esm/blueprint/graph/mermaid-parser.d.ts +0 -3
  93. package/dist/esm/blueprint/graph/mermaid-parser.js +0 -93
  94. package/dist/esm/blueprint/graph/mermaid-serializer.d.ts +0 -3
  95. package/dist/esm/blueprint/graph/mermaid-serializer.js +0 -20
  96. package/dist/esm/blueprint/graph/schema.d.ts +0 -89
  97. package/dist/esm/blueprint/graph/schema.js +0 -104
  98. package/dist/esm/blueprint/graph/task-graph-adapter.d.ts +0 -6
  99. package/dist/esm/blueprint/graph/task-graph-adapter.js +0 -30
@@ -18,6 +18,7 @@ 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';
@@ -93,6 +94,13 @@ async function resolveSyncAdapter(cwd) {
93
94
  };
94
95
  }
95
96
  const DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS = 5_000;
97
+ function todayIsoDate() {
98
+ return new Date().toISOString().split('T')[0] ?? new Date().toISOString();
99
+ }
100
+ function formatBlueprintProgress(totalTasks, doneTasks, blockedTasks) {
101
+ const percent = totalTasks === 0 ? 0 : Math.round((doneTasks / totalTasks) * 100);
102
+ return `${percent}% (${doneTasks}/${totalTasks} tasks done, ${blockedTasks} blocked, updated ${todayIsoDate()})`;
103
+ }
96
104
  function readPlatformMutationTimeoutMs() {
97
105
  const parsed = Number.parseInt(process.env['WP_BLUEPRINT_PLATFORM_MUTATION_TIMEOUT_MS'] ??
98
106
  String(DEFAULT_PLATFORM_MUTATION_TIMEOUT_MS), 10);
@@ -242,6 +250,18 @@ function openDbRW(cwd) {
242
250
  async function reIngest(cwd) {
243
251
  await reIngestProjection(cwd);
244
252
  }
253
+ async function persistBlueprintMarkdown(input) {
254
+ const { projectCwd, slug, overviewPath, markdown } = input;
255
+ mkdirSync(path.dirname(overviewPath), { recursive: true });
256
+ parseBlueprint(markdown, slug);
257
+ writeFileSync(overviewPath, markdown, 'utf8');
258
+ await reIngest(projectCwd);
259
+ const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
260
+ if (!refreshed.blueprint) {
261
+ throw new Error(`Blueprint "${slug}" did not appear in the projection after write`);
262
+ }
263
+ return refreshed.blueprint;
264
+ }
245
265
  function findBlueprintDir(blueprintRoot, slug, states) {
246
266
  for (const state of states) {
247
267
  const d = path.join(blueprintRoot, state, slug);
@@ -963,10 +983,6 @@ async function handlePromote(projectResolver, cwd, raw) {
963
983
  return err('wp_blueprint_promote failed', `Blueprint "${slug}" not found in any state directory`);
964
984
  const { dir: currentDir, state: currentState } = found;
965
985
  const overviewPath = path.join(currentDir, '_overview.md');
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.`);
970
986
  if (to_state === 'completed') {
971
987
  try {
972
988
  assertBlueprintCanComplete(overviewPath, slug);
@@ -999,40 +1015,34 @@ async function handlePromote(projectResolver, cwd, raw) {
999
1015
  catch (e) {
1000
1016
  return err('wp_blueprint_promote failed', toStr(e));
1001
1017
  }
1002
- const { renameSync } = await import('node:fs');
1003
- const destDir = path.join(root, to_state, slug);
1004
- mkdirSync(path.dirname(destDir), { recursive: true });
1005
1018
  try {
1006
- renameSync(currentDir, destDir);
1019
+ const transitioned = await applyLocalBlueprintTransition({
1020
+ found,
1021
+ projectCwd,
1022
+ slug,
1023
+ to_state,
1024
+ });
1025
+ const payload = {
1026
+ summary: `Blueprint "${slug}" promoted from "${currentState}" to "${to_state}"`,
1027
+ slug,
1028
+ from_state: currentState,
1029
+ to_state,
1030
+ new_path: transitioned.overviewPath,
1031
+ status: transitioned.blueprint.status,
1032
+ content_hash: transitioned.blueprint.content_hash,
1033
+ revision: transitioned.blueprint.content_hash,
1034
+ ingested_at: transitioned.blueprint.ingested_at,
1035
+ failures: [],
1036
+ bytes: 0,
1037
+ tokensSaved: 0,
1038
+ };
1039
+ if (currentState === 'draft' && to_state === 'planned')
1040
+ appendHint(payload, projectCwd, 'PLAN_REFINE');
1041
+ return finishPayload(payload);
1007
1042
  }
1008
1043
  catch (e) {
1009
- return err('wp_blueprint_promote failed', `Directory move error: ${toStr(e)}`);
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 */
1044
+ return err('wp_blueprint_promote failed', toStr(e));
1022
1045
  }
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
1046
  }
1037
1047
  const finalizeSchema = z.object({ project_id: z.string().optional(), slug: z.string() });
1038
1048
  async function handleFinalize(projectResolver, cwd, raw) {
@@ -1102,39 +1112,32 @@ async function handleFinalize(projectResolver, cwd, raw) {
1102
1112
  catch (e) {
1103
1113
  return err('wp_blueprint_finalize failed', toStr(e));
1104
1114
  }
1105
- const { renameSync } = await import('node:fs');
1106
- const destDir = path.join(root, 'completed', slug);
1107
- mkdirSync(path.dirname(destDir), { recursive: true });
1108
1115
  try {
1109
- renameSync(found.dir, destDir);
1116
+ const transitioned = await applyLocalBlueprintTransition({
1117
+ found,
1118
+ projectCwd,
1119
+ slug,
1120
+ to_state: 'completed',
1121
+ });
1122
+ const payload = {
1123
+ summary: `Blueprint "${slug}" finalized and moved to completed`,
1124
+ slug,
1125
+ new_path: transitioned.overviewPath,
1126
+ status: transitioned.blueprint.status,
1127
+ content_hash: transitioned.blueprint.content_hash,
1128
+ revision: transitioned.blueprint.content_hash,
1129
+ ingested_at: transitioned.blueprint.ingested_at,
1130
+ failures: [],
1131
+ bytes: 0,
1132
+ tokensSaved: 0,
1133
+ };
1134
+ if (hasRecentAuditFinding(projectCwd))
1135
+ appendHint(payload, projectCwd, 'AUDIT_FIX');
1136
+ return finishPayload(payload);
1110
1137
  }
1111
1138
  catch (e) {
1112
- return err('wp_blueprint_finalize failed', `Directory move error: ${toStr(e)}`);
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 */
1139
+ return err('wp_blueprint_finalize failed', toStr(e));
1126
1140
  }
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
1141
  }
1139
1142
  function assertBlueprintCanComplete(overviewPath, slug) {
1140
1143
  const markdown = readFileSync(overviewPath, 'utf8');
@@ -1698,6 +1701,243 @@ const createSchema = MutationTarget.extend({
1698
1701
  request_id: z.string().min(1).optional(),
1699
1702
  head_at_ingest: z.string().nullable().optional(),
1700
1703
  });
1704
+ const putTaskSchema = z.object({
1705
+ id: z.string().min(1),
1706
+ title: z.string().min(1),
1707
+ status: z.enum(['todo', 'in-progress', 'blocked', 'done', 'dropped']).default('todo'),
1708
+ wave: z.string().optional(),
1709
+ lane: z.string().optional(),
1710
+ description: z.string().optional(),
1711
+ acceptance: z.array(z.string().min(1)).min(1),
1712
+ });
1713
+ const putDocumentSchema = z.object({
1714
+ type: z.literal('blueprint').default('blueprint'),
1715
+ title: z.string().min(1),
1716
+ status: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
1717
+ complexity: z.enum(['XS', 'S', 'M', 'L', 'XL']),
1718
+ owner: z.string().min(1),
1719
+ created: z.string().min(1),
1720
+ last_updated: z.string().min(1),
1721
+ progress: z.string().optional(),
1722
+ tags: z.array(z.string()).optional(),
1723
+ product_wedge_anchor: z.object({
1724
+ stage_outcome: z.string().min(1),
1725
+ consuming_surface: z.string().min(1),
1726
+ new_user_visible_capability: z.string().min(1),
1727
+ }),
1728
+ summary: z.string().min(1),
1729
+ tasks: z.array(putTaskSchema).min(1),
1730
+ });
1731
+ const putSchema = MutationTarget.extend({
1732
+ slug: z.string().min(1),
1733
+ document: putDocumentSchema,
1734
+ request_id: z.string().min(1).optional(),
1735
+ head_at_ingest: z.string().nullable().optional(),
1736
+ });
1737
+ function renderBlueprintMarkdownFromDocument(slug, document) {
1738
+ const frontmatter = [
1739
+ '---',
1740
+ `type: ${document.type}`,
1741
+ `title: ${document.title}`,
1742
+ `status: ${document.status}`,
1743
+ `complexity: ${document.complexity}`,
1744
+ `owner: ${document.owner}`,
1745
+ `created: '${document.created}'`,
1746
+ `last_updated: '${document.last_updated}'`,
1747
+ ...(document.progress ? [`progress: ${JSON.stringify(document.progress)}`] : []),
1748
+ ...(document.tags && document.tags.length > 0
1749
+ ? ['tags:', ...document.tags.map((tag) => ` - ${tag}`)]
1750
+ : []),
1751
+ '---',
1752
+ '',
1753
+ ];
1754
+ const sections = [
1755
+ `# ${document.title}`,
1756
+ '',
1757
+ '## Product wedge anchor',
1758
+ '',
1759
+ `- **Stage outcome:** ${document.product_wedge_anchor.stage_outcome}`,
1760
+ `- **Consuming surface:** ${document.product_wedge_anchor.consuming_surface}`,
1761
+ `- **New user-visible capability:** ${document.product_wedge_anchor.new_user_visible_capability}`,
1762
+ '',
1763
+ '## Summary',
1764
+ '',
1765
+ document.summary,
1766
+ '',
1767
+ ];
1768
+ const taskBlocks = document.tasks.flatMap((task) => [
1769
+ `#### Task ${task.id}: ${task.title}`,
1770
+ '',
1771
+ `**Status:** ${task.status}`,
1772
+ ...(task.wave ? [`**Wave:** ${task.wave}`] : []),
1773
+ ...(task.lane ? [`**Lane:** ${task.lane}`] : []),
1774
+ ...(task.description ? ['', task.description] : []),
1775
+ '',
1776
+ '**Acceptance:**',
1777
+ ...task.acceptance.map((item) => `- [ ] ${item}`),
1778
+ '',
1779
+ ]);
1780
+ return [...frontmatter, ...sections, ...taskBlocks].join('\n').trimEnd() + '\n';
1781
+ }
1782
+ async function handleBlueprintPut(projectResolver, cwd, raw) {
1783
+ const p = putSchema.safeParse(raw);
1784
+ if (!p.success)
1785
+ return err('wp_blueprint_put validation error', p.error.message);
1786
+ const { project_id, slug, document, request_id, head_at_ingest } = p.data;
1787
+ const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
1788
+ if ('content' in resolvedProject)
1789
+ return resolvedProject;
1790
+ const projectCwd = resolvedProject.cwd;
1791
+ await ensureProjectionReady(projectCwd);
1792
+ const freshnessFailure = validateMutationFreshnessToken(projectCwd, head_at_ingest, 'wp_blueprint_put', 'wp_blueprint_get');
1793
+ if (freshnessFailure)
1794
+ return freshnessFailure;
1795
+ const payloadHash = hashMutationPayload({ slug, document });
1796
+ const replay = request_id !== undefined
1797
+ ? readMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash)
1798
+ : null;
1799
+ if (replay)
1800
+ return replay;
1801
+ const root = resolveBlueprintRoot(projectCwd);
1802
+ const found = findBlueprintDir(root, slug, ALL_STATES);
1803
+ if (found && found.state !== document.status) {
1804
+ return err('wp_blueprint_put refused', `Blueprint "${slug}" currently lives in "${found.state}" and cannot be rewritten as "${document.status}" without a lifecycle transition.`);
1805
+ }
1806
+ if (!found && document.status !== 'draft') {
1807
+ return err('wp_blueprint_put refused', `New blueprint "${slug}" must start in "draft"; use wp_blueprint_transition for later lifecycle moves.`);
1808
+ }
1809
+ const overviewPath = found
1810
+ ? path.join(found.dir, '_overview.md')
1811
+ : path.join(root, document.status, slug, '_overview.md');
1812
+ try {
1813
+ const markdown = renderBlueprintMarkdownFromDocument(slug, document);
1814
+ const blueprint = await persistBlueprintMarkdown({
1815
+ projectCwd,
1816
+ slug,
1817
+ overviewPath,
1818
+ markdown,
1819
+ });
1820
+ const payload = {
1821
+ summary: `Blueprint "${slug}" written to ${overviewPath}`,
1822
+ slug,
1823
+ path: overviewPath,
1824
+ status: blueprint.status,
1825
+ content_hash: blueprint.content_hash,
1826
+ ingested_at: blueprint.ingested_at,
1827
+ revision: blueprint.content_hash,
1828
+ idempotent: false,
1829
+ failures: [],
1830
+ next_action: makeNextAction('verify_task', 'Blueprint written. Next: validate or transition the latest revision token through the structured surface.'),
1831
+ project_id: resolvedProject.project_id ?? projectCwd,
1832
+ };
1833
+ if (request_id !== undefined) {
1834
+ recordMutationReplay(projectCwd, 'wp_blueprint_put', request_id, payloadHash, payload);
1835
+ }
1836
+ return finishPayload(payload);
1837
+ }
1838
+ catch (e) {
1839
+ return err('wp_blueprint_put failed', toStr(e));
1840
+ }
1841
+ }
1842
+ const transitionSchema = MutationTarget.extend({
1843
+ slug: z.string().min(1),
1844
+ to_state: z.enum(['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived']),
1845
+ expected_version: z.string().min(1),
1846
+ });
1847
+ async function handleBlueprintTransition(projectResolver, cwd, raw) {
1848
+ const p = transitionSchema.safeParse(raw);
1849
+ if (!p.success)
1850
+ return err('wp_blueprint_transition validation error', p.error.message);
1851
+ const { project_id, slug, to_state, expected_version } = p.data;
1852
+ const resolvedProject = await resolveToolProject(projectResolver, cwd, project_id);
1853
+ if ('content' in resolvedProject)
1854
+ return resolvedProject;
1855
+ const projectCwd = resolvedProject.cwd;
1856
+ await ensureProjectionReady(projectCwd);
1857
+ const target = dbPath(projectCwd);
1858
+ if (!existsSync(target))
1859
+ return err('wp_blueprint_transition failed', 'Blueprint DB not found');
1860
+ const current = getCurrentProjectBlueprint(projectCwd, slug);
1861
+ if (!current.blueprint) {
1862
+ return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found`);
1863
+ }
1864
+ if (current.blueprint.content_hash !== expected_version) {
1865
+ return jsonContent({
1866
+ summary: `wp_blueprint_transition rejected a stale blueprint revision`,
1867
+ failures: [
1868
+ `expected_version "${expected_version}" does not match current content hash "${current.blueprint.content_hash}"`,
1869
+ ],
1870
+ error: 'stale_blueprint_revision',
1871
+ next_action: makeNextAction('reingest_project', 'Call wp_blueprint_get again to fetch the latest content_hash before retrying the transition.'),
1872
+ bytes: 0,
1873
+ tokensSaved: 0,
1874
+ }, true);
1875
+ }
1876
+ const root = resolveBlueprintRoot(projectCwd);
1877
+ const found = findBlueprintDir(root, slug, ALL_STATES);
1878
+ if (!found)
1879
+ return err('wp_blueprint_transition failed', `Blueprint "${slug}" not found on disk`);
1880
+ try {
1881
+ const refreshed = await applyLocalBlueprintTransition({
1882
+ found,
1883
+ projectCwd,
1884
+ slug,
1885
+ to_state,
1886
+ });
1887
+ return finishPayload({
1888
+ summary: `Blueprint "${slug}" transitioned to ${to_state}`,
1889
+ slug,
1890
+ old_status: current.blueprint.status,
1891
+ new_status: to_state,
1892
+ status: refreshed.blueprint.status,
1893
+ content_hash: refreshed.blueprint.content_hash,
1894
+ revision: refreshed.blueprint.content_hash,
1895
+ ingested_at: refreshed.blueprint.ingested_at,
1896
+ failures: [],
1897
+ project_id: resolvedProject.project_id ?? projectCwd,
1898
+ });
1899
+ }
1900
+ catch (e) {
1901
+ return err('wp_blueprint_transition failed', toStr(e));
1902
+ }
1903
+ }
1904
+ async function applyLocalBlueprintTransition(input) {
1905
+ const { projectCwd, slug, to_state, found } = input;
1906
+ const root = resolveBlueprintRoot(projectCwd);
1907
+ const overviewPath = path.join(found.dir, '_overview.md');
1908
+ const parsed = runValidate(overviewPath);
1909
+ if (!parsed.valid) {
1910
+ throw new Error(parsed.gaps.join('; '));
1911
+ }
1912
+ const markdown = readFileSync(overviewPath, 'utf8');
1913
+ const currentBlueprint = parseBlueprint(markdown, slug);
1914
+ const updated = setBlueprintFrontmatterFields(markdown, {
1915
+ status: to_state,
1916
+ last_updated: todayIsoDate(),
1917
+ completed_at: to_state === 'completed' ? todayIsoDate() : undefined,
1918
+ progress: formatBlueprintProgress(currentBlueprint.tasks.length, currentBlueprint.tasks.filter((task) => task.status === 'done').length, currentBlueprint.tasks.filter((task) => task.status === 'blocked').length),
1919
+ });
1920
+ parseBlueprint(updated, slug);
1921
+ const destDir = path.join(root, to_state, slug);
1922
+ mkdirSync(path.dirname(destDir), { recursive: true });
1923
+ let finalOverviewPath = overviewPath;
1924
+ if (found.state !== to_state) {
1925
+ const { renameSync } = await import('node:fs');
1926
+ renameSync(found.dir, destDir);
1927
+ finalOverviewPath = path.join(destDir, '_overview.md');
1928
+ }
1929
+ writeFileSync(finalOverviewPath, updated, 'utf8');
1930
+ await reIngest(projectCwd);
1931
+ const refreshed = getCurrentProjectBlueprint(projectCwd, slug);
1932
+ if (!refreshed.blueprint) {
1933
+ throw new Error(`Blueprint "${slug}" did not appear in the projection after transition`);
1934
+ }
1935
+ return {
1936
+ blueprint: refreshed.blueprint,
1937
+ overviewPath: finalOverviewPath,
1938
+ fromState: found.state,
1939
+ };
1940
+ }
1701
1941
  async function handleBlueprintCreate(projectResolver, cwd, raw) {
1702
1942
  const p = createSchema.safeParse(raw);
1703
1943
  if (!p.success)
@@ -1728,9 +1968,12 @@ async function handleBlueprintCreate(projectResolver, cwd, raw) {
1728
1968
  .replace(/{COMPLEXITY}/g, complexity)
1729
1969
  .replace(/{DATE}/g, today)
1730
1970
  .replace('{GOAL}', goal);
1731
- writeFileSync(overviewPath, content, 'utf8');
1732
- // Re-ingest so the DB reflects the new blueprint
1733
- await reIngest(projectCwd);
1971
+ await persistBlueprintMarkdown({
1972
+ projectCwd,
1973
+ slug,
1974
+ overviewPath,
1975
+ markdown: content,
1976
+ });
1734
1977
  const b = bytes(content);
1735
1978
  const payload = {
1736
1979
  summary: `Blueprint "${slug}" created at ${overviewPath}`,
@@ -1788,6 +2031,58 @@ export async function registerBlueprintTools(registrar, cwd, projectResolver = c
1788
2031
  },
1789
2032
  required: ['title', 'goal_prompt'],
1790
2033
  }, undefined, (r) => handleNew(cwd, r), { title: 'Blueprint New', readOnlyHint: true, openWorldHint: false });
2034
+ 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.', {
2035
+ type: 'object',
2036
+ properties: {
2037
+ project_id: { type: 'string' },
2038
+ slug: { type: 'string' },
2039
+ document: { type: 'object' },
2040
+ request_id: { type: 'string' },
2041
+ head_at_ingest: { type: ['string', 'null'] },
2042
+ },
2043
+ required: ['project_id', 'slug', 'document'],
2044
+ }, {
2045
+ ...summaryEnvelopeOutputSchema,
2046
+ properties: {
2047
+ ...summaryEnvelopeOutputSchema.properties,
2048
+ slug: { type: 'string' },
2049
+ path: { type: 'string' },
2050
+ status: { type: 'string' },
2051
+ content_hash: { type: 'string' },
2052
+ ingested_at: { type: 'number' },
2053
+ revision: { type: 'string' },
2054
+ idempotent: { type: 'boolean' },
2055
+ project_id: { type: 'string' },
2056
+ next_action: nextActionOutputSchema,
2057
+ },
2058
+ }, (r) => handleBlueprintPut(projectResolver, cwd, r), { title: 'Blueprint Put', destructiveHint: false, openWorldHint: false });
2059
+ 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.', {
2060
+ type: 'object',
2061
+ properties: {
2062
+ project_id: { type: 'string' },
2063
+ slug: { type: 'string' },
2064
+ to_state: {
2065
+ type: 'string',
2066
+ enum: ['draft', 'planned', 'in-progress', 'completed', 'parked', 'archived'],
2067
+ },
2068
+ expected_version: { type: 'string' },
2069
+ },
2070
+ required: ['project_id', 'slug', 'to_state', 'expected_version'],
2071
+ }, {
2072
+ ...summaryEnvelopeOutputSchema,
2073
+ properties: {
2074
+ ...summaryEnvelopeOutputSchema.properties,
2075
+ slug: { type: 'string' },
2076
+ old_status: { type: 'string' },
2077
+ new_status: { type: 'string' },
2078
+ status: { type: 'string' },
2079
+ content_hash: { type: 'string' },
2080
+ revision: { type: 'string' },
2081
+ ingested_at: { type: 'number' },
2082
+ project_id: { type: 'string' },
2083
+ next_action: nextActionOutputSchema,
2084
+ },
2085
+ }, (r) => handleBlueprintTransition(projectResolver, cwd, r), { title: 'Blueprint Transition', destructiveHint: false, openWorldHint: false });
1791
2086
  registrar.registerTool('wp_blueprint_validate', 'Validate _overview.md structure. 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
2087
  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
2088
  type: 'object',
@@ -15,9 +15,8 @@
15
15
  * — the handler never throws out, so the MCP server stays responsive.
16
16
  */
17
17
  import { spawn } from 'node:child_process';
18
- import { existsSync } from 'node:fs';
19
18
  import { z } from 'zod';
20
- import { resolvePackageAsset } from '#utils/package-assets';
19
+ import { resolveAuditScriptPath } from '#audit/resolve-audit-script';
21
20
  import { applyOutputTransform } from '#output-transforms/index';
22
21
  import { createSummaryOutputSchema, createSummaryResult } from './_shared/result.js';
23
22
  const KINDS = [
@@ -62,12 +61,7 @@ const outputSchema = createSummaryOutputSchema({
62
61
  kind: z.enum(KINDS),
63
62
  });
64
63
  function resolveAuditScript(name) {
65
- // Source layout: `src/mcp/tools/audit.ts` → `../../audit/<name>`.
66
- const fromSource = new URL(`../../audit/${name}`, import.meta.url);
67
- if (existsSync(fromSource)) {
68
- return fromSource.pathname;
69
- }
70
- return resolvePackageAsset(`src/audit/${name}`);
64
+ return resolveAuditScriptPath(name, { moduleUrl: import.meta.url });
71
65
  }
72
66
  async function runScript(script) {
73
67
  return new Promise((resolve) => {
@@ -12,8 +12,8 @@ declare const inputSchema: z.ZodObject<{
12
12
  suite: z.ZodOptional<z.ZodString>;
13
13
  runner: z.ZodOptional<z.ZodEnum<{
14
14
  command: "command";
15
- playwright: "playwright";
16
15
  vitest: "vitest";
16
+ playwright: "playwright";
17
17
  }>>;
18
18
  config: z.ZodOptional<z.ZodString>;
19
19
  files: z.ZodDefault<z.ZodOptional<z.ZodArray<z.ZodString>>>;
@@ -48,6 +48,9 @@
48
48
  "#mcp/*": "./mcp/*.js",
49
49
  "#content/*.js": "./content/*.js",
50
50
  "#content/*": "./content/*.js",
51
+ "#tool-runtime": "./tool-runtime/index.js",
52
+ "#tool-runtime/*.js": "./tool-runtime/*.js",
53
+ "#tool-runtime/*": "./tool-runtime/*.js",
51
54
  "#output-transforms/*.js": "./output-transforms/*.js",
52
55
  "#output-transforms/*": "./output-transforms/*.js",
53
56
  "#lint/*.js": "./lint/*.js",
@@ -5,9 +5,13 @@ const SIGNAL_TO_EXIT_CODE = {
5
5
  SIGKILL: 9,
6
6
  SIGTERM: 15,
7
7
  };
8
+ const DIRECT_ENV_PROFILES = new Set(['none', 'public']);
8
9
  export function buildSecretGateCommand(options) {
9
10
  const runner = options.runner?.trim() || 'with-secrets';
10
11
  const envProfile = options.envProfile?.trim();
12
+ if (runner === 'with-secrets' && envProfile && DIRECT_ENV_PROFILES.has(envProfile)) {
13
+ return { command: options.command, args: [...(options.args ?? [])] };
14
+ }
11
15
  const args = envProfile
12
16
  ? ['--env-profile', envProfile, '--', options.command, ...(options.args ?? [])]
13
17
  : ['--', options.command, ...(options.args ?? [])];
@@ -17,6 +17,7 @@ export interface TestCommandOptions {
17
17
  concurrencyLimit?: number;
18
18
  log?: VpRunLogMode;
19
19
  passthrough?: readonly string[];
20
+ filterOutput?: boolean;
20
21
  }
21
22
  export declare function buildTestCommand(target: ResolvedTestTarget, options?: TestCommandOptions): CommandConfig;
22
23
  export declare function buildVpTestCommand(filters: readonly string[], options?: TestCommandOptions): CommandConfig;
@@ -1,3 +1,4 @@
1
+ import { getManagedRunner } from '#tool-runtime';
1
2
  export function buildTestCommand(target, options = {}) {
2
3
  if (target.type === 'file') {
3
4
  return buildVitestCommand(target.values, options);
@@ -17,8 +18,12 @@ export function buildVpTestCommand(filters, options = {}) {
17
18
  if (passthrough.length > 0) {
18
19
  args.push('--', ...passthrough);
19
20
  }
21
+ const resolution = getManagedRunner('vp', { filterOutput: options.filterOutput });
20
22
  const env = buildVpRunEnv(options);
21
- return env ? { command: 'vp', args, env } : { command: 'vp', args };
23
+ const mergedArgs = [...resolution.args, ...args];
24
+ return env
25
+ ? { command: resolution.command, args: mergedArgs, env }
26
+ : { command: resolution.command, args: mergedArgs };
22
27
  }
23
28
  export function buildVitestCommand(files, options = {}) {
24
29
  const args = [options.watch ? '--watch' : 'run'];
@@ -40,7 +45,8 @@ export function buildVitestCommand(files, options = {}) {
40
45
  args.push('--config', configFile);
41
46
  }
42
47
  args.push(...buildVitestPassthrough(options), ...testFiles);
43
- return { command: 'vitest', args };
48
+ const resolution = getManagedRunner('vitest', { filterOutput: options.filterOutput });
49
+ return { command: resolution.command, args: [...resolution.args, ...args] };
44
50
  }
45
51
  export function getVpTestTask(options) {
46
52
  if (options.mutation)
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Hermetic environment baseline for the whole test suite.
3
+ *
4
+ * agent-kit's suite is routinely run *inside* an agent session (Claude Code,
5
+ * Codex), and those sessions export ambient variables that the production code
6
+ * legitimately honors at runtime:
7
+ *
8
+ * - `CLAUDE_PROJECT_DIR` — `resolveProjectRoot` (src/mcp/tools/_shared/project-root.ts)
9
+ * ranks this above the discovered cwd, so a leaked value makes blueprint-server
10
+ * tests target the real repo + its shared SQLite DB instead of their temp dir,
11
+ * producing wrong-data assertions and lock-contention timeouts.
12
+ * - `WP_SKIP_UPDATE_CHECK` — suppresses the managed-CLI refresh spawn that the
13
+ * init scaffolder tests assert fires; a leaked value (exported by the shell or
14
+ * left behind by a sibling test file under the shared forks worker) drops the
15
+ * expected spawn and fails spawn-count assertions.
16
+ *
17
+ * The fix is isolation, not changing the runtime precedence: reset these before
18
+ * every test so the suite is deterministic regardless of the launching
19
+ * environment. Tests that exercise these variables set them explicitly in their
20
+ * own body (which runs after this hook), so behavior coverage is unaffected.
21
+ */
22
+ export declare const LEAKY_ENV_KEYS: readonly ["CLAUDE_PROJECT_DIR", "WP_SKIP_UPDATE_CHECK"];
23
+ /** Delete every agent-session-leaked env var so each test starts hermetic. */
24
+ export declare function resetLeakyEnv(): void;
25
+ //# sourceMappingURL=hermetic-env.d.ts.map
@@ -0,0 +1,31 @@
1
+ import { beforeEach } from 'vitest';
2
+ /**
3
+ * Hermetic environment baseline for the whole test suite.
4
+ *
5
+ * agent-kit's suite is routinely run *inside* an agent session (Claude Code,
6
+ * Codex), and those sessions export ambient variables that the production code
7
+ * legitimately honors at runtime:
8
+ *
9
+ * - `CLAUDE_PROJECT_DIR` — `resolveProjectRoot` (src/mcp/tools/_shared/project-root.ts)
10
+ * ranks this above the discovered cwd, so a leaked value makes blueprint-server
11
+ * tests target the real repo + its shared SQLite DB instead of their temp dir,
12
+ * producing wrong-data assertions and lock-contention timeouts.
13
+ * - `WP_SKIP_UPDATE_CHECK` — suppresses the managed-CLI refresh spawn that the
14
+ * init scaffolder tests assert fires; a leaked value (exported by the shell or
15
+ * left behind by a sibling test file under the shared forks worker) drops the
16
+ * expected spawn and fails spawn-count assertions.
17
+ *
18
+ * The fix is isolation, not changing the runtime precedence: reset these before
19
+ * every test so the suite is deterministic regardless of the launching
20
+ * environment. Tests that exercise these variables set them explicitly in their
21
+ * own body (which runs after this hook), so behavior coverage is unaffected.
22
+ */
23
+ export const LEAKY_ENV_KEYS = ['CLAUDE_PROJECT_DIR', 'WP_SKIP_UPDATE_CHECK'];
24
+ /** Delete every agent-session-leaked env var so each test starts hermetic. */
25
+ export function resetLeakyEnv() {
26
+ for (const key of LEAKY_ENV_KEYS) {
27
+ delete process.env[key];
28
+ }
29
+ }
30
+ beforeEach(resetLeakyEnv);
31
+ //# sourceMappingURL=hermetic-env.js.map
@@ -0,0 +1,5 @@
1
+ import { type ManagedRunnerResolution, type ResolveRunnerOptions } from './resolve-runner.js';
2
+ export declare function getManagedRunner(tool: string, options?: ResolveRunnerOptions): ManagedRunnerResolution;
3
+ export declare function clearManagedRunnerCache(): void;
4
+ export type { ManagedRunnerResolution, ResolveRunnerOptions };
5
+ //# sourceMappingURL=index.d.ts.map