@stackwright-pro/mcp 0.2.0-alpha.8 → 0.2.0-alpha.80

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.js CHANGED
@@ -27,15 +27,44 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
27
27
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
28
28
 
29
29
  // src/tools/data-explorer.ts
30
- var import_zod = require("zod");
30
+ var import_zod2 = require("zod");
31
31
  var import_cli_data_explorer = require("@stackwright-pro/cli-data-explorer");
32
+
33
+ // src/coerce.ts
34
+ var import_zod = require("zod");
35
+ function jsonCoerce(schema) {
36
+ return import_zod.z.preprocess((v) => {
37
+ if (typeof v === "string") {
38
+ try {
39
+ return JSON.parse(v);
40
+ } catch {
41
+ return v;
42
+ }
43
+ }
44
+ return v;
45
+ }, schema);
46
+ }
47
+ function boolCoerce(schema) {
48
+ return import_zod.z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, schema);
49
+ }
50
+ function numCoerce(schema) {
51
+ return import_zod.z.preprocess((v) => {
52
+ if (typeof v === "string" && v.trim() !== "") {
53
+ const n = Number(v);
54
+ if (!Number.isNaN(n)) return n;
55
+ }
56
+ return v;
57
+ }, schema);
58
+ }
59
+
60
+ // src/tools/data-explorer.ts
32
61
  function registerDataExplorerTools(server2) {
33
62
  server2.tool(
34
63
  "stackwright_pro_list_entities",
35
64
  "List all available API entities from OpenAPI specs or generated Zod schemas. Use this to discover what entities are available before generating endpoint filters. Returns entity names, endpoints, and field counts. Part of the Pro Otter Raft for building API-integrated Stackwright applications.",
36
65
  {
37
- specPath: import_zod.z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
38
- projectRoot: import_zod.z.string().optional().describe("Project root directory (auto-detected if omitted)")
66
+ specPath: import_zod2.z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
67
+ projectRoot: import_zod2.z.string().optional().describe("Project root directory (auto-detected if omitted)")
39
68
  },
40
69
  async ({ specPath, projectRoot }) => {
41
70
  const result = (0, import_cli_data_explorer.listEntities)({
@@ -83,9 +112,13 @@ function registerDataExplorerTools(server2) {
83
112
  "stackwright_pro_generate_filter",
84
113
  "Generate endpoint filter configuration from selected entities. Creates include/exclude patterns for stackwright.yml OpenAPI integration. Use this after stackwright_pro_list_entities to select which API endpoints the application needs. Only selected endpoints will generate client code, reducing bundle size and improving security.",
85
114
  {
86
- selectedEntities: import_zod.z.array(import_zod.z.string()).describe('Entity slugs to include (e.g., ["equipment", "supplies"])'),
87
- excludePatterns: import_zod.z.array(import_zod.z.string()).optional().describe('Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'),
88
- projectRoot: import_zod.z.string().optional().describe("Project root directory")
115
+ selectedEntities: jsonCoerce(import_zod2.z.array(import_zod2.z.string())).describe(
116
+ 'Entity slugs to include (e.g., ["equipment", "supplies"])'
117
+ ),
118
+ excludePatterns: jsonCoerce(import_zod2.z.array(import_zod2.z.string()).optional()).describe(
119
+ 'Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'
120
+ ),
121
+ projectRoot: import_zod2.z.string().optional().describe("Project root directory")
89
122
  },
90
123
  async ({ selectedEntities, excludePatterns, projectRoot }) => {
91
124
  const result = (0, import_cli_data_explorer.generateFilter)({
@@ -141,7 +174,7 @@ function registerDataExplorerTools(server2) {
141
174
  }
142
175
 
143
176
  // src/tools/security.ts
144
- var import_zod2 = require("zod");
177
+ var import_zod3 = require("zod");
145
178
  var import_crypto = require("crypto");
146
179
  var import_fs = __toESM(require("fs"));
147
180
  var import_path = __toESM(require("path"));
@@ -150,8 +183,8 @@ function registerSecurityTools(server2) {
150
183
  "stackwright_pro_validate_spec",
151
184
  "Validate an OpenAPI spec against the enterprise approved-specs configuration. Checks if the spec URL is on the allowlist and verifies SHA-256 hash integrity. Use this in enterprise environments where only pre-approved API specs are allowed. Fails build if spec is not approved or has been modified.",
152
185
  {
153
- specPath: import_zod2.z.string().describe("URL or file path to the OpenAPI spec to validate"),
154
- configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml with prebuild.security config")
186
+ specPath: import_zod3.z.string().describe("URL or file path to the OpenAPI spec to validate"),
187
+ configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml with prebuild.security config")
155
188
  },
156
189
  async ({ specPath, configPath }) => {
157
190
  let securityEnabled = false;
@@ -259,16 +292,15 @@ Status: Valid (${allowlist.length} specs on allowlist)`
259
292
  "stackwright_pro_add_approved_spec",
260
293
  "Add an OpenAPI spec to the approved-specs allowlist in stackwright.yml. Computes the SHA-256 hash of the spec and adds it to the security configuration. Use this when onboarding a new API in enterprise environments.",
261
294
  {
262
- name: import_zod2.z.string().describe("Human-readable name for the spec"),
263
- url: import_zod2.z.string().describe("URL or file path to the OpenAPI spec"),
264
- configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml")
295
+ name: import_zod3.z.string().describe("Human-readable name for the spec"),
296
+ url: import_zod3.z.string().describe("URL or file path to the OpenAPI spec"),
297
+ configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
265
298
  },
266
299
  async ({ name, url, configPath }) => {
267
300
  const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
268
301
  let sha256 = "<computed-at-build>";
269
302
  try {
270
303
  if (url.startsWith("http://") || url.startsWith("https://")) {
271
- sha256 = "<computed-at-build>";
272
304
  } else if (import_fs.default.existsSync(url)) {
273
305
  const specContent = import_fs.default.readFileSync(url, "utf8");
274
306
  sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
@@ -315,7 +347,7 @@ SHA-256 computed: ${sha256}`
315
347
  "stackwright_pro_list_approved_specs",
316
348
  "List all specs currently on the approved-specs allowlist. Shows spec names, URLs, and hash prefixes. Use this to audit what APIs are approved in the project.",
317
349
  {
318
- configPath: import_zod2.z.string().optional().describe("Path to stackwright.yml")
350
+ configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
319
351
  },
320
352
  async ({ configPath }) => {
321
353
  const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
@@ -384,16 +416,18 @@ SHA-256 computed: ${sha256}`
384
416
  }
385
417
 
386
418
  // src/tools/isr.ts
387
- var import_zod3 = require("zod");
419
+ var import_zod4 = require("zod");
388
420
  function registerIsrTools(server2) {
389
421
  server2.tool(
390
422
  "stackwright_pro_configure_isr",
391
423
  "Configure Incremental Static Regeneration (ISR) for an API-backed collection. ISR allows API data to be cached and refreshed on a schedule, providing real-time data with the performance of static generation. Use this after stackwright_pro_generate_filter to set revalidation intervals.",
392
424
  {
393
- collection: import_zod3.z.string().describe('Collection name (e.g., "equipment", "supplies")'),
394
- revalidateSeconds: import_zod3.z.number().optional().describe("Revalidation interval in seconds (default: 60)"),
395
- fallback: import_zod3.z.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
396
- configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
425
+ collection: import_zod4.z.string().describe('Collection name (e.g., "equipment", "supplies")'),
426
+ revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe(
427
+ "Revalidation interval in seconds (default: 60)"
428
+ ),
429
+ fallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
430
+ configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
397
431
  },
398
432
  async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
399
433
  const revalidate = revalidateSeconds;
@@ -404,7 +438,7 @@ function registerIsrTools(server2) {
404
438
  isr:
405
439
  revalidate: ${revalidate}
406
440
  fallback: ${fallback}`;
407
- let description = "";
441
+ let description;
408
442
  if (revalidate < 60) {
409
443
  description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
410
444
  } else if (revalidate < 300) {
@@ -436,14 +470,16 @@ ${yamlSnippet}
436
470
  "stackwright_pro_configure_isr_batch",
437
471
  "Configure ISR for multiple collections at once. More efficient than calling stackwright_pro_configure_isr multiple times. Provide different revalidation intervals based on data freshness requirements.",
438
472
  {
439
- collections: import_zod3.z.array(
440
- import_zod3.z.object({
441
- name: import_zod3.z.string().describe("Collection name"),
442
- revalidateSeconds: import_zod3.z.number().optional().describe("Revalidation interval")
443
- })
473
+ collections: jsonCoerce(
474
+ import_zod4.z.array(
475
+ import_zod4.z.object({
476
+ name: import_zod4.z.string().describe("Collection name"),
477
+ revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe("Revalidation interval")
478
+ })
479
+ )
444
480
  ).describe("Array of collection configurations"),
445
- defaultFallback: import_zod3.z.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
446
- configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
481
+ defaultFallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
482
+ configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
447
483
  },
448
484
  async ({ collections, defaultFallback = "blocking", configPath }) => {
449
485
  const lines = [`\u2699\uFE0F Batch ISR Configuration:
@@ -471,17 +507,17 @@ Fallback: ${defaultFallback}`);
471
507
  }
472
508
 
473
509
  // src/tools/dashboard.ts
474
- var import_zod4 = require("zod");
510
+ var import_zod5 = require("zod");
475
511
  var import_cli_data_explorer2 = require("@stackwright-pro/cli-data-explorer");
476
512
  function registerDashboardTools(server2) {
477
513
  server2.tool(
478
514
  "stackwright_pro_generate_dashboard",
479
515
  "Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with grid, metric_card, data_table, and collection_list content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
480
516
  {
481
- entities: import_zod4.z.array(import_zod4.z.string()).describe("Entity slugs to include in dashboard"),
482
- layout: import_zod4.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
483
- pageTitle: import_zod4.z.string().optional().describe("Page title"),
484
- specPath: import_zod4.z.string().optional().describe("Path to OpenAPI spec for entity details")
517
+ entities: jsonCoerce(import_zod5.z.array(import_zod5.z.string())).describe("Entity slugs to include in dashboard"),
518
+ layout: import_zod5.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
519
+ pageTitle: import_zod5.z.string().optional().describe("Page title"),
520
+ specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for entity details")
485
521
  },
486
522
  async ({ entities, layout = "mixed", pageTitle, specPath }) => {
487
523
  let entityDetails = [];
@@ -567,9 +603,9 @@ ${yaml}
567
603
  "stackwright_pro_generate_detail_page",
568
604
  "Generate a detail view page for a single API entity. Creates YAML content for displaying all fields of an individual record. Use this to create detail pages that complement collection listing pages.",
569
605
  {
570
- entity: import_zod4.z.string().describe('Entity slug (e.g., "equipment")'),
571
- slugField: import_zod4.z.string().optional().describe('Field to use as URL slug (default: "id")'),
572
- specPath: import_zod4.z.string().optional().describe("Path to OpenAPI spec for field details")
606
+ entity: import_zod5.z.string().describe('Entity slug (e.g., "equipment")'),
607
+ slugField: import_zod5.z.string().optional().describe('Field to use as URL slug (default: "id")'),
608
+ specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for field details")
573
609
  },
574
610
  async ({ entity, slugField = "id", specPath }) => {
575
611
  let fields = [];
@@ -592,39 +628,49 @@ ${yaml}
592
628
  ` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
593
629
  "",
594
630
  " content_items:",
595
- " - main:",
596
- ' label: "detail-header"',
597
- " heading:",
598
- ` text: "{{ ${entity}.${slugField} }}"`,
599
- ' textSize: "h1"',
600
- " textBlocks:",
601
- ` - text: "Details for this ${entity}"`,
602
- ' textSize: "body1"',
603
- ' background: "primary"',
604
- ' color: "text"',
631
+ " - type: text_block",
632
+ ' label: "detail-header"',
633
+ " heading:",
634
+ ` text: "{{ ${entity}.${slugField} }}"`,
635
+ ' textSize: "h1"',
636
+ " textBlocks:",
637
+ ` - text: "Details for this ${entity}"`,
638
+ ' textSize: "body1"',
605
639
  "",
606
- " - grid:",
607
- ' label: "detail-fields"',
608
- " columns: 2",
609
- " items:"
640
+ " - type: grid",
641
+ ' label: "detail-fields"',
642
+ " columns:"
610
643
  ];
611
644
  const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
612
645
  { name: slugField, type: "string" },
613
646
  { name: "created_at", type: "datetime" },
614
647
  { name: "updated_at", type: "datetime" }
615
648
  ];
616
- for (const field of displayFields) {
617
- const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
618
- yamlLines.push(` - card:`);
619
- yamlLines.push(` label: "${field.name}-field"`);
620
- yamlLines.push(` heading:`);
621
- yamlLines.push(` text: "${label}"`);
622
- yamlLines.push(` textSize: "h4"`);
623
- yamlLines.push(` content:`);
624
- yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
625
- yamlLines.push(` textSize: "body1"`);
626
- }
627
- yamlLines.push(' background: "background"');
649
+ const mid = Math.ceil(displayFields.length / 2);
650
+ const col1 = displayFields.slice(0, mid);
651
+ const col2 = displayFields.slice(mid);
652
+ for (const [colIndex, colFields] of [col1, col2].entries()) {
653
+ yamlLines.push(" - width: 1");
654
+ yamlLines.push(" content_items:");
655
+ for (const field of colFields) {
656
+ const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
657
+ yamlLines.push(" - type: text_block");
658
+ yamlLines.push(` label: "${field.name}-field"`);
659
+ yamlLines.push(" heading:");
660
+ yamlLines.push(` text: "${label}"`);
661
+ yamlLines.push(' textSize: "h4"');
662
+ yamlLines.push(" textBlocks:");
663
+ yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
664
+ yamlLines.push(' textSize: "body1"');
665
+ }
666
+ }
667
+ yamlLines.push("");
668
+ yamlLines.push(" - type: action_bar");
669
+ yamlLines.push(" actions:");
670
+ yamlLines.push(` - label: "<- Back to ${entityName}"`);
671
+ yamlLines.push(" action: navigate");
672
+ yamlLines.push(` href: "/${entity}"`);
673
+ yamlLines.push(" style: secondary");
628
674
  const yaml = yamlLines.join("\n");
629
675
  return {
630
676
  content: [
@@ -645,7 +691,7 @@ ${yaml}
645
691
  }
646
692
 
647
693
  // src/tools/clarification.ts
648
- var import_zod5 = require("zod");
694
+ var import_zod6 = require("zod");
649
695
  var CONTRADICTION_PATTERNS = [
650
696
  {
651
697
  keywords: ["minimal", "clean", "simple"],
@@ -733,12 +779,14 @@ function registerClarificationTools(server2) {
733
779
  "stackwright_pro_clarify",
734
780
  "Ask the user for clarification when a specialist otter encounters ambiguity. This is for MID-EXECUTION questions, NOT upfront question collection (use the Question Manifest Protocol for that). Returns a structured response for the foreman to present to the user via ask_user_question (closed_choice) or directly (open_text).",
735
781
  {
736
- context: import_zod5.z.string().optional().describe("Context about what the otter is trying to do"),
737
- question_type: import_zod5.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
738
- question: import_zod5.z.string().describe("The clarification question to ask the user"),
739
- choices: import_zod5.z.array(import_zod5.z.string()).optional().describe("Options for closed_choice questions"),
740
- priority: import_zod5.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
741
- target_field: import_zod5.z.string().optional().describe("What field/config does this clarify?")
782
+ context: import_zod6.z.string().optional().describe("Context about what the otter is trying to do"),
783
+ question_type: import_zod6.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
784
+ question: import_zod6.z.string().describe("The clarification question to ask the user"),
785
+ choices: jsonCoerce(import_zod6.z.array(import_zod6.z.string()).optional()).describe(
786
+ "Options for closed_choice questions"
787
+ ),
788
+ priority: import_zod6.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
789
+ target_field: import_zod6.z.string().optional().describe("What field/config does this clarify?")
742
790
  },
743
791
  async ({ context, question_type, question, choices, priority, target_field }) => {
744
792
  const result = handleClarify({
@@ -767,8 +815,10 @@ function registerClarificationTools(server2) {
767
815
  "stackwright_pro_detect_conflict",
768
816
  "Detect when a user's stated preference conflicts with their selected choices. Uses keyword heuristics against known contradiction patterns (minimal vs vibrant, dark vs light, etc). Returns conflict details and resolution options.",
769
817
  {
770
- stated_preference: import_zod5.z.string().describe("What the user said they wanted"),
771
- selected_values: import_zod5.z.record(import_zod5.z.string(), import_zod5.z.string()).describe("What the user actually selected (field \u2192 value)")
818
+ stated_preference: import_zod6.z.string().describe("What the user said they wanted"),
819
+ selected_values: jsonCoerce(import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string())).describe(
820
+ "What the user actually selected (field \u2192 value)"
821
+ )
772
822
  },
773
823
  async ({ stated_preference, selected_values }) => {
774
824
  const result = handleDetectConflict({ stated_preference, selected_values });
@@ -813,7 +863,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
813
863
  }
814
864
 
815
865
  // src/tools/packages.ts
816
- var import_zod6 = require("zod");
866
+ var import_zod7 = require("zod");
817
867
  var import_fs2 = require("fs");
818
868
  var import_child_process = require("child_process");
819
869
  var import_path2 = __toESM(require("path"));
@@ -834,17 +884,23 @@ function registerPackageTools(server2) {
834
884
  "Ensures pro packages are present in a project's package.json. Safe to call multiple times \u2014 never overwrites existing version pins. Use this to bootstrap dependencies before specialist otters run. Pass includeBaseline: true to automatically include all required @stackwright-pro/* baseline dependencies. Safe to call on existing projects \u2014 never overwrites pinned versions.",
835
885
  {
836
886
  // FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
837
- packages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).describe(
838
- 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
887
+ packages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional().default({})).describe(
888
+ 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
889
+ ),
890
+ devPackages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
891
+ "devDependencies to add. Same format as packages."
839
892
  ),
840
- devPackages: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("devDependencies to add. Same format as packages."),
841
- scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional().describe("npm scripts to add. Only adds if key does not already exist."),
842
- targetDir: import_zod6.z.string().optional().describe(
893
+ scripts: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
894
+ "npm scripts to add. Only adds if key does not already exist."
895
+ ),
896
+ targetDir: import_zod7.z.string().optional().describe(
843
897
  "Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
844
898
  ),
845
- runInstall: import_zod6.z.boolean().optional().default(true).describe("Run pnpm install after writing package.json. Defaults to true."),
846
- includeBaseline: import_zod6.z.boolean().optional().default(false).describe(
847
- "When true, automatically merges BASELINE_DEPS and BASELINE_DEV_DEPS before applying packages/devPackages args. Safe to call on existing projects \u2014 never overwrites pinned versions."
899
+ runInstall: boolCoerce(import_zod7.z.boolean().optional().default(true)).describe(
900
+ "Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
901
+ ),
902
+ includeBaseline: boolCoerce(import_zod7.z.boolean().optional().default(false)).describe(
903
+ "When true, automatically merges BASELINE_DEPS and BASELINE_DEV_DEPS before applying packages/devPackages args. Safe to call on existing projects \u2014 never overwrites pinned versions. Pass boolean true/false."
848
904
  )
849
905
  },
850
906
  async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
@@ -1002,11 +1058,11 @@ function setupPackages(opts) {
1002
1058
  }
1003
1059
  }
1004
1060
  const raw = (0, import_fs2.readFileSync)(realPackageJsonPath, "utf8");
1005
- const PackageJsonSchema = import_zod6.z.object({
1061
+ const PackageJsonSchema = import_zod7.z.object({
1006
1062
  // Zod v4: z.record(keySchema, valueSchema) — two-arg form required
1007
- dependencies: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional(),
1008
- devDependencies: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional(),
1009
- scripts: import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string()).optional()
1063
+ dependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
1064
+ devDependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
1065
+ scripts: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()
1010
1066
  }).passthrough();
1011
1067
  const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
1012
1068
  if (!schemaResult.success) {
@@ -1088,8 +1144,9 @@ function setupPackages(opts) {
1088
1144
 
1089
1145
  // src/tools/questions.ts
1090
1146
  var import_promises = require("fs/promises");
1147
+ var import_node_fs = require("fs");
1091
1148
  var import_node_path = require("path");
1092
- var import_zod7 = require("zod");
1149
+ var import_zod8 = require("zod");
1093
1150
 
1094
1151
  // src/question-adapter.ts
1095
1152
  function truncate(str, maxLength) {
@@ -1274,23 +1331,30 @@ function answersToManifestFormat(answers, questions) {
1274
1331
  return result;
1275
1332
  }
1276
1333
 
1334
+ // src/validation.ts
1335
+ var SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1336
+ var SAFE_QUESTION_ID = /^[a-z0-9][a-z0-9-]{0,63}$/i;
1337
+ function isValidPhase(phase) {
1338
+ return SAFE_PHASE.test(phase);
1339
+ }
1340
+
1277
1341
  // src/tools/questions.ts
1278
- var ManifestQuestionSchema = import_zod7.z.object({
1279
- id: import_zod7.z.string(),
1280
- question: import_zod7.z.string(),
1281
- type: import_zod7.z.enum(["text", "select", "multi-select", "confirm"]),
1282
- required: import_zod7.z.boolean().optional(),
1283
- options: import_zod7.z.array(
1284
- import_zod7.z.object({
1285
- label: import_zod7.z.string(),
1286
- value: import_zod7.z.string()
1342
+ var ManifestQuestionSchema = import_zod8.z.object({
1343
+ id: import_zod8.z.string(),
1344
+ question: import_zod8.z.string(),
1345
+ type: import_zod8.z.enum(["text", "select", "multi-select", "confirm"]),
1346
+ required: import_zod8.z.boolean().optional(),
1347
+ options: import_zod8.z.array(
1348
+ import_zod8.z.object({
1349
+ label: import_zod8.z.string(),
1350
+ value: import_zod8.z.string()
1287
1351
  })
1288
1352
  ).optional(),
1289
- default: import_zod7.z.union([import_zod7.z.string(), import_zod7.z.boolean(), import_zod7.z.array(import_zod7.z.string())]).optional(),
1290
- help: import_zod7.z.string().optional(),
1291
- dependsOn: import_zod7.z.object({
1292
- questionId: import_zod7.z.string(),
1293
- value: import_zod7.z.union([import_zod7.z.string(), import_zod7.z.array(import_zod7.z.string())])
1353
+ default: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.boolean(), import_zod8.z.array(import_zod8.z.string())]).optional(),
1354
+ help: import_zod8.z.string().optional(),
1355
+ dependsOn: import_zod8.z.object({
1356
+ questionId: import_zod8.z.string(),
1357
+ value: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string())])
1294
1358
  }).optional()
1295
1359
  });
1296
1360
  function registerQuestionTools(server2) {
@@ -1298,15 +1362,16 @@ function registerQuestionTools(server2) {
1298
1362
  "stackwright_pro_present_phase_questions",
1299
1363
  "Adapt manifest-format questions from a specialist otter and present them to the user via ask_user_question. Pass only the phase name \u2014 this tool reads questions from .stackwright/question-manifest.json automatically. The questions parameter is optional and only needed if the manifest has not been written yet. Use this instead of calling ask_user_question directly \u2014 it handles label truncation (50-char limit), header generation, confirm/text defaults, and correct array formatting automatically. IMPORTANT: This is the ONLY approved way to prepare questions before calling ask_user_question. Never call ask_user_question with raw manifest questions. Never retry ask_user_question validation errors by calling it directly \u2014 always re-call this tool.",
1300
1364
  {
1301
- phase: import_zod7.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1302
- questions: import_zod7.z.array(ManifestQuestionSchema).optional().describe(
1365
+ phase: import_zod8.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1366
+ questions: jsonCoerce(import_zod8.z.array(ManifestQuestionSchema).optional()).describe(
1303
1367
  "Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
1304
1368
  ),
1305
- answers: import_zod7.z.record(import_zod7.z.union([import_zod7.z.string(), import_zod7.z.array(import_zod7.z.string()), import_zod7.z.boolean()])).optional().describe("Previously collected answers used to resolve dependsOn conditions")
1369
+ answers: jsonCoerce(
1370
+ import_zod8.z.record(import_zod8.z.string(), import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string()), import_zod8.z.boolean()])).optional()
1371
+ ).describe("Previously collected answers used to resolve dependsOn conditions")
1306
1372
  },
1307
1373
  async ({ phase, questions, answers }) => {
1308
1374
  let resolvedQuestions;
1309
- const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1310
1375
  if (!SAFE_PHASE.test(phase)) {
1311
1376
  return {
1312
1377
  content: [
@@ -1353,17 +1418,36 @@ function registerQuestionTools(server2) {
1353
1418
  }
1354
1419
  }
1355
1420
  const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
1421
+ const labelSchema = import_zod8.z.string().max(50, "Value should have at most 50 characters");
1422
+ for (const q of adapted) {
1423
+ for (const opt of q.options) {
1424
+ const check = labelSchema.safeParse(opt.label);
1425
+ if (!check.success) {
1426
+ return {
1427
+ content: [
1428
+ {
1429
+ type: "text",
1430
+ text: JSON.stringify({
1431
+ error: `Option label for phase "${phase}" exceeds 50 characters: ${check.error.issues[0]?.message ?? "label too long"}. Truncate option labels to \u226450 characters before calling this tool.`
1432
+ })
1433
+ }
1434
+ ],
1435
+ isError: true
1436
+ };
1437
+ }
1438
+ }
1439
+ }
1356
1440
  if (adapted.length === 0) {
1357
1441
  return {
1358
1442
  content: [
1359
1443
  {
1360
1444
  type: "text",
1361
- text: JSON.stringify({
1362
- phase,
1363
- skipped: true,
1364
- reason: "No questions to present (all filtered by dependsOn conditions)",
1365
- answers: []
1366
- })
1445
+ text: `Phase "${phase}" has no questions to present. Do NOT call ask_user_question. Call stackwright_pro_save_phase_answers({ phase: "${phase}", rawAnswers: [] }) directly, then proceed to the execution step for this phase.`
1446
+ },
1447
+ {
1448
+ type: "text",
1449
+ // Empty array — second block always present so the foreman's two-block contract holds
1450
+ text: JSON.stringify([])
1367
1451
  }
1368
1452
  ],
1369
1453
  isError: false
@@ -1384,10 +1468,146 @@ function registerQuestionTools(server2) {
1384
1468
  };
1385
1469
  }
1386
1470
  );
1471
+ server2.tool(
1472
+ "stackwright_pro_get_next_question",
1473
+ "Returns the next unanswered question for a phase as a plain JSON object \u2014 one question at a time. Returns { done: true } when all questions are answered or the phase has no questions. Call this in a loop: present the question in plain chat, get user reply, call record_answer, repeat.",
1474
+ { phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
1475
+ async ({ phase }) => {
1476
+ if (!SAFE_PHASE.test(phase)) {
1477
+ return {
1478
+ content: [
1479
+ {
1480
+ type: "text",
1481
+ text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
1482
+ }
1483
+ ],
1484
+ isError: true
1485
+ };
1486
+ }
1487
+ const cwd = process.cwd();
1488
+ const questionsPath = (0, import_node_path.join)(cwd, ".stackwright", "questions", `${phase}.json`);
1489
+ let questions = [];
1490
+ try {
1491
+ const raw = await (0, import_promises.readFile)(questionsPath, "utf-8");
1492
+ const parsed = JSON.parse(raw);
1493
+ questions = parsed.questions ?? [];
1494
+ } catch {
1495
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1496
+ }
1497
+ if (questions.length === 0) {
1498
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1499
+ }
1500
+ const answersPath = (0, import_node_path.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1501
+ let answeredIds = /* @__PURE__ */ new Set();
1502
+ try {
1503
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1504
+ const parsed = JSON.parse(raw);
1505
+ answeredIds = new Set(Object.keys(parsed.answers ?? {}));
1506
+ } catch {
1507
+ }
1508
+ const next = questions.find((q) => !answeredIds.has(q.id));
1509
+ if (!next) {
1510
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1511
+ }
1512
+ return {
1513
+ content: [
1514
+ {
1515
+ type: "text",
1516
+ text: JSON.stringify({
1517
+ done: false,
1518
+ questionId: next.id,
1519
+ question: next.question,
1520
+ type: next.type,
1521
+ options: next.options ?? null,
1522
+ help: next.help ?? null,
1523
+ index: questions.indexOf(next) + 1,
1524
+ total: questions.length
1525
+ })
1526
+ }
1527
+ ]
1528
+ };
1529
+ }
1530
+ );
1531
+ server2.tool(
1532
+ "stackwright_pro_record_answer",
1533
+ "Records a single answer to a phase question. Appends to .stackwright/answers/{phase}.json incrementally. Idempotent \u2014 calling twice for the same questionId overwrites. Call after receiving each user reply in the conversational question loop.",
1534
+ {
1535
+ phase: import_zod8.z.string().describe('Phase name e.g. "designer"'),
1536
+ questionId: import_zod8.z.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
1537
+ answer: import_zod8.z.string().describe("The user's free-text answer")
1538
+ },
1539
+ async ({ phase, questionId, answer }) => {
1540
+ if (!SAFE_PHASE.test(phase)) {
1541
+ return {
1542
+ content: [
1543
+ { type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
1544
+ ],
1545
+ isError: true
1546
+ };
1547
+ }
1548
+ if (!SAFE_QUESTION_ID.test(questionId)) {
1549
+ return {
1550
+ content: [
1551
+ { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
1552
+ ],
1553
+ isError: true
1554
+ };
1555
+ }
1556
+ const safeAnswer = answer.slice(0, 2e3);
1557
+ const cwd = process.cwd();
1558
+ const answersDir = (0, import_node_path.join)(cwd, ".stackwright", "answers");
1559
+ const answersPath = (0, import_node_path.join)(answersDir, `${phase}.json`);
1560
+ if ((0, import_node_fs.existsSync)(answersDir) && (0, import_node_fs.lstatSync)(answersDir).isSymbolicLink()) {
1561
+ return {
1562
+ content: [
1563
+ {
1564
+ type: "text",
1565
+ text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
1566
+ }
1567
+ ],
1568
+ isError: true
1569
+ };
1570
+ }
1571
+ (0, import_node_fs.mkdirSync)(answersDir, { recursive: true });
1572
+ let existing = {
1573
+ version: "1.0",
1574
+ phase,
1575
+ answers: {}
1576
+ };
1577
+ try {
1578
+ if ((0, import_node_fs.existsSync)(answersPath)) {
1579
+ if ((0, import_node_fs.lstatSync)(answersPath).isSymbolicLink()) {
1580
+ return {
1581
+ content: [
1582
+ {
1583
+ type: "text",
1584
+ text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
1585
+ }
1586
+ ],
1587
+ isError: true
1588
+ };
1589
+ }
1590
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1591
+ existing = JSON.parse(raw);
1592
+ }
1593
+ } catch {
1594
+ }
1595
+ existing.answers[questionId] = safeAnswer;
1596
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1597
+ const tmp = `${answersPath}.tmp`;
1598
+ await (0, import_promises.writeFile)(tmp, JSON.stringify(existing, null, 2), "utf-8");
1599
+ (0, import_node_fs.renameSync)(tmp, answersPath);
1600
+ return {
1601
+ content: [
1602
+ { type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
1603
+ ]
1604
+ };
1605
+ }
1606
+ );
1387
1607
  }
1388
1608
 
1389
1609
  // src/tools/orchestration.ts
1390
- var import_zod8 = require("zod");
1610
+ var import_zod9 = require("zod");
1391
1611
  var import_fs3 = require("fs");
1392
1612
  var import_path3 = require("path");
1393
1613
  var OTTER_NAME_TO_PHASE = [
@@ -1398,7 +1618,9 @@ var OTTER_NAME_TO_PHASE = [
1398
1618
  ["dashboard", "dashboard"],
1399
1619
  ["data", "data"],
1400
1620
  ["page", "pages"],
1401
- ["workflow", "workflow"]
1621
+ ["workflow", "workflow"],
1622
+ ["polish", "polish"],
1623
+ ["geo", "geo"]
1402
1624
  ];
1403
1625
  var PHASE_TO_OTTER = {
1404
1626
  designer: "stackwright-pro-designer-otter",
@@ -1408,7 +1630,9 @@ var PHASE_TO_OTTER = {
1408
1630
  pages: "stackwright-pro-page-otter",
1409
1631
  dashboard: "stackwright-pro-dashboard-otter",
1410
1632
  data: "stackwright-pro-data-otter",
1411
- workflow: "stackwright-pro-workflow-otter"
1633
+ workflow: "stackwright-pro-workflow-otter",
1634
+ polish: "stackwright-pro-polish-otter",
1635
+ geo: "stackwright-pro-geo-otter"
1412
1636
  };
1413
1637
  function detectPhase(otterName) {
1414
1638
  const lower = otterName.toLowerCase();
@@ -1474,14 +1698,78 @@ function handleSaveManifest(input) {
1474
1698
  }
1475
1699
  }
1476
1700
  function handleSavePhaseAnswers(input) {
1701
+ if (!isValidPhase(input.phase)) {
1702
+ return {
1703
+ text: JSON.stringify({
1704
+ success: false,
1705
+ error: `Invalid phase name: "${input.phase.slice(0, 50)}". Must be lowercase alphanumeric with hyphens, max 31 chars.`
1706
+ }),
1707
+ isError: true
1708
+ };
1709
+ }
1710
+ for (let i = 0; i < input.rawAnswers.length; i++) {
1711
+ const a = input.rawAnswers[i];
1712
+ if (!a.question_header || a.question_header.trim().length === 0) {
1713
+ return {
1714
+ text: JSON.stringify({
1715
+ success: false,
1716
+ error: `rawAnswers[${i}].question_header is empty \u2014 every answer must have a non-empty question_header.`
1717
+ }),
1718
+ isError: true
1719
+ };
1720
+ }
1721
+ if (!a.selected_options || a.selected_options.length === 0) {
1722
+ return {
1723
+ text: JSON.stringify({
1724
+ success: false,
1725
+ error: `rawAnswers[${i}].selected_options is empty for question_header "${a.question_header}" \u2014 every answer must have at least one selected option.`
1726
+ }),
1727
+ isError: true
1728
+ };
1729
+ }
1730
+ }
1731
+ if (input.questions && input.questions.length > 0 && input.rawAnswers.length === 0) {
1732
+ return {
1733
+ text: JSON.stringify({
1734
+ success: false,
1735
+ error: `${input.questions.length} questions provided but rawAnswers is empty \u2014 this is a malformed payload. If the phase truly has no questions, omit the questions parameter.`
1736
+ }),
1737
+ isError: true
1738
+ };
1739
+ }
1477
1740
  const cwd = input._cwd ?? process.cwd();
1478
1741
  const dir = (0, import_path3.join)(cwd, ".stackwright", "answers");
1479
1742
  const filePath = (0, import_path3.join)(dir, `${input.phase}.json`);
1480
1743
  try {
1481
1744
  (0, import_fs3.mkdirSync)(dir, { recursive: true });
1745
+ const warnings = [];
1482
1746
  let answers;
1483
1747
  if (input.questions && input.questions.length > 0) {
1484
- answers = answersToManifestFormat(input.rawAnswers, input.questions);
1748
+ try {
1749
+ answers = answersToManifestFormat(input.rawAnswers, input.questions);
1750
+ } catch (mapErr) {
1751
+ answers = {};
1752
+ warnings.push(
1753
+ `Reverse-mapping threw (${mapErr instanceof Error ? mapErr.message : String(mapErr)}) \u2014 falling back to direct key mapping.`
1754
+ );
1755
+ }
1756
+ if (Object.keys(answers).length === 0 && input.rawAnswers.length > 0) {
1757
+ answers = Object.fromEntries(
1758
+ input.rawAnswers.map((a) => [
1759
+ a.question_header,
1760
+ a.selected_options.length > 1 ? a.selected_options : a.selected_options[0] ?? ""
1761
+ ])
1762
+ );
1763
+ warnings.push(
1764
+ `Reverse-mapping produced empty answers despite ${input.rawAnswers.length} rawAnswers \u2014 used direct key mapping as fallback. Ensure question_header values match the manifest header format (e.g. "DESI-1", not descriptive keys).`
1765
+ );
1766
+ }
1767
+ const requiredCount = input.questions.filter((q) => q.required !== false).length;
1768
+ if (input.rawAnswers.length < requiredCount) {
1769
+ warnings.push(
1770
+ `Only ${input.rawAnswers.length} answers provided for ${requiredCount} required questions (${input.questions.length} total) \u2014 answers may be incomplete.`
1771
+ );
1772
+ }
1485
1773
  } else {
1486
1774
  answers = Object.fromEntries(
1487
1775
  input.rawAnswers.map((a) => [a.question_header, a.selected_options[0] ?? ""])
@@ -1508,7 +1796,8 @@ function handleSavePhaseAnswers(input) {
1508
1796
  text: JSON.stringify({
1509
1797
  success: true,
1510
1798
  path: filePath,
1511
- answersCount: Object.keys(answers).length
1799
+ answersCount: Object.keys(answers).length,
1800
+ ...warnings.length > 0 ? { warnings } : {}
1512
1801
  }),
1513
1802
  isError: false
1514
1803
  };
@@ -1555,13 +1844,63 @@ function handleGetOtterName(input) {
1555
1844
  isError: false
1556
1845
  };
1557
1846
  }
1847
+ function handleSaveBuildContext(input) {
1848
+ const cwd = input._cwd ?? process.cwd();
1849
+ const dir = (0, import_path3.join)(cwd, ".stackwright");
1850
+ const filePath = (0, import_path3.join)(dir, "build-context.json");
1851
+ try {
1852
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
1853
+ if ((0, import_fs3.existsSync)(filePath)) {
1854
+ const stat = (0, import_fs3.lstatSync)(filePath);
1855
+ if (stat.isSymbolicLink()) {
1856
+ return {
1857
+ text: JSON.stringify({
1858
+ success: false,
1859
+ error: `Refusing to write to symlink: ${filePath}`
1860
+ }),
1861
+ isError: true
1862
+ };
1863
+ }
1864
+ }
1865
+ const payload = {
1866
+ version: "1.0",
1867
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1868
+ buildContext: input.buildContext
1869
+ };
1870
+ (0, import_fs3.writeFileSync)(filePath, JSON.stringify(payload, null, 2) + "\n");
1871
+ return {
1872
+ text: JSON.stringify({ success: true, path: filePath }),
1873
+ isError: false
1874
+ };
1875
+ } catch (err) {
1876
+ const message = err instanceof Error ? err.message : String(err);
1877
+ return {
1878
+ text: JSON.stringify({ success: false, error: message }),
1879
+ isError: true
1880
+ };
1881
+ }
1882
+ }
1558
1883
  function registerOrchestrationTools(server2) {
1884
+ server2.tool(
1885
+ "stackwright_pro_save_build_context",
1886
+ `Save the user's initial build description to .stackwright/build-context.json. Call this once at startup after the user answers the opening "what are you building" question. The saved context is automatically prepended to specialist prompts by stackwright_pro_build_specialist_prompt.`,
1887
+ {
1888
+ buildContext: import_zod9.z.string().describe("Free-text description of what the user wants to build")
1889
+ },
1890
+ async ({ buildContext }) => {
1891
+ const { text, isError } = handleSaveBuildContext({ buildContext });
1892
+ return {
1893
+ content: [{ type: "text", text }],
1894
+ isError
1895
+ };
1896
+ }
1897
+ );
1559
1898
  server2.tool(
1560
1899
  "stackwright_pro_parse_otter_response",
1561
1900
  "Parse and validate a specialist otter's QUESTION_COLLECTION_MODE JSON response. Handles JSON extraction from LLM responses (strips markdown, fixes single quotes, trailing commas). Detects the phase from the otter name. Use this immediately after invoke_agent() to get a validated manifest phase object.",
1562
1901
  {
1563
- otterName: import_zod8.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1564
- responseText: import_zod8.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1902
+ otterName: import_zod9.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1903
+ responseText: import_zod9.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1565
1904
  },
1566
1905
  async ({ otterName, responseText }) => {
1567
1906
  const { result, isError } = handleParseOtterResponse({ otterName, responseText });
@@ -1575,16 +1914,18 @@ function registerOrchestrationTools(server2) {
1575
1914
  "stackwright_pro_save_manifest",
1576
1915
  "Write the question manifest to .stackwright/question-manifest.json. Call this after collecting and parsing questions from all otters via stackwright_pro_parse_otter_response.",
1577
1916
  {
1578
- phases: import_zod8.z.array(
1579
- import_zod8.z.object({
1580
- phase: import_zod8.z.string(),
1581
- otter: import_zod8.z.string(),
1582
- questions: import_zod8.z.array(import_zod8.z.any()),
1583
- requiredPackages: import_zod8.z.object({
1584
- dependencies: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string()).optional(),
1585
- devPackages: import_zod8.z.record(import_zod8.z.string(), import_zod8.z.string()).optional()
1586
- }).optional()
1587
- })
1917
+ phases: jsonCoerce(
1918
+ import_zod9.z.array(
1919
+ import_zod9.z.object({
1920
+ phase: import_zod9.z.string(),
1921
+ otter: import_zod9.z.string(),
1922
+ questions: import_zod9.z.array(import_zod9.z.any()),
1923
+ requiredPackages: import_zod9.z.object({
1924
+ dependencies: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional(),
1925
+ devPackages: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional()
1926
+ }).optional()
1927
+ })
1928
+ )
1588
1929
  ).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
1589
1930
  },
1590
1931
  async ({ phases }) => {
@@ -1599,15 +1940,37 @@ function registerOrchestrationTools(server2) {
1599
1940
  "stackwright_pro_save_phase_answers",
1600
1941
  "Save user answers for a phase to .stackwright/answers/{phase}.json. Pass rawAnswers directly from ask_user_question and the original manifest questions for label-to-value reverse mapping.",
1601
1942
  {
1602
- phase: import_zod8.z.string().describe('Phase name, e.g. "designer"'),
1603
- rawAnswers: import_zod8.z.array(
1604
- import_zod8.z.object({
1605
- question_header: import_zod8.z.string(),
1606
- selected_options: import_zod8.z.array(import_zod8.z.string()),
1607
- other_text: import_zod8.z.string().nullable().optional()
1608
- })
1609
- ).describe("Answers as returned by ask_user_question"),
1610
- questions: import_zod8.z.array(import_zod8.z.any()).optional().describe("Original manifest questions for label\u2192value reverse-mapping")
1943
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
1944
+ rawAnswers: jsonCoerce(
1945
+ import_zod9.z.array(
1946
+ import_zod9.z.object({
1947
+ question_header: import_zod9.z.string().min(1, "question_header must not be empty"),
1948
+ selected_options: import_zod9.z.array(import_zod9.z.string()).min(1, "selected_options must have at least one entry"),
1949
+ other_text: import_zod9.z.string().nullable().optional()
1950
+ })
1951
+ )
1952
+ ).describe(
1953
+ "Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
1954
+ ),
1955
+ questions: jsonCoerce(
1956
+ import_zod9.z.array(
1957
+ import_zod9.z.object({
1958
+ id: import_zod9.z.string(),
1959
+ question: import_zod9.z.string(),
1960
+ type: import_zod9.z.string(),
1961
+ options: import_zod9.z.array(import_zod9.z.object({ label: import_zod9.z.string(), value: import_zod9.z.string() })).optional(),
1962
+ required: import_zod9.z.boolean().optional(),
1963
+ default: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.boolean(), import_zod9.z.array(import_zod9.z.string())]).optional(),
1964
+ help: import_zod9.z.string().optional(),
1965
+ dependsOn: import_zod9.z.object({
1966
+ questionId: import_zod9.z.string(),
1967
+ value: import_zod9.z.union([import_zod9.z.string(), import_zod9.z.array(import_zod9.z.string())])
1968
+ }).optional()
1969
+ }).passthrough()
1970
+ ).optional()
1971
+ ).describe(
1972
+ "Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
1973
+ )
1611
1974
  },
1612
1975
  async ({ phase, rawAnswers, questions }) => {
1613
1976
  const { text, isError } = handleSavePhaseAnswers({
@@ -1625,7 +1988,7 @@ function registerOrchestrationTools(server2) {
1625
1988
  "stackwright_pro_read_phase_answers",
1626
1989
  "Read saved answers for a phase from .stackwright/answers/{phase}.json. Returns { missing: true } when no answers exist yet \u2014 use this to skip phases safely in the execution loop.",
1627
1990
  {
1628
- phase: import_zod8.z.string().describe('Phase name, e.g. "designer"')
1991
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer"')
1629
1992
  },
1630
1993
  async ({ phase }) => {
1631
1994
  const { text, isError } = handleReadPhaseAnswers({ phase });
@@ -1639,7 +2002,7 @@ function registerOrchestrationTools(server2) {
1639
2002
  "stackwright_pro_get_otter_name",
1640
2003
  "Get the agent name for a phase (e.g. 'designer' \u2192 'stackwright-pro-designer-otter'). Use this in the execution loop to invoke the correct specialist otter without hardcoding names in the prompt.",
1641
2004
  {
1642
- phase: import_zod8.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
2005
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
1643
2006
  },
1644
2007
  async ({ phase }) => {
1645
2008
  const { text, isError } = handleGetOtterName({ phase });
@@ -1652,50 +2015,410 @@ function registerOrchestrationTools(server2) {
1652
2015
  }
1653
2016
 
1654
2017
  // src/tools/pipeline.ts
1655
- var import_zod9 = require("zod");
2018
+ var import_zod11 = require("zod");
2019
+ var import_fs5 = require("fs");
2020
+ var import_path5 = require("path");
2021
+ var import_crypto3 = require("crypto");
2022
+
2023
+ // src/artifact-signing.ts
2024
+ var import_crypto2 = require("crypto");
1656
2025
  var import_fs4 = require("fs");
1657
2026
  var import_path4 = require("path");
1658
- var PHASE_ORDER = [
1659
- "designer",
1660
- "theme",
1661
- "api",
1662
- "auth",
1663
- "data",
1664
- "pages",
1665
- "dashboard",
1666
- "workflow"
1667
- ];
1668
- var PHASE_DEPENDENCIES = {
1669
- designer: [],
1670
- theme: ["designer"],
1671
- api: [],
1672
- auth: [],
1673
- data: ["api"],
1674
- pages: ["designer", "theme", "api", "data", "auth"],
1675
- dashboard: ["designer", "theme", "api", "data"],
1676
- workflow: ["auth"]
1677
- };
1678
- var PHASE_ARTIFACT = {
1679
- designer: "design-language.json",
1680
- theme: "theme-tokens.json",
1681
- api: "api-config.json",
1682
- auth: "auth-config.json",
1683
- data: "data-config.json",
1684
- pages: "pages-manifest.json",
1685
- dashboard: "dashboard-manifest.json",
1686
- workflow: "workflow-config.json"
1687
- };
1688
- var PHASE_TO_OTTER2 = {
1689
- designer: "stackwright-pro-designer-otter",
1690
- theme: "stackwright-pro-theme-otter",
1691
- api: "stackwright-pro-api-otter",
1692
- auth: "stackwright-pro-auth-otter",
1693
- data: "stackwright-pro-data-otter",
2027
+ var import_zod10 = require("zod");
2028
+ var ALGORITHM = "ECDSA-P384-SHA384";
2029
+ var KEY_FILE = "pipeline-keys.json";
2030
+ var KEY_DIR = ".stackwright";
2031
+ var SIGNATURE_MANIFEST = "signatures.json";
2032
+ var ARTIFACTS_DIR = ".stackwright/artifacts";
2033
+ function rejectSymlink(filePath, context) {
2034
+ if (!(0, import_fs4.existsSync)(filePath)) return;
2035
+ const stat = (0, import_fs4.lstatSync)(filePath);
2036
+ if (stat.isSymbolicLink()) {
2037
+ throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
2038
+ }
2039
+ }
2040
+ function computeSha384(data) {
2041
+ return (0, import_crypto2.createHash)("sha384").update(data).digest("hex");
2042
+ }
2043
+ function safeDigestEqual(a, b) {
2044
+ if (a.length !== b.length) return false;
2045
+ return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
2046
+ }
2047
+ function emptyManifest() {
2048
+ return {
2049
+ version: "1.0",
2050
+ algorithm: ALGORITHM,
2051
+ signatures: {}
2052
+ };
2053
+ }
2054
+ function initPipelineKeys(cwd) {
2055
+ const keyDir = (0, import_path4.join)(cwd, KEY_DIR);
2056
+ const keyPath = (0, import_path4.join)(keyDir, KEY_FILE);
2057
+ rejectSymlink(keyPath, "pipeline-keys.json");
2058
+ (0, import_fs4.mkdirSync)(keyDir, { recursive: true });
2059
+ const { publicKey, privateKey } = (0, import_crypto2.generateKeyPairSync)("ec", {
2060
+ namedCurve: "P-384"
2061
+ });
2062
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
2063
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
2064
+ const fingerprint = (0, import_crypto2.createHash)("sha256").update(publicKeyPem).digest("hex");
2065
+ const keyFile = {
2066
+ version: "1.0",
2067
+ algorithm: ALGORITHM,
2068
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
2069
+ publicKeyPem,
2070
+ privateKeyPem
2071
+ };
2072
+ (0, import_fs4.writeFileSync)(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
2073
+ return { publicKeyPem, fingerprint };
2074
+ }
2075
+ function loadPipelineKeys(cwd) {
2076
+ const keyPath = (0, import_path4.join)(cwd, KEY_DIR, KEY_FILE);
2077
+ rejectSymlink(keyPath, "pipeline-keys.json");
2078
+ if (!(0, import_fs4.existsSync)(keyPath)) {
2079
+ throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
2080
+ }
2081
+ let raw;
2082
+ try {
2083
+ raw = (0, import_fs4.readFileSync)(keyPath, "utf-8");
2084
+ } catch (err) {
2085
+ const msg = err instanceof Error ? err.message : String(err);
2086
+ throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
2087
+ }
2088
+ let parsed;
2089
+ try {
2090
+ parsed = JSON.parse(raw);
2091
+ } catch {
2092
+ throw new Error("Pipeline keys file is not valid JSON");
2093
+ }
2094
+ if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
2095
+ throw new Error("Invalid public key PEM in pipeline keys file");
2096
+ }
2097
+ if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
2098
+ throw new Error("Invalid private key PEM in pipeline keys file");
2099
+ }
2100
+ const publicKey = (0, import_crypto2.createPublicKey)(parsed.publicKeyPem);
2101
+ const privateKey = (0, import_crypto2.createPrivateKey)(parsed.privateKeyPem);
2102
+ return { privateKey, publicKey };
2103
+ }
2104
+ function signArtifact(artifactBytes, privateKey) {
2105
+ const digest = computeSha384(artifactBytes);
2106
+ const sig = (0, import_crypto2.sign)("SHA384", artifactBytes, privateKey);
2107
+ return {
2108
+ digest,
2109
+ signature: sig.toString("base64"),
2110
+ algorithm: ALGORITHM,
2111
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
2112
+ };
2113
+ }
2114
+ function verifyArtifact(artifactBytes, signature, publicKey) {
2115
+ const sigValid = (0, import_crypto2.verify)(
2116
+ "SHA384",
2117
+ artifactBytes,
2118
+ publicKey,
2119
+ Buffer.from(signature.signature, "base64")
2120
+ );
2121
+ if (!sigValid) return false;
2122
+ const actualDigest = computeSha384(artifactBytes);
2123
+ return safeDigestEqual(actualDigest, signature.digest);
2124
+ }
2125
+ function loadSignatureManifest(cwd) {
2126
+ const manifestPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
2127
+ rejectSymlink(manifestPath, "signatures.json");
2128
+ if (!(0, import_fs4.existsSync)(manifestPath)) return emptyManifest();
2129
+ let raw;
2130
+ try {
2131
+ raw = (0, import_fs4.readFileSync)(manifestPath, "utf-8");
2132
+ } catch (err) {
2133
+ const msg = err instanceof Error ? err.message : String(err);
2134
+ throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
2135
+ }
2136
+ try {
2137
+ const parsed = JSON.parse(raw);
2138
+ if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
2139
+ throw new Error("Malformed signature manifest: invalid version or missing signatures object");
2140
+ }
2141
+ return parsed;
2142
+ } catch (err) {
2143
+ if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
2144
+ throw new Error("Signature manifest is not valid JSON", { cause: err });
2145
+ }
2146
+ }
2147
+ function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
2148
+ const artifactsDir = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
2149
+ const manifestPath = (0, import_path4.join)(artifactsDir, SIGNATURE_MANIFEST);
2150
+ rejectSymlink(manifestPath, "signatures.json (save)");
2151
+ (0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
2152
+ const manifest = loadSignatureManifest(cwd);
2153
+ manifest.signatures[artifactFilename] = {
2154
+ ...sig,
2155
+ signedBy: signerOtter
2156
+ };
2157
+ (0, import_fs4.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
2158
+ }
2159
+ function getArtifactSignature(cwd, artifactFilename) {
2160
+ const manifest = loadSignatureManifest(cwd);
2161
+ const entry = manifest.signatures[artifactFilename];
2162
+ if (!entry) return null;
2163
+ return {
2164
+ digest: entry.digest,
2165
+ signature: entry.signature,
2166
+ algorithm: entry.algorithm,
2167
+ signedAt: entry.signedAt
2168
+ };
2169
+ }
2170
+ function emitSignatureAuditEvent(params) {
2171
+ const record = JSON.stringify({
2172
+ level: "AUDIT",
2173
+ event: "ARTIFACT_SIGNATURE_FAIL",
2174
+ timestamp: params.timestamp,
2175
+ source: params.source,
2176
+ artifactFilename: params.artifactFilename,
2177
+ expectedDigest: params.expectedDigest,
2178
+ actualDigest: params.actualDigest,
2179
+ phase: params.phase
2180
+ });
2181
+ process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
2182
+ `);
2183
+ }
2184
+ function registerArtifactSigningTools(server2) {
2185
+ server2.tool(
2186
+ "stackwright_pro_verify_artifact_signatures",
2187
+ "Verify ECDSA P-384 signatures for all pipeline artifacts in .stackwright/artifacts/. Auto-discovers keys from .stackwright/pipeline-keys.json. Returns per-artifact verification status.",
2188
+ {
2189
+ cwd: import_zod10.z.string().optional().describe("Project root directory. Defaults to process.cwd().")
2190
+ },
2191
+ async ({ cwd: cwdParam }) => {
2192
+ const cwd = cwdParam ?? process.cwd();
2193
+ let publicKey;
2194
+ try {
2195
+ const keys = loadPipelineKeys(cwd);
2196
+ publicKey = keys.publicKey;
2197
+ } catch (err) {
2198
+ const msg = err instanceof Error ? err.message : String(err);
2199
+ return {
2200
+ content: [
2201
+ {
2202
+ type: "text",
2203
+ text: JSON.stringify({
2204
+ error: true,
2205
+ message: `Cannot load pipeline keys: ${msg}`
2206
+ })
2207
+ }
2208
+ ],
2209
+ isError: true
2210
+ };
2211
+ }
2212
+ let manifest;
2213
+ try {
2214
+ manifest = loadSignatureManifest(cwd);
2215
+ } catch (err) {
2216
+ const msg = err instanceof Error ? err.message : String(err);
2217
+ return {
2218
+ content: [
2219
+ {
2220
+ type: "text",
2221
+ text: JSON.stringify({
2222
+ error: true,
2223
+ message: `Cannot load signature manifest: ${msg}`
2224
+ })
2225
+ }
2226
+ ],
2227
+ isError: true
2228
+ };
2229
+ }
2230
+ const artifactsPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
2231
+ let artifactFiles = [];
2232
+ try {
2233
+ if ((0, import_fs4.existsSync)(artifactsPath)) {
2234
+ artifactFiles = (0, import_fs4.readdirSync)(artifactsPath).filter(
2235
+ (f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
2236
+ );
2237
+ }
2238
+ } catch {
2239
+ }
2240
+ const results = [];
2241
+ let hasFailure = false;
2242
+ for (const filename of artifactFiles) {
2243
+ const filePath = (0, import_path4.join)(artifactsPath, filename);
2244
+ try {
2245
+ rejectSymlink(filePath, `artifact ${filename}`);
2246
+ } catch {
2247
+ results.push({
2248
+ filename,
2249
+ verified: false,
2250
+ error: "Refusing to verify symlink"
2251
+ });
2252
+ hasFailure = true;
2253
+ continue;
2254
+ }
2255
+ let artifactBytes;
2256
+ try {
2257
+ artifactBytes = (0, import_fs4.readFileSync)(filePath);
2258
+ } catch (err) {
2259
+ const msg = err instanceof Error ? err.message : String(err);
2260
+ results.push({
2261
+ filename,
2262
+ verified: false,
2263
+ error: `Cannot read artifact: ${msg}`
2264
+ });
2265
+ hasFailure = true;
2266
+ continue;
2267
+ }
2268
+ const entry = manifest.signatures[filename];
2269
+ if (!entry) {
2270
+ results.push({
2271
+ filename,
2272
+ verified: false,
2273
+ error: "No signature found in manifest"
2274
+ });
2275
+ hasFailure = true;
2276
+ continue;
2277
+ }
2278
+ const sig = {
2279
+ digest: entry.digest,
2280
+ signature: entry.signature,
2281
+ algorithm: entry.algorithm,
2282
+ signedAt: entry.signedAt
2283
+ };
2284
+ const verified = verifyArtifact(artifactBytes, sig, publicKey);
2285
+ if (!verified) {
2286
+ const actualDigest = computeSha384(artifactBytes);
2287
+ emitSignatureAuditEvent({
2288
+ artifactFilename: filename,
2289
+ expectedDigest: sig.digest,
2290
+ actualDigest,
2291
+ phase: entry.signedBy ?? "unknown",
2292
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2293
+ source: "stackwright_pro_verify_artifact_signatures"
2294
+ });
2295
+ results.push({
2296
+ filename,
2297
+ verified: false,
2298
+ error: `Signature verification failed \u2014 artifact may have been tampered with`,
2299
+ signedBy: entry.signedBy,
2300
+ signedAt: entry.signedAt
2301
+ });
2302
+ hasFailure = true;
2303
+ } else {
2304
+ results.push({
2305
+ filename,
2306
+ verified: true,
2307
+ signedBy: entry.signedBy,
2308
+ signedAt: entry.signedAt
2309
+ });
2310
+ }
2311
+ }
2312
+ for (const manifestFilename of Object.keys(manifest.signatures)) {
2313
+ if (!artifactFiles.includes(manifestFilename)) {
2314
+ results.push({
2315
+ filename: manifestFilename,
2316
+ verified: false,
2317
+ error: "Artifact referenced in manifest but missing from disk"
2318
+ });
2319
+ hasFailure = true;
2320
+ }
2321
+ }
2322
+ const verifiedCount = results.filter((r) => r.verified).length;
2323
+ const failedCount = results.filter((r) => !r.verified).length;
2324
+ return {
2325
+ content: [
2326
+ {
2327
+ type: "text",
2328
+ text: JSON.stringify({
2329
+ totalArtifacts: artifactFiles.length,
2330
+ verifiedCount,
2331
+ failedCount,
2332
+ results,
2333
+ ...hasFailure ? {
2334
+ error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
2335
+ } : {}
2336
+ })
2337
+ }
2338
+ ],
2339
+ isError: hasFailure
2340
+ };
2341
+ }
2342
+ );
2343
+ }
2344
+
2345
+ // src/tools/pipeline.ts
2346
+ var import_types = require("@stackwright-pro/types");
2347
+ var PHASE_ORDER = [
2348
+ "designer",
2349
+ "theme",
2350
+ "scaffold",
2351
+ // generates app/ directory from config
2352
+ "api",
2353
+ "data",
2354
+ "geo",
2355
+ "workflow",
2356
+ "services",
2357
+ "pages",
2358
+ "dashboard",
2359
+ "auth",
2360
+ "polish"
2361
+ ];
2362
+ var PHASE_DEPENDENCIES = {
2363
+ designer: [],
2364
+ theme: ["designer"],
2365
+ scaffold: ["designer", "theme"],
2366
+ // needs stackwright.yml + theme-tokens
2367
+ api: [],
2368
+ data: ["api"],
2369
+ geo: ["data"],
2370
+ // workflow: no hard deps — uses service: references with Prism mock fallback
2371
+ // when API artifacts aren't available; roles come from user answers
2372
+ workflow: [],
2373
+ // services: needs API endpoints to know what to wire, and optionally
2374
+ // workflow states as trigger sources. Produces OpenAPI specs consumed
2375
+ // by pages and dashboard for typed data wiring.
2376
+ services: ["api", "workflow"],
2377
+ // pages/dashboard: depend on services for typed endpoint wiring (OpenAPI
2378
+ // specs) and on geo so the specialist prompt includes geo-manifest.json
2379
+ // — page/dashboard otters see which page slugs are already claimed and
2380
+ // won't attempt to overwrite them (swp-73c). 'api' is still transitive
2381
+ // through 'data'; auth removed — page-otter has graceful fallback.
2382
+ pages: ["designer", "theme", "data", "services", "geo", "scaffold"],
2383
+ dashboard: ["designer", "theme", "data", "services", "geo", "scaffold"],
2384
+ // auth is the penultimate phase — runs after all content-producing phases
2385
+ // so it can read pages, dashboard, workflow, and geo artifacts for route
2386
+ // protection and RBAC wiring. Skipped upstream phases still satisfy deps
2387
+ // (the foreman marks them executed=true), so auth always runs.
2388
+ auth: ["pages", "dashboard", "workflow", "geo"],
2389
+ // polish is the terminal phase — runs after auth so the landing page and
2390
+ // nav reflect the final set of protected routes and all generated pages.
2391
+ polish: ["auth"]
2392
+ };
2393
+ var PHASE_ARTIFACT = {
2394
+ designer: "design-language.json",
2395
+ theme: "theme-tokens.json",
2396
+ scaffold: "scaffold-manifest.json",
2397
+ api: "api-config.json",
2398
+ auth: "auth-config.json",
2399
+ data: "data-config.json",
2400
+ pages: "pages-manifest.json",
2401
+ dashboard: "dashboard-manifest.json",
2402
+ workflow: "workflow-config.json",
2403
+ services: "services-config.json",
2404
+ polish: "polish-manifest.json",
2405
+ geo: "geo-manifest.json"
2406
+ };
2407
+ var PHASE_TO_OTTER2 = {
2408
+ designer: "stackwright-pro-designer-otter",
2409
+ theme: "stackwright-pro-theme-otter",
2410
+ scaffold: "stackwright-pro-scaffold-otter",
2411
+ api: "stackwright-pro-api-otter",
2412
+ auth: "stackwright-pro-auth-otter",
2413
+ data: "stackwright-pro-data-otter",
1694
2414
  pages: "stackwright-pro-page-otter",
1695
2415
  dashboard: "stackwright-pro-dashboard-otter",
1696
- workflow: "stackwright-pro-workflow-otter"
2416
+ workflow: "stackwright-pro-workflow-otter",
2417
+ services: "stackwright-services-otter",
2418
+ polish: "stackwright-pro-polish-otter",
2419
+ geo: "stackwright-pro-geo-otter"
1697
2420
  };
1698
- function isValidPhase(phase) {
2421
+ function isValidPhase2(phase) {
1699
2422
  return PHASE_ORDER.includes(phase);
1700
2423
  }
1701
2424
  function defaultPhaseStatus() {
@@ -1723,11 +2446,11 @@ function createDefaultState() {
1723
2446
  };
1724
2447
  }
1725
2448
  function statePath(cwd) {
1726
- return (0, import_path4.join)(cwd, ".stackwright", "pipeline-state.json");
2449
+ return (0, import_path5.join)(cwd, ".stackwright", "pipeline-state.json");
1727
2450
  }
1728
2451
  function readState(cwd) {
1729
2452
  const p = statePath(cwd);
1730
- if (!(0, import_fs4.existsSync)(p)) return createDefaultState();
2453
+ if (!(0, import_fs5.existsSync)(p)) return createDefaultState();
1731
2454
  const raw = JSON.parse(safeReadSync(p));
1732
2455
  if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1733
2456
  return createDefaultState();
@@ -1735,26 +2458,26 @@ function readState(cwd) {
1735
2458
  return raw;
1736
2459
  }
1737
2460
  function safeWriteSync(filePath, content) {
1738
- if ((0, import_fs4.existsSync)(filePath)) {
1739
- const stat = (0, import_fs4.lstatSync)(filePath);
2461
+ if ((0, import_fs5.existsSync)(filePath)) {
2462
+ const stat = (0, import_fs5.lstatSync)(filePath);
1740
2463
  if (stat.isSymbolicLink()) {
1741
2464
  throw new Error(`Refusing to write to symlink: ${filePath}`);
1742
2465
  }
1743
2466
  }
1744
- (0, import_fs4.writeFileSync)(filePath, content);
2467
+ (0, import_fs5.writeFileSync)(filePath, content);
1745
2468
  }
1746
2469
  function safeReadSync(filePath) {
1747
- if ((0, import_fs4.existsSync)(filePath)) {
1748
- const stat = (0, import_fs4.lstatSync)(filePath);
2470
+ if ((0, import_fs5.existsSync)(filePath)) {
2471
+ const stat = (0, import_fs5.lstatSync)(filePath);
1749
2472
  if (stat.isSymbolicLink()) {
1750
2473
  throw new Error(`Refusing to read symlink: ${filePath}`);
1751
2474
  }
1752
2475
  }
1753
- return (0, import_fs4.readFileSync)(filePath, "utf-8");
2476
+ return (0, import_fs5.readFileSync)(filePath, "utf-8");
1754
2477
  }
1755
2478
  function writeState(cwd, state) {
1756
- const dir = (0, import_path4.join)(cwd, ".stackwright");
1757
- (0, import_fs4.mkdirSync)(dir, { recursive: true });
2479
+ const dir = (0, import_path5.join)(cwd, ".stackwright");
2480
+ (0, import_fs5.mkdirSync)(dir, { recursive: true });
1758
2481
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1759
2482
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1760
2483
  }
@@ -1775,6 +2498,15 @@ function handleGetPipelineState(_cwd) {
1775
2498
  const cwd = _cwd ?? process.cwd();
1776
2499
  try {
1777
2500
  const state = readState(cwd);
2501
+ const keyPath = (0, import_path5.join)(cwd, ".stackwright", "pipeline-keys.json");
2502
+ if (!(0, import_fs5.existsSync)(keyPath)) {
2503
+ try {
2504
+ const { fingerprint } = initPipelineKeys(cwd);
2505
+ state["signingKeyFingerprint"] = fingerprint;
2506
+ writeState(cwd, state);
2507
+ } catch {
2508
+ }
2509
+ }
1778
2510
  return { text: JSON.stringify(state), isError: false };
1779
2511
  } catch (err) {
1780
2512
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
@@ -1782,7 +2514,7 @@ function handleGetPipelineState(_cwd) {
1782
2514
  }
1783
2515
  function handleSetPipelineState(input) {
1784
2516
  const cwd = input._cwd ?? process.cwd();
1785
- if (input.phase && !isValidPhase(input.phase)) {
2517
+ if (input.phase && !isValidPhase2(input.phase)) {
1786
2518
  return {
1787
2519
  text: JSON.stringify({
1788
2520
  error: true,
@@ -1801,6 +2533,28 @@ function handleSetPipelineState(input) {
1801
2533
  isError: true
1802
2534
  };
1803
2535
  }
2536
+ if (input.updates && input.updates.length > 0) {
2537
+ for (const update of input.updates) {
2538
+ if (!isValidPhase2(update.phase)) {
2539
+ return {
2540
+ text: JSON.stringify({
2541
+ error: true,
2542
+ message: `Invalid phase in batch update: ${update.phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2543
+ }),
2544
+ isError: true
2545
+ };
2546
+ }
2547
+ if (!VALID_FIELDS.includes(update.field)) {
2548
+ return {
2549
+ text: JSON.stringify({
2550
+ error: true,
2551
+ message: `Invalid field in batch update: ${update.field}. Valid fields are: ${VALID_FIELDS.join(", ")}`
2552
+ }),
2553
+ isError: true
2554
+ };
2555
+ }
2556
+ }
2557
+ }
1804
2558
  try {
1805
2559
  const state = readState(cwd);
1806
2560
  if (input.status) {
@@ -1820,34 +2574,78 @@ function handleSetPipelineState(input) {
1820
2574
  }
1821
2575
  state.currentPhase = phase;
1822
2576
  }
2577
+ if (input.updates && input.updates.length > 0) {
2578
+ for (const update of input.updates) {
2579
+ if (!state.phases[update.phase]) {
2580
+ state.phases[update.phase] = defaultPhaseStatus();
2581
+ }
2582
+ const batchPhaseState = state.phases[update.phase];
2583
+ batchPhaseState[update.field] = update.value;
2584
+ state.currentPhase = update.phase;
2585
+ }
2586
+ }
1823
2587
  writeState(cwd, state);
1824
2588
  return { text: JSON.stringify(state), isError: false };
1825
2589
  } catch (err) {
1826
2590
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1827
2591
  }
1828
2592
  }
1829
- function handleCheckExecutionReady(_cwd) {
2593
+ function handleCheckExecutionReady(_cwd, phase) {
1830
2594
  const cwd = _cwd ?? process.cwd();
2595
+ if (phase) {
2596
+ if (!isValidPhase2(phase)) {
2597
+ return {
2598
+ text: JSON.stringify({
2599
+ error: true,
2600
+ message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2601
+ }),
2602
+ isError: true
2603
+ };
2604
+ }
2605
+ const answerFile = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
2606
+ if (!(0, import_fs5.existsSync)(answerFile)) {
2607
+ return {
2608
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
2609
+ isError: false
2610
+ };
2611
+ }
2612
+ try {
2613
+ const raw = safeReadSync(answerFile);
2614
+ const parsed = JSON.parse(raw);
2615
+ if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
2616
+ return {
2617
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
2618
+ isError: false
2619
+ };
2620
+ }
2621
+ return { text: JSON.stringify({ ready: true, phase }), isError: false };
2622
+ } catch {
2623
+ return {
2624
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
2625
+ isError: false
2626
+ };
2627
+ }
2628
+ }
1831
2629
  try {
1832
- const answersDir = (0, import_path4.join)(cwd, ".stackwright", "answers");
2630
+ const answersDir = (0, import_path5.join)(cwd, ".stackwright", "answers");
1833
2631
  const answeredPhases = [];
1834
2632
  const missingPhases = [];
1835
- for (const phase of PHASE_ORDER) {
1836
- const answerFile = (0, import_path4.join)(answersDir, `${phase}.json`);
1837
- if ((0, import_fs4.existsSync)(answerFile)) {
2633
+ for (const phase2 of PHASE_ORDER) {
2634
+ const answerFile = (0, import_path5.join)(answersDir, `${phase2}.json`);
2635
+ if ((0, import_fs5.existsSync)(answerFile)) {
1838
2636
  try {
1839
2637
  const raw = safeReadSync(answerFile);
1840
2638
  const parsed = JSON.parse(raw);
1841
2639
  if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
1842
- missingPhases.push(phase);
2640
+ missingPhases.push(phase2);
1843
2641
  continue;
1844
2642
  }
1845
- answeredPhases.push(phase);
2643
+ answeredPhases.push(phase2);
1846
2644
  } catch {
1847
- missingPhases.push(phase);
2645
+ missingPhases.push(phase2);
1848
2646
  }
1849
2647
  } else {
1850
- missingPhases.push(phase);
2648
+ missingPhases.push(phase2);
1851
2649
  }
1852
2650
  }
1853
2651
  return {
@@ -1863,18 +2661,74 @@ function handleCheckExecutionReady(_cwd) {
1863
2661
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1864
2662
  }
1865
2663
  }
2664
+ function handleGetReadyPhases(_cwd) {
2665
+ const cwd = _cwd ?? process.cwd();
2666
+ try {
2667
+ const state = readState(cwd);
2668
+ const completed = [];
2669
+ const ready = [];
2670
+ const blocked = [];
2671
+ for (const phase of PHASE_ORDER) {
2672
+ const ps = state.phases[phase];
2673
+ if (ps?.executed) {
2674
+ completed.push(phase);
2675
+ continue;
2676
+ }
2677
+ const deps = PHASE_DEPENDENCIES[phase];
2678
+ const unmetDeps = deps.filter((dep) => !state.phases[dep]?.executed);
2679
+ if (unmetDeps.length === 0) {
2680
+ ready.push(phase);
2681
+ } else {
2682
+ blocked.push(phase);
2683
+ }
2684
+ }
2685
+ return {
2686
+ text: JSON.stringify({
2687
+ readyPhases: ready,
2688
+ completedPhases: completed,
2689
+ blockedPhases: blocked,
2690
+ waveSize: ready.length,
2691
+ allComplete: ready.length === 0 && blocked.length === 0
2692
+ }),
2693
+ isError: false
2694
+ };
2695
+ } catch (err) {
2696
+ const message = err instanceof Error ? err.message : String(err);
2697
+ return { text: JSON.stringify({ error: true, message }), isError: true };
2698
+ }
2699
+ }
1866
2700
  function handleListArtifacts(_cwd) {
1867
2701
  const cwd = _cwd ?? process.cwd();
1868
2702
  try {
1869
- const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
2703
+ const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
2704
+ let manifest = null;
2705
+ try {
2706
+ manifest = loadSignatureManifest(cwd);
2707
+ } catch {
2708
+ }
1870
2709
  const artifacts = [];
1871
2710
  let completedCount = 0;
1872
2711
  for (const phase of PHASE_ORDER) {
1873
2712
  const expectedFile = PHASE_ARTIFACT[phase];
1874
- const fullPath = (0, import_path4.join)(artifactsDir, expectedFile);
1875
- const exists = (0, import_fs4.existsSync)(fullPath);
2713
+ const fullPath = (0, import_path5.join)(artifactsDir, expectedFile);
2714
+ const exists = (0, import_fs5.existsSync)(fullPath);
1876
2715
  if (exists) completedCount++;
1877
- artifacts.push({ phase, expectedFile, exists, path: fullPath });
2716
+ let signed = false;
2717
+ let signatureValid = null;
2718
+ if (exists && manifest) {
2719
+ const entry = manifest.signatures[expectedFile];
2720
+ if (entry) {
2721
+ signed = true;
2722
+ try {
2723
+ const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
2724
+ const { publicKey } = loadPipelineKeys(cwd);
2725
+ signatureValid = verifyArtifact(rawBytes, entry, publicKey);
2726
+ } catch {
2727
+ signatureValid = null;
2728
+ }
2729
+ }
2730
+ }
2731
+ artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
1878
2732
  }
1879
2733
  return {
1880
2734
  text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
@@ -1887,7 +2741,7 @@ function handleListArtifacts(_cwd) {
1887
2741
  function handleWritePhaseQuestions(input) {
1888
2742
  const cwd = input._cwd ?? process.cwd();
1889
2743
  const { phase, responseText } = input;
1890
- if (!isValidPhase(phase)) {
2744
+ if (!isValidPhase2(phase)) {
1891
2745
  return {
1892
2746
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
1893
2747
  isError: true
@@ -1910,9 +2764,9 @@ function handleWritePhaseQuestions(input) {
1910
2764
  }
1911
2765
  } catch {
1912
2766
  }
1913
- const questionsDir = (0, import_path4.join)(cwd, ".stackwright", "questions");
1914
- (0, import_fs4.mkdirSync)(questionsDir, { recursive: true });
1915
- const filePath = (0, import_path4.join)(questionsDir, `${phase}.json`);
2767
+ const questionsDir = (0, import_path5.join)(cwd, ".stackwright", "questions");
2768
+ (0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
2769
+ const filePath = (0, import_path5.join)(questionsDir, `${phase}.json`);
1916
2770
  const payload = {
1917
2771
  version: "1.0",
1918
2772
  phase,
@@ -1945,43 +2799,88 @@ function handleWritePhaseQuestions(input) {
1945
2799
  function handleBuildSpecialistPrompt(input) {
1946
2800
  const cwd = input._cwd ?? process.cwd();
1947
2801
  const { phase } = input;
1948
- if (!isValidPhase(phase)) {
2802
+ if (!isValidPhase2(phase)) {
1949
2803
  return {
1950
2804
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
1951
2805
  isError: true
1952
2806
  };
1953
2807
  }
1954
2808
  try {
1955
- const answersPath = (0, import_path4.join)(cwd, ".stackwright", "answers", `${phase}.json`);
2809
+ const answersPath = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1956
2810
  let answers = {};
1957
- if ((0, import_fs4.existsSync)(answersPath)) {
2811
+ if ((0, import_fs5.existsSync)(answersPath)) {
1958
2812
  answers = JSON.parse(safeReadSync(answersPath));
1959
2813
  }
2814
+ let buildContextText = "";
2815
+ const buildContextPath = (0, import_path5.join)(cwd, ".stackwright", "build-context.json");
2816
+ if ((0, import_fs5.existsSync)(buildContextPath)) {
2817
+ try {
2818
+ const bcRaw = JSON.parse(safeReadSync(buildContextPath));
2819
+ if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
2820
+ buildContextText = bcRaw.buildContext.trim();
2821
+ }
2822
+ } catch {
2823
+ }
2824
+ }
1960
2825
  const deps = PHASE_DEPENDENCIES[phase];
1961
2826
  const artifactSections = [];
1962
2827
  const missingDependencies = [];
1963
2828
  for (const dep of deps) {
1964
2829
  const artifactFile = PHASE_ARTIFACT[dep];
1965
- const artifactPath = (0, import_path4.join)(cwd, ".stackwright", "artifacts", artifactFile);
1966
- if ((0, import_fs4.existsSync)(artifactPath)) {
1967
- const content = JSON.parse(safeReadSync(artifactPath));
2830
+ const artifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", artifactFile);
2831
+ if ((0, import_fs5.existsSync)(artifactPath)) {
2832
+ const rawContent = safeReadSync(artifactPath);
2833
+ const rawBytes = Buffer.from(rawContent, "utf-8");
2834
+ const content = JSON.parse(rawContent);
2835
+ let signatureVerified = false;
2836
+ let signatureAvailable = false;
2837
+ try {
2838
+ const sig = getArtifactSignature(cwd, artifactFile);
2839
+ if (sig) {
2840
+ signatureAvailable = true;
2841
+ const { publicKey } = loadPipelineKeys(cwd);
2842
+ signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
2843
+ if (!signatureVerified) {
2844
+ const actualDigest = (0, import_crypto3.createHash)("sha384").update(rawBytes).digest("hex");
2845
+ emitSignatureAuditEvent({
2846
+ artifactFilename: artifactFile,
2847
+ expectedDigest: sig.digest,
2848
+ actualDigest,
2849
+ phase: dep,
2850
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2851
+ source: "stackwright_pro_build_specialist_prompt"
2852
+ });
2853
+ missingDependencies.push(dep);
2854
+ artifactSections.push(
2855
+ `[${artifactFile}]:
2856
+ (integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
2857
+ );
2858
+ continue;
2859
+ }
2860
+ }
2861
+ } catch {
2862
+ }
1968
2863
  const expectedOtter = PHASE_TO_OTTER2[dep];
1969
2864
  const artifactOtter = content["generatedBy"];
2865
+ const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
1970
2866
  if (!artifactOtter) {
1971
2867
  missingDependencies.push(dep);
1972
2868
  artifactSections.push(
1973
2869
  `[${artifactFile}]:
1974
2870
  (integrity check failed: missing generatedBy field)`
1975
2871
  );
1976
- } else if (artifactOtter !== expectedOtter) {
2872
+ } else if (normalizedOtter !== expectedOtter) {
1977
2873
  missingDependencies.push(dep);
1978
2874
  artifactSections.push(
1979
2875
  `[${artifactFile}]:
1980
2876
  (integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
1981
2877
  );
1982
2878
  } else {
1983
- artifactSections.push(`[${artifactFile}]:
1984
- ${JSON.stringify(content, null, 2)}`);
2879
+ const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
2880
+ artifactSections.push(
2881
+ `[${artifactFile}]${sigStatus}:
2882
+ ${JSON.stringify(content, null, 2)}`
2883
+ );
1985
2884
  }
1986
2885
  } else {
1987
2886
  missingDependencies.push(dep);
@@ -1989,10 +2888,29 @@ ${JSON.stringify(content, null, 2)}`);
1989
2888
  (not yet available)`);
1990
2889
  }
1991
2890
  }
1992
- const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
2891
+ const parts = [];
2892
+ if (buildContextText) {
2893
+ parts.push("BUILD_CONTEXT:", buildContextText, "");
2894
+ }
2895
+ if (phase === "auth") {
2896
+ const initContextPath = (0, import_path5.join)(cwd, ".stackwright", "init-context.json");
2897
+ if ((0, import_fs5.existsSync)(initContextPath)) {
2898
+ try {
2899
+ const initContext = JSON.parse(safeReadSync(initContextPath));
2900
+ if (initContext.devOnly === true || initContext.nonInteractive === true) {
2901
+ answers["devOnly"] = true;
2902
+ }
2903
+ } catch {
2904
+ }
2905
+ }
2906
+ }
2907
+ parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
1993
2908
  if (artifactSections.length > 0) {
1994
2909
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
1995
2910
  }
2911
+ const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
2912
+ parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
2913
+ parts.push(artifactSchema);
1996
2914
  parts.push("", "Execute using these answers and the upstream artifacts provided.");
1997
2915
  const prompt = parts.join("\n");
1998
2916
  const dependenciesSatisfied = missingDependencies.length === 0;
@@ -2027,49 +2945,326 @@ var OFF_SCRIPT_PATTERNS = [
2027
2945
  var PHASE_REQUIRED_KEYS = {
2028
2946
  designer: ["designLanguage", "themeTokenSeeds"],
2029
2947
  theme: ["tokens"],
2030
- api: ["entities"],
2948
+ api: ["entities", "version", "generatedBy"],
2031
2949
  auth: ["version", "generatedBy"],
2032
- data: ["version", "generatedBy"],
2950
+ data: ["version", "generatedBy", "strategy", "collections"],
2033
2951
  pages: ["version", "generatedBy"],
2034
2952
  dashboard: ["version", "generatedBy"],
2035
- workflow: ["version", "generatedBy"]
2953
+ workflow: ["version", "generatedBy"],
2954
+ services: ["version", "generatedBy", "flows"],
2955
+ polish: ["version", "generatedBy"],
2956
+ geo: ["version", "generatedBy", "geoCollections"]
2036
2957
  };
2037
- function handleValidateArtifact(input) {
2038
- const cwd = input._cwd ?? process.cwd();
2039
- const { phase, responseText } = input;
2040
- if (!isValidPhase(phase)) {
2041
- return {
2042
- text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2043
- isError: true
2044
- };
2045
- }
2046
- for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2047
- if (pattern.test(responseText)) {
2048
- const result = {
2049
- valid: false,
2050
- phase,
2051
- violation: "off-script",
2052
- retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2053
- };
2054
- return { text: JSON.stringify(result), isError: false };
2055
- }
2056
- }
2057
- let artifact;
2058
- try {
2059
- artifact = extractJsonFromResponse(responseText);
2060
- } catch {
2061
- const result = {
2062
- valid: false,
2063
- phase,
2064
- violation: "invalid-json",
2065
- retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
2066
- };
2067
- return { text: JSON.stringify(result), isError: false };
2068
- }
2069
- if (!artifact.version || !artifact.generatedBy) {
2070
- const result = {
2071
- valid: false,
2072
- phase,
2958
+ var PHASE_ARTIFACT_SCHEMA = {
2959
+ designer: JSON.stringify(
2960
+ {
2961
+ version: "1.0",
2962
+ generatedBy: "stackwright-pro-designer-otter",
2963
+ application: {
2964
+ type: "<operational|data-explorer|admin|logistics|general>",
2965
+ environment: "<workstation|field|control-room|mixed>",
2966
+ density: "<compact|balanced|spacious>",
2967
+ accessibility: "<wcag-aa|section-508|none>",
2968
+ colorScheme: "<light|dark|both>"
2969
+ },
2970
+ designLanguage: {
2971
+ rationale: "<design rationale>",
2972
+ spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
2973
+ colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
2974
+ typography: {
2975
+ dataFont: "Inter",
2976
+ headingFont: "Inter",
2977
+ monoFont: "monospace",
2978
+ dataSizePx: 12,
2979
+ bodySizePx: 14
2980
+ },
2981
+ contrastRatio: "4.5",
2982
+ borderRadius: "4",
2983
+ shadowElevation: "standard"
2984
+ },
2985
+ themeTokenSeeds: {
2986
+ light: {
2987
+ background: "#ffffff",
2988
+ foreground: "#1a1a1a",
2989
+ primary: "#1a365d",
2990
+ surface: "#f7f7f7",
2991
+ border: "#e2e8f0"
2992
+ },
2993
+ dark: {
2994
+ background: "#1a1a1a",
2995
+ foreground: "#ffffff",
2996
+ primary: "#90cdf4",
2997
+ surface: "#2d2d2d",
2998
+ border: "#4a5568"
2999
+ }
3000
+ },
3001
+ conformsTo: null,
3002
+ operationalNotes: []
3003
+ },
3004
+ null,
3005
+ 2
3006
+ ),
3007
+ theme: JSON.stringify(
3008
+ {
3009
+ version: "1.0",
3010
+ generatedBy: "stackwright-pro-theme-otter",
3011
+ componentLibrary: "shadcn",
3012
+ colorScheme: "<light|dark|both>",
3013
+ tokens: {
3014
+ colors: { "primary-500": "#1a365d", background: "#ffffff" },
3015
+ spacing: { "spacing-1": "8px", "spacing-2": "16px" },
3016
+ typography: { "font-data": "Inter", "text-sm": "12px" },
3017
+ shape: { "radius-sm": "4px", "radius-md": "8px" },
3018
+ shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
3019
+ },
3020
+ cssVariables: {
3021
+ "--background": "0 0% 100%",
3022
+ "--foreground": "222.2 84% 4.9%",
3023
+ "--primary": "222.2 47.4% 11.2%",
3024
+ "--primary-foreground": "210 40% 98%",
3025
+ "--surface": "210 40% 98%",
3026
+ "--border": "214.3 31.8% 91.4%"
3027
+ },
3028
+ dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
3029
+ },
3030
+ null,
3031
+ 2
3032
+ ),
3033
+ api: JSON.stringify(
3034
+ {
3035
+ version: "1.0",
3036
+ generatedBy: "stackwright-pro-api-otter",
3037
+ entities: [
3038
+ {
3039
+ name: "Shipment",
3040
+ endpoint: "/shipments",
3041
+ method: "GET",
3042
+ revalidate: 60,
3043
+ mutationType: null
3044
+ }
3045
+ ],
3046
+ skipped: [
3047
+ {
3048
+ spec: "ais-message-models.yaml",
3049
+ format: "asyncapi",
3050
+ reason: "AsyncAPI spec \u2014 WebSocket/event integration required (no REST endpoints)"
3051
+ }
3052
+ ],
3053
+ warnings: [
3054
+ "Spec contains external $ref URLs \u2014 recommend bundle: true in stackwright.yml integration config"
3055
+ ],
3056
+ auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
3057
+ baseUrl: "https://api.example.mil/v2",
3058
+ specPath: "./specs/api.yaml"
3059
+ },
3060
+ null,
3061
+ 2
3062
+ ),
3063
+ data: JSON.stringify(
3064
+ {
3065
+ version: "1.0",
3066
+ generatedBy: "stackwright-pro-data-otter",
3067
+ strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
3068
+ pulseMode: false,
3069
+ collections: [{ name: "equipment", revalidate: 60, pulse: false }],
3070
+ endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
3071
+ requiredPackages: { dependencies: {}, devPackages: {} }
3072
+ },
3073
+ null,
3074
+ 2
3075
+ ),
3076
+ geo: JSON.stringify(
3077
+ {
3078
+ version: "1.0",
3079
+ generatedBy: "stackwright-pro-geo-otter",
3080
+ geoCollections: [
3081
+ {
3082
+ collection: "vessels",
3083
+ latField: "latitude",
3084
+ lngField: "longitude",
3085
+ labelField: "vesselName",
3086
+ colorField: "navigationStatus"
3087
+ }
3088
+ ],
3089
+ pages: [
3090
+ {
3091
+ slug: "fleet-tracker",
3092
+ type: "<tracker|zone|route|combined>",
3093
+ collections: ["vessels"],
3094
+ hasLayers: false
3095
+ }
3096
+ ]
3097
+ },
3098
+ null,
3099
+ 2
3100
+ ),
3101
+ workflow: JSON.stringify(
3102
+ {
3103
+ version: "1.0",
3104
+ generatedBy: "stackwright-pro-workflow-otter",
3105
+ workflowConfig: {
3106
+ id: "procurement-approval",
3107
+ route: "/procurement",
3108
+ files: ["workflows/procurement-approval.yml"],
3109
+ serviceDependencies: ["service:workflow-state"],
3110
+ warnings: []
3111
+ }
3112
+ },
3113
+ null,
3114
+ 2
3115
+ ),
3116
+ services: JSON.stringify(
3117
+ {
3118
+ version: "1.0",
3119
+ generatedBy: "stackwright-services-otter",
3120
+ flows: [
3121
+ {
3122
+ name: "at-risk-patients",
3123
+ trigger: "<http|event|schedule|queue>",
3124
+ steps: 3,
3125
+ outputSpec: "at-risk-patients.openapi.json"
3126
+ }
3127
+ ],
3128
+ workflows: [
3129
+ {
3130
+ name: "evacuation-coordination",
3131
+ states: 4,
3132
+ transitions: 5
3133
+ }
3134
+ ],
3135
+ openApiSpecs: ["at-risk-patients.openapi.json"],
3136
+ capabilitiesUsed: ["service.call", "collection.join", "collection.filter"]
3137
+ },
3138
+ null,
3139
+ 2
3140
+ ),
3141
+ pages: JSON.stringify(
3142
+ {
3143
+ version: "1.0",
3144
+ generatedBy: "stackwright-pro-page-otter",
3145
+ pages: [
3146
+ {
3147
+ slug: "catalog",
3148
+ type: "collection_listing",
3149
+ collection: "products",
3150
+ themeApplied: true,
3151
+ authRequired: false
3152
+ },
3153
+ {
3154
+ slug: "admin",
3155
+ type: "protected",
3156
+ collection: null,
3157
+ themeApplied: true,
3158
+ authRequired: true
3159
+ }
3160
+ ]
3161
+ },
3162
+ null,
3163
+ 2
3164
+ ),
3165
+ dashboard: JSON.stringify(
3166
+ {
3167
+ version: "1.0",
3168
+ generatedBy: "stackwright-pro-dashboard-otter",
3169
+ pages: [
3170
+ {
3171
+ slug: "dashboard",
3172
+ layout: "<grid|table|mixed>",
3173
+ collections: ["equipment", "supplies"],
3174
+ mode: "<ISR|Pulse>"
3175
+ }
3176
+ ]
3177
+ },
3178
+ null,
3179
+ 2
3180
+ ),
3181
+ // type: 'pki' = CAC/DoD certificate auth | 'oidc' = enterprise SSO
3182
+ // For dev-only mock auth: use type: 'oidc' with devOnly: true (Zod strips devOnly — it's a convention only)
3183
+ auth: JSON.stringify(
3184
+ {
3185
+ version: "1.0",
3186
+ generatedBy: "stackwright-pro-auth-otter",
3187
+ authConfig: {
3188
+ type: "<pki|oidc>",
3189
+ // OIDC-only fields (omit for pki):
3190
+ provider: "<azure_ad|okta|cognito|auth0|authentik|keycloak|custom>",
3191
+ discoveryUrl: "<IdP OIDC discovery URL>",
3192
+ clientId: "<OIDC client ID>",
3193
+ clientSecret: "<OIDC client secret>",
3194
+ rbacRoles: ["ADMIN", "ANALYST"],
3195
+ rbacDefaultRole: "ANALYST",
3196
+ protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
3197
+ auditEnabled: true,
3198
+ auditRetentionDays: 90
3199
+ },
3200
+ // devScripts is OPTIONAL — only present when devOnly: true was used
3201
+ // Polish otter and foreman MUST check devScripts.written before referencing scripts
3202
+ devScripts: {
3203
+ written: true,
3204
+ scripts: { "dev:admin": "MOCK_USER=admin next dev" }
3205
+ }
3206
+ },
3207
+ null,
3208
+ 2
3209
+ ),
3210
+ polish: JSON.stringify(
3211
+ {
3212
+ version: "1.0",
3213
+ generatedBy: "stackwright-pro-polish-otter",
3214
+ landingPage: { slug: "", rewritten: true },
3215
+ navigation: { itemCount: 5, items: ["/dashboard", "/equipment", "/workflows"] },
3216
+ gettingStarted: "<rewritten|redirected|not-found>",
3217
+ scaffoldCleanup: {
3218
+ deleted: ["content/posts/getting-started.yaml"],
3219
+ rewritten: ["app/not-found.tsx"],
3220
+ skipped: []
3221
+ }
3222
+ },
3223
+ null,
3224
+ 2
3225
+ )
3226
+ };
3227
+ function handleValidateArtifact(input) {
3228
+ const cwd = input._cwd ?? process.cwd();
3229
+ const { phase, responseText, artifact: directArtifact } = input;
3230
+ if (!isValidPhase2(phase)) {
3231
+ return {
3232
+ text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
3233
+ isError: true
3234
+ };
3235
+ }
3236
+ let artifact;
3237
+ if (directArtifact) {
3238
+ artifact = directArtifact;
3239
+ } else {
3240
+ const text = responseText ?? "";
3241
+ for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
3242
+ if (pattern.test(text)) {
3243
+ const result = {
3244
+ valid: false,
3245
+ phase,
3246
+ violation: "off-script",
3247
+ retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
3248
+ };
3249
+ return { text: JSON.stringify(result), isError: false };
3250
+ }
3251
+ }
3252
+ try {
3253
+ artifact = extractJsonFromResponse(text);
3254
+ } catch {
3255
+ const result = {
3256
+ valid: false,
3257
+ phase,
3258
+ violation: "invalid-json",
3259
+ retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
3260
+ };
3261
+ return { text: JSON.stringify(result), isError: false };
3262
+ }
3263
+ }
3264
+ if (!artifact.version || !artifact.generatedBy) {
3265
+ const result = {
3266
+ valid: false,
3267
+ phase,
2073
3268
  violation: "missing-fields",
2074
3269
  retryPrompt: 'Your JSON artifact is missing required fields. Every artifact MUST include "version" and "generatedBy" at the top level.'
2075
3270
  };
@@ -2086,12 +3281,58 @@ function handleValidateArtifact(input) {
2086
3281
  };
2087
3282
  return { text: JSON.stringify(result), isError: false };
2088
3283
  }
3284
+ const PHASE_ZOD_VALIDATORS = {
3285
+ workflow: (artifact2) => {
3286
+ const workflowConfig = artifact2["workflowConfig"];
3287
+ if (!workflowConfig) return { success: true };
3288
+ const result = import_types.WorkflowFileSchema.safeParse(workflowConfig);
3289
+ if (!result.success) {
3290
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3291
+ return { success: false, error: { message: issues } };
3292
+ }
3293
+ return { success: true };
3294
+ },
3295
+ auth: (artifact2) => {
3296
+ const authConfig = artifact2["authConfig"];
3297
+ if (!authConfig) return { success: true };
3298
+ const result = import_types.authConfigSchema.safeParse(authConfig);
3299
+ if (!result.success) {
3300
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3301
+ return { success: false, error: { message: issues } };
3302
+ }
3303
+ return { success: true };
3304
+ }
3305
+ };
3306
+ const zodValidator = PHASE_ZOD_VALIDATORS[phase];
3307
+ if (zodValidator) {
3308
+ const zodResult = zodValidator(artifact);
3309
+ if (!zodResult.success) {
3310
+ const result = {
3311
+ valid: false,
3312
+ phase,
3313
+ violation: "schema-mismatch",
3314
+ retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
3315
+ };
3316
+ return { text: JSON.stringify(result), isError: false };
3317
+ }
3318
+ }
2089
3319
  try {
2090
- const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
2091
- (0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
3320
+ const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
3321
+ (0, import_fs5.mkdirSync)(artifactsDir, { recursive: true });
2092
3322
  const artifactFile = PHASE_ARTIFACT[phase];
2093
- const artifactPath = (0, import_path4.join)(artifactsDir, artifactFile);
2094
- safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
3323
+ const artifactPath = (0, import_path5.join)(artifactsDir, artifactFile);
3324
+ const serialized = JSON.stringify(artifact, null, 2) + "\n";
3325
+ const artifactBytes = Buffer.from(serialized, "utf-8");
3326
+ safeWriteSync(artifactPath, serialized);
3327
+ let signed = false;
3328
+ try {
3329
+ const { privateKey } = loadPipelineKeys(cwd);
3330
+ const sig = signArtifact(artifactBytes, privateKey);
3331
+ const signerOtter = PHASE_TO_OTTER2[phase];
3332
+ saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
3333
+ signed = true;
3334
+ } catch {
3335
+ }
2095
3336
  const state = readState(cwd);
2096
3337
  if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
2097
3338
  const ps = state.phases[phase];
@@ -2102,7 +3343,7 @@ function handleValidateArtifact(input) {
2102
3343
  valid: true,
2103
3344
  phase,
2104
3345
  artifactPath,
2105
- summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
3346
+ summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
2106
3347
  };
2107
3348
  return { text: JSON.stringify(result), isError: false };
2108
3349
  } catch (err) {
@@ -2126,11 +3367,24 @@ function registerPipelineTools(server2) {
2126
3367
  "stackwright_pro_set_pipeline_state",
2127
3368
  `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
2128
3369
  {
2129
- phase: import_zod9.z.string().optional().describe('Phase to update, e.g. "designer"'),
2130
- field: import_zod9.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
2131
- value: import_zod9.z.boolean().optional().describe("Value for the field"),
2132
- status: import_zod9.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
2133
- incrementRetry: import_zod9.z.boolean().optional().describe("Bump retryCount by 1")
3370
+ phase: import_zod11.z.string().optional().describe('Phase to update, e.g. "designer"'),
3371
+ field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
3372
+ value: boolCoerce(import_zod11.z.boolean().optional()).describe(
3373
+ 'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
3374
+ ),
3375
+ status: import_zod11.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3376
+ incrementRetry: boolCoerce(import_zod11.z.boolean().optional()).describe(
3377
+ "Bump retryCount by 1 \u2014 must be a JSON boolean"
3378
+ ),
3379
+ updates: jsonCoerce(
3380
+ import_zod11.z.array(
3381
+ import_zod11.z.object({
3382
+ phase: import_zod11.z.string(),
3383
+ field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]),
3384
+ value: import_zod11.z.boolean()
3385
+ })
3386
+ ).optional()
3387
+ ).describe("Batch updates \u2014 apply multiple field changes in one atomic read\u2192modify\u2192write")
2134
3388
  },
2135
3389
  async (args) => res(
2136
3390
  handleSetPipelineState({
@@ -2138,15 +3392,24 @@ function registerPipelineTools(server2) {
2138
3392
  ...args.field != null ? { field: args.field } : {},
2139
3393
  ...args.value != null ? { value: args.value } : {},
2140
3394
  ...args.status != null ? { status: args.status } : {},
2141
- ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {}
3395
+ ...args.incrementRetry != null ? { incrementRetry: args.incrementRetry } : {},
3396
+ ...args.updates != null ? { updates: args.updates } : {}
2142
3397
  })
2143
3398
  )
2144
3399
  );
2145
3400
  server2.tool(
2146
3401
  "stackwright_pro_check_execution_ready",
2147
- `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
3402
+ `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
3403
+ {
3404
+ phase: import_zod11.z.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
3405
+ },
3406
+ async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
3407
+ );
3408
+ server2.tool(
3409
+ "stackwright_pro_get_ready_phases",
3410
+ `Return phases whose dependencies are all satisfied (executed=true). Use to determine which phases can run next \u2014 enables wave-based execution. ${DESC}`,
2148
3411
  {},
2149
- async () => res(handleCheckExecutionReady())
3412
+ async () => res(handleGetReadyPhases())
2150
3413
  );
2151
3414
  server2.tool(
2152
3415
  "stackwright_pro_list_artifacts",
@@ -2156,40 +3419,91 @@ function registerPipelineTools(server2) {
2156
3419
  );
2157
3420
  server2.tool(
2158
3421
  "stackwright_pro_write_phase_questions",
2159
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
3422
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2160
3423
  {
2161
- phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
2162
- responseText: import_zod9.z.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
3424
+ phase: import_zod11.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
3425
+ responseText: import_zod11.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
3426
+ questions: jsonCoerce(import_zod11.z.array(import_zod11.z.any()).optional()).describe(
3427
+ "Questions array for direct specialist write"
3428
+ )
2163
3429
  },
2164
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
3430
+ async ({ phase, responseText, questions }) => {
3431
+ if (phase && questions && Array.isArray(questions)) {
3432
+ if (!SAFE_PHASE.test(phase)) {
3433
+ return {
3434
+ content: [
3435
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
3436
+ ],
3437
+ isError: true
3438
+ };
3439
+ }
3440
+ const questionsDir = (0, import_path5.join)(process.cwd(), ".stackwright", "questions");
3441
+ (0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
3442
+ const outPath = (0, import_path5.join)(questionsDir, `${phase}.json`);
3443
+ (0, import_fs5.writeFileSync)(
3444
+ outPath,
3445
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
3446
+ );
3447
+ return {
3448
+ content: [
3449
+ {
3450
+ type: "text",
3451
+ text: JSON.stringify({
3452
+ written: true,
3453
+ phase,
3454
+ count: questions.length
3455
+ })
3456
+ }
3457
+ ]
3458
+ };
3459
+ }
3460
+ return res(
3461
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
3462
+ );
3463
+ }
2165
3464
  );
2166
3465
  server2.tool(
2167
3466
  "stackwright_pro_build_specialist_prompt",
2168
3467
  `Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
2169
- { phase: import_zod9.z.string().describe('Phase to build prompt for, e.g. "pages"') },
3468
+ { phase: import_zod11.z.string().describe('Phase to build prompt for, e.g. "pages"') },
2170
3469
  async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
2171
3470
  );
2172
3471
  server2.tool(
2173
3472
  "stackwright_pro_validate_artifact",
2174
- `Validate specialist response + write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
3473
+ `Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
2175
3474
  {
2176
- phase: import_zod9.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
2177
- responseText: import_zod9.z.string().describe("Raw response text from the specialist otter")
3475
+ phase: import_zod11.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
3476
+ responseText: import_zod11.z.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
3477
+ artifact: jsonCoerce(import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()).optional()).describe(
3478
+ "Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
3479
+ )
2178
3480
  },
2179
- async ({ phase, responseText }) => res(handleValidateArtifact({ phase, responseText }))
3481
+ async ({ phase, responseText, artifact }) => {
3482
+ if (artifact) {
3483
+ return res(
3484
+ handleValidateArtifact({ phase, artifact })
3485
+ );
3486
+ }
3487
+ return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
3488
+ }
2180
3489
  );
2181
3490
  }
2182
3491
 
2183
3492
  // src/tools/safe-write.ts
2184
- var import_zod10 = require("zod");
2185
- var import_fs5 = require("fs");
2186
- var import_path5 = require("path");
3493
+ var import_zod12 = require("zod");
3494
+ var import_fs6 = require("fs");
3495
+ var import_path6 = require("path");
2187
3496
  var OTTER_WRITE_ALLOWLISTS = {
2188
3497
  "stackwright-pro-designer-otter": [
2189
3498
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
2190
3499
  ],
2191
3500
  "stackwright-pro-theme-otter": [
2192
- { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Theme tokens artifact" }
3501
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Theme tokens artifact" },
3502
+ {
3503
+ prefix: "stackwright.theme.",
3504
+ suffix: ".yml",
3505
+ description: "Theme config YAML (merged at build time)"
3506
+ }
2193
3507
  ],
2194
3508
  "stackwright-pro-auth-otter": [
2195
3509
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Auth config artifact" },
@@ -2199,6 +3513,16 @@ var OTTER_WRITE_ALLOWLISTS = {
2199
3513
  prefix: ".env",
2200
3514
  suffix: "",
2201
3515
  description: "Dotenv files (.env, .env.local, .env.production, etc.)"
3516
+ },
3517
+ {
3518
+ prefix: "lib/mock-auth",
3519
+ suffix: ".ts",
3520
+ description: "Mock auth module (DEV_ONLY_MODE role updates)"
3521
+ },
3522
+ {
3523
+ prefix: "package.json",
3524
+ suffix: ".json",
3525
+ description: "Project package.json (DEV_ONLY_MODE script cleanup)"
2202
3526
  }
2203
3527
  ],
2204
3528
  "stackwright-pro-data-otter": [
@@ -2220,21 +3544,125 @@ var OTTER_WRITE_ALLOWLISTS = {
2220
3544
  { prefix: "workflows/", suffix: ".yaml", description: "Workflow definition" },
2221
3545
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Workflow config" }
2222
3546
  ],
3547
+ "stackwright-pro-scaffold-otter": [
3548
+ { prefix: "app/", suffix: ".tsx", description: "Next.js App Router shell files" },
3549
+ {
3550
+ prefix: ".stackwright/artifacts/",
3551
+ suffix: ".json",
3552
+ description: "Scaffold manifest artifact"
3553
+ }
3554
+ ],
2223
3555
  "stackwright-pro-api-otter": [
2224
3556
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "API config artifact" }
3557
+ ],
3558
+ "stackwright-pro-geo-otter": [
3559
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Geo manifest artifact" },
3560
+ { prefix: "pages/", suffix: "/content.yml", description: "Map page content" },
3561
+ { prefix: "pages/", suffix: "/content.yaml", description: "Map page content" }
3562
+ ],
3563
+ "stackwright-pro-polish-otter": [
3564
+ {
3565
+ prefix: "stackwright.yml",
3566
+ suffix: "",
3567
+ description: "Stackwright config (navigation updates)"
3568
+ },
3569
+ { prefix: "pages/", suffix: "/content.yml", description: "Landing page content" },
3570
+ { prefix: "pages/", suffix: "/content.yaml", description: "Landing page content" },
3571
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Polish artifact" },
3572
+ { prefix: "README.md", suffix: "", description: "Project README rewrite" }
3573
+ ],
3574
+ "stackwright-services-otter": [
3575
+ { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Services config artifact" },
3576
+ { prefix: "services/", suffix: ".ts", description: "Service implementation files" },
3577
+ { prefix: "services/", suffix: ".yaml", description: "Service flow definitions" },
3578
+ { prefix: "services/", suffix: ".yml", description: "Service flow definitions" },
3579
+ { prefix: "lib/seeds/", suffix: ".ts", description: "Seed data files" },
3580
+ { prefix: "specs/", suffix: ".json", description: "Generated OpenAPI specs" },
3581
+ { prefix: "specs/", suffix: ".yaml", description: "Generated OpenAPI specs" },
3582
+ { prefix: "stackwright-generated/", suffix: ".json", description: "Services manifest" }
2225
3583
  ]
2226
3584
  };
2227
3585
  var PROTECTED_PATH_PREFIXES = [
2228
3586
  ".stackwright/pipeline-state.json",
3587
+ ".stackwright/pipeline-keys.json",
3588
+ // ephemeral signing keys
3589
+ ".stackwright/artifacts/signatures.json",
3590
+ // artifact signature manifest
2229
3591
  ".stackwright/questions/",
2230
- ".stackwright/answers/"
3592
+ ".stackwright/answers/",
3593
+ ".stackwright/page-registry.json"
3594
+ // page ownership — managed by safe_write internally
2231
3595
  ];
3596
+ var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
3597
+ var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
3598
+ var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
3599
+ var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
3600
+ function getMaxBytesForPath(filePath) {
3601
+ if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
3602
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
3603
+ return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
3604
+ if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
3605
+ return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
3606
+ return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
3607
+ }
3608
+ var PAGE_REGISTRY_FILE = ".stackwright/page-registry.json";
3609
+ function extractPageSlug(filePath) {
3610
+ const match = (0, import_path6.normalize)(filePath).match(/^pages\/(.+?)\/content\.ya?ml$/);
3611
+ return match?.[1] ?? null;
3612
+ }
3613
+ function extractContentTypes(yamlContent) {
3614
+ const typePattern = /^\s*-?\s*type:\s*(\S+)/gm;
3615
+ const types = [];
3616
+ let m;
3617
+ while ((m = typePattern.exec(yamlContent)) !== null) {
3618
+ const typeName = m[1];
3619
+ if (typeName && !types.includes(typeName)) {
3620
+ types.push(typeName);
3621
+ }
3622
+ }
3623
+ return types.length > 0 ? types : ["unknown"];
3624
+ }
3625
+ function readPageRegistry(cwd) {
3626
+ const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
3627
+ if (!(0, import_fs6.existsSync)(regPath)) return {};
3628
+ try {
3629
+ const stat = (0, import_fs6.lstatSync)(regPath);
3630
+ if (stat.isSymbolicLink()) return {};
3631
+ return JSON.parse((0, import_fs6.readFileSync)(regPath, "utf-8"));
3632
+ } catch {
3633
+ return {};
3634
+ }
3635
+ }
3636
+ function writePageRegistry(cwd, registry) {
3637
+ const dir = (0, import_path6.join)(cwd, ".stackwright");
3638
+ (0, import_fs6.mkdirSync)(dir, { recursive: true });
3639
+ const regPath = (0, import_path6.join)(cwd, PAGE_REGISTRY_FILE);
3640
+ if ((0, import_fs6.existsSync)(regPath)) {
3641
+ const stat = (0, import_fs6.lstatSync)(regPath);
3642
+ if (stat.isSymbolicLink()) {
3643
+ throw new Error("Refusing to write page registry through symlink");
3644
+ }
3645
+ }
3646
+ (0, import_fs6.writeFileSync)(regPath, JSON.stringify(registry, null, 2) + "\n", { encoding: "utf-8" });
3647
+ }
3648
+ function checkPageOwnership(cwd, slug, callerOtter) {
3649
+ const registry = readPageRegistry(cwd);
3650
+ const existing = registry[slug];
3651
+ if (!existing || existing.claimedBy === callerOtter) {
3652
+ return { allowed: true };
3653
+ }
3654
+ return {
3655
+ allowed: false,
3656
+ owner: existing.claimedBy,
3657
+ contentTypes: existing.contentTypes
3658
+ };
3659
+ }
2232
3660
  function checkPathAllowed(callerOtter, filePath) {
2233
- const normalized = (0, import_path5.normalize)(filePath);
3661
+ const normalized = (0, import_path6.normalize)(filePath);
2234
3662
  if (normalized.includes("..")) {
2235
3663
  return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
2236
3664
  }
2237
- if ((0, import_path5.isAbsolute)(normalized)) {
3665
+ if ((0, import_path6.isAbsolute)(normalized)) {
2238
3666
  return {
2239
3667
  allowed: false,
2240
3668
  error: "Absolute paths are not allowed \u2014 use paths relative to project root"
@@ -2270,6 +3698,16 @@ function checkPathAllowed(callerOtter, filePath) {
2270
3698
  continue;
2271
3699
  }
2272
3700
  }
3701
+ if (rule.prefix === "lib/mock-auth" && rule.suffix === ".ts") {
3702
+ if (normalized !== "lib/mock-auth.ts") {
3703
+ continue;
3704
+ }
3705
+ }
3706
+ if (rule.prefix === "package.json" && rule.suffix === ".json") {
3707
+ if (normalized !== "package.json") {
3708
+ continue;
3709
+ }
3710
+ }
2273
3711
  return { allowed: true, rule: rule.description };
2274
3712
  }
2275
3713
  }
@@ -2354,11 +3792,23 @@ function handleSafeWrite(input) {
2354
3792
  };
2355
3793
  return { text: JSON.stringify(result), isError: true };
2356
3794
  }
2357
- const normalized = (0, import_path5.normalize)(filePath);
2358
- const fullPath = (0, import_path5.join)(cwd, normalized);
2359
- if ((0, import_fs5.existsSync)(fullPath)) {
3795
+ const contentBytes = Buffer.byteLength(content, "utf-8");
3796
+ const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
3797
+ if (contentBytes > maxBytes) {
3798
+ const result = {
3799
+ success: false,
3800
+ error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
3801
+ callerOtter,
3802
+ attemptedPath: filePath,
3803
+ allowedPaths: []
3804
+ };
3805
+ return { text: JSON.stringify(result), isError: true };
3806
+ }
3807
+ const normalized = (0, import_path6.normalize)(filePath);
3808
+ const fullPath = (0, import_path6.join)(cwd, normalized);
3809
+ if ((0, import_fs6.existsSync)(fullPath)) {
2360
3810
  try {
2361
- const stat = (0, import_fs5.lstatSync)(fullPath);
3811
+ const stat = (0, import_fs6.lstatSync)(fullPath);
2362
3812
  if (stat.isSymbolicLink()) {
2363
3813
  const result = {
2364
3814
  success: false,
@@ -2383,11 +3833,38 @@ function handleSafeWrite(input) {
2383
3833
  };
2384
3834
  return { text: JSON.stringify(result), isError: true };
2385
3835
  }
3836
+ const pageSlug = extractPageSlug(normalized);
3837
+ if (pageSlug) {
3838
+ const ownership = checkPageOwnership(cwd, pageSlug, callerOtter);
3839
+ if (!ownership.allowed) {
3840
+ const result = {
3841
+ success: false,
3842
+ error: `Page slug "${pageSlug}" is already claimed by ${ownership.owner} (content types: ${ownership.contentTypes.join(", ")}). Use a different slug or coordinate with the owning otter.`,
3843
+ callerOtter,
3844
+ attemptedPath: filePath,
3845
+ allowedPaths: []
3846
+ };
3847
+ return { text: JSON.stringify(result), isError: true };
3848
+ }
3849
+ }
2386
3850
  try {
2387
3851
  if (createDirectories) {
2388
- (0, import_fs5.mkdirSync)((0, import_path5.dirname)(fullPath), { recursive: true });
3852
+ (0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
3853
+ }
3854
+ (0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
3855
+ if (pageSlug) {
3856
+ try {
3857
+ const registry = readPageRegistry(cwd);
3858
+ registry[pageSlug] = {
3859
+ slug: pageSlug,
3860
+ claimedBy: callerOtter,
3861
+ contentTypes: extractContentTypes(content),
3862
+ writtenAt: (/* @__PURE__ */ new Date()).toISOString()
3863
+ };
3864
+ writePageRegistry(cwd, registry);
3865
+ } catch {
3866
+ }
2389
3867
  }
2390
- (0, import_fs5.writeFileSync)(fullPath, content, { encoding: "utf-8" });
2391
3868
  const result = {
2392
3869
  success: true,
2393
3870
  path: normalized,
@@ -2413,10 +3890,12 @@ function registerSafeWriteTools(server2) {
2413
3890
  "stackwright_pro_safe_write",
2414
3891
  DESC,
2415
3892
  {
2416
- callerOtter: import_zod10.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
2417
- filePath: import_zod10.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
2418
- content: import_zod10.z.string().describe("File content to write"),
2419
- createDirectories: import_zod10.z.boolean().optional().describe("Create parent directories if they don't exist. Default: true")
3893
+ callerOtter: import_zod12.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
3894
+ filePath: import_zod12.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
3895
+ content: import_zod12.z.string().describe("File content to write"),
3896
+ createDirectories: boolCoerce(import_zod12.z.boolean().optional().default(true)).describe(
3897
+ "Create parent directories if they don't exist. Default: true"
3898
+ )
2420
3899
  },
2421
3900
  async ({ callerOtter, filePath, content, createDirectories }) => {
2422
3901
  const result = handleSafeWrite({
@@ -2431,9 +3910,9 @@ function registerSafeWriteTools(server2) {
2431
3910
  }
2432
3911
 
2433
3912
  // src/tools/auth.ts
2434
- var import_zod11 = require("zod");
2435
- var import_fs6 = require("fs");
2436
- var import_path6 = require("path");
3913
+ var import_zod13 = require("zod");
3914
+ var import_fs7 = require("fs");
3915
+ var import_path7 = require("path");
2437
3916
  function buildHierarchy(roles) {
2438
3917
  const h = {};
2439
3918
  for (let i = 0; i < roles.length - 1; i++) {
@@ -2453,6 +3932,19 @@ function routesToYaml(routes, defaultRole, indent) {
2453
3932
  return routes.map((r) => `${indent}- pattern: ${r}
2454
3933
  ${indent} requiredRole: ${defaultRole}`).join("\n");
2455
3934
  }
3935
+ function detectNextMajorVersion(cwd) {
3936
+ try {
3937
+ const pkgPath = (0, import_path7.join)(cwd, "package.json");
3938
+ if (!(0, import_fs7.existsSync)(pkgPath)) return null;
3939
+ const pkg = JSON.parse((0, import_fs7.readFileSync)(pkgPath, "utf8"));
3940
+ const nextVersion = pkg.dependencies?.next ?? pkg.devDependencies?.next;
3941
+ if (!nextVersion) return null;
3942
+ const match = nextVersion.match(/(\d+)/);
3943
+ return match ? parseInt(match[1], 10) : null;
3944
+ } catch {
3945
+ return null;
3946
+ }
3947
+ }
2456
3948
  function upsertAuthBlock(existing, authYaml) {
2457
3949
  const lines = existing.split("\n");
2458
3950
  let authStart = -1;
@@ -2495,7 +3987,7 @@ function generateMiddlewareContent(method, params, roles, defaultRole, hierarchy
2495
3987
  // \u26A0\uFE0F SECURITY REVIEW REQUIRED \u2014 CAC/PKI certificate validation
2496
3988
  // DoD security officer review required before production deployment.
2497
3989
  // Verify: CA bundle completeness, EDIPI lookup endpoint, OCSP accessibility.
2498
- import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
3990
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
2499
3991
 
2500
3992
  export const middleware = createProMiddleware({
2501
3993
  method: 'cac',
@@ -2517,7 +4009,7 @@ ${configBlock}
2517
4009
  const scopes2 = params.oidcScopes ?? "openid profile email";
2518
4010
  const roleClaim = params.oidcRoleClaim ?? "roles";
2519
4011
  return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
2520
- import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
4012
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
2521
4013
 
2522
4014
  export const middleware = createProMiddleware({
2523
4015
  method: 'oidc',
@@ -2538,7 +4030,7 @@ ${configBlock}
2538
4030
  }
2539
4031
  const scopes = params.oauth2Scopes ?? "read write";
2540
4032
  return `// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs
2541
- import { createProMiddleware } from '@stackwright-pro/auth-nextjs';
4033
+ import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';
2542
4034
 
2543
4035
  export const middleware = createProMiddleware({
2544
4036
  method: 'oauth2',
@@ -2554,6 +4046,91 @@ ${auditBlock}
2554
4046
  ${routesBlock}
2555
4047
  });
2556
4048
 
4049
+ ${configBlock}
4050
+ `;
4051
+ }
4052
+ function generateProxyContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4053
+ const rbacBlock = ` rbac: {
4054
+ roles: ${JSON.stringify(roles)},
4055
+ defaultRole: '${defaultRole}',
4056
+ hierarchy: ${JSON.stringify(hierarchy, null, 4)},
4057
+ },`;
4058
+ const auditBlock = ` audit: {
4059
+ enabled: ${auditEnabled},
4060
+ retentionDays: ${auditRetentionDays},
4061
+ },`;
4062
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4063
+ const configBlock = `export const config = {
4064
+ matcher: ${JSON.stringify(protectedRoutes)},
4065
+ };`;
4066
+ if (method === "cac") {
4067
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4068
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4069
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4070
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4071
+ return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
4072
+ // SECURITY REVIEW REQUIRED \u2014 CAC/PKI certificate validation
4073
+ // DoD security officer review required before production deployment.
4074
+ // Verify: CA bundle completeness, EDIPI lookup endpoint, OCSP accessibility.
4075
+ import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
4076
+
4077
+ export const proxy = createProProxy({
4078
+ method: 'cac',
4079
+ cac: {
4080
+ caBundle: process.env.CAC_CA_BUNDLE ?? '${caBundle}',
4081
+ edipiLookup: '${edipiLookup}',
4082
+ ocspEndpoint: process.env.CAC_OCSP_ENDPOINT ?? '${ocspEndpoint}',
4083
+ certHeader: '${certHeader}',
4084
+ },
4085
+ ${rbacBlock}
4086
+ ${auditBlock}
4087
+ ${routesBlock}
4088
+ });
4089
+
4090
+ ${configBlock}
4091
+ `;
4092
+ }
4093
+ if (method === "oidc") {
4094
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4095
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4096
+ return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
4097
+ import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
4098
+
4099
+ export const proxy = createProProxy({
4100
+ method: 'oidc',
4101
+ oidc: {
4102
+ discoveryUrl: process.env.OIDC_DISCOVERY_URL!,
4103
+ clientId: process.env.OIDC_CLIENT_ID!,
4104
+ clientSecret: process.env.OIDC_CLIENT_SECRET!,
4105
+ scopes: '${scopes2}',
4106
+ roleClaim: '${roleClaim}',
4107
+ },
4108
+ ${rbacBlock}
4109
+ ${auditBlock}
4110
+ ${routesBlock}
4111
+ });
4112
+
4113
+ ${configBlock}
4114
+ `;
4115
+ }
4116
+ const scopes = params.oauth2Scopes ?? "read write";
4117
+ return `// proxy.ts \u2014 generated by @stackwright-pro/auth (Next.js >=16)
4118
+ import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';
4119
+
4120
+ export const proxy = createProProxy({
4121
+ method: 'oauth2',
4122
+ oauth2: {
4123
+ authorizationUrl: process.env.OAUTH2_AUTH_URL!,
4124
+ tokenUrl: process.env.OAUTH2_TOKEN_URL!,
4125
+ clientId: process.env.OAUTH2_CLIENT_ID!,
4126
+ clientSecret: process.env.OAUTH2_CLIENT_SECRET!,
4127
+ scopes: '${scopes}',
4128
+ },
4129
+ ${rbacBlock}
4130
+ ${auditBlock}
4131
+ ${routesBlock}
4132
+ });
4133
+
2557
4134
  ${configBlock}
2558
4135
  `;
2559
4136
  }
@@ -2583,7 +4160,7 @@ OAUTH2_CLIENT_ID=your-client-id
2583
4160
  OAUTH2_CLIENT_SECRET=your-client-secret
2584
4161
  `;
2585
4162
  }
2586
- function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4163
+ function generateYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
2587
4164
  const rbacSection = ` rbac:
2588
4165
  roles:
2589
4166
  ${rolesToYaml(roles, " ")}
@@ -2606,7 +4183,7 @@ ${routesToYaml(protectedRoutes, " ", defaultRole)}`.replace(/\n\s+,/g, "");
2606
4183
  const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
2607
4184
  return `auth:
2608
4185
  method: cac
2609
- ${providerLine} middleware: ./middleware.ts
4186
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
2610
4187
  cac:
2611
4188
  caBundle: \${CAC_CA_BUNDLE}
2612
4189
  edipiLookup: ${edipiLookup}
@@ -2623,7 +4200,7 @@ ${auditSection}
2623
4200
  const roleClaim = params.oidcRoleClaim ?? "roles";
2624
4201
  return `auth:
2625
4202
  method: oidc
2626
- ${providerLine} middleware: ./middleware.ts
4203
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
2627
4204
  oidc:
2628
4205
  discoveryUrl: \${OIDC_DISCOVERY_URL}
2629
4206
  clientId: \${OIDC_CLIENT_ID}
@@ -2639,7 +4216,7 @@ ${auditSection}
2639
4216
  const scopes = params.oauth2Scopes ?? "read write";
2640
4217
  return `auth:
2641
4218
  method: oauth2
2642
- ${providerLine} middleware: ./middleware.ts
4219
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
2643
4220
  oauth2:
2644
4221
  authorizationUrl: \${OAUTH2_AUTH_URL}
2645
4222
  tokenUrl: \${OAUTH2_TOKEN_URL}
@@ -2652,10 +4229,321 @@ ${routeLines}
2652
4229
  ${auditSection}
2653
4230
  `;
2654
4231
  }
4232
+ function generateDevOnlyMiddlewareContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4233
+ const rbacBlock = ` rbac: {
4234
+ roles: ${JSON.stringify(roles)},
4235
+ defaultRole: '${defaultRole}',
4236
+ hierarchy: ${JSON.stringify(hierarchy, null, 4)},
4237
+ },`;
4238
+ const auditBlock = ` audit: {
4239
+ enabled: ${auditEnabled},
4240
+ retentionDays: ${auditRetentionDays},
4241
+ },`;
4242
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4243
+ const configBlock = `export const config = {
4244
+ matcher: ${JSON.stringify(protectedRoutes)},
4245
+ };`;
4246
+ const devHeader = [
4247
+ "// middleware.ts \u2014 generated by @stackwright-pro/auth-nextjs (dev-only mock)",
4248
+ "// DEV ONLY \u2014 uses mock authentication from lib/mock-auth.ts",
4249
+ "// Do NOT deploy to production.",
4250
+ "import { createProMiddleware } from '@stackwright-pro/auth-nextjs/edge';",
4251
+ "import { mockAuthProvider } from './lib/mock-auth';"
4252
+ ].join("\n");
4253
+ if (method === "cac") {
4254
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4255
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4256
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4257
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4258
+ return `${devHeader}
4259
+
4260
+ export const middleware = createProMiddleware({
4261
+ method: 'cac',
4262
+ cac: {
4263
+ caBundle: '${caBundle}',
4264
+ edipiLookup: '${edipiLookup}',
4265
+ ocspEndpoint: '${ocspEndpoint}',
4266
+ certHeader: '${certHeader}',
4267
+ provider: mockAuthProvider,
4268
+ },
4269
+ ${rbacBlock}
4270
+ ${auditBlock}
4271
+ ${routesBlock}
4272
+ });
4273
+
4274
+ ${configBlock}
4275
+ `;
4276
+ }
4277
+ if (method === "oidc") {
4278
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4279
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4280
+ return `${devHeader}
4281
+
4282
+ export const middleware = createProMiddleware({
4283
+ method: 'oidc',
4284
+ oidc: {
4285
+ discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
4286
+ clientId: 'dev-mock-client',
4287
+ clientSecret: 'dev-mock-secret',
4288
+ scopes: '${scopes2}',
4289
+ roleClaim: '${roleClaim}',
4290
+ provider: mockAuthProvider,
4291
+ },
4292
+ ${rbacBlock}
4293
+ ${auditBlock}
4294
+ ${routesBlock}
4295
+ });
4296
+
4297
+ ${configBlock}
4298
+ `;
4299
+ }
4300
+ const scopes = params.oauth2Scopes ?? "read write";
4301
+ return `${devHeader}
4302
+
4303
+ export const middleware = createProMiddleware({
4304
+ method: 'oauth2',
4305
+ oauth2: {
4306
+ authorizationUrl: 'https://dev-mock-oauth2/authorize',
4307
+ tokenUrl: 'https://dev-mock-oauth2/token',
4308
+ clientId: 'dev-mock-client',
4309
+ clientSecret: 'dev-mock-secret',
4310
+ scopes: '${scopes}',
4311
+ provider: mockAuthProvider,
4312
+ },
4313
+ ${rbacBlock}
4314
+ ${auditBlock}
4315
+ ${routesBlock}
4316
+ });
4317
+
4318
+ ${configBlock}
4319
+ `;
4320
+ }
4321
+ function generateDevOnlyProxyContent(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes) {
4322
+ const rbacBlock = ` rbac: {
4323
+ roles: ${JSON.stringify(roles)},
4324
+ defaultRole: '${defaultRole}',
4325
+ hierarchy: ${JSON.stringify(hierarchy, null, 4)},
4326
+ },`;
4327
+ const auditBlock = ` audit: {
4328
+ enabled: ${auditEnabled},
4329
+ retentionDays: ${auditRetentionDays},
4330
+ },`;
4331
+ const routesBlock = ` protectedRoutes: ${JSON.stringify(protectedRoutes)},`;
4332
+ const configBlock = `export const config = {
4333
+ matcher: ${JSON.stringify(protectedRoutes)},
4334
+ };`;
4335
+ const devHeader = [
4336
+ "// proxy.ts -- generated by @stackwright-pro/auth (Next.js >=16, dev-only mock)",
4337
+ "// DEV ONLY -- uses mock authentication from lib/mock-auth.ts",
4338
+ "// Do NOT deploy to production.",
4339
+ "import { createProProxy } from '@stackwright-pro/auth-nextjs/edge';",
4340
+ "import { mockAuthProvider } from './lib/mock-auth';"
4341
+ ].join("\n");
4342
+ if (method === "cac") {
4343
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4344
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4345
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4346
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4347
+ return `${devHeader}
4348
+
4349
+ export const proxy = createProProxy({
4350
+ method: 'cac',
4351
+ cac: {
4352
+ caBundle: '${caBundle}',
4353
+ edipiLookup: '${edipiLookup}',
4354
+ ocspEndpoint: '${ocspEndpoint}',
4355
+ certHeader: '${certHeader}',
4356
+ provider: mockAuthProvider,
4357
+ },
4358
+ ${rbacBlock}
4359
+ ${auditBlock}
4360
+ ${routesBlock}
4361
+ });
4362
+
4363
+ ${configBlock}
4364
+ `;
4365
+ }
4366
+ if (method === "oidc") {
4367
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4368
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4369
+ return `${devHeader}
4370
+
4371
+ export const proxy = createProProxy({
4372
+ method: 'oidc',
4373
+ oidc: {
4374
+ discoveryUrl: 'https://dev-mock-oidc/.well-known/openid-configuration',
4375
+ clientId: 'dev-mock-client',
4376
+ clientSecret: 'dev-mock-secret',
4377
+ scopes: '${scopes2}',
4378
+ roleClaim: '${roleClaim}',
4379
+ provider: mockAuthProvider,
4380
+ },
4381
+ ${rbacBlock}
4382
+ ${auditBlock}
4383
+ ${routesBlock}
4384
+ });
4385
+
4386
+ ${configBlock}
4387
+ `;
4388
+ }
4389
+ const scopes = params.oauth2Scopes ?? "read write";
4390
+ return `${devHeader}
4391
+
4392
+ export const proxy = createProProxy({
4393
+ method: 'oauth2',
4394
+ oauth2: {
4395
+ authorizationUrl: 'https://dev-mock-oauth2/authorize',
4396
+ tokenUrl: 'https://dev-mock-oauth2/token',
4397
+ clientId: 'dev-mock-client',
4398
+ clientSecret: 'dev-mock-secret',
4399
+ scopes: '${scopes}',
4400
+ provider: mockAuthProvider,
4401
+ },
4402
+ ${rbacBlock}
4403
+ ${auditBlock}
4404
+ ${routesBlock}
4405
+ });
4406
+
4407
+ ${configBlock}
4408
+ `;
4409
+ }
4410
+ function generateDevOnlyYamlBlock(method, params, roles, defaultRole, hierarchy, auditEnabled, auditRetentionDays, protectedRoutes, useProxy) {
4411
+ const rbacSection = ` rbac:
4412
+ roles:
4413
+ ${rolesToYaml(roles, " ")}
4414
+ defaultRole: ${defaultRole}
4415
+ hierarchy:
4416
+ ${hierarchyToYaml(hierarchy, " ")}`;
4417
+ const auditSection = ` audit:
4418
+ enabled: ${auditEnabled}
4419
+ retentionDays: ${auditRetentionDays}`;
4420
+ const routeLines = protectedRoutes.map((r) => ` - pattern: ${r}
4421
+ requiredRole: ${defaultRole}`).join("\n");
4422
+ const providerLine = params.provider ? ` provider: ${params.provider}
4423
+ ` : "";
4424
+ if (method === "cac") {
4425
+ const caBundle = params.cacCaBundle ?? "./certs/dod-ca-bundle.pem";
4426
+ const edipiLookup = params.cacEdipiLookup ?? "./config/edipi-lookup.json";
4427
+ const ocspEndpoint = params.cacOcspEndpoint ?? "https://ocsp.disa.mil";
4428
+ const certHeader = params.cacCertHeader ?? "X-SSL-Client-Cert";
4429
+ return `auth:
4430
+ method: cac
4431
+ devOnly: true
4432
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4433
+ cac:
4434
+ caBundle: ${caBundle}
4435
+ edipiLookup: ${edipiLookup}
4436
+ ocspEndpoint: ${ocspEndpoint}
4437
+ certHeader: ${certHeader}
4438
+ ${rbacSection}
4439
+ protectedRoutes:
4440
+ ${routeLines}
4441
+ ${auditSection}
4442
+ `;
4443
+ }
4444
+ if (method === "oidc") {
4445
+ const scopes2 = params.oidcScopes ?? "openid profile email";
4446
+ const roleClaim = params.oidcRoleClaim ?? "roles";
4447
+ return `auth:
4448
+ method: oidc
4449
+ devOnly: true
4450
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4451
+ oidc:
4452
+ discoveryUrl: https://dev-mock-oidc/.well-known/openid-configuration
4453
+ clientId: dev-mock-client
4454
+ clientSecret: dev-mock-secret
4455
+ scopes: ${scopes2}
4456
+ roleClaim: ${roleClaim}
4457
+ ${rbacSection}
4458
+ protectedRoutes:
4459
+ ${routeLines}
4460
+ ${auditSection}
4461
+ `;
4462
+ }
4463
+ const scopes = params.oauth2Scopes ?? "read write";
4464
+ return `auth:
4465
+ method: oauth2
4466
+ devOnly: true
4467
+ ${providerLine} ${useProxy ? "proxy" : "middleware"}: ./${useProxy ? "proxy" : "middleware"}.ts
4468
+ oauth2:
4469
+ authorizationUrl: https://dev-mock-oauth2/authorize
4470
+ tokenUrl: https://dev-mock-oauth2/token
4471
+ clientId: dev-mock-client
4472
+ clientSecret: dev-mock-secret
4473
+ scopes: ${scopes}
4474
+ ${rbacSection}
4475
+ protectedRoutes:
4476
+ ${routeLines}
4477
+ ${auditSection}
4478
+ `;
4479
+ }
4480
+ function deriveDevKey(roleName) {
4481
+ const segment = roleName.split("_")[0];
4482
+ return (segment ?? roleName).toLowerCase();
4483
+ }
4484
+ function generateMockAuthContent(roles, mockUsers) {
4485
+ const entries = [];
4486
+ const scriptLines = [];
4487
+ for (let i = 0; i < roles.length; i++) {
4488
+ const role = roles[i];
4489
+ const devKey = deriveDevKey(role);
4490
+ const persona = mockUsers?.[i];
4491
+ const name = persona?.name ?? `Dev ${role}`;
4492
+ const email = persona?.email ?? `dev-${devKey}@example.mil`;
4493
+ const edipi = String(i + 1).padStart(10, "0");
4494
+ entries.push(` ${devKey}: {
4495
+ id: 'mock-${devKey}-${String(i + 1).padStart(3, "0")}',
4496
+ name: '${name}',
4497
+ email: '${email}',
4498
+ roles: ['${role}'],
4499
+ edipi: '${edipi}',
4500
+ }`);
4501
+ scriptLines.push(` * pnpm dev:${devKey} \u2014 ${name}`);
4502
+ }
4503
+ return `/**
4504
+ * Mock authentication for development mode.
4505
+ *
4506
+ * DEV_ONLY_MODE \u2014 no real auth provider. Select a persona via MOCK_USER env var
4507
+ * or use the convenience scripts in package.json:
4508
+ ${scriptLines.join("\n")}
4509
+ */
4510
+
4511
+ export const MOCK_USERS = {
4512
+ ${entries.join(",\n")}
4513
+ } as const;
4514
+
4515
+ export type MockRole = keyof typeof MOCK_USERS;
4516
+
4517
+ export function getMockUser(role?: string) {
4518
+ const key = (role ?? process.env.MOCK_USER) as MockRole | undefined;
4519
+ if (!key) return null;
4520
+ return MOCK_USERS[key] ?? null;
4521
+ }
4522
+
4523
+ export function mockAuthProvider() {
4524
+ return getMockUser();
4525
+ }
4526
+ `;
4527
+ }
4528
+ function updatePackageJsonScripts(existingJson, roles, mockUsers) {
4529
+ const pkg = JSON.parse(existingJson);
4530
+ const scripts = pkg.scripts ?? {};
4531
+ delete scripts["dev:admin"];
4532
+ delete scripts["dev:analyst"];
4533
+ delete scripts["dev:viewer"];
4534
+ for (let i = 0; i < roles.length; i++) {
4535
+ const devKey = deriveDevKey(roles[i]);
4536
+ scripts[`dev:${devKey}`] = `MOCK_USER=${devKey} next dev`;
4537
+ }
4538
+ pkg.scripts = scripts;
4539
+ return JSON.stringify(pkg, null, 2) + "\n";
4540
+ }
2655
4541
  async function configureAuthHandler(params, cwd) {
2656
4542
  const {
2657
4543
  method,
2658
4544
  provider,
4545
+ devOnly = false,
4546
+ mockUsers,
2659
4547
  rbacRoles = ["SUPER_ADMIN", "ADMIN", "ANALYST"],
2660
4548
  auditEnabled = true,
2661
4549
  auditRetentionDays = 90,
@@ -2664,6 +4552,9 @@ async function configureAuthHandler(params, cwd) {
2664
4552
  const roles = rbacRoles;
2665
4553
  const defaultRole = params.rbacDefaultRole ?? roles[roles.length - 1];
2666
4554
  const hierarchy = buildHierarchy(roles);
4555
+ const detectedVersion = params.nextMajorVersion ?? detectNextMajorVersion(cwd);
4556
+ const useProxy = (detectedVersion ?? 0) >= 16;
4557
+ const conventionFile = useProxy ? "proxy.ts" : "middleware.ts";
2667
4558
  if (method === "none") {
2668
4559
  return {
2669
4560
  content: [
@@ -2685,7 +4576,34 @@ async function configureAuthHandler(params, cwd) {
2685
4576
  }
2686
4577
  const filesWritten = [];
2687
4578
  try {
2688
- const middlewareContent = generateMiddlewareContent(
4579
+ const authFileContent = devOnly ? useProxy ? generateDevOnlyProxyContent(
4580
+ method,
4581
+ params,
4582
+ roles,
4583
+ defaultRole,
4584
+ hierarchy,
4585
+ auditEnabled,
4586
+ auditRetentionDays,
4587
+ protectedRoutes
4588
+ ) : generateDevOnlyMiddlewareContent(
4589
+ method,
4590
+ params,
4591
+ roles,
4592
+ defaultRole,
4593
+ hierarchy,
4594
+ auditEnabled,
4595
+ auditRetentionDays,
4596
+ protectedRoutes
4597
+ ) : useProxy ? generateProxyContent(
4598
+ method,
4599
+ params,
4600
+ roles,
4601
+ defaultRole,
4602
+ hierarchy,
4603
+ auditEnabled,
4604
+ auditRetentionDays,
4605
+ protectedRoutes
4606
+ ) : generateMiddlewareContent(
2689
4607
  method,
2690
4608
  params,
2691
4609
  roles,
@@ -2695,44 +4613,95 @@ async function configureAuthHandler(params, cwd) {
2695
4613
  auditRetentionDays,
2696
4614
  protectedRoutes
2697
4615
  );
2698
- (0, import_fs6.writeFileSync)((0, import_path6.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
2699
- filesWritten.push("middleware.ts");
4616
+ (0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, conventionFile), authFileContent, "utf8");
4617
+ filesWritten.push(conventionFile);
2700
4618
  } catch (err) {
2701
4619
  const msg = err instanceof Error ? err.message : String(err);
2702
4620
  return {
2703
4621
  content: [
2704
4622
  {
2705
4623
  type: "text",
2706
- text: JSON.stringify({ success: false, error: `Failed writing middleware.ts: ${msg}` })
4624
+ text: JSON.stringify({
4625
+ success: false,
4626
+ error: `Failed writing ${conventionFile}: ${msg}`
4627
+ })
2707
4628
  }
2708
4629
  ],
2709
4630
  isError: true
2710
4631
  };
2711
4632
  }
2712
- try {
2713
- const envBlock = generateEnvBlock(method, params);
2714
- const envPath = (0, import_path6.join)(cwd, ".env.example");
2715
- if ((0, import_fs6.existsSync)(envPath)) {
2716
- const existing = (0, import_fs6.readFileSync)(envPath, "utf8");
2717
- (0, import_fs6.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2718
- } else {
2719
- (0, import_fs6.writeFileSync)(envPath, envBlock, "utf8");
4633
+ if (!devOnly) {
4634
+ try {
4635
+ const envBlock = generateEnvBlock(method, params);
4636
+ const envPath = (0, import_path7.join)(cwd, ".env.example");
4637
+ if ((0, import_fs7.existsSync)(envPath)) {
4638
+ const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
4639
+ (0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
4640
+ } else {
4641
+ (0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
4642
+ }
4643
+ filesWritten.push(".env.example");
4644
+ } catch (err) {
4645
+ const msg = err instanceof Error ? err.message : String(err);
4646
+ return {
4647
+ content: [
4648
+ {
4649
+ type: "text",
4650
+ text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
4651
+ }
4652
+ ],
4653
+ isError: true
4654
+ };
4655
+ }
4656
+ }
4657
+ if (devOnly) {
4658
+ try {
4659
+ const mockAuthDir = (0, import_path7.join)(cwd, "lib");
4660
+ (0, import_fs7.mkdirSync)(mockAuthDir, { recursive: true });
4661
+ const mockAuthContent = generateMockAuthContent(roles, mockUsers);
4662
+ (0, import_fs7.writeFileSync)((0, import_path7.join)(mockAuthDir, "mock-auth.ts"), mockAuthContent, "utf8");
4663
+ filesWritten.push("lib/mock-auth.ts");
4664
+ } catch (err) {
4665
+ const msg = err instanceof Error ? err.message : String(err);
4666
+ return {
4667
+ content: [
4668
+ {
4669
+ type: "text",
4670
+ text: JSON.stringify({
4671
+ success: false,
4672
+ error: `Failed writing lib/mock-auth.ts: ${msg}`
4673
+ })
4674
+ }
4675
+ ],
4676
+ isError: true
4677
+ };
4678
+ }
4679
+ try {
4680
+ const pkgPath = (0, import_path7.join)(cwd, "package.json");
4681
+ if ((0, import_fs7.existsSync)(pkgPath)) {
4682
+ const existingPkg = (0, import_fs7.readFileSync)(pkgPath, "utf8");
4683
+ const updatedPkg = updatePackageJsonScripts(existingPkg, roles, mockUsers);
4684
+ (0, import_fs7.writeFileSync)(pkgPath, updatedPkg, "utf8");
4685
+ filesWritten.push("package.json");
4686
+ }
4687
+ } catch (err) {
4688
+ const msg = err instanceof Error ? err.message : String(err);
4689
+ return {
4690
+ content: [
4691
+ {
4692
+ type: "text",
4693
+ text: JSON.stringify({
4694
+ success: false,
4695
+ error: `Failed updating package.json: ${msg}`
4696
+ })
4697
+ }
4698
+ ],
4699
+ isError: true
4700
+ };
2720
4701
  }
2721
- filesWritten.push(".env.example");
2722
- } catch (err) {
2723
- const msg = err instanceof Error ? err.message : String(err);
2724
- return {
2725
- content: [
2726
- {
2727
- type: "text",
2728
- text: JSON.stringify({ success: false, error: `Failed writing .env.example: ${msg}` })
2729
- }
2730
- ],
2731
- isError: true
2732
- };
2733
4702
  }
2734
4703
  try {
2735
- const authYaml = generateYamlBlock(
4704
+ const authYaml = devOnly ? generateDevOnlyYamlBlock(
2736
4705
  method,
2737
4706
  params,
2738
4707
  roles,
@@ -2740,14 +4709,25 @@ async function configureAuthHandler(params, cwd) {
2740
4709
  hierarchy,
2741
4710
  auditEnabled,
2742
4711
  auditRetentionDays,
2743
- protectedRoutes
4712
+ protectedRoutes,
4713
+ useProxy
4714
+ ) : generateYamlBlock(
4715
+ method,
4716
+ params,
4717
+ roles,
4718
+ defaultRole,
4719
+ hierarchy,
4720
+ auditEnabled,
4721
+ auditRetentionDays,
4722
+ protectedRoutes,
4723
+ useProxy
2744
4724
  );
2745
- const ymlPath = (0, import_path6.join)(cwd, "stackwright.yml");
2746
- if (!(0, import_fs6.existsSync)(ymlPath)) {
2747
- (0, import_fs6.writeFileSync)(ymlPath, authYaml, "utf8");
4725
+ const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
4726
+ if (!(0, import_fs7.existsSync)(ymlPath)) {
4727
+ (0, import_fs7.writeFileSync)(ymlPath, authYaml, "utf8");
2748
4728
  } else {
2749
- const existing = (0, import_fs6.readFileSync)(ymlPath, "utf8");
2750
- (0, import_fs6.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
4729
+ const existing = (0, import_fs7.readFileSync)(ymlPath, "utf8");
4730
+ (0, import_fs7.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
2751
4731
  }
2752
4732
  filesWritten.push("stackwright.yml");
2753
4733
  } catch (err) {
@@ -2775,7 +4755,23 @@ async function configureAuthHandler(params, cwd) {
2775
4755
  rbacDefaultRole: defaultRole,
2776
4756
  protectedRoutesCount: protectedRoutes.length,
2777
4757
  filesWritten,
2778
- securityWarning
4758
+ convention: useProxy ? "proxy" : "middleware",
4759
+ nextMajorVersion: params.nextMajorVersion ?? null,
4760
+ securityWarning,
4761
+ ...devOnly ? {
4762
+ devScripts: {
4763
+ written: filesWritten.includes("package.json"),
4764
+ scripts: filesWritten.includes("package.json") ? Object.fromEntries(
4765
+ roles.map((r) => {
4766
+ const k = deriveDevKey(r);
4767
+ return [`dev:${k}`, `MOCK_USER=${k} next dev`];
4768
+ })
4769
+ ) : {},
4770
+ ...filesWritten.includes("package.json") ? {} : {
4771
+ warning: "package.json not found \u2014 dev scripts were not written. Run the auth phase again after scaffolding creates package.json."
4772
+ }
4773
+ }
4774
+ } : {}
2779
4775
  })
2780
4776
  }
2781
4777
  ]
@@ -2786,35 +4782,38 @@ function registerAuthTools(server2) {
2786
4782
  "stackwright_pro_configure_auth",
2787
4783
  "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.",
2788
4784
  {
2789
- method: import_zod11.z.enum(["cac", "oidc", "oauth2", "none"]),
2790
- provider: import_zod11.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
4785
+ method: import_zod13.z.enum(["cac", "oidc", "oauth2", "none"]),
4786
+ provider: import_zod13.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
2791
4787
  // CAC
2792
- cacCaBundle: import_zod11.z.string().optional(),
2793
- cacEdipiLookup: import_zod11.z.string().optional(),
2794
- cacOcspEndpoint: import_zod11.z.string().optional(),
2795
- cacCertHeader: import_zod11.z.string().optional(),
4788
+ cacCaBundle: import_zod13.z.string().optional(),
4789
+ cacEdipiLookup: import_zod13.z.string().optional(),
4790
+ cacOcspEndpoint: import_zod13.z.string().optional(),
4791
+ cacCertHeader: import_zod13.z.string().optional(),
2796
4792
  // OIDC
2797
- oidcDiscoveryUrl: import_zod11.z.string().optional(),
2798
- oidcClientId: import_zod11.z.string().optional(),
2799
- oidcClientSecret: import_zod11.z.string().optional(),
2800
- oidcScopes: import_zod11.z.string().optional(),
2801
- oidcRoleClaim: import_zod11.z.string().optional(),
4793
+ oidcDiscoveryUrl: import_zod13.z.string().optional(),
4794
+ oidcClientId: import_zod13.z.string().optional(),
4795
+ oidcClientSecret: import_zod13.z.string().optional(),
4796
+ oidcScopes: import_zod13.z.string().optional(),
4797
+ oidcRoleClaim: import_zod13.z.string().optional(),
2802
4798
  // OAuth2
2803
- oauth2AuthUrl: import_zod11.z.string().optional(),
2804
- oauth2TokenUrl: import_zod11.z.string().optional(),
2805
- oauth2ClientId: import_zod11.z.string().optional(),
2806
- oauth2ClientSecret: import_zod11.z.string().optional(),
2807
- oauth2Scopes: import_zod11.z.string().optional(),
4799
+ oauth2AuthUrl: import_zod13.z.string().optional(),
4800
+ oauth2TokenUrl: import_zod13.z.string().optional(),
4801
+ oauth2ClientId: import_zod13.z.string().optional(),
4802
+ oauth2ClientSecret: import_zod13.z.string().optional(),
4803
+ oauth2Scopes: import_zod13.z.string().optional(),
2808
4804
  // RBAC
2809
- rbacRoles: import_zod11.z.array(import_zod11.z.string()).optional(),
2810
- rbacDefaultRole: import_zod11.z.string().optional(),
4805
+ rbacRoles: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
4806
+ rbacDefaultRole: import_zod13.z.string().optional(),
2811
4807
  // Audit
2812
- auditEnabled: import_zod11.z.boolean().optional(),
2813
- auditRetentionDays: import_zod11.z.number().int().positive().optional(),
4808
+ auditEnabled: boolCoerce(import_zod13.z.boolean().optional()),
4809
+ auditRetentionDays: numCoerce(import_zod13.z.number().int().positive().optional()),
2814
4810
  // Routes
2815
- protectedRoutes: import_zod11.z.array(import_zod11.z.string()).optional(),
4811
+ protectedRoutes: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
2816
4812
  // Injection for tests
2817
- _cwd: import_zod11.z.string().optional()
4813
+ _cwd: import_zod13.z.string().optional(),
4814
+ devOnly: boolCoerce(import_zod13.z.boolean().optional()),
4815
+ mockUsers: jsonCoerce(import_zod13.z.array(import_zod13.z.object({ name: import_zod13.z.string(), email: import_zod13.z.string() })).optional()),
4816
+ nextMajorVersion: numCoerce(import_zod13.z.number().int().positive().optional())
2818
4817
  },
2819
4818
  async (params) => {
2820
4819
  const cwd = params._cwd ?? process.cwd();
@@ -2824,45 +4823,65 @@ function registerAuthTools(server2) {
2824
4823
  }
2825
4824
 
2826
4825
  // src/integrity.ts
2827
- var import_crypto2 = require("crypto");
2828
- var import_fs7 = require("fs");
2829
- var import_path7 = require("path");
4826
+ var import_crypto4 = require("crypto");
4827
+ var import_fs8 = require("fs");
4828
+ var import_path8 = require("path");
2830
4829
  var _checksums = /* @__PURE__ */ new Map([
2831
4830
  [
2832
4831
  "stackwright-pro-api-otter.json",
2833
- "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
4832
+ "1b9fea59a3e5b1e1015229a4e8d1231b6b87e1d12f23a81ecbbd79fe8657a2e0"
2834
4833
  ],
2835
4834
  [
2836
4835
  "stackwright-pro-auth-otter.json",
2837
- "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
4836
+ "f91f82468da9c273fbe6481a3a40957d4c7aa3fa72528c492b6ac5d8e06a563a"
2838
4837
  ],
2839
4838
  [
2840
4839
  "stackwright-pro-dashboard-otter.json",
2841
- "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
4840
+ "9c319d311801730e8dc9bc142eebb8fc5a7f48da48fa0b8d8c3b7431652447be"
2842
4841
  ],
2843
4842
  [
2844
4843
  "stackwright-pro-data-otter.json",
2845
- "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
4844
+ "f39061981caf0bd14e4eb83e74ac200450425536d14b8c00d60be94759a1ca9d"
2846
4845
  ],
2847
4846
  [
2848
4847
  "stackwright-pro-designer-otter.json",
2849
- "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
4848
+ "af09ac8f06385bdbac63e2820daa2ff7d38b8ff1ff383c161f07e3fb9d9359c5"
4849
+ ],
4850
+ [
4851
+ "stackwright-pro-domain-expert-otter.json",
4852
+ "41e3a5838f05f6a51ed272860dd2d5b1df1f20bfc847eb8f39be109b89738e99"
2850
4853
  ],
2851
4854
  [
2852
4855
  "stackwright-pro-foreman-otter.json",
2853
- "84e9a5d427f0fcdbe92b53ebd956806c88c094bca4cdf11aaa0db023f0a00c08"
4856
+ "4948b8189223e4ced81b632cca7d14b79fde85f204483ab8e847182692c4ee57"
4857
+ ],
4858
+ [
4859
+ "stackwright-pro-geo-otter.json",
4860
+ "ff1191af8108cc70c09afe6a99b008dcb46f71e20f884da1ee424e431868a1b6"
2854
4861
  ],
2855
4862
  [
2856
4863
  "stackwright-pro-page-otter.json",
2857
- "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
4864
+ "cdd878857c1d809c60263693ab0c6cbb45087fd9c3eeda5b4628bd7026d2bded"
4865
+ ],
4866
+ [
4867
+ "stackwright-pro-polish-otter.json",
4868
+ "e5f88d054dd536646f48e5d47cbd64f825f493100002309b90c912fa69ef279e"
4869
+ ],
4870
+ [
4871
+ "stackwright-pro-scaffold-otter.json",
4872
+ "c9950a34cc6a729a2ad22a4052afc3d7f9b033fe390d9edba4014b76e1d076b6"
2858
4873
  ],
2859
4874
  [
2860
4875
  "stackwright-pro-theme-otter.json",
2861
- "64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732"
4876
+ "532d9ca7db6911a09ce5029a8c74c89360bdf6cb8ede8c2636866b509cbe06f9"
2862
4877
  ],
2863
4878
  [
2864
4879
  "stackwright-pro-workflow-otter.json",
2865
- "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
4880
+ "80eb6c8d3f079e00533dcb4a3c7fd6baae4d5f4192ab6c27c85d2de5359592fd"
4881
+ ],
4882
+ [
4883
+ "stackwright-services-otter.json",
4884
+ "b8a252d6a2c5090138899925d3ea95bf3339b6e49adc276a1f5f7304b4ae5134"
2866
4885
  ]
2867
4886
  ]);
2868
4887
  Object.freeze(_checksums);
@@ -2877,21 +4896,21 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
2877
4896
  }
2878
4897
  var MAX_OTTER_BYTES = 1 * 1024 * 1024;
2879
4898
  function computeSha256(data) {
2880
- return (0, import_crypto2.createHash)("sha256").update(data).digest("hex");
4899
+ return (0, import_crypto4.createHash)("sha256").update(data).digest("hex");
2881
4900
  }
2882
4901
  function safeEqual(a, b) {
2883
4902
  if (a.length !== b.length) return false;
2884
- return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
4903
+ return (0, import_crypto4.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
2885
4904
  }
2886
4905
  function verifyOtterFile(filePath) {
2887
- const filename = (0, import_path7.basename)(filePath);
4906
+ const filename = (0, import_path8.basename)(filePath);
2888
4907
  const expected = CANONICAL_CHECKSUMS.get(filename);
2889
4908
  if (expected === void 0) {
2890
4909
  return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
2891
4910
  }
2892
4911
  let stat;
2893
4912
  try {
2894
- stat = (0, import_fs7.lstatSync)(filePath);
4913
+ stat = (0, import_fs8.lstatSync)(filePath);
2895
4914
  } catch (err) {
2896
4915
  const msg = err instanceof Error ? err.message : String(err);
2897
4916
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -2909,7 +4928,7 @@ function verifyOtterFile(filePath) {
2909
4928
  }
2910
4929
  let raw;
2911
4930
  try {
2912
- raw = (0, import_fs7.readFileSync)(filePath);
4931
+ raw = (0, import_fs8.readFileSync)(filePath);
2913
4932
  } catch (err) {
2914
4933
  const msg = err instanceof Error ? err.message : String(err);
2915
4934
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -2942,12 +4961,24 @@ function verifyOtterFile(filePath) {
2942
4961
  return { verified: true, filename };
2943
4962
  }
2944
4963
  function verifyAllOtters(otterDir) {
4964
+ if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
4965
+ return {
4966
+ verified: [],
4967
+ failed: [
4968
+ {
4969
+ filename: "<directory>",
4970
+ error: `Security: path traversal sequence detected in otter directory parameter`
4971
+ }
4972
+ ],
4973
+ unknown: []
4974
+ };
4975
+ }
2945
4976
  const verified = [];
2946
4977
  const failed = [];
2947
4978
  const unknown = [];
2948
4979
  let entries;
2949
4980
  try {
2950
- entries = (0, import_fs7.readdirSync)(otterDir);
4981
+ entries = (0, import_fs8.readdirSync)(otterDir);
2951
4982
  } catch (err) {
2952
4983
  const msg = err instanceof Error ? err.message : String(err);
2953
4984
  return {
@@ -2958,9 +4989,9 @@ function verifyAllOtters(otterDir) {
2958
4989
  }
2959
4990
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
2960
4991
  for (const filename of otterFiles) {
2961
- const filePath = (0, import_path7.join)(otterDir, filename);
4992
+ const filePath = (0, import_path8.join)(otterDir, filename);
2962
4993
  try {
2963
- if ((0, import_fs7.lstatSync)(filePath).isSymbolicLink()) {
4994
+ if ((0, import_fs8.lstatSync)(filePath).isSymbolicLink()) {
2964
4995
  failed.push({ filename, error: "Skipped: symlink" });
2965
4996
  continue;
2966
4997
  }
@@ -2986,15 +5017,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
2986
5017
  function resolveOtterDir() {
2987
5018
  const cwd = process.cwd();
2988
5019
  for (const relative of DEFAULT_SEARCH_PATHS) {
2989
- const candidate = (0, import_path7.join)(cwd, relative);
5020
+ const candidate = (0, import_path8.join)(cwd, relative);
2990
5021
  try {
2991
- (0, import_fs7.lstatSync)(candidate);
5022
+ (0, import_fs8.lstatSync)(candidate);
2992
5023
  return candidate;
2993
5024
  } catch {
2994
5025
  }
2995
5026
  }
2996
5027
  return null;
2997
5028
  }
5029
+ function emitIntegrityAuditEvent(params) {
5030
+ const record = JSON.stringify({
5031
+ level: "AUDIT",
5032
+ event: "INTEGRITY_FAIL",
5033
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
5034
+ source: "stackwright_pro_verify_otter_integrity",
5035
+ otterDir: params.otterDir,
5036
+ failedCount: params.failed.length,
5037
+ unknownCount: params.unknown.length,
5038
+ failures: params.failed,
5039
+ unknown: params.unknown
5040
+ });
5041
+ process.stderr.write(`INTEGRITY_FAIL ${record}
5042
+ `);
5043
+ }
2998
5044
  function registerIntegrityTools(server2) {
2999
5045
  server2.tool(
3000
5046
  "stackwright_pro_verify_otter_integrity",
@@ -3018,6 +5064,13 @@ function registerIntegrityTools(server2) {
3018
5064
  }
3019
5065
  const result = verifyAllOtters(resolved);
3020
5066
  const allGood = result.failed.length === 0 && result.unknown.length === 0;
5067
+ if (!allGood) {
5068
+ emitIntegrityAuditEvent({
5069
+ otterDir: resolved,
5070
+ failed: result.failed,
5071
+ unknown: result.unknown
5072
+ });
5073
+ }
3021
5074
  return {
3022
5075
  content: [
3023
5076
  {
@@ -3030,7 +5083,10 @@ function registerIntegrityTools(server2) {
3030
5083
  unknownCount: result.unknown.length,
3031
5084
  verified: result.verified,
3032
5085
  failed: result.failed,
3033
- unknown: result.unknown
5086
+ unknown: result.unknown,
5087
+ ...allGood ? {} : {
5088
+ error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
5089
+ }
3034
5090
  })
3035
5091
  }
3036
5092
  ],
@@ -3041,14 +5097,14 @@ function registerIntegrityTools(server2) {
3041
5097
  }
3042
5098
 
3043
5099
  // src/tools/domain.ts
3044
- var import_zod12 = require("zod");
3045
- var import_fs8 = require("fs");
3046
- var import_path8 = require("path");
5100
+ var import_zod14 = require("zod");
5101
+ var import_fs9 = require("fs");
5102
+ var import_path9 = require("path");
3047
5103
  function handleListCollections(input) {
3048
5104
  const cwd = input._cwd ?? process.cwd();
3049
5105
  const sources = [
3050
5106
  {
3051
- path: (0, import_path8.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
5107
+ path: (0, import_path9.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
3052
5108
  source: "data-config.json",
3053
5109
  parse: (raw) => {
3054
5110
  const parsed = JSON.parse(raw);
@@ -3059,15 +5115,15 @@ function handleListCollections(input) {
3059
5115
  }
3060
5116
  },
3061
5117
  {
3062
- path: (0, import_path8.join)(cwd, "stackwright.yml"),
5118
+ path: (0, import_path9.join)(cwd, "stackwright.yml"),
3063
5119
  source: "stackwright.yml",
3064
5120
  parse: extractCollectionsFromYaml
3065
5121
  }
3066
5122
  ];
3067
5123
  for (const { path: path3, source, parse } of sources) {
3068
- if (!(0, import_fs8.existsSync)(path3)) continue;
5124
+ if (!(0, import_fs9.existsSync)(path3)) continue;
3069
5125
  try {
3070
- const collections = parse((0, import_fs8.readFileSync)(path3, "utf8"));
5126
+ const collections = parse((0, import_fs9.readFileSync)(path3, "utf8"));
3071
5127
  return {
3072
5128
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
3073
5129
  isError: false
@@ -3218,8 +5274,8 @@ function handleValidateWorkflow(input) {
3218
5274
  if (input.workflow && Object.keys(input.workflow).length > 0) {
3219
5275
  raw = input.workflow;
3220
5276
  } else {
3221
- const artifactPath = (0, import_path8.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
3222
- if (!(0, import_fs8.existsSync)(artifactPath)) {
5277
+ const artifactPath = (0, import_path9.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
5278
+ if (!(0, import_fs9.existsSync)(artifactPath)) {
3223
5279
  return fail([
3224
5280
  {
3225
5281
  code: "NO_WORKFLOW",
@@ -3228,7 +5284,7 @@ function handleValidateWorkflow(input) {
3228
5284
  ]);
3229
5285
  }
3230
5286
  try {
3231
- raw = JSON.parse((0, import_fs8.readFileSync)(artifactPath, "utf8"));
5287
+ raw = JSON.parse((0, import_fs9.readFileSync)(artifactPath, "utf8"));
3232
5288
  } catch (err) {
3233
5289
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
3234
5290
  }
@@ -3427,7 +5483,7 @@ function registerDomainTools(server2) {
3427
5483
  "stackwright_pro_resolve_data_strategy",
3428
5484
  "Look up the data freshness strategy configuration from the user's answer. Returns mechanism, revalidation seconds, required packages, and the exact MCP tool call to make. Replaces the strategy table in the data-otter prompt.",
3429
5485
  {
3430
- strategy: import_zod12.z.string().describe(
5486
+ strategy: import_zod14.z.string().describe(
3431
5487
  'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
3432
5488
  )
3433
5489
  },
@@ -3437,7 +5493,7 @@ function registerDomainTools(server2) {
3437
5493
  "stackwright_pro_validate_workflow",
3438
5494
  "Validate a workflow definition against the Stackwright workflow schema. Checks step ID uniqueness, transition targets, terminal state existence, and service references. Call this after the workflow otter produces output.",
3439
5495
  {
3440
- workflow: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
5496
+ workflow: jsonCoerce(import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional()).describe(
3441
5497
  "Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
3442
5498
  )
3443
5499
  },
@@ -3445,18 +5501,524 @@ function registerDomainTools(server2) {
3445
5501
  );
3446
5502
  }
3447
5503
 
5504
+ // src/tools/type-schemas.ts
5505
+ var import_zod15 = require("zod");
5506
+ function buildTypeSchemaSummary() {
5507
+ return {
5508
+ version: "1.0",
5509
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5510
+ domains: {
5511
+ workflow: {
5512
+ description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
5513
+ schemas: [
5514
+ "WorkflowFileSchema",
5515
+ "WorkflowDefinitionSchema",
5516
+ "WorkflowStepSchema",
5517
+ "WorkflowStepTypeSchema",
5518
+ "WorkflowFieldSchema",
5519
+ "WorkflowActionSchema",
5520
+ "WorkflowAuthSchema",
5521
+ "WorkflowThemeSchema",
5522
+ "TransitionConditionSchema",
5523
+ "PersistenceSchema"
5524
+ ],
5525
+ otter: "stackwright-pro-workflow-otter",
5526
+ artifactKey: "workflowConfig"
5527
+ },
5528
+ auth: {
5529
+ description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
5530
+ schemas: [
5531
+ "authConfigSchema",
5532
+ "pkiConfigSchema",
5533
+ "oidcConfigSchema",
5534
+ "rbacConfigSchema",
5535
+ "componentAuthSchema",
5536
+ "authUserSchema",
5537
+ "authSessionSchema"
5538
+ ],
5539
+ otter: "stackwright-pro-auth-otter",
5540
+ artifactKey: "authConfig"
5541
+ },
5542
+ openapi: {
5543
+ description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
5544
+ interfaces: [
5545
+ "OpenAPIConfig",
5546
+ "ActionConfig",
5547
+ "EndpointFilter",
5548
+ "ApprovedSpec",
5549
+ "PrebuildSecurityConfig",
5550
+ "SiteConfig",
5551
+ "ValidationResult"
5552
+ ],
5553
+ otter: "stackwright-pro-api-otter",
5554
+ artifactKey: "apiConfig"
5555
+ },
5556
+ pulse: {
5557
+ description: "Real-time data polling \u2014 source-agnostic polling options and states",
5558
+ interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
5559
+ note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
5560
+ },
5561
+ enterprise: {
5562
+ description: "Enterprise collection access \u2014 multi-tenant provider extension",
5563
+ interfaces: [
5564
+ "EnterpriseCollectionProvider",
5565
+ "CollectionProvider",
5566
+ "CollectionEntry",
5567
+ "CollectionListOptions",
5568
+ "CollectionListResult",
5569
+ "TenantFilter",
5570
+ "Collection"
5571
+ ],
5572
+ note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
5573
+ }
5574
+ }
5575
+ };
5576
+ }
5577
+ function registerTypeSchemasTool(server2) {
5578
+ server2.tool(
5579
+ "stackwright_pro_get_type_schemas",
5580
+ "Returns a structured summary of all canonical @stackwright-pro/types schemas, organized by domain. Use this to determine which otter owns a given schema and what artifact key to expect.",
5581
+ {
5582
+ format: import_zod15.z.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
5583
+ },
5584
+ async ({ format }) => {
5585
+ const summary = buildTypeSchemaSummary();
5586
+ const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
5587
+ return {
5588
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
5589
+ };
5590
+ }
5591
+ );
5592
+ }
5593
+
5594
+ // src/tools/scaffold-cleanup.ts
5595
+ var import_fs10 = require("fs");
5596
+ var import_path10 = require("path");
5597
+ var SCAFFOLD_FILES_TO_DELETE = [
5598
+ "content/posts/getting-started.yaml",
5599
+ "content/posts/hello-world.yaml"
5600
+ ];
5601
+ var SCAFFOLD_DIRS_TO_PRUNE = [
5602
+ "content/posts"
5603
+ // Don't prune 'content' — user may have other content directories
5604
+ ];
5605
+ var NOT_FOUND_PATH = "app/not-found.tsx";
5606
+ var FALLBACK_COLORS = {
5607
+ background: "#ffffff",
5608
+ foreground: "#171717",
5609
+ primary: "#525252",
5610
+ mutedForeground: "#737373"
5611
+ };
5612
+ function readThemeColors(cwd) {
5613
+ const tokenPath = (0, import_path10.join)(cwd, ".stackwright", "artifacts", "theme-tokens.json");
5614
+ if (!(0, import_fs10.existsSync)(tokenPath)) return { ...FALLBACK_COLORS };
5615
+ const stat = (0, import_fs10.lstatSync)(tokenPath);
5616
+ if (stat.isSymbolicLink()) return { ...FALLBACK_COLORS };
5617
+ try {
5618
+ const raw = JSON.parse((0, import_fs10.readFileSync)(tokenPath, "utf-8"));
5619
+ const css = raw?.cssVariables ?? {};
5620
+ const toHsl = (hslValue) => {
5621
+ if (!hslValue || typeof hslValue !== "string") return null;
5622
+ return `hsl(${hslValue})`;
5623
+ };
5624
+ return {
5625
+ background: toHsl(css["--background"]) ?? FALLBACK_COLORS.background,
5626
+ foreground: toHsl(css["--foreground"]) ?? FALLBACK_COLORS.foreground,
5627
+ primary: toHsl(css["--primary"]) ?? FALLBACK_COLORS.primary,
5628
+ mutedForeground: toHsl(css["--muted-foreground"]) ?? FALLBACK_COLORS.mutedForeground
5629
+ };
5630
+ } catch {
5631
+ return { ...FALLBACK_COLORS };
5632
+ }
5633
+ }
5634
+ function generateNotFoundPage(colors) {
5635
+ return `export default function NotFound() {
5636
+ return (
5637
+ <div
5638
+ style={{
5639
+ display: 'flex',
5640
+ flexDirection: 'column',
5641
+ alignItems: 'center',
5642
+ justifyContent: 'center',
5643
+ minHeight: '100vh',
5644
+ backgroundColor: '${colors.background}',
5645
+ color: '${colors.foreground}',
5646
+ fontFamily: 'system-ui, -apple-system, sans-serif',
5647
+ padding: '2rem',
5648
+ textAlign: 'center',
5649
+ }}
5650
+ >
5651
+ <h1 style={{ fontSize: '4rem', fontWeight: 700, margin: 0, color: '${colors.primary}' }}>
5652
+ 404
5653
+ </h1>
5654
+ <p style={{ fontSize: '1.25rem', color: '${colors.mutedForeground}', marginTop: '0.5rem' }}>
5655
+ Page not found
5656
+ </p>
5657
+ <a
5658
+ href="/"
5659
+ style={{
5660
+ marginTop: '2rem',
5661
+ padding: '0.75rem 1.5rem',
5662
+ backgroundColor: '${colors.primary}',
5663
+ color: '${colors.background}',
5664
+ borderRadius: '0.5rem',
5665
+ textDecoration: 'none',
5666
+ fontSize: '0.875rem',
5667
+ fontWeight: 500,
5668
+ }}
5669
+ >
5670
+ Go Home
5671
+ </a>
5672
+ </div>
5673
+ );
5674
+ }
5675
+ `;
5676
+ }
5677
+ function isSafePath(fullPath) {
5678
+ if (!(0, import_fs10.existsSync)(fullPath)) return true;
5679
+ const stat = (0, import_fs10.lstatSync)(fullPath);
5680
+ return !stat.isSymbolicLink();
5681
+ }
5682
+ function isDirEmpty(dirPath) {
5683
+ if (!(0, import_fs10.existsSync)(dirPath)) return false;
5684
+ const stat = (0, import_fs10.lstatSync)(dirPath);
5685
+ if (!stat.isDirectory()) return false;
5686
+ return (0, import_fs10.readdirSync)(dirPath).length === 0;
5687
+ }
5688
+ function handleCleanupScaffold(_cwd) {
5689
+ const cwd = _cwd ?? process.cwd();
5690
+ const result = {
5691
+ deleted: [],
5692
+ rewritten: [],
5693
+ skipped: [],
5694
+ errors: [],
5695
+ prunedDirs: []
5696
+ };
5697
+ for (const relPath of SCAFFOLD_FILES_TO_DELETE) {
5698
+ const fullPath = (0, import_path10.join)(cwd, relPath);
5699
+ if (!(0, import_fs10.existsSync)(fullPath)) {
5700
+ result.skipped.push(relPath);
5701
+ continue;
5702
+ }
5703
+ if (!isSafePath(fullPath)) {
5704
+ result.errors.push(`Refusing to delete symlink: ${relPath}`);
5705
+ continue;
5706
+ }
5707
+ try {
5708
+ (0, import_fs10.unlinkSync)(fullPath);
5709
+ result.deleted.push(relPath);
5710
+ } catch (err) {
5711
+ result.errors.push(`Failed to delete ${relPath}: ${String(err)}`);
5712
+ }
5713
+ }
5714
+ for (const relDir of SCAFFOLD_DIRS_TO_PRUNE) {
5715
+ const fullDir = (0, import_path10.join)(cwd, relDir);
5716
+ if (!(0, import_fs10.existsSync)(fullDir)) continue;
5717
+ if (!isSafePath(fullDir)) {
5718
+ result.errors.push(`Refusing to prune symlink directory: ${relDir}`);
5719
+ continue;
5720
+ }
5721
+ if (isDirEmpty(fullDir)) {
5722
+ try {
5723
+ (0, import_fs10.rmdirSync)(fullDir);
5724
+ result.prunedDirs.push(relDir);
5725
+ } catch (err) {
5726
+ result.errors.push(`Failed to prune directory ${relDir}: ${String(err)}`);
5727
+ }
5728
+ }
5729
+ }
5730
+ {
5731
+ const fullPath = (0, import_path10.join)(cwd, NOT_FOUND_PATH);
5732
+ if (!(0, import_fs10.existsSync)(fullPath)) {
5733
+ result.skipped.push(NOT_FOUND_PATH);
5734
+ } else if (!isSafePath(fullPath)) {
5735
+ result.errors.push(`Refusing to rewrite symlink: ${NOT_FOUND_PATH}`);
5736
+ } else {
5737
+ try {
5738
+ const colors = readThemeColors(cwd);
5739
+ const content = generateNotFoundPage(colors);
5740
+ const dir = (0, import_path10.dirname)(fullPath);
5741
+ (0, import_fs10.mkdirSync)(dir, { recursive: true });
5742
+ (0, import_fs10.writeFileSync)(fullPath, content, "utf-8");
5743
+ result.rewritten.push(NOT_FOUND_PATH);
5744
+ } catch (err) {
5745
+ result.errors.push(`Failed to rewrite ${NOT_FOUND_PATH}: ${String(err)}`);
5746
+ }
5747
+ }
5748
+ }
5749
+ return {
5750
+ text: JSON.stringify(result),
5751
+ isError: false
5752
+ };
5753
+ }
5754
+ function registerScaffoldCleanupTools(server2) {
5755
+ const DESC = "Deterministic scaffold remnant cleanup \u2014 removes stale OSS scaffold files after raft run.";
5756
+ server2.tool(
5757
+ "stackwright_pro_cleanup_scaffold",
5758
+ `Remove/rewrite stale scaffold files (blog posts, hardcoded not-found page) after the raft pipeline completes. ${DESC}`,
5759
+ {},
5760
+ async () => {
5761
+ const r = handleCleanupScaffold();
5762
+ return {
5763
+ content: [{ type: "text", text: r.text }],
5764
+ isError: r.isError
5765
+ };
5766
+ }
5767
+ );
5768
+ }
5769
+
5770
+ // src/tools/contrast.ts
5771
+ var import_zod16 = require("zod");
5772
+ function linearizeChannel(c255) {
5773
+ const c = c255 / 255;
5774
+ return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
5775
+ }
5776
+ function relativeLuminance(r, g, b) {
5777
+ return 0.2126 * linearizeChannel(r) + 0.7152 * linearizeChannel(g) + 0.0722 * linearizeChannel(b);
5778
+ }
5779
+ function contrastRatioFromLuminance(l1, l2) {
5780
+ const lighter = Math.max(l1, l2);
5781
+ const darker = Math.min(l1, l2);
5782
+ return (lighter + 0.05) / (darker + 0.05);
5783
+ }
5784
+ function parseHex(hex) {
5785
+ const clean = hex.replace("#", "").trim().toLowerCase();
5786
+ if (clean.length === 3) {
5787
+ const r = parseInt(clean.charAt(0) + clean.charAt(0), 16);
5788
+ const g = parseInt(clean.charAt(1) + clean.charAt(1), 16);
5789
+ const b = parseInt(clean.charAt(2) + clean.charAt(2), 16);
5790
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
5791
+ return [r, g, b];
5792
+ }
5793
+ if (clean.length === 6) {
5794
+ const r = parseInt(clean.slice(0, 2), 16);
5795
+ const g = parseInt(clean.slice(2, 4), 16);
5796
+ const b = parseInt(clean.slice(4, 6), 16);
5797
+ if (isNaN(r) || isNaN(g) || isNaN(b)) return null;
5798
+ return [r, g, b];
5799
+ }
5800
+ return null;
5801
+ }
5802
+ function hslToRgb(h, s, l) {
5803
+ const hn = h / 360;
5804
+ const sn = s / 100;
5805
+ const ln = l / 100;
5806
+ const hue2rgb = (p, q, t) => {
5807
+ let tt = t;
5808
+ if (tt < 0) tt += 1;
5809
+ if (tt > 1) tt -= 1;
5810
+ if (tt < 1 / 6) return p + (q - p) * 6 * tt;
5811
+ if (tt < 1 / 2) return q;
5812
+ if (tt < 2 / 3) return p + (q - p) * (2 / 3 - tt) * 6;
5813
+ return p;
5814
+ };
5815
+ let r, g, b;
5816
+ if (sn === 0) {
5817
+ r = g = b = ln;
5818
+ } else {
5819
+ const q = ln < 0.5 ? ln * (1 + sn) : ln + sn - ln * sn;
5820
+ const p = 2 * ln - q;
5821
+ r = hue2rgb(p, q, hn + 1 / 3);
5822
+ g = hue2rgb(p, q, hn);
5823
+ b = hue2rgb(p, q, hn - 1 / 3);
5824
+ }
5825
+ return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
5826
+ }
5827
+ function parseHsl(hsl) {
5828
+ let str = hsl.replace(/^hsl\s*\(\s*/i, "").replace(/\s*\)$/, "").trim();
5829
+ str = str.replace(/%/g, "");
5830
+ const parts = str.split(/[\s,]+/).filter(Boolean);
5831
+ if (parts.length !== 3) return null;
5832
+ const h = parseFloat(parts[0]);
5833
+ const s = parseFloat(parts[1]);
5834
+ const l = parseFloat(parts[2]);
5835
+ if (isNaN(h) || isNaN(s) || isNaN(l)) return null;
5836
+ return hslToRgb(h, s, l);
5837
+ }
5838
+ function parseColor(color) {
5839
+ const trimmed = color.trim();
5840
+ if (trimmed.startsWith("#")) return parseHex(trimmed);
5841
+ if (/^hsl\s*\(/i.test(trimmed)) return parseHsl(trimmed);
5842
+ if (/^\d[\d.]*[\s,]+\d[\d.]*%?[\s,]+\d[\d.]*%?$/.test(trimmed)) return parseHsl(trimmed);
5843
+ return null;
5844
+ }
5845
+ function rgbToHex(r, g, b) {
5846
+ return "#" + [r, g, b].map(
5847
+ (c) => Math.max(0, Math.min(255, Math.round(c))).toString(16).padStart(2, "0")
5848
+ ).join("");
5849
+ }
5850
+ function handleCheckContrast(fg, bg) {
5851
+ const fgRgb = parseColor(fg);
5852
+ const bgRgb = parseColor(bg);
5853
+ if (!fgRgb) throw new Error(`Cannot parse foreground color: "${fg}"`);
5854
+ if (!bgRgb) throw new Error(`Cannot parse background color: "${bg}"`);
5855
+ const fgL = relativeLuminance(...fgRgb);
5856
+ const bgL = relativeLuminance(...bgRgb);
5857
+ const ratio = parseFloat(contrastRatioFromLuminance(fgL, bgL).toFixed(2));
5858
+ return {
5859
+ fgHex: rgbToHex(...fgRgb),
5860
+ bgHex: rgbToHex(...bgRgb),
5861
+ ratio,
5862
+ aa: ratio >= 4.5,
5863
+ aaLarge: ratio >= 3,
5864
+ aaa: ratio >= 7,
5865
+ aaaLarge: ratio >= 4.5
5866
+ };
5867
+ }
5868
+ function handleDeriveAccessiblePalette(seed, targetRatio) {
5869
+ const seedRgb = parseColor(seed);
5870
+ if (!seedRgb) throw new Error(`Cannot parse seed color: "${seed}"`);
5871
+ const seedHex = rgbToHex(...seedRgb);
5872
+ const seedL = relativeLuminance(...seedRgb);
5873
+ const whiteRatio = parseFloat(contrastRatioFromLuminance(1, seedL).toFixed(2));
5874
+ const blackRatio = parseFloat(contrastRatioFromLuminance(0, seedL).toFixed(2));
5875
+ const whiteWorks = whiteRatio >= targetRatio;
5876
+ const blackWorks = blackRatio >= targetRatio;
5877
+ let strategy;
5878
+ let foreground;
5879
+ let ratio;
5880
+ let alternativeForeground;
5881
+ let alternativeRatio;
5882
+ if (blackWorks && whiteWorks) {
5883
+ if (blackRatio >= whiteRatio) {
5884
+ strategy = "black";
5885
+ foreground = "#000000";
5886
+ ratio = blackRatio;
5887
+ alternativeForeground = "#ffffff";
5888
+ alternativeRatio = whiteRatio;
5889
+ } else {
5890
+ strategy = "white";
5891
+ foreground = "#ffffff";
5892
+ ratio = whiteRatio;
5893
+ alternativeForeground = "#000000";
5894
+ alternativeRatio = blackRatio;
5895
+ }
5896
+ } else if (blackWorks) {
5897
+ strategy = "black";
5898
+ foreground = "#000000";
5899
+ ratio = blackRatio;
5900
+ alternativeForeground = "#ffffff";
5901
+ alternativeRatio = whiteRatio;
5902
+ } else if (whiteWorks) {
5903
+ strategy = "white";
5904
+ foreground = "#ffffff";
5905
+ ratio = whiteRatio;
5906
+ alternativeForeground = "#000000";
5907
+ alternativeRatio = blackRatio;
5908
+ } else {
5909
+ strategy = "unachievable";
5910
+ if (blackRatio >= whiteRatio) {
5911
+ foreground = "#000000";
5912
+ ratio = blackRatio;
5913
+ alternativeForeground = "#ffffff";
5914
+ alternativeRatio = whiteRatio;
5915
+ } else {
5916
+ foreground = "#ffffff";
5917
+ ratio = whiteRatio;
5918
+ alternativeForeground = "#000000";
5919
+ alternativeRatio = blackRatio;
5920
+ }
5921
+ }
5922
+ return {
5923
+ seedHex,
5924
+ foreground,
5925
+ ratio,
5926
+ aa: ratio >= 4.5,
5927
+ aaLarge: ratio >= 3,
5928
+ aaa: ratio >= 7,
5929
+ meetsTarget: ratio >= targetRatio,
5930
+ strategy,
5931
+ alternativeForeground,
5932
+ alternativeRatio
5933
+ };
5934
+ }
5935
+ var PASS = "[PASS]";
5936
+ var FAIL = "[FAIL]";
5937
+ var mark = (ok) => ok ? PASS : FAIL;
5938
+ function registerContrastTools(server2) {
5939
+ server2.tool(
5940
+ "stackwright_pro_check_contrast",
5941
+ 'Compute the exact WCAG 2.1 contrast ratio between a foreground and background color. Returns the ratio and AA/AAA pass/fail flags. Use this to verify any foreground/background color pair before writing it to the token set. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
5942
+ {
5943
+ fg: import_zod16.z.string().describe("Foreground (text) color \u2014 hex or HSL"),
5944
+ bg: import_zod16.z.string().describe("Background color \u2014 hex or HSL")
5945
+ },
5946
+ async ({ fg, bg }) => {
5947
+ let result;
5948
+ try {
5949
+ result = handleCheckContrast(fg, bg);
5950
+ } catch (err) {
5951
+ return {
5952
+ content: [{ type: "text", text: `Error: ${err.message}` }]
5953
+ };
5954
+ }
5955
+ const text = [
5956
+ `WCAG 2.1 Contrast Check`,
5957
+ ``,
5958
+ ` Foreground : ${result.fgHex}`,
5959
+ ` Background : ${result.bgHex}`,
5960
+ ` Ratio : ${result.ratio}:1`,
5961
+ ``,
5962
+ ` ${mark(result.aa)} AA \u2014 normal text (\u22654.5:1)`,
5963
+ ` ${mark(result.aaLarge)} AA \u2014 large text (\u22653.0:1)`,
5964
+ ` ${mark(result.aaa)} AAA \u2014 normal text (\u22657.0:1)`,
5965
+ ` ${mark(result.aaaLarge)} AAA \u2014 large text (\u22654.5:1)`
5966
+ ].join("\n");
5967
+ return { content: [{ type: "text", text }] };
5968
+ }
5969
+ );
5970
+ server2.tool(
5971
+ "stackwright_pro_derive_accessible_palette",
5972
+ 'Given a seed color (treated as a background), derive the best accessible foreground (text) color that meets the target contrast ratio. Evaluates #ffffff and #000000 against the seed and picks whichever satisfies the target ratio with the highest contrast. Use this for every {name}-foreground token derivation instead of guessing white vs black. Accepts #RGB, #RRGGBB, hsl(...), or shadcn HSL format ("H S% L%").',
5973
+ {
5974
+ seed: import_zod16.z.string().describe("Background (seed) color \u2014 hex or HSL"),
5975
+ targetRatio: numCoerce(import_zod16.z.number().default(4.5)).describe(
5976
+ "Minimum required contrast ratio (default 4.5 for WCAG AA)"
5977
+ )
5978
+ },
5979
+ async ({ seed, targetRatio }) => {
5980
+ let result;
5981
+ try {
5982
+ result = handleDeriveAccessiblePalette(seed, targetRatio ?? 4.5);
5983
+ } catch (err) {
5984
+ return {
5985
+ content: [{ type: "text", text: `Error: ${err.message}` }]
5986
+ };
5987
+ }
5988
+ const metLine = result.meetsTarget ? `${PASS} Meets target (${targetRatio}:1)` : `${FAIL} Target (${targetRatio}:1) UNACHIEVABLE with white or black \u2014 best available used`;
5989
+ const altNote = result.alternativeRatio >= (targetRatio ?? 4.5) ? `also passes (${result.alternativeRatio}:1)` : `fails at ${result.alternativeRatio}:1`;
5990
+ const text = [
5991
+ `Accessible Palette Derivation`,
5992
+ ``,
5993
+ ` Background (seed) : ${result.seedHex}`,
5994
+ ` Foreground : ${result.foreground} (${result.strategy})`,
5995
+ ` Contrast ratio : ${result.ratio}:1`,
5996
+ ` ${metLine}`,
5997
+ ``,
5998
+ ` ${mark(result.aa)} AA \u2014 normal text (\u22654.5:1)`,
5999
+ ` ${mark(result.aaLarge)} AA \u2014 large text (\u22653.0:1)`,
6000
+ ` ${mark(result.aaa)} AAA \u2014 normal text (\u22657.0:1)`,
6001
+ ``,
6002
+ ` Alternative (${result.alternativeForeground}): ${altNote}`
6003
+ ].join("\n");
6004
+ return { content: [{ type: "text", text }] };
6005
+ }
6006
+ );
6007
+ }
6008
+
3448
6009
  // package.json
3449
6010
  var package_default = {
3450
6011
  dependencies: {
6012
+ "@stackwright-pro/types": "workspace:*",
3451
6013
  "@modelcontextprotocol/sdk": "^1.10.0",
3452
6014
  "@stackwright-pro/cli-data-explorer": "workspace:*",
3453
- zod: "^4.3.6"
6015
+ zod: "^4.4.3"
3454
6016
  },
3455
6017
  devDependencies: {
3456
- "@types/node": "^24.1.0",
3457
- tsup: "^8.5.0",
3458
- typescript: "^5.8.3",
3459
- vitest: "^4.0.18"
6018
+ "@types/node": "catalog:",
6019
+ tsup: "catalog:",
6020
+ typescript: "catalog:",
6021
+ vitest: "catalog:"
3460
6022
  },
3461
6023
  scripts: {
3462
6024
  prepublishOnly: "node scripts/verify-integrity-sync.js",
@@ -3467,10 +6029,13 @@ var package_default = {
3467
6029
  "test:coverage": "vitest run --coverage"
3468
6030
  },
3469
6031
  name: "@stackwright-pro/mcp",
3470
- version: "0.2.0-alpha.8",
6032
+ version: "0.2.0-alpha.80",
3471
6033
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3472
- license: "PROPRIETARY",
6034
+ license: "SEE LICENSE IN LICENSE",
3473
6035
  main: "./dist/server.js",
6036
+ bin: {
6037
+ "stackwright-pro-mcp": "./dist/server.js"
6038
+ },
3474
6039
  module: "./dist/server.mjs",
3475
6040
  types: "./dist/server.d.ts",
3476
6041
  exports: {
@@ -3483,6 +6048,11 @@ var package_default = {
3483
6048
  types: "./dist/integrity.d.ts",
3484
6049
  import: "./dist/integrity.mjs",
3485
6050
  require: "./dist/integrity.js"
6051
+ },
6052
+ "./type-schemas": {
6053
+ types: "./dist/tools/type-schemas.d.ts",
6054
+ import: "./dist/tools/type-schemas.mjs",
6055
+ require: "./dist/tools/type-schemas.js"
3486
6056
  }
3487
6057
  },
3488
6058
  files: [
@@ -3510,9 +6080,22 @@ registerPipelineTools(server);
3510
6080
  registerSafeWriteTools(server);
3511
6081
  registerAuthTools(server);
3512
6082
  registerIntegrityTools(server);
6083
+ registerArtifactSigningTools(server);
3513
6084
  registerDomainTools(server);
6085
+ registerTypeSchemasTool(server);
6086
+ registerScaffoldCleanupTools(server);
6087
+ registerContrastTools(server);
3514
6088
  async function main() {
3515
6089
  const transport = new import_stdio.StdioServerTransport();
6090
+ try {
6091
+ const servicesRegisterPkg = "@stackwright-services/mcp/register";
6092
+ const mod = await import(servicesRegisterPkg);
6093
+ if (typeof mod.registerServicesTools === "function") {
6094
+ mod.registerServicesTools(server);
6095
+ console.error("Stackwright Services tools registered on pro MCP");
6096
+ }
6097
+ } catch {
6098
+ }
3516
6099
  await server.connect(transport);
3517
6100
  console.error("Stackwright Pro MCP server running on stdio");
3518
6101
  }