@stackwright-pro/mcp 0.2.0-alpha.90 → 0.2.0-alpha.95

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
@@ -120,8 +120,8 @@ function registerDataExplorerTools(server2) {
120
120
  lines.push(" endpoints:");
121
121
  if (result.filter.include && result.filter.include.length > 0) {
122
122
  lines.push(" include:");
123
- for (const path3 of result.filter.include) {
124
- lines.push(` - ${path3}`);
123
+ for (const path4 of result.filter.include) {
124
+ lines.push(` - ${path4}`);
125
125
  }
126
126
  }
127
127
  if (result.filter.exclude && result.filter.exclude.length > 0) {
@@ -1595,6 +1595,9 @@ var OTTER_NAME_TO_PHASE = [
1595
1595
  ["dashboard", "dashboard"],
1596
1596
  ["data", "data"],
1597
1597
  ["page", "pages"],
1598
+ // swp-4071: workflow-otter renamed to form-wizard-otter; keep 'workflow' for
1599
+ // any in-flight session that still uses the old name (belt-and-suspenders)
1600
+ ["form-wizard", "workflow"],
1598
1601
  ["workflow", "workflow"],
1599
1602
  ["services", "services"],
1600
1603
  ["polish", "polish"],
@@ -1609,7 +1612,7 @@ var PHASE_TO_OTTER = {
1609
1612
  pages: "stackwright-pro-page-otter",
1610
1613
  dashboard: "stackwright-pro-dashboard-otter",
1611
1614
  data: "stackwright-pro-data-otter",
1612
- workflow: "stackwright-pro-workflow-otter",
1615
+ workflow: "stackwright-pro-form-wizard-otter",
1613
1616
  services: "stackwright-services-otter",
1614
1617
  polish: "stackwright-pro-polish-otter",
1615
1618
  geo: "stackwright-pro-geo-otter"
@@ -1867,8 +1870,8 @@ function registerOrchestrationTools(server2) {
1867
1870
  {
1868
1871
  buildContext: z9.string().describe("Free-text description of what the user wants to build")
1869
1872
  },
1870
- async ({ buildContext }) => {
1871
- const { text, isError } = handleSaveBuildContext({ buildContext });
1873
+ async ({ buildContext: buildContext2 }) => {
1874
+ const { text, isError } = handleSaveBuildContext({ buildContext: buildContext2 });
1872
1875
  return {
1873
1876
  content: [{ type: "text", text }],
1874
1877
  isError
@@ -1996,8 +1999,8 @@ function registerOrchestrationTools(server2) {
1996
1999
 
1997
2000
  // src/tools/pipeline.ts
1998
2001
  import { z as z13 } from "zod";
1999
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
2000
- import { join as join4 } from "path";
2002
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync6 } from "fs";
2003
+ import { join as join5 } from "path";
2001
2004
  import { createHash as createHash3 } from "crypto";
2002
2005
 
2003
2006
  // src/artifact-signing.ts
@@ -2687,6 +2690,223 @@ function registerGetSchemaTool(server2) {
2687
2690
  );
2688
2691
  }
2689
2692
 
2693
+ // src/tools/pipeline-graph.ts
2694
+ import { readFileSync as readFileSync4, readdirSync as readdirSync2, lstatSync as lstatSync5 } from "fs";
2695
+ import { join as join4 } from "path";
2696
+ var MANIFEST_NAME_TO_PHASE = {
2697
+ "stackwright-pro-designer-otter": "designer",
2698
+ "stackwright-pro-theme-otter": "theme",
2699
+ "stackwright-pro-scaffold-otter": "scaffold",
2700
+ "stackwright-pro-api-otter": "api",
2701
+ "stackwright-pro-auth-otter": "auth",
2702
+ "stackwright-pro-data-otter": "data",
2703
+ "stackwright-pro-page-otter": "pages",
2704
+ "stackwright-pro-dashboard-otter": "dashboard",
2705
+ "stackwright-pro-form-wizard-otter": "workflow",
2706
+ "stackwright-pro-geo-otter": "geo",
2707
+ "stackwright-pro-polish-otter": "polish",
2708
+ "stackwright-services-otter": "services"
2709
+ };
2710
+ function manifestNameToPhase(name) {
2711
+ return MANIFEST_NAME_TO_PHASE[name] ?? null;
2712
+ }
2713
+ var OTTER_SEARCH_PATHS = [
2714
+ "node_modules/@stackwright-pro/otters/src/",
2715
+ // production: installed package
2716
+ "packages/otters/src/",
2717
+ // monorepo: run from repo root
2718
+ "../otters/src/"
2719
+ // monorepo: run from packages/mcp/
2720
+ ];
2721
+ function resolveOtterDirFromCwd() {
2722
+ const cwd = process.cwd();
2723
+ for (const relative of OTTER_SEARCH_PATHS) {
2724
+ const candidate = join4(cwd, relative);
2725
+ try {
2726
+ lstatSync5(candidate);
2727
+ return candidate;
2728
+ } catch {
2729
+ }
2730
+ }
2731
+ throw new Error(
2732
+ `Cannot locate otter directory. Searched: ${OTTER_SEARCH_PATHS.join(", ")} (relative to ${cwd}). Make sure @stackwright-pro/otters is installed.`
2733
+ );
2734
+ }
2735
+ function loadOtterManifestsFromDir(otterDir) {
2736
+ const entries = readdirSync2(otterDir);
2737
+ const manifests = [];
2738
+ for (const filename of entries) {
2739
+ if (!filename.endsWith("-otter.json")) continue;
2740
+ const filePath = join4(otterDir, filename);
2741
+ if (lstatSync5(filePath).isSymbolicLink()) continue;
2742
+ try {
2743
+ const raw = readFileSync4(filePath, "utf-8");
2744
+ const manifest = JSON.parse(raw);
2745
+ manifests.push(manifest);
2746
+ } catch (err) {
2747
+ const msg = err instanceof Error ? err.message : String(err);
2748
+ throw new Error(`Failed to load otter manifest "${filename}": ${msg}`, { cause: err });
2749
+ }
2750
+ }
2751
+ return manifests;
2752
+ }
2753
+ function topologicalSort(dependencies) {
2754
+ const phases = Object.keys(dependencies);
2755
+ const inDegree = {};
2756
+ const adjList = {};
2757
+ for (const phase of phases) {
2758
+ inDegree[phase] ??= 0;
2759
+ adjList[phase] ??= [];
2760
+ }
2761
+ for (const phase of phases) {
2762
+ for (const dep of dependencies[phase] ?? []) {
2763
+ inDegree[phase] = (inDegree[phase] ?? 0) + 1;
2764
+ adjList[dep] ??= [];
2765
+ adjList[dep].push(phase);
2766
+ }
2767
+ }
2768
+ const queue = phases.filter((p) => (inDegree[p] ?? 0) === 0);
2769
+ const result = [];
2770
+ while (queue.length > 0) {
2771
+ const node = queue.shift();
2772
+ result.push(node);
2773
+ for (const dependent of adjList[node] ?? []) {
2774
+ inDegree[dependent] -= 1;
2775
+ if (inDegree[dependent] === 0) {
2776
+ queue.push(dependent);
2777
+ }
2778
+ }
2779
+ }
2780
+ if (result.length !== phases.length) {
2781
+ const cycleNodes = phases.filter((p) => (inDegree[p] ?? 0) > 0);
2782
+ throw new Error(
2783
+ `Pipeline dependency cycle detected involving: [${cycleNodes.join(", ")}]. Check otter pipeline declarations for circular dependencies.`
2784
+ );
2785
+ }
2786
+ return result;
2787
+ }
2788
+ function buildPipelineGraph(manifests) {
2789
+ const sinkProducers = {};
2790
+ const artifactProducers = {};
2791
+ for (const m of manifests) {
2792
+ const phase = manifestNameToPhase(m.name);
2793
+ if (!phase) continue;
2794
+ const out = m.pipeline?.outputs;
2795
+ if (!out) continue;
2796
+ for (const sink of out.sinks ?? []) {
2797
+ if (sinkProducers[sink]) {
2798
+ throw new Error(
2799
+ `Sink "${sink}" is produced by both "${sinkProducers[sink]}" and "${phase}". Each sink must have exactly one producer.`
2800
+ );
2801
+ }
2802
+ sinkProducers[sink] = phase;
2803
+ }
2804
+ if (out.artifact) {
2805
+ if (artifactProducers[out.artifact]) {
2806
+ throw new Error(
2807
+ `Artifact "${out.artifact}" is produced by both "${artifactProducers[out.artifact]}" and "${phase}". Each artifact must have exactly one producer.`
2808
+ );
2809
+ }
2810
+ artifactProducers[out.artifact] = phase;
2811
+ }
2812
+ }
2813
+ const dependencies = {};
2814
+ for (const m of manifests) {
2815
+ const phase = manifestNameToPhase(m.name);
2816
+ if (!phase) continue;
2817
+ const deps = /* @__PURE__ */ new Set();
2818
+ const inp = m.pipeline?.inputs;
2819
+ if (inp) {
2820
+ for (const sink of inp.sinks ?? []) {
2821
+ const producer = sinkProducers[sink];
2822
+ if (!producer) {
2823
+ throw new Error(
2824
+ `Phase "${phase}" declares sink input "${sink}" but no otter produces it. Add an otter with outputs.sinks including "${sink}".`
2825
+ );
2826
+ }
2827
+ if (producer !== phase) deps.add(producer);
2828
+ }
2829
+ for (const artifact of inp.artifacts ?? []) {
2830
+ const producer = artifactProducers[artifact];
2831
+ if (!producer) {
2832
+ throw new Error(
2833
+ `Phase "${phase}" declares artifact input "${artifact}" but no otter produces it. Add an otter with outputs.artifact = "${artifact}".`
2834
+ );
2835
+ }
2836
+ if (producer !== phase) deps.add(producer);
2837
+ }
2838
+ }
2839
+ dependencies[phase] = Array.from(deps);
2840
+ }
2841
+ const order = topologicalSort(dependencies);
2842
+ return { dependencies, order, sinkProducers, artifactProducers };
2843
+ }
2844
+ var DISTRIBUTED_NAMESPACES = ["@stackwright-pro", "@stackwright"];
2845
+ function loadDistributedOtterManifests(cwd) {
2846
+ const results = [];
2847
+ for (const namespace of DISTRIBUTED_NAMESPACES) {
2848
+ const nsDir = join4(cwd, "node_modules", namespace);
2849
+ let pkgDirNames;
2850
+ try {
2851
+ pkgDirNames = readdirSync2(nsDir);
2852
+ } catch {
2853
+ continue;
2854
+ }
2855
+ for (const pkgDirName of pkgDirNames) {
2856
+ const pkgDir = join4(nsDir, pkgDirName);
2857
+ const packageName = `${namespace}/${pkgDirName}`;
2858
+ let otterRelPaths;
2859
+ try {
2860
+ const raw = readFileSync4(join4(pkgDir, "package.json"), "utf-8");
2861
+ const pkgJson = JSON.parse(raw);
2862
+ const declared = pkgJson.stackwright?.otters;
2863
+ if (!Array.isArray(declared) || declared.length === 0) continue;
2864
+ otterRelPaths = declared;
2865
+ } catch {
2866
+ continue;
2867
+ }
2868
+ for (const relPath of otterRelPaths) {
2869
+ const otterFilePath = join4(pkgDir, relPath);
2870
+ try {
2871
+ const raw = readFileSync4(otterFilePath, "utf-8");
2872
+ const manifest = JSON.parse(raw);
2873
+ results.push({ manifest, packageName });
2874
+ } catch (err) {
2875
+ const msg = err instanceof Error ? err.message : String(err);
2876
+ console.warn(
2877
+ `[pipeline-graph] Failed to load distributed otter at "${otterFilePath}" from "${packageName}": ${msg}`
2878
+ );
2879
+ }
2880
+ }
2881
+ }
2882
+ }
2883
+ return results;
2884
+ }
2885
+ function _mergeManifestPools(central, distributed) {
2886
+ const centralNames = new Set(central.map((m) => m.name));
2887
+ const merged = [...central];
2888
+ for (const { manifest, packageName } of distributed) {
2889
+ if (centralNames.has(manifest.name)) {
2890
+ console.warn(
2891
+ `[pipeline-graph] Otter "${manifest.name}" declared by both @stackwright-pro/otters and ${packageName} \u2014 using @stackwright-pro/otters version.`
2892
+ );
2893
+ } else {
2894
+ merged.push(manifest);
2895
+ }
2896
+ }
2897
+ return merged;
2898
+ }
2899
+ var _cachedGraph = null;
2900
+ function loadPipelineGraph() {
2901
+ if (_cachedGraph) return _cachedGraph;
2902
+ const otterDir = resolveOtterDirFromCwd();
2903
+ const central = loadOtterManifestsFromDir(otterDir);
2904
+ const distributed = loadDistributedOtterManifests(process.cwd());
2905
+ const manifests = _mergeManifestPools(central, distributed);
2906
+ _cachedGraph = buildPipelineGraph(manifests);
2907
+ return _cachedGraph;
2908
+ }
2909
+
2690
2910
  // src/tools/pipeline.ts
2691
2911
  var PHASE_ORDER = [
2692
2912
  "designer",
@@ -2695,45 +2915,15 @@ var PHASE_ORDER = [
2695
2915
  // generates app/ directory from config
2696
2916
  "api",
2697
2917
  "data",
2918
+ "auth",
2919
+ // moved earlier — only depends on design-language.json (designer)
2698
2920
  "geo",
2699
2921
  "workflow",
2700
2922
  "services",
2701
2923
  "pages",
2702
2924
  "dashboard",
2703
- "auth",
2704
2925
  "polish"
2705
2926
  ];
2706
- var PHASE_DEPENDENCIES = {
2707
- designer: [],
2708
- theme: ["designer"],
2709
- scaffold: ["designer", "theme"],
2710
- // needs stackwright.yml + theme-tokens
2711
- api: [],
2712
- data: ["api"],
2713
- geo: ["data"],
2714
- // workflow: no hard deps — uses service: references with Prism mock fallback
2715
- // when API artifacts aren't available; roles come from user answers
2716
- workflow: [],
2717
- // services: needs API endpoints to know what to wire, and optionally
2718
- // workflow states as trigger sources. Produces OpenAPI specs consumed
2719
- // by pages and dashboard for typed data wiring.
2720
- services: ["api", "workflow"],
2721
- // pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
2722
- // specs) and on geo so the specialist prompt includes geo-manifest.json
2723
- // — page/dashboard otters see which page slugs are already claimed and
2724
- // won't attempt to overwrite them (swp-73c). 'api' is still transitive
2725
- // through 'data'; auth removed — page-otter has graceful fallback.
2726
- pages: ["designer", "theme", "data", "services", "geo", "scaffold"],
2727
- dashboard: ["designer", "theme", "data", "services", "geo", "scaffold"],
2728
- // auth is the penultimate phase — runs after all content-producing phases
2729
- // so it can read pages, dashboard, workflow, and geo artifacts for route
2730
- // protection and RBAC wiring. Skipped upstream phases still satisfy deps
2731
- // (the foreman marks them executed=true), so auth always runs.
2732
- auth: ["pages", "dashboard", "workflow", "geo"],
2733
- // polish is the terminal phase — runs after auth so the landing page and
2734
- // nav reflect the final set of protected routes and all generated pages.
2735
- polish: ["auth"]
2736
- };
2737
2927
  var PHASE_ARTIFACT = {
2738
2928
  designer: "design-language.json",
2739
2929
  theme: "theme-tokens.json",
@@ -2757,7 +2947,7 @@ var PHASE_TO_OTTER2 = {
2757
2947
  data: "stackwright-pro-data-otter",
2758
2948
  pages: "stackwright-pro-page-otter",
2759
2949
  dashboard: "stackwright-pro-dashboard-otter",
2760
- workflow: "stackwright-pro-workflow-otter",
2950
+ workflow: "stackwright-pro-form-wizard-otter",
2761
2951
  services: "stackwright-services-otter",
2762
2952
  polish: "stackwright-pro-polish-otter",
2763
2953
  geo: "stackwright-pro-geo-otter"
@@ -2790,7 +2980,7 @@ function createDefaultState() {
2790
2980
  };
2791
2981
  }
2792
2982
  function statePath(cwd) {
2793
- return join4(cwd, ".stackwright", "pipeline-state.json");
2983
+ return join5(cwd, ".stackwright", "pipeline-state.json");
2794
2984
  }
2795
2985
  function readState(cwd) {
2796
2986
  const p = statePath(cwd);
@@ -2803,7 +2993,7 @@ function readState(cwd) {
2803
2993
  }
2804
2994
  function safeWriteSync(filePath, content) {
2805
2995
  if (existsSync5(filePath)) {
2806
- const stat = lstatSync5(filePath);
2996
+ const stat = lstatSync6(filePath);
2807
2997
  if (stat.isSymbolicLink()) {
2808
2998
  throw new Error(`Refusing to write to symlink: ${filePath}`);
2809
2999
  }
@@ -2812,15 +3002,15 @@ function safeWriteSync(filePath, content) {
2812
3002
  }
2813
3003
  function safeReadSync(filePath) {
2814
3004
  if (existsSync5(filePath)) {
2815
- const stat = lstatSync5(filePath);
3005
+ const stat = lstatSync6(filePath);
2816
3006
  if (stat.isSymbolicLink()) {
2817
3007
  throw new Error(`Refusing to read symlink: ${filePath}`);
2818
3008
  }
2819
3009
  }
2820
- return readFileSync4(filePath, "utf-8");
3010
+ return readFileSync5(filePath, "utf-8");
2821
3011
  }
2822
3012
  function writeState(cwd, state) {
2823
- const dir = join4(cwd, ".stackwright");
3013
+ const dir = join5(cwd, ".stackwright");
2824
3014
  mkdirSync4(dir, { recursive: true });
2825
3015
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2826
3016
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
@@ -2842,7 +3032,7 @@ function handleGetPipelineState(_cwd) {
2842
3032
  const cwd = _cwd ?? process.cwd();
2843
3033
  try {
2844
3034
  const state = readState(cwd);
2845
- const keyPath = join4(cwd, ".stackwright", "pipeline-keys.json");
3035
+ const keyPath = join5(cwd, ".stackwright", "pipeline-keys.json");
2846
3036
  if (!existsSync5(keyPath)) {
2847
3037
  try {
2848
3038
  const { fingerprint } = initPipelineKeys(cwd);
@@ -2946,7 +3136,7 @@ function handleCheckExecutionReady(_cwd, phase) {
2946
3136
  isError: true
2947
3137
  };
2948
3138
  }
2949
- const answerFile = join4(cwd, ".stackwright", "answers", `${phase}.json`);
3139
+ const answerFile = join5(cwd, ".stackwright", "answers", `${phase}.json`);
2950
3140
  if (!existsSync5(answerFile)) {
2951
3141
  return {
2952
3142
  text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
@@ -2971,11 +3161,11 @@ function handleCheckExecutionReady(_cwd, phase) {
2971
3161
  }
2972
3162
  }
2973
3163
  try {
2974
- const answersDir = join4(cwd, ".stackwright", "answers");
3164
+ const answersDir = join5(cwd, ".stackwright", "answers");
2975
3165
  const answeredPhases = [];
2976
3166
  const missingPhases = [];
2977
3167
  for (const phase2 of PHASE_ORDER) {
2978
- const answerFile = join4(answersDir, `${phase2}.json`);
3168
+ const answerFile = join5(answersDir, `${phase2}.json`);
2979
3169
  if (existsSync5(answerFile)) {
2980
3170
  try {
2981
3171
  const raw = safeReadSync(answerFile);
@@ -3009,6 +3199,7 @@ function handleGetReadyPhases(_cwd) {
3009
3199
  const cwd = _cwd ?? process.cwd();
3010
3200
  try {
3011
3201
  const state = readState(cwd);
3202
+ const graph = loadPipelineGraph();
3012
3203
  const completed = [];
3013
3204
  const ready = [];
3014
3205
  const blocked = [];
@@ -3018,7 +3209,7 @@ function handleGetReadyPhases(_cwd) {
3018
3209
  completed.push(phase);
3019
3210
  continue;
3020
3211
  }
3021
- const deps = PHASE_DEPENDENCIES[phase];
3212
+ const deps = graph.dependencies[phase] ?? [];
3022
3213
  const unmetDeps = deps.filter((dep) => !state.phases[dep]?.executed);
3023
3214
  if (unmetDeps.length === 0) {
3024
3215
  ready.push(phase);
@@ -3044,7 +3235,7 @@ function handleGetReadyPhases(_cwd) {
3044
3235
  function handleListArtifacts(_cwd) {
3045
3236
  const cwd = _cwd ?? process.cwd();
3046
3237
  try {
3047
- const artifactsDir = join4(cwd, ".stackwright", "artifacts");
3238
+ const artifactsDir = join5(cwd, ".stackwright", "artifacts");
3048
3239
  let manifest = null;
3049
3240
  try {
3050
3241
  manifest = loadSignatureManifest(cwd);
@@ -3054,7 +3245,7 @@ function handleListArtifacts(_cwd) {
3054
3245
  let completedCount = 0;
3055
3246
  for (const phase of PHASE_ORDER) {
3056
3247
  const expectedFile = PHASE_ARTIFACT[phase];
3057
- const fullPath = join4(artifactsDir, expectedFile);
3248
+ const fullPath = join5(artifactsDir, expectedFile);
3058
3249
  const exists = existsSync5(fullPath);
3059
3250
  if (exists) completedCount++;
3060
3251
  let signed = false;
@@ -3108,9 +3299,9 @@ function handleWritePhaseQuestions(input) {
3108
3299
  }
3109
3300
  } catch {
3110
3301
  }
3111
- const questionsDir = join4(cwd, ".stackwright", "questions");
3302
+ const questionsDir = join5(cwd, ".stackwright", "questions");
3112
3303
  mkdirSync4(questionsDir, { recursive: true });
3113
- const filePath = join4(questionsDir, `${phase}.json`);
3304
+ const filePath = join5(questionsDir, `${phase}.json`);
3114
3305
  const payload = {
3115
3306
  version: "1.0",
3116
3307
  phase,
@@ -3150,13 +3341,13 @@ function handleBuildSpecialistPrompt(input) {
3150
3341
  };
3151
3342
  }
3152
3343
  try {
3153
- const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
3344
+ const answersPath = join5(cwd, ".stackwright", "answers", `${phase}.json`);
3154
3345
  let answers = {};
3155
3346
  if (existsSync5(answersPath)) {
3156
3347
  answers = JSON.parse(safeReadSync(answersPath));
3157
3348
  }
3158
3349
  let buildContextText = "";
3159
- const buildContextPath = join4(cwd, ".stackwright", "build-context.json");
3350
+ const buildContextPath = join5(cwd, ".stackwright", "build-context.json");
3160
3351
  if (existsSync5(buildContextPath)) {
3161
3352
  try {
3162
3353
  const bcRaw = JSON.parse(safeReadSync(buildContextPath));
@@ -3166,12 +3357,13 @@ function handleBuildSpecialistPrompt(input) {
3166
3357
  } catch {
3167
3358
  }
3168
3359
  }
3169
- const deps = PHASE_DEPENDENCIES[phase];
3360
+ const graph = loadPipelineGraph();
3361
+ const deps = graph.dependencies[phase] ?? [];
3170
3362
  const artifactSections = [];
3171
3363
  const missingDependencies = [];
3172
3364
  for (const dep of deps) {
3173
3365
  const artifactFile = PHASE_ARTIFACT[dep];
3174
- const artifactPath = join4(cwd, ".stackwright", "artifacts", artifactFile);
3366
+ const artifactPath = join5(cwd, ".stackwright", "artifacts", artifactFile);
3175
3367
  if (existsSync5(artifactPath)) {
3176
3368
  const rawContent = safeReadSync(artifactPath);
3177
3369
  const rawBytes = Buffer.from(rawContent, "utf-8");
@@ -3237,7 +3429,7 @@ ${JSON.stringify(content, null, 2)}`
3237
3429
  parts.push("BUILD_CONTEXT:", buildContextText, "");
3238
3430
  }
3239
3431
  if (phase === "auth") {
3240
- const initContextPath = join4(cwd, ".stackwright", "init-context.json");
3432
+ const initContextPath = join5(cwd, ".stackwright", "init-context.json");
3241
3433
  if (existsSync5(initContextPath)) {
3242
3434
  try {
3243
3435
  const initContext = JSON.parse(safeReadSync(initContextPath));
@@ -3470,7 +3662,7 @@ var PHASE_ARTIFACT_SCHEMA = {
3470
3662
  workflow: JSON.stringify(
3471
3663
  {
3472
3664
  version: "1.0",
3473
- generatedBy: "stackwright-pro-workflow-otter",
3665
+ generatedBy: "stackwright-pro-form-wizard-otter",
3474
3666
  // Root key is `workflow` — NOT `workflowConfig` (see swp-k7cl)
3475
3667
  workflow: {
3476
3668
  id: "procurement-approval",
@@ -3680,6 +3872,57 @@ function handleValidateArtifact(input) {
3680
3872
  const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3681
3873
  return { success: false, error: { message: issues } };
3682
3874
  }
3875
+ const rbacRoles = authConfig["rbacRoles"];
3876
+ const rbacDefaultRole = authConfig["rbacDefaultRole"];
3877
+ const protectedRoutes = authConfig["protectedRoutes"];
3878
+ if (Array.isArray(rbacRoles) && rbacRoles.length > 2 && Array.isArray(protectedRoutes) && protectedRoutes.length > 0 && typeof rbacDefaultRole === "string") {
3879
+ const uniqueRoles = new Set(
3880
+ protectedRoutes.map(
3881
+ (r) => typeof r === "string" ? rbacDefaultRole : r.requiredRole ?? rbacDefaultRole
3882
+ )
3883
+ );
3884
+ if (uniqueRoles.size === 1 && uniqueRoles.has(rbacDefaultRole)) {
3885
+ return {
3886
+ success: false,
3887
+ error: {
3888
+ message: `RBAC theatre detected: all ${protectedRoutes.length} protectedRoutes require '${rbacDefaultRole}' (the defaultRole), but ${rbacRoles.length} roles are defined. This defeats the RBAC hierarchy. Each route MUST have the minimum role required for its function. See auth-otter PER-ROUTE RBAC GRANULARITY section.`
3889
+ }
3890
+ };
3891
+ }
3892
+ }
3893
+ return { success: true };
3894
+ },
3895
+ // swp-4071 — opt-in cross-reference validator for the services phase.
3896
+ // If workflow-config exists and declares serviceHooks, every hook ref must resolve
3897
+ // to a flow or state-machine in this services-config artifact.
3898
+ // Skips gracefully when wizard-otter didn't run (Pro-only install) or declared no hooks.
3899
+ services: (artifact2) => {
3900
+ const workflowArtifactPath = join5(cwd, ".stackwright", "artifacts", "workflow-config.json");
3901
+ if (!existsSync5(workflowArtifactPath)) {
3902
+ return { success: true };
3903
+ }
3904
+ let workflowArtifact;
3905
+ try {
3906
+ workflowArtifact = JSON.parse(readFileSync5(workflowArtifactPath, "utf-8"));
3907
+ } catch {
3908
+ return { success: true };
3909
+ }
3910
+ const declaredHooks = workflowArtifact.serviceHooks ?? [];
3911
+ if (declaredHooks.length === 0) {
3912
+ return { success: true };
3913
+ }
3914
+ const flows = artifact2["flows"] ?? [];
3915
+ const workflows = artifact2["workflows"] ?? [];
3916
+ const flowNames = /* @__PURE__ */ new Set([...flows.map((f) => f.name), ...workflows.map((w) => w.name)]);
3917
+ const unresolved = declaredHooks.filter((h) => !flowNames.has(h.ref));
3918
+ if (unresolved.length > 0) {
3919
+ return {
3920
+ success: false,
3921
+ error: {
3922
+ message: `Workflow declared ${declaredHooks.length} service hook(s) but ${unresolved.length} unresolved by services-config: ` + unresolved.map((h) => `${h.ref} (${h.kind ?? "unknown"}: ${h.purpose ?? "no purpose"})`).join("; ") + ". Either add matching flows to services-config OR remove the hooks from the workflow YAML."
3923
+ }
3924
+ };
3925
+ }
3683
3926
  return { success: true };
3684
3927
  }
3685
3928
  };
@@ -3697,10 +3940,10 @@ function handleValidateArtifact(input) {
3697
3940
  }
3698
3941
  }
3699
3942
  try {
3700
- const artifactsDir = join4(cwd, ".stackwright", "artifacts");
3943
+ const artifactsDir = join5(cwd, ".stackwright", "artifacts");
3701
3944
  mkdirSync4(artifactsDir, { recursive: true });
3702
3945
  const artifactFile = PHASE_ARTIFACT[phase];
3703
- const artifactPath = join4(artifactsDir, artifactFile);
3946
+ const artifactPath = join5(artifactsDir, artifactFile);
3704
3947
  const serialized = JSON.stringify(artifact, null, 2) + "\n";
3705
3948
  const artifactBytes = Buffer.from(serialized, "utf-8");
3706
3949
  safeWriteSync(artifactPath, serialized);
@@ -3817,9 +4060,9 @@ function registerPipelineTools(server2) {
3817
4060
  isError: true
3818
4061
  };
3819
4062
  }
3820
- const questionsDir = join4(process.cwd(), ".stackwright", "questions");
4063
+ const questionsDir = join5(process.cwd(), ".stackwright", "questions");
3821
4064
  mkdirSync4(questionsDir, { recursive: true });
3822
- const outPath = join4(questionsDir, `${phase}.json`);
4065
+ const outPath = join5(questionsDir, `${phase}.json`);
3823
4066
  writeFileSync4(
3824
4067
  outPath,
3825
4068
  JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
@@ -3871,8 +4114,8 @@ function registerPipelineTools(server2) {
3871
4114
 
3872
4115
  // src/tools/safe-write.ts
3873
4116
  import { z as z14 } from "zod";
3874
- import { writeFileSync as writeFileSync5, readFileSync as readFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3875
- import { normalize, isAbsolute, dirname, join as join5 } from "path";
4117
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync7 } from "fs";
4118
+ import { normalize, isAbsolute, dirname, join as join6 } from "path";
3876
4119
  var OTTER_WRITE_ALLOWLISTS = {
3877
4120
  "stackwright-pro-designer-otter": [
3878
4121
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
@@ -3887,8 +4130,11 @@ var OTTER_WRITE_ALLOWLISTS = {
3887
4130
  ],
3888
4131
  "stackwright-pro-auth-otter": [
3889
4132
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
3890
- { prefix: "config/", suffix: ".yml", description: "Auth YAML config" },
3891
- { prefix: "config/", suffix: ".yaml", description: "Auth YAML config" },
4133
+ {
4134
+ prefix: "stackwright.auth.",
4135
+ suffix: ".yml",
4136
+ description: "Auth config YAML (stackwright.auth.yml \u2014 compiled to _auth.json)"
4137
+ },
3892
4138
  {
3893
4139
  prefix: ".env",
3894
4140
  suffix: "",
@@ -3907,7 +4153,11 @@ var OTTER_WRITE_ALLOWLISTS = {
3907
4153
  ],
3908
4154
  "stackwright-pro-data-otter": [
3909
4155
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Data config artifact" },
3910
- { prefix: "stackwright.yml", suffix: "", description: "Stackwright config" }
4156
+ {
4157
+ prefix: "stackwright.collections.",
4158
+ suffix: ".yml",
4159
+ description: "Collections config YAML (stackwright.collections.yml \u2014 compiled to _collections.json)"
4160
+ }
3911
4161
  ],
3912
4162
  "stackwright-pro-page-otter": [
3913
4163
  { prefix: "pages/", suffix: "/content.yml", description: "Page content YAML" },
@@ -3919,7 +4169,7 @@ var OTTER_WRITE_ALLOWLISTS = {
3919
4169
  { prefix: "pages/", suffix: "/content.yaml", description: "Dashboard content YAML" },
3920
4170
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Dashboard manifest" }
3921
4171
  ],
3922
- "stackwright-pro-workflow-otter": [
4172
+ "stackwright-pro-form-wizard-otter": [
3923
4173
  { prefix: "workflows/", suffix: ".yml", description: "Workflow definition" },
3924
4174
  { prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
3925
4175
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
@@ -3933,7 +4183,12 @@ var OTTER_WRITE_ALLOWLISTS = {
3933
4183
  }
3934
4184
  ],
3935
4185
  "stackwright-pro-api-otter": [
3936
- { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" }
4186
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" },
4187
+ {
4188
+ prefix: "stackwright.integrations.",
4189
+ suffix: ".yml",
4190
+ description: "Integrations config YAML (stackwright.integrations.yml \u2014 compiled to _integrations.json)"
4191
+ }
3937
4192
  ],
3938
4193
  "stackwright-pro-geo-otter": [
3939
4194
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Geo manifest artifact" },
@@ -4012,22 +4267,22 @@ function extractContentTypes(yamlContent) {
4012
4267
  return types.length > 0 ? types : ["unknown"];
4013
4268
  }
4014
4269
  function readPageRegistry(cwd) {
4015
- const regPath = join5(cwd, PAGE_REGISTRY_FILE);
4270
+ const regPath = join6(cwd, PAGE_REGISTRY_FILE);
4016
4271
  if (!existsSync6(regPath)) return {};
4017
4272
  try {
4018
- const stat = lstatSync6(regPath);
4273
+ const stat = lstatSync7(regPath);
4019
4274
  if (stat.isSymbolicLink()) return {};
4020
- return JSON.parse(readFileSync5(regPath, "utf-8"));
4275
+ return JSON.parse(readFileSync6(regPath, "utf-8"));
4021
4276
  } catch {
4022
4277
  return {};
4023
4278
  }
4024
4279
  }
4025
4280
  function writePageRegistry(cwd, registry) {
4026
- const dir = join5(cwd, ".stackwright");
4281
+ const dir = join6(cwd, ".stackwright");
4027
4282
  mkdirSync5(dir, { recursive: true });
4028
- const regPath = join5(cwd, PAGE_REGISTRY_FILE);
4283
+ const regPath = join6(cwd, PAGE_REGISTRY_FILE);
4029
4284
  if (existsSync6(regPath)) {
4030
- const stat = lstatSync6(regPath);
4285
+ const stat = lstatSync7(regPath);
4031
4286
  if (stat.isSymbolicLink()) {
4032
4287
  throw new Error("Refusing to write page registry through symlink");
4033
4288
  }
@@ -4047,8 +4302,8 @@ function checkPageOwnership(cwd, slug, callerOtter) {
4047
4302
  };
4048
4303
  }
4049
4304
  var NEXTJS_BRACKET_SEGMENT_RE = /\[\[\.{3}[a-zA-Z_][a-zA-Z0-9_]*\]\]|\[\.{3}[a-zA-Z_][a-zA-Z0-9_]*\]|\[[a-zA-Z_][a-zA-Z0-9_]*\]/g;
4050
- function stripNextjsBracketSegments(path3) {
4051
- return path3.replace(NEXTJS_BRACKET_SEGMENT_RE, "");
4305
+ function stripNextjsBracketSegments(path4) {
4306
+ return path4.replace(NEXTJS_BRACKET_SEGMENT_RE, "");
4052
4307
  }
4053
4308
  function checkPathAllowed(callerOtter, filePath) {
4054
4309
  const normalized = normalize(filePath);
@@ -4198,10 +4453,10 @@ function handleSafeWrite(input) {
4198
4453
  return { text: JSON.stringify(result), isError: true };
4199
4454
  }
4200
4455
  const normalized = normalize(filePath);
4201
- const fullPath = join5(cwd, normalized);
4456
+ const fullPath = join6(cwd, normalized);
4202
4457
  if (existsSync6(fullPath)) {
4203
4458
  try {
4204
- const stat = lstatSync6(fullPath);
4459
+ const stat = lstatSync7(fullPath);
4205
4460
  if (stat.isSymbolicLink()) {
4206
4461
  const result = {
4207
4462
  success: false,
@@ -4304,8 +4559,8 @@ function registerSafeWriteTools(server2) {
4304
4559
 
4305
4560
  // src/tools/auth.ts
4306
4561
  import { z as z15 } from "zod";
4307
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
4308
- import { join as join6 } from "path";
4562
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
4563
+ import { join as join7 } from "path";
4309
4564
  function buildHierarchy(roles) {
4310
4565
  const h = {};
4311
4566
  for (let i = 0; i < roles.length - 1; i++) {
@@ -4319,17 +4574,22 @@ function hierarchyToYaml(hierarchy, indent) {
4319
4574
  return entries.map(([role, subs]) => `${indent}${role}: [${subs.join(", ")}]`).join("\n");
4320
4575
  }
4321
4576
  function rolesToYaml(roles, indent) {
4322
- return roles.map((r) => `${indent}- ${r}`).join("\n");
4577
+ return roles.map((r) => `${indent}- name: ${r}`).join("\n");
4578
+ }
4579
+ function normalizeRoutes(routes, defaultRole) {
4580
+ return routes.map(
4581
+ (r) => typeof r === "string" ? { pattern: r, requiredRole: defaultRole } : { pattern: r.pattern, requiredRole: r.requiredRole ?? defaultRole }
4582
+ );
4323
4583
  }
4324
- function routesToYaml(routes, defaultRole, indent) {
4325
- return routes.map((r) => `${indent}- pattern: ${r}
4326
- ${indent} requiredRole: ${defaultRole}`).join("\n");
4584
+ function routesToYaml(routes, indent) {
4585
+ return routes.map((r) => `${indent}- pattern: ${r.pattern}
4586
+ ${indent} requiredRole: ${r.requiredRole}`).join("\n");
4327
4587
  }
4328
4588
  function detectNextMajorVersion(cwd) {
4329
4589
  try {
4330
- const pkgPath = join6(cwd, "package.json");
4590
+ const pkgPath = join7(cwd, "package.json");
4331
4591
  if (!existsSync7(pkgPath)) return null;
4332
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
4592
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
4333
4593
  const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
4334
4594
  if (!nextVersion) return null;
4335
4595
  const match = nextVersion.match(/(\d+)/);
@@ -4338,25 +4598,6 @@ function detectNextMajorVersion(cwd) {
4338
4598
  return null;
4339
4599
  }
4340
4600
  }
4341
- function upsertAuthBlock(existing, authYaml) {
4342
- const lines = existing.split("\n");
4343
- let authStart = -1;
4344
- let authEnd = lines.length;
4345
- for (let i = 0; i < lines.length; i++) {
4346
- if (/^auth:/.test(lines[i])) {
4347
- authStart = i;
4348
- } else if (authStart >= 0 && i > authStart && /^\S/.test(lines[i]) && lines[i].trim() !== "") {
4349
- authEnd = i;
4350
- break;
4351
- }
4352
- }
4353
- if (authStart < 0) {
4354
- return existing.trimEnd() + "\n" + authYaml + "\n";
4355
- }
4356
- const before = lines.slice(0, authStart);
4357
- const after = lines.slice(authEnd);
4358
- return [...before, ...authYaml.trimEnd().split("\n"), ...after.length ? after : []].join("\n");
4359
- }
4360
4601
  function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4361
4602
  const rbacBlock = ` rbac: {
4362
4603
  roles: ${JSON.stringify(roles)},
@@ -4367,9 +4608,10 @@ function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy
4367
4608
  enabled: ${auditEnabled},
4368
4609
  retentionDays: ${auditRetentionDays},
4369
4610
  },`;
4370
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4611
+ const patterns = protectedRoutes.map((r) => r.pattern);
4612
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4371
4613
  const configBlock = `export const config = {
4372
- matcher: ${JSON.stringify(protectedRoutes)},
4614
+ matcher: ${JSON.stringify(patterns)},
4373
4615
  };`;
4374
4616
  if (method === "cac") {
4375
4617
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
@@ -4452,9 +4694,10 @@ function generateProxyContent(method, params, roles, defaultRole, hierarchy, aud
4452
4694
  enabled: ${auditEnabled},
4453
4695
  retentionDays: ${auditRetentionDays},
4454
4696
  },`;
4455
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4697
+ const patterns = protectedRoutes.map((r) => r.pattern);
4698
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4456
4699
  const configBlock = `export const config = {
4457
- matcher: ${JSON.stringify(protectedRoutes)},
4700
+ matcher: ${JSON.stringify(patterns)},
4458
4701
  };`;
4459
4702
  if (method === "cac") {
4460
4703
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
@@ -4554,36 +4797,33 @@ OAUTH2_CLIENT_SECRET=your-client-secret
4554
4797
  `;
4555
4798
  }
4556
4799
  function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
4557
- const rbacSection = ` rbac:
4558
- roles:
4559
- ${rolesToYaml(roles, " ")}
4560
- defaultRole: ${defaultRole}
4561
- hierarchy:
4562
- ${hierarchyToYaml(hierarchy, " ")}`;
4563
- const auditSection = ` audit:
4564
- enabled: ${auditEnabled}
4565
- retentionDays: ${auditRetentionDays}`;
4566
- const routesSection = ` protectedRoutes:
4567
- ${routesToYaml(protectedRoutes, " ", defaultRole)}`.replace(/\n\s+,/g, "");
4568
- const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4569
- requiredRole: ${defaultRole}`).join("\n");
4570
- const providerLine = params.provider ? ` provider: ${params.provider}
4800
+ const rbacSection = `rbac:
4801
+ roles:
4802
+ ${rolesToYaml(roles, " ")}
4803
+ defaultRole: ${defaultRole}
4804
+ hierarchy:
4805
+ ${hierarchyToYaml(hierarchy, " ")}`;
4806
+ const auditSection = `audit:
4807
+ enabled: ${auditEnabled}
4808
+ retentionDays: ${auditRetentionDays}`;
4809
+ const routeLines = routesToYaml(protectedRoutes, " ");
4810
+ const providerLine = params.provider ? `provider: ${params.provider}
4571
4811
  ` : "";
4572
4812
  if (method === "cac") {
4573
4813
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4574
4814
  const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4575
4815
  const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4576
4816
  const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4577
- return `auth:
4578
- method: cac
4579
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4580
- cac:
4581
- caBundle: \${CAC_CA_BUNDLE}
4582
- edipiLookup: ${edipiLookup}
4583
- ocspEndpoint: \${CAC_OCSP_ENDPOINT}
4584
- certHeader: ${certHeader}
4817
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4818
+ method: cac
4819
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4820
+ cac:
4821
+ caBundle: \${CAC_CA_BUNDLE}
4822
+ edipiLookup: ${edipiLookup}
4823
+ ocspEndpoint: \${CAC_OCSP_ENDPOINT}
4824
+ certHeader: ${certHeader}
4585
4825
  ${rbacSection}
4586
- protectedRoutes:
4826
+ protectedRoutes:
4587
4827
  ${routeLines}
4588
4828
  ${auditSection}
4589
4829
  `;
@@ -4591,33 +4831,33 @@ ${auditSection}
4591
4831
  if (method === "oidc") {
4592
4832
  const scopes2 = params.oidcScopes ?? "openid profile email";
4593
4833
  const roleClaim = params.oidcRoleClaim ?? "roles";
4594
- return `auth:
4595
- method: oidc
4596
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4597
- oidc:
4598
- discoveryUrl: \${OIDC_DISCOVERY_URL}
4599
- clientId: \${OIDC_CLIENT_ID}
4600
- clientSecret: \${OIDC_CLIENT_SECRET}
4601
- scopes: ${scopes2}
4602
- roleClaim: ${roleClaim}
4834
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4835
+ method: oidc
4836
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4837
+ oidc:
4838
+ discoveryUrl: \${OIDC_DISCOVERY_URL}
4839
+ clientId: \${OIDC_CLIENT_ID}
4840
+ clientSecret: \${OIDC_CLIENT_SECRET}
4841
+ scopes: ${scopes2}
4842
+ roleClaim: ${roleClaim}
4603
4843
  ${rbacSection}
4604
- protectedRoutes:
4844
+ protectedRoutes:
4605
4845
  ${routeLines}
4606
4846
  ${auditSection}
4607
4847
  `;
4608
4848
  }
4609
4849
  const scopes = params.oauth2Scopes ?? "read write";
4610
- return `auth:
4611
- method: oauth2
4612
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4613
- oauth2:
4614
- authorizationUrl: \${OAUTH2_AUTH_URL}
4615
- tokenUrl: \${OAUTH2_TOKEN_URL}
4616
- clientId: \${OAUTH2_CLIENT_ID}
4617
- clientSecret: \${OAUTH2_CLIENT_SECRET}
4618
- scopes: ${scopes}
4850
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4851
+ method: oauth2
4852
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4853
+ oauth2:
4854
+ authorizationUrl: \${OAUTH2_AUTH_URL}
4855
+ tokenUrl: \${OAUTH2_TOKEN_URL}
4856
+ clientId: \${OAUTH2_CLIENT_ID}
4857
+ clientSecret: \${OAUTH2_CLIENT_SECRET}
4858
+ scopes: ${scopes}
4619
4859
  ${rbacSection}
4620
- protectedRoutes:
4860
+ protectedRoutes:
4621
4861
  ${routeLines}
4622
4862
  ${auditSection}
4623
4863
  `;
@@ -4632,9 +4872,10 @@ function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hi
4632
4872
  enabled: ${auditEnabled},
4633
4873
  retentionDays: ${auditRetentionDays},
4634
4874
  },`;
4635
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4875
+ const patterns = protectedRoutes.map((r) => r.pattern);
4876
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4636
4877
  const configBlock = `export const config = {
4637
- matcher: ${JSON.stringify(protectedRoutes)},
4878
+ matcher: ${JSON.stringify(patterns)},
4638
4879
  };`;
4639
4880
  const devHeader = [
4640
4881
  "// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
@@ -4721,9 +4962,10 @@ function generateDevOnlyProxyContent(method, params, roles, defaultRole, hierarc
4721
4962
  enabled: ${auditEnabled},
4722
4963
  retentionDays: ${auditRetentionDays},
4723
4964
  },`;
4724
- const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4965
+ const patterns = protectedRoutes.map((r) => r.pattern);
4966
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(patterns)},`;
4725
4967
  const configBlock = `export const config = {
4726
- matcher: ${JSON.stringify(protectedRoutes)},
4968
+ matcher: ${JSON.stringify(patterns)},
4727
4969
  };`;
4728
4970
  const devHeader = [
4729
4971
  "// proxy.ts -- generated by @stackwright-pro/auth (Next.js >=16, dev-only mock)",
@@ -4801,35 +5043,35 @@ ${configBlock}
4801
5043
  `;
4802
5044
  }
4803
5045
  function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
4804
- const rbacSection = ` rbac:
4805
- roles:
4806
- ${rolesToYaml(roles, " ")}
4807
- defaultRole: ${defaultRole}
4808
- hierarchy:
4809
- ${hierarchyToYaml(hierarchy, " ")}`;
4810
- const auditSection = ` audit:
4811
- enabled: ${auditEnabled}
4812
- retentionDays: ${auditRetentionDays}`;
4813
- const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4814
- requiredRole: ${defaultRole}`).join("\n");
4815
- const providerLine = params.provider ? ` provider: ${params.provider}
5046
+ const rbacSection = `rbac:
5047
+ roles:
5048
+ ${rolesToYaml(roles, " ")}
5049
+ defaultRole: ${defaultRole}
5050
+ hierarchy:
5051
+ ${hierarchyToYaml(hierarchy, " ")}`;
5052
+ const auditSection = `audit:
5053
+ enabled: ${auditEnabled}
5054
+ retentionDays: ${auditRetentionDays}`;
5055
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r.pattern}
5056
+ requiredRole: ${r.requiredRole}`).join("\n");
5057
+ const providerLine = params.provider ? `provider: ${params.provider}
4816
5058
  ` : "";
4817
5059
  if (method === "cac") {
4818
5060
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4819
5061
  const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4820
5062
  const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4821
5063
  const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4822
- return `auth:
4823
- method: cac
4824
- devOnly: true
4825
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4826
- cac:
4827
- caBundle: ${caBundle}
4828
- edipiLookup: ${edipiLookup}
4829
- ocspEndpoint: ${ocspEndpoint}
4830
- certHeader: ${certHeader}
5064
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5065
+ method: cac
5066
+ devOnly: true
5067
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5068
+ cac:
5069
+ caBundle: ${caBundle}
5070
+ edipiLookup: ${edipiLookup}
5071
+ ocspEndpoint: ${ocspEndpoint}
5072
+ certHeader: ${certHeader}
4831
5073
  ${rbacSection}
4832
- protectedRoutes:
5074
+ protectedRoutes:
4833
5075
  ${routeLines}
4834
5076
  ${auditSection}
4835
5077
  `;
@@ -4837,35 +5079,35 @@ ${auditSection}
4837
5079
  if (method === "oidc") {
4838
5080
  const scopes2 = params.oidcScopes ?? "openid profile email";
4839
5081
  const roleClaim = params.oidcRoleClaim ?? "roles";
4840
- return `auth:
4841
- method: oidc
4842
- devOnly: true
4843
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4844
- oidc:
4845
- discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
4846
- clientId: dev-mock-client
4847
- clientSecret: dev-mock-secret
4848
- scopes: ${scopes2}
4849
- roleClaim: ${roleClaim}
5082
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5083
+ method: oidc
5084
+ devOnly: true
5085
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5086
+ oidc:
5087
+ discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
5088
+ clientId: dev-mock-client
5089
+ clientSecret: dev-mock-secret
5090
+ scopes: ${scopes2}
5091
+ roleClaim: ${roleClaim}
4850
5092
  ${rbacSection}
4851
- protectedRoutes:
5093
+ protectedRoutes:
4852
5094
  ${routeLines}
4853
5095
  ${auditSection}
4854
5096
  `;
4855
5097
  }
4856
5098
  const scopes = params.oauth2Scopes ?? "read write";
4857
- return `auth:
4858
- method: oauth2
4859
- devOnly: true
4860
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4861
- oauth2:
4862
- authorizationUrl: https://dev-mock-oauth2/authorize
4863
- tokenUrl: https://dev-mock-oauth2/token
4864
- clientId: dev-mock-client
4865
- clientSecret: dev-mock-secret
4866
- scopes: ${scopes}
5099
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5100
+ method: oauth2
5101
+ devOnly: true
5102
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5103
+ oauth2:
5104
+ authorizationUrl: https://dev-mock-oauth2/authorize
5105
+ tokenUrl: https://dev-mock-oauth2/token
5106
+ clientId: dev-mock-client
5107
+ clientSecret: dev-mock-secret
5108
+ scopes: ${scopes}
4867
5109
  ${rbacSection}
4868
- protectedRoutes:
5110
+ protectedRoutes:
4869
5111
  ${routeLines}
4870
5112
  ${auditSection}
4871
5113
  `;
@@ -4944,6 +5186,7 @@ async function configureAuthHandler(params, cwd) {
4944
5186
  } = params;
4945
5187
  const roles = rbacRoles;
4946
5188
  const defaultRole = params.rbacDefaultRole ?? roles[roles.length - 1];
5189
+ const normalizedRoutes = normalizeRoutes(protectedRoutes, defaultRole);
4947
5190
  const hierarchy = buildHierarchy(roles);
4948
5191
  const detectedVersion = params.nextMajorVersion ?? detectNextMajorVersion(cwd);
4949
5192
  const useProxy = (detectedVersion ?? 0) >= 16;
@@ -4980,7 +5223,7 @@ async function configureAuthHandler(params, cwd) {
4980
5223
  hierarchy,
4981
5224
  auditEnabled,
4982
5225
  auditRetentionDays,
4983
- protectedRoutes
5226
+ normalizedRoutes
4984
5227
  ) : generateDevOnlyMiddlewareContent(
4985
5228
  effectiveMethod,
4986
5229
  params,
@@ -4989,7 +5232,7 @@ async function configureAuthHandler(params, cwd) {
4989
5232
  hierarchy,
4990
5233
  auditEnabled,
4991
5234
  auditRetentionDays,
4992
- protectedRoutes
5235
+ normalizedRoutes
4993
5236
  ) : useProxy ? generateProxyContent(
4994
5237
  effectiveMethod,
4995
5238
  params,
@@ -4998,7 +5241,7 @@ async function configureAuthHandler(params, cwd) {
4998
5241
  hierarchy,
4999
5242
  auditEnabled,
5000
5243
  auditRetentionDays,
5001
- protectedRoutes
5244
+ normalizedRoutes
5002
5245
  ) : generateMiddlewareContent(
5003
5246
  effectiveMethod,
5004
5247
  params,
@@ -5007,9 +5250,9 @@ async function configureAuthHandler(params, cwd) {
5007
5250
  hierarchy,
5008
5251
  auditEnabled,
5009
5252
  auditRetentionDays,
5010
- protectedRoutes
5253
+ normalizedRoutes
5011
5254
  );
5012
- writeFileSync6(join6(cwd, conventionFile), authFileContent, "utf8");
5255
+ writeFileSync6(join7(cwd, conventionFile), authFileContent, "utf8");
5013
5256
  filesWritten.push(conventionFile);
5014
5257
  } catch (err) {
5015
5258
  const msg = err instanceof Error ? err.message : String(err);
@@ -5029,9 +5272,9 @@ async function configureAuthHandler(params, cwd) {
5029
5272
  if (!devOnly) {
5030
5273
  try {
5031
5274
  const envBlock = generateEnvBlock(effectiveMethod, params);
5032
- const envPath = join6(cwd, ".env.example");
5275
+ const envPath = join7(cwd, ".env.example");
5033
5276
  if (existsSync7(envPath)) {
5034
- const existing = readFileSync6(envPath, "utf8");
5277
+ const existing = readFileSync7(envPath, "utf8");
5035
5278
  writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
5036
5279
  } else {
5037
5280
  writeFileSync6(envPath, envBlock, "utf8");
@@ -5052,10 +5295,10 @@ async function configureAuthHandler(params, cwd) {
5052
5295
  }
5053
5296
  if (devOnly) {
5054
5297
  try {
5055
- const mockAuthDir = join6(cwd, "lib");
5298
+ const mockAuthDir = join7(cwd, "lib");
5056
5299
  mkdirSync6(mockAuthDir, { recursive: true });
5057
5300
  const mockAuthContent = generateMockAuthContent(roles, mockUsers);
5058
- writeFileSync6(join6(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
5301
+ writeFileSync6(join7(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
5059
5302
  filesWritten.push("lib/mock-auth.ts");
5060
5303
  } catch (err) {
5061
5304
  const msg = err instanceof Error ? err.message : String(err);
@@ -5073,9 +5316,9 @@ async function configureAuthHandler(params, cwd) {
5073
5316
  };
5074
5317
  }
5075
5318
  try {
5076
- const pkgPath = join6(cwd, "package.json");
5319
+ const pkgPath = join7(cwd, "package.json");
5077
5320
  if (existsSync7(pkgPath)) {
5078
- const existingPkg = readFileSync6(pkgPath, "utf8");
5321
+ const existingPkg = readFileSync7(pkgPath, "utf8");
5079
5322
  const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
5080
5323
  writeFileSync6(pkgPath, updatedPkg, "utf8");
5081
5324
  filesWritten.push("package.json");
@@ -5105,7 +5348,7 @@ async function configureAuthHandler(params, cwd) {
5105
5348
  hierarchy,
5106
5349
  auditEnabled,
5107
5350
  auditRetentionDays,
5108
- protectedRoutes,
5351
+ normalizedRoutes,
5109
5352
  useProxy
5110
5353
  ) : generateYamlBlock(
5111
5354
  effectiveMethod,
@@ -5115,24 +5358,21 @@ async function configureAuthHandler(params, cwd) {
5115
5358
  hierarchy,
5116
5359
  auditEnabled,
5117
5360
  auditRetentionDays,
5118
- protectedRoutes,
5361
+ normalizedRoutes,
5119
5362
  useProxy
5120
5363
  );
5121
- const ymlPath = join6(cwd, "stackwright.yml");
5122
- if (!existsSync7(ymlPath)) {
5123
- writeFileSync6(ymlPath, authYaml, "utf8");
5124
- } else {
5125
- const existing = readFileSync6(ymlPath, "utf8");
5126
- writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
5127
- }
5128
- filesWritten.push("stackwright.yml");
5364
+ writeFileSync6(join7(cwd, "stackwright.auth.yml"), authYaml, "utf8");
5365
+ filesWritten.push("stackwright.auth.yml");
5129
5366
  } catch (err) {
5130
5367
  const msg = err instanceof Error ? err.message : String(err);
5131
5368
  return {
5132
5369
  content: [
5133
5370
  {
5134
5371
  type: "text",
5135
- text: JSON.stringify({ success: false, error: `Failed writing stackwright.yml: ${msg}` })
5372
+ text: JSON.stringify({
5373
+ success: false,
5374
+ error: `Failed writing stackwright.auth.yml: ${msg}`
5375
+ })
5136
5376
  }
5137
5377
  ],
5138
5378
  isError: true
@@ -5177,7 +5417,7 @@ async function configureAuthHandler(params, cwd) {
5177
5417
  function registerAuthTools(server2) {
5178
5418
  server2.tool(
5179
5419
  "stackwright_pro_configure_auth",
5180
- "Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` from a secure template, appends/updates the `auth:` section in `stackwright.yml`, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
5420
+ "Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` (or `proxy.ts` for Next.js >=16) from a secure template, writes a standalone `stackwright.auth.yml` sibling file, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
5181
5421
  {
5182
5422
  method: z15.enum(["cac", "oidc", "oauth2", "none"]),
5183
5423
  provider: z15.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
@@ -5204,8 +5444,15 @@ function registerAuthTools(server2) {
5204
5444
  // Audit
5205
5445
  auditEnabled: boolCoerce(z15.boolean().optional()),
5206
5446
  auditRetentionDays: numCoerce(z15.number().int().positive().optional()),
5207
- // Routes
5208
- protectedRoutes: jsonCoerce(z15.array(z15.string()).optional()),
5447
+ // Routes — accepts bare strings (backward-compat) or per-route objects (swp-owu0)
5448
+ protectedRoutes: jsonCoerce(
5449
+ z15.array(
5450
+ z15.union([
5451
+ z15.string(),
5452
+ z15.object({ pattern: z15.string(), requiredRole: z15.string().optional() })
5453
+ ])
5454
+ ).optional()
5455
+ ),
5209
5456
  // Injection for tests
5210
5457
  _cwd: z15.string().optional(),
5211
5458
  devOnly: boolCoerce(z15.boolean().optional()),
@@ -5221,28 +5468,28 @@ function registerAuthTools(server2) {
5221
5468
 
5222
5469
  // src/integrity.ts
5223
5470
  import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
5224
- import { readFileSync as readFileSync7, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
5225
- import { join as join7, basename } from "path";
5471
+ import { readFileSync as readFileSync8, readdirSync as readdirSync3, lstatSync as lstatSync8 } from "fs";
5472
+ import { join as join8, basename } from "path";
5226
5473
  var _checksums = /* @__PURE__ */ new Map([
5227
5474
  [
5228
5475
  "stackwright-pro-api-otter.json",
5229
- "df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd"
5476
+ "822b35d7a330ed8ac0b42a63b0f70a41885aa9b5ea23cc5b8b998b549da4c05c"
5230
5477
  ],
5231
5478
  [
5232
5479
  "stackwright-pro-auth-otter.json",
5233
- "07134125ab4f8c82ee015f27587e4d84bdeefca5ec211d1563bcb25b8aa61eb3"
5480
+ "d6cd5732667018d99456be77d5955e454da7a0999a0330b5f03a483aa1b9b33f"
5234
5481
  ],
5235
5482
  [
5236
5483
  "stackwright-pro-dashboard-otter.json",
5237
- "160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01"
5484
+ "e3b82555fcffbd77285bd304828814e9f9f7257130797c9de3785de97527668c"
5238
5485
  ],
5239
5486
  [
5240
5487
  "stackwright-pro-data-otter.json",
5241
- "709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25"
5488
+ "bb66eaab31c6872536dca4d3ff145724a46356946dd0665cc4f0346714d3bacb"
5242
5489
  ],
5243
5490
  [
5244
5491
  "stackwright-pro-designer-otter.json",
5245
- "1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9"
5492
+ "b54121c6a2ab5b1ad68c1262c58b2e04e6315feacd6bb8de8e78eb36f2fa6be6"
5246
5493
  ],
5247
5494
  [
5248
5495
  "stackwright-pro-domain-expert-otter.json",
@@ -5250,35 +5497,35 @@ var _checksums = /* @__PURE__ */ new Map([
5250
5497
  ],
5251
5498
  [
5252
5499
  "stackwright-pro-foreman-otter.json",
5253
- "438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
5500
+ "abb1cc5e40a8c5ff98057273f43a9e90e2791a15951090cd4d98407c0c3618e3"
5501
+ ],
5502
+ [
5503
+ "stackwright-pro-form-wizard-otter.json",
5504
+ "975ad68e8fb6fe5a10373be278946fa4d9d7ec2238a0b2bd9e4a412e5b1fdd6d"
5254
5505
  ],
5255
5506
  [
5256
5507
  "stackwright-pro-geo-otter.json",
5257
- "2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
5508
+ "ac440da786d8c5ab1feddbf861449c3be3a433a04370578e1b2d35bf6ae7a601"
5258
5509
  ],
5259
5510
  [
5260
5511
  "stackwright-pro-page-otter.json",
5261
- "0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03"
5512
+ "b70bd6e5d2a40230f7c24ff4a6113585a1614e5b6a215ece2bd47ff053c30f58"
5262
5513
  ],
5263
5514
  [
5264
5515
  "stackwright-pro-polish-otter.json",
5265
- "bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3"
5516
+ "48ef3cf7f29ac6b5e8ab2004d4d41e2fb899e781689178d3905993241c84095a"
5266
5517
  ],
5267
5518
  [
5268
5519
  "stackwright-pro-scaffold-otter.json",
5269
- "91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a"
5520
+ "e8662b63bbc9ce38851d94f9656bbebefdb7843d038862adb9fde2c23f9e77c2"
5270
5521
  ],
5271
5522
  [
5272
5523
  "stackwright-pro-theme-otter.json",
5273
- "fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
5274
- ],
5275
- [
5276
- "stackwright-pro-workflow-otter.json",
5277
- "5f6209fadc1355580e2455a16386f06bd7d5814f047b2dd5b33800abf6a48ff8"
5524
+ "fb62e56642f7f94c50ce173f3e1ce978b3f20ab1567e87403b901d6dd991a7db"
5278
5525
  ],
5279
5526
  [
5280
5527
  "stackwright-services-otter.json",
5281
- "c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
5528
+ "0a5dac670482871c718099767b30bf23d8ec57e942bd40a00ce295926d82c6bd"
5282
5529
  ]
5283
5530
  ]);
5284
5531
  Object.freeze(_checksums);
@@ -5307,7 +5554,7 @@ function verifyOtterFile(filePath) {
5307
5554
  }
5308
5555
  let stat;
5309
5556
  try {
5310
- stat = lstatSync7(filePath);
5557
+ stat = lstatSync8(filePath);
5311
5558
  } catch (err) {
5312
5559
  const msg = err instanceof Error ? err.message : String(err);
5313
5560
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -5325,7 +5572,7 @@ function verifyOtterFile(filePath) {
5325
5572
  }
5326
5573
  let raw;
5327
5574
  try {
5328
- raw = readFileSync7(filePath);
5575
+ raw = readFileSync8(filePath);
5329
5576
  } catch (err) {
5330
5577
  const msg = err instanceof Error ? err.message : String(err);
5331
5578
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -5375,7 +5622,7 @@ function verifyAllOtters(otterDir) {
5375
5622
  const unknown = [];
5376
5623
  let entries;
5377
5624
  try {
5378
- entries = readdirSync2(otterDir);
5625
+ entries = readdirSync3(otterDir);
5379
5626
  } catch (err) {
5380
5627
  const msg = err instanceof Error ? err.message : String(err);
5381
5628
  return {
@@ -5386,9 +5633,9 @@ function verifyAllOtters(otterDir) {
5386
5633
  }
5387
5634
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
5388
5635
  for (const filename of otterFiles) {
5389
- const filePath = join7(otterDir, filename);
5636
+ const filePath = join8(otterDir, filename);
5390
5637
  try {
5391
- if (lstatSync7(filePath).isSymbolicLink()) {
5638
+ if (lstatSync8(filePath).isSymbolicLink()) {
5392
5639
  failed.push({ filename, error: "Skipped: symlink" });
5393
5640
  continue;
5394
5641
  }
@@ -5414,9 +5661,9 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
5414
5661
  function resolveOtterDir() {
5415
5662
  const cwd = process.cwd();
5416
5663
  for (const relative of DEFAULT_SEARCH_PATHS) {
5417
- const candidate = join7(cwd, relative);
5664
+ const candidate = join8(cwd, relative);
5418
5665
  try {
5419
- lstatSync7(candidate);
5666
+ lstatSync8(candidate);
5420
5667
  return candidate;
5421
5668
  } catch {
5422
5669
  }
@@ -5495,13 +5742,13 @@ function registerIntegrityTools(server2) {
5495
5742
 
5496
5743
  // src/tools/domain.ts
5497
5744
  import { z as z16 } from "zod";
5498
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
5499
- import { join as join8 } from "path";
5745
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
5746
+ import { join as join9 } from "path";
5500
5747
  function handleListCollections(input) {
5501
5748
  const cwd = input._cwd ?? process.cwd();
5502
5749
  const sources = [
5503
5750
  {
5504
- path: join8(cwd, ".stackwright", "artifacts", "data-config.json"),
5751
+ path: join9(cwd, ".stackwright", "artifacts", "data-config.json"),
5505
5752
  source: "data-config.json",
5506
5753
  parse: (raw) => {
5507
5754
  const parsed = JSON.parse(raw);
@@ -5512,15 +5759,15 @@ function handleListCollections(input) {
5512
5759
  }
5513
5760
  },
5514
5761
  {
5515
- path: join8(cwd, "stackwright.yml"),
5762
+ path: join9(cwd, "stackwright.yml"),
5516
5763
  source: "stackwright.yml",
5517
5764
  parse: extractCollectionsFromYaml
5518
5765
  }
5519
5766
  ];
5520
- for (const { path: path3, source, parse } of sources) {
5521
- if (!existsSync8(path3)) continue;
5767
+ for (const { path: path4, source, parse } of sources) {
5768
+ if (!existsSync8(path4)) continue;
5522
5769
  try {
5523
- const collections = parse(readFileSync8(path3, "utf8"));
5770
+ const collections = parse(readFileSync9(path4, "utf8"));
5524
5771
  return {
5525
5772
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
5526
5773
  isError: false
@@ -5671,7 +5918,7 @@ function handleValidateWorkflow(input) {
5671
5918
  if (input.workflow && Object.keys(input.workflow).length > 0) {
5672
5919
  raw = input.workflow;
5673
5920
  } else {
5674
- const artifactPath = join8(cwd, ".stackwright", "artifacts", "workflow-config.json");
5921
+ const artifactPath = join9(cwd, ".stackwright", "artifacts", "workflow-config.json");
5675
5922
  if (!existsSync8(artifactPath)) {
5676
5923
  return fail([
5677
5924
  {
@@ -5681,7 +5928,7 @@ function handleValidateWorkflow(input) {
5681
5928
  ]);
5682
5929
  }
5683
5930
  try {
5684
- raw = JSON.parse(readFileSync8(artifactPath, "utf8"));
5931
+ raw = JSON.parse(readFileSync9(artifactPath, "utf8"));
5685
5932
  } catch (err) {
5686
5933
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
5687
5934
  }
@@ -5793,12 +6040,12 @@ function handleValidateWorkflow(input) {
5793
6040
  return { text: JSON.stringify({ valid: errors.length === 0, errors, warnings }), isError: false };
5794
6041
  }
5795
6042
  function validateTransitionTargets(step, stepId, stepIds, errors) {
5796
- const check = (target, path3) => {
6043
+ const check = (target, path4) => {
5797
6044
  if (typeof target === "string" && !stepIds.has(target)) {
5798
6045
  errors.push({
5799
6046
  code: "ORPHANED_TRANSITION",
5800
6047
  message: `Step "${stepId}" transitions to "${target}" which does not exist`,
5801
- path: path3
6048
+ path: path4
5802
6049
  });
5803
6050
  }
5804
6051
  };
@@ -5837,11 +6084,11 @@ function validateTransitionTargets(step, stepId, stepIds, errors) {
5837
6084
  }
5838
6085
  function collectServiceWarnings(step, stepId, warnings) {
5839
6086
  const isService = (val) => typeof val === "string" && val.startsWith("service:");
5840
- const warn = (path3) => {
6087
+ const warn = (path4) => {
5841
6088
  warnings.push({
5842
6089
  code: "WARN_SERVICE_REFERENCE",
5843
- message: `service: reference at "${path3}" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.`,
5844
- path: path3
6090
+ message: `service: reference at "${path4}" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.`,
6091
+ path: path4
5845
6092
  });
5846
6093
  };
5847
6094
  const onSubmit = step.on_submit;
@@ -5919,7 +6166,7 @@ function buildTypeSchemaSummary() {
5919
6166
  "TransitionConditionSchema",
5920
6167
  "PersistenceSchema"
5921
6168
  ],
5922
- otter: "stackwright-pro-workflow-otter",
6169
+ otter: "stackwright-pro-form-wizard-otter",
5923
6170
  artifactKey: "workflowConfig"
5924
6171
  },
5925
6172
  auth: {
@@ -5991,15 +6238,15 @@ function registerTypeSchemasTool(server2) {
5991
6238
  // src/tools/scaffold-cleanup.ts
5992
6239
  import {
5993
6240
  existsSync as existsSync9,
5994
- lstatSync as lstatSync8,
6241
+ lstatSync as lstatSync9,
5995
6242
  unlinkSync as unlinkSync2,
5996
- readFileSync as readFileSync9,
6243
+ readFileSync as readFileSync10,
5997
6244
  writeFileSync as writeFileSync7,
5998
- readdirSync as readdirSync3,
6245
+ readdirSync as readdirSync4,
5999
6246
  rmdirSync,
6000
6247
  mkdirSync as mkdirSync7
6001
6248
  } from "fs";
6002
- import { join as join9, dirname as dirname2 } from "path";
6249
+ import { join as join10, dirname as dirname2 } from "path";
6003
6250
  var SCAFFOLD_FILES_TO_DELETE = [
6004
6251
  "content/posts/getting-started.yaml",
6005
6252
  "content/posts/hello-world.yaml"
@@ -6024,12 +6271,12 @@ var FALLBACK_COLORS = {
6024
6271
  mutedForeground: "#737373"
6025
6272
  };
6026
6273
  function readThemeColors(cwd) {
6027
- const tokenPath = join9(cwd, ".stackwright", "artifacts", "theme-tokens.json");
6274
+ const tokenPath = join10(cwd, ".stackwright", "artifacts", "theme-tokens.json");
6028
6275
  if (!existsSync9(tokenPath)) return { ...FALLBACK_COLORS };
6029
- const stat = lstatSync8(tokenPath);
6276
+ const stat = lstatSync9(tokenPath);
6030
6277
  if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
6031
6278
  try {
6032
- const raw = JSON.parse(readFileSync9(tokenPath, "utf-8"));
6279
+ const raw = JSON.parse(readFileSync10(tokenPath, "utf-8"));
6033
6280
  const css = raw?.cssVariables ?? {};
6034
6281
  const toHsl = (hslValue) => {
6035
6282
  if (!hslValue || typeof hslValue !== "string") return null;
@@ -6090,14 +6337,14 @@ function generateNotFoundPage(colors) {
6090
6337
  }
6091
6338
  function isSafePath(fullPath) {
6092
6339
  if (!existsSync9(fullPath)) return true;
6093
- const stat = lstatSync8(fullPath);
6340
+ const stat = lstatSync9(fullPath);
6094
6341
  return !stat.isSymbolicLink();
6095
6342
  }
6096
6343
  function isDirEmpty(dirPath) {
6097
6344
  if (!existsSync9(dirPath)) return false;
6098
- const stat = lstatSync8(dirPath);
6345
+ const stat = lstatSync9(dirPath);
6099
6346
  if (!stat.isDirectory()) return false;
6100
- return readdirSync3(dirPath).length === 0;
6347
+ return readdirSync4(dirPath).length === 0;
6101
6348
  }
6102
6349
  function handleCleanupScaffold(_cwd) {
6103
6350
  const cwd = _cwd ?? process.cwd();
@@ -6110,7 +6357,7 @@ function handleCleanupScaffold(_cwd) {
6110
6357
  cleanupWarnings: []
6111
6358
  };
6112
6359
  for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
6113
- const fullPath = join9(cwd, relPath);
6360
+ const fullPath = join10(cwd, relPath);
6114
6361
  if (!existsSync9(fullPath)) {
6115
6362
  result.skipped.push(relPath);
6116
6363
  continue;
@@ -6128,13 +6375,13 @@ function handleCleanupScaffold(_cwd) {
6128
6375
  }
6129
6376
  {
6130
6377
  const relPath = "pages/getting-started/content.yml";
6131
- const fullPath = join9(cwd, relPath);
6378
+ const fullPath = join10(cwd, relPath);
6132
6379
  if (!existsSync9(fullPath)) {
6133
6380
  result.skipped.push(relPath);
6134
6381
  } else if (!isSafePath(fullPath)) {
6135
6382
  result.errors.push(`Refusing to delete symlink: ${relPath}`);
6136
6383
  } else {
6137
- const content = readFileSync9(fullPath, "utf-8");
6384
+ const content = readFileSync10(fullPath, "utf-8");
6138
6385
  const isScaffoldDefault = GETTING_STARTED_SCAFFOLD_FINGERPRINTS.some(
6139
6386
  (marker) => content.includes(marker)
6140
6387
  );
@@ -6154,8 +6401,8 @@ function handleCleanupScaffold(_cwd) {
6154
6401
  }
6155
6402
  {
6156
6403
  const relPath = `content/posts/${POSTS_COLLECTION_FILE}`;
6157
- const fullPath = join9(cwd, relPath);
6158
- const postsDir = join9(cwd, "content/posts");
6404
+ const fullPath = join10(cwd, relPath);
6405
+ const postsDir = join10(cwd, "content/posts");
6159
6406
  if (!existsSync9(fullPath)) {
6160
6407
  result.skipped.push(relPath);
6161
6408
  } else if (!isSafePath(fullPath)) {
@@ -6163,7 +6410,7 @@ function handleCleanupScaffold(_cwd) {
6163
6410
  } else if (!existsSync9(postsDir)) {
6164
6411
  result.skipped.push(relPath);
6165
6412
  } else {
6166
- const userPostFiles = readdirSync3(postsDir).filter(
6413
+ const userPostFiles = readdirSync4(postsDir).filter(
6167
6414
  (f) => f !== POSTS_COLLECTION_FILE && POST_FILE_EXTENSIONS.some((ext) => f.endsWith(ext))
6168
6415
  );
6169
6416
  if (userPostFiles.length === 0) {
@@ -6181,7 +6428,7 @@ function handleCleanupScaffold(_cwd) {
6181
6428
  }
6182
6429
  }
6183
6430
  for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
6184
- const fullDir = join9(cwd, relDir);
6431
+ const fullDir = join10(cwd, relDir);
6185
6432
  if (!existsSync9(fullDir)) continue;
6186
6433
  if (!isSafePath(fullDir)) {
6187
6434
  result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
@@ -6197,7 +6444,7 @@ function handleCleanupScaffold(_cwd) {
6197
6444
  }
6198
6445
  }
6199
6446
  {
6200
- const fullPath = join9(cwd, NOT_FOUND_PATH);
6447
+ const fullPath = join10(cwd, NOT_FOUND_PATH);
6201
6448
  if (!existsSync9(fullPath)) {
6202
6449
  result.skipped.push(NOT_FOUND_PATH);
6203
6450
  } else if (!isSafePath(fullPath)) {
@@ -6475,11 +6722,256 @@ function registerContrastTools(server2) {
6475
6722
  );
6476
6723
  }
6477
6724
 
6725
+ // src/tools/compile.ts
6726
+ import { z as z19 } from "zod";
6727
+ import fs2 from "fs";
6728
+ import path3 from "path";
6729
+ import { compileCollections } from "@stackwright-pro/pulse/server";
6730
+ import { compileAuth } from "@stackwright-pro/auth-nextjs/server";
6731
+ import { compileIntegrations } from "@stackwright-pro/openapi/server";
6732
+ function buildContext(projectRoot) {
6733
+ return {
6734
+ projectRoot,
6735
+ contentOutDir: path3.join(projectRoot, "public", "stackwright-content"),
6736
+ imagesDir: path3.join(projectRoot, "public", "images"),
6737
+ siteConfig: loadSiteConfig(projectRoot)
6738
+ };
6739
+ }
6740
+ function loadSiteConfig(projectRoot) {
6741
+ try {
6742
+ const sitePath = path3.join(projectRoot, "public", "stackwright-content", "_site.json");
6743
+ return JSON.parse(fs2.readFileSync(sitePath, "utf8"));
6744
+ } catch {
6745
+ return {};
6746
+ }
6747
+ }
6748
+ function formatDuration(startMs) {
6749
+ const ms = Date.now() - startMs;
6750
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(2)}s`;
6751
+ }
6752
+ async function tryOssCompile(fn, ctx, extra) {
6753
+ try {
6754
+ const bsPath = path3.join(ctx.projectRoot, "node_modules", "@stackwright", "build-scripts");
6755
+ if (!fs2.existsSync(bsPath)) {
6756
+ return {
6757
+ ok: false,
6758
+ error: `@stackwright/build-scripts not found at ${bsPath}. Is it installed in your project?`
6759
+ };
6760
+ }
6761
+ const bs = await import(bsPath);
6762
+ if (typeof bs[fn] !== "function") {
6763
+ return { ok: false, error: `${fn} not exported from @stackwright/build-scripts` };
6764
+ }
6765
+ await bs[fn](ctx, ...extra ? [extra] : []);
6766
+ return { ok: true };
6767
+ } catch (err) {
6768
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
6769
+ }
6770
+ }
6771
+ function registerCompileTools(server2) {
6772
+ const projectRootArg = z19.string().optional().describe("Absolute path to project root (defaults to cwd)");
6773
+ server2.tool(
6774
+ "stackwright_pro_compile_collections",
6775
+ "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.",
6776
+ { projectRoot: projectRootArg },
6777
+ async ({ projectRoot }) => {
6778
+ const start = Date.now();
6779
+ const root = projectRoot ?? process.cwd();
6780
+ try {
6781
+ await compileCollections(buildContext(root));
6782
+ return {
6783
+ content: [
6784
+ {
6785
+ type: "text",
6786
+ text: ` _collections.json written (${formatDuration(start)})`
6787
+ }
6788
+ ]
6789
+ };
6790
+ } catch (err) {
6791
+ return {
6792
+ content: [
6793
+ {
6794
+ type: "text",
6795
+ text: ` compile_collections failed: ${err instanceof Error ? err.message : String(err)}`
6796
+ }
6797
+ ]
6798
+ };
6799
+ }
6800
+ }
6801
+ );
6802
+ server2.tool(
6803
+ "stackwright_pro_compile_auth",
6804
+ "Compile stackwright.auth.yml \u2192 public/stackwright-content/_auth.json. Reads and validates the auth sidecar config file and emits the JSON sink consumed at app startup to configure auth middleware.",
6805
+ { projectRoot: projectRootArg },
6806
+ async ({ projectRoot }) => {
6807
+ const start = Date.now();
6808
+ const root = projectRoot ?? process.cwd();
6809
+ try {
6810
+ await compileAuth(buildContext(root));
6811
+ return {
6812
+ content: [
6813
+ {
6814
+ type: "text",
6815
+ text: ` _auth.json written (${formatDuration(start)})`
6816
+ }
6817
+ ]
6818
+ };
6819
+ } catch (err) {
6820
+ return {
6821
+ content: [
6822
+ {
6823
+ type: "text",
6824
+ text: ` compile_auth failed: ${err instanceof Error ? err.message : String(err)}`
6825
+ }
6826
+ ]
6827
+ };
6828
+ }
6829
+ }
6830
+ );
6831
+ server2.tool(
6832
+ "stackwright_pro_compile_integrations",
6833
+ "Compile stackwright.integrations.yml \u2192 public/stackwright-content/_integrations.json. If no yml file exists, falls back to extracting integration metadata from _site.json (collections data is excluded \u2014 it lives in _collections.json).",
6834
+ { projectRoot: projectRootArg },
6835
+ async ({ projectRoot }) => {
6836
+ const start = Date.now();
6837
+ const root = projectRoot ?? process.cwd();
6838
+ try {
6839
+ await compileIntegrations(buildContext(root));
6840
+ return {
6841
+ content: [
6842
+ {
6843
+ type: "text",
6844
+ text: ` _integrations.json written (${formatDuration(start)})`
6845
+ }
6846
+ ]
6847
+ };
6848
+ } catch (err) {
6849
+ return {
6850
+ content: [
6851
+ {
6852
+ type: "text",
6853
+ text: ` compile_integrations failed: ${err instanceof Error ? err.message : String(err)}`
6854
+ }
6855
+ ]
6856
+ };
6857
+ }
6858
+ }
6859
+ );
6860
+ server2.tool(
6861
+ "stackwright_pro_compile_all",
6862
+ "Run all Pro compile sinks (collections + auth + integrations) in sequence. Equivalent to running compile_collections, compile_auth, and compile_integrations individually. Use this for a full Pro prebuild pass.",
6863
+ { projectRoot: projectRootArg },
6864
+ async ({ projectRoot }) => {
6865
+ const start = Date.now();
6866
+ const root = projectRoot ?? process.cwd();
6867
+ const ctx = buildContext(root);
6868
+ const results = [];
6869
+ const errors = [];
6870
+ for (const [label, fn] of [
6871
+ ["collections", () => compileCollections(ctx)],
6872
+ ["auth", () => compileAuth(ctx)],
6873
+ ["integrations", () => compileIntegrations(ctx)]
6874
+ ]) {
6875
+ try {
6876
+ await fn();
6877
+ results.push(` _${label}.json`);
6878
+ } catch (err) {
6879
+ errors.push(` ${label}: ${err instanceof Error ? err.message : String(err)}`);
6880
+ }
6881
+ }
6882
+ const summary = errors.length > 0 ? ` compile_all finished with errors (${formatDuration(start)}):
6883
+ ${results.join("\n")}
6884
+ Errors:
6885
+ ${errors.join("\n")}` : ` compile_all complete (${formatDuration(start)}):
6886
+ ${results.join("\n")}`;
6887
+ return { content: [{ type: "text", text: summary }] };
6888
+ }
6889
+ );
6890
+ server2.tool(
6891
+ "stackwright_pro_compile_theme",
6892
+ "Compile stackwright.yml theme config \u2192 _theme.json. Delegates to compileTheme from @stackwright/build-scripts installed in the project.",
6893
+ { projectRoot: projectRootArg },
6894
+ async ({ projectRoot }) => {
6895
+ const start = Date.now();
6896
+ const root = projectRoot ?? process.cwd();
6897
+ const { ok, error } = await tryOssCompile("compileTheme", buildContext(root));
6898
+ return {
6899
+ content: [
6900
+ {
6901
+ type: "text",
6902
+ text: ok ? ` _theme.json written (${formatDuration(start)})` : ` compile_theme failed: ${error}`
6903
+ }
6904
+ ]
6905
+ };
6906
+ }
6907
+ );
6908
+ server2.tool(
6909
+ "stackwright_pro_compile_site",
6910
+ "Compile stackwright.yml \u2192 _site.json. Delegates to compileSite from @stackwright/build-scripts installed in the project.",
6911
+ { projectRoot: projectRootArg },
6912
+ async ({ projectRoot }) => {
6913
+ const start = Date.now();
6914
+ const root = projectRoot ?? process.cwd();
6915
+ const { ok, error } = await tryOssCompile("compileSite", buildContext(root));
6916
+ return {
6917
+ content: [
6918
+ {
6919
+ type: "text",
6920
+ text: ok ? ` _site.json written (${formatDuration(start)})` : ` compile_site failed: ${error}`
6921
+ }
6922
+ ]
6923
+ };
6924
+ }
6925
+ );
6926
+ server2.tool(
6927
+ "stackwright_pro_compile_pages",
6928
+ "Compile all pages content.yml files \u2192 page JSON files. Delegates to compilePages from @stackwright/build-scripts installed in the project.",
6929
+ { projectRoot: projectRootArg },
6930
+ async ({ projectRoot }) => {
6931
+ const start = Date.now();
6932
+ const root = projectRoot ?? process.cwd();
6933
+ const { ok, error } = await tryOssCompile("compilePages", buildContext(root));
6934
+ return {
6935
+ content: [
6936
+ {
6937
+ type: "text",
6938
+ text: ok ? ` pages compiled (${formatDuration(start)})` : ` compile_pages failed: ${error}`
6939
+ }
6940
+ ]
6941
+ };
6942
+ }
6943
+ );
6944
+ server2.tool(
6945
+ "stackwright_pro_compile_page",
6946
+ "Compile a single page by slug. Delegates to compilePage from @stackwright/build-scripts installed in the project.",
6947
+ {
6948
+ slug: z19.string().describe('Page slug (relative path under pages/, e.g. "dashboard")'),
6949
+ projectRoot: projectRootArg
6950
+ },
6951
+ async ({ slug, projectRoot }) => {
6952
+ const start = Date.now();
6953
+ const root = projectRoot ?? process.cwd();
6954
+ const { ok, error } = await tryOssCompile("compilePage", buildContext(root), { slug });
6955
+ return {
6956
+ content: [
6957
+ {
6958
+ type: "text",
6959
+ text: ok ? ` page "${slug}" compiled (${formatDuration(start)})` : ` compile_page failed: ${error}`
6960
+ }
6961
+ ]
6962
+ };
6963
+ }
6964
+ );
6965
+ }
6966
+
6478
6967
  // package.json
6479
6968
  var package_default = {
6480
6969
  dependencies: {
6481
6970
  "@modelcontextprotocol/sdk": "^1.10.0",
6971
+ "@stackwright-pro/auth-nextjs": "workspace:*",
6482
6972
  "@stackwright-pro/cli-data-explorer": "workspace:*",
6973
+ "@stackwright-pro/openapi": "workspace:*",
6974
+ "@stackwright-pro/pulse": "workspace:*",
6483
6975
  "@stackwright-pro/types": "workspace:*",
6484
6976
  "@types/js-yaml": "^4.0.9",
6485
6977
  "js-yaml": "^4.2.0",
@@ -6500,7 +6992,7 @@ var package_default = {
6500
6992
  "test:coverage": "vitest run --coverage"
6501
6993
  },
6502
6994
  name: "@stackwright-pro/mcp",
6503
- version: "0.2.0-alpha.90",
6995
+ version: "0.2.0-alpha.95",
6504
6996
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
6505
6997
  license: "SEE LICENSE IN LICENSE",
6506
6998
  main: "./dist/server.js",
@@ -6558,7 +7050,22 @@ registerValidateYamlFragmentTool(server);
6558
7050
  registerGetSchemaTool(server);
6559
7051
  registerScaffoldCleanupTools(server);
6560
7052
  registerContrastTools(server);
7053
+ registerCompileTools(server);
6561
7054
  async function main() {
7055
+ const pipelineGraph = loadPipelineGraph();
7056
+ const phasePosition = new Map(PHASE_ORDER.map((p, i) => [p, i]));
7057
+ for (const [phase, deps] of Object.entries(pipelineGraph.dependencies)) {
7058
+ const phaseIdx = phasePosition.get(phase);
7059
+ if (phaseIdx === void 0) continue;
7060
+ for (const dep of deps) {
7061
+ const depIdx = phasePosition.get(dep);
7062
+ if (depIdx !== void 0 && depIdx >= phaseIdx) {
7063
+ throw new Error(
7064
+ `PHASE_ORDER sanity check failed: "${dep}" (position ${depIdx}) must precede "${phase}" (position ${phaseIdx}) but it doesn't. Update PHASE_ORDER in pipeline.ts to reflect the new dependency structure.`
7065
+ );
7066
+ }
7067
+ }
7068
+ }
6562
7069
  const transport = new StdioServerTransport();
6563
7070
  try {
6564
7071
  const servicesRegisterPkg = "@stackwright-services/mcp/register";