@stackwright-pro/mcp 0.2.0-alpha.92 → 0.2.0-alpha.97

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) {
@@ -1870,8 +1870,8 @@ function registerOrchestrationTools(server2) {
1870
1870
  {
1871
1871
  buildContext: z9.string().describe("Free-text description of what the user wants to build")
1872
1872
  },
1873
- async ({ buildContext }) => {
1874
- const { text, isError } = handleSaveBuildContext({ buildContext });
1873
+ async ({ buildContext: buildContext2 }) => {
1874
+ const { text, isError } = handleSaveBuildContext({ buildContext: buildContext2 });
1875
1875
  return {
1876
1876
  content: [{ type: "text", text }],
1877
1877
  isError
@@ -1999,8 +1999,8 @@ function registerOrchestrationTools(server2) {
1999
1999
 
2000
2000
  // src/tools/pipeline.ts
2001
2001
  import { z as z13 } from "zod";
2002
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
2003
- 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";
2004
2004
  import { createHash as createHash3 } from "crypto";
2005
2005
 
2006
2006
  // src/artifact-signing.ts
@@ -2690,6 +2690,224 @@ function registerGetSchemaTool(server2) {
2690
2690
  );
2691
2691
  }
2692
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
+ "stackwright-pro-qa-otter": "qa"
2710
+ };
2711
+ function manifestNameToPhase(name) {
2712
+ return MANIFEST_NAME_TO_PHASE[name] ?? null;
2713
+ }
2714
+ var OTTER_SEARCH_PATHS = [
2715
+ "node_modules/@stackwright-pro/otters/src/",
2716
+ // production: installed package
2717
+ "packages/otters/src/",
2718
+ // monorepo: run from repo root
2719
+ "../otters/src/"
2720
+ // monorepo: run from packages/mcp/
2721
+ ];
2722
+ function resolveOtterDirFromCwd() {
2723
+ const cwd = process.cwd();
2724
+ for (const relative of OTTER_SEARCH_PATHS) {
2725
+ const candidate = join4(cwd, relative);
2726
+ try {
2727
+ lstatSync5(candidate);
2728
+ return candidate;
2729
+ } catch {
2730
+ }
2731
+ }
2732
+ throw new Error(
2733
+ `Cannot locate otter directory. Searched: ${OTTER_SEARCH_PATHS.join(", ")} (relative to ${cwd}). Make sure @stackwright-pro/otters is installed.`
2734
+ );
2735
+ }
2736
+ function loadOtterManifestsFromDir(otterDir) {
2737
+ const entries = readdirSync2(otterDir);
2738
+ const manifests = [];
2739
+ for (const filename of entries) {
2740
+ if (!filename.endsWith("-otter.json")) continue;
2741
+ const filePath = join4(otterDir, filename);
2742
+ if (lstatSync5(filePath).isSymbolicLink()) continue;
2743
+ try {
2744
+ const raw = readFileSync4(filePath, "utf-8");
2745
+ const manifest = JSON.parse(raw);
2746
+ manifests.push(manifest);
2747
+ } catch (err) {
2748
+ const msg = err instanceof Error ? err.message : String(err);
2749
+ throw new Error(`Failed to load otter manifest "${filename}": ${msg}`, { cause: err });
2750
+ }
2751
+ }
2752
+ return manifests;
2753
+ }
2754
+ function topologicalSort(dependencies) {
2755
+ const phases = Object.keys(dependencies);
2756
+ const inDegree = {};
2757
+ const adjList = {};
2758
+ for (const phase of phases) {
2759
+ inDegree[phase] ??= 0;
2760
+ adjList[phase] ??= [];
2761
+ }
2762
+ for (const phase of phases) {
2763
+ for (const dep of dependencies[phase] ?? []) {
2764
+ inDegree[phase] = (inDegree[phase] ?? 0) + 1;
2765
+ adjList[dep] ??= [];
2766
+ adjList[dep].push(phase);
2767
+ }
2768
+ }
2769
+ const queue = phases.filter((p) => (inDegree[p] ?? 0) === 0);
2770
+ const result = [];
2771
+ while (queue.length > 0) {
2772
+ const node = queue.shift();
2773
+ result.push(node);
2774
+ for (const dependent of adjList[node] ?? []) {
2775
+ inDegree[dependent] -= 1;
2776
+ if (inDegree[dependent] === 0) {
2777
+ queue.push(dependent);
2778
+ }
2779
+ }
2780
+ }
2781
+ if (result.length !== phases.length) {
2782
+ const cycleNodes = phases.filter((p) => (inDegree[p] ?? 0) > 0);
2783
+ throw new Error(
2784
+ `Pipeline dependency cycle detected involving: [${cycleNodes.join(", ")}]. Check otter pipeline declarations for circular dependencies.`
2785
+ );
2786
+ }
2787
+ return result;
2788
+ }
2789
+ function buildPipelineGraph(manifests) {
2790
+ const sinkProducers = {};
2791
+ const artifactProducers = {};
2792
+ for (const m of manifests) {
2793
+ const phase = manifestNameToPhase(m.name);
2794
+ if (!phase) continue;
2795
+ const out = m.pipeline?.outputs;
2796
+ if (!out) continue;
2797
+ for (const sink of out.sinks ?? []) {
2798
+ if (sinkProducers[sink]) {
2799
+ throw new Error(
2800
+ `Sink "${sink}" is produced by both "${sinkProducers[sink]}" and "${phase}". Each sink must have exactly one producer.`
2801
+ );
2802
+ }
2803
+ sinkProducers[sink] = phase;
2804
+ }
2805
+ if (out.artifact) {
2806
+ if (artifactProducers[out.artifact]) {
2807
+ throw new Error(
2808
+ `Artifact "${out.artifact}" is produced by both "${artifactProducers[out.artifact]}" and "${phase}". Each artifact must have exactly one producer.`
2809
+ );
2810
+ }
2811
+ artifactProducers[out.artifact] = phase;
2812
+ }
2813
+ }
2814
+ const dependencies = {};
2815
+ for (const m of manifests) {
2816
+ const phase = manifestNameToPhase(m.name);
2817
+ if (!phase) continue;
2818
+ const deps = /* @__PURE__ */ new Set();
2819
+ const inp = m.pipeline?.inputs;
2820
+ if (inp) {
2821
+ for (const sink of inp.sinks ?? []) {
2822
+ const producer = sinkProducers[sink];
2823
+ if (!producer) {
2824
+ throw new Error(
2825
+ `Phase "${phase}" declares sink input "${sink}" but no otter produces it. Add an otter with outputs.sinks including "${sink}".`
2826
+ );
2827
+ }
2828
+ if (producer !== phase) deps.add(producer);
2829
+ }
2830
+ for (const artifact of inp.artifacts ?? []) {
2831
+ const producer = artifactProducers[artifact];
2832
+ if (!producer) {
2833
+ throw new Error(
2834
+ `Phase "${phase}" declares artifact input "${artifact}" but no otter produces it. Add an otter with outputs.artifact = "${artifact}".`
2835
+ );
2836
+ }
2837
+ if (producer !== phase) deps.add(producer);
2838
+ }
2839
+ }
2840
+ dependencies[phase] = Array.from(deps);
2841
+ }
2842
+ const order = topologicalSort(dependencies);
2843
+ return { dependencies, order, sinkProducers, artifactProducers };
2844
+ }
2845
+ var DISTRIBUTED_NAMESPACES = ["@stackwright-pro", "@stackwright"];
2846
+ function loadDistributedOtterManifests(cwd) {
2847
+ const results = [];
2848
+ for (const namespace of DISTRIBUTED_NAMESPACES) {
2849
+ const nsDir = join4(cwd, "node_modules", namespace);
2850
+ let pkgDirNames;
2851
+ try {
2852
+ pkgDirNames = readdirSync2(nsDir);
2853
+ } catch {
2854
+ continue;
2855
+ }
2856
+ for (const pkgDirName of pkgDirNames) {
2857
+ const pkgDir = join4(nsDir, pkgDirName);
2858
+ const packageName = `${namespace}/${pkgDirName}`;
2859
+ let otterRelPaths;
2860
+ try {
2861
+ const raw = readFileSync4(join4(pkgDir, "package.json"), "utf-8");
2862
+ const pkgJson = JSON.parse(raw);
2863
+ const declared = pkgJson.stackwright?.otters;
2864
+ if (!Array.isArray(declared) || declared.length === 0) continue;
2865
+ otterRelPaths = declared;
2866
+ } catch {
2867
+ continue;
2868
+ }
2869
+ for (const relPath of otterRelPaths) {
2870
+ const otterFilePath = join4(pkgDir, relPath);
2871
+ try {
2872
+ const raw = readFileSync4(otterFilePath, "utf-8");
2873
+ const manifest = JSON.parse(raw);
2874
+ results.push({ manifest, packageName });
2875
+ } catch (err) {
2876
+ const msg = err instanceof Error ? err.message : String(err);
2877
+ console.warn(
2878
+ `[pipeline-graph] Failed to load distributed otter at "${otterFilePath}" from "${packageName}": ${msg}`
2879
+ );
2880
+ }
2881
+ }
2882
+ }
2883
+ }
2884
+ return results;
2885
+ }
2886
+ function _mergeManifestPools(central, distributed) {
2887
+ const centralNames = new Set(central.map((m) => m.name));
2888
+ const merged = [...central];
2889
+ for (const { manifest, packageName } of distributed) {
2890
+ if (centralNames.has(manifest.name)) {
2891
+ console.warn(
2892
+ `[pipeline-graph] Otter "${manifest.name}" declared by both @stackwright-pro/otters and ${packageName} \u2014 using @stackwright-pro/otters version.`
2893
+ );
2894
+ } else {
2895
+ merged.push(manifest);
2896
+ }
2897
+ }
2898
+ return merged;
2899
+ }
2900
+ var _cachedGraph = null;
2901
+ function loadPipelineGraph() {
2902
+ if (_cachedGraph) return _cachedGraph;
2903
+ const otterDir = resolveOtterDirFromCwd();
2904
+ const central = loadOtterManifestsFromDir(otterDir);
2905
+ const distributed = loadDistributedOtterManifests(process.cwd());
2906
+ const manifests = _mergeManifestPools(central, distributed);
2907
+ _cachedGraph = buildPipelineGraph(manifests);
2908
+ return _cachedGraph;
2909
+ }
2910
+
2693
2911
  // src/tools/pipeline.ts
2694
2912
  var PHASE_ORDER = [
2695
2913
  "designer",
@@ -2698,45 +2916,16 @@ var PHASE_ORDER = [
2698
2916
  // generates app/ directory from config
2699
2917
  "api",
2700
2918
  "data",
2919
+ "auth",
2920
+ // moved earlier — only depends on design-language.json (designer)
2701
2921
  "geo",
2702
2922
  "workflow",
2703
2923
  "services",
2704
2924
  "pages",
2705
2925
  "dashboard",
2706
- "auth",
2707
- "polish"
2926
+ "polish",
2927
+ "qa"
2708
2928
  ];
2709
- var PHASE_DEPENDENCIES = {
2710
- designer: [],
2711
- theme: ["designer"],
2712
- scaffold: ["designer", "theme"],
2713
- // needs stackwright.yml + theme-tokens
2714
- api: [],
2715
- data: ["api"],
2716
- geo: ["data"],
2717
- // workflow: no hard deps — uses service: references with Prism mock fallback
2718
- // when API artifacts aren't available; roles come from user answers
2719
- workflow: [],
2720
- // services: needs API endpoints to know what to wire, and optionally
2721
- // workflow states as trigger sources. Produces OpenAPI specs consumed
2722
- // by pages and dashboard for typed data wiring.
2723
- services: ["api", "workflow"],
2724
- // pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
2725
- // specs) and on geo so the specialist prompt includes geo-manifest.json
2726
- // — page/dashboard otters see which page slugs are already claimed and
2727
- // won't attempt to overwrite them (swp-73c). 'api' is still transitive
2728
- // through 'data'; auth removed — page-otter has graceful fallback.
2729
- pages: ["designer", "theme", "data", "services", "geo", "scaffold"],
2730
- dashboard: ["designer", "theme", "data", "services", "geo", "scaffold"],
2731
- // auth is the penultimate phase — runs after all content-producing phases
2732
- // so it can read pages, dashboard, workflow, and geo artifacts for route
2733
- // protection and RBAC wiring. Skipped upstream phases still satisfy deps
2734
- // (the foreman marks them executed=true), so auth always runs.
2735
- auth: ["pages", "dashboard", "workflow", "geo"],
2736
- // polish is the terminal phase — runs after auth so the landing page and
2737
- // nav reflect the final set of protected routes and all generated pages.
2738
- polish: ["auth"]
2739
- };
2740
2929
  var PHASE_ARTIFACT = {
2741
2930
  designer: "design-language.json",
2742
2931
  theme: "theme-tokens.json",
@@ -2749,7 +2938,8 @@ var PHASE_ARTIFACT = {
2749
2938
  workflow: "workflow-config.json",
2750
2939
  services: "services-config.json",
2751
2940
  polish: "polish-manifest.json",
2752
- geo: "geo-manifest.json"
2941
+ geo: "geo-manifest.json",
2942
+ qa: "qa-findings.json"
2753
2943
  };
2754
2944
  var PHASE_TO_OTTER2 = {
2755
2945
  designer: "stackwright-pro-designer-otter",
@@ -2763,7 +2953,8 @@ var PHASE_TO_OTTER2 = {
2763
2953
  workflow: "stackwright-pro-form-wizard-otter",
2764
2954
  services: "stackwright-services-otter",
2765
2955
  polish: "stackwright-pro-polish-otter",
2766
- geo: "stackwright-pro-geo-otter"
2956
+ geo: "stackwright-pro-geo-otter",
2957
+ qa: "stackwright-pro-qa-otter"
2767
2958
  };
2768
2959
  function isValidPhase2(phase) {
2769
2960
  return PHASE_ORDER.includes(phase);
@@ -2793,7 +2984,7 @@ function createDefaultState() {
2793
2984
  };
2794
2985
  }
2795
2986
  function statePath(cwd) {
2796
- return join4(cwd, ".stackwright", "pipeline-state.json");
2987
+ return join5(cwd, ".stackwright", "pipeline-state.json");
2797
2988
  }
2798
2989
  function readState(cwd) {
2799
2990
  const p = statePath(cwd);
@@ -2806,7 +2997,7 @@ function readState(cwd) {
2806
2997
  }
2807
2998
  function safeWriteSync(filePath, content) {
2808
2999
  if (existsSync5(filePath)) {
2809
- const stat = lstatSync5(filePath);
3000
+ const stat = lstatSync6(filePath);
2810
3001
  if (stat.isSymbolicLink()) {
2811
3002
  throw new Error(`Refusing to write to symlink: ${filePath}`);
2812
3003
  }
@@ -2815,15 +3006,15 @@ function safeWriteSync(filePath, content) {
2815
3006
  }
2816
3007
  function safeReadSync(filePath) {
2817
3008
  if (existsSync5(filePath)) {
2818
- const stat = lstatSync5(filePath);
3009
+ const stat = lstatSync6(filePath);
2819
3010
  if (stat.isSymbolicLink()) {
2820
3011
  throw new Error(`Refusing to read symlink: ${filePath}`);
2821
3012
  }
2822
3013
  }
2823
- return readFileSync4(filePath, "utf-8");
3014
+ return readFileSync5(filePath, "utf-8");
2824
3015
  }
2825
3016
  function writeState(cwd, state) {
2826
- const dir = join4(cwd, ".stackwright");
3017
+ const dir = join5(cwd, ".stackwright");
2827
3018
  mkdirSync4(dir, { recursive: true });
2828
3019
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
2829
3020
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
@@ -2845,7 +3036,7 @@ function handleGetPipelineState(_cwd) {
2845
3036
  const cwd = _cwd ?? process.cwd();
2846
3037
  try {
2847
3038
  const state = readState(cwd);
2848
- const keyPath = join4(cwd, ".stackwright", "pipeline-keys.json");
3039
+ const keyPath = join5(cwd, ".stackwright", "pipeline-keys.json");
2849
3040
  if (!existsSync5(keyPath)) {
2850
3041
  try {
2851
3042
  const { fingerprint } = initPipelineKeys(cwd);
@@ -2949,7 +3140,7 @@ function handleCheckExecutionReady(_cwd, phase) {
2949
3140
  isError: true
2950
3141
  };
2951
3142
  }
2952
- const answerFile = join4(cwd, ".stackwright", "answers", `${phase}.json`);
3143
+ const answerFile = join5(cwd, ".stackwright", "answers", `${phase}.json`);
2953
3144
  if (!existsSync5(answerFile)) {
2954
3145
  return {
2955
3146
  text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
@@ -2974,11 +3165,11 @@ function handleCheckExecutionReady(_cwd, phase) {
2974
3165
  }
2975
3166
  }
2976
3167
  try {
2977
- const answersDir = join4(cwd, ".stackwright", "answers");
3168
+ const answersDir = join5(cwd, ".stackwright", "answers");
2978
3169
  const answeredPhases = [];
2979
3170
  const missingPhases = [];
2980
3171
  for (const phase2 of PHASE_ORDER) {
2981
- const answerFile = join4(answersDir, `${phase2}.json`);
3172
+ const answerFile = join5(answersDir, `${phase2}.json`);
2982
3173
  if (existsSync5(answerFile)) {
2983
3174
  try {
2984
3175
  const raw = safeReadSync(answerFile);
@@ -3012,6 +3203,7 @@ function handleGetReadyPhases(_cwd) {
3012
3203
  const cwd = _cwd ?? process.cwd();
3013
3204
  try {
3014
3205
  const state = readState(cwd);
3206
+ const graph = loadPipelineGraph();
3015
3207
  const completed = [];
3016
3208
  const ready = [];
3017
3209
  const blocked = [];
@@ -3021,7 +3213,7 @@ function handleGetReadyPhases(_cwd) {
3021
3213
  completed.push(phase);
3022
3214
  continue;
3023
3215
  }
3024
- const deps = PHASE_DEPENDENCIES[phase];
3216
+ const deps = graph.dependencies[phase] ?? [];
3025
3217
  const unmetDeps = deps.filter((dep) => !state.phases[dep]?.executed);
3026
3218
  if (unmetDeps.length === 0) {
3027
3219
  ready.push(phase);
@@ -3047,7 +3239,7 @@ function handleGetReadyPhases(_cwd) {
3047
3239
  function handleListArtifacts(_cwd) {
3048
3240
  const cwd = _cwd ?? process.cwd();
3049
3241
  try {
3050
- const artifactsDir = join4(cwd, ".stackwright", "artifacts");
3242
+ const artifactsDir = join5(cwd, ".stackwright", "artifacts");
3051
3243
  let manifest = null;
3052
3244
  try {
3053
3245
  manifest = loadSignatureManifest(cwd);
@@ -3057,7 +3249,7 @@ function handleListArtifacts(_cwd) {
3057
3249
  let completedCount = 0;
3058
3250
  for (const phase of PHASE_ORDER) {
3059
3251
  const expectedFile = PHASE_ARTIFACT[phase];
3060
- const fullPath = join4(artifactsDir, expectedFile);
3252
+ const fullPath = join5(artifactsDir, expectedFile);
3061
3253
  const exists = existsSync5(fullPath);
3062
3254
  if (exists) completedCount++;
3063
3255
  let signed = false;
@@ -3111,9 +3303,9 @@ function handleWritePhaseQuestions(input) {
3111
3303
  }
3112
3304
  } catch {
3113
3305
  }
3114
- const questionsDir = join4(cwd, ".stackwright", "questions");
3306
+ const questionsDir = join5(cwd, ".stackwright", "questions");
3115
3307
  mkdirSync4(questionsDir, { recursive: true });
3116
- const filePath = join4(questionsDir, `${phase}.json`);
3308
+ const filePath = join5(questionsDir, `${phase}.json`);
3117
3309
  const payload = {
3118
3310
  version: "1.0",
3119
3311
  phase,
@@ -3153,13 +3345,13 @@ function handleBuildSpecialistPrompt(input) {
3153
3345
  };
3154
3346
  }
3155
3347
  try {
3156
- const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
3348
+ const answersPath = join5(cwd, ".stackwright", "answers", `${phase}.json`);
3157
3349
  let answers = {};
3158
3350
  if (existsSync5(answersPath)) {
3159
3351
  answers = JSON.parse(safeReadSync(answersPath));
3160
3352
  }
3161
3353
  let buildContextText = "";
3162
- const buildContextPath = join4(cwd, ".stackwright", "build-context.json");
3354
+ const buildContextPath = join5(cwd, ".stackwright", "build-context.json");
3163
3355
  if (existsSync5(buildContextPath)) {
3164
3356
  try {
3165
3357
  const bcRaw = JSON.parse(safeReadSync(buildContextPath));
@@ -3169,12 +3361,13 @@ function handleBuildSpecialistPrompt(input) {
3169
3361
  } catch {
3170
3362
  }
3171
3363
  }
3172
- const deps = PHASE_DEPENDENCIES[phase];
3364
+ const graph = loadPipelineGraph();
3365
+ const deps = graph.dependencies[phase] ?? [];
3173
3366
  const artifactSections = [];
3174
3367
  const missingDependencies = [];
3175
3368
  for (const dep of deps) {
3176
3369
  const artifactFile = PHASE_ARTIFACT[dep];
3177
- const artifactPath = join4(cwd, ".stackwright", "artifacts", artifactFile);
3370
+ const artifactPath = join5(cwd, ".stackwright", "artifacts", artifactFile);
3178
3371
  if (existsSync5(artifactPath)) {
3179
3372
  const rawContent = safeReadSync(artifactPath);
3180
3373
  const rawBytes = Buffer.from(rawContent, "utf-8");
@@ -3240,7 +3433,7 @@ ${JSON.stringify(content, null, 2)}`
3240
3433
  parts.push("BUILD_CONTEXT:", buildContextText, "");
3241
3434
  }
3242
3435
  if (phase === "auth") {
3243
- const initContextPath = join4(cwd, ".stackwright", "init-context.json");
3436
+ const initContextPath = join5(cwd, ".stackwright", "init-context.json");
3244
3437
  if (existsSync5(initContextPath)) {
3245
3438
  try {
3246
3439
  const initContext = JSON.parse(safeReadSync(initContextPath));
@@ -3305,7 +3498,10 @@ var PHASE_REQUIRED_KEYS = {
3305
3498
  workflow: ["version", "generatedBy"],
3306
3499
  services: ["version", "generatedBy", "flows"],
3307
3500
  polish: ["version", "generatedBy"],
3308
- geo: ["version", "generatedBy", "geoCollections"]
3501
+ geo: ["version", "generatedBy", "geoCollections"],
3502
+ // qa: skipped=true path only needs version+generatedBy+skipped;
3503
+ // skipped=false path also needs summary+findings — Zod validator enforces the conditional.
3504
+ qa: ["version", "generatedBy", "skipped"]
3309
3505
  };
3310
3506
  var PHASE_ARTIFACT_SCHEMA = {
3311
3507
  designer: JSON.stringify(
@@ -3595,6 +3791,41 @@ var PHASE_ARTIFACT_SCHEMA = {
3595
3791
  },
3596
3792
  null,
3597
3793
  2
3794
+ ),
3795
+ qa: JSON.stringify(
3796
+ {
3797
+ version: "1.0",
3798
+ generatedBy: "stackwright-pro-qa-otter",
3799
+ // skipped: true path — when dev server is unreachable at audit time
3800
+ // skipped: false path — full audit completed
3801
+ skipped: false,
3802
+ skipReason: null,
3803
+ wcagLevel: "<AA|AAA>",
3804
+ summary: {
3805
+ routesAudited: 3,
3806
+ serious: 0,
3807
+ moderate: 1,
3808
+ minor: 0
3809
+ },
3810
+ findings: [
3811
+ {
3812
+ id: "qa-001",
3813
+ route: "/dashboard",
3814
+ severity: "<serious|moderate|minor>",
3815
+ category: "<visual|a11y|design-contract|runtime>",
3816
+ finding: "Human-readable description of what was observed",
3817
+ evidence: {
3818
+ screenshot: ".stackwright/qa/screenshots/dashboard-light.png",
3819
+ consoleErrors: [],
3820
+ axeViolations: []
3821
+ },
3822
+ suggested_otters: ["stackwright-pro-theme-otter"],
3823
+ suggested_fix: "Specific, concrete next step the repair otter should take"
3824
+ }
3825
+ ]
3826
+ },
3827
+ null,
3828
+ 2
3598
3829
  )
3599
3830
  };
3600
3831
  function handleValidateArtifact(input) {
@@ -3708,13 +3939,13 @@ function handleValidateArtifact(input) {
3708
3939
  // to a flow or state-machine in this services-config artifact.
3709
3940
  // Skips gracefully when wizard-otter didn't run (Pro-only install) or declared no hooks.
3710
3941
  services: (artifact2) => {
3711
- const workflowArtifactPath = join4(cwd, ".stackwright", "artifacts", "workflow-config.json");
3942
+ const workflowArtifactPath = join5(cwd, ".stackwright", "artifacts", "workflow-config.json");
3712
3943
  if (!existsSync5(workflowArtifactPath)) {
3713
3944
  return { success: true };
3714
3945
  }
3715
3946
  let workflowArtifact;
3716
3947
  try {
3717
- workflowArtifact = JSON.parse(readFileSync4(workflowArtifactPath, "utf-8"));
3948
+ workflowArtifact = JSON.parse(readFileSync5(workflowArtifactPath, "utf-8"));
3718
3949
  } catch {
3719
3950
  return { success: true };
3720
3951
  }
@@ -3735,6 +3966,40 @@ function handleValidateArtifact(input) {
3735
3966
  };
3736
3967
  }
3737
3968
  return { success: true };
3969
+ },
3970
+ // qa artifact — permissive validator: requires version + generatedBy + skipped.
3971
+ // When skipped=true: skipReason must be a string.
3972
+ // When skipped=false: findings must be an array and summary must be an object.
3973
+ qa: (artifact2) => {
3974
+ const skipped = artifact2["skipped"];
3975
+ if (typeof skipped !== "boolean") {
3976
+ return {
3977
+ success: false,
3978
+ error: { message: '"skipped" must be a boolean (true or false).' }
3979
+ };
3980
+ }
3981
+ if (skipped === true) {
3982
+ if (typeof artifact2["skipReason"] !== "string") {
3983
+ return {
3984
+ success: false,
3985
+ error: { message: 'When skipped=true, "skipReason" must be a string.' }
3986
+ };
3987
+ }
3988
+ return { success: true };
3989
+ }
3990
+ if (!Array.isArray(artifact2["findings"])) {
3991
+ return {
3992
+ success: false,
3993
+ error: { message: 'When skipped=false, "findings" must be an array.' }
3994
+ };
3995
+ }
3996
+ if (typeof artifact2["summary"] !== "object" || artifact2["summary"] === null) {
3997
+ return {
3998
+ success: false,
3999
+ error: { message: 'When skipped=false, "summary" must be an object.' }
4000
+ };
4001
+ }
4002
+ return { success: true };
3738
4003
  }
3739
4004
  };
3740
4005
  const zodValidator = PHASE_ZOD_VALIDATORS[phase];
@@ -3751,10 +4016,10 @@ function handleValidateArtifact(input) {
3751
4016
  }
3752
4017
  }
3753
4018
  try {
3754
- const artifactsDir = join4(cwd, ".stackwright", "artifacts");
4019
+ const artifactsDir = join5(cwd, ".stackwright", "artifacts");
3755
4020
  mkdirSync4(artifactsDir, { recursive: true });
3756
4021
  const artifactFile = PHASE_ARTIFACT[phase];
3757
- const artifactPath = join4(artifactsDir, artifactFile);
4022
+ const artifactPath = join5(artifactsDir, artifactFile);
3758
4023
  const serialized = JSON.stringify(artifact, null, 2) + "\n";
3759
4024
  const artifactBytes = Buffer.from(serialized, "utf-8");
3760
4025
  safeWriteSync(artifactPath, serialized);
@@ -3871,9 +4136,9 @@ function registerPipelineTools(server2) {
3871
4136
  isError: true
3872
4137
  };
3873
4138
  }
3874
- const questionsDir = join4(process.cwd(), ".stackwright", "questions");
4139
+ const questionsDir = join5(process.cwd(), ".stackwright", "questions");
3875
4140
  mkdirSync4(questionsDir, { recursive: true });
3876
- const outPath = join4(questionsDir, `${phase}.json`);
4141
+ const outPath = join5(questionsDir, `${phase}.json`);
3877
4142
  writeFileSync4(
3878
4143
  outPath,
3879
4144
  JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
@@ -3925,8 +4190,8 @@ function registerPipelineTools(server2) {
3925
4190
 
3926
4191
  // src/tools/safe-write.ts
3927
4192
  import { z as z14 } from "zod";
3928
- import { writeFileSync as writeFileSync5, readFileSync as readFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3929
- import { normalize, isAbsolute, dirname, join as join5 } from "path";
4193
+ import { writeFileSync as writeFileSync5, readFileSync as readFileSync6, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync7 } from "fs";
4194
+ import { normalize, isAbsolute, dirname, join as join6 } from "path";
3930
4195
  var OTTER_WRITE_ALLOWLISTS = {
3931
4196
  "stackwright-pro-designer-otter": [
3932
4197
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
@@ -3941,8 +4206,11 @@ var OTTER_WRITE_ALLOWLISTS = {
3941
4206
  ],
3942
4207
  "stackwright-pro-auth-otter": [
3943
4208
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
3944
- { prefix: "config/", suffix: ".yml", description: "Auth YAML config" },
3945
- { prefix: "config/", suffix: ".yaml", description: "Auth YAML config" },
4209
+ {
4210
+ prefix: "stackwright.auth.",
4211
+ suffix: ".yml",
4212
+ description: "Auth config YAML (stackwright.auth.yml \u2014 compiled to _auth.json)"
4213
+ },
3946
4214
  {
3947
4215
  prefix: ".env",
3948
4216
  suffix: "",
@@ -3961,7 +4229,11 @@ var OTTER_WRITE_ALLOWLISTS = {
3961
4229
  ],
3962
4230
  "stackwright-pro-data-otter": [
3963
4231
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Data config artifact" },
3964
- { prefix: "stackwright.yml", suffix: "", description: "Stackwright config" }
4232
+ {
4233
+ prefix: "stackwright.collections.",
4234
+ suffix: ".yml",
4235
+ description: "Collections config YAML (stackwright.collections.yml \u2014 compiled to _collections.json)"
4236
+ }
3965
4237
  ],
3966
4238
  "stackwright-pro-page-otter": [
3967
4239
  { prefix: "pages/", suffix: "/content.yml", description: "Page content YAML" },
@@ -3987,7 +4259,12 @@ var OTTER_WRITE_ALLOWLISTS = {
3987
4259
  }
3988
4260
  ],
3989
4261
  "stackwright-pro-api-otter": [
3990
- { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" }
4262
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" },
4263
+ {
4264
+ prefix: "stackwright.integrations.",
4265
+ suffix: ".yml",
4266
+ description: "Integrations config YAML (stackwright.integrations.yml \u2014 compiled to _integrations.json)"
4267
+ }
3991
4268
  ],
3992
4269
  "stackwright-pro-geo-otter": [
3993
4270
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Geo manifest artifact" },
@@ -4010,6 +4287,25 @@ var OTTER_WRITE_ALLOWLISTS = {
4010
4287
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" },
4011
4288
  { prefix: "README.md", suffix: "", description: "Project README rewrite" }
4012
4289
  ],
4290
+ // QA otter writes ONLY to the qa/ directory and the qa-findings artifact.
4291
+ // It never writes .tsx, .ts, .yml, or .json outside these paths.
4292
+ "stackwright-pro-qa-otter": [
4293
+ {
4294
+ prefix: ".stackwright/artifacts/qa-findings.json",
4295
+ suffix: "",
4296
+ description: "QA findings artifact"
4297
+ },
4298
+ {
4299
+ prefix: ".stackwright/qa/",
4300
+ suffix: ".md",
4301
+ description: "QA report markdown"
4302
+ },
4303
+ {
4304
+ prefix: ".stackwright/qa/screenshots/",
4305
+ suffix: ".png",
4306
+ description: "Route screenshots for evidence"
4307
+ }
4308
+ ],
4013
4309
  // domain-expert-otter is a reader/answerer only — it writes via stackwright_pro_save_phase_answers,
4014
4310
  // not via safe_write. Entry required here because the MCP tool availability guard boilerplate
4015
4311
  // in its system prompt mentions stackwright_pro_safe_write (triggering the coherence test).
@@ -4066,22 +4362,22 @@ function extractContentTypes(yamlContent) {
4066
4362
  return types.length > 0 ? types : ["unknown"];
4067
4363
  }
4068
4364
  function readPageRegistry(cwd) {
4069
- const regPath = join5(cwd, PAGE_REGISTRY_FILE);
4365
+ const regPath = join6(cwd, PAGE_REGISTRY_FILE);
4070
4366
  if (!existsSync6(regPath)) return {};
4071
4367
  try {
4072
- const stat = lstatSync6(regPath);
4368
+ const stat = lstatSync7(regPath);
4073
4369
  if (stat.isSymbolicLink()) return {};
4074
- return JSON.parse(readFileSync5(regPath, "utf-8"));
4370
+ return JSON.parse(readFileSync6(regPath, "utf-8"));
4075
4371
  } catch {
4076
4372
  return {};
4077
4373
  }
4078
4374
  }
4079
4375
  function writePageRegistry(cwd, registry) {
4080
- const dir = join5(cwd, ".stackwright");
4376
+ const dir = join6(cwd, ".stackwright");
4081
4377
  mkdirSync5(dir, { recursive: true });
4082
- const regPath = join5(cwd, PAGE_REGISTRY_FILE);
4378
+ const regPath = join6(cwd, PAGE_REGISTRY_FILE);
4083
4379
  if (existsSync6(regPath)) {
4084
- const stat = lstatSync6(regPath);
4380
+ const stat = lstatSync7(regPath);
4085
4381
  if (stat.isSymbolicLink()) {
4086
4382
  throw new Error("Refusing to write page registry through symlink");
4087
4383
  }
@@ -4101,8 +4397,8 @@ function checkPageOwnership(cwd, slug, callerOtter) {
4101
4397
  };
4102
4398
  }
4103
4399
  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;
4104
- function stripNextjsBracketSegments(path3) {
4105
- return path3.replace(NEXTJS_BRACKET_SEGMENT_RE, "");
4400
+ function stripNextjsBracketSegments(path4) {
4401
+ return path4.replace(NEXTJS_BRACKET_SEGMENT_RE, "");
4106
4402
  }
4107
4403
  function checkPathAllowed(callerOtter, filePath) {
4108
4404
  const normalized = normalize(filePath);
@@ -4252,10 +4548,10 @@ function handleSafeWrite(input) {
4252
4548
  return { text: JSON.stringify(result), isError: true };
4253
4549
  }
4254
4550
  const normalized = normalize(filePath);
4255
- const fullPath = join5(cwd, normalized);
4551
+ const fullPath = join6(cwd, normalized);
4256
4552
  if (existsSync6(fullPath)) {
4257
4553
  try {
4258
- const stat = lstatSync6(fullPath);
4554
+ const stat = lstatSync7(fullPath);
4259
4555
  if (stat.isSymbolicLink()) {
4260
4556
  const result = {
4261
4557
  success: false,
@@ -4358,8 +4654,8 @@ function registerSafeWriteTools(server2) {
4358
4654
 
4359
4655
  // src/tools/auth.ts
4360
4656
  import { z as z15 } from "zod";
4361
- import { readFileSync as readFileSync6, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
4362
- import { join as join6 } from "path";
4657
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
4658
+ import { join as join7 } from "path";
4363
4659
  function buildHierarchy(roles) {
4364
4660
  const h = {};
4365
4661
  for (let i = 0; i < roles.length - 1; i++) {
@@ -4373,7 +4669,7 @@ function hierarchyToYaml(hierarchy, indent) {
4373
4669
  return entries.map(([role, subs]) => `${indent}${role}: [${subs.join(", ")}]`).join("\n");
4374
4670
  }
4375
4671
  function rolesToYaml(roles, indent) {
4376
- return roles.map((r) => `${indent}- ${r}`).join("\n");
4672
+ return roles.map((r) => `${indent}- name: ${r}`).join("\n");
4377
4673
  }
4378
4674
  function normalizeRoutes(routes, defaultRole) {
4379
4675
  return routes.map(
@@ -4386,9 +4682,9 @@ ${indent} requiredRole: ${r.requiredRole}`).join("\n");
4386
4682
  }
4387
4683
  function detectNextMajorVersion(cwd) {
4388
4684
  try {
4389
- const pkgPath = join6(cwd, "package.json");
4685
+ const pkgPath = join7(cwd, "package.json");
4390
4686
  if (!existsSync7(pkgPath)) return null;
4391
- const pkg = JSON.parse(readFileSync6(pkgPath, "utf8"));
4687
+ const pkg = JSON.parse(readFileSync7(pkgPath, "utf8"));
4392
4688
  const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
4393
4689
  if (!nextVersion) return null;
4394
4690
  const match = nextVersion.match(/(\d+)/);
@@ -4397,25 +4693,6 @@ function detectNextMajorVersion(cwd) {
4397
4693
  return null;
4398
4694
  }
4399
4695
  }
4400
- function upsertAuthBlock(existing, authYaml) {
4401
- const lines = existing.split("\n");
4402
- let authStart = -1;
4403
- let authEnd = lines.length;
4404
- for (let i = 0; i < lines.length; i++) {
4405
- if (/^auth:/.test(lines[i])) {
4406
- authStart = i;
4407
- } else if (authStart >= 0 && i > authStart && /^\S/.test(lines[i]) && lines[i].trim() !== "") {
4408
- authEnd = i;
4409
- break;
4410
- }
4411
- }
4412
- if (authStart < 0) {
4413
- return existing.trimEnd() + "\n" + authYaml + "\n";
4414
- }
4415
- const before = lines.slice(0, authStart);
4416
- const after = lines.slice(authEnd);
4417
- return [...before, ...authYaml.trimEnd().split("\n"), ...after.length ? after : []].join("\n");
4418
- }
4419
4696
  function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4420
4697
  const rbacBlock = ` rbac: {
4421
4698
  roles: ${JSON.stringify(roles)},
@@ -4615,33 +4892,33 @@ OAUTH2_CLIENT_SECRET=your-client-secret
4615
4892
  `;
4616
4893
  }
4617
4894
  function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
4618
- const rbacSection = ` rbac:
4619
- roles:
4620
- ${rolesToYaml(roles, " ")}
4621
- defaultRole: ${defaultRole}
4622
- hierarchy:
4623
- ${hierarchyToYaml(hierarchy, " ")}`;
4624
- const auditSection = ` audit:
4625
- enabled: ${auditEnabled}
4626
- retentionDays: ${auditRetentionDays}`;
4627
- const routeLines = routesToYaml(protectedRoutes, " ");
4628
- const providerLine = params.provider ? ` provider: ${params.provider}
4895
+ const rbacSection = `rbac:
4896
+ roles:
4897
+ ${rolesToYaml(roles, " ")}
4898
+ defaultRole: ${defaultRole}
4899
+ hierarchy:
4900
+ ${hierarchyToYaml(hierarchy, " ")}`;
4901
+ const auditSection = `audit:
4902
+ enabled: ${auditEnabled}
4903
+ retentionDays: ${auditRetentionDays}`;
4904
+ const routeLines = routesToYaml(protectedRoutes, " ");
4905
+ const providerLine = params.provider ? `provider: ${params.provider}
4629
4906
  ` : "";
4630
4907
  if (method === "cac") {
4631
4908
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4632
4909
  const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4633
4910
  const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4634
4911
  const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4635
- return `auth:
4636
- method: cac
4637
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4638
- cac:
4639
- caBundle: \${CAC_CA_BUNDLE}
4640
- edipiLookup: ${edipiLookup}
4641
- ocspEndpoint: \${CAC_OCSP_ENDPOINT}
4642
- certHeader: ${certHeader}
4912
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4913
+ method: cac
4914
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4915
+ cac:
4916
+ caBundle: \${CAC_CA_BUNDLE}
4917
+ edipiLookup: ${edipiLookup}
4918
+ ocspEndpoint: \${CAC_OCSP_ENDPOINT}
4919
+ certHeader: ${certHeader}
4643
4920
  ${rbacSection}
4644
- protectedRoutes:
4921
+ protectedRoutes:
4645
4922
  ${routeLines}
4646
4923
  ${auditSection}
4647
4924
  `;
@@ -4649,33 +4926,33 @@ ${auditSection}
4649
4926
  if (method === "oidc") {
4650
4927
  const scopes2 = params.oidcScopes ?? "openid profile email";
4651
4928
  const roleClaim = params.oidcRoleClaim ?? "roles";
4652
- return `auth:
4653
- method: oidc
4654
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4655
- oidc:
4656
- discoveryUrl: \${OIDC_DISCOVERY_URL}
4657
- clientId: \${OIDC_CLIENT_ID}
4658
- clientSecret: \${OIDC_CLIENT_SECRET}
4659
- scopes: ${scopes2}
4660
- roleClaim: ${roleClaim}
4929
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4930
+ method: oidc
4931
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4932
+ oidc:
4933
+ discoveryUrl: \${OIDC_DISCOVERY_URL}
4934
+ clientId: \${OIDC_CLIENT_ID}
4935
+ clientSecret: \${OIDC_CLIENT_SECRET}
4936
+ scopes: ${scopes2}
4937
+ roleClaim: ${roleClaim}
4661
4938
  ${rbacSection}
4662
- protectedRoutes:
4939
+ protectedRoutes:
4663
4940
  ${routeLines}
4664
4941
  ${auditSection}
4665
4942
  `;
4666
4943
  }
4667
4944
  const scopes = params.oauth2Scopes ?? "read write";
4668
- return `auth:
4669
- method: oauth2
4670
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4671
- oauth2:
4672
- authorizationUrl: \${OAUTH2_AUTH_URL}
4673
- tokenUrl: \${OAUTH2_TOKEN_URL}
4674
- clientId: \${OAUTH2_CLIENT_ID}
4675
- clientSecret: \${OAUTH2_CLIENT_SECRET}
4676
- scopes: ${scopes}
4945
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
4946
+ method: oauth2
4947
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4948
+ oauth2:
4949
+ authorizationUrl: \${OAUTH2_AUTH_URL}
4950
+ tokenUrl: \${OAUTH2_TOKEN_URL}
4951
+ clientId: \${OAUTH2_CLIENT_ID}
4952
+ clientSecret: \${OAUTH2_CLIENT_SECRET}
4953
+ scopes: ${scopes}
4677
4954
  ${rbacSection}
4678
- protectedRoutes:
4955
+ protectedRoutes:
4679
4956
  ${routeLines}
4680
4957
  ${auditSection}
4681
4958
  `;
@@ -4861,35 +5138,35 @@ ${configBlock}
4861
5138
  `;
4862
5139
  }
4863
5140
  function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
4864
- const rbacSection = ` rbac:
4865
- roles:
4866
- ${rolesToYaml(roles, " ")}
4867
- defaultRole: ${defaultRole}
4868
- hierarchy:
4869
- ${hierarchyToYaml(hierarchy, " ")}`;
4870
- const auditSection = ` audit:
4871
- enabled: ${auditEnabled}
4872
- retentionDays: ${auditRetentionDays}`;
4873
- const routeLines = protectedRoutes.map((r) => ` - pattern: ${r.pattern}
4874
- requiredRole: ${r.requiredRole}`).join("\n");
4875
- const providerLine = params.provider ? ` provider: ${params.provider}
5141
+ const rbacSection = `rbac:
5142
+ roles:
5143
+ ${rolesToYaml(roles, " ")}
5144
+ defaultRole: ${defaultRole}
5145
+ hierarchy:
5146
+ ${hierarchyToYaml(hierarchy, " ")}`;
5147
+ const auditSection = `audit:
5148
+ enabled: ${auditEnabled}
5149
+ retentionDays: ${auditRetentionDays}`;
5150
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r.pattern}
5151
+ requiredRole: ${r.requiredRole}`).join("\n");
5152
+ const providerLine = params.provider ? `provider: ${params.provider}
4876
5153
  ` : "";
4877
5154
  if (method === "cac") {
4878
5155
  const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4879
5156
  const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4880
5157
  const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4881
5158
  const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4882
- return `auth:
4883
- method: cac
4884
- devOnly: true
4885
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4886
- cac:
4887
- caBundle: ${caBundle}
4888
- edipiLookup: ${edipiLookup}
4889
- ocspEndpoint: ${ocspEndpoint}
4890
- certHeader: ${certHeader}
5159
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5160
+ method: cac
5161
+ devOnly: true
5162
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5163
+ cac:
5164
+ caBundle: ${caBundle}
5165
+ edipiLookup: ${edipiLookup}
5166
+ ocspEndpoint: ${ocspEndpoint}
5167
+ certHeader: ${certHeader}
4891
5168
  ${rbacSection}
4892
- protectedRoutes:
5169
+ protectedRoutes:
4893
5170
  ${routeLines}
4894
5171
  ${auditSection}
4895
5172
  `;
@@ -4897,35 +5174,35 @@ ${auditSection}
4897
5174
  if (method === "oidc") {
4898
5175
  const scopes2 = params.oidcScopes ?? "openid profile email";
4899
5176
  const roleClaim = params.oidcRoleClaim ?? "roles";
4900
- return `auth:
4901
- method: oidc
4902
- devOnly: true
4903
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4904
- oidc:
4905
- discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
4906
- clientId: dev-mock-client
4907
- clientSecret: dev-mock-secret
4908
- scopes: ${scopes2}
4909
- roleClaim: ${roleClaim}
5177
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5178
+ method: oidc
5179
+ devOnly: true
5180
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5181
+ oidc:
5182
+ discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
5183
+ clientId: dev-mock-client
5184
+ clientSecret: dev-mock-secret
5185
+ scopes: ${scopes2}
5186
+ roleClaim: ${roleClaim}
4910
5187
  ${rbacSection}
4911
- protectedRoutes:
5188
+ protectedRoutes:
4912
5189
  ${routeLines}
4913
5190
  ${auditSection}
4914
5191
  `;
4915
5192
  }
4916
5193
  const scopes = params.oauth2Scopes ?? "read write";
4917
- return `auth:
4918
- method: oauth2
4919
- devOnly: true
4920
- ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4921
- oauth2:
4922
- authorizationUrl: https://dev-mock-oauth2/authorize
4923
- tokenUrl: https://dev-mock-oauth2/token
4924
- clientId: dev-mock-client
4925
- clientSecret: dev-mock-secret
4926
- scopes: ${scopes}
5194
+ return `# stackwright.auth.yml \u2014 generated by Auth Otter
5195
+ method: oauth2
5196
+ devOnly: true
5197
+ ${providerLine}${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
5198
+ oauth2:
5199
+ authorizationUrl: https://dev-mock-oauth2/authorize
5200
+ tokenUrl: https://dev-mock-oauth2/token
5201
+ clientId: dev-mock-client
5202
+ clientSecret: dev-mock-secret
5203
+ scopes: ${scopes}
4927
5204
  ${rbacSection}
4928
- protectedRoutes:
5205
+ protectedRoutes:
4929
5206
  ${routeLines}
4930
5207
  ${auditSection}
4931
5208
  `;
@@ -5070,7 +5347,7 @@ async function configureAuthHandler(params, cwd) {
5070
5347
  auditRetentionDays,
5071
5348
  normalizedRoutes
5072
5349
  );
5073
- writeFileSync6(join6(cwd, conventionFile), authFileContent, "utf8");
5350
+ writeFileSync6(join7(cwd, conventionFile), authFileContent, "utf8");
5074
5351
  filesWritten.push(conventionFile);
5075
5352
  } catch (err) {
5076
5353
  const msg = err instanceof Error ? err.message : String(err);
@@ -5090,9 +5367,9 @@ async function configureAuthHandler(params, cwd) {
5090
5367
  if (!devOnly) {
5091
5368
  try {
5092
5369
  const envBlock = generateEnvBlock(effectiveMethod, params);
5093
- const envPath = join6(cwd, ".env.example");
5370
+ const envPath = join7(cwd, ".env.example");
5094
5371
  if (existsSync7(envPath)) {
5095
- const existing = readFileSync6(envPath, "utf8");
5372
+ const existing = readFileSync7(envPath, "utf8");
5096
5373
  writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
5097
5374
  } else {
5098
5375
  writeFileSync6(envPath, envBlock, "utf8");
@@ -5113,10 +5390,10 @@ async function configureAuthHandler(params, cwd) {
5113
5390
  }
5114
5391
  if (devOnly) {
5115
5392
  try {
5116
- const mockAuthDir = join6(cwd, "lib");
5393
+ const mockAuthDir = join7(cwd, "lib");
5117
5394
  mkdirSync6(mockAuthDir, { recursive: true });
5118
5395
  const mockAuthContent = generateMockAuthContent(roles, mockUsers);
5119
- writeFileSync6(join6(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
5396
+ writeFileSync6(join7(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
5120
5397
  filesWritten.push("lib/mock-auth.ts");
5121
5398
  } catch (err) {
5122
5399
  const msg = err instanceof Error ? err.message : String(err);
@@ -5134,9 +5411,9 @@ async function configureAuthHandler(params, cwd) {
5134
5411
  };
5135
5412
  }
5136
5413
  try {
5137
- const pkgPath = join6(cwd, "package.json");
5414
+ const pkgPath = join7(cwd, "package.json");
5138
5415
  if (existsSync7(pkgPath)) {
5139
- const existingPkg = readFileSync6(pkgPath, "utf8");
5416
+ const existingPkg = readFileSync7(pkgPath, "utf8");
5140
5417
  const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
5141
5418
  writeFileSync6(pkgPath, updatedPkg, "utf8");
5142
5419
  filesWritten.push("package.json");
@@ -5179,21 +5456,18 @@ async function configureAuthHandler(params, cwd) {
5179
5456
  normalizedRoutes,
5180
5457
  useProxy
5181
5458
  );
5182
- const ymlPath = join6(cwd, "stackwright.yml");
5183
- if (!existsSync7(ymlPath)) {
5184
- writeFileSync6(ymlPath, authYaml, "utf8");
5185
- } else {
5186
- const existing = readFileSync6(ymlPath, "utf8");
5187
- writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
5188
- }
5189
- filesWritten.push("stackwright.yml");
5459
+ writeFileSync6(join7(cwd, "stackwright.auth.yml"), authYaml, "utf8");
5460
+ filesWritten.push("stackwright.auth.yml");
5190
5461
  } catch (err) {
5191
5462
  const msg = err instanceof Error ? err.message : String(err);
5192
5463
  return {
5193
5464
  content: [
5194
5465
  {
5195
5466
  type: "text",
5196
- text: JSON.stringify({ success: false, error: `Failed writing stackwright.yml: ${msg}` })
5467
+ text: JSON.stringify({
5468
+ success: false,
5469
+ error: `Failed writing stackwright.auth.yml: ${msg}`
5470
+ })
5197
5471
  }
5198
5472
  ],
5199
5473
  isError: true
@@ -5238,7 +5512,7 @@ async function configureAuthHandler(params, cwd) {
5238
5512
  function registerAuthTools(server2) {
5239
5513
  server2.tool(
5240
5514
  "stackwright_pro_configure_auth",
5241
- "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.",
5515
+ "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.",
5242
5516
  {
5243
5517
  method: z15.enum(["cac", "oidc", "oauth2", "none"]),
5244
5518
  provider: z15.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
@@ -5289,28 +5563,28 @@ function registerAuthTools(server2) {
5289
5563
 
5290
5564
  // src/integrity.ts
5291
5565
  import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
5292
- import { readFileSync as readFileSync7, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
5293
- import { join as join7, basename } from "path";
5566
+ import { readFileSync as readFileSync8, readdirSync as readdirSync3, lstatSync as lstatSync8 } from "fs";
5567
+ import { join as join8, basename } from "path";
5294
5568
  var _checksums = /* @__PURE__ */ new Map([
5295
5569
  [
5296
5570
  "stackwright-pro-api-otter.json",
5297
- "df79f4389a576c2885efa07b04f613c60eb8ebf4a8b1d4c7da5e4bb6ee4248dd"
5571
+ "822b35d7a330ed8ac0b42a63b0f70a41885aa9b5ea23cc5b8b998b549da4c05c"
5298
5572
  ],
5299
5573
  [
5300
5574
  "stackwright-pro-auth-otter.json",
5301
- "643344b88e42992e0467845fb0184d3932e115b85130fbc6a47a3175759678c8"
5575
+ "d6cd5732667018d99456be77d5955e454da7a0999a0330b5f03a483aa1b9b33f"
5302
5576
  ],
5303
5577
  [
5304
5578
  "stackwright-pro-dashboard-otter.json",
5305
- "160af221e04200f53709f8c3e249ca6dafb38a325b232fabd478b28f5492ab01"
5579
+ "19268b1ab423bfbb628ebfbc9f6767d5c0bfedd03963c6d445a2724a8bb78f9c"
5306
5580
  ],
5307
5581
  [
5308
5582
  "stackwright-pro-data-otter.json",
5309
- "709c8e49328908549fe87de181a5991e09c918ef24bbfe6948f9888f0c758c25"
5583
+ "bb66eaab31c6872536dca4d3ff145724a46356946dd0665cc4f0346714d3bacb"
5310
5584
  ],
5311
5585
  [
5312
5586
  "stackwright-pro-designer-otter.json",
5313
- "1364b2c235c07b0b798e9aab90a68604f60019a5508d41ba576977f173e20cd9"
5587
+ "b54121c6a2ab5b1ad68c1262c58b2e04e6315feacd6bb8de8e78eb36f2fa6be6"
5314
5588
  ],
5315
5589
  [
5316
5590
  "stackwright-pro-domain-expert-otter.json",
@@ -5318,35 +5592,39 @@ var _checksums = /* @__PURE__ */ new Map([
5318
5592
  ],
5319
5593
  [
5320
5594
  "stackwright-pro-foreman-otter.json",
5321
- "438b03d2c35f64536055c68b3a9044fe3b5e4f2889acde4500dd801fad724be1"
5595
+ "abb1cc5e40a8c5ff98057273f43a9e90e2791a15951090cd4d98407c0c3618e3"
5322
5596
  ],
5323
5597
  [
5324
5598
  "stackwright-pro-form-wizard-otter.json",
5325
- "3dffa3ef2d2ef057578391f784cbea779d768e87d98321894ea5bcd9e3ab7e60"
5599
+ "975ad68e8fb6fe5a10373be278946fa4d9d7ec2238a0b2bd9e4a412e5b1fdd6d"
5326
5600
  ],
5327
5601
  [
5328
5602
  "stackwright-pro-geo-otter.json",
5329
- "2ec83c26a08c413d9553ff8b8f0f0f643c1a8da043741b356e27106cad78f05f"
5603
+ "eef152e5b58947c8eb1ffec2c37acffc39f84b61be4c6cb827310f052221935b"
5330
5604
  ],
5331
5605
  [
5332
5606
  "stackwright-pro-page-otter.json",
5333
- "0057ea97f7c2e33a8673140f59a0237eb627d82c676af3fae4faa31033598e03"
5607
+ "ddfa497a4d4980080fa2918aec4d6dfd78d5c28ec6426b4b52f4f26fccaddabb"
5334
5608
  ],
5335
5609
  [
5336
5610
  "stackwright-pro-polish-otter.json",
5337
- "bd87327b9a9a62fabaee8837492ce943feae8bfc15e5d43b45ba0e84619daec3"
5611
+ "5e6532c1fe0737ed83b1f46dd55642e1076adb6ef23d4de636523eb3d88d3087"
5612
+ ],
5613
+ [
5614
+ "stackwright-pro-qa-otter.json",
5615
+ "ecb1e76170723fce43f515044e304d9da32253dadecbf29bf08c90bb8f375f77"
5338
5616
  ],
5339
5617
  [
5340
5618
  "stackwright-pro-scaffold-otter.json",
5341
- "91de5861f1406043d1d387388302fb1492211b81e2eea9777dec60e48f317f2a"
5619
+ "96ac7754b54c15732206cd4d8043b558d0c465757a0bc70fae6b498d6f02f22a"
5342
5620
  ],
5343
5621
  [
5344
5622
  "stackwright-pro-theme-otter.json",
5345
- "fe10108e3ba67106751ea662f512bb5f4eb58f7abda26880ef4cf6b0f9feb944"
5623
+ "fb62e56642f7f94c50ce173f3e1ce978b3f20ab1567e87403b901d6dd991a7db"
5346
5624
  ],
5347
5625
  [
5348
5626
  "stackwright-services-otter.json",
5349
- "c013d7fc2475e62d0af54d57bc988182feee7766bac0edf841cbfad5c73e9261"
5627
+ "0a5dac670482871c718099767b30bf23d8ec57e942bd40a00ce295926d82c6bd"
5350
5628
  ]
5351
5629
  ]);
5352
5630
  Object.freeze(_checksums);
@@ -5359,6 +5637,7 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
5359
5637
  );
5360
5638
  }
5361
5639
  }
5640
+ var CANONICAL_OTTERS = Object.freeze([...CANONICAL_CHECKSUMS.keys()]);
5362
5641
  var MAX_OTTER_BYTES = 1 * 1024 * 1024;
5363
5642
  function computeSha256(data) {
5364
5643
  return createHash4("sha256").update(data).digest("hex");
@@ -5375,7 +5654,7 @@ function verifyOtterFile(filePath) {
5375
5654
  }
5376
5655
  let stat;
5377
5656
  try {
5378
- stat = lstatSync7(filePath);
5657
+ stat = lstatSync8(filePath);
5379
5658
  } catch (err) {
5380
5659
  const msg = err instanceof Error ? err.message : String(err);
5381
5660
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -5393,7 +5672,7 @@ function verifyOtterFile(filePath) {
5393
5672
  }
5394
5673
  let raw;
5395
5674
  try {
5396
- raw = readFileSync7(filePath);
5675
+ raw = readFileSync8(filePath);
5397
5676
  } catch (err) {
5398
5677
  const msg = err instanceof Error ? err.message : String(err);
5399
5678
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -5443,7 +5722,7 @@ function verifyAllOtters(otterDir) {
5443
5722
  const unknown = [];
5444
5723
  let entries;
5445
5724
  try {
5446
- entries = readdirSync2(otterDir);
5725
+ entries = readdirSync3(otterDir);
5447
5726
  } catch (err) {
5448
5727
  const msg = err instanceof Error ? err.message : String(err);
5449
5728
  return {
@@ -5454,9 +5733,9 @@ function verifyAllOtters(otterDir) {
5454
5733
  }
5455
5734
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
5456
5735
  for (const filename of otterFiles) {
5457
- const filePath = join7(otterDir, filename);
5736
+ const filePath = join8(otterDir, filename);
5458
5737
  try {
5459
- if (lstatSync7(filePath).isSymbolicLink()) {
5738
+ if (lstatSync8(filePath).isSymbolicLink()) {
5460
5739
  failed.push({ filename, error: "Skipped: symlink" });
5461
5740
  continue;
5462
5741
  }
@@ -5482,9 +5761,9 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
5482
5761
  function resolveOtterDir() {
5483
5762
  const cwd = process.cwd();
5484
5763
  for (const relative of DEFAULT_SEARCH_PATHS) {
5485
- const candidate = join7(cwd, relative);
5764
+ const candidate = join8(cwd, relative);
5486
5765
  try {
5487
- lstatSync7(candidate);
5766
+ lstatSync8(candidate);
5488
5767
  return candidate;
5489
5768
  } catch {
5490
5769
  }
@@ -5563,13 +5842,13 @@ function registerIntegrityTools(server2) {
5563
5842
 
5564
5843
  // src/tools/domain.ts
5565
5844
  import { z as z16 } from "zod";
5566
- import { readFileSync as readFileSync8, existsSync as existsSync8 } from "fs";
5567
- import { join as join8 } from "path";
5845
+ import { readFileSync as readFileSync9, existsSync as existsSync8 } from "fs";
5846
+ import { join as join9 } from "path";
5568
5847
  function handleListCollections(input) {
5569
5848
  const cwd = input._cwd ?? process.cwd();
5570
5849
  const sources = [
5571
5850
  {
5572
- path: join8(cwd, ".stackwright", "artifacts", "data-config.json"),
5851
+ path: join9(cwd, ".stackwright", "artifacts", "data-config.json"),
5573
5852
  source: "data-config.json",
5574
5853
  parse: (raw) => {
5575
5854
  const parsed = JSON.parse(raw);
@@ -5580,15 +5859,15 @@ function handleListCollections(input) {
5580
5859
  }
5581
5860
  },
5582
5861
  {
5583
- path: join8(cwd, "stackwright.yml"),
5862
+ path: join9(cwd, "stackwright.yml"),
5584
5863
  source: "stackwright.yml",
5585
5864
  parse: extractCollectionsFromYaml
5586
5865
  }
5587
5866
  ];
5588
- for (const { path: path3, source, parse } of sources) {
5589
- if (!existsSync8(path3)) continue;
5867
+ for (const { path: path4, source, parse } of sources) {
5868
+ if (!existsSync8(path4)) continue;
5590
5869
  try {
5591
- const collections = parse(readFileSync8(path3, "utf8"));
5870
+ const collections = parse(readFileSync9(path4, "utf8"));
5592
5871
  return {
5593
5872
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
5594
5873
  isError: false
@@ -5739,7 +6018,7 @@ function handleValidateWorkflow(input) {
5739
6018
  if (input.workflow && Object.keys(input.workflow).length > 0) {
5740
6019
  raw = input.workflow;
5741
6020
  } else {
5742
- const artifactPath = join8(cwd, ".stackwright", "artifacts", "workflow-config.json");
6021
+ const artifactPath = join9(cwd, ".stackwright", "artifacts", "workflow-config.json");
5743
6022
  if (!existsSync8(artifactPath)) {
5744
6023
  return fail([
5745
6024
  {
@@ -5749,7 +6028,7 @@ function handleValidateWorkflow(input) {
5749
6028
  ]);
5750
6029
  }
5751
6030
  try {
5752
- raw = JSON.parse(readFileSync8(artifactPath, "utf8"));
6031
+ raw = JSON.parse(readFileSync9(artifactPath, "utf8"));
5753
6032
  } catch (err) {
5754
6033
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
5755
6034
  }
@@ -5861,12 +6140,12 @@ function handleValidateWorkflow(input) {
5861
6140
  return { text: JSON.stringify({ valid: errors.length === 0, errors, warnings }), isError: false };
5862
6141
  }
5863
6142
  function validateTransitionTargets(step, stepId, stepIds, errors) {
5864
- const check = (target, path3) => {
6143
+ const check = (target, path4) => {
5865
6144
  if (typeof target === "string" && !stepIds.has(target)) {
5866
6145
  errors.push({
5867
6146
  code: "ORPHANED_TRANSITION",
5868
6147
  message: `Step "${stepId}" transitions to "${target}" which does not exist`,
5869
- path: path3
6148
+ path: path4
5870
6149
  });
5871
6150
  }
5872
6151
  };
@@ -5905,11 +6184,11 @@ function validateTransitionTargets(step, stepId, stepIds, errors) {
5905
6184
  }
5906
6185
  function collectServiceWarnings(step, stepId, warnings) {
5907
6186
  const isService = (val) => typeof val === "string" && val.startsWith("service:");
5908
- const warn = (path3) => {
6187
+ const warn = (path4) => {
5909
6188
  warnings.push({
5910
6189
  code: "WARN_SERVICE_REFERENCE",
5911
- message: `service: reference at "${path3}" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.`,
5912
- path: path3
6190
+ message: `service: reference at "${path4}" requires @stackwright-pro/services. Prism mock fallback will be used until services layer is configured.`,
6191
+ path: path4
5913
6192
  });
5914
6193
  };
5915
6194
  const onSubmit = step.on_submit;
@@ -6059,15 +6338,15 @@ function registerTypeSchemasTool(server2) {
6059
6338
  // src/tools/scaffold-cleanup.ts
6060
6339
  import {
6061
6340
  existsSync as existsSync9,
6062
- lstatSync as lstatSync8,
6341
+ lstatSync as lstatSync9,
6063
6342
  unlinkSync as unlinkSync2,
6064
- readFileSync as readFileSync9,
6343
+ readFileSync as readFileSync10,
6065
6344
  writeFileSync as writeFileSync7,
6066
- readdirSync as readdirSync3,
6345
+ readdirSync as readdirSync4,
6067
6346
  rmdirSync,
6068
6347
  mkdirSync as mkdirSync7
6069
6348
  } from "fs";
6070
- import { join as join9, dirname as dirname2 } from "path";
6349
+ import { join as join10, dirname as dirname2 } from "path";
6071
6350
  var SCAFFOLD_FILES_TO_DELETE = [
6072
6351
  "content/posts/getting-started.yaml",
6073
6352
  "content/posts/hello-world.yaml"
@@ -6092,12 +6371,12 @@ var FALLBACK_COLORS = {
6092
6371
  mutedForeground: "#737373"
6093
6372
  };
6094
6373
  function readThemeColors(cwd) {
6095
- const tokenPath = join9(cwd, ".stackwright", "artifacts", "theme-tokens.json");
6374
+ const tokenPath = join10(cwd, ".stackwright", "artifacts", "theme-tokens.json");
6096
6375
  if (!existsSync9(tokenPath)) return { ...FALLBACK_COLORS };
6097
- const stat = lstatSync8(tokenPath);
6376
+ const stat = lstatSync9(tokenPath);
6098
6377
  if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
6099
6378
  try {
6100
- const raw = JSON.parse(readFileSync9(tokenPath, "utf-8"));
6379
+ const raw = JSON.parse(readFileSync10(tokenPath, "utf-8"));
6101
6380
  const css = raw?.cssVariables ?? {};
6102
6381
  const toHsl = (hslValue) => {
6103
6382
  if (!hslValue || typeof hslValue !== "string") return null;
@@ -6158,14 +6437,14 @@ function generateNotFoundPage(colors) {
6158
6437
  }
6159
6438
  function isSafePath(fullPath) {
6160
6439
  if (!existsSync9(fullPath)) return true;
6161
- const stat = lstatSync8(fullPath);
6440
+ const stat = lstatSync9(fullPath);
6162
6441
  return !stat.isSymbolicLink();
6163
6442
  }
6164
6443
  function isDirEmpty(dirPath) {
6165
6444
  if (!existsSync9(dirPath)) return false;
6166
- const stat = lstatSync8(dirPath);
6445
+ const stat = lstatSync9(dirPath);
6167
6446
  if (!stat.isDirectory()) return false;
6168
- return readdirSync3(dirPath).length === 0;
6447
+ return readdirSync4(dirPath).length === 0;
6169
6448
  }
6170
6449
  function handleCleanupScaffold(_cwd) {
6171
6450
  const cwd = _cwd ?? process.cwd();
@@ -6178,7 +6457,7 @@ function handleCleanupScaffold(_cwd) {
6178
6457
  cleanupWarnings: []
6179
6458
  };
6180
6459
  for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
6181
- const fullPath = join9(cwd, relPath);
6460
+ const fullPath = join10(cwd, relPath);
6182
6461
  if (!existsSync9(fullPath)) {
6183
6462
  result.skipped.push(relPath);
6184
6463
  continue;
@@ -6196,13 +6475,13 @@ function handleCleanupScaffold(_cwd) {
6196
6475
  }
6197
6476
  {
6198
6477
  const relPath = "pages/getting-started/content.yml";
6199
- const fullPath = join9(cwd, relPath);
6478
+ const fullPath = join10(cwd, relPath);
6200
6479
  if (!existsSync9(fullPath)) {
6201
6480
  result.skipped.push(relPath);
6202
6481
  } else if (!isSafePath(fullPath)) {
6203
6482
  result.errors.push(`Refusing to delete symlink: ${relPath}`);
6204
6483
  } else {
6205
- const content = readFileSync9(fullPath, "utf-8");
6484
+ const content = readFileSync10(fullPath, "utf-8");
6206
6485
  const isScaffoldDefault = GETTING_STARTED_SCAFFOLD_FINGERPRINTS.some(
6207
6486
  (marker) => content.includes(marker)
6208
6487
  );
@@ -6222,8 +6501,8 @@ function handleCleanupScaffold(_cwd) {
6222
6501
  }
6223
6502
  {
6224
6503
  const relPath = `content/posts/${POSTS_COLLECTION_FILE}`;
6225
- const fullPath = join9(cwd, relPath);
6226
- const postsDir = join9(cwd, "content/posts");
6504
+ const fullPath = join10(cwd, relPath);
6505
+ const postsDir = join10(cwd, "content/posts");
6227
6506
  if (!existsSync9(fullPath)) {
6228
6507
  result.skipped.push(relPath);
6229
6508
  } else if (!isSafePath(fullPath)) {
@@ -6231,7 +6510,7 @@ function handleCleanupScaffold(_cwd) {
6231
6510
  } else if (!existsSync9(postsDir)) {
6232
6511
  result.skipped.push(relPath);
6233
6512
  } else {
6234
- const userPostFiles = readdirSync3(postsDir).filter(
6513
+ const userPostFiles = readdirSync4(postsDir).filter(
6235
6514
  (f) => f !== POSTS_COLLECTION_FILE && POST_FILE_EXTENSIONS.some((ext) => f.endsWith(ext))
6236
6515
  );
6237
6516
  if (userPostFiles.length === 0) {
@@ -6249,7 +6528,7 @@ function handleCleanupScaffold(_cwd) {
6249
6528
  }
6250
6529
  }
6251
6530
  for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
6252
- const fullDir = join9(cwd, relDir);
6531
+ const fullDir = join10(cwd, relDir);
6253
6532
  if (!existsSync9(fullDir)) continue;
6254
6533
  if (!isSafePath(fullDir)) {
6255
6534
  result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
@@ -6265,7 +6544,7 @@ function handleCleanupScaffold(_cwd) {
6265
6544
  }
6266
6545
  }
6267
6546
  {
6268
- const fullPath = join9(cwd, NOT_FOUND_PATH);
6547
+ const fullPath = join10(cwd, NOT_FOUND_PATH);
6269
6548
  if (!existsSync9(fullPath)) {
6270
6549
  result.skipped.push(NOT_FOUND_PATH);
6271
6550
  } else if (!isSafePath(fullPath)) {
@@ -6543,12 +6822,258 @@ function registerContrastTools(server2) {
6543
6822
  );
6544
6823
  }
6545
6824
 
6825
+ // src/tools/compile.ts
6826
+ import { z as z19 } from "zod";
6827
+ import fs2 from "fs";
6828
+ import path3 from "path";
6829
+ import { compileCollections } from "@stackwright-pro/pulse/server";
6830
+ import { compileAuth } from "@stackwright-pro/auth-nextjs/server";
6831
+ import { compileIntegrations } from "@stackwright-pro/openapi/server";
6832
+ function buildContext(projectRoot) {
6833
+ return {
6834
+ projectRoot,
6835
+ contentOutDir: path3.join(projectRoot, "public", "stackwright-content"),
6836
+ imagesDir: path3.join(projectRoot, "public", "images"),
6837
+ siteConfig: loadSiteConfig(projectRoot)
6838
+ };
6839
+ }
6840
+ function loadSiteConfig(projectRoot) {
6841
+ try {
6842
+ const sitePath = path3.join(projectRoot, "public", "stackwright-content", "_site.json");
6843
+ return JSON.parse(fs2.readFileSync(sitePath, "utf8"));
6844
+ } catch {
6845
+ return {};
6846
+ }
6847
+ }
6848
+ function formatDuration(startMs) {
6849
+ const ms = Date.now() - startMs;
6850
+ return ms < 1e3 ? `${ms}ms` : `${(ms / 1e3).toFixed(2)}s`;
6851
+ }
6852
+ async function tryOssCompile(fn, ctx, extra) {
6853
+ try {
6854
+ const bsPath = path3.join(ctx.projectRoot, "node_modules", "@stackwright", "build-scripts");
6855
+ if (!fs2.existsSync(bsPath)) {
6856
+ return {
6857
+ ok: false,
6858
+ error: `@stackwright/build-scripts not found at ${bsPath}. Is it installed in your project?`
6859
+ };
6860
+ }
6861
+ const bs = await import(bsPath);
6862
+ if (typeof bs[fn] !== "function") {
6863
+ return { ok: false, error: `${fn} not exported from @stackwright/build-scripts` };
6864
+ }
6865
+ await bs[fn](ctx, ...extra ? [extra] : []);
6866
+ return { ok: true };
6867
+ } catch (err) {
6868
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
6869
+ }
6870
+ }
6871
+ function registerCompileTools(server2) {
6872
+ const projectRootArg = z19.string().optional().describe("Absolute path to project root (defaults to cwd)");
6873
+ server2.tool(
6874
+ "stackwright_pro_compile_collections",
6875
+ "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.",
6876
+ { projectRoot: projectRootArg },
6877
+ async ({ projectRoot }) => {
6878
+ const start = Date.now();
6879
+ const root = projectRoot ?? process.cwd();
6880
+ try {
6881
+ await compileCollections(buildContext(root));
6882
+ return {
6883
+ content: [
6884
+ {
6885
+ type: "text",
6886
+ text: ` _collections.json written (${formatDuration(start)})`
6887
+ }
6888
+ ]
6889
+ };
6890
+ } catch (err) {
6891
+ return {
6892
+ content: [
6893
+ {
6894
+ type: "text",
6895
+ text: ` compile_collections failed: ${err instanceof Error ? err.message : String(err)}`
6896
+ }
6897
+ ]
6898
+ };
6899
+ }
6900
+ }
6901
+ );
6902
+ server2.tool(
6903
+ "stackwright_pro_compile_auth",
6904
+ "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.",
6905
+ { projectRoot: projectRootArg },
6906
+ async ({ projectRoot }) => {
6907
+ const start = Date.now();
6908
+ const root = projectRoot ?? process.cwd();
6909
+ try {
6910
+ await compileAuth(buildContext(root));
6911
+ return {
6912
+ content: [
6913
+ {
6914
+ type: "text",
6915
+ text: ` _auth.json written (${formatDuration(start)})`
6916
+ }
6917
+ ]
6918
+ };
6919
+ } catch (err) {
6920
+ return {
6921
+ content: [
6922
+ {
6923
+ type: "text",
6924
+ text: ` compile_auth failed: ${err instanceof Error ? err.message : String(err)}`
6925
+ }
6926
+ ]
6927
+ };
6928
+ }
6929
+ }
6930
+ );
6931
+ server2.tool(
6932
+ "stackwright_pro_compile_integrations",
6933
+ "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).",
6934
+ { projectRoot: projectRootArg },
6935
+ async ({ projectRoot }) => {
6936
+ const start = Date.now();
6937
+ const root = projectRoot ?? process.cwd();
6938
+ try {
6939
+ await compileIntegrations(buildContext(root));
6940
+ return {
6941
+ content: [
6942
+ {
6943
+ type: "text",
6944
+ text: ` _integrations.json written (${formatDuration(start)})`
6945
+ }
6946
+ ]
6947
+ };
6948
+ } catch (err) {
6949
+ return {
6950
+ content: [
6951
+ {
6952
+ type: "text",
6953
+ text: ` compile_integrations failed: ${err instanceof Error ? err.message : String(err)}`
6954
+ }
6955
+ ]
6956
+ };
6957
+ }
6958
+ }
6959
+ );
6960
+ server2.tool(
6961
+ "stackwright_pro_compile_all",
6962
+ "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.",
6963
+ { projectRoot: projectRootArg },
6964
+ async ({ projectRoot }) => {
6965
+ const start = Date.now();
6966
+ const root = projectRoot ?? process.cwd();
6967
+ const ctx = buildContext(root);
6968
+ const results = [];
6969
+ const errors = [];
6970
+ for (const [label, fn] of [
6971
+ ["collections", () => compileCollections(ctx)],
6972
+ ["auth", () => compileAuth(ctx)],
6973
+ ["integrations", () => compileIntegrations(ctx)]
6974
+ ]) {
6975
+ try {
6976
+ await fn();
6977
+ results.push(` _${label}.json`);
6978
+ } catch (err) {
6979
+ errors.push(` ${label}: ${err instanceof Error ? err.message : String(err)}`);
6980
+ }
6981
+ }
6982
+ const summary = errors.length > 0 ? ` compile_all finished with errors (${formatDuration(start)}):
6983
+ ${results.join("\n")}
6984
+ Errors:
6985
+ ${errors.join("\n")}` : ` compile_all complete (${formatDuration(start)}):
6986
+ ${results.join("\n")}`;
6987
+ return { content: [{ type: "text", text: summary }] };
6988
+ }
6989
+ );
6990
+ server2.tool(
6991
+ "stackwright_pro_compile_theme",
6992
+ "Compile stackwright.yml theme config \u2192 _theme.json. Delegates to compileTheme from @stackwright/build-scripts installed in the project.",
6993
+ { projectRoot: projectRootArg },
6994
+ async ({ projectRoot }) => {
6995
+ const start = Date.now();
6996
+ const root = projectRoot ?? process.cwd();
6997
+ const { ok, error } = await tryOssCompile("compileTheme", buildContext(root));
6998
+ return {
6999
+ content: [
7000
+ {
7001
+ type: "text",
7002
+ text: ok ? ` _theme.json written (${formatDuration(start)})` : ` compile_theme failed: ${error}`
7003
+ }
7004
+ ]
7005
+ };
7006
+ }
7007
+ );
7008
+ server2.tool(
7009
+ "stackwright_pro_compile_site",
7010
+ "Compile stackwright.yml \u2192 _site.json. Delegates to compileSite from @stackwright/build-scripts installed in the project.",
7011
+ { projectRoot: projectRootArg },
7012
+ async ({ projectRoot }) => {
7013
+ const start = Date.now();
7014
+ const root = projectRoot ?? process.cwd();
7015
+ const { ok, error } = await tryOssCompile("compileSite", buildContext(root));
7016
+ return {
7017
+ content: [
7018
+ {
7019
+ type: "text",
7020
+ text: ok ? ` _site.json written (${formatDuration(start)})` : ` compile_site failed: ${error}`
7021
+ }
7022
+ ]
7023
+ };
7024
+ }
7025
+ );
7026
+ server2.tool(
7027
+ "stackwright_pro_compile_pages",
7028
+ "Compile all pages content.yml files \u2192 page JSON files. Delegates to compilePages from @stackwright/build-scripts installed in the project.",
7029
+ { projectRoot: projectRootArg },
7030
+ async ({ projectRoot }) => {
7031
+ const start = Date.now();
7032
+ const root = projectRoot ?? process.cwd();
7033
+ const { ok, error } = await tryOssCompile("compilePages", buildContext(root));
7034
+ return {
7035
+ content: [
7036
+ {
7037
+ type: "text",
7038
+ text: ok ? ` pages compiled (${formatDuration(start)})` : ` compile_pages failed: ${error}`
7039
+ }
7040
+ ]
7041
+ };
7042
+ }
7043
+ );
7044
+ server2.tool(
7045
+ "stackwright_pro_compile_page",
7046
+ "Compile a single page by slug. Delegates to compilePage from @stackwright/build-scripts installed in the project.",
7047
+ {
7048
+ slug: z19.string().describe('Page slug (relative path under pages/, e.g. "dashboard")'),
7049
+ projectRoot: projectRootArg
7050
+ },
7051
+ async ({ slug, projectRoot }) => {
7052
+ const start = Date.now();
7053
+ const root = projectRoot ?? process.cwd();
7054
+ const { ok, error } = await tryOssCompile("compilePage", buildContext(root), { slug });
7055
+ return {
7056
+ content: [
7057
+ {
7058
+ type: "text",
7059
+ text: ok ? ` page "${slug}" compiled (${formatDuration(start)})` : ` compile_page failed: ${error}`
7060
+ }
7061
+ ]
7062
+ };
7063
+ }
7064
+ );
7065
+ }
7066
+
6546
7067
  // package.json
6547
7068
  var package_default = {
6548
7069
  dependencies: {
6549
7070
  "@modelcontextprotocol/sdk": "^1.10.0",
7071
+ "@stackwright-pro/auth-nextjs": "workspace:*",
6550
7072
  "@stackwright-pro/cli-data-explorer": "workspace:*",
7073
+ "@stackwright-pro/openapi": "workspace:*",
7074
+ "@stackwright-pro/pulse": "workspace:*",
6551
7075
  "@stackwright-pro/types": "workspace:*",
7076
+ "@stackwright/mcp": "0.6.0-alpha.1",
6552
7077
  "@types/js-yaml": "^4.0.9",
6553
7078
  "js-yaml": "^4.2.0",
6554
7079
  zod: "^4.4.3"
@@ -6568,7 +7093,7 @@ var package_default = {
6568
7093
  "test:coverage": "vitest run --coverage"
6569
7094
  },
6570
7095
  name: "@stackwright-pro/mcp",
6571
- version: "0.2.0-alpha.92",
7096
+ version: "0.2.0-alpha.97",
6572
7097
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
6573
7098
  license: "SEE LICENSE IN LICENSE",
6574
7099
  main: "./dist/server.js",
@@ -6592,6 +7117,11 @@ var package_default = {
6592
7117
  types: "./dist/tools/type-schemas.d.ts",
6593
7118
  import: "./dist/tools/type-schemas.mjs",
6594
7119
  require: "./dist/tools/type-schemas.js"
7120
+ },
7121
+ "./pipeline-constants": {
7122
+ types: "./dist/pipeline-constants.d.ts",
7123
+ import: "./dist/pipeline-constants.mjs",
7124
+ require: "./dist/pipeline-constants.js"
6595
7125
  }
6596
7126
  },
6597
7127
  files: [
@@ -6603,6 +7133,20 @@ var package_default = {
6603
7133
  };
6604
7134
 
6605
7135
  // src/server.ts
7136
+ import {
7137
+ registerContentTypeTools,
7138
+ registerPageTools,
7139
+ registerSiteTools,
7140
+ registerProjectTools,
7141
+ registerGitOpsTools,
7142
+ registerBoardTools,
7143
+ registerCollectionTools,
7144
+ registerIntegrationTools,
7145
+ registerComposeTools,
7146
+ registerRenderTools,
7147
+ registerA11yTools,
7148
+ closeBrowser
7149
+ } from "@stackwright/mcp/register";
6606
7150
  var server = new McpServer({
6607
7151
  name: "stackwright-pro",
6608
7152
  version: package_default.version
@@ -6626,7 +7170,45 @@ registerValidateYamlFragmentTool(server);
6626
7170
  registerGetSchemaTool(server);
6627
7171
  registerScaffoldCleanupTools(server);
6628
7172
  registerContrastTools(server);
7173
+ registerCompileTools(server);
7174
+ registerContentTypeTools(server);
7175
+ registerPageTools(server);
7176
+ registerSiteTools(server);
7177
+ registerProjectTools(server);
7178
+ registerGitOpsTools(server);
7179
+ registerBoardTools(server);
7180
+ registerCollectionTools(server);
7181
+ registerIntegrationTools(server);
7182
+ registerComposeTools(server);
7183
+ registerRenderTools(server);
7184
+ registerA11yTools(server);
7185
+ process.on("SIGINT", async () => {
7186
+ const forceExit = setTimeout(() => process.exit(1), 3e3);
7187
+ forceExit.unref();
7188
+ await closeBrowser();
7189
+ process.exit(0);
7190
+ });
7191
+ process.on("SIGTERM", async () => {
7192
+ const forceExit = setTimeout(() => process.exit(1), 3e3);
7193
+ forceExit.unref();
7194
+ await closeBrowser();
7195
+ process.exit(0);
7196
+ });
6629
7197
  async function main() {
7198
+ const pipelineGraph = loadPipelineGraph();
7199
+ const phasePosition = new Map(PHASE_ORDER.map((p, i) => [p, i]));
7200
+ for (const [phase, deps] of Object.entries(pipelineGraph.dependencies)) {
7201
+ const phaseIdx = phasePosition.get(phase);
7202
+ if (phaseIdx === void 0) continue;
7203
+ for (const dep of deps) {
7204
+ const depIdx = phasePosition.get(dep);
7205
+ if (depIdx !== void 0 && depIdx >= phaseIdx) {
7206
+ throw new Error(
7207
+ `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.`
7208
+ );
7209
+ }
7210
+ }
7211
+ }
6630
7212
  const transport = new StdioServerTransport();
6631
7213
  try {
6632
7214
  const servicesRegisterPkg = "@stackwright-services/mcp/register";