@stackwright-pro/mcp 0.2.0-alpha.113 → 0.2.0-alpha.115

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