@stackwright-pro/mcp 0.2.0-alpha.5 → 0.2.0-alpha.50

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 = [];
@@ -645,7 +681,7 @@ ${yaml}
645
681
  }
646
682
 
647
683
  // src/tools/clarification.ts
648
- var import_zod5 = require("zod");
684
+ var import_zod6 = require("zod");
649
685
  var CONTRADICTION_PATTERNS = [
650
686
  {
651
687
  keywords: ["minimal", "clean", "simple"],
@@ -733,12 +769,14 @@ function registerClarificationTools(server2) {
733
769
  "stackwright_pro_clarify",
734
770
  "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
771
  {
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?")
772
+ context: import_zod6.z.string().optional().describe("Context about what the otter is trying to do"),
773
+ question_type: import_zod6.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
774
+ question: import_zod6.z.string().describe("The clarification question to ask the user"),
775
+ choices: jsonCoerce(import_zod6.z.array(import_zod6.z.string()).optional()).describe(
776
+ "Options for closed_choice questions"
777
+ ),
778
+ priority: import_zod6.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
779
+ target_field: import_zod6.z.string().optional().describe("What field/config does this clarify?")
742
780
  },
743
781
  async ({ context, question_type, question, choices, priority, target_field }) => {
744
782
  const result = handleClarify({
@@ -767,8 +805,10 @@ function registerClarificationTools(server2) {
767
805
  "stackwright_pro_detect_conflict",
768
806
  "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
807
  {
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)")
808
+ stated_preference: import_zod6.z.string().describe("What the user said they wanted"),
809
+ selected_values: jsonCoerce(import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string())).describe(
810
+ "What the user actually selected (field \u2192 value)"
811
+ )
772
812
  },
773
813
  async ({ stated_preference, selected_values }) => {
774
814
  const result = handleDetectConflict({ stated_preference, selected_values });
@@ -813,7 +853,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
813
853
  }
814
854
 
815
855
  // src/tools/packages.ts
816
- var import_zod6 = require("zod");
856
+ var import_zod7 = require("zod");
817
857
  var import_fs2 = require("fs");
818
858
  var import_child_process = require("child_process");
819
859
  var import_path2 = __toESM(require("path"));
@@ -834,17 +874,23 @@ function registerPackageTools(server2) {
834
874
  "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
875
  {
836
876
  // 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" }'
877
+ packages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional().default({})).describe(
878
+ 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
879
+ ),
880
+ devPackages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
881
+ "devDependencies to add. Same format as packages."
839
882
  ),
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(
883
+ scripts: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
884
+ "npm scripts to add. Only adds if key does not already exist."
885
+ ),
886
+ targetDir: import_zod7.z.string().optional().describe(
843
887
  "Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
844
888
  ),
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."
889
+ runInstall: boolCoerce(import_zod7.z.boolean().optional().default(true)).describe(
890
+ "Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
891
+ ),
892
+ includeBaseline: boolCoerce(import_zod7.z.boolean().optional().default(false)).describe(
893
+ "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
894
  )
849
895
  },
850
896
  async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
@@ -1002,11 +1048,11 @@ function setupPackages(opts) {
1002
1048
  }
1003
1049
  }
1004
1050
  const raw = (0, import_fs2.readFileSync)(realPackageJsonPath, "utf8");
1005
- const PackageJsonSchema = import_zod6.z.object({
1051
+ const PackageJsonSchema = import_zod7.z.object({
1006
1052
  // 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()
1053
+ dependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
1054
+ devDependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
1055
+ scripts: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()
1010
1056
  }).passthrough();
1011
1057
  const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
1012
1058
  if (!schemaResult.success) {
@@ -1088,8 +1134,9 @@ function setupPackages(opts) {
1088
1134
 
1089
1135
  // src/tools/questions.ts
1090
1136
  var import_promises = require("fs/promises");
1137
+ var import_node_fs = require("fs");
1091
1138
  var import_node_path = require("path");
1092
- var import_zod7 = require("zod");
1139
+ var import_zod8 = require("zod");
1093
1140
 
1094
1141
  // src/question-adapter.ts
1095
1142
  function truncate(str, maxLength) {
@@ -1275,22 +1322,22 @@ function answersToManifestFormat(answers, questions) {
1275
1322
  }
1276
1323
 
1277
1324
  // 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()
1325
+ var ManifestQuestionSchema = import_zod8.z.object({
1326
+ id: import_zod8.z.string(),
1327
+ question: import_zod8.z.string(),
1328
+ type: import_zod8.z.enum(["text", "select", "multi-select", "confirm"]),
1329
+ required: import_zod8.z.boolean().optional(),
1330
+ options: import_zod8.z.array(
1331
+ import_zod8.z.object({
1332
+ label: import_zod8.z.string(),
1333
+ value: import_zod8.z.string()
1287
1334
  })
1288
1335
  ).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())])
1336
+ default: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.boolean(), import_zod8.z.array(import_zod8.z.string())]).optional(),
1337
+ help: import_zod8.z.string().optional(),
1338
+ dependsOn: import_zod8.z.object({
1339
+ questionId: import_zod8.z.string(),
1340
+ value: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string())])
1294
1341
  }).optional()
1295
1342
  });
1296
1343
  function registerQuestionTools(server2) {
@@ -1298,11 +1345,13 @@ function registerQuestionTools(server2) {
1298
1345
  "stackwright_pro_present_phase_questions",
1299
1346
  "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
1347
  {
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(
1348
+ phase: import_zod8.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1349
+ questions: jsonCoerce(import_zod8.z.array(ManifestQuestionSchema).optional()).describe(
1303
1350
  "Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
1304
1351
  ),
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")
1352
+ answers: jsonCoerce(
1353
+ 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()
1354
+ ).describe("Previously collected answers used to resolve dependsOn conditions")
1306
1355
  },
1307
1356
  async ({ phase, questions, answers }) => {
1308
1357
  let resolvedQuestions;
@@ -1353,17 +1402,36 @@ function registerQuestionTools(server2) {
1353
1402
  }
1354
1403
  }
1355
1404
  const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
1405
+ const labelSchema = import_zod8.z.string().max(50, "Value should have at most 50 characters");
1406
+ for (const q of adapted) {
1407
+ for (const opt of q.options) {
1408
+ const check = labelSchema.safeParse(opt.label);
1409
+ if (!check.success) {
1410
+ return {
1411
+ content: [
1412
+ {
1413
+ type: "text",
1414
+ text: JSON.stringify({
1415
+ 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.`
1416
+ })
1417
+ }
1418
+ ],
1419
+ isError: true
1420
+ };
1421
+ }
1422
+ }
1423
+ }
1356
1424
  if (adapted.length === 0) {
1357
1425
  return {
1358
1426
  content: [
1359
1427
  {
1360
1428
  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
- })
1429
+ 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.`
1430
+ },
1431
+ {
1432
+ type: "text",
1433
+ // Empty array — second block always present so the foreman's two-block contract holds
1434
+ text: JSON.stringify([])
1367
1435
  }
1368
1436
  ],
1369
1437
  isError: false
@@ -1384,10 +1452,148 @@ function registerQuestionTools(server2) {
1384
1452
  };
1385
1453
  }
1386
1454
  );
1455
+ server2.tool(
1456
+ "stackwright_pro_get_next_question",
1457
+ "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.",
1458
+ { phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
1459
+ async ({ phase }) => {
1460
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1461
+ if (!SAFE_PHASE.test(phase)) {
1462
+ return {
1463
+ content: [
1464
+ {
1465
+ type: "text",
1466
+ text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
1467
+ }
1468
+ ],
1469
+ isError: true
1470
+ };
1471
+ }
1472
+ const cwd = process.cwd();
1473
+ const questionsPath = (0, import_node_path.join)(cwd, ".stackwright", "questions", `${phase}.json`);
1474
+ let questions = [];
1475
+ try {
1476
+ const raw = await (0, import_promises.readFile)(questionsPath, "utf-8");
1477
+ const parsed = JSON.parse(raw);
1478
+ questions = parsed.questions ?? [];
1479
+ } catch {
1480
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1481
+ }
1482
+ if (questions.length === 0) {
1483
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1484
+ }
1485
+ const answersPath = (0, import_node_path.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1486
+ let answeredIds = /* @__PURE__ */ new Set();
1487
+ try {
1488
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1489
+ const parsed = JSON.parse(raw);
1490
+ answeredIds = new Set(Object.keys(parsed.answers ?? {}));
1491
+ } catch {
1492
+ }
1493
+ const next = questions.find((q) => !answeredIds.has(q.id));
1494
+ if (!next) {
1495
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1496
+ }
1497
+ return {
1498
+ content: [
1499
+ {
1500
+ type: "text",
1501
+ text: JSON.stringify({
1502
+ done: false,
1503
+ questionId: next.id,
1504
+ question: next.question,
1505
+ type: next.type,
1506
+ options: next.options ?? null,
1507
+ help: next.help ?? null,
1508
+ index: questions.indexOf(next) + 1,
1509
+ total: questions.length
1510
+ })
1511
+ }
1512
+ ]
1513
+ };
1514
+ }
1515
+ );
1516
+ server2.tool(
1517
+ "stackwright_pro_record_answer",
1518
+ "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.",
1519
+ {
1520
+ phase: import_zod8.z.string().describe('Phase name e.g. "designer"'),
1521
+ questionId: import_zod8.z.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
1522
+ answer: import_zod8.z.string().describe("The user's free-text answer")
1523
+ },
1524
+ async ({ phase, questionId, answer }) => {
1525
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1526
+ if (!SAFE_PHASE.test(phase)) {
1527
+ return {
1528
+ content: [
1529
+ { type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
1530
+ ],
1531
+ isError: true
1532
+ };
1533
+ }
1534
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
1535
+ return {
1536
+ content: [
1537
+ { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
1538
+ ],
1539
+ isError: true
1540
+ };
1541
+ }
1542
+ const safeAnswer = answer.slice(0, 2e3);
1543
+ const cwd = process.cwd();
1544
+ const answersDir = (0, import_node_path.join)(cwd, ".stackwright", "answers");
1545
+ const answersPath = (0, import_node_path.join)(answersDir, `${phase}.json`);
1546
+ if ((0, import_node_fs.existsSync)(answersDir) && (0, import_node_fs.lstatSync)(answersDir).isSymbolicLink()) {
1547
+ return {
1548
+ content: [
1549
+ {
1550
+ type: "text",
1551
+ text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
1552
+ }
1553
+ ],
1554
+ isError: true
1555
+ };
1556
+ }
1557
+ (0, import_node_fs.mkdirSync)(answersDir, { recursive: true });
1558
+ let existing = {
1559
+ version: "1.0",
1560
+ phase,
1561
+ answers: {}
1562
+ };
1563
+ try {
1564
+ if ((0, import_node_fs.existsSync)(answersPath)) {
1565
+ if ((0, import_node_fs.lstatSync)(answersPath).isSymbolicLink()) {
1566
+ return {
1567
+ content: [
1568
+ {
1569
+ type: "text",
1570
+ text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
1571
+ }
1572
+ ],
1573
+ isError: true
1574
+ };
1575
+ }
1576
+ const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
1577
+ existing = JSON.parse(raw);
1578
+ }
1579
+ } catch {
1580
+ }
1581
+ existing.answers[questionId] = safeAnswer;
1582
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1583
+ const tmp = `${answersPath}.tmp`;
1584
+ await (0, import_promises.writeFile)(tmp, JSON.stringify(existing, null, 2), "utf-8");
1585
+ (0, import_node_fs.renameSync)(tmp, answersPath);
1586
+ return {
1587
+ content: [
1588
+ { type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
1589
+ ]
1590
+ };
1591
+ }
1592
+ );
1387
1593
  }
1388
1594
 
1389
1595
  // src/tools/orchestration.ts
1390
- var import_zod8 = require("zod");
1596
+ var import_zod9 = require("zod");
1391
1597
  var import_fs3 = require("fs");
1392
1598
  var import_path3 = require("path");
1393
1599
  var OTTER_NAME_TO_PHASE = [
@@ -1555,13 +1761,63 @@ function handleGetOtterName(input) {
1555
1761
  isError: false
1556
1762
  };
1557
1763
  }
1764
+ function handleSaveBuildContext(input) {
1765
+ const cwd = input._cwd ?? process.cwd();
1766
+ const dir = (0, import_path3.join)(cwd, ".stackwright");
1767
+ const filePath = (0, import_path3.join)(dir, "build-context.json");
1768
+ try {
1769
+ (0, import_fs3.mkdirSync)(dir, { recursive: true });
1770
+ if ((0, import_fs3.existsSync)(filePath)) {
1771
+ const stat = (0, import_fs3.lstatSync)(filePath);
1772
+ if (stat.isSymbolicLink()) {
1773
+ return {
1774
+ text: JSON.stringify({
1775
+ success: false,
1776
+ error: `Refusing to write to symlink: ${filePath}`
1777
+ }),
1778
+ isError: true
1779
+ };
1780
+ }
1781
+ }
1782
+ const payload = {
1783
+ version: "1.0",
1784
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1785
+ buildContext: input.buildContext
1786
+ };
1787
+ (0, import_fs3.writeFileSync)(filePath, JSON.stringify(payload, null, 2) + "\n");
1788
+ return {
1789
+ text: JSON.stringify({ success: true, path: filePath }),
1790
+ isError: false
1791
+ };
1792
+ } catch (err) {
1793
+ const message = err instanceof Error ? err.message : String(err);
1794
+ return {
1795
+ text: JSON.stringify({ success: false, error: message }),
1796
+ isError: true
1797
+ };
1798
+ }
1799
+ }
1558
1800
  function registerOrchestrationTools(server2) {
1801
+ server2.tool(
1802
+ "stackwright_pro_save_build_context",
1803
+ `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.`,
1804
+ {
1805
+ buildContext: import_zod9.z.string().describe("Free-text description of what the user wants to build")
1806
+ },
1807
+ async ({ buildContext }) => {
1808
+ const { text, isError } = handleSaveBuildContext({ buildContext });
1809
+ return {
1810
+ content: [{ type: "text", text }],
1811
+ isError
1812
+ };
1813
+ }
1814
+ );
1559
1815
  server2.tool(
1560
1816
  "stackwright_pro_parse_otter_response",
1561
1817
  "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
1818
  {
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")
1819
+ otterName: import_zod9.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1820
+ responseText: import_zod9.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1565
1821
  },
1566
1822
  async ({ otterName, responseText }) => {
1567
1823
  const { result, isError } = handleParseOtterResponse({ otterName, responseText });
@@ -1575,16 +1831,18 @@ function registerOrchestrationTools(server2) {
1575
1831
  "stackwright_pro_save_manifest",
1576
1832
  "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
1833
  {
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
- })
1834
+ phases: jsonCoerce(
1835
+ import_zod9.z.array(
1836
+ import_zod9.z.object({
1837
+ phase: import_zod9.z.string(),
1838
+ otter: import_zod9.z.string(),
1839
+ questions: import_zod9.z.array(import_zod9.z.any()),
1840
+ requiredPackages: import_zod9.z.object({
1841
+ dependencies: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional(),
1842
+ devPackages: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional()
1843
+ }).optional()
1844
+ })
1845
+ )
1588
1846
  ).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
1589
1847
  },
1590
1848
  async ({ phases }) => {
@@ -1599,15 +1857,21 @@ function registerOrchestrationTools(server2) {
1599
1857
  "stackwright_pro_save_phase_answers",
1600
1858
  "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
1859
  {
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")
1860
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
1861
+ rawAnswers: jsonCoerce(
1862
+ import_zod9.z.array(
1863
+ import_zod9.z.object({
1864
+ question_header: import_zod9.z.string(),
1865
+ selected_options: import_zod9.z.array(import_zod9.z.string()),
1866
+ other_text: import_zod9.z.string().nullable().optional()
1867
+ })
1868
+ )
1869
+ ).describe(
1870
+ "Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
1871
+ ),
1872
+ questions: jsonCoerce(import_zod9.z.array(import_zod9.z.any()).optional()).describe(
1873
+ "Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
1874
+ )
1611
1875
  },
1612
1876
  async ({ phase, rawAnswers, questions }) => {
1613
1877
  const { text, isError } = handleSavePhaseAnswers({
@@ -1625,7 +1889,7 @@ function registerOrchestrationTools(server2) {
1625
1889
  "stackwright_pro_read_phase_answers",
1626
1890
  "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
1891
  {
1628
- phase: import_zod8.z.string().describe('Phase name, e.g. "designer"')
1892
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer"')
1629
1893
  },
1630
1894
  async ({ phase }) => {
1631
1895
  const { text, isError } = handleReadPhaseAnswers({ phase });
@@ -1639,41 +1903,373 @@ function registerOrchestrationTools(server2) {
1639
1903
  "stackwright_pro_get_otter_name",
1640
1904
  "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
1905
  {
1642
- phase: import_zod8.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
1906
+ phase: import_zod9.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
1643
1907
  },
1644
1908
  async ({ phase }) => {
1645
1909
  const { text, isError } = handleGetOtterName({ phase });
1646
1910
  return {
1647
- content: [{ type: "text", text }],
1648
- isError
1911
+ content: [{ type: "text", text }],
1912
+ isError
1913
+ };
1914
+ }
1915
+ );
1916
+ }
1917
+
1918
+ // src/tools/pipeline.ts
1919
+ var import_zod11 = require("zod");
1920
+ var import_fs5 = require("fs");
1921
+ var import_path5 = require("path");
1922
+ var import_crypto3 = require("crypto");
1923
+
1924
+ // src/artifact-signing.ts
1925
+ var import_crypto2 = require("crypto");
1926
+ var import_fs4 = require("fs");
1927
+ var import_path4 = require("path");
1928
+ var import_zod10 = require("zod");
1929
+ var ALGORITHM = "ECDSA-P384-SHA384";
1930
+ var KEY_FILE = "pipeline-keys.json";
1931
+ var KEY_DIR = ".stackwright";
1932
+ var SIGNATURE_MANIFEST = "signatures.json";
1933
+ var ARTIFACTS_DIR = ".stackwright/artifacts";
1934
+ function rejectSymlink(filePath, context) {
1935
+ if (!(0, import_fs4.existsSync)(filePath)) return;
1936
+ const stat = (0, import_fs4.lstatSync)(filePath);
1937
+ if (stat.isSymbolicLink()) {
1938
+ throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
1939
+ }
1940
+ }
1941
+ function computeSha384(data) {
1942
+ return (0, import_crypto2.createHash)("sha384").update(data).digest("hex");
1943
+ }
1944
+ function safeDigestEqual(a, b) {
1945
+ if (a.length !== b.length) return false;
1946
+ return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
1947
+ }
1948
+ function emptyManifest() {
1949
+ return {
1950
+ version: "1.0",
1951
+ algorithm: ALGORITHM,
1952
+ signatures: {}
1953
+ };
1954
+ }
1955
+ function initPipelineKeys(cwd) {
1956
+ const keyDir = (0, import_path4.join)(cwd, KEY_DIR);
1957
+ const keyPath = (0, import_path4.join)(keyDir, KEY_FILE);
1958
+ rejectSymlink(keyPath, "pipeline-keys.json");
1959
+ (0, import_fs4.mkdirSync)(keyDir, { recursive: true });
1960
+ const { publicKey, privateKey } = (0, import_crypto2.generateKeyPairSync)("ec", {
1961
+ namedCurve: "P-384"
1962
+ });
1963
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
1964
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
1965
+ const fingerprint = (0, import_crypto2.createHash)("sha256").update(publicKeyPem).digest("hex");
1966
+ const keyFile = {
1967
+ version: "1.0",
1968
+ algorithm: ALGORITHM,
1969
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1970
+ publicKeyPem,
1971
+ privateKeyPem
1972
+ };
1973
+ (0, import_fs4.writeFileSync)(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
1974
+ return { publicKeyPem, fingerprint };
1975
+ }
1976
+ function loadPipelineKeys(cwd) {
1977
+ const keyPath = (0, import_path4.join)(cwd, KEY_DIR, KEY_FILE);
1978
+ rejectSymlink(keyPath, "pipeline-keys.json");
1979
+ if (!(0, import_fs4.existsSync)(keyPath)) {
1980
+ throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
1981
+ }
1982
+ let raw;
1983
+ try {
1984
+ raw = (0, import_fs4.readFileSync)(keyPath, "utf-8");
1985
+ } catch (err) {
1986
+ const msg = err instanceof Error ? err.message : String(err);
1987
+ throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
1988
+ }
1989
+ let parsed;
1990
+ try {
1991
+ parsed = JSON.parse(raw);
1992
+ } catch {
1993
+ throw new Error("Pipeline keys file is not valid JSON");
1994
+ }
1995
+ if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
1996
+ throw new Error("Invalid public key PEM in pipeline keys file");
1997
+ }
1998
+ if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
1999
+ throw new Error("Invalid private key PEM in pipeline keys file");
2000
+ }
2001
+ const publicKey = (0, import_crypto2.createPublicKey)(parsed.publicKeyPem);
2002
+ const privateKey = (0, import_crypto2.createPrivateKey)(parsed.privateKeyPem);
2003
+ return { privateKey, publicKey };
2004
+ }
2005
+ function signArtifact(artifactBytes, privateKey) {
2006
+ const digest = computeSha384(artifactBytes);
2007
+ const sig = (0, import_crypto2.sign)("SHA384", artifactBytes, privateKey);
2008
+ return {
2009
+ digest,
2010
+ signature: sig.toString("base64"),
2011
+ algorithm: ALGORITHM,
2012
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
2013
+ };
2014
+ }
2015
+ function verifyArtifact(artifactBytes, signature, publicKey) {
2016
+ const sigValid = (0, import_crypto2.verify)(
2017
+ "SHA384",
2018
+ artifactBytes,
2019
+ publicKey,
2020
+ Buffer.from(signature.signature, "base64")
2021
+ );
2022
+ if (!sigValid) return false;
2023
+ const actualDigest = computeSha384(artifactBytes);
2024
+ return safeDigestEqual(actualDigest, signature.digest);
2025
+ }
2026
+ function loadSignatureManifest(cwd) {
2027
+ const manifestPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
2028
+ rejectSymlink(manifestPath, "signatures.json");
2029
+ if (!(0, import_fs4.existsSync)(manifestPath)) return emptyManifest();
2030
+ let raw;
2031
+ try {
2032
+ raw = (0, import_fs4.readFileSync)(manifestPath, "utf-8");
2033
+ } catch (err) {
2034
+ const msg = err instanceof Error ? err.message : String(err);
2035
+ throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
2036
+ }
2037
+ try {
2038
+ const parsed = JSON.parse(raw);
2039
+ if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
2040
+ throw new Error("Malformed signature manifest: invalid version or missing signatures object");
2041
+ }
2042
+ return parsed;
2043
+ } catch (err) {
2044
+ if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
2045
+ throw new Error("Signature manifest is not valid JSON", { cause: err });
2046
+ }
2047
+ }
2048
+ function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
2049
+ const artifactsDir = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
2050
+ const manifestPath = (0, import_path4.join)(artifactsDir, SIGNATURE_MANIFEST);
2051
+ rejectSymlink(manifestPath, "signatures.json (save)");
2052
+ (0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
2053
+ const manifest = loadSignatureManifest(cwd);
2054
+ manifest.signatures[artifactFilename] = {
2055
+ ...sig,
2056
+ signedBy: signerOtter
2057
+ };
2058
+ (0, import_fs4.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
2059
+ }
2060
+ function getArtifactSignature(cwd, artifactFilename) {
2061
+ const manifest = loadSignatureManifest(cwd);
2062
+ const entry = manifest.signatures[artifactFilename];
2063
+ if (!entry) return null;
2064
+ return {
2065
+ digest: entry.digest,
2066
+ signature: entry.signature,
2067
+ algorithm: entry.algorithm,
2068
+ signedAt: entry.signedAt
2069
+ };
2070
+ }
2071
+ function emitSignatureAuditEvent(params) {
2072
+ const record = JSON.stringify({
2073
+ level: "AUDIT",
2074
+ event: "ARTIFACT_SIGNATURE_FAIL",
2075
+ timestamp: params.timestamp,
2076
+ source: params.source,
2077
+ artifactFilename: params.artifactFilename,
2078
+ expectedDigest: params.expectedDigest,
2079
+ actualDigest: params.actualDigest,
2080
+ phase: params.phase
2081
+ });
2082
+ process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
2083
+ `);
2084
+ }
2085
+ function registerArtifactSigningTools(server2) {
2086
+ server2.tool(
2087
+ "stackwright_pro_verify_artifact_signatures",
2088
+ "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.",
2089
+ {
2090
+ cwd: import_zod10.z.string().optional().describe("Project root directory. Defaults to process.cwd().")
2091
+ },
2092
+ async ({ cwd: cwdParam }) => {
2093
+ const cwd = cwdParam ?? process.cwd();
2094
+ let publicKey;
2095
+ try {
2096
+ const keys = loadPipelineKeys(cwd);
2097
+ publicKey = keys.publicKey;
2098
+ } catch (err) {
2099
+ const msg = err instanceof Error ? err.message : String(err);
2100
+ return {
2101
+ content: [
2102
+ {
2103
+ type: "text",
2104
+ text: JSON.stringify({
2105
+ error: true,
2106
+ message: `Cannot load pipeline keys: ${msg}`
2107
+ })
2108
+ }
2109
+ ],
2110
+ isError: true
2111
+ };
2112
+ }
2113
+ let manifest;
2114
+ try {
2115
+ manifest = loadSignatureManifest(cwd);
2116
+ } catch (err) {
2117
+ const msg = err instanceof Error ? err.message : String(err);
2118
+ return {
2119
+ content: [
2120
+ {
2121
+ type: "text",
2122
+ text: JSON.stringify({
2123
+ error: true,
2124
+ message: `Cannot load signature manifest: ${msg}`
2125
+ })
2126
+ }
2127
+ ],
2128
+ isError: true
2129
+ };
2130
+ }
2131
+ const artifactsPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
2132
+ let artifactFiles = [];
2133
+ try {
2134
+ if ((0, import_fs4.existsSync)(artifactsPath)) {
2135
+ artifactFiles = (0, import_fs4.readdirSync)(artifactsPath).filter(
2136
+ (f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
2137
+ );
2138
+ }
2139
+ } catch {
2140
+ }
2141
+ const results = [];
2142
+ let hasFailure = false;
2143
+ for (const filename of artifactFiles) {
2144
+ const filePath = (0, import_path4.join)(artifactsPath, filename);
2145
+ try {
2146
+ rejectSymlink(filePath, `artifact ${filename}`);
2147
+ } catch {
2148
+ results.push({
2149
+ filename,
2150
+ verified: false,
2151
+ error: "Refusing to verify symlink"
2152
+ });
2153
+ hasFailure = true;
2154
+ continue;
2155
+ }
2156
+ let artifactBytes;
2157
+ try {
2158
+ artifactBytes = (0, import_fs4.readFileSync)(filePath);
2159
+ } catch (err) {
2160
+ const msg = err instanceof Error ? err.message : String(err);
2161
+ results.push({
2162
+ filename,
2163
+ verified: false,
2164
+ error: `Cannot read artifact: ${msg}`
2165
+ });
2166
+ hasFailure = true;
2167
+ continue;
2168
+ }
2169
+ const entry = manifest.signatures[filename];
2170
+ if (!entry) {
2171
+ results.push({
2172
+ filename,
2173
+ verified: false,
2174
+ error: "No signature found in manifest"
2175
+ });
2176
+ hasFailure = true;
2177
+ continue;
2178
+ }
2179
+ const sig = {
2180
+ digest: entry.digest,
2181
+ signature: entry.signature,
2182
+ algorithm: entry.algorithm,
2183
+ signedAt: entry.signedAt
2184
+ };
2185
+ const verified = verifyArtifact(artifactBytes, sig, publicKey);
2186
+ if (!verified) {
2187
+ const actualDigest = computeSha384(artifactBytes);
2188
+ emitSignatureAuditEvent({
2189
+ artifactFilename: filename,
2190
+ expectedDigest: sig.digest,
2191
+ actualDigest,
2192
+ phase: entry.signedBy ?? "unknown",
2193
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2194
+ source: "stackwright_pro_verify_artifact_signatures"
2195
+ });
2196
+ results.push({
2197
+ filename,
2198
+ verified: false,
2199
+ error: `Signature verification failed \u2014 artifact may have been tampered with`,
2200
+ signedBy: entry.signedBy,
2201
+ signedAt: entry.signedAt
2202
+ });
2203
+ hasFailure = true;
2204
+ } else {
2205
+ results.push({
2206
+ filename,
2207
+ verified: true,
2208
+ signedBy: entry.signedBy,
2209
+ signedAt: entry.signedAt
2210
+ });
2211
+ }
2212
+ }
2213
+ for (const manifestFilename of Object.keys(manifest.signatures)) {
2214
+ if (!artifactFiles.includes(manifestFilename)) {
2215
+ results.push({
2216
+ filename: manifestFilename,
2217
+ verified: false,
2218
+ error: "Artifact referenced in manifest but missing from disk"
2219
+ });
2220
+ hasFailure = true;
2221
+ }
2222
+ }
2223
+ const verifiedCount = results.filter((r) => r.verified).length;
2224
+ const failedCount = results.filter((r) => !r.verified).length;
2225
+ return {
2226
+ content: [
2227
+ {
2228
+ type: "text",
2229
+ text: JSON.stringify({
2230
+ totalArtifacts: artifactFiles.length,
2231
+ verifiedCount,
2232
+ failedCount,
2233
+ results,
2234
+ ...hasFailure ? {
2235
+ error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
2236
+ } : {}
2237
+ })
2238
+ }
2239
+ ],
2240
+ isError: hasFailure
1649
2241
  };
1650
2242
  }
1651
2243
  );
1652
2244
  }
1653
2245
 
1654
2246
  // src/tools/pipeline.ts
1655
- var import_zod9 = require("zod");
1656
- var import_fs4 = require("fs");
1657
- var import_path4 = require("path");
2247
+ var import_types = require("@stackwright-pro/types");
1658
2248
  var PHASE_ORDER = [
1659
2249
  "designer",
1660
2250
  "theme",
1661
2251
  "api",
1662
- "auth",
1663
2252
  "data",
2253
+ "workflow",
1664
2254
  "pages",
1665
2255
  "dashboard",
1666
- "workflow"
2256
+ "auth"
1667
2257
  ];
1668
2258
  var PHASE_DEPENDENCIES = {
1669
2259
  designer: [],
1670
2260
  theme: ["designer"],
1671
2261
  api: [],
1672
- auth: [],
1673
2262
  data: ["api"],
1674
- pages: ["designer", "theme", "api", "data", "auth"],
1675
- dashboard: ["designer", "theme", "api", "data"],
1676
- workflow: ["auth"]
2263
+ // workflow: no hard deps uses service: references with Prism mock fallback
2264
+ // when API artifacts aren't available; roles come from user answers
2265
+ workflow: [],
2266
+ // pages: 'api' is transitive through 'data'; auth removed — page-otter has
2267
+ // graceful fallback and reads role names from workflow artifacts instead
2268
+ pages: ["designer", "theme", "data"],
2269
+ dashboard: ["designer", "theme", "data"],
2270
+ // auth is the terminal phase — reads all available artifacts at runtime;
2271
+ // no hard upstream requirements so it always runs and never gets skipped
2272
+ auth: []
1677
2273
  };
1678
2274
  var PHASE_ARTIFACT = {
1679
2275
  designer: "design-language.json",
@@ -1723,11 +2319,11 @@ function createDefaultState() {
1723
2319
  };
1724
2320
  }
1725
2321
  function statePath(cwd) {
1726
- return (0, import_path4.join)(cwd, ".stackwright", "pipeline-state.json");
2322
+ return (0, import_path5.join)(cwd, ".stackwright", "pipeline-state.json");
1727
2323
  }
1728
2324
  function readState(cwd) {
1729
2325
  const p = statePath(cwd);
1730
- if (!(0, import_fs4.existsSync)(p)) return createDefaultState();
2326
+ if (!(0, import_fs5.existsSync)(p)) return createDefaultState();
1731
2327
  const raw = JSON.parse(safeReadSync(p));
1732
2328
  if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1733
2329
  return createDefaultState();
@@ -1735,26 +2331,26 @@ function readState(cwd) {
1735
2331
  return raw;
1736
2332
  }
1737
2333
  function safeWriteSync(filePath, content) {
1738
- if ((0, import_fs4.existsSync)(filePath)) {
1739
- const stat = (0, import_fs4.lstatSync)(filePath);
2334
+ if ((0, import_fs5.existsSync)(filePath)) {
2335
+ const stat = (0, import_fs5.lstatSync)(filePath);
1740
2336
  if (stat.isSymbolicLink()) {
1741
2337
  throw new Error(`Refusing to write to symlink: ${filePath}`);
1742
2338
  }
1743
2339
  }
1744
- (0, import_fs4.writeFileSync)(filePath, content);
2340
+ (0, import_fs5.writeFileSync)(filePath, content);
1745
2341
  }
1746
2342
  function safeReadSync(filePath) {
1747
- if ((0, import_fs4.existsSync)(filePath)) {
1748
- const stat = (0, import_fs4.lstatSync)(filePath);
2343
+ if ((0, import_fs5.existsSync)(filePath)) {
2344
+ const stat = (0, import_fs5.lstatSync)(filePath);
1749
2345
  if (stat.isSymbolicLink()) {
1750
2346
  throw new Error(`Refusing to read symlink: ${filePath}`);
1751
2347
  }
1752
2348
  }
1753
- return (0, import_fs4.readFileSync)(filePath, "utf-8");
2349
+ return (0, import_fs5.readFileSync)(filePath, "utf-8");
1754
2350
  }
1755
2351
  function writeState(cwd, state) {
1756
- const dir = (0, import_path4.join)(cwd, ".stackwright");
1757
- (0, import_fs4.mkdirSync)(dir, { recursive: true });
2352
+ const dir = (0, import_path5.join)(cwd, ".stackwright");
2353
+ (0, import_fs5.mkdirSync)(dir, { recursive: true });
1758
2354
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1759
2355
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1760
2356
  }
@@ -1775,6 +2371,15 @@ function handleGetPipelineState(_cwd) {
1775
2371
  const cwd = _cwd ?? process.cwd();
1776
2372
  try {
1777
2373
  const state = readState(cwd);
2374
+ const keyPath = (0, import_path5.join)(cwd, ".stackwright", "pipeline-keys.json");
2375
+ if (!(0, import_fs5.existsSync)(keyPath)) {
2376
+ try {
2377
+ const { fingerprint } = initPipelineKeys(cwd);
2378
+ state["signingKeyFingerprint"] = fingerprint;
2379
+ writeState(cwd, state);
2380
+ } catch {
2381
+ }
2382
+ }
1778
2383
  return { text: JSON.stringify(state), isError: false };
1779
2384
  } catch (err) {
1780
2385
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
@@ -1826,28 +2431,62 @@ function handleSetPipelineState(input) {
1826
2431
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1827
2432
  }
1828
2433
  }
1829
- function handleCheckExecutionReady(_cwd) {
2434
+ function handleCheckExecutionReady(_cwd, phase) {
1830
2435
  const cwd = _cwd ?? process.cwd();
2436
+ if (phase) {
2437
+ if (!isValidPhase(phase)) {
2438
+ return {
2439
+ text: JSON.stringify({
2440
+ error: true,
2441
+ message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2442
+ }),
2443
+ isError: true
2444
+ };
2445
+ }
2446
+ const answerFile = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
2447
+ if (!(0, import_fs5.existsSync)(answerFile)) {
2448
+ return {
2449
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
2450
+ isError: false
2451
+ };
2452
+ }
2453
+ try {
2454
+ const raw = safeReadSync(answerFile);
2455
+ const parsed = JSON.parse(raw);
2456
+ if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
2457
+ return {
2458
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
2459
+ isError: false
2460
+ };
2461
+ }
2462
+ return { text: JSON.stringify({ ready: true, phase }), isError: false };
2463
+ } catch {
2464
+ return {
2465
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
2466
+ isError: false
2467
+ };
2468
+ }
2469
+ }
1831
2470
  try {
1832
- const answersDir = (0, import_path4.join)(cwd, ".stackwright", "answers");
2471
+ const answersDir = (0, import_path5.join)(cwd, ".stackwright", "answers");
1833
2472
  const answeredPhases = [];
1834
2473
  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)) {
2474
+ for (const phase2 of PHASE_ORDER) {
2475
+ const answerFile = (0, import_path5.join)(answersDir, `${phase2}.json`);
2476
+ if ((0, import_fs5.existsSync)(answerFile)) {
1838
2477
  try {
1839
2478
  const raw = safeReadSync(answerFile);
1840
2479
  const parsed = JSON.parse(raw);
1841
2480
  if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
1842
- missingPhases.push(phase);
2481
+ missingPhases.push(phase2);
1843
2482
  continue;
1844
2483
  }
1845
- answeredPhases.push(phase);
2484
+ answeredPhases.push(phase2);
1846
2485
  } catch {
1847
- missingPhases.push(phase);
2486
+ missingPhases.push(phase2);
1848
2487
  }
1849
2488
  } else {
1850
- missingPhases.push(phase);
2489
+ missingPhases.push(phase2);
1851
2490
  }
1852
2491
  }
1853
2492
  return {
@@ -1866,15 +2505,35 @@ function handleCheckExecutionReady(_cwd) {
1866
2505
  function handleListArtifacts(_cwd) {
1867
2506
  const cwd = _cwd ?? process.cwd();
1868
2507
  try {
1869
- const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
2508
+ const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
2509
+ let manifest = null;
2510
+ try {
2511
+ manifest = loadSignatureManifest(cwd);
2512
+ } catch {
2513
+ }
1870
2514
  const artifacts = [];
1871
2515
  let completedCount = 0;
1872
2516
  for (const phase of PHASE_ORDER) {
1873
2517
  const expectedFile = PHASE_ARTIFACT[phase];
1874
- const fullPath = (0, import_path4.join)(artifactsDir, expectedFile);
1875
- const exists = (0, import_fs4.existsSync)(fullPath);
2518
+ const fullPath = (0, import_path5.join)(artifactsDir, expectedFile);
2519
+ const exists = (0, import_fs5.existsSync)(fullPath);
1876
2520
  if (exists) completedCount++;
1877
- artifacts.push({ phase, expectedFile, exists, path: fullPath });
2521
+ let signed = false;
2522
+ let signatureValid = null;
2523
+ if (exists && manifest) {
2524
+ const entry = manifest.signatures[expectedFile];
2525
+ if (entry) {
2526
+ signed = true;
2527
+ try {
2528
+ const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
2529
+ const { publicKey } = loadPipelineKeys(cwd);
2530
+ signatureValid = verifyArtifact(rawBytes, entry, publicKey);
2531
+ } catch {
2532
+ signatureValid = null;
2533
+ }
2534
+ }
2535
+ }
2536
+ artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
1878
2537
  }
1879
2538
  return {
1880
2539
  text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
@@ -1910,9 +2569,9 @@ function handleWritePhaseQuestions(input) {
1910
2569
  }
1911
2570
  } catch {
1912
2571
  }
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`);
2572
+ const questionsDir = (0, import_path5.join)(cwd, ".stackwright", "questions");
2573
+ (0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
2574
+ const filePath = (0, import_path5.join)(questionsDir, `${phase}.json`);
1916
2575
  const payload = {
1917
2576
  version: "1.0",
1918
2577
  phase,
@@ -1952,36 +2611,81 @@ function handleBuildSpecialistPrompt(input) {
1952
2611
  };
1953
2612
  }
1954
2613
  try {
1955
- const answersPath = (0, import_path4.join)(cwd, ".stackwright", "answers", `${phase}.json`);
2614
+ const answersPath = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
1956
2615
  let answers = {};
1957
- if ((0, import_fs4.existsSync)(answersPath)) {
2616
+ if ((0, import_fs5.existsSync)(answersPath)) {
1958
2617
  answers = JSON.parse(safeReadSync(answersPath));
1959
2618
  }
2619
+ let buildContextText = "";
2620
+ const buildContextPath = (0, import_path5.join)(cwd, ".stackwright", "build-context.json");
2621
+ if ((0, import_fs5.existsSync)(buildContextPath)) {
2622
+ try {
2623
+ const bcRaw = JSON.parse(safeReadSync(buildContextPath));
2624
+ if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
2625
+ buildContextText = bcRaw.buildContext.trim();
2626
+ }
2627
+ } catch {
2628
+ }
2629
+ }
1960
2630
  const deps = PHASE_DEPENDENCIES[phase];
1961
2631
  const artifactSections = [];
1962
2632
  const missingDependencies = [];
1963
2633
  for (const dep of deps) {
1964
2634
  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));
2635
+ const artifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", artifactFile);
2636
+ if ((0, import_fs5.existsSync)(artifactPath)) {
2637
+ const rawContent = safeReadSync(artifactPath);
2638
+ const rawBytes = Buffer.from(rawContent, "utf-8");
2639
+ const content = JSON.parse(rawContent);
2640
+ let signatureVerified = false;
2641
+ let signatureAvailable = false;
2642
+ try {
2643
+ const sig = getArtifactSignature(cwd, artifactFile);
2644
+ if (sig) {
2645
+ signatureAvailable = true;
2646
+ const { publicKey } = loadPipelineKeys(cwd);
2647
+ signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
2648
+ if (!signatureVerified) {
2649
+ const actualDigest = (0, import_crypto3.createHash)("sha384").update(rawBytes).digest("hex");
2650
+ emitSignatureAuditEvent({
2651
+ artifactFilename: artifactFile,
2652
+ expectedDigest: sig.digest,
2653
+ actualDigest,
2654
+ phase: dep,
2655
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2656
+ source: "stackwright_pro_build_specialist_prompt"
2657
+ });
2658
+ missingDependencies.push(dep);
2659
+ artifactSections.push(
2660
+ `[${artifactFile}]:
2661
+ (integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
2662
+ );
2663
+ continue;
2664
+ }
2665
+ }
2666
+ } catch {
2667
+ }
1968
2668
  const expectedOtter = PHASE_TO_OTTER2[dep];
1969
2669
  const artifactOtter = content["generatedBy"];
2670
+ const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
1970
2671
  if (!artifactOtter) {
1971
2672
  missingDependencies.push(dep);
1972
2673
  artifactSections.push(
1973
2674
  `[${artifactFile}]:
1974
2675
  (integrity check failed: missing generatedBy field)`
1975
2676
  );
1976
- } else if (artifactOtter !== expectedOtter) {
2677
+ } else if (normalizedOtter !== expectedOtter) {
1977
2678
  missingDependencies.push(dep);
1978
2679
  artifactSections.push(
1979
2680
  `[${artifactFile}]:
1980
2681
  (integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
1981
2682
  );
1982
2683
  } else {
1983
- artifactSections.push(`[${artifactFile}]:
1984
- ${JSON.stringify(content, null, 2)}`);
2684
+ const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
2685
+ artifactSections.push(
2686
+ `[${artifactFile}]${sigStatus}:
2687
+ ${JSON.stringify(content, null, 2)}`
2688
+ );
1985
2689
  }
1986
2690
  } else {
1987
2691
  missingDependencies.push(dep);
@@ -1989,10 +2693,17 @@ ${JSON.stringify(content, null, 2)}`);
1989
2693
  (not yet available)`);
1990
2694
  }
1991
2695
  }
1992
- const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
2696
+ const parts = [];
2697
+ if (buildContextText) {
2698
+ parts.push("BUILD_CONTEXT:", buildContextText, "");
2699
+ }
2700
+ parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
1993
2701
  if (artifactSections.length > 0) {
1994
2702
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
1995
2703
  }
2704
+ const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
2705
+ parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
2706
+ parts.push(artifactSchema);
1996
2707
  parts.push("", "Execute using these answers and the upstream artifacts provided.");
1997
2708
  const prompt = parts.join("\n");
1998
2709
  const dependenciesSatisfied = missingDependencies.length === 0;
@@ -2027,45 +2738,231 @@ var OFF_SCRIPT_PATTERNS = [
2027
2738
  var PHASE_REQUIRED_KEYS = {
2028
2739
  designer: ["designLanguage", "themeTokenSeeds"],
2029
2740
  theme: ["tokens"],
2030
- api: ["entities"],
2741
+ api: ["entities", "version", "generatedBy"],
2031
2742
  auth: ["version", "generatedBy"],
2032
- data: ["version", "generatedBy"],
2743
+ data: ["version", "generatedBy", "strategy", "collections"],
2033
2744
  pages: ["version", "generatedBy"],
2034
2745
  dashboard: ["version", "generatedBy"],
2035
2746
  workflow: ["version", "generatedBy"]
2036
2747
  };
2748
+ var PHASE_ARTIFACT_SCHEMA = {
2749
+ designer: JSON.stringify(
2750
+ {
2751
+ version: "1.0",
2752
+ generatedBy: "stackwright-pro-designer-otter",
2753
+ application: {
2754
+ type: "<operational|data-explorer|admin|logistics|general>",
2755
+ environment: "<workstation|field|control-room|mixed>",
2756
+ density: "<compact|balanced|spacious>",
2757
+ accessibility: "<wcag-aa|section-508|none>",
2758
+ colorScheme: "<light|dark|both>"
2759
+ },
2760
+ designLanguage: {
2761
+ rationale: "<design rationale>",
2762
+ spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
2763
+ colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
2764
+ typography: {
2765
+ dataFont: "Inter",
2766
+ headingFont: "Inter",
2767
+ monoFont: "monospace",
2768
+ dataSizePx: 12,
2769
+ bodySizePx: 14
2770
+ },
2771
+ contrastRatio: "4.5",
2772
+ borderRadius: "4",
2773
+ shadowElevation: "standard"
2774
+ },
2775
+ themeTokenSeeds: {
2776
+ light: {
2777
+ background: "#ffffff",
2778
+ foreground: "#1a1a1a",
2779
+ primary: "#1a365d",
2780
+ surface: "#f7f7f7",
2781
+ border: "#e2e8f0"
2782
+ },
2783
+ dark: {
2784
+ background: "#1a1a1a",
2785
+ foreground: "#ffffff",
2786
+ primary: "#90cdf4",
2787
+ surface: "#2d2d2d",
2788
+ border: "#4a5568"
2789
+ }
2790
+ },
2791
+ conformsTo: null,
2792
+ operationalNotes: []
2793
+ },
2794
+ null,
2795
+ 2
2796
+ ),
2797
+ theme: JSON.stringify(
2798
+ {
2799
+ version: "1.0",
2800
+ generatedBy: "stackwright-pro-theme-otter",
2801
+ componentLibrary: "shadcn",
2802
+ colorScheme: "<light|dark|both>",
2803
+ tokens: {
2804
+ colors: { "primary-500": "#1a365d", background: "#ffffff" },
2805
+ spacing: { "spacing-1": "8px", "spacing-2": "16px" },
2806
+ typography: { "font-data": "Inter", "text-sm": "12px" },
2807
+ shape: { "radius-sm": "4px", "radius-md": "8px" },
2808
+ shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
2809
+ },
2810
+ cssVariables: {
2811
+ "--background": "0 0% 100%",
2812
+ "--foreground": "222.2 84% 4.9%",
2813
+ "--primary": "222.2 47.4% 11.2%",
2814
+ "--primary-foreground": "210 40% 98%",
2815
+ "--surface": "210 40% 98%",
2816
+ "--border": "214.3 31.8% 91.4%"
2817
+ },
2818
+ dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
2819
+ },
2820
+ null,
2821
+ 2
2822
+ ),
2823
+ api: JSON.stringify(
2824
+ {
2825
+ version: "1.0",
2826
+ generatedBy: "stackwright-pro-api-otter",
2827
+ entities: [
2828
+ {
2829
+ name: "Shipment",
2830
+ endpoint: "/shipments",
2831
+ method: "GET",
2832
+ revalidate: 60,
2833
+ mutationType: null
2834
+ }
2835
+ ],
2836
+ auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
2837
+ baseUrl: "https://api.example.mil/v2",
2838
+ specPath: "./specs/api.yaml"
2839
+ },
2840
+ null,
2841
+ 2
2842
+ ),
2843
+ data: JSON.stringify(
2844
+ {
2845
+ version: "1.0",
2846
+ generatedBy: "stackwright-pro-data-otter",
2847
+ strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
2848
+ pulseMode: false,
2849
+ collections: [{ name: "equipment", revalidate: 60, pulse: false }],
2850
+ endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
2851
+ requiredPackages: { dependencies: {}, devPackages: {} }
2852
+ },
2853
+ null,
2854
+ 2
2855
+ ),
2856
+ workflow: JSON.stringify(
2857
+ {
2858
+ version: "1.0",
2859
+ generatedBy: "stackwright-pro-workflow-otter",
2860
+ workflowConfig: {
2861
+ id: "procurement-approval",
2862
+ route: "/procurement",
2863
+ files: ["workflows/procurement-approval.yml"],
2864
+ serviceDependencies: ["service:workflow-state"],
2865
+ warnings: []
2866
+ }
2867
+ },
2868
+ null,
2869
+ 2
2870
+ ),
2871
+ pages: JSON.stringify(
2872
+ {
2873
+ version: "1.0",
2874
+ generatedBy: "stackwright-pro-page-otter",
2875
+ pages: [
2876
+ {
2877
+ slug: "catalog",
2878
+ type: "collection_listing",
2879
+ collection: "products",
2880
+ themeApplied: true,
2881
+ authRequired: false
2882
+ },
2883
+ {
2884
+ slug: "admin",
2885
+ type: "protected",
2886
+ collection: null,
2887
+ themeApplied: true,
2888
+ authRequired: true
2889
+ }
2890
+ ]
2891
+ },
2892
+ null,
2893
+ 2
2894
+ ),
2895
+ dashboard: JSON.stringify(
2896
+ {
2897
+ version: "1.0",
2898
+ generatedBy: "stackwright-pro-dashboard-otter",
2899
+ pages: [
2900
+ {
2901
+ slug: "dashboard",
2902
+ layout: "<grid|table|mixed>",
2903
+ collections: ["equipment", "supplies"],
2904
+ mode: "<ISR|Pulse>"
2905
+ }
2906
+ ]
2907
+ },
2908
+ null,
2909
+ 2
2910
+ ),
2911
+ auth: JSON.stringify(
2912
+ {
2913
+ version: "1.0",
2914
+ generatedBy: "stackwright-pro-auth-otter",
2915
+ authConfig: {
2916
+ method: "<cac|oidc|oauth2|none>",
2917
+ provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
2918
+ rbacRoles: ["ADMIN", "ANALYST"],
2919
+ rbacDefaultRole: "ANALYST",
2920
+ protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
2921
+ auditEnabled: true,
2922
+ auditRetentionDays: 90
2923
+ }
2924
+ },
2925
+ null,
2926
+ 2
2927
+ )
2928
+ };
2037
2929
  function handleValidateArtifact(input) {
2038
2930
  const cwd = input._cwd ?? process.cwd();
2039
- const { phase, responseText } = input;
2931
+ const { phase, responseText, artifact: directArtifact } = input;
2040
2932
  if (!isValidPhase(phase)) {
2041
2933
  return {
2042
2934
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2043
2935
  isError: true
2044
2936
  };
2045
2937
  }
2046
- for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2047
- if (pattern.test(responseText)) {
2938
+ let artifact;
2939
+ if (directArtifact) {
2940
+ artifact = directArtifact;
2941
+ } else {
2942
+ const text = responseText ?? "";
2943
+ for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2944
+ if (pattern.test(text)) {
2945
+ const result = {
2946
+ valid: false,
2947
+ phase,
2948
+ violation: "off-script",
2949
+ retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2950
+ };
2951
+ return { text: JSON.stringify(result), isError: false };
2952
+ }
2953
+ }
2954
+ try {
2955
+ artifact = extractJsonFromResponse(text);
2956
+ } catch {
2048
2957
  const result = {
2049
2958
  valid: false,
2050
2959
  phase,
2051
- violation: "off-script",
2052
- retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2960
+ violation: "invalid-json",
2961
+ retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
2053
2962
  };
2054
2963
  return { text: JSON.stringify(result), isError: false };
2055
2964
  }
2056
2965
  }
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
2966
  if (!artifact.version || !artifact.generatedBy) {
2070
2967
  const result = {
2071
2968
  valid: false,
@@ -2086,12 +2983,58 @@ function handleValidateArtifact(input) {
2086
2983
  };
2087
2984
  return { text: JSON.stringify(result), isError: false };
2088
2985
  }
2986
+ const PHASE_ZOD_VALIDATORS = {
2987
+ workflow: (artifact2) => {
2988
+ const workflowConfig = artifact2["workflowConfig"];
2989
+ if (!workflowConfig) return { success: true };
2990
+ const result = import_types.WorkflowFileSchema.safeParse(workflowConfig);
2991
+ if (!result.success) {
2992
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2993
+ return { success: false, error: { message: issues } };
2994
+ }
2995
+ return { success: true };
2996
+ },
2997
+ auth: (artifact2) => {
2998
+ const authConfig = artifact2["authConfig"];
2999
+ if (!authConfig) return { success: true };
3000
+ const result = import_types.authConfigSchema.safeParse(authConfig);
3001
+ if (!result.success) {
3002
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3003
+ return { success: false, error: { message: issues } };
3004
+ }
3005
+ return { success: true };
3006
+ }
3007
+ };
3008
+ const zodValidator = PHASE_ZOD_VALIDATORS[phase];
3009
+ if (zodValidator) {
3010
+ const zodResult = zodValidator(artifact);
3011
+ if (!zodResult.success) {
3012
+ const result = {
3013
+ valid: false,
3014
+ phase,
3015
+ violation: "schema-mismatch",
3016
+ retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
3017
+ };
3018
+ return { text: JSON.stringify(result), isError: false };
3019
+ }
3020
+ }
2089
3021
  try {
2090
- const artifactsDir = (0, import_path4.join)(cwd, ".stackwright", "artifacts");
2091
- (0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
3022
+ const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
3023
+ (0, import_fs5.mkdirSync)(artifactsDir, { recursive: true });
2092
3024
  const artifactFile = PHASE_ARTIFACT[phase];
2093
- const artifactPath = (0, import_path4.join)(artifactsDir, artifactFile);
2094
- safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
3025
+ const artifactPath = (0, import_path5.join)(artifactsDir, artifactFile);
3026
+ const serialized = JSON.stringify(artifact, null, 2) + "\n";
3027
+ const artifactBytes = Buffer.from(serialized, "utf-8");
3028
+ safeWriteSync(artifactPath, serialized);
3029
+ let signed = false;
3030
+ try {
3031
+ const { privateKey } = loadPipelineKeys(cwd);
3032
+ const sig = signArtifact(artifactBytes, privateKey);
3033
+ const signerOtter = PHASE_TO_OTTER2[phase];
3034
+ saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
3035
+ signed = true;
3036
+ } catch {
3037
+ }
2095
3038
  const state = readState(cwd);
2096
3039
  if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
2097
3040
  const ps = state.phases[phase];
@@ -2102,7 +3045,7 @@ function handleValidateArtifact(input) {
2102
3045
  valid: true,
2103
3046
  phase,
2104
3047
  artifactPath,
2105
- summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
3048
+ summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
2106
3049
  };
2107
3050
  return { text: JSON.stringify(result), isError: false };
2108
3051
  } catch (err) {
@@ -2126,11 +3069,15 @@ function registerPipelineTools(server2) {
2126
3069
  "stackwright_pro_set_pipeline_state",
2127
3070
  `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
2128
3071
  {
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")
3072
+ phase: import_zod11.z.string().optional().describe('Phase to update, e.g. "designer"'),
3073
+ field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
3074
+ value: boolCoerce(import_zod11.z.boolean().optional()).describe(
3075
+ 'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
3076
+ ),
3077
+ status: import_zod11.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3078
+ incrementRetry: boolCoerce(import_zod11.z.boolean().optional()).describe(
3079
+ "Bump retryCount by 1 \u2014 must be a JSON boolean"
3080
+ )
2134
3081
  },
2135
3082
  async (args) => res(
2136
3083
  handleSetPipelineState({
@@ -2144,9 +3091,11 @@ function registerPipelineTools(server2) {
2144
3091
  );
2145
3092
  server2.tool(
2146
3093
  "stackwright_pro_check_execution_ready",
2147
- `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
2148
- {},
2149
- async () => res(handleCheckExecutionReady())
3094
+ `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
3095
+ {
3096
+ phase: import_zod11.z.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
3097
+ },
3098
+ async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
2150
3099
  );
2151
3100
  server2.tool(
2152
3101
  "stackwright_pro_list_artifacts",
@@ -2156,34 +3105,81 @@ function registerPipelineTools(server2) {
2156
3105
  );
2157
3106
  server2.tool(
2158
3107
  "stackwright_pro_write_phase_questions",
2159
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
3108
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2160
3109
  {
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")
3110
+ phase: import_zod11.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
3111
+ responseText: import_zod11.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
3112
+ questions: jsonCoerce(import_zod11.z.array(import_zod11.z.any()).optional()).describe(
3113
+ "Questions array for direct specialist write"
3114
+ )
2163
3115
  },
2164
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
3116
+ async ({ phase, responseText, questions }) => {
3117
+ if (phase && questions && Array.isArray(questions)) {
3118
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
3119
+ if (!SAFE_PHASE.test(phase)) {
3120
+ return {
3121
+ content: [
3122
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
3123
+ ],
3124
+ isError: true
3125
+ };
3126
+ }
3127
+ const questionsDir = (0, import_path5.join)(process.cwd(), ".stackwright", "questions");
3128
+ (0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
3129
+ const outPath = (0, import_path5.join)(questionsDir, `${phase}.json`);
3130
+ (0, import_fs5.writeFileSync)(
3131
+ outPath,
3132
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
3133
+ );
3134
+ return {
3135
+ content: [
3136
+ {
3137
+ type: "text",
3138
+ text: JSON.stringify({
3139
+ written: true,
3140
+ phase,
3141
+ count: questions.length
3142
+ })
3143
+ }
3144
+ ]
3145
+ };
3146
+ }
3147
+ return res(
3148
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
3149
+ );
3150
+ }
2165
3151
  );
2166
3152
  server2.tool(
2167
3153
  "stackwright_pro_build_specialist_prompt",
2168
3154
  `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"') },
3155
+ { phase: import_zod11.z.string().describe('Phase to build prompt for, e.g. "pages"') },
2170
3156
  async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
2171
3157
  );
2172
3158
  server2.tool(
2173
3159
  "stackwright_pro_validate_artifact",
2174
- `Validate specialist response + write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
3160
+ `Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
2175
3161
  {
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")
3162
+ phase: import_zod11.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
3163
+ responseText: import_zod11.z.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
3164
+ artifact: jsonCoerce(import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()).optional()).describe(
3165
+ "Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
3166
+ )
2178
3167
  },
2179
- async ({ phase, responseText }) => res(handleValidateArtifact({ phase, responseText }))
3168
+ async ({ phase, responseText, artifact }) => {
3169
+ if (artifact) {
3170
+ return res(
3171
+ handleValidateArtifact({ phase, artifact })
3172
+ );
3173
+ }
3174
+ return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
3175
+ }
2180
3176
  );
2181
3177
  }
2182
3178
 
2183
3179
  // src/tools/safe-write.ts
2184
- var import_zod10 = require("zod");
2185
- var import_fs5 = require("fs");
2186
- var import_path5 = require("path");
3180
+ var import_zod12 = require("zod");
3181
+ var import_fs6 = require("fs");
3182
+ var import_path6 = require("path");
2187
3183
  var OTTER_WRITE_ALLOWLISTS = {
2188
3184
  "stackwright-pro-designer-otter": [
2189
3185
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
@@ -2226,15 +3222,31 @@ var OTTER_WRITE_ALLOWLISTS = {
2226
3222
  };
2227
3223
  var PROTECTED_PATH_PREFIXES = [
2228
3224
  ".stackwright/pipeline-state.json",
3225
+ ".stackwright/pipeline-keys.json",
3226
+ // ephemeral signing keys
3227
+ ".stackwright/artifacts/signatures.json",
3228
+ // artifact signature manifest
2229
3229
  ".stackwright/questions/",
2230
3230
  ".stackwright/answers/"
2231
3231
  ];
3232
+ var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
3233
+ var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
3234
+ var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
3235
+ var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
3236
+ function getMaxBytesForPath(filePath) {
3237
+ if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
3238
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
3239
+ return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
3240
+ if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
3241
+ return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
3242
+ return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
3243
+ }
2232
3244
  function checkPathAllowed(callerOtter, filePath) {
2233
- const normalized = (0, import_path5.normalize)(filePath);
3245
+ const normalized = (0, import_path6.normalize)(filePath);
2234
3246
  if (normalized.includes("..")) {
2235
3247
  return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
2236
3248
  }
2237
- if ((0, import_path5.isAbsolute)(normalized)) {
3249
+ if ((0, import_path6.isAbsolute)(normalized)) {
2238
3250
  return {
2239
3251
  allowed: false,
2240
3252
  error: "Absolute paths are not allowed \u2014 use paths relative to project root"
@@ -2354,11 +3366,23 @@ function handleSafeWrite(input) {
2354
3366
  };
2355
3367
  return { text: JSON.stringify(result), isError: true };
2356
3368
  }
2357
- const normalized = (0, import_path5.normalize)(filePath);
2358
- const fullPath = (0, import_path5.join)(cwd, normalized);
2359
- if ((0, import_fs5.existsSync)(fullPath)) {
3369
+ const contentBytes = Buffer.byteLength(content, "utf-8");
3370
+ const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
3371
+ if (contentBytes > maxBytes) {
3372
+ const result = {
3373
+ success: false,
3374
+ error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
3375
+ callerOtter,
3376
+ attemptedPath: filePath,
3377
+ allowedPaths: []
3378
+ };
3379
+ return { text: JSON.stringify(result), isError: true };
3380
+ }
3381
+ const normalized = (0, import_path6.normalize)(filePath);
3382
+ const fullPath = (0, import_path6.join)(cwd, normalized);
3383
+ if ((0, import_fs6.existsSync)(fullPath)) {
2360
3384
  try {
2361
- const stat = (0, import_fs5.lstatSync)(fullPath);
3385
+ const stat = (0, import_fs6.lstatSync)(fullPath);
2362
3386
  if (stat.isSymbolicLink()) {
2363
3387
  const result = {
2364
3388
  success: false,
@@ -2385,9 +3409,9 @@ function handleSafeWrite(input) {
2385
3409
  }
2386
3410
  try {
2387
3411
  if (createDirectories) {
2388
- (0, import_fs5.mkdirSync)((0, import_path5.dirname)(fullPath), { recursive: true });
3412
+ (0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
2389
3413
  }
2390
- (0, import_fs5.writeFileSync)(fullPath, content, { encoding: "utf-8" });
3414
+ (0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
2391
3415
  const result = {
2392
3416
  success: true,
2393
3417
  path: normalized,
@@ -2413,10 +3437,12 @@ function registerSafeWriteTools(server2) {
2413
3437
  "stackwright_pro_safe_write",
2414
3438
  DESC,
2415
3439
  {
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")
3440
+ callerOtter: import_zod12.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
3441
+ filePath: import_zod12.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
3442
+ content: import_zod12.z.string().describe("File content to write"),
3443
+ createDirectories: boolCoerce(import_zod12.z.boolean().optional().default(true)).describe(
3444
+ "Create parent directories if they don't exist. Default: true"
3445
+ )
2420
3446
  },
2421
3447
  async ({ callerOtter, filePath, content, createDirectories }) => {
2422
3448
  const result = handleSafeWrite({
@@ -2431,9 +3457,9 @@ function registerSafeWriteTools(server2) {
2431
3457
  }
2432
3458
 
2433
3459
  // src/tools/auth.ts
2434
- var import_zod11 = require("zod");
2435
- var import_fs6 = require("fs");
2436
- var import_path6 = require("path");
3460
+ var import_zod13 = require("zod");
3461
+ var import_fs7 = require("fs");
3462
+ var import_path7 = require("path");
2437
3463
  function buildHierarchy(roles) {
2438
3464
  const h = {};
2439
3465
  for (let i = 0; i < roles.length - 1; i++) {
@@ -2695,7 +3721,7 @@ async function configureAuthHandler(params, cwd) {
2695
3721
  auditRetentionDays,
2696
3722
  protectedRoutes
2697
3723
  );
2698
- (0, import_fs6.writeFileSync)((0, import_path6.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
3724
+ (0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
2699
3725
  filesWritten.push("middleware.ts");
2700
3726
  } catch (err) {
2701
3727
  const msg = err instanceof Error ? err.message : String(err);
@@ -2711,12 +3737,12 @@ async function configureAuthHandler(params, cwd) {
2711
3737
  }
2712
3738
  try {
2713
3739
  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");
3740
+ const envPath = (0, import_path7.join)(cwd, ".env.example");
3741
+ if ((0, import_fs7.existsSync)(envPath)) {
3742
+ const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
3743
+ (0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2718
3744
  } else {
2719
- (0, import_fs6.writeFileSync)(envPath, envBlock, "utf8");
3745
+ (0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
2720
3746
  }
2721
3747
  filesWritten.push(".env.example");
2722
3748
  } catch (err) {
@@ -2742,12 +3768,12 @@ async function configureAuthHandler(params, cwd) {
2742
3768
  auditRetentionDays,
2743
3769
  protectedRoutes
2744
3770
  );
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");
3771
+ const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
3772
+ if (!(0, import_fs7.existsSync)(ymlPath)) {
3773
+ (0, import_fs7.writeFileSync)(ymlPath, authYaml, "utf8");
2748
3774
  } else {
2749
- const existing = (0, import_fs6.readFileSync)(ymlPath, "utf8");
2750
- (0, import_fs6.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
3775
+ const existing = (0, import_fs7.readFileSync)(ymlPath, "utf8");
3776
+ (0, import_fs7.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
2751
3777
  }
2752
3778
  filesWritten.push("stackwright.yml");
2753
3779
  } catch (err) {
@@ -2786,35 +3812,35 @@ function registerAuthTools(server2) {
2786
3812
  "stackwright_pro_configure_auth",
2787
3813
  "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
3814
  {
2789
- method: import_zod11.z.enum(["cac", "oidc", "oauth2", "none"]),
2790
- provider: import_zod11.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
3815
+ method: import_zod13.z.enum(["cac", "oidc", "oauth2", "none"]),
3816
+ provider: import_zod13.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
2791
3817
  // 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(),
3818
+ cacCaBundle: import_zod13.z.string().optional(),
3819
+ cacEdipiLookup: import_zod13.z.string().optional(),
3820
+ cacOcspEndpoint: import_zod13.z.string().optional(),
3821
+ cacCertHeader: import_zod13.z.string().optional(),
2796
3822
  // 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(),
3823
+ oidcDiscoveryUrl: import_zod13.z.string().optional(),
3824
+ oidcClientId: import_zod13.z.string().optional(),
3825
+ oidcClientSecret: import_zod13.z.string().optional(),
3826
+ oidcScopes: import_zod13.z.string().optional(),
3827
+ oidcRoleClaim: import_zod13.z.string().optional(),
2802
3828
  // 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(),
3829
+ oauth2AuthUrl: import_zod13.z.string().optional(),
3830
+ oauth2TokenUrl: import_zod13.z.string().optional(),
3831
+ oauth2ClientId: import_zod13.z.string().optional(),
3832
+ oauth2ClientSecret: import_zod13.z.string().optional(),
3833
+ oauth2Scopes: import_zod13.z.string().optional(),
2808
3834
  // RBAC
2809
- rbacRoles: import_zod11.z.array(import_zod11.z.string()).optional(),
2810
- rbacDefaultRole: import_zod11.z.string().optional(),
3835
+ rbacRoles: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
3836
+ rbacDefaultRole: import_zod13.z.string().optional(),
2811
3837
  // Audit
2812
- auditEnabled: import_zod11.z.boolean().optional(),
2813
- auditRetentionDays: import_zod11.z.number().int().positive().optional(),
3838
+ auditEnabled: boolCoerce(import_zod13.z.boolean().optional()),
3839
+ auditRetentionDays: numCoerce(import_zod13.z.number().int().positive().optional()),
2814
3840
  // Routes
2815
- protectedRoutes: import_zod11.z.array(import_zod11.z.string()).optional(),
3841
+ protectedRoutes: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
2816
3842
  // Injection for tests
2817
- _cwd: import_zod11.z.string().optional()
3843
+ _cwd: import_zod13.z.string().optional()
2818
3844
  },
2819
3845
  async (params) => {
2820
3846
  const cwd = params._cwd ?? process.cwd();
@@ -2824,45 +3850,45 @@ function registerAuthTools(server2) {
2824
3850
  }
2825
3851
 
2826
3852
  // src/integrity.ts
2827
- var import_crypto2 = require("crypto");
2828
- var import_fs7 = require("fs");
2829
- var import_path7 = require("path");
3853
+ var import_crypto4 = require("crypto");
3854
+ var import_fs8 = require("fs");
3855
+ var import_path8 = require("path");
2830
3856
  var _checksums = /* @__PURE__ */ new Map([
2831
3857
  [
2832
3858
  "stackwright-pro-api-otter.json",
2833
- "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
3859
+ "9fbaed0ce6116b82d0289f24432037d04637c89b8e73062ed946e5d49b294734"
2834
3860
  ],
2835
3861
  [
2836
3862
  "stackwright-pro-auth-otter.json",
2837
- "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
3863
+ "bf0e66e35d15ba818ba6ff1a007df34975565bacbb35cc0c80151fb1da13e573"
2838
3864
  ],
2839
3865
  [
2840
3866
  "stackwright-pro-dashboard-otter.json",
2841
- "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
3867
+ "4f9fcb73c1f168846dbaf7af09f4ca99887339739160de83f739dca693464a23"
2842
3868
  ],
2843
3869
  [
2844
3870
  "stackwright-pro-data-otter.json",
2845
- "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
3871
+ "fb3e34977908d36a30eb448f069ecf41bff6c309c0daddf00471bb6f4822d025"
2846
3872
  ],
2847
3873
  [
2848
3874
  "stackwright-pro-designer-otter.json",
2849
- "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
3875
+ "af09ac8f06385bdbac63e2820daa2ff7d38b8ff1ff383c161f07e3fb9d9359c5"
2850
3876
  ],
2851
3877
  [
2852
3878
  "stackwright-pro-foreman-otter.json",
2853
- "84ba692e710ac3efab94d27332bcac19c6785b7f41d9076e8e7c860cdd6f8097"
3879
+ "582a26766a5bc80ef9f3aef53e82794c7f47f618bd28e4e70583bfc2b569398c"
2854
3880
  ],
2855
3881
  [
2856
3882
  "stackwright-pro-page-otter.json",
2857
- "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
3883
+ "1ba5754f1e8ac8918483881d1d29e6f0ced6cead57d6e7557ff70b247dc4b979"
2858
3884
  ],
2859
3885
  [
2860
3886
  "stackwright-pro-theme-otter.json",
2861
- "64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732"
3887
+ "b323259ccda02a3397b40df3a5c93286c16c97908535eb89fcf6fa9a2c2c05d4"
2862
3888
  ],
2863
3889
  [
2864
3890
  "stackwright-pro-workflow-otter.json",
2865
- "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
3891
+ "c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
2866
3892
  ]
2867
3893
  ]);
2868
3894
  Object.freeze(_checksums);
@@ -2877,21 +3903,21 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
2877
3903
  }
2878
3904
  var MAX_OTTER_BYTES = 1 * 1024 * 1024;
2879
3905
  function computeSha256(data) {
2880
- return (0, import_crypto2.createHash)("sha256").update(data).digest("hex");
3906
+ return (0, import_crypto4.createHash)("sha256").update(data).digest("hex");
2881
3907
  }
2882
3908
  function safeEqual(a, b) {
2883
3909
  if (a.length !== b.length) return false;
2884
- return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
3910
+ return (0, import_crypto4.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
2885
3911
  }
2886
3912
  function verifyOtterFile(filePath) {
2887
- const filename = (0, import_path7.basename)(filePath);
3913
+ const filename = (0, import_path8.basename)(filePath);
2888
3914
  const expected = CANONICAL_CHECKSUMS.get(filename);
2889
3915
  if (expected === void 0) {
2890
3916
  return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
2891
3917
  }
2892
3918
  let stat;
2893
3919
  try {
2894
- stat = (0, import_fs7.lstatSync)(filePath);
3920
+ stat = (0, import_fs8.lstatSync)(filePath);
2895
3921
  } catch (err) {
2896
3922
  const msg = err instanceof Error ? err.message : String(err);
2897
3923
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -2909,7 +3935,7 @@ function verifyOtterFile(filePath) {
2909
3935
  }
2910
3936
  let raw;
2911
3937
  try {
2912
- raw = (0, import_fs7.readFileSync)(filePath);
3938
+ raw = (0, import_fs8.readFileSync)(filePath);
2913
3939
  } catch (err) {
2914
3940
  const msg = err instanceof Error ? err.message : String(err);
2915
3941
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -2942,12 +3968,24 @@ function verifyOtterFile(filePath) {
2942
3968
  return { verified: true, filename };
2943
3969
  }
2944
3970
  function verifyAllOtters(otterDir) {
3971
+ if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
3972
+ return {
3973
+ verified: [],
3974
+ failed: [
3975
+ {
3976
+ filename: "<directory>",
3977
+ error: `Security: path traversal sequence detected in otter directory parameter`
3978
+ }
3979
+ ],
3980
+ unknown: []
3981
+ };
3982
+ }
2945
3983
  const verified = [];
2946
3984
  const failed = [];
2947
3985
  const unknown = [];
2948
3986
  let entries;
2949
3987
  try {
2950
- entries = (0, import_fs7.readdirSync)(otterDir);
3988
+ entries = (0, import_fs8.readdirSync)(otterDir);
2951
3989
  } catch (err) {
2952
3990
  const msg = err instanceof Error ? err.message : String(err);
2953
3991
  return {
@@ -2958,9 +3996,9 @@ function verifyAllOtters(otterDir) {
2958
3996
  }
2959
3997
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
2960
3998
  for (const filename of otterFiles) {
2961
- const filePath = (0, import_path7.join)(otterDir, filename);
3999
+ const filePath = (0, import_path8.join)(otterDir, filename);
2962
4000
  try {
2963
- if ((0, import_fs7.lstatSync)(filePath).isSymbolicLink()) {
4001
+ if ((0, import_fs8.lstatSync)(filePath).isSymbolicLink()) {
2964
4002
  failed.push({ filename, error: "Skipped: symlink" });
2965
4003
  continue;
2966
4004
  }
@@ -2986,15 +4024,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
2986
4024
  function resolveOtterDir() {
2987
4025
  const cwd = process.cwd();
2988
4026
  for (const relative of DEFAULT_SEARCH_PATHS) {
2989
- const candidate = (0, import_path7.join)(cwd, relative);
4027
+ const candidate = (0, import_path8.join)(cwd, relative);
2990
4028
  try {
2991
- (0, import_fs7.lstatSync)(candidate);
4029
+ (0, import_fs8.lstatSync)(candidate);
2992
4030
  return candidate;
2993
4031
  } catch {
2994
4032
  }
2995
4033
  }
2996
4034
  return null;
2997
4035
  }
4036
+ function emitIntegrityAuditEvent(params) {
4037
+ const record = JSON.stringify({
4038
+ level: "AUDIT",
4039
+ event: "INTEGRITY_FAIL",
4040
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4041
+ source: "stackwright_pro_verify_otter_integrity",
4042
+ otterDir: params.otterDir,
4043
+ failedCount: params.failed.length,
4044
+ unknownCount: params.unknown.length,
4045
+ failures: params.failed,
4046
+ unknown: params.unknown
4047
+ });
4048
+ process.stderr.write(`INTEGRITY_FAIL ${record}
4049
+ `);
4050
+ }
2998
4051
  function registerIntegrityTools(server2) {
2999
4052
  server2.tool(
3000
4053
  "stackwright_pro_verify_otter_integrity",
@@ -3018,6 +4071,13 @@ function registerIntegrityTools(server2) {
3018
4071
  }
3019
4072
  const result = verifyAllOtters(resolved);
3020
4073
  const allGood = result.failed.length === 0 && result.unknown.length === 0;
4074
+ if (!allGood) {
4075
+ emitIntegrityAuditEvent({
4076
+ otterDir: resolved,
4077
+ failed: result.failed,
4078
+ unknown: result.unknown
4079
+ });
4080
+ }
3021
4081
  return {
3022
4082
  content: [
3023
4083
  {
@@ -3030,7 +4090,10 @@ function registerIntegrityTools(server2) {
3030
4090
  unknownCount: result.unknown.length,
3031
4091
  verified: result.verified,
3032
4092
  failed: result.failed,
3033
- unknown: result.unknown
4093
+ unknown: result.unknown,
4094
+ ...allGood ? {} : {
4095
+ error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
4096
+ }
3034
4097
  })
3035
4098
  }
3036
4099
  ],
@@ -3041,14 +4104,14 @@ function registerIntegrityTools(server2) {
3041
4104
  }
3042
4105
 
3043
4106
  // src/tools/domain.ts
3044
- var import_zod12 = require("zod");
3045
- var import_fs8 = require("fs");
3046
- var import_path8 = require("path");
4107
+ var import_zod14 = require("zod");
4108
+ var import_fs9 = require("fs");
4109
+ var import_path9 = require("path");
3047
4110
  function handleListCollections(input) {
3048
4111
  const cwd = input._cwd ?? process.cwd();
3049
4112
  const sources = [
3050
4113
  {
3051
- path: (0, import_path8.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
4114
+ path: (0, import_path9.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
3052
4115
  source: "data-config.json",
3053
4116
  parse: (raw) => {
3054
4117
  const parsed = JSON.parse(raw);
@@ -3059,15 +4122,15 @@ function handleListCollections(input) {
3059
4122
  }
3060
4123
  },
3061
4124
  {
3062
- path: (0, import_path8.join)(cwd, "stackwright.yml"),
4125
+ path: (0, import_path9.join)(cwd, "stackwright.yml"),
3063
4126
  source: "stackwright.yml",
3064
4127
  parse: extractCollectionsFromYaml
3065
4128
  }
3066
4129
  ];
3067
4130
  for (const { path: path3, source, parse } of sources) {
3068
- if (!(0, import_fs8.existsSync)(path3)) continue;
4131
+ if (!(0, import_fs9.existsSync)(path3)) continue;
3069
4132
  try {
3070
- const collections = parse((0, import_fs8.readFileSync)(path3, "utf8"));
4133
+ const collections = parse((0, import_fs9.readFileSync)(path3, "utf8"));
3071
4134
  return {
3072
4135
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
3073
4136
  isError: false
@@ -3218,8 +4281,8 @@ function handleValidateWorkflow(input) {
3218
4281
  if (input.workflow && Object.keys(input.workflow).length > 0) {
3219
4282
  raw = input.workflow;
3220
4283
  } else {
3221
- const artifactPath = (0, import_path8.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
3222
- if (!(0, import_fs8.existsSync)(artifactPath)) {
4284
+ const artifactPath = (0, import_path9.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
4285
+ if (!(0, import_fs9.existsSync)(artifactPath)) {
3223
4286
  return fail([
3224
4287
  {
3225
4288
  code: "NO_WORKFLOW",
@@ -3228,7 +4291,7 @@ function handleValidateWorkflow(input) {
3228
4291
  ]);
3229
4292
  }
3230
4293
  try {
3231
- raw = JSON.parse((0, import_fs8.readFileSync)(artifactPath, "utf8"));
4294
+ raw = JSON.parse((0, import_fs9.readFileSync)(artifactPath, "utf8"));
3232
4295
  } catch (err) {
3233
4296
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
3234
4297
  }
@@ -3427,7 +4490,7 @@ function registerDomainTools(server2) {
3427
4490
  "stackwright_pro_resolve_data_strategy",
3428
4491
  "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
4492
  {
3430
- strategy: import_zod12.z.string().describe(
4493
+ strategy: import_zod14.z.string().describe(
3431
4494
  'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
3432
4495
  )
3433
4496
  },
@@ -3437,7 +4500,7 @@ function registerDomainTools(server2) {
3437
4500
  "stackwright_pro_validate_workflow",
3438
4501
  "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
4502
  {
3440
- workflow: import_zod12.z.record(import_zod12.z.string(), import_zod12.z.unknown()).optional().describe(
4503
+ workflow: jsonCoerce(import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional()).describe(
3441
4504
  "Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
3442
4505
  )
3443
4506
  },
@@ -3445,20 +4508,112 @@ function registerDomainTools(server2) {
3445
4508
  );
3446
4509
  }
3447
4510
 
4511
+ // src/tools/type-schemas.ts
4512
+ var import_zod15 = require("zod");
4513
+ function buildTypeSchemaSummary() {
4514
+ return {
4515
+ version: "1.0",
4516
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4517
+ domains: {
4518
+ workflow: {
4519
+ description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
4520
+ schemas: [
4521
+ "WorkflowFileSchema",
4522
+ "WorkflowDefinitionSchema",
4523
+ "WorkflowStepSchema",
4524
+ "WorkflowStepTypeSchema",
4525
+ "WorkflowFieldSchema",
4526
+ "WorkflowActionSchema",
4527
+ "WorkflowAuthSchema",
4528
+ "WorkflowThemeSchema",
4529
+ "TransitionConditionSchema",
4530
+ "PersistenceSchema"
4531
+ ],
4532
+ otter: "stackwright-pro-workflow-otter",
4533
+ artifactKey: "workflowConfig"
4534
+ },
4535
+ auth: {
4536
+ description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
4537
+ schemas: [
4538
+ "authConfigSchema",
4539
+ "pkiConfigSchema",
4540
+ "oidcConfigSchema",
4541
+ "rbacConfigSchema",
4542
+ "componentAuthSchema",
4543
+ "authUserSchema",
4544
+ "authSessionSchema"
4545
+ ],
4546
+ otter: "stackwright-pro-auth-otter",
4547
+ artifactKey: "authConfig"
4548
+ },
4549
+ openapi: {
4550
+ description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
4551
+ interfaces: [
4552
+ "OpenAPIConfig",
4553
+ "ActionConfig",
4554
+ "EndpointFilter",
4555
+ "ApprovedSpec",
4556
+ "PrebuildSecurityConfig",
4557
+ "SiteConfig",
4558
+ "ValidationResult"
4559
+ ],
4560
+ otter: "stackwright-pro-api-otter",
4561
+ artifactKey: "apiConfig"
4562
+ },
4563
+ pulse: {
4564
+ description: "Real-time data polling \u2014 source-agnostic polling options and states",
4565
+ interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
4566
+ note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
4567
+ },
4568
+ enterprise: {
4569
+ description: "Enterprise collection access \u2014 multi-tenant provider extension",
4570
+ interfaces: [
4571
+ "EnterpriseCollectionProvider",
4572
+ "CollectionProvider",
4573
+ "CollectionEntry",
4574
+ "CollectionListOptions",
4575
+ "CollectionListResult",
4576
+ "TenantFilter",
4577
+ "Collection"
4578
+ ],
4579
+ note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
4580
+ }
4581
+ }
4582
+ };
4583
+ }
4584
+ function registerTypeSchemasTool(server2) {
4585
+ server2.tool(
4586
+ "stackwright_pro_get_type_schemas",
4587
+ "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.",
4588
+ {
4589
+ format: import_zod15.z.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
4590
+ },
4591
+ async ({ format }) => {
4592
+ const summary = buildTypeSchemaSummary();
4593
+ const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
4594
+ return {
4595
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
4596
+ };
4597
+ }
4598
+ );
4599
+ }
4600
+
3448
4601
  // package.json
3449
4602
  var package_default = {
3450
4603
  dependencies: {
4604
+ "@stackwright-pro/types": "workspace:*",
3451
4605
  "@modelcontextprotocol/sdk": "^1.10.0",
3452
4606
  "@stackwright-pro/cli-data-explorer": "workspace:*",
3453
- zod: "^4.3.6"
4607
+ zod: "^4.4.3"
3454
4608
  },
3455
4609
  devDependencies: {
3456
- "@types/node": "^24.1.0",
3457
- tsup: "^8.5.0",
3458
- typescript: "^5.8.3",
3459
- vitest: "^4.0.18"
4610
+ "@types/node": "catalog:",
4611
+ tsup: "catalog:",
4612
+ typescript: "catalog:",
4613
+ vitest: "catalog:"
3460
4614
  },
3461
4615
  scripts: {
4616
+ prepublishOnly: "node scripts/verify-integrity-sync.js",
3462
4617
  build: "tsup",
3463
4618
  dev: "tsup --watch",
3464
4619
  start: "node dist/server.js",
@@ -3466,10 +4621,13 @@ var package_default = {
3466
4621
  "test:coverage": "vitest run --coverage"
3467
4622
  },
3468
4623
  name: "@stackwright-pro/mcp",
3469
- version: "0.2.0-alpha.5",
4624
+ version: "0.2.0-alpha.49",
3470
4625
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3471
- license: "PROPRIETARY",
4626
+ license: "SEE LICENSE IN LICENSE",
3472
4627
  main: "./dist/server.js",
4628
+ bin: {
4629
+ "stackwright-pro-mcp": "./dist/server.js"
4630
+ },
3473
4631
  module: "./dist/server.mjs",
3474
4632
  types: "./dist/server.d.ts",
3475
4633
  exports: {
@@ -3482,6 +4640,11 @@ var package_default = {
3482
4640
  types: "./dist/integrity.d.ts",
3483
4641
  import: "./dist/integrity.mjs",
3484
4642
  require: "./dist/integrity.js"
4643
+ },
4644
+ "./type-schemas": {
4645
+ types: "./dist/tools/type-schemas.d.ts",
4646
+ import: "./dist/tools/type-schemas.mjs",
4647
+ require: "./dist/tools/type-schemas.js"
3485
4648
  }
3486
4649
  },
3487
4650
  files: [
@@ -3509,7 +4672,9 @@ registerPipelineTools(server);
3509
4672
  registerSafeWriteTools(server);
3510
4673
  registerAuthTools(server);
3511
4674
  registerIntegrityTools(server);
4675
+ registerArtifactSigningTools(server);
3512
4676
  registerDomainTools(server);
4677
+ registerTypeSchemasTool(server);
3513
4678
  async function main() {
3514
4679
  const transport = new import_stdio.StdioServerTransport();
3515
4680
  await server.connect(transport);