@stackwright-pro/mcp 0.2.0-alpha.114 → 0.2.0-alpha.117

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/dist/server.mjs CHANGED
@@ -1290,7 +1290,7 @@ function answersToManifestFormat(answers, questions) {
1290
1290
  return qHeader.startsWith(headerLower.split("-")[0]);
1291
1291
  });
1292
1292
  if (matched) {
1293
- result[matched.id] = answer.selected_options[0] || "";
1293
+ result[matched.id] = answer.selected_options.length > 1 ? answer.selected_options : answer.selected_options[0] || "";
1294
1294
  }
1295
1295
  continue;
1296
1296
  }
@@ -1762,7 +1762,10 @@ function handleSavePhaseAnswers(input) {
1762
1762
  }
1763
1763
  } else {
1764
1764
  answers = Object.fromEntries(
1765
- input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
1765
+ input.rawAnswers.map((a) => [
1766
+ a.question_header,
1767
+ a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
1768
+ ])
1766
1769
  );
1767
1770
  }
1768
1771
  const payload = {
@@ -2006,9 +2009,9 @@ function registerOrchestrationTools(server2) {
2006
2009
 
2007
2010
  // src/tools/pipeline.ts
2008
2011
  import { z as z14 } from "zod";
2009
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync7, mkdirSync as mkdirSync4, lstatSync as lstatSync6, readdirSync as readdirSync5 } from "fs";
2012
+ import { readFileSync as readFileSync11, writeFileSync as writeFileSync4, existsSync as existsSync11, mkdirSync as mkdirSync4, lstatSync as lstatSync6, readdirSync as readdirSync8 } from "fs";
2010
2013
  import { lockSync } from "proper-lockfile";
2011
- import { join as join7 } from "path";
2014
+ import { join as join11 } from "path";
2012
2015
  import { createHash as createHash3 } from "crypto";
2013
2016
 
2014
2017
  // src/artifact-signing.ts
@@ -2551,6 +2554,598 @@ function formatPulseRetryPrompt(result) {
2551
2554
  return lines.join("\n");
2552
2555
  }
2553
2556
 
2557
+ // src/tools/validate-theme-tokens.ts
2558
+ import { readdirSync as readdirSync3, readFileSync as readFileSync6, existsSync as existsSync7 } from "fs";
2559
+ import { join as join6, relative as relative2 } from "path";
2560
+ import { load as yamlLoad2 } from "js-yaml";
2561
+
2562
+ // src/tools/schema-registry.ts
2563
+ import { z as z11 } from "zod";
2564
+ import { readFileSync as readFileSync5, existsSync as existsSync6 } from "fs";
2565
+ import { join as join5 } from "path";
2566
+ import {
2567
+ WorkflowStepSchema,
2568
+ WorkflowDefinitionSchema,
2569
+ WorkflowFieldSchema,
2570
+ WorkflowActionSchema,
2571
+ WorkflowWizardContentSchema,
2572
+ authConfigSchema,
2573
+ themeTokensFileSchema
2574
+ } from "@stackwright-pro/types";
2575
+ import {
2576
+ MetricCardPulseSchema,
2577
+ DataTablePulseSchema,
2578
+ StatusBadgePulseSchema,
2579
+ MapPulseSchema
2580
+ } from "@stackwright-pro/pulse";
2581
+ function getSchemaForEntry(entry, ctx) {
2582
+ if (entry.schema) return entry.schema;
2583
+ if (entry.schemaFactory) {
2584
+ if (ctx) return entry.schemaFactory(ctx);
2585
+ console.warn(
2586
+ "[schema-registry] schemaFactory entry requires projectRoot context. Pass { projectRoot } to get the project-specific schema. Falling back to z.string() (no enforcement)."
2587
+ );
2588
+ return z11.string();
2589
+ }
2590
+ return z11.unknown();
2591
+ }
2592
+ var NavigationLinkSchema = z11.lazy(
2593
+ () => z11.object({
2594
+ label: z11.string().min(1),
2595
+ href: z11.string().min(1),
2596
+ children: z11.array(NavigationLinkSchema).optional()
2597
+ })
2598
+ );
2599
+ var NavigationSectionSchema = z11.object({
2600
+ section: z11.string().min(1),
2601
+ items: z11.array(NavigationLinkSchema)
2602
+ });
2603
+ var NavigationItemSchema = z11.union([
2604
+ NavigationLinkSchema,
2605
+ NavigationSectionSchema
2606
+ ]);
2607
+ var THEME_TOKENS_PATHS = [
2608
+ join5(".stackwright", "artifacts", "theme-tokens.json"),
2609
+ join5("public", "stackwright-content", "_theme.json")
2610
+ ];
2611
+ function resolveStatusLadder(projectRoot) {
2612
+ for (const relPath of THEME_TOKENS_PATHS) {
2613
+ const fullPath = join5(projectRoot, relPath);
2614
+ if (!existsSync6(fullPath)) continue;
2615
+ try {
2616
+ const raw = readFileSync5(fullPath, "utf-8");
2617
+ const parsed = JSON.parse(raw);
2618
+ if (Array.isArray(parsed["statusLadder"]) && parsed["statusLadder"].length > 0) {
2619
+ return parsed["statusLadder"].filter(
2620
+ (t) => typeof t === "string" && t.startsWith("status-")
2621
+ );
2622
+ }
2623
+ const colors = parsed["tokens"]?.["colors"];
2624
+ if (colors && typeof colors === "object") {
2625
+ const statusKeys = Object.keys(colors).filter((k) => k.startsWith("status-"));
2626
+ if (statusKeys.length > 0) return statusKeys;
2627
+ }
2628
+ } catch {
2629
+ }
2630
+ }
2631
+ return [];
2632
+ }
2633
+ function buildThemeStatusTokenSchema(ctx) {
2634
+ const ladder = resolveStatusLadder(ctx.projectRoot);
2635
+ if (ladder.length === 0) {
2636
+ return z11.string();
2637
+ }
2638
+ return z11.enum(ladder);
2639
+ }
2640
+ var REGISTRY = /* @__PURE__ */ new Map([
2641
+ // ── Workflow schemas ──────────────────────────────────────────────────────
2642
+ [
2643
+ "workflow_step",
2644
+ {
2645
+ schema: WorkflowStepSchema,
2646
+ synonyms: {
2647
+ label: ["title", "name"],
2648
+ message: ["description"],
2649
+ "theme.variant": ["style"],
2650
+ "on_submit.transition": ["transitions"]
2651
+ },
2652
+ phaseAffinity: ["workflow"]
2653
+ }
2654
+ ],
2655
+ [
2656
+ "workflow_definition",
2657
+ {
2658
+ schema: WorkflowDefinitionSchema,
2659
+ synonyms: {
2660
+ label: ["title", "name"],
2661
+ id: ["type"]
2662
+ },
2663
+ phaseAffinity: ["workflow"]
2664
+ }
2665
+ ],
2666
+ [
2667
+ "workflow_field",
2668
+ {
2669
+ schema: WorkflowFieldSchema,
2670
+ synonyms: {
2671
+ name: ["id"],
2672
+ label: ["title", "description"]
2673
+ },
2674
+ phaseAffinity: ["workflow"]
2675
+ }
2676
+ ],
2677
+ [
2678
+ "workflow_action",
2679
+ {
2680
+ schema: WorkflowActionSchema,
2681
+ synonyms: {
2682
+ label: ["title", "name"],
2683
+ transition: ["then"],
2684
+ "theme.variant": ["style"],
2685
+ action: ["on_click"]
2686
+ },
2687
+ phaseAffinity: ["workflow"]
2688
+ }
2689
+ ],
2690
+ // ── Navigation ────────────────────────────────────────────────────────────
2691
+ [
2692
+ "navigation_item",
2693
+ {
2694
+ schema: NavigationItemSchema,
2695
+ synonyms: {
2696
+ items: ["children"],
2697
+ section: ["label", "title"]
2698
+ },
2699
+ phaseAffinity: ["pages", "dashboard", "polish"]
2700
+ }
2701
+ ],
2702
+ // ── Auth ──────────────────────────────────────────────────────────────────
2703
+ [
2704
+ "auth_config",
2705
+ {
2706
+ schema: authConfigSchema,
2707
+ synonyms: {
2708
+ type: ["method", "provider_type", "strategy"]
2709
+ },
2710
+ phaseAffinity: ["auth"]
2711
+ }
2712
+ ],
2713
+ // ── Pulse content types ───────────────────────────────────────────────────
2714
+ [
2715
+ "metric_card_pulse",
2716
+ {
2717
+ schema: MetricCardPulseSchema,
2718
+ synonyms: {
2719
+ field: ["valueField", "valuePath", "dataField", "value"]
2720
+ },
2721
+ phaseAffinity: ["dashboard", "pages"]
2722
+ }
2723
+ ],
2724
+ [
2725
+ "data_table_pulse",
2726
+ {
2727
+ schema: DataTablePulseSchema,
2728
+ synonyms: {
2729
+ header: ["label"],
2730
+ type: ["renderAs"],
2731
+ colorMap: ["severityMap"]
2732
+ },
2733
+ phaseAffinity: ["dashboard", "pages"]
2734
+ }
2735
+ ],
2736
+ [
2737
+ "status_badge_pulse",
2738
+ {
2739
+ schema: StatusBadgePulseSchema,
2740
+ synonyms: {},
2741
+ phaseAffinity: ["dashboard", "pages"]
2742
+ }
2743
+ ],
2744
+ [
2745
+ "map_pulse",
2746
+ {
2747
+ schema: MapPulseSchema,
2748
+ synonyms: {},
2749
+ phaseAffinity: ["geo", "dashboard", "pages"]
2750
+ }
2751
+ ],
2752
+ // ── Workflow container page content type (swp-dvde) ───────────────────────
2753
+ [
2754
+ "workflow_wizard",
2755
+ {
2756
+ schema: WorkflowWizardContentSchema,
2757
+ synonyms: {
2758
+ workflowFile: ["workflowPath", "file", "path", "yamlFile"],
2759
+ workflowId: ["id", "workflowName", "name"],
2760
+ persistenceKey: ["stateKey", "storageKey"]
2761
+ },
2762
+ phaseAffinity: ["workflow"]
2763
+ }
2764
+ ],
2765
+ // ── Theme schemas (swp-cxwu) ──────────────────────────────────────────────
2766
+ [
2767
+ "theme_tokens",
2768
+ {
2769
+ schema: themeTokensFileSchema,
2770
+ synonyms: {},
2771
+ phaseAffinity: ["theme"]
2772
+ }
2773
+ ],
2774
+ [
2775
+ "theme_status_token",
2776
+ {
2777
+ // No static schema — this is a project-specific runtime enum.
2778
+ // Call getSchemaForEntry(entry, { projectRoot }) to get the project's ladder.
2779
+ schemaFactory: buildThemeStatusTokenSchema,
2780
+ synonyms: {},
2781
+ phaseAffinity: ["pages", "dashboard", "geo"]
2782
+ }
2783
+ ]
2784
+ ]);
2785
+ var SCHEMA_REGISTRY = REGISTRY;
2786
+ var REGISTRY_SCHEMA_NAMES = Array.from(REGISTRY.keys());
2787
+ function buildReverseSynonyms(synonyms) {
2788
+ const reverse = {};
2789
+ for (const [correct, wrongs] of Object.entries(synonyms)) {
2790
+ for (const wrong of wrongs) {
2791
+ reverse[wrong] = correct;
2792
+ }
2793
+ }
2794
+ return reverse;
2795
+ }
2796
+ function buildPhaseSchemaNames() {
2797
+ const phaseMap = {};
2798
+ for (const [name, entry] of REGISTRY.entries()) {
2799
+ for (const phase of entry.phaseAffinity ?? []) {
2800
+ if (!phaseMap[phase]) phaseMap[phase] = [];
2801
+ phaseMap[phase].push(name);
2802
+ }
2803
+ }
2804
+ return phaseMap;
2805
+ }
2806
+
2807
+ // src/tools/validate-theme-tokens.ts
2808
+ var COLOR_FIELD_KEYS = /* @__PURE__ */ new Set([
2809
+ "color",
2810
+ "colorMap",
2811
+ "layerColor",
2812
+ "borderColor",
2813
+ "backgroundColor",
2814
+ "iconColor",
2815
+ "fill",
2816
+ "stroke",
2817
+ "markerColor",
2818
+ "fillColor",
2819
+ "outlineColor",
2820
+ "statusColor",
2821
+ "badgeColor",
2822
+ "tagColor"
2823
+ ]);
2824
+ var LEGACY_OSS_TOKENS = /* @__PURE__ */ new Set(["success", "warning", "danger", "info", "muted"]);
2825
+ var LEGACY_MAPPING_HINT = "Suggested mapping: success->status-safe, warning->status-warning, danger->status-critical, info->status-watch, muted->status-at-risk.";
2826
+ var HEX_RE = /^#[0-9a-f]{3,8}$/i;
2827
+ var STATUS_TOKEN_RE = /^status-[a-z0-9-]+$/;
2828
+ function walkYaml(node, fieldName, pathParts, inColorContext, visitor) {
2829
+ if (typeof node === "string") {
2830
+ visitor(node, fieldName, pathParts.join(".") || "(root)", inColorContext);
2831
+ return;
2832
+ }
2833
+ if (Array.isArray(node)) {
2834
+ node.forEach((item, idx) => {
2835
+ walkYaml(item, fieldName, [...pathParts, `[${idx}]`], inColorContext, visitor);
2836
+ });
2837
+ return;
2838
+ }
2839
+ if (node !== null && typeof node === "object") {
2840
+ for (const [key, val] of Object.entries(node)) {
2841
+ const thisIsColorField = COLOR_FIELD_KEYS.has(key);
2842
+ walkYaml(val, key, [...pathParts, key], inColorContext || thisIsColorField, visitor);
2843
+ }
2844
+ }
2845
+ }
2846
+ function detectViolationsInYaml(yamlContent, ladder, ladderSet, relPath) {
2847
+ const violations = [];
2848
+ walkYaml(yamlContent, null, [], false, (value, fieldName, path5, inColorContext) => {
2849
+ const isColorField = inColorContext || fieldName !== null && COLOR_FIELD_KEYS.has(fieldName);
2850
+ if (STATUS_TOKEN_RE.test(value) && !ladderSet.has(value)) {
2851
+ violations.push({
2852
+ type: "undefined_token",
2853
+ message: `Undefined theme token: "${value}" not in declared theme ladder. Declared tokens: ${ladder.join(", ")}`,
2854
+ file: relPath,
2855
+ path: path5,
2856
+ value
2857
+ });
2858
+ return;
2859
+ }
2860
+ if (!isColorField) return;
2861
+ if (HEX_RE.test(value)) {
2862
+ violations.push({
2863
+ type: "raw_hex",
2864
+ message: `Raw hex color "${value}" in color field "${fieldName ?? "<color context>"}". Use declared theme tokens instead. Available: ${ladder.join(", ")}`,
2865
+ file: relPath,
2866
+ path: path5,
2867
+ value
2868
+ });
2869
+ return;
2870
+ }
2871
+ if (LEGACY_OSS_TOKENS.has(value)) {
2872
+ violations.push({
2873
+ type: "legacy_vocab",
2874
+ message: `Legacy status token "${value}" in color field "${fieldName ?? "<color context>"}". This project's ladder is: ${ladder.join(", ")}. ` + LEGACY_MAPPING_HINT,
2875
+ file: relPath,
2876
+ path: path5,
2877
+ value
2878
+ });
2879
+ }
2880
+ });
2881
+ return violations;
2882
+ }
2883
+ function findContentFiles(pagesRoot) {
2884
+ if (!existsSync7(pagesRoot)) return [];
2885
+ const files = [];
2886
+ try {
2887
+ const slugDirs = readdirSync3(pagesRoot, { withFileTypes: true });
2888
+ for (const entry of slugDirs) {
2889
+ if (!entry.isDirectory()) continue;
2890
+ const slugDir = join6(pagesRoot, entry.name);
2891
+ for (const candidate of ["content.yml", "content.yaml"]) {
2892
+ const p = join6(slugDir, candidate);
2893
+ if (existsSync7(p)) files.push(p);
2894
+ }
2895
+ }
2896
+ } catch {
2897
+ }
2898
+ return files;
2899
+ }
2900
+ function validateThemeTokenUsage(projectRoot) {
2901
+ const ladder = resolveStatusLadder(projectRoot);
2902
+ if (ladder.length === 0) {
2903
+ return {
2904
+ valid: true,
2905
+ violations: [],
2906
+ skipped: true
2907
+ };
2908
+ }
2909
+ const ladderSet = new Set(ladder);
2910
+ const pagesRoot = join6(projectRoot, "pages");
2911
+ const contentFiles = findContentFiles(pagesRoot);
2912
+ const allViolations = [];
2913
+ for (const absPath of contentFiles) {
2914
+ const relPath = relative2(projectRoot, absPath);
2915
+ try {
2916
+ const raw = readFileSync6(absPath, "utf-8");
2917
+ const parsed = yamlLoad2(raw);
2918
+ const fileViolations = detectViolationsInYaml(parsed, ladder, ladderSet, relPath);
2919
+ allViolations.push(...fileViolations);
2920
+ } catch {
2921
+ }
2922
+ }
2923
+ if (allViolations.length === 0) {
2924
+ return { valid: true, violations: [] };
2925
+ }
2926
+ const lines = [
2927
+ `Theme token enforcement found ${allViolations.length} violation(s):`,
2928
+ "",
2929
+ ...allViolations.map((v, i) => {
2930
+ const typeLabel = v.type === "undefined_token" ? "UNDEFINED TOKEN" : v.type === "raw_hex" ? "RAW HEX" : "LEGACY VOCAB";
2931
+ return ` ${i + 1}. [${typeLabel}] ${v.message}
2932
+ Emitted in: ${v.file} at ${v.path}`;
2933
+ }),
2934
+ "",
2935
+ `Fix all violations to use declared theme tokens: ${ladder.join(", ")}`
2936
+ ];
2937
+ return {
2938
+ valid: false,
2939
+ violations: allViolations,
2940
+ retryPrompt: lines.join("\n")
2941
+ };
2942
+ }
2943
+
2944
+ // src/tools/validate-build-context-globals.ts
2945
+ import { existsSync as existsSync8, readFileSync as readFileSync7, readdirSync as readdirSync4 } from "fs";
2946
+ import { join as join7, relative as relative3 } from "path";
2947
+ import { load as yamlLoad3 } from "js-yaml";
2948
+ var DATA_WIDGET_TYPES = /* @__PURE__ */ new Set([
2949
+ "data_table_pulse",
2950
+ "stats_grid",
2951
+ "metric_card_pulse",
2952
+ "map_pulse"
2953
+ ]);
2954
+ function readGlobalDefaults(projectRoot) {
2955
+ const bcPath = join7(projectRoot, ".stackwright", "build-context.json");
2956
+ if (!existsSync8(bcPath)) return null;
2957
+ try {
2958
+ const raw = readFileSync7(bcPath, "utf-8");
2959
+ const parsed = JSON.parse(raw);
2960
+ const gd = parsed.globalDefaults;
2961
+ if (!gd || typeof gd !== "object") return null;
2962
+ return gd;
2963
+ } catch {
2964
+ return null;
2965
+ }
2966
+ }
2967
+ function findDataWidgets(node, pathParts, out) {
2968
+ if (node === null || node === void 0 || typeof node !== "object") return;
2969
+ if (Array.isArray(node)) {
2970
+ node.forEach((item, idx) => {
2971
+ findDataWidgets(item, [...pathParts, `[${idx}]`], out);
2972
+ });
2973
+ return;
2974
+ }
2975
+ const obj = node;
2976
+ const contentType = typeof obj["type"] === "string" ? obj["type"] : null;
2977
+ if (contentType && DATA_WIDGET_TYPES.has(contentType)) {
2978
+ out.push({
2979
+ type: contentType,
2980
+ label: typeof obj["label"] === "string" ? obj["label"] : void 0,
2981
+ data: obj,
2982
+ path: pathParts.join(".") || "(root)"
2983
+ });
2984
+ }
2985
+ for (const [key, val] of Object.entries(obj)) {
2986
+ findDataWidgets(val, [...pathParts, key], out);
2987
+ }
2988
+ }
2989
+ function findContentFiles2(pagesRoot) {
2990
+ if (!existsSync8(pagesRoot)) return [];
2991
+ const files = [];
2992
+ try {
2993
+ const slugDirs = readdirSync4(pagesRoot, { withFileTypes: true });
2994
+ for (const entry of slugDirs) {
2995
+ if (!entry.isDirectory()) continue;
2996
+ for (const candidate of ["content.yml", "content.yaml"]) {
2997
+ const p = join7(pagesRoot, entry.name, candidate);
2998
+ if (existsSync8(p)) files.push(p);
2999
+ }
3000
+ }
3001
+ } catch {
3002
+ }
3003
+ return files;
3004
+ }
3005
+ function validateBuildContextGlobals(projectRoot) {
3006
+ const globalDefaults = readGlobalDefaults(projectRoot);
3007
+ if (!globalDefaults) {
3008
+ return { valid: true, violations: [], skipped: true };
3009
+ }
3010
+ const requiredFields = Object.entries(globalDefaults).filter(([, v]) => v === true).map(([k]) => k);
3011
+ if (requiredFields.length === 0) {
3012
+ return { valid: true, violations: [], skipped: true };
3013
+ }
3014
+ const pagesRoot = join7(projectRoot, "pages");
3015
+ const contentFiles = findContentFiles2(pagesRoot);
3016
+ const allViolations = [];
3017
+ for (const absPath of contentFiles) {
3018
+ const relPath = relative3(projectRoot, absPath);
3019
+ try {
3020
+ const raw = readFileSync7(absPath, "utf-8");
3021
+ const parsed = yamlLoad3(raw);
3022
+ const widgets = [];
3023
+ findDataWidgets(parsed, [], widgets);
3024
+ for (const widget of widgets) {
3025
+ for (const field of requiredFields) {
3026
+ if (widget.data[field] !== true) {
3027
+ allViolations.push({
3028
+ field,
3029
+ widgetType: widget.type,
3030
+ widgetLabel: widget.label,
3031
+ file: relPath,
3032
+ path: widget.path
3033
+ });
3034
+ }
3035
+ }
3036
+ }
3037
+ } catch {
3038
+ }
3039
+ }
3040
+ if (allViolations.length === 0) {
3041
+ return { valid: true, violations: [] };
3042
+ }
3043
+ const lines = [
3044
+ `BUILD_CONTEXT global defaults: ${allViolations.length} violation(s) found.`,
3045
+ "",
3046
+ ...allViolations.map((v, i) => {
3047
+ const widgetId = v.widgetLabel ? `"${v.widgetLabel}" (${v.widgetType})` : `${v.widgetType}`;
3048
+ return ` ${i + 1}. BUILD_CONTEXT.${v.field} is globally true but widget ${widgetId} omits it.
3049
+ Set ${v.field}: true on this widget.
3050
+ Emitted in: ${v.file} at ${v.path}`;
3051
+ }),
3052
+ "",
3053
+ `Required global defaults: ${requiredFields.map((f) => `${f}: true`).join(", ")}`
3054
+ ];
3055
+ return {
3056
+ valid: false,
3057
+ violations: allViolations,
3058
+ retryPrompt: lines.join("\n")
3059
+ };
3060
+ }
3061
+
3062
+ // src/tools/validate-workflow-container-pages.ts
3063
+ import { existsSync as existsSync9, readFileSync as readFileSync8, readdirSync as readdirSync5 } from "fs";
3064
+ import { join as join8 } from "path";
3065
+ import { load as yamlLoad4 } from "js-yaml";
3066
+ function discoverWorkflowIds(projectRoot) {
3067
+ const artifactsDir = join8(projectRoot, ".stackwright", "artifacts");
3068
+ if (!existsSync9(artifactsDir)) return [];
3069
+ const entries = [];
3070
+ let files;
3071
+ try {
3072
+ files = readdirSync5(artifactsDir);
3073
+ } catch {
3074
+ return [];
3075
+ }
3076
+ for (const filename of files) {
3077
+ if (filename !== "workflow-config.json" && !filename.startsWith("workflow-config-")) continue;
3078
+ if (!filename.endsWith(".json")) continue;
3079
+ const fullPath = join8(artifactsDir, filename);
3080
+ try {
3081
+ const raw = JSON.parse(readFileSync8(fullPath, "utf-8"));
3082
+ const workflowData = raw["workflow"] ?? raw["workflowConfig"];
3083
+ const workflowId = typeof workflowData?.["id"] === "string" ? workflowData["id"] : null;
3084
+ if (workflowId && /^[a-z0-9-]+$/.test(workflowId)) {
3085
+ entries.push({ workflowId, sourceFile: filename });
3086
+ }
3087
+ } catch {
3088
+ }
3089
+ }
3090
+ return entries;
3091
+ }
3092
+ function hasWorkflowWizardType(filePath) {
3093
+ try {
3094
+ const raw = readFileSync8(filePath, "utf-8");
3095
+ const parsed = yamlLoad4(raw);
3096
+ if (!parsed) return false;
3097
+ const content = parsed["content"];
3098
+ if (!content) return false;
3099
+ const items = content["content_items"];
3100
+ if (!Array.isArray(items)) return false;
3101
+ return items.some((item) => item["type"] === "workflow_wizard");
3102
+ } catch {
3103
+ return false;
3104
+ }
3105
+ }
3106
+ function validateWorkflowContainerPages(projectRoot) {
3107
+ const workflows = discoverWorkflowIds(projectRoot);
3108
+ if (workflows.length === 0) {
3109
+ return { valid: true, violations: [], skipped: true };
3110
+ }
3111
+ const violations = [];
3112
+ for (const { workflowId, sourceFile: _sourceFile } of workflows) {
3113
+ const expectedPath = `pages/workflows/${workflowId}/content.yml`;
3114
+ const fullPath = join8(projectRoot, expectedPath);
3115
+ if (!existsSync9(fullPath)) {
3116
+ violations.push({ workflowId, expectedPath, reason: "missing-file" });
3117
+ continue;
3118
+ }
3119
+ if (!hasWorkflowWizardType(fullPath)) {
3120
+ violations.push({ workflowId, expectedPath, reason: "missing-workflow-wizard-type" });
3121
+ }
3122
+ }
3123
+ if (violations.length === 0) {
3124
+ return { valid: true, violations: [] };
3125
+ }
3126
+ const lines = [
3127
+ `Workflow container page cross-check failed: ${violations.length} workflow(s) missing container pages.`,
3128
+ "",
3129
+ ...violations.map((v, i) => {
3130
+ const reason = v.reason === "missing-file" ? `File does not exist. Create it via stackwright_pro_write_workflow_container_page.` : `File exists but has no workflow_wizard content item. Must contain type: workflow_wizard.`;
3131
+ return ` ${i + 1}. workflowId "${v.workflowId}"
3132
+ Expected: ${v.expectedPath}
3133
+ Issue: ${reason}`;
3134
+ }),
3135
+ "",
3136
+ "Fix: workflow-otter must call stackwright_pro_write_workflow_container_page immediately",
3137
+ "after writing each workflow YAML. Required args:",
3138
+ ' { workflowId, workflowFile, pageSlug: "workflows/<workflowId>", label, requiredRoles }',
3139
+ "",
3140
+ "Without the container page, the route 404s and auth-reconcile cannot protect it."
3141
+ ];
3142
+ return {
3143
+ valid: false,
3144
+ violations,
3145
+ retryPrompt: lines.join("\n")
3146
+ };
3147
+ }
3148
+
2554
3149
  // src/tools/validate-api-integration.ts
2555
3150
  var CANONICAL_API_AUTH_TYPES = ["apiKey", "bearer", "oauth2", "oidc", "none"];
2556
3151
  var CANONICAL_SET = new Set(CANONICAL_API_AUTH_TYPES);
@@ -2743,9 +3338,9 @@ function validateAuthConfig(artifact, artifactPath) {
2743
3338
  }
2744
3339
 
2745
3340
  // src/tools/auth-manifest-aggregator.ts
2746
- import { existsSync as existsSync6, readFileSync as readFileSync5, readdirSync as readdirSync3 } from "fs";
2747
- import { join as join5 } from "path";
2748
- import { z as z11 } from "zod";
3341
+ import { existsSync as existsSync10, readFileSync as readFileSync9, readdirSync as readdirSync6 } from "fs";
3342
+ import { join as join9 } from "path";
3343
+ import { z as z12 } from "zod";
2749
3344
  function computeLowestRole(requiredRoles, rbacRoles) {
2750
3345
  let lowestIndex = -1;
2751
3346
  let lowestRole = requiredRoles[0];
@@ -2765,17 +3360,17 @@ function extractPatternBaseSlug(pattern) {
2765
3360
  }
2766
3361
  function extractManifestSlugs(artifactsDir) {
2767
3362
  const slugs = /* @__PURE__ */ new Set();
2768
- if (!existsSync6(artifactsDir)) return slugs;
3363
+ if (!existsSync10(artifactsDir)) return slugs;
2769
3364
  let entries;
2770
3365
  try {
2771
- entries = readdirSync3(artifactsDir);
3366
+ entries = readdirSync6(artifactsDir);
2772
3367
  } catch {
2773
3368
  return slugs;
2774
3369
  }
2775
3370
  for (const entry of entries) {
2776
3371
  if (!entry.endsWith("-manifest.json")) continue;
2777
3372
  try {
2778
- const content = JSON.parse(readFileSync5(join5(artifactsDir, entry), "utf-8"));
3373
+ const content = JSON.parse(readFileSync9(join9(artifactsDir, entry), "utf-8"));
2779
3374
  const pages = content.pages;
2780
3375
  if (!Array.isArray(pages)) continue;
2781
3376
  for (const page of pages) {
@@ -2818,18 +3413,18 @@ function deriveProtectedRoutes(pages, rbacRoles) {
2818
3413
  }
2819
3414
  function readAllManifestPages(artifactsDir) {
2820
3415
  const allPages = [];
2821
- if (!existsSync6(artifactsDir)) return allPages;
3416
+ if (!existsSync10(artifactsDir)) return allPages;
2822
3417
  let entries;
2823
3418
  try {
2824
- entries = readdirSync3(artifactsDir);
3419
+ entries = readdirSync6(artifactsDir);
2825
3420
  } catch {
2826
3421
  return allPages;
2827
3422
  }
2828
3423
  for (const entry of entries) {
2829
3424
  if (entry.endsWith("-manifest.json")) {
2830
- const filePath = join5(artifactsDir, entry);
3425
+ const filePath = join9(artifactsDir, entry);
2831
3426
  try {
2832
- const raw = JSON.parse(readFileSync5(filePath, "utf-8"));
3427
+ const raw = JSON.parse(readFileSync9(filePath, "utf-8"));
2833
3428
  if (!Array.isArray(raw.pages)) continue;
2834
3429
  for (const page of raw.pages) {
2835
3430
  if (typeof page.slug !== "string" || !page.slug) continue;
@@ -2845,9 +3440,9 @@ function readAllManifestPages(artifactsDir) {
2845
3440
  continue;
2846
3441
  }
2847
3442
  if (entry === "workflow-config.json" || entry.startsWith("workflow-config-")) {
2848
- const filePath = join5(artifactsDir, entry);
3443
+ const filePath = join9(artifactsDir, entry);
2849
3444
  try {
2850
- const raw = JSON.parse(readFileSync5(filePath, "utf-8"));
3445
+ const raw = JSON.parse(readFileSync9(filePath, "utf-8"));
2851
3446
  const route = raw.workflow?.route;
2852
3447
  if (typeof route !== "string" || !route) continue;
2853
3448
  const slug = route.startsWith("/") ? route.slice(1) : route;
@@ -2869,16 +3464,16 @@ function registerReadAuthManifestsTool(server2) {
2869
3464
  "stackwright_pro_read_auth_manifests",
2870
3465
  "Read ALL route-producing artifacts from the pipeline artifacts directory: *-manifest.json files (pages, dashboard, geo, form-wizard) AND workflow-config.json / workflow-config-*.json (different shape: workflow.route). Deterministically derives protectedRoutes and uncoveredSlugs. Uses the both-forms invariant (swp-n7kk): every authRequired route registers BOTH /{slug} (exact) AND /{slug}/:path* (sub-path). Workflow routes appear in uncoveredSlugs (no requiredRoles declared) for auth-reconcile to resolve via cross-cutting rules from auth-config.json. Extended by swp-hng0 to cover all four manifest types (was: pages+dashboard only).",
2871
3466
  {
2872
- rbacRoles: z11.array(z11.string()).describe(
3467
+ rbacRoles: z12.array(z12.string()).describe(
2873
3468
  'RBAC role hierarchy in descending privilege order, e.g. ["ESF8_COORDINATOR", "HOSPITAL_EM", "OBSERVER"]. Used to compute the lowestRole for each authRequired page.'
2874
3469
  ),
2875
- artifactsDir: z11.string().optional().describe(
3470
+ artifactsDir: z12.string().optional().describe(
2876
3471
  "Absolute or relative path to the pipeline artifacts directory. Defaults to .stackwright/artifacts relative to cwd."
2877
3472
  )
2878
3473
  },
2879
3474
  (params) => {
2880
3475
  const cwd = process.cwd();
2881
- const artifactsDir = params.artifactsDir ? params.artifactsDir : join5(cwd, ".stackwright", "artifacts");
3476
+ const artifactsDir = params.artifactsDir ? params.artifactsDir : join9(cwd, ".stackwright", "artifacts");
2882
3477
  const allPages = readAllManifestPages(artifactsDir);
2883
3478
  const { protectedRoutes, uncoveredSlugs } = deriveProtectedRoutes(allPages, params.rbacRoles);
2884
3479
  const manifestsRead = [...new Set(allPages.map((p) => p.source))].sort();
@@ -2901,195 +3496,11 @@ function registerReadAuthManifestsTool(server2) {
2901
3496
  }
2902
3497
 
2903
3498
  // src/tools/pipeline.ts
2904
- import { WorkflowFileSchema, authConfigSchema as authConfigSchema3 } from "@stackwright-pro/types";
3499
+ import { WorkflowFileSchema, authConfigSchema as authConfigSchema2 } from "@stackwright-pro/types";
2905
3500
 
2906
3501
  // src/tools/get-schema.ts
2907
3502
  import { z as z13 } from "zod";
2908
- import {
2909
- WorkflowStepSchema as WorkflowStepSchema2,
2910
- WorkflowDefinitionSchema as WorkflowDefinitionSchema2,
2911
- WorkflowFieldSchema as WorkflowFieldSchema2,
2912
- WorkflowActionSchema as WorkflowActionSchema2,
2913
- authConfigSchema as authConfigSchema2
2914
- } from "@stackwright-pro/types";
2915
-
2916
- // src/tools/validate-yaml-fragment.ts
2917
- import { z as z12 } from "zod";
2918
- import { load as yamlLoad2 } from "js-yaml";
2919
- import {
2920
- WorkflowStepSchema,
2921
- WorkflowDefinitionSchema,
2922
- WorkflowFieldSchema,
2923
- WorkflowActionSchema,
2924
- authConfigSchema
2925
- } from "@stackwright-pro/types";
2926
- var SUPPORTED_SCHEMA_NAMES = [
2927
- "workflow_step",
2928
- "workflow_definition",
2929
- "workflow_field",
2930
- "workflow_action",
2931
- "navigation_item",
2932
- "auth_config"
2933
- ];
2934
- var NavigationLinkSchema = z12.lazy(
2935
- () => z12.object({
2936
- label: z12.string().min(1),
2937
- href: z12.string().min(1),
2938
- children: z12.array(NavigationLinkSchema).optional()
2939
- })
2940
- );
2941
- var NavigationSectionSchema = z12.object({
2942
- section: z12.string().min(1),
2943
- items: z12.array(NavigationLinkSchema)
2944
- });
2945
- var NavigationItemSchema = z12.union([
2946
- NavigationLinkSchema,
2947
- NavigationSectionSchema
2948
- ]);
2949
- var FIELD_SYNONYMS = {
2950
- workflow_step: {
2951
- title: "label",
2952
- name: "label (at step level, use label: not name:)",
2953
- description: "message (use message: for step-level descriptive text)",
2954
- style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
2955
- transitions: 'on_submit.transition \u2014 use on_submit: { transition: "next_step" } not transitions: [{then:...}]'
2956
- },
2957
- workflow_definition: {
2958
- title: "label",
2959
- name: "label (workflow-level label, not name:)",
2960
- description: "description is valid here \u2014 but message: is not (that is a step field)",
2961
- type: "id (workflows are identified by id:, not type:)"
2962
- },
2963
- workflow_field: {
2964
- id: "name (field-level identifier is name:, NOT id:)",
2965
- title: "label (field display text is label:, NOT title:)",
2966
- description: "placeholder or label (no description field on WorkflowField)"
2967
- },
2968
- workflow_action: {
2969
- style: 'theme.variant \u2014 use theme: { variant: "primary" } not style: "primary"',
2970
- then: "transition (action-level next-step is transition:, NOT then:)",
2971
- on_click: 'action: "service:..." (extract the service reference from on_click.action)',
2972
- title: "label (action display text is label:, NOT title:)",
2973
- name: "label (action display text is label:, NOT name:)"
2974
- },
2975
- navigation_item: {
2976
- children: "items (NavigationSection uses items: not children: \u2014 also use section: instead of label: for section headers)",
2977
- label: 'section (if this is a nav group/section, use section: "Title" and items: [...], not label:)',
2978
- title: "section (NavigationSection header field is section:, NOT title:)"
2979
- },
2980
- auth_config: {
2981
- method: 'type \u2014 auth config discriminates on type: "oidc" | "pki", NOT method:',
2982
- provider_type: 'type \u2014 use type: "oidc" or type: "pki"',
2983
- strategy: "type \u2014 top-level discriminator is type:, not strategy:"
2984
- }
2985
- };
2986
- function getSchema(name) {
2987
- switch (name) {
2988
- case "workflow_step":
2989
- return WorkflowStepSchema;
2990
- case "workflow_definition":
2991
- return WorkflowDefinitionSchema;
2992
- case "workflow_field":
2993
- return WorkflowFieldSchema;
2994
- case "workflow_action":
2995
- return WorkflowActionSchema;
2996
- case "navigation_item":
2997
- return NavigationItemSchema;
2998
- case "auth_config":
2999
- return authConfigSchema;
3000
- }
3001
- }
3002
- function enrichErrors(issues, synonyms) {
3003
- return issues.map((issue) => {
3004
- const pathStr = issue.path.join(".");
3005
- const lastSegment = issue.path[issue.path.length - 1];
3006
- const fieldName = typeof lastSegment === "string" ? lastSegment : void 0;
3007
- const didYouMean = fieldName ? synonyms[fieldName] : void 0;
3008
- return {
3009
- path: pathStr || "(root)",
3010
- message: issue.message,
3011
- ...didYouMean ? { didYouMean } : {}
3012
- };
3013
- });
3014
- }
3015
- function handleValidateYamlFragment(input) {
3016
- const { schemaName, yaml: yaml4 } = input;
3017
- if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
3018
- return {
3019
- valid: false,
3020
- parseError: `Unknown schemaName: "${schemaName}". Supported: ${SUPPORTED_SCHEMA_NAMES.join(", ")}`
3021
- };
3022
- }
3023
- const name = schemaName;
3024
- let parsed;
3025
- try {
3026
- parsed = yamlLoad2(yaml4);
3027
- } catch (err) {
3028
- return {
3029
- valid: false,
3030
- parseError: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`
3031
- };
3032
- }
3033
- const schema = getSchema(name);
3034
- const result = schema.safeParse(parsed);
3035
- if (result.success) {
3036
- return { valid: true };
3037
- }
3038
- const synonyms = FIELD_SYNONYMS[name];
3039
- const errors = enrichErrors(result.error.issues, synonyms);
3040
- return { valid: false, errors };
3041
- }
3042
- function registerValidateYamlFragmentTool(server2) {
3043
- server2.tool(
3044
- "stackwright_pro_validate_yaml_fragment",
3045
- 'Validates a YAML snippet against a canonical Zod schema before writing to disk. Returns actionable errors with "did you mean?" suggestions for common field-name drift (title vs label, id vs name, style vs theme.variant, etc.). Call this before stackwright_pro_safe_write to catch schema violations in-session.',
3046
- {
3047
- schemaName: z12.enum(SUPPORTED_SCHEMA_NAMES).describe(
3048
- "Schema to validate against. One of: workflow_step, workflow_definition, workflow_field, workflow_action, navigation_item, auth_config"
3049
- ),
3050
- yaml: z12.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
3051
- },
3052
- async ({ schemaName, yaml: yaml4 }) => {
3053
- const result = handleValidateYamlFragment({ schemaName, yaml: yaml4 });
3054
- return {
3055
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
3056
- };
3057
- }
3058
- );
3059
- }
3060
-
3061
- // src/tools/get-schema.ts
3062
- var NavigationLinkSchema2 = z13.lazy(
3063
- () => z13.object({
3064
- label: z13.string().min(1),
3065
- href: z13.string().min(1),
3066
- children: z13.array(NavigationLinkSchema2).optional()
3067
- })
3068
- );
3069
- var NavigationSectionSchema2 = z13.object({
3070
- section: z13.string().min(1),
3071
- items: z13.array(NavigationLinkSchema2)
3072
- });
3073
- var NavigationItemSchema2 = z13.union([
3074
- NavigationLinkSchema2,
3075
- NavigationSectionSchema2
3076
- ]);
3077
- function getZodSchema(name) {
3078
- switch (name) {
3079
- case "workflow_step":
3080
- return WorkflowStepSchema2;
3081
- case "workflow_definition":
3082
- return WorkflowDefinitionSchema2;
3083
- case "workflow_field":
3084
- return WorkflowFieldSchema2;
3085
- case "workflow_action":
3086
- return WorkflowActionSchema2;
3087
- case "navigation_item":
3088
- return NavigationItemSchema2;
3089
- case "auth_config":
3090
- return authConfigSchema2;
3091
- }
3092
- }
3503
+ var PHASE_SCHEMA_NAMES = buildPhaseSchemaNames();
3093
3504
  function formatType(node, depth = 0) {
3094
3505
  if (node.const !== void 0) return JSON.stringify(node.const);
3095
3506
  if (node.enum) return node.enum.map((v) => JSON.stringify(v)).join(" | ");
@@ -3116,7 +3527,7 @@ function formatType(node, depth = 0) {
3116
3527
  if (node.maximum !== void 0) constraints.push(`max ${node.maximum}`);
3117
3528
  return constraints.length > 0 ? `${base} (${constraints.join(", ")})` : base;
3118
3529
  }
3119
- function formatObjectSchema(node, schemaName, synonyms) {
3530
+ function formatObjectSchema(node, schemaName, reverseSynonyms) {
3120
3531
  const lines = [];
3121
3532
  const displayName = schemaName.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join("");
3122
3533
  lines.push(`${displayName}:`);
@@ -3130,7 +3541,8 @@ function formatObjectSchema(node, schemaName, synonyms) {
3130
3541
  for (const [field, fieldNode] of Object.entries(branch.properties ?? {})) {
3131
3542
  if (fieldNode.const !== void 0) continue;
3132
3543
  const isRequired = req.has(field);
3133
- const hint = synonyms[field] ? ` -- WARNING: use ${field}: not ${synonyms[field]}` : "";
3544
+ const correctFor = reverseSynonyms[field];
3545
+ const hint = correctFor ? ` -- WARNING: use ${correctFor}: not ${field}:` : "";
3134
3546
  lines.push(
3135
3547
  ` ${isRequired ? "REQUIRED" : "optional"}: ${field}: ${formatType(fieldNode)}${hint}`
3136
3548
  );
@@ -3138,6 +3550,12 @@ function formatObjectSchema(node, schemaName, synonyms) {
3138
3550
  }
3139
3551
  return lines.join("\n");
3140
3552
  }
3553
+ if (!node.properties && !node.oneOf && !node.anyOf) {
3554
+ const typeStr = formatType(node);
3555
+ lines.push(` type: ${typeStr}`);
3556
+ if (node.description) lines.push(` description: ${node.description}`);
3557
+ return lines.join("\n");
3558
+ }
3141
3559
  const required = new Set(node.required ?? []);
3142
3560
  const properties = node.properties ?? {};
3143
3561
  const requiredFields = Object.entries(properties).filter(([k]) => required.has(k));
@@ -3146,8 +3564,7 @@ function formatObjectSchema(node, schemaName, synonyms) {
3146
3564
  lines.push(" REQUIRED:");
3147
3565
  for (const [field, fieldNode] of requiredFields) {
3148
3566
  const typeStr = formatType(fieldNode);
3149
- const warn = synonyms[field] ? ` -- CAUTION: this schema uses ${field}` : "";
3150
- lines.push(` ${field}: ${typeStr}${warn}`);
3567
+ lines.push(` ${field}: ${typeStr}`);
3151
3568
  }
3152
3569
  }
3153
3570
  if (optionalFields.length > 0) {
@@ -3161,24 +3578,30 @@ function formatObjectSchema(node, schemaName, synonyms) {
3161
3578
  }
3162
3579
  function handleGetSchema(input) {
3163
3580
  const { schemaName } = input;
3164
- if (!SUPPORTED_SCHEMA_NAMES.includes(schemaName)) {
3581
+ const entry = SCHEMA_REGISTRY.get(schemaName);
3582
+ if (!entry) {
3165
3583
  return {
3166
- error: `Unknown schemaName: "${schemaName}". Supported: ${SUPPORTED_SCHEMA_NAMES.join(", ")}`
3584
+ error: `Unknown schemaName: "${schemaName}". Supported: ${REGISTRY_SCHEMA_NAMES.join(", ")}`
3167
3585
  };
3168
3586
  }
3169
- const name = schemaName;
3170
- const schema = getZodSchema(name);
3171
- const synonyms = FIELD_SYNONYMS[name];
3587
+ const synonyms = entry.synonyms ?? {};
3588
+ const reverseSynonyms = {};
3589
+ for (const [correct, wrongs] of Object.entries(synonyms)) {
3590
+ for (const wrong of wrongs) {
3591
+ reverseSynonyms[wrong] = correct;
3592
+ }
3593
+ }
3594
+ const ctx = input.projectRoot ? { projectRoot: input.projectRoot } : void 0;
3595
+ const resolvedSchema = getSchemaForEntry(entry, ctx);
3172
3596
  let jsonSchema;
3173
3597
  try {
3174
- const raw = z13.toJSONSchema(schema, { unrepresentable: "any" });
3175
- jsonSchema = raw;
3598
+ jsonSchema = z13.toJSONSchema(resolvedSchema, { unrepresentable: "any" });
3176
3599
  } catch {
3177
3600
  jsonSchema = { type: "object", description: "Schema introspection unavailable for this type" };
3178
3601
  }
3179
- const summary = formatObjectSchema(jsonSchema, name, synonyms);
3602
+ const summary = formatObjectSchema(jsonSchema, schemaName, reverseSynonyms);
3180
3603
  const synonymWarnings = Object.entries(synonyms).map(
3181
- ([wrong, correct]) => ` WARNING: use "${correct}" \u2014 NOT "${wrong}"`
3604
+ ([correct, wrongs]) => ` WARNING: use "${correct}" \u2014 NOT ${wrongs.map((w) => `"${w}"`).join(" or ")}`
3182
3605
  );
3183
3606
  const fullText = [
3184
3607
  summary,
@@ -3188,19 +3611,12 @@ function handleGetSchema(input) {
3188
3611
  ].join("\n");
3189
3612
  const tokenEstimate = Math.ceil(fullText.length / 4);
3190
3613
  return {
3191
- schemaName: name,
3614
+ schemaName,
3192
3615
  summary,
3193
3616
  synonymWarnings,
3194
3617
  tokenEstimate
3195
3618
  };
3196
3619
  }
3197
- var PHASE_SCHEMA_NAMES = {
3198
- workflow: ["workflow_step", "workflow_definition", "workflow_field", "workflow_action"],
3199
- pages: ["navigation_item"],
3200
- dashboard: ["navigation_item"],
3201
- polish: ["navigation_item"],
3202
- auth: ["auth_config"]
3203
- };
3204
3620
  function buildSchemaReferenceBlock(phase) {
3205
3621
  const schemaNames = PHASE_SCHEMA_NAMES[phase];
3206
3622
  if (!schemaNames || schemaNames.length === 0) return null;
@@ -3225,12 +3641,13 @@ function registerGetSchemaTool(server2) {
3225
3641
  "stackwright_pro_get_schema",
3226
3642
  'Returns a compact, LLM-friendly summary of a canonical schema derived programmatically from Zod. Includes required/optional fields, types, constraints, and "did you mean?" warnings for common field-name drift. Use before authoring YAML to confirm exact field names.',
3227
3643
  {
3228
- schemaName: z13.enum(SUPPORTED_SCHEMA_NAMES).describe(
3229
- "Schema to summarize. One of: workflow_step, workflow_definition, workflow_field, workflow_action, navigation_item, auth_config"
3644
+ schemaName: z13.string().describe(`Schema to summarize. Available: ${REGISTRY_SCHEMA_NAMES.join(", ")}`),
3645
+ projectRoot: z13.string().optional().describe(
3646
+ "Absolute path to the project root. Required for factory schemas like theme_status_token. Defaults to cwd."
3230
3647
  )
3231
3648
  },
3232
- async ({ schemaName }) => {
3233
- const result = handleGetSchema({ schemaName });
3649
+ async ({ schemaName, projectRoot }) => {
3650
+ const result = handleGetSchema({ schemaName, projectRoot: projectRoot ?? process.cwd() });
3234
3651
  if ("error" in result) {
3235
3652
  return {
3236
3653
  content: [{ type: "text", text: JSON.stringify({ error: result.error }) }],
@@ -3250,8 +3667,8 @@ function registerGetSchemaTool(server2) {
3250
3667
  }
3251
3668
 
3252
3669
  // src/tools/pipeline-graph.ts
3253
- import { readFileSync as readFileSync6, readdirSync as readdirSync4, lstatSync as lstatSync5 } from "fs";
3254
- import { join as join6 } from "path";
3670
+ import { readFileSync as readFileSync10, readdirSync as readdirSync7, lstatSync as lstatSync5 } from "fs";
3671
+ import { join as join10 } from "path";
3255
3672
  var MANIFEST_NAME_TO_PHASE = {
3256
3673
  "stackwright-pro-designer-otter": "designer",
3257
3674
  "stackwright-pro-theme-otter": "theme",
@@ -3283,8 +3700,8 @@ var OTTER_SEARCH_PATHS = [
3283
3700
  ];
3284
3701
  function resolveOtterDirFromCwd() {
3285
3702
  const cwd = process.cwd();
3286
- for (const relative2 of OTTER_SEARCH_PATHS) {
3287
- const candidate = join6(cwd, relative2);
3703
+ for (const relative4 of OTTER_SEARCH_PATHS) {
3704
+ const candidate = join10(cwd, relative4);
3288
3705
  try {
3289
3706
  lstatSync5(candidate);
3290
3707
  return candidate;
@@ -3296,14 +3713,14 @@ function resolveOtterDirFromCwd() {
3296
3713
  );
3297
3714
  }
3298
3715
  function loadOtterManifestsFromDir(otterDir) {
3299
- const entries = readdirSync4(otterDir);
3716
+ const entries = readdirSync7(otterDir);
3300
3717
  const manifests = [];
3301
3718
  for (const filename of entries) {
3302
3719
  if (!filename.endsWith("-otter.json")) continue;
3303
- const filePath = join6(otterDir, filename);
3720
+ const filePath = join10(otterDir, filename);
3304
3721
  if (lstatSync5(filePath).isSymbolicLink()) continue;
3305
3722
  try {
3306
- const raw = readFileSync6(filePath, "utf-8");
3723
+ const raw = readFileSync10(filePath, "utf-8");
3307
3724
  const manifest = JSON.parse(raw);
3308
3725
  manifests.push(manifest);
3309
3726
  } catch (err) {
@@ -3408,19 +3825,19 @@ var DISTRIBUTED_NAMESPACES = ["@stackwright-pro", "@stackwright"];
3408
3825
  function loadDistributedOtterManifests(cwd) {
3409
3826
  const results = [];
3410
3827
  for (const namespace of DISTRIBUTED_NAMESPACES) {
3411
- const nsDir = join6(cwd, "node_modules", namespace);
3828
+ const nsDir = join10(cwd, "node_modules", namespace);
3412
3829
  let pkgDirNames;
3413
3830
  try {
3414
- pkgDirNames = readdirSync4(nsDir);
3831
+ pkgDirNames = readdirSync7(nsDir);
3415
3832
  } catch {
3416
3833
  continue;
3417
3834
  }
3418
3835
  for (const pkgDirName of pkgDirNames) {
3419
- const pkgDir = join6(nsDir, pkgDirName);
3836
+ const pkgDir = join10(nsDir, pkgDirName);
3420
3837
  const packageName = `${namespace}/${pkgDirName}`;
3421
3838
  let otterRelPaths;
3422
3839
  try {
3423
- const raw = readFileSync6(join6(pkgDir, "package.json"), "utf-8");
3840
+ const raw = readFileSync10(join10(pkgDir, "package.json"), "utf-8");
3424
3841
  const pkgJson = JSON.parse(raw);
3425
3842
  const declared = pkgJson.stackwright?.otters;
3426
3843
  if (!Array.isArray(declared) || declared.length === 0) continue;
@@ -3429,9 +3846,9 @@ function loadDistributedOtterManifests(cwd) {
3429
3846
  continue;
3430
3847
  }
3431
3848
  for (const relPath of otterRelPaths) {
3432
- const otterFilePath = join6(pkgDir, relPath);
3849
+ const otterFilePath = join10(pkgDir, relPath);
3433
3850
  try {
3434
- const raw = readFileSync6(otterFilePath, "utf-8");
3851
+ const raw = readFileSync10(otterFilePath, "utf-8");
3435
3852
  const manifest = JSON.parse(raw);
3436
3853
  results.push({ manifest, packageName });
3437
3854
  } catch (err) {
@@ -3576,18 +3993,18 @@ function createDefaultState() {
3576
3993
  };
3577
3994
  }
3578
3995
  function statePath(cwd) {
3579
- return join7(cwd, ".stackwright", "pipeline-state.json");
3996
+ return join11(cwd, ".stackwright", "pipeline-state.json");
3580
3997
  }
3581
3998
  function readState(cwd) {
3582
3999
  const p = statePath(cwd);
3583
- if (!existsSync7(p)) return createDefaultState();
4000
+ if (!existsSync11(p)) return createDefaultState();
3584
4001
  const raw = JSON.parse(safeReadSync(p));
3585
4002
  const result = PipelineStateSchema.safeParse(raw);
3586
4003
  if (!result.success) return createDefaultState();
3587
4004
  return result.data;
3588
4005
  }
3589
4006
  function safeWriteSync(filePath, content) {
3590
- if (existsSync7(filePath)) {
4007
+ if (existsSync11(filePath)) {
3591
4008
  const stat = lstatSync6(filePath);
3592
4009
  if (stat.isSymbolicLink()) {
3593
4010
  throw new Error(`Refusing to write to symlink: ${filePath}`);
@@ -3596,25 +4013,25 @@ function safeWriteSync(filePath, content) {
3596
4013
  writeFileSync4(filePath, content);
3597
4014
  }
3598
4015
  function safeReadSync(filePath) {
3599
- if (existsSync7(filePath)) {
4016
+ if (existsSync11(filePath)) {
3600
4017
  const stat = lstatSync6(filePath);
3601
4018
  if (stat.isSymbolicLink()) {
3602
4019
  throw new Error(`Refusing to read symlink: ${filePath}`);
3603
4020
  }
3604
4021
  }
3605
- return readFileSync7(filePath, "utf-8");
4022
+ return readFileSync11(filePath, "utf-8");
3606
4023
  }
3607
4024
  function writeState(cwd, state) {
3608
- const dir = join7(cwd, ".stackwright");
4025
+ const dir = join11(cwd, ".stackwright");
3609
4026
  mkdirSync4(dir, { recursive: true });
3610
4027
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
3611
4028
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
3612
4029
  }
3613
4030
  function updateStateAtomic(cwd, mutator) {
3614
- const dir = join7(cwd, ".stackwright");
4031
+ const dir = join11(cwd, ".stackwright");
3615
4032
  mkdirSync4(dir, { recursive: true });
3616
4033
  const path5 = statePath(cwd);
3617
- if (!existsSync7(path5)) {
4034
+ if (!existsSync11(path5)) {
3618
4035
  safeWriteSync(path5, JSON.stringify(createDefaultState(), null, 2) + "\n");
3619
4036
  }
3620
4037
  const release = lockSync(path5, {
@@ -3648,8 +4065,8 @@ function handleGetPipelineState(_cwd) {
3648
4065
  const cwd = _cwd ?? process.cwd();
3649
4066
  try {
3650
4067
  const state = readState(cwd);
3651
- const keyPath = join7(cwd, ".stackwright", "pipeline-keys.json");
3652
- if (!existsSync7(keyPath)) {
4068
+ const keyPath = join11(cwd, ".stackwright", "pipeline-keys.json");
4069
+ if (!existsSync11(keyPath)) {
3653
4070
  try {
3654
4071
  const { fingerprint } = initPipelineKeys(cwd);
3655
4072
  state["signingKeyFingerprint"] = fingerprint;
@@ -3788,8 +4205,8 @@ function handleCheckExecutionReady(_cwd, phase) {
3788
4205
  if (AUTO_EXECUTE_PHASES.has(phase)) {
3789
4206
  return { text: JSON.stringify({ ready: true, phase, autoExecute: true }), isError: false };
3790
4207
  }
3791
- const answerFile = join7(cwd, ".stackwright", "answers", `${phase}.json`);
3792
- if (!existsSync7(answerFile)) {
4208
+ const answerFile = join11(cwd, ".stackwright", "answers", `${phase}.json`);
4209
+ if (!existsSync11(answerFile)) {
3793
4210
  return {
3794
4211
  text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
3795
4212
  isError: false
@@ -3813,7 +4230,7 @@ function handleCheckExecutionReady(_cwd, phase) {
3813
4230
  }
3814
4231
  }
3815
4232
  try {
3816
- const answersDir = join7(cwd, ".stackwright", "answers");
4233
+ const answersDir = join11(cwd, ".stackwright", "answers");
3817
4234
  const answeredPhases = [];
3818
4235
  const missingPhases = [];
3819
4236
  for (const phase2 of PHASE_ORDER) {
@@ -3821,8 +4238,8 @@ function handleCheckExecutionReady(_cwd, phase) {
3821
4238
  answeredPhases.push(phase2);
3822
4239
  continue;
3823
4240
  }
3824
- const answerFile = join7(answersDir, `${phase2}.json`);
3825
- if (existsSync7(answerFile)) {
4241
+ const answerFile = join11(answersDir, `${phase2}.json`);
4242
+ if (existsSync11(answerFile)) {
3826
4243
  try {
3827
4244
  const raw = safeReadSync(answerFile);
3828
4245
  const parsed = JSON.parse(raw);
@@ -3891,7 +4308,7 @@ function handleGetReadyPhases(_cwd) {
3891
4308
  function handleListArtifacts(_cwd) {
3892
4309
  const cwd = _cwd ?? process.cwd();
3893
4310
  try {
3894
- const artifactsDir = join7(cwd, ".stackwright", "artifacts");
4311
+ const artifactsDir = join11(cwd, ".stackwright", "artifacts");
3895
4312
  let manifest = null;
3896
4313
  try {
3897
4314
  manifest = loadSignatureManifest(cwd);
@@ -3901,8 +4318,8 @@ function handleListArtifacts(_cwd) {
3901
4318
  let completedCount = 0;
3902
4319
  for (const phase of PHASE_ORDER) {
3903
4320
  const expectedFile = PHASE_ARTIFACT[phase];
3904
- const fullPath = join7(artifactsDir, expectedFile);
3905
- const exists = existsSync7(fullPath);
4321
+ const fullPath = join11(artifactsDir, expectedFile);
4322
+ const exists = existsSync11(fullPath);
3906
4323
  if (exists) completedCount++;
3907
4324
  let signed = false;
3908
4325
  let signatureValid = null;
@@ -3955,9 +4372,9 @@ function handleWritePhaseQuestions(input) {
3955
4372
  }
3956
4373
  } catch {
3957
4374
  }
3958
- const questionsDir = join7(cwd, ".stackwright", "questions");
4375
+ const questionsDir = join11(cwd, ".stackwright", "questions");
3959
4376
  mkdirSync4(questionsDir, { recursive: true });
3960
- const filePath = join7(questionsDir, `${phase}.json`);
4377
+ const filePath = join11(questionsDir, `${phase}.json`);
3961
4378
  const payload = {
3962
4379
  version: "1.0",
3963
4380
  phase,
@@ -3997,14 +4414,14 @@ function handleBuildSpecialistPrompt(input) {
3997
4414
  };
3998
4415
  }
3999
4416
  try {
4000
- const answersPath = join7(cwd, ".stackwright", "answers", `${phase}.json`);
4417
+ const answersPath = join11(cwd, ".stackwright", "answers", `${phase}.json`);
4001
4418
  let answers = {};
4002
- if (existsSync7(answersPath)) {
4419
+ if (existsSync11(answersPath)) {
4003
4420
  answers = JSON.parse(safeReadSync(answersPath));
4004
4421
  }
4005
4422
  let buildContextText = "";
4006
- const buildContextPath = join7(cwd, ".stackwright", "build-context.json");
4007
- if (existsSync7(buildContextPath)) {
4423
+ const buildContextPath = join11(cwd, ".stackwright", "build-context.json");
4424
+ if (existsSync11(buildContextPath)) {
4008
4425
  try {
4009
4426
  const bcRaw = JSON.parse(safeReadSync(buildContextPath));
4010
4427
  if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
@@ -4019,8 +4436,8 @@ function handleBuildSpecialistPrompt(input) {
4019
4436
  const missingDependencies = [];
4020
4437
  for (const dep of deps) {
4021
4438
  const artifactFile = PHASE_ARTIFACT[dep];
4022
- const artifactPath = join7(cwd, ".stackwright", "artifacts", artifactFile);
4023
- if (existsSync7(artifactPath)) {
4439
+ const artifactPath = join11(cwd, ".stackwright", "artifacts", artifactFile);
4440
+ if (existsSync11(artifactPath)) {
4024
4441
  const rawContent = safeReadSync(artifactPath);
4025
4442
  const rawBytes = Buffer.from(rawContent, "utf-8");
4026
4443
  const content = JSON.parse(rawContent);
@@ -4085,8 +4502,8 @@ ${JSON.stringify(content, null, 2)}`
4085
4502
  parts.push("BUILD_CONTEXT:", buildContextText, "");
4086
4503
  }
4087
4504
  if (phase === "auth") {
4088
- const initContextPath = join7(cwd, ".stackwright", "init-context.json");
4089
- if (existsSync7(initContextPath)) {
4505
+ const initContextPath = join11(cwd, ".stackwright", "init-context.json");
4506
+ if (existsSync11(initContextPath)) {
4090
4507
  try {
4091
4508
  const initContext = JSON.parse(safeReadSync(initContextPath));
4092
4509
  if (initContext.devOnly === true || initContext.nonInteractive === true) {
@@ -4227,7 +4644,12 @@ var PHASE_ARTIFACT_SCHEMA = {
4227
4644
  "--surface": "210 40% 98%",
4228
4645
  "--border": "214.3 31.8% 91.4%"
4229
4646
  },
4230
- dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
4647
+ dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" },
4648
+ // Optional — include when design language has status semantics (swp-cxwu).
4649
+ // Array of status-* token names emitted in tokens.colors.
4650
+ // Consumed by validate_artifact theme enforcement to reject unknown status tokens.
4651
+ // OMIT if project has no status-coded data.
4652
+ statusLadder: ["status-ok", "status-warning", "status-error", "status-info"]
4231
4653
  },
4232
4654
  null,
4233
4655
  2
@@ -4665,7 +5087,7 @@ function _validateArtifactInner(input) {
4665
5087
  auth: (artifact2) => {
4666
5088
  const authConfig = artifact2["authConfig"];
4667
5089
  if (!authConfig) return { success: true };
4668
- const result = authConfigSchema3.safeParse(authConfig);
5090
+ const result = authConfigSchema2.safeParse(authConfig);
4669
5091
  if (!result.success) {
4670
5092
  const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
4671
5093
  return { success: false, error: { message: issues } };
@@ -4697,21 +5119,21 @@ function _validateArtifactInner(input) {
4697
5119
  // state-machine in this services-config artifact.
4698
5120
  // Skips gracefully when wizard-otter didn't run (Pro-only install) or declared no hooks.
4699
5121
  services: (artifact2) => {
4700
- const artifactsDir = join7(cwd, ".stackwright", "artifacts");
5122
+ const artifactsDir = join11(cwd, ".stackwright", "artifacts");
4701
5123
  const workflowArtifactFiles = [];
4702
- if (existsSync7(artifactsDir)) {
5124
+ if (existsSync11(artifactsDir)) {
4703
5125
  try {
4704
- const entries = readdirSync5(artifactsDir);
5126
+ const entries = readdirSync8(artifactsDir);
4705
5127
  for (const entry of entries) {
4706
5128
  if (entry.startsWith("workflow-config-") && entry.endsWith(".json")) {
4707
- workflowArtifactFiles.push(join7(artifactsDir, entry));
5129
+ workflowArtifactFiles.push(join11(artifactsDir, entry));
4708
5130
  }
4709
5131
  }
4710
5132
  } catch {
4711
5133
  }
4712
5134
  }
4713
- const legacyPath = join7(artifactsDir, "workflow-config.json");
4714
- if (existsSync7(legacyPath)) {
5135
+ const legacyPath = join11(artifactsDir, "workflow-config.json");
5136
+ if (existsSync11(legacyPath)) {
4715
5137
  workflowArtifactFiles.push(legacyPath);
4716
5138
  }
4717
5139
  if (workflowArtifactFiles.length === 0) {
@@ -4720,7 +5142,7 @@ function _validateArtifactInner(input) {
4720
5142
  const declaredHooks = [];
4721
5143
  for (const filePath of workflowArtifactFiles) {
4722
5144
  try {
4723
- const parsed = JSON.parse(readFileSync7(filePath, "utf-8"));
5145
+ const parsed = JSON.parse(readFileSync11(filePath, "utf-8"));
4724
5146
  if (Array.isArray(parsed.serviceHooks)) {
4725
5147
  declaredHooks.push(...parsed.serviceHooks);
4726
5148
  }
@@ -4810,6 +5232,42 @@ function _validateArtifactInner(input) {
4810
5232
  return { text: JSON.stringify(result), isError: false };
4811
5233
  }
4812
5234
  }
5235
+ if (phase === "pages" || phase === "dashboard" || phase === "geo") {
5236
+ const themeResult = validateThemeTokenUsage(cwd);
5237
+ if (!themeResult.valid && !themeResult.skipped) {
5238
+ const result = {
5239
+ valid: false,
5240
+ phase,
5241
+ violation: "schema-mismatch",
5242
+ retryPrompt: themeResult.retryPrompt
5243
+ };
5244
+ return { text: JSON.stringify(result), isError: false };
5245
+ }
5246
+ }
5247
+ if (phase === "pages" || phase === "dashboard" || phase === "geo") {
5248
+ const globalsResult = validateBuildContextGlobals(cwd);
5249
+ if (!globalsResult.valid && !globalsResult.skipped) {
5250
+ const result = {
5251
+ valid: false,
5252
+ phase,
5253
+ violation: "schema-mismatch",
5254
+ retryPrompt: globalsResult.retryPrompt
5255
+ };
5256
+ return { text: JSON.stringify(result), isError: false };
5257
+ }
5258
+ }
5259
+ if (phase === "pages") {
5260
+ const workflowCrossCheck = validateWorkflowContainerPages(cwd);
5261
+ if (!workflowCrossCheck.valid) {
5262
+ const result = {
5263
+ valid: false,
5264
+ phase,
5265
+ violation: "schema-mismatch",
5266
+ retryPrompt: workflowCrossCheck.retryPrompt
5267
+ };
5268
+ return { text: JSON.stringify(result), isError: false };
5269
+ }
5270
+ }
4813
5271
  if (phase === "api") {
4814
5272
  const apiAuthResult = validateApiIntegration(artifact, input.integrationName);
4815
5273
  if (!apiAuthResult.valid) {
@@ -4838,7 +5296,7 @@ function _validateArtifactInner(input) {
4838
5296
  const authCfg = artifact["authConfig"];
4839
5297
  const routes = authCfg?.["protectedRoutes"];
4840
5298
  if (Array.isArray(routes) && routes.length > 0) {
4841
- const artifactsDir = join7(cwd, ".stackwright", "artifacts");
5299
+ const artifactsDir = join11(cwd, ".stackwright", "artifacts");
4842
5300
  const knownSlugs = extractManifestSlugs(artifactsDir);
4843
5301
  const orphanPatterns = [];
4844
5302
  for (const route of routes) {
@@ -4855,10 +5313,10 @@ function _validateArtifactInner(input) {
4855
5313
  }
4856
5314
  }
4857
5315
  try {
4858
- const artifactsDir = join7(cwd, ".stackwright", "artifacts");
5316
+ const artifactsDir = join11(cwd, ".stackwright", "artifacts");
4859
5317
  mkdirSync4(artifactsDir, { recursive: true });
4860
5318
  const artifactFile = phase === "api" && input.integrationName ? `api-config-${input.integrationName}.json` : phase === "workflow" && input.workflowName ? `workflow-config-${input.workflowName}.json` : PHASE_ARTIFACT[phase];
4861
- const artifactPath = join7(artifactsDir, artifactFile);
5319
+ const artifactPath = join11(artifactsDir, artifactFile);
4862
5320
  const serialized = JSON.stringify(artifact, null, 2) + "\n";
4863
5321
  const artifactBytes = Buffer.from(serialized, "utf-8");
4864
5322
  safeWriteSync(artifactPath, serialized);
@@ -5088,9 +5546,9 @@ function registerPipelineTools(server2) {
5088
5546
  isError: true
5089
5547
  };
5090
5548
  }
5091
- const questionsDir = join7(process.cwd(), ".stackwright", "questions");
5549
+ const questionsDir = join11(process.cwd(), ".stackwright", "questions");
5092
5550
  mkdirSync4(questionsDir, { recursive: true });
5093
- const outPath = join7(questionsDir, `${phase}.json`);
5551
+ const outPath = join11(questionsDir, `${phase}.json`);
5094
5552
  writeFileSync4(
5095
5553
  outPath,
5096
5554
  JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
@@ -5183,8 +5641,8 @@ function registerPipelineTools(server2) {
5183
5641
 
5184
5642
  // src/tools/safe-write.ts
5185
5643
  import { z as z15 } from "zod";
5186
- import { writeFileSync as writeFileSync5, readFileSync as readFileSync8, existsSync as existsSync8, mkdirSync as mkdirSync5, lstatSync as lstatSync7 } from "fs";
5187
- import { normalize, isAbsolute, dirname, join as join8 } from "path";
5644
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync12, existsSync as existsSync12, mkdirSync as mkdirSync5, lstatSync as lstatSync7 } from "fs";
5645
+ import { normalize, isAbsolute, dirname, join as join12 } from "path";
5188
5646
  import { emit as emit2 } from "@stackwright-pro/telemetry";
5189
5647
  var OTTER_WRITE_ALLOWLISTS = {
5190
5648
  "stackwright-pro-designer-otter": [
@@ -5261,7 +5719,18 @@ var OTTER_WRITE_ALLOWLISTS = {
5261
5719
  "stackwright-pro-form-wizard-otter": [
5262
5720
  { prefix: "workflows/", suffix: ".yml", description: "Workflow definition" },
5263
5721
  { prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
5264
- { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
5722
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" },
5723
+ // swp-dvde: workflow-otter owns the container page to prevent the 404 gap
5724
+ {
5725
+ prefix: "pages/workflows/",
5726
+ suffix: "/content.yml",
5727
+ description: "Workflow container page content (swp-dvde)"
5728
+ },
5729
+ {
5730
+ prefix: "pages/workflows/",
5731
+ suffix: "/content.yaml",
5732
+ description: "Workflow container page content (swp-dvde)"
5733
+ }
5265
5734
  ],
5266
5735
  "stackwright-pro-scaffold-otter": [
5267
5736
  { prefix: "app/", suffix: ".tsx", description: "Next.js App Router shell files" },
@@ -5396,21 +5865,21 @@ function extractContentTypes(yamlContent) {
5396
5865
  return types.length > 0 ? types : ["unknown"];
5397
5866
  }
5398
5867
  function readPageRegistry(cwd) {
5399
- const regPath = join8(cwd, PAGE_REGISTRY_FILE);
5400
- if (!existsSync8(regPath)) return {};
5868
+ const regPath = join12(cwd, PAGE_REGISTRY_FILE);
5869
+ if (!existsSync12(regPath)) return {};
5401
5870
  try {
5402
5871
  const stat = lstatSync7(regPath);
5403
5872
  if (stat.isSymbolicLink()) return {};
5404
- return JSON.parse(readFileSync8(regPath, "utf-8"));
5873
+ return JSON.parse(readFileSync12(regPath, "utf-8"));
5405
5874
  } catch {
5406
5875
  return {};
5407
5876
  }
5408
5877
  }
5409
5878
  function writePageRegistry(cwd, registry) {
5410
- const dir = join8(cwd, ".stackwright");
5879
+ const dir = join12(cwd, ".stackwright");
5411
5880
  mkdirSync5(dir, { recursive: true });
5412
- const regPath = join8(cwd, PAGE_REGISTRY_FILE);
5413
- if (existsSync8(regPath)) {
5881
+ const regPath = join12(cwd, PAGE_REGISTRY_FILE);
5882
+ if (existsSync12(regPath)) {
5414
5883
  const stat = lstatSync7(regPath);
5415
5884
  if (stat.isSymbolicLink()) {
5416
5885
  throw new Error("Refusing to write page registry through symlink");
@@ -5582,8 +6051,8 @@ function _safeWriteInner(input) {
5582
6051
  return { text: JSON.stringify(result), isError: true };
5583
6052
  }
5584
6053
  const normalized = normalize(filePath);
5585
- const fullPath = join8(cwd, normalized);
5586
- if (existsSync8(fullPath)) {
6054
+ const fullPath = join12(cwd, normalized);
6055
+ if (existsSync12(fullPath)) {
5587
6056
  try {
5588
6057
  const stat = lstatSync7(fullPath);
5589
6058
  if (stat.isSymbolicLink()) {
@@ -5713,8 +6182,8 @@ function registerSafeWriteTools(server2) {
5713
6182
 
5714
6183
  // src/tools/auth.ts
5715
6184
  import { z as z16 } from "zod";
5716
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync9, mkdirSync as mkdirSync6 } from "fs";
5717
- import { join as join9 } from "path";
6185
+ import { readFileSync as readFileSync13, writeFileSync as writeFileSync6, existsSync as existsSync13, mkdirSync as mkdirSync6 } from "fs";
6186
+ import { join as join13 } from "path";
5718
6187
  function normalizeProviderSlug(slug) {
5719
6188
  return slug.replace(/-/g, "_");
5720
6189
  }
@@ -5744,9 +6213,9 @@ ${indent} requiredRole: ${r.requiredRole}`).join("\n");
5744
6213
  }
5745
6214
  function detectNextMajorVersion(cwd) {
5746
6215
  try {
5747
- const pkgPath = join9(cwd, "package.json");
5748
- if (!existsSync9(pkgPath)) return null;
5749
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
6216
+ const pkgPath = join13(cwd, "package.json");
6217
+ if (!existsSync13(pkgPath)) return null;
6218
+ const pkg = JSON.parse(readFileSync13(pkgPath, "utf8"));
5750
6219
  const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
5751
6220
  if (!nextVersion) return null;
5752
6221
  const match = nextVersion.match(/(\d+)/);
@@ -6408,7 +6877,7 @@ async function configureAuthHandler(params, cwd) {
6408
6877
  auditRetentionDays,
6409
6878
  normalizedRoutes
6410
6879
  );
6411
- writeFileSync6(join9(cwd, conventionFile), authFileContent, "utf8");
6880
+ writeFileSync6(join13(cwd, conventionFile), authFileContent, "utf8");
6412
6881
  filesWritten.push(conventionFile);
6413
6882
  } catch (err) {
6414
6883
  const msg = err instanceof Error ? err.message : String(err);
@@ -6428,9 +6897,9 @@ async function configureAuthHandler(params, cwd) {
6428
6897
  if (!devOnly) {
6429
6898
  try {
6430
6899
  const envBlock = generateEnvBlock(effectiveMethod, params);
6431
- const envPath = join9(cwd, ".env.example");
6432
- if (existsSync9(envPath)) {
6433
- const existing = readFileSync9(envPath, "utf8");
6900
+ const envPath = join13(cwd, ".env.example");
6901
+ if (existsSync13(envPath)) {
6902
+ const existing = readFileSync13(envPath, "utf8");
6434
6903
  writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
6435
6904
  } else {
6436
6905
  writeFileSync6(envPath, envBlock, "utf8");
@@ -6451,10 +6920,10 @@ async function configureAuthHandler(params, cwd) {
6451
6920
  }
6452
6921
  if (devOnly) {
6453
6922
  try {
6454
- const mockAuthDir = join9(cwd, "lib");
6923
+ const mockAuthDir = join13(cwd, "lib");
6455
6924
  mkdirSync6(mockAuthDir, { recursive: true });
6456
6925
  const mockAuthContent = generateMockAuthContent(roles, mockUsers);
6457
- writeFileSync6(join9(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
6926
+ writeFileSync6(join13(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
6458
6927
  filesWritten.push("lib/mock-auth.ts");
6459
6928
  } catch (err) {
6460
6929
  const msg = err instanceof Error ? err.message : String(err);
@@ -6472,9 +6941,9 @@ async function configureAuthHandler(params, cwd) {
6472
6941
  };
6473
6942
  }
6474
6943
  try {
6475
- const pkgPath = join9(cwd, "package.json");
6476
- if (existsSync9(pkgPath)) {
6477
- const existingPkg = readFileSync9(pkgPath, "utf8");
6944
+ const pkgPath = join13(cwd, "package.json");
6945
+ if (existsSync13(pkgPath)) {
6946
+ const existingPkg = readFileSync13(pkgPath, "utf8");
6478
6947
  const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
6479
6948
  writeFileSync6(pkgPath, updatedPkg, "utf8");
6480
6949
  filesWritten.push("package.json");
@@ -6517,7 +6986,7 @@ async function configureAuthHandler(params, cwd) {
6517
6986
  normalizedRoutes,
6518
6987
  useProxy
6519
6988
  );
6520
- writeFileSync6(join9(cwd, "stackwright.auth.yml"), authYaml, "utf8");
6989
+ writeFileSync6(join13(cwd, "stackwright.auth.yml"), authYaml, "utf8");
6521
6990
  filesWritten.push("stackwright.auth.yml");
6522
6991
  } catch (err) {
6523
6992
  const msg = err instanceof Error ? err.message : String(err);
@@ -6631,8 +7100,8 @@ function registerAuthTools(server2) {
6631
7100
 
6632
7101
  // src/integrity.ts
6633
7102
  import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
6634
- import { readFileSync as readFileSync10, readdirSync as readdirSync6, lstatSync as lstatSync8 } from "fs";
6635
- import { join as join10, basename } from "path";
7103
+ import { readFileSync as readFileSync14, readdirSync as readdirSync9, lstatSync as lstatSync8 } from "fs";
7104
+ import { join as join14, basename } from "path";
6636
7105
  var _checksums = /* @__PURE__ */ new Map([
6637
7106
  [
6638
7107
  "stackwright-pro-api-otter.json",
@@ -6648,7 +7117,7 @@ var _checksums = /* @__PURE__ */ new Map([
6648
7117
  ],
6649
7118
  [
6650
7119
  "stackwright-pro-dashboard-otter.json",
6651
- "f55081bf4d6545e5435128919f6f9233956ba37671fc9f85300f240ad2497ab6"
7120
+ "ac0a049da72ee20bfb711f640bac571263bf2b05f80cd22a0c2eb5778c40989c"
6652
7121
  ],
6653
7122
  [
6654
7123
  "stackwright-pro-data-otter.json",
@@ -6668,15 +7137,15 @@ var _checksums = /* @__PURE__ */ new Map([
6668
7137
  ],
6669
7138
  [
6670
7139
  "stackwright-pro-form-wizard-otter.json",
6671
- "cb350297a0068ead2424e703e8c775d91c0b87249ea6904f43b903baf8a84b79"
7140
+ "941bb11b767530460f96fdd08af638b3ede9f7a1ae6f19de04b7d81c95cc62ad"
6672
7141
  ],
6673
7142
  [
6674
7143
  "stackwright-pro-geo-otter.json",
6675
- "3032fbd27de36c0cdc0e19e46a32d771208d8e86714482f5a368d46342c8317a"
7144
+ "bee276605a3ae3203f5b6a18476316f615cf0ea6f4c73ecf57dcd627417de7f3"
6676
7145
  ],
6677
7146
  [
6678
7147
  "stackwright-pro-page-otter.json",
6679
- "cc0db24b00f0130f8b4ec8d385c73ea1bcf5d57ba45aca6b986bcbd5da328930"
7148
+ "8c04dd8a14c6e1aa9b0b6685a71a657547c20d99c25448e3523f73d670f87a4a"
6680
7149
  ],
6681
7150
  [
6682
7151
  "stackwright-pro-polish-otter.json",
@@ -6692,7 +7161,7 @@ var _checksums = /* @__PURE__ */ new Map([
6692
7161
  ],
6693
7162
  [
6694
7163
  "stackwright-pro-theme-otter.json",
6695
- "afb47f8381907145c0e098bdcaae4dd9f5c4ff825a8e88d00a218d2f6858820b"
7164
+ "e3cb6ef9d9f7cbf662a26f637f3d244ec33cb19cb6a4a55dcd148cbfdaa4d71e"
6696
7165
  ],
6697
7166
  [
6698
7167
  "stackwright-services-otter.json",
@@ -6744,7 +7213,7 @@ function verifyOtterFile(filePath) {
6744
7213
  }
6745
7214
  let raw;
6746
7215
  try {
6747
- raw = readFileSync10(filePath);
7216
+ raw = readFileSync14(filePath);
6748
7217
  } catch (err) {
6749
7218
  const msg = err instanceof Error ? err.message : String(err);
6750
7219
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -6794,7 +7263,7 @@ function verifyAllOtters(otterDir) {
6794
7263
  const unknown = [];
6795
7264
  let entries;
6796
7265
  try {
6797
- entries = readdirSync6(otterDir);
7266
+ entries = readdirSync9(otterDir);
6798
7267
  } catch (err) {
6799
7268
  const msg = err instanceof Error ? err.message : String(err);
6800
7269
  return {
@@ -6805,7 +7274,7 @@ function verifyAllOtters(otterDir) {
6805
7274
  }
6806
7275
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
6807
7276
  for (const filename of otterFiles) {
6808
- const filePath = join10(otterDir, filename);
7277
+ const filePath = join14(otterDir, filename);
6809
7278
  try {
6810
7279
  if (lstatSync8(filePath).isSymbolicLink()) {
6811
7280
  failed.push({ filename, error: "Skipped: symlink" });
@@ -6832,8 +7301,8 @@ function verifyAllOtters(otterDir) {
6832
7301
  var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packages/otters/src/"];
6833
7302
  function resolveOtterDir() {
6834
7303
  const cwd = process.cwd();
6835
- for (const relative2 of DEFAULT_SEARCH_PATHS) {
6836
- const candidate = join10(cwd, relative2);
7304
+ for (const relative4 of DEFAULT_SEARCH_PATHS) {
7305
+ const candidate = join14(cwd, relative4);
6837
7306
  try {
6838
7307
  lstatSync8(candidate);
6839
7308
  return candidate;
@@ -6914,13 +7383,13 @@ function registerIntegrityTools(server2) {
6914
7383
 
6915
7384
  // src/tools/domain.ts
6916
7385
  import { z as z17 } from "zod";
6917
- import { readFileSync as readFileSync11, existsSync as existsSync10 } from "fs";
6918
- import { join as join11 } from "path";
7386
+ import { readFileSync as readFileSync15, existsSync as existsSync14 } from "fs";
7387
+ import { join as join15 } from "path";
6919
7388
  function handleListCollections(input) {
6920
7389
  const cwd = input._cwd ?? process.cwd();
6921
7390
  const sources = [
6922
7391
  {
6923
- path: join11(cwd, ".stackwright", "artifacts", "data-config.json"),
7392
+ path: join15(cwd, ".stackwright", "artifacts", "data-config.json"),
6924
7393
  source: "data-config.json",
6925
7394
  parse: (raw) => {
6926
7395
  const parsed = JSON.parse(raw);
@@ -6931,15 +7400,15 @@ function handleListCollections(input) {
6931
7400
  }
6932
7401
  },
6933
7402
  {
6934
- path: join11(cwd, "stackwright.yml"),
7403
+ path: join15(cwd, "stackwright.yml"),
6935
7404
  source: "stackwright.yml",
6936
7405
  parse: extractCollectionsFromYaml
6937
7406
  }
6938
7407
  ];
6939
7408
  for (const { path: path5, source, parse } of sources) {
6940
- if (!existsSync10(path5)) continue;
7409
+ if (!existsSync14(path5)) continue;
6941
7410
  try {
6942
- const collections = parse(readFileSync11(path5, "utf8"));
7411
+ const collections = parse(readFileSync15(path5, "utf8"));
6943
7412
  return {
6944
7413
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
6945
7414
  isError: false
@@ -7090,8 +7559,8 @@ function handleValidateWorkflow(input) {
7090
7559
  if (input.workflow && Object.keys(input.workflow).length > 0) {
7091
7560
  raw = input.workflow;
7092
7561
  } else {
7093
- const artifactPath = join11(cwd, ".stackwright", "artifacts", "workflow-config.json");
7094
- if (!existsSync10(artifactPath)) {
7562
+ const artifactPath = join15(cwd, ".stackwright", "artifacts", "workflow-config.json");
7563
+ if (!existsSync14(artifactPath)) {
7095
7564
  return fail([
7096
7565
  {
7097
7566
  code: "NO_WORKFLOW",
@@ -7100,7 +7569,7 @@ function handleValidateWorkflow(input) {
7100
7569
  ]);
7101
7570
  }
7102
7571
  try {
7103
- raw = JSON.parse(readFileSync11(artifactPath, "utf8"));
7572
+ raw = JSON.parse(readFileSync15(artifactPath, "utf8"));
7104
7573
  } catch (err) {
7105
7574
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
7106
7575
  }
@@ -7407,12 +7876,92 @@ function registerTypeSchemasTool(server2) {
7407
7876
  );
7408
7877
  }
7409
7878
 
7879
+ // src/tools/validate-yaml-fragment.ts
7880
+ import { z as z19 } from "zod";
7881
+ import { load as yamlLoad5 } from "js-yaml";
7882
+ var FIELD_SYNONYMS = Object.fromEntries(
7883
+ Array.from(SCHEMA_REGISTRY.entries()).map(([name, entry]) => [
7884
+ name,
7885
+ buildReverseSynonyms(entry.synonyms ?? {})
7886
+ ])
7887
+ );
7888
+ function enrichErrors(issues, reverseSynonyms) {
7889
+ return issues.map((issue) => {
7890
+ const pathStr = issue.path.join(".");
7891
+ const lastSegment = issue.path[issue.path.length - 1];
7892
+ const fieldName = typeof lastSegment === "string" ? lastSegment : void 0;
7893
+ const didYouMean = fieldName ? reverseSynonyms[fieldName] : void 0;
7894
+ return {
7895
+ path: pathStr || "(root)",
7896
+ message: issue.message,
7897
+ ...didYouMean ? { didYouMean } : {}
7898
+ };
7899
+ });
7900
+ }
7901
+ function handleValidateYamlFragment(input) {
7902
+ const { schemaName, yaml: yaml4 } = input;
7903
+ const entry = SCHEMA_REGISTRY.get(schemaName);
7904
+ if (!entry) {
7905
+ return {
7906
+ valid: false,
7907
+ parseError: `Unknown schemaName: "${schemaName}". Supported: ${REGISTRY_SCHEMA_NAMES.join(", ")}`
7908
+ };
7909
+ }
7910
+ let parsed;
7911
+ try {
7912
+ parsed = yamlLoad5(yaml4);
7913
+ } catch (err) {
7914
+ return {
7915
+ valid: false,
7916
+ parseError: `YAML parse error: ${err instanceof Error ? err.message : String(err)}`
7917
+ };
7918
+ }
7919
+ const ctx = input.projectRoot ? { projectRoot: input.projectRoot } : void 0;
7920
+ const resolvedSchema = getSchemaForEntry(entry, ctx);
7921
+ const result = resolvedSchema.safeParse(parsed);
7922
+ if (result.success) {
7923
+ const projectRoot = input.projectRoot ?? process.cwd();
7924
+ const ladder = resolveStatusLadder(projectRoot);
7925
+ if (ladder.length > 0) {
7926
+ const ladderSet = new Set(ladder);
7927
+ const tokenViolations = detectViolationsInYaml(parsed, ladder, ladderSet, "fragment");
7928
+ if (tokenViolations.length > 0) {
7929
+ const tokenErrors = tokenViolations.map((v) => ({
7930
+ path: v.path,
7931
+ message: v.message
7932
+ }));
7933
+ return { valid: false, errors: tokenErrors };
7934
+ }
7935
+ }
7936
+ return { valid: true };
7937
+ }
7938
+ const reverseSynonyms = buildReverseSynonyms(entry.synonyms ?? {});
7939
+ const errors = enrichErrors(result.error.issues, reverseSynonyms);
7940
+ return { valid: false, errors };
7941
+ }
7942
+ function registerValidateYamlFragmentTool(server2) {
7943
+ server2.tool(
7944
+ "stackwright_pro_validate_yaml_fragment",
7945
+ 'Validates a YAML snippet against a canonical Zod schema before writing to disk. Returns actionable errors with "did you mean?" suggestions for common field-name drift (title vs label, id vs name, style vs theme.variant, etc.). Call this before stackwright_pro_safe_write to catch schema violations in-session.',
7946
+ {
7947
+ schemaName: z19.string().describe(`Schema to validate against. Available: ${REGISTRY_SCHEMA_NAMES.join(", ")}`),
7948
+ yaml: z19.string().describe("YAML string to validate (can also be JSON \u2014 js-yaml parses both)")
7949
+ },
7950
+ async ({ schemaName, yaml: yaml4 }) => {
7951
+ const result = handleValidateYamlFragment({ schemaName, yaml: yaml4 });
7952
+ return {
7953
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
7954
+ };
7955
+ }
7956
+ );
7957
+ }
7958
+
7410
7959
  // src/tools/pages.ts
7411
7960
  import * as fs2 from "fs";
7412
7961
  import * as os from "os";
7413
7962
  import * as path3 from "path";
7414
- import { z as z19 } from "zod";
7415
- import { load as yamlLoad3, dump as yamlDump } from "js-yaml";
7963
+ import { z as z20 } from "zod";
7964
+ import { load as yamlLoad6, dump as yamlDump } from "js-yaml";
7416
7965
  import { writePage, resolvePagesDir } from "@stackwright/cli";
7417
7966
  import { isProContentType, isOssTypeWithProExtension } from "@stackwright-pro/types";
7418
7967
  var BCP47_RE = /^[a-z]{2}(-[A-Z]{2})?$/;
@@ -7466,7 +8015,7 @@ function handleProWritePage(input) {
7466
8015
  }
7467
8016
  let parsed;
7468
8017
  try {
7469
- parsed = yamlLoad3(content);
8018
+ parsed = yamlLoad6(content);
7470
8019
  } catch (err) {
7471
8020
  return {
7472
8021
  isError: true,
@@ -7523,10 +8072,10 @@ function registerProPageTools(server2) {
7523
8072
  "stackwright_pro_write_page",
7524
8073
  "Write or update a page's YAML content with Pro content-type awareness. Validates against the Stackwright schema (including Pro types like stats_grid, data_table_pulse, map_pulse, alert_banner). Invalid YAML or schema violations on OSS-type fields are rejected with field-level errors. Use this instead of stackwright_write_page for any page containing Pro content types.",
7525
8074
  {
7526
- projectRoot: z19.string().describe("Absolute path to the Stackwright project root"),
7527
- slug: z19.string().describe('Page slug (e.g. "dashboard/main" or "/dashboard/main")'),
7528
- content: z19.string().describe("Full YAML content for the page (content.yml contents)"),
7529
- locale: z19.string().optional().describe(
8075
+ projectRoot: z20.string().describe("Absolute path to the Stackwright project root"),
8076
+ slug: z20.string().describe('Page slug (e.g. "dashboard/main" or "/dashboard/main")'),
8077
+ content: z20.string().describe("Full YAML content for the page (content.yml contents)"),
8078
+ locale: z20.string().optional().describe(
7530
8079
  'BCP 47 locale tag (e.g. "fr", "de"). If provided, writes content.<locale>.yml instead of content.yml.'
7531
8080
  )
7532
8081
  },
@@ -7538,16 +8087,16 @@ function registerProPageTools(server2) {
7538
8087
 
7539
8088
  // src/tools/scaffold-cleanup.ts
7540
8089
  import {
7541
- existsSync as existsSync12,
8090
+ existsSync as existsSync16,
7542
8091
  lstatSync as lstatSync9,
7543
8092
  unlinkSync as unlinkSync2,
7544
- readFileSync as readFileSync12,
8093
+ readFileSync as readFileSync16,
7545
8094
  writeFileSync as writeFileSync8,
7546
- readdirSync as readdirSync7,
8095
+ readdirSync as readdirSync10,
7547
8096
  rmdirSync,
7548
8097
  mkdirSync as mkdirSync8
7549
8098
  } from "fs";
7550
- import { join as join13, dirname as dirname3 } from "path";
8099
+ import { join as join17, dirname as dirname3 } from "path";
7551
8100
  var SCAFFOLD_FILES_TO_DELETE = [
7552
8101
  "content/posts/getting-started.yaml",
7553
8102
  "content/posts/hello-world.yaml"
@@ -7572,12 +8121,12 @@ var FALLBACK_COLORS = {
7572
8121
  mutedForeground: "#737373"
7573
8122
  };
7574
8123
  function readThemeColors(cwd) {
7575
- const tokenPath = join13(cwd, ".stackwright", "artifacts", "theme-tokens.json");
7576
- if (!existsSync12(tokenPath)) return { ...FALLBACK_COLORS };
8124
+ const tokenPath = join17(cwd, ".stackwright", "artifacts", "theme-tokens.json");
8125
+ if (!existsSync16(tokenPath)) return { ...FALLBACK_COLORS };
7577
8126
  const stat = lstatSync9(tokenPath);
7578
8127
  if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
7579
8128
  try {
7580
- const raw = JSON.parse(readFileSync12(tokenPath, "utf-8"));
8129
+ const raw = JSON.parse(readFileSync16(tokenPath, "utf-8"));
7581
8130
  const css = raw?.cssVariables ?? {};
7582
8131
  const toHsl = (hslValue) => {
7583
8132
  if (!hslValue || typeof hslValue !== "string") return null;
@@ -7637,15 +8186,15 @@ function generateNotFoundPage(colors) {
7637
8186
  `;
7638
8187
  }
7639
8188
  function isSafePath(fullPath) {
7640
- if (!existsSync12(fullPath)) return true;
8189
+ if (!existsSync16(fullPath)) return true;
7641
8190
  const stat = lstatSync9(fullPath);
7642
8191
  return !stat.isSymbolicLink();
7643
8192
  }
7644
8193
  function isDirEmpty(dirPath) {
7645
- if (!existsSync12(dirPath)) return false;
8194
+ if (!existsSync16(dirPath)) return false;
7646
8195
  const stat = lstatSync9(dirPath);
7647
8196
  if (!stat.isDirectory()) return false;
7648
- return readdirSync7(dirPath).length === 0;
8197
+ return readdirSync10(dirPath).length === 0;
7649
8198
  }
7650
8199
  function handleCleanupScaffold(_cwd) {
7651
8200
  const cwd = _cwd ?? process.cwd();
@@ -7658,8 +8207,8 @@ function handleCleanupScaffold(_cwd) {
7658
8207
  cleanupWarnings: []
7659
8208
  };
7660
8209
  for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
7661
- const fullPath = join13(cwd, relPath);
7662
- if (!existsSync12(fullPath)) {
8210
+ const fullPath = join17(cwd, relPath);
8211
+ if (!existsSync16(fullPath)) {
7663
8212
  result.skipped.push(relPath);
7664
8213
  continue;
7665
8214
  }
@@ -7676,13 +8225,13 @@ function handleCleanupScaffold(_cwd) {
7676
8225
  }
7677
8226
  {
7678
8227
  const relPath = "pages/getting-started/content.yml";
7679
- const fullPath = join13(cwd, relPath);
7680
- if (!existsSync12(fullPath)) {
8228
+ const fullPath = join17(cwd, relPath);
8229
+ if (!existsSync16(fullPath)) {
7681
8230
  result.skipped.push(relPath);
7682
8231
  } else if (!isSafePath(fullPath)) {
7683
8232
  result.errors.push(`Refusing to delete symlink: ${relPath}`);
7684
8233
  } else {
7685
- const content = readFileSync12(fullPath, "utf-8");
8234
+ const content = readFileSync16(fullPath, "utf-8");
7686
8235
  const isScaffoldDefault = GETTING_STARTED_SCAFFOLD_FINGERPRINTS.some(
7687
8236
  (marker) => content.includes(marker)
7688
8237
  );
@@ -7702,16 +8251,16 @@ function handleCleanupScaffold(_cwd) {
7702
8251
  }
7703
8252
  {
7704
8253
  const relPath = `content/posts/${POSTS_COLLECTION_FILE}`;
7705
- const fullPath = join13(cwd, relPath);
7706
- const postsDir = join13(cwd, "content/posts");
7707
- if (!existsSync12(fullPath)) {
8254
+ const fullPath = join17(cwd, relPath);
8255
+ const postsDir = join17(cwd, "content/posts");
8256
+ if (!existsSync16(fullPath)) {
7708
8257
  result.skipped.push(relPath);
7709
8258
  } else if (!isSafePath(fullPath)) {
7710
8259
  result.errors.push(`Refusing to delete symlink: ${relPath}`);
7711
- } else if (!existsSync12(postsDir)) {
8260
+ } else if (!existsSync16(postsDir)) {
7712
8261
  result.skipped.push(relPath);
7713
8262
  } else {
7714
- const userPostFiles = readdirSync7(postsDir).filter(
8263
+ const userPostFiles = readdirSync10(postsDir).filter(
7715
8264
  (f) => f !== POSTS_COLLECTION_FILE && POST_FILE_EXTENSIONS.some((ext) => f.endsWith(ext))
7716
8265
  );
7717
8266
  if (userPostFiles.length === 0) {
@@ -7729,8 +8278,8 @@ function handleCleanupScaffold(_cwd) {
7729
8278
  }
7730
8279
  }
7731
8280
  for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
7732
- const fullDir = join13(cwd, relDir);
7733
- if (!existsSync12(fullDir)) continue;
8281
+ const fullDir = join17(cwd, relDir);
8282
+ if (!existsSync16(fullDir)) continue;
7734
8283
  if (!isSafePath(fullDir)) {
7735
8284
  result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
7736
8285
  continue;
@@ -7745,8 +8294,8 @@ function handleCleanupScaffold(_cwd) {
7745
8294
  }
7746
8295
  }
7747
8296
  {
7748
- const fullPath = join13(cwd, NOT_FOUND_PATH);
7749
- if (!existsSync12(fullPath)) {
8297
+ const fullPath = join17(cwd, NOT_FOUND_PATH);
8298
+ if (!existsSync16(fullPath)) {
7750
8299
  result.skipped.push(NOT_FOUND_PATH);
7751
8300
  } else if (!isSafePath(fullPath)) {
7752
8301
  result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
@@ -7784,8 +8333,159 @@ function registerScaffoldCleanupTools(server2) {
7784
8333
  );
7785
8334
  }
7786
8335
 
8336
+ // src/tools/write-workflow-container-page.ts
8337
+ import { z as z21 } from "zod";
8338
+ import { dump as yamlDump2 } from "js-yaml";
8339
+ import { WorkflowWizardContentSchema as WorkflowWizardContentSchema2 } from "@stackwright-pro/types";
8340
+ var WriteWorkflowContainerPageInputSchema = z21.object({
8341
+ /**
8342
+ * Test-only: override process.cwd() for deterministic temp-dir testing.
8343
+ * Never set in production — omit entirely in normal MCP tool calls.
8344
+ */
8345
+ _cwd: z21.string().optional(),
8346
+ /**
8347
+ * Kebab-case workflow ID — must match `workflow.id` in the referenced YAML.
8348
+ * Example: `patient-evacuation-authorization`
8349
+ */
8350
+ workflowId: z21.string().regex(/^[a-z0-9-]+$/, {
8351
+ message: "workflowId must be kebab-case (lowercase alphanumeric + hyphens)"
8352
+ }),
8353
+ /**
8354
+ * Relative path to the workflow YAML file from project root.
8355
+ * Example: `workflows/patient-evacuation-authorization.yml`
8356
+ */
8357
+ workflowFile: z21.string().min(1).regex(/^workflows\/[a-z0-9-]+\.(yml|yaml)$/, {
8358
+ message: "workflowFile must be: workflows/<id>.yml"
8359
+ }),
8360
+ /**
8361
+ * URL slug for the container page route.
8362
+ * Convention: `workflows/<workflowId>` → page at `/workflows/<workflowId>`.
8363
+ * Example: `workflows/patient-evacuation`
8364
+ *
8365
+ * The tool writes to `pages/<pageSlug>/content.yml`.
8366
+ */
8367
+ pageSlug: z21.string().min(1).regex(/^workflows\/[a-z0-9-]+$/, {
8368
+ message: 'pageSlug must start with "workflows/" and be kebab-case. Example: workflows/patient-evacuation'
8369
+ }),
8370
+ /**
8371
+ * Human-readable label for the workflow (used in page title + breadcrumbs).
8372
+ * Example: `Patient Evacuation Authorization`
8373
+ */
8374
+ label: z21.string().min(1).max(120),
8375
+ /**
8376
+ * Union of required roles across all steps in the workflow YAML.
8377
+ * Consumed by auth-reconcile to build protectedRoutes.
8378
+ * Must be non-empty — workflow pages are always auth-protected.
8379
+ */
8380
+ requiredRoles: z21.array(z21.string().min(1)).min(1, {
8381
+ message: "requiredRoles must have at least one role \u2014 workflow pages are always auth-protected"
8382
+ }),
8383
+ /**
8384
+ * Persistence key for workflow state (localStorage slot).
8385
+ * Defaults to `workflow-<workflowId>` when omitted.
8386
+ */
8387
+ persistenceKey: z21.string().optional(),
8388
+ /**
8389
+ * Layout mode for the container page. Defaults to `app-shell` per Pro convention.
8390
+ * Only override when you have a specific reason — `app-shell` is correct for all
8391
+ * workflow container pages in data-dense Pro apps.
8392
+ */
8393
+ layoutMode: z21.enum(["app-shell", "page"]).default("app-shell")
8394
+ });
8395
+ function handleWriteWorkflowContainerPage(input) {
8396
+ const parsed = WriteWorkflowContainerPageInputSchema.safeParse(input);
8397
+ if (!parsed.success) {
8398
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
8399
+ return {
8400
+ text: JSON.stringify({ error: `Invalid input: ${issues}` }),
8401
+ isError: true
8402
+ };
8403
+ }
8404
+ const { workflowId, workflowFile, pageSlug, label, requiredRoles, persistenceKey, layoutMode } = parsed.data;
8405
+ const resolvedPersistenceKey = persistenceKey ?? `workflow-${workflowId}`;
8406
+ const wizardItem = {
8407
+ type: "workflow_wizard",
8408
+ label: workflowId,
8409
+ // slug-form label for the content item (page title comes from meta)
8410
+ workflowFile,
8411
+ workflowId,
8412
+ persistenceKey: resolvedPersistenceKey
8413
+ };
8414
+ const schemaCheck = WorkflowWizardContentSchema2.safeParse(wizardItem);
8415
+ if (!schemaCheck.success) {
8416
+ const issues = schemaCheck.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
8417
+ return {
8418
+ text: JSON.stringify({ error: `Internal schema validation failed: ${issues}` }),
8419
+ isError: true
8420
+ };
8421
+ }
8422
+ const pageContent = {
8423
+ layoutMode,
8424
+ meta: {
8425
+ title: `${label} | Workflow`,
8426
+ description: `${label} \u2014 multi-step workflow. Requires authorization.`,
8427
+ authRequired: true,
8428
+ requiredRoles
8429
+ },
8430
+ content: {
8431
+ content_items: [wizardItem]
8432
+ }
8433
+ };
8434
+ const yamlContent = yamlDump2(pageContent, {
8435
+ indent: 2,
8436
+ lineWidth: 120,
8437
+ quotingType: "'",
8438
+ forceQuotes: false
8439
+ });
8440
+ const filePath = `pages/${pageSlug}/content.yml`;
8441
+ const writeResult = handleSafeWrite({
8442
+ callerOtter: "stackwright-pro-form-wizard-otter",
8443
+ filePath,
8444
+ content: yamlContent,
8445
+ createDirectories: true,
8446
+ _cwd: parsed.data._cwd
8447
+ });
8448
+ if (writeResult.isError) {
8449
+ return writeResult;
8450
+ }
8451
+ const result = {
8452
+ written: true,
8453
+ path: filePath,
8454
+ contentTypesEmitted: ["workflow_wizard"],
8455
+ content: yamlContent
8456
+ };
8457
+ return {
8458
+ text: JSON.stringify(result),
8459
+ isError: false
8460
+ };
8461
+ }
8462
+ function registerWriteWorkflowContainerPageTool(server2) {
8463
+ server2.tool(
8464
+ "stackwright_pro_write_workflow_container_page",
8465
+ [
8466
+ "Create the canonical container page for a workflow at pages/workflows/<workflowId>/content.yml.",
8467
+ "",
8468
+ "ALWAYS call this immediately after writing the workflow YAML via stackwright_pro_safe_write.",
8469
+ "Without it, the route /workflows/<workflowId> 404s at runtime.",
8470
+ "",
8471
+ "The tool deterministically emits:",
8472
+ " - layoutMode: app-shell",
8473
+ " - meta.authRequired: true, meta.requiredRoles: <your input>",
8474
+ " - One workflow_wizard content item binding workflowFile + workflowId",
8475
+ "",
8476
+ "requiredRoles: supply the UNION of required roles from all steps in your workflow YAML.",
8477
+ 'pageSlug: convention is "workflows/<workflowId>" \u2014 must start with "workflows/".'
8478
+ ].join("\n"),
8479
+ WriteWorkflowContainerPageInputSchema.shape,
8480
+ async (args) => {
8481
+ const result = handleWriteWorkflowContainerPage(args);
8482
+ return { content: [{ type: "text", text: result.text }] };
8483
+ }
8484
+ );
8485
+ }
8486
+
7787
8487
  // src/tools/contrast.ts
7788
- import { z as z20 } from "zod";
8488
+ import { z as z22 } from "zod";
7789
8489
  function linearizeChannel(c255) {
7790
8490
  const c = c255 / 255;
7791
8491
  return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
@@ -7957,8 +8657,8 @@ function registerContrastTools(server2) {
7957
8657
  "stackwright_pro_check_contrast",
7958
8658
  'Compute the exact WCAG 2.1 contrast ratio between a foreground and background color. Returns the ratio and AA/AAA pass/fail flags. Use this to verify any foreground/background color pair before writing it to the token set. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
7959
8659
  {
7960
- fg: z20.string().describe("Foreground (text) color \u2014 hex or HSL"),
7961
- bg: z20.string().describe("Background color \u2014 hex or HSL")
8660
+ fg: z22.string().describe("Foreground (text) color \u2014 hex or HSL"),
8661
+ bg: z22.string().describe("Background color \u2014 hex or HSL")
7962
8662
  },
7963
8663
  async ({ fg, bg }) => {
7964
8664
  let result;
@@ -7988,8 +8688,8 @@ function registerContrastTools(server2) {
7988
8688
  "stackwright_pro_derive_accessible_palette",
7989
8689
  'Given a seed color (treated as a background), derive the best accessible foreground (text) color that meets the target contrast ratio. Evaluates #ffffff and #000000 against the seed and picks whichever satisfies the target ratio with the highest contrast. Use this for every {name}-foreground token derivation instead of guessing white vs black. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
7990
8690
  {
7991
- seed: z20.string().describe("Background (seed) color \u2014 hex or HSL"),
7992
- targetRatio: numCoerce(z20.number().default(4.5)).describe(
8691
+ seed: z22.string().describe("Background (seed) color \u2014 hex or HSL"),
8692
+ targetRatio: numCoerce(z22.number().default(4.5)).describe(
7993
8693
  "Minimum required contrast ratio (default 4.5 for WCAG AA)"
7994
8694
  )
7995
8695
  },
@@ -8024,7 +8724,7 @@ function registerContrastTools(server2) {
8024
8724
  }
8025
8725
 
8026
8726
  // src/tools/compile.ts
8027
- import { z as z21 } from "zod";
8727
+ import { z as z23 } from "zod";
8028
8728
  import fs3 from "fs";
8029
8729
  import path4 from "path";
8030
8730
  import { compileCollections } from "@stackwright-pro/pulse/server";
@@ -8070,7 +8770,7 @@ async function tryOssCompile(fn, ctx, extra) {
8070
8770
  }
8071
8771
  }
8072
8772
  function registerCompileTools(server2) {
8073
- const projectRootArg = z21.string().optional().describe("Absolute path to project root (defaults to cwd)");
8773
+ const projectRootArg = z23.string().optional().describe("Absolute path to project root (defaults to cwd)");
8074
8774
  server2.tool(
8075
8775
  "stackwright_pro_compile_collections",
8076
8776
  "Compile stackwright.collections.yml \u2192 public/stackwright-content/_collections.json. Reads and validates the live-collections sidecar config file and emits the JSON sink consumed at runtime by PulseCollectionProvider.",
@@ -8246,7 +8946,7 @@ ${results.join("\n")}`;
8246
8946
  "stackwright_pro_compile_page",
8247
8947
  "Compile a single page by slug. Delegates to compilePage from @stackwright/build-scripts installed in the project.",
8248
8948
  {
8249
- slug: z21.string().describe('Page slug (relative path under pages/, e.g. "dashboard")'),
8949
+ slug: z23.string().describe('Page slug (relative path under pages/, e.g. "dashboard")'),
8250
8950
  projectRoot: projectRootArg
8251
8951
  },
8252
8952
  async ({ slug, projectRoot }) => {
@@ -8266,15 +8966,15 @@ ${results.join("\n")}`;
8266
8966
  }
8267
8967
 
8268
8968
  // src/tools/strip-legacy-integrations.ts
8269
- import { z as z22 } from "zod";
8270
- import { existsSync as existsSync13, readFileSync as readFileSync13, writeFileSync as writeFileSync9 } from "fs";
8271
- import { join as join14 } from "path";
8969
+ import { z as z24 } from "zod";
8970
+ import { existsSync as existsSync17, readFileSync as readFileSync17, writeFileSync as writeFileSync9 } from "fs";
8971
+ import { join as join18 } from "path";
8272
8972
  function handleStripLegacyIntegrations(input) {
8273
8973
  const { projectRoot } = input;
8274
- const ymlPath = join14(projectRoot, "stackwright.yml");
8275
- const yamlPath = join14(projectRoot, "stackwright.yaml");
8276
- if (!existsSync13(ymlPath)) {
8277
- if (existsSync13(yamlPath)) {
8974
+ const ymlPath = join18(projectRoot, "stackwright.yml");
8975
+ const yamlPath = join18(projectRoot, "stackwright.yaml");
8976
+ if (!existsSync17(ymlPath)) {
8977
+ if (existsSync17(yamlPath)) {
8278
8978
  return {
8279
8979
  changed: false,
8280
8980
  removedEntries: 0,
@@ -8287,7 +8987,7 @@ function handleStripLegacyIntegrations(input) {
8287
8987
  message: "stackwright.yml not found \u2014 no-op."
8288
8988
  };
8289
8989
  }
8290
- const raw = readFileSync13(ymlPath, "utf-8");
8990
+ const raw = readFileSync17(ymlPath, "utf-8");
8291
8991
  const isCRLF = raw.includes("\r\n");
8292
8992
  const content = isCRLF ? raw.replace(/\r\n/g, "\n") : raw;
8293
8993
  const lines = content.split("\n");
@@ -8329,7 +9029,7 @@ function registerStripLegacyIntegrationsTool(server2) {
8329
9029
  "stackwright_pro_strip_legacy_integrations",
8330
9030
  "Removes the deprecated top-level integrations[] block from stackwright.yml. The authoritative source for integration config is stackwright.integrations.yml \u2014 this tool cleans up legacy entries from older raft runs. Idempotent: no-op if no legacy block present. Preserves all other top-level keys, comments, and formatting.",
8331
9031
  {
8332
- projectRoot: z22.string().describe("Absolute path to the project root directory")
9032
+ projectRoot: z24.string().describe("Absolute path to the project root directory")
8333
9033
  },
8334
9034
  async ({ projectRoot }) => {
8335
9035
  const result = handleStripLegacyIntegrations({ projectRoot });
@@ -8341,16 +9041,16 @@ function registerStripLegacyIntegrationsTool(server2) {
8341
9041
  }
8342
9042
 
8343
9043
  // src/tools/emitters.ts
8344
- import { z as z23 } from "zod";
8345
- import { readFileSync as readFileSync14, existsSync as existsSync14, readdirSync as readdirSync8 } from "fs";
8346
- import { join as join15 } from "path";
9044
+ import { z as z25 } from "zod";
9045
+ import { readFileSync as readFileSync18, existsSync as existsSync18, readdirSync as readdirSync11 } from "fs";
9046
+ import { join as join19 } from "path";
8347
9047
  import yaml from "js-yaml";
8348
9048
  import { emitEnvLocal, emitProvidersFile, emitMockPorts } from "@stackwright-pro/emitters";
8349
9049
  function readIntegrations(cwd) {
8350
- const filePath = join15(cwd, "stackwright.integrations.yml");
8351
- if (!existsSync14(filePath)) return [];
9050
+ const filePath = join19(cwd, "stackwright.integrations.yml");
9051
+ if (!existsSync18(filePath)) return [];
8352
9052
  try {
8353
- const raw = yaml.load(readFileSync14(filePath, "utf-8"));
9053
+ const raw = yaml.load(readFileSync18(filePath, "utf-8"));
8354
9054
  if (!raw?.integrations || !Array.isArray(raw.integrations)) return [];
8355
9055
  return raw.integrations.filter((i) => typeof i.name === "string").map((i) => ({
8356
9056
  name: i.name,
@@ -8361,15 +9061,15 @@ function readIntegrations(cwd) {
8361
9061
  }
8362
9062
  }
8363
9063
  function readServiceTokenRefs(cwd) {
8364
- const servicesDir = join15(cwd, "services");
8365
- if (!existsSync14(servicesDir)) return [];
9064
+ const servicesDir = join19(cwd, "services");
9065
+ if (!existsSync18(servicesDir)) return [];
8366
9066
  try {
8367
- const files = readdirSync8(servicesDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
9067
+ const files = readdirSync11(servicesDir).filter((f) => f.endsWith(".yml") || f.endsWith(".yaml"));
8368
9068
  const refs = [];
8369
9069
  for (const file of files) {
8370
9070
  try {
8371
9071
  const raw = yaml.load(
8372
- readFileSync14(join15(servicesDir, file), "utf-8")
9072
+ readFileSync18(join19(servicesDir, file), "utf-8")
8373
9073
  );
8374
9074
  if (raw?.tokenEnv && typeof raw.tokenEnv === "string") {
8375
9075
  refs.push({
@@ -8400,7 +9100,7 @@ Call this tool, then write both files with stackwright_pro_safe_write:
8400
9100
 
8401
9101
  Do NOT compose the contents by hand. This emitter is the source of truth. If an env var is missing, update integrations.yml or the emitter \u2014 not the file. This is the first concrete instance of the swp-zf9y deterministic emitter pattern. ${ENV_LOCAL_DESC}`,
8402
9102
  {
8403
- cwd: z23.string().describe(
9103
+ cwd: z25.string().describe(
8404
9104
  "Absolute path to the project root containing stackwright.integrations.yml and optionally services/*.yml"
8405
9105
  )
8406
9106
  },
@@ -8447,8 +9147,8 @@ mapProvider options:
8447
9147
 
8448
9148
  This is the second concrete instance of the swp-zf9y deterministic emitter pattern. Refs: swp-57ku, swp-2m89, swp-zf9y, drawer 957. ${PROVIDERS_DESC}`,
8449
9149
  {
8450
- cwd: z23.string().describe("Absolute path to the project root containing stackwright.integrations.yml"),
8451
- mapProvider: z23.enum(["maplibre", "cesium", "none"]).optional().describe(
9150
+ cwd: z25.string().describe("Absolute path to the project root containing stackwright.integrations.yml"),
9151
+ mapProvider: z25.enum(["maplibre", "cesium", "none"]).optional().describe(
8452
9152
  "Map provider to register (default: cesium for Pro \u2014 3D globe + terrain). Use maplibre for OSS-style 2D dashboards (explicit opt-out). Use none if pages have no map content_items."
8453
9153
  )
8454
9154
  },
@@ -8489,7 +9189,7 @@ Workflow:
8489
9189
 
8490
9190
  This is the third concrete instance of the swp-zf9y deterministic emitter pattern. Refs: swp-phkh, swp-zf9y. ${PORTS_DESC}`,
8491
9191
  {
8492
- cwd: z23.string().describe("Absolute path to the project root containing stackwright.integrations.yml")
9192
+ cwd: z25.string().describe("Absolute path to the project root containing stackwright.integrations.yml")
8493
9193
  },
8494
9194
  async ({ cwd }) => {
8495
9195
  const integrations = readIntegrations(cwd);
@@ -8518,9 +9218,9 @@ This is the third concrete instance of the swp-zf9y deterministic emitter patter
8518
9218
  }
8519
9219
 
8520
9220
  // src/tools/list-specs.ts
8521
- import { z as z24 } from "zod";
8522
- import { readdirSync as readdirSync9, readFileSync as readFileSync15, statSync as statSync2, existsSync as existsSync15 } from "fs";
8523
- import { join as join16, extname, basename as basename2 } from "path";
9221
+ import { z as z26 } from "zod";
9222
+ import { readdirSync as readdirSync12, readFileSync as readFileSync19, statSync as statSync2, existsSync as existsSync19 } from "fs";
9223
+ import { join as join20, extname, basename as basename2 } from "path";
8524
9224
  var STRIP_SUFFIXES = ["-openapi", "-asyncapi", "-api", "-spec"];
8525
9225
  var ALLOWED_EXTS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
8526
9226
  function deriveIntegrationName(fileName) {
@@ -8542,18 +9242,18 @@ function detectFormat(snippet) {
8542
9242
  }
8543
9243
  function handleListSpecs(input) {
8544
9244
  const { projectRoot } = input;
8545
- const specsDir = join16(projectRoot, "specs");
9245
+ const specsDir = join20(projectRoot, "specs");
8546
9246
  const empty = { projectRoot, specs: [], totalCount: 0 };
8547
- if (!existsSync15(specsDir)) return empty;
9247
+ if (!existsSync19(specsDir)) return empty;
8548
9248
  let entries;
8549
9249
  try {
8550
- entries = readdirSync9(specsDir);
9250
+ entries = readdirSync12(specsDir);
8551
9251
  } catch {
8552
9252
  return empty;
8553
9253
  }
8554
9254
  const specs = [];
8555
9255
  for (const entry of entries) {
8556
- const fullPath = join16(specsDir, entry);
9256
+ const fullPath = join20(specsDir, entry);
8557
9257
  let stat;
8558
9258
  try {
8559
9259
  stat = statSync2(fullPath);
@@ -8564,7 +9264,7 @@ function handleListSpecs(input) {
8564
9264
  if (!ALLOWED_EXTS.has(extname(entry).toLowerCase())) continue;
8565
9265
  let snippet = "";
8566
9266
  try {
8567
- snippet = readFileSync15(fullPath, "utf-8").slice(0, 2048);
9267
+ snippet = readFileSync19(fullPath, "utf-8").slice(0, 2048);
8568
9268
  } catch {
8569
9269
  }
8570
9270
  specs.push({
@@ -8583,7 +9283,7 @@ function registerListSpecsTool(server2) {
8583
9283
  "stackwright_pro_list_specs",
8584
9284
  "Enumerate OpenAPI/AsyncAPI specs in the project's specs/ directory. Returns one entry per spec with integration name (derived from filename), detected format, and size. Used by the foreman to fan out the api phase with one api-otter invocation per spec.",
8585
9285
  {
8586
- projectRoot: z24.string().describe("Absolute path to the project root directory")
9286
+ projectRoot: z26.string().describe("Absolute path to the project root directory")
8587
9287
  },
8588
9288
  async ({ projectRoot }) => {
8589
9289
  const result = handleListSpecs({ projectRoot });
@@ -8595,8 +9295,8 @@ function registerListSpecsTool(server2) {
8595
9295
  }
8596
9296
 
8597
9297
  // src/tools/describe-endpoint.ts
8598
- import { z as z25 } from "zod";
8599
- import { readFileSync as readFileSync16 } from "fs";
9298
+ import { z as z27 } from "zod";
9299
+ import { readFileSync as readFileSync20 } from "fs";
8600
9300
  import { extname as extname2 } from "path";
8601
9301
  import yaml2 from "js-yaml";
8602
9302
  function resolveRef(root, ref) {
@@ -8629,7 +9329,7 @@ function deref(root, obj) {
8629
9329
  return o;
8630
9330
  }
8631
9331
  function loadSpec(specPath) {
8632
- const raw = readFileSync16(specPath, "utf-8");
9332
+ const raw = readFileSync20(specPath, "utf-8");
8633
9333
  const ext = extname2(specPath).toLowerCase();
8634
9334
  if (ext === ".json") {
8635
9335
  return JSON.parse(raw);
@@ -8742,9 +9442,9 @@ function registerDescribeEndpointTool(server2) {
8742
9442
  "from required query params and resolved auth query params (swp-8r8i + swp-1l6l)."
8743
9443
  ].join("\n"),
8744
9444
  {
8745
- specPath: z25.string().describe("Absolute path to the OpenAPI spec file (.yaml, .yml, or .json)"),
8746
- path: z25.string().describe('The API path to inspect (e.g. "/stations/{stationId}/observations")'),
8747
- method: z25.string().describe('HTTP method (case-insensitive: "get", "post", "put", etc.)')
9445
+ specPath: z27.string().describe("Absolute path to the OpenAPI spec file (.yaml, .yml, or .json)"),
9446
+ path: z27.string().describe('The API path to inspect (e.g. "/stations/{stationId}/observations")'),
9447
+ method: z27.string().describe('HTTP method (case-insensitive: "get", "post", "put", etc.)')
8748
9448
  },
8749
9449
  async ({ specPath, path: path5, method }) => {
8750
9450
  try {
@@ -8764,10 +9464,10 @@ function registerDescribeEndpointTool(server2) {
8764
9464
  }
8765
9465
 
8766
9466
  // src/tools/consolidate-integrations.ts
8767
- import { z as z26 } from "zod";
8768
- import { readdirSync as readdirSync10, readFileSync as readFileSync17, writeFileSync as writeFileSync10, existsSync as existsSync16, mkdirSync as mkdirSync9 } from "fs";
9467
+ import { z as z28 } from "zod";
9468
+ import { readdirSync as readdirSync13, readFileSync as readFileSync21, writeFileSync as writeFileSync10, existsSync as existsSync20, mkdirSync as mkdirSync9 } from "fs";
8769
9469
  import { lockSync as lockSync2 } from "proper-lockfile";
8770
- import { join as join17, basename as basename3 } from "path";
9470
+ import { join as join21, basename as basename3 } from "path";
8771
9471
  import yaml3 from "js-yaml";
8772
9472
  var BASE_MOCK_PORT = 4011;
8773
9473
  var STRIP_SUFFIXES2 = ["-openapi", "-asyncapi", "-api", "-spec"];
@@ -8780,13 +9480,13 @@ function deriveNameFromFilename(filename) {
8780
9480
  }
8781
9481
  function handleConsolidateIntegrations(input) {
8782
9482
  const { projectRoot } = input;
8783
- const artifactsDir = join17(projectRoot, ".stackwright", "artifacts");
8784
- if (!existsSync16(artifactsDir)) {
9483
+ const artifactsDir = join21(projectRoot, ".stackwright", "artifacts");
9484
+ if (!existsSync20(artifactsDir)) {
8785
9485
  return { written: false, integrationCount: 0, reason: "no per-spec artifacts found" };
8786
9486
  }
8787
9487
  let entries;
8788
9488
  try {
8789
- entries = readdirSync10(artifactsDir).filter(
9489
+ entries = readdirSync13(artifactsDir).filter(
8790
9490
  (f) => f.startsWith("api-config-") && f.endsWith(".json") && f !== "api-config.json"
8791
9491
  );
8792
9492
  } catch {
@@ -8801,7 +9501,7 @@ function handleConsolidateIntegrations(input) {
8801
9501
  entries.forEach((filename, index) => {
8802
9502
  let artifact = {};
8803
9503
  try {
8804
- const raw = readFileSync17(join17(artifactsDir, filename), "utf-8");
9504
+ const raw = readFileSync21(join21(artifactsDir, filename), "utf-8");
8805
9505
  artifact = JSON.parse(raw);
8806
9506
  } catch {
8807
9507
  }
@@ -8834,19 +9534,19 @@ function handleConsolidateIntegrations(input) {
8834
9534
  });
8835
9535
  const integrations = Array.from(integrationsByName.values());
8836
9536
  const dedupedCount = entries.length - skippedIntegrationsList.length - integrations.length;
8837
- const integrationsYmlPath = join17(projectRoot, "stackwright.integrations.yml");
9537
+ const integrationsYmlPath = join21(projectRoot, "stackwright.integrations.yml");
8838
9538
  let existing = {};
8839
- if (existsSync16(integrationsYmlPath)) {
9539
+ if (existsSync20(integrationsYmlPath)) {
8840
9540
  try {
8841
- const raw = readFileSync17(integrationsYmlPath, "utf-8");
9541
+ const raw = readFileSync21(integrationsYmlPath, "utf-8");
8842
9542
  existing = yaml3.load(raw) ?? {};
8843
9543
  } catch {
8844
9544
  existing = {};
8845
9545
  }
8846
9546
  }
8847
9547
  const merged = { ...existing, integrations };
8848
- mkdirSync9(join17(projectRoot), { recursive: true });
8849
- if (!existsSync16(integrationsYmlPath)) {
9548
+ mkdirSync9(join21(projectRoot), { recursive: true });
9549
+ if (!existsSync20(integrationsYmlPath)) {
8850
9550
  writeFileSync10(integrationsYmlPath, "", "utf-8");
8851
9551
  }
8852
9552
  const release = lockSync2(integrationsYmlPath, {
@@ -8885,7 +9585,7 @@ function registerConsolidateIntegrationsTool(server2) {
8885
9585
  "no per-spec artifacts are found."
8886
9586
  ].join(" "),
8887
9587
  {
8888
- projectRoot: z26.string().describe("Absolute path to the project root directory")
9588
+ projectRoot: z28.string().describe("Absolute path to the project root directory")
8889
9589
  },
8890
9590
  async ({ projectRoot }) => {
8891
9591
  try {
@@ -8921,7 +9621,7 @@ var package_default = {
8921
9621
  "@stackwright-pro/pulse": "workspace:*",
8922
9622
  "@stackwright-pro/telemetry": "workspace:*",
8923
9623
  "@stackwright-pro/types": "workspace:*",
8924
- "@stackwright/mcp": "0.6.0-alpha.2",
9624
+ "@stackwright/mcp": "0.6.0-alpha.5",
8925
9625
  "@types/js-yaml": "^4.0.9",
8926
9626
  "js-yaml": "^4.2.0",
8927
9627
  "proper-lockfile": "^4.1.2",
@@ -8943,7 +9643,7 @@ var package_default = {
8943
9643
  "test:coverage": "vitest run --coverage"
8944
9644
  },
8945
9645
  name: "@stackwright-pro/mcp",
8946
- version: "0.2.0-alpha.114",
9646
+ version: "0.2.0-alpha.117",
8947
9647
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
8948
9648
  license: "SEE LICENSE IN LICENSE",
8949
9649
  main: "./dist/server.js",
@@ -9021,6 +9721,7 @@ registerValidateYamlFragmentTool(server);
9021
9721
  registerProPageTools(server);
9022
9722
  registerGetSchemaTool(server);
9023
9723
  registerScaffoldCleanupTools(server);
9724
+ registerWriteWorkflowContainerPageTool(server);
9024
9725
  registerContrastTools(server);
9025
9726
  registerCompileTools(server);
9026
9727
  registerStripLegacyIntegrationsTool(server);