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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server.mjs CHANGED
@@ -3,15 +3,44 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
 
5
5
  // src/tools/data-explorer.ts
6
- import { z } from "zod";
6
+ import { z as z2 } from "zod";
7
7
  import { listEntities, generateFilter } from "@stackwright-pro/cli-data-explorer";
8
+
9
+ // src/coerce.ts
10
+ import { z } from "zod";
11
+ function jsonCoerce(schema) {
12
+ return z.preprocess((v) => {
13
+ if (typeof v === "string") {
14
+ try {
15
+ return JSON.parse(v);
16
+ } catch {
17
+ return v;
18
+ }
19
+ }
20
+ return v;
21
+ }, schema);
22
+ }
23
+ function boolCoerce(schema) {
24
+ return z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, schema);
25
+ }
26
+ function numCoerce(schema) {
27
+ return z.preprocess((v) => {
28
+ if (typeof v === "string" && v.trim() !== "") {
29
+ const n = Number(v);
30
+ if (!Number.isNaN(n)) return n;
31
+ }
32
+ return v;
33
+ }, schema);
34
+ }
35
+
36
+ // src/tools/data-explorer.ts
8
37
  function registerDataExplorerTools(server2) {
9
38
  server2.tool(
10
39
  "stackwright_pro_list_entities",
11
40
  "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.",
12
41
  {
13
- specPath: z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
14
- projectRoot: z.string().optional().describe("Project root directory (auto-detected if omitted)")
42
+ specPath: z2.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
43
+ projectRoot: z2.string().optional().describe("Project root directory (auto-detected if omitted)")
15
44
  },
16
45
  async ({ specPath, projectRoot }) => {
17
46
  const result = listEntities({
@@ -59,9 +88,13 @@ function registerDataExplorerTools(server2) {
59
88
  "stackwright_pro_generate_filter",
60
89
  "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.",
61
90
  {
62
- selectedEntities: z.array(z.string()).describe('Entity slugs to include (e.g., ["equipment", "supplies"])'),
63
- excludePatterns: z.array(z.string()).optional().describe('Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'),
64
- projectRoot: z.string().optional().describe("Project root directory")
91
+ selectedEntities: jsonCoerce(z2.array(z2.string())).describe(
92
+ 'Entity slugs to include (e.g., ["equipment", "supplies"])'
93
+ ),
94
+ excludePatterns: jsonCoerce(z2.array(z2.string()).optional()).describe(
95
+ 'Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'
96
+ ),
97
+ projectRoot: z2.string().optional().describe("Project root directory")
65
98
  },
66
99
  async ({ selectedEntities, excludePatterns, projectRoot }) => {
67
100
  const result = generateFilter({
@@ -117,7 +150,7 @@ function registerDataExplorerTools(server2) {
117
150
  }
118
151
 
119
152
  // src/tools/security.ts
120
- import { z as z2 } from "zod";
153
+ import { z as z3 } from "zod";
121
154
  import { createHash } from "crypto";
122
155
  import fs from "fs";
123
156
  import path from "path";
@@ -126,8 +159,8 @@ function registerSecurityTools(server2) {
126
159
  "stackwright_pro_validate_spec",
127
160
  "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.",
128
161
  {
129
- specPath: z2.string().describe("URL or file path to the OpenAPI spec to validate"),
130
- configPath: z2.string().optional().describe("Path to stackwright.yml with prebuild.security config")
162
+ specPath: z3.string().describe("URL or file path to the OpenAPI spec to validate"),
163
+ configPath: z3.string().optional().describe("Path to stackwright.yml with prebuild.security config")
131
164
  },
132
165
  async ({ specPath, configPath }) => {
133
166
  let securityEnabled = false;
@@ -235,16 +268,15 @@ Status: Valid (${allowlist.length} specs on allowlist)`
235
268
  "stackwright_pro_add_approved_spec",
236
269
  "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.",
237
270
  {
238
- name: z2.string().describe("Human-readable name for the spec"),
239
- url: z2.string().describe("URL or file path to the OpenAPI spec"),
240
- configPath: z2.string().optional().describe("Path to stackwright.yml")
271
+ name: z3.string().describe("Human-readable name for the spec"),
272
+ url: z3.string().describe("URL or file path to the OpenAPI spec"),
273
+ configPath: z3.string().optional().describe("Path to stackwright.yml")
241
274
  },
242
275
  async ({ name, url, configPath }) => {
243
276
  const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
244
277
  let sha256 = "<computed-at-build>";
245
278
  try {
246
279
  if (url.startsWith("http://") || url.startsWith("https://")) {
247
- sha256 = "<computed-at-build>";
248
280
  } else if (fs.existsSync(url)) {
249
281
  const specContent = fs.readFileSync(url, "utf8");
250
282
  sha256 = createHash("sha256").update(specContent).digest("hex");
@@ -291,7 +323,7 @@ SHA-256 computed: ${sha256}`
291
323
  "stackwright_pro_list_approved_specs",
292
324
  "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.",
293
325
  {
294
- configPath: z2.string().optional().describe("Path to stackwright.yml")
326
+ configPath: z3.string().optional().describe("Path to stackwright.yml")
295
327
  },
296
328
  async ({ configPath }) => {
297
329
  const configFile = configPath || path.join(process.cwd(), "stackwright.yml");
@@ -360,16 +392,18 @@ SHA-256 computed: ${sha256}`
360
392
  }
361
393
 
362
394
  // src/tools/isr.ts
363
- import { z as z3 } from "zod";
395
+ import { z as z4 } from "zod";
364
396
  function registerIsrTools(server2) {
365
397
  server2.tool(
366
398
  "stackwright_pro_configure_isr",
367
399
  "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.",
368
400
  {
369
- collection: z3.string().describe('Collection name (e.g., "equipment", "supplies")'),
370
- revalidateSeconds: z3.number().optional().describe("Revalidation interval in seconds (default: 60)"),
371
- fallback: z3.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
372
- configPath: z3.string().optional().describe("Path to stackwright.yml")
401
+ collection: z4.string().describe('Collection name (e.g., "equipment", "supplies")'),
402
+ revalidateSeconds: numCoerce(z4.number().optional()).describe(
403
+ "Revalidation interval in seconds (default: 60)"
404
+ ),
405
+ fallback: z4.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
406
+ configPath: z4.string().optional().describe("Path to stackwright.yml")
373
407
  },
374
408
  async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
375
409
  const revalidate = revalidateSeconds;
@@ -380,7 +414,7 @@ function registerIsrTools(server2) {
380
414
  isr:
381
415
  revalidate: ${revalidate}
382
416
  fallback: ${fallback}`;
383
- let description = "";
417
+ let description;
384
418
  if (revalidate < 60) {
385
419
  description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
386
420
  } else if (revalidate < 300) {
@@ -412,14 +446,16 @@ ${yamlSnippet}
412
446
  "stackwright_pro_configure_isr_batch",
413
447
  "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.",
414
448
  {
415
- collections: z3.array(
416
- z3.object({
417
- name: z3.string().describe("Collection name"),
418
- revalidateSeconds: z3.number().optional().describe("Revalidation interval")
419
- })
449
+ collections: jsonCoerce(
450
+ z4.array(
451
+ z4.object({
452
+ name: z4.string().describe("Collection name"),
453
+ revalidateSeconds: numCoerce(z4.number().optional()).describe("Revalidation interval")
454
+ })
455
+ )
420
456
  ).describe("Array of collection configurations"),
421
- defaultFallback: z3.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
422
- configPath: z3.string().optional().describe("Path to stackwright.yml")
457
+ defaultFallback: z4.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
458
+ configPath: z4.string().optional().describe("Path to stackwright.yml")
423
459
  },
424
460
  async ({ collections, defaultFallback = "blocking", configPath }) => {
425
461
  const lines = [`\u2699\uFE0F Batch ISR Configuration:
@@ -447,17 +483,17 @@ Fallback: ${defaultFallback}`);
447
483
  }
448
484
 
449
485
  // src/tools/dashboard.ts
450
- import { z as z4 } from "zod";
486
+ import { z as z5 } from "zod";
451
487
  import { listEntities as listEntities2 } from "@stackwright-pro/cli-data-explorer";
452
488
  function registerDashboardTools(server2) {
453
489
  server2.tool(
454
490
  "stackwright_pro_generate_dashboard",
455
491
  "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.",
456
492
  {
457
- entities: z4.array(z4.string()).describe("Entity slugs to include in dashboard"),
458
- layout: z4.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
459
- pageTitle: z4.string().optional().describe("Page title"),
460
- specPath: z4.string().optional().describe("Path to OpenAPI spec for entity details")
493
+ entities: jsonCoerce(z5.array(z5.string())).describe("Entity slugs to include in dashboard"),
494
+ layout: z5.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
495
+ pageTitle: z5.string().optional().describe("Page title"),
496
+ specPath: z5.string().optional().describe("Path to OpenAPI spec for entity details")
461
497
  },
462
498
  async ({ entities, layout = "mixed", pageTitle, specPath }) => {
463
499
  let entityDetails = [];
@@ -543,9 +579,9 @@ ${yaml}
543
579
  "stackwright_pro_generate_detail_page",
544
580
  "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.",
545
581
  {
546
- entity: z4.string().describe('Entity slug (e.g., "equipment")'),
547
- slugField: z4.string().optional().describe('Field to use as URL slug (default: "id")'),
548
- specPath: z4.string().optional().describe("Path to OpenAPI spec for field details")
582
+ entity: z5.string().describe('Entity slug (e.g., "equipment")'),
583
+ slugField: z5.string().optional().describe('Field to use as URL slug (default: "id")'),
584
+ specPath: z5.string().optional().describe("Path to OpenAPI spec for field details")
549
585
  },
550
586
  async ({ entity, slugField = "id", specPath }) => {
551
587
  let fields = [];
@@ -568,39 +604,49 @@ ${yaml}
568
604
  ` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
569
605
  "",
570
606
  " content_items:",
571
- " - main:",
572
- ' label: "detail-header"',
573
- " heading:",
574
- ` text: "{{ ${entity}.${slugField} }}"`,
575
- ' textSize: "h1"',
576
- " textBlocks:",
577
- ` - text: "Details for this ${entity}"`,
578
- ' textSize: "body1"',
579
- ' background: "primary"',
580
- ' color: "text"',
607
+ " - type: text_block",
608
+ ' label: "detail-header"',
609
+ " heading:",
610
+ ` text: "{{ ${entity}.${slugField} }}"`,
611
+ ' textSize: "h1"',
612
+ " textBlocks:",
613
+ ` - text: "Details for this ${entity}"`,
614
+ ' textSize: "body1"',
581
615
  "",
582
- " - grid:",
583
- ' label: "detail-fields"',
584
- " columns: 2",
585
- " items:"
616
+ " - type: grid",
617
+ ' label: "detail-fields"',
618
+ " columns:"
586
619
  ];
587
620
  const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
588
621
  { name: slugField, type: "string" },
589
622
  { name: "created_at", type: "datetime" },
590
623
  { name: "updated_at", type: "datetime" }
591
624
  ];
592
- for (const field of displayFields) {
593
- const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
594
- yamlLines.push(` - card:`);
595
- yamlLines.push(` label: "${field.name}-field"`);
596
- yamlLines.push(` heading:`);
597
- yamlLines.push(` text: "${label}"`);
598
- yamlLines.push(` textSize: "h4"`);
599
- yamlLines.push(` content:`);
600
- yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
601
- yamlLines.push(` textSize: "body1"`);
602
- }
603
- yamlLines.push(' background: "background"');
625
+ const mid = Math.ceil(displayFields.length / 2);
626
+ const col1 = displayFields.slice(0, mid);
627
+ const col2 = displayFields.slice(mid);
628
+ for (const [colIndex, colFields] of [col1, col2].entries()) {
629
+ yamlLines.push(" - width: 1");
630
+ yamlLines.push(" content_items:");
631
+ for (const field of colFields) {
632
+ const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
633
+ yamlLines.push(" - type: text_block");
634
+ yamlLines.push(` label: "${field.name}-field"`);
635
+ yamlLines.push(" heading:");
636
+ yamlLines.push(` text: "${label}"`);
637
+ yamlLines.push(' textSize: "h4"');
638
+ yamlLines.push(" textBlocks:");
639
+ yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
640
+ yamlLines.push(' textSize: "body1"');
641
+ }
642
+ }
643
+ yamlLines.push("");
644
+ yamlLines.push(" - type: action_bar");
645
+ yamlLines.push(" actions:");
646
+ yamlLines.push(` - label: "<- Back to ${entityName}"`);
647
+ yamlLines.push(" action: navigate");
648
+ yamlLines.push(` href: "/${entity}"`);
649
+ yamlLines.push(" style: secondary");
604
650
  const yaml = yamlLines.join("\n");
605
651
  return {
606
652
  content: [
@@ -621,7 +667,7 @@ ${yaml}
621
667
  }
622
668
 
623
669
  // src/tools/clarification.ts
624
- import { z as z5 } from "zod";
670
+ import { z as z6 } from "zod";
625
671
  var CONTRADICTION_PATTERNS = [
626
672
  {
627
673
  keywords: ["minimal", "clean", "simple"],
@@ -709,12 +755,14 @@ function registerClarificationTools(server2) {
709
755
  "stackwright_pro_clarify",
710
756
  "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).",
711
757
  {
712
- context: z5.string().optional().describe("Context about what the otter is trying to do"),
713
- question_type: z5.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
714
- question: z5.string().describe("The clarification question to ask the user"),
715
- choices: z5.array(z5.string()).optional().describe("Options for closed_choice questions"),
716
- priority: z5.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
717
- target_field: z5.string().optional().describe("What field/config does this clarify?")
758
+ context: z6.string().optional().describe("Context about what the otter is trying to do"),
759
+ question_type: z6.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
760
+ question: z6.string().describe("The clarification question to ask the user"),
761
+ choices: jsonCoerce(z6.array(z6.string()).optional()).describe(
762
+ "Options for closed_choice questions"
763
+ ),
764
+ priority: z6.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
765
+ target_field: z6.string().optional().describe("What field/config does this clarify?")
718
766
  },
719
767
  async ({ context, question_type, question, choices, priority, target_field }) => {
720
768
  const result = handleClarify({
@@ -743,8 +791,10 @@ function registerClarificationTools(server2) {
743
791
  "stackwright_pro_detect_conflict",
744
792
  "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.",
745
793
  {
746
- stated_preference: z5.string().describe("What the user said they wanted"),
747
- selected_values: z5.record(z5.string(), z5.string()).describe("What the user actually selected (field \u2192 value)")
794
+ stated_preference: z6.string().describe("What the user said they wanted"),
795
+ selected_values: jsonCoerce(z6.record(z6.string(), z6.string())).describe(
796
+ "What the user actually selected (field \u2192 value)"
797
+ )
748
798
  },
749
799
  async ({ stated_preference, selected_values }) => {
750
800
  const result = handleDetectConflict({ stated_preference, selected_values });
@@ -789,7 +839,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
789
839
  }
790
840
 
791
841
  // src/tools/packages.ts
792
- import { z as z6 } from "zod";
842
+ import { z as z7 } from "zod";
793
843
  import { readFileSync, writeFileSync, existsSync, realpathSync, lstatSync } from "fs";
794
844
  import { execSync } from "child_process";
795
845
  import path2 from "path";
@@ -810,17 +860,23 @@ function registerPackageTools(server2) {
810
860
  "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.",
811
861
  {
812
862
  // FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
813
- packages: z6.record(z6.string(), z6.string()).describe(
814
- 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
863
+ packages: jsonCoerce(z7.record(z7.string(), z7.string()).optional().default({})).describe(
864
+ 'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
865
+ ),
866
+ devPackages: jsonCoerce(z7.record(z7.string(), z7.string()).optional()).describe(
867
+ "devDependencies to add. Same format as packages."
815
868
  ),
816
- devPackages: z6.record(z6.string(), z6.string()).optional().describe("devDependencies to add. Same format as packages."),
817
- scripts: z6.record(z6.string(), z6.string()).optional().describe("npm scripts to add. Only adds if key does not already exist."),
818
- targetDir: z6.string().optional().describe(
869
+ scripts: jsonCoerce(z7.record(z7.string(), z7.string()).optional()).describe(
870
+ "npm scripts to add. Only adds if key does not already exist."
871
+ ),
872
+ targetDir: z7.string().optional().describe(
819
873
  "Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
820
874
  ),
821
- runInstall: z6.boolean().optional().default(true).describe("Run pnpm install after writing package.json. Defaults to true."),
822
- includeBaseline: z6.boolean().optional().default(false).describe(
823
- "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."
875
+ runInstall: boolCoerce(z7.boolean().optional().default(true)).describe(
876
+ "Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
877
+ ),
878
+ includeBaseline: boolCoerce(z7.boolean().optional().default(false)).describe(
879
+ "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."
824
880
  )
825
881
  },
826
882
  async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
@@ -978,11 +1034,11 @@ function setupPackages(opts) {
978
1034
  }
979
1035
  }
980
1036
  const raw = readFileSync(realPackageJsonPath, "utf8");
981
- const PackageJsonSchema = z6.object({
1037
+ const PackageJsonSchema = z7.object({
982
1038
  // Zod v4: z.record(keySchema, valueSchema) — two-arg form required
983
- dependencies: z6.record(z6.string(), z6.string()).optional(),
984
- devDependencies: z6.record(z6.string(), z6.string()).optional(),
985
- scripts: z6.record(z6.string(), z6.string()).optional()
1039
+ dependencies: z7.record(z7.string(), z7.string()).optional(),
1040
+ devDependencies: z7.record(z7.string(), z7.string()).optional(),
1041
+ scripts: z7.record(z7.string(), z7.string()).optional()
986
1042
  }).passthrough();
987
1043
  const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
988
1044
  if (!schemaResult.success) {
@@ -1063,9 +1119,10 @@ function setupPackages(opts) {
1063
1119
  }
1064
1120
 
1065
1121
  // src/tools/questions.ts
1066
- import { readFile } from "fs/promises";
1122
+ import { readFile, writeFile } from "fs/promises";
1123
+ import { existsSync as existsSync2, lstatSync as lstatSync2, mkdirSync, renameSync } from "fs";
1067
1124
  import { join } from "path";
1068
- import { z as z7 } from "zod";
1125
+ import { z as z8 } from "zod";
1069
1126
 
1070
1127
  // src/question-adapter.ts
1071
1128
  function truncate(str, maxLength) {
@@ -1251,22 +1308,22 @@ function answersToManifestFormat(answers, questions) {
1251
1308
  }
1252
1309
 
1253
1310
  // src/tools/questions.ts
1254
- var ManifestQuestionSchema = z7.object({
1255
- id: z7.string(),
1256
- question: z7.string(),
1257
- type: z7.enum(["text", "select", "multi-select", "confirm"]),
1258
- required: z7.boolean().optional(),
1259
- options: z7.array(
1260
- z7.object({
1261
- label: z7.string(),
1262
- value: z7.string()
1311
+ var ManifestQuestionSchema = z8.object({
1312
+ id: z8.string(),
1313
+ question: z8.string(),
1314
+ type: z8.enum(["text", "select", "multi-select", "confirm"]),
1315
+ required: z8.boolean().optional(),
1316
+ options: z8.array(
1317
+ z8.object({
1318
+ label: z8.string(),
1319
+ value: z8.string()
1263
1320
  })
1264
1321
  ).optional(),
1265
- default: z7.union([z7.string(), z7.boolean(), z7.array(z7.string())]).optional(),
1266
- help: z7.string().optional(),
1267
- dependsOn: z7.object({
1268
- questionId: z7.string(),
1269
- value: z7.union([z7.string(), z7.array(z7.string())])
1322
+ default: z8.union([z8.string(), z8.boolean(), z8.array(z8.string())]).optional(),
1323
+ help: z8.string().optional(),
1324
+ dependsOn: z8.object({
1325
+ questionId: z8.string(),
1326
+ value: z8.union([z8.string(), z8.array(z8.string())])
1270
1327
  }).optional()
1271
1328
  });
1272
1329
  function registerQuestionTools(server2) {
@@ -1274,11 +1331,13 @@ function registerQuestionTools(server2) {
1274
1331
  "stackwright_pro_present_phase_questions",
1275
1332
  "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.",
1276
1333
  {
1277
- phase: z7.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1278
- questions: z7.array(ManifestQuestionSchema).optional().describe(
1334
+ phase: z8.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
1335
+ questions: jsonCoerce(z8.array(ManifestQuestionSchema).optional()).describe(
1279
1336
  "Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
1280
1337
  ),
1281
- answers: z7.record(z7.union([z7.string(), z7.array(z7.string()), z7.boolean()])).optional().describe("Previously collected answers used to resolve dependsOn conditions")
1338
+ answers: jsonCoerce(
1339
+ z8.record(z8.string(), z8.union([z8.string(), z8.array(z8.string()), z8.boolean()])).optional()
1340
+ ).describe("Previously collected answers used to resolve dependsOn conditions")
1282
1341
  },
1283
1342
  async ({ phase, questions, answers }) => {
1284
1343
  let resolvedQuestions;
@@ -1329,17 +1388,36 @@ function registerQuestionTools(server2) {
1329
1388
  }
1330
1389
  }
1331
1390
  const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
1391
+ const labelSchema = z8.string().max(50, "Value should have at most 50 characters");
1392
+ for (const q of adapted) {
1393
+ for (const opt of q.options) {
1394
+ const check = labelSchema.safeParse(opt.label);
1395
+ if (!check.success) {
1396
+ return {
1397
+ content: [
1398
+ {
1399
+ type: "text",
1400
+ text: JSON.stringify({
1401
+ 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.`
1402
+ })
1403
+ }
1404
+ ],
1405
+ isError: true
1406
+ };
1407
+ }
1408
+ }
1409
+ }
1332
1410
  if (adapted.length === 0) {
1333
1411
  return {
1334
1412
  content: [
1335
1413
  {
1336
1414
  type: "text",
1337
- text: JSON.stringify({
1338
- phase,
1339
- skipped: true,
1340
- reason: "No questions to present (all filtered by dependsOn conditions)",
1341
- answers: []
1342
- })
1415
+ 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.`
1416
+ },
1417
+ {
1418
+ type: "text",
1419
+ // Empty array — second block always present so the foreman's two-block contract holds
1420
+ text: JSON.stringify([])
1343
1421
  }
1344
1422
  ],
1345
1423
  isError: false
@@ -1360,11 +1438,149 @@ function registerQuestionTools(server2) {
1360
1438
  };
1361
1439
  }
1362
1440
  );
1441
+ server2.tool(
1442
+ "stackwright_pro_get_next_question",
1443
+ "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.",
1444
+ { phase: z8.string().describe('Phase name e.g. "designer", "api", "auth"') },
1445
+ async ({ phase }) => {
1446
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1447
+ if (!SAFE_PHASE.test(phase)) {
1448
+ return {
1449
+ content: [
1450
+ {
1451
+ type: "text",
1452
+ text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
1453
+ }
1454
+ ],
1455
+ isError: true
1456
+ };
1457
+ }
1458
+ const cwd = process.cwd();
1459
+ const questionsPath = join(cwd, ".stackwright", "questions", `${phase}.json`);
1460
+ let questions = [];
1461
+ try {
1462
+ const raw = await readFile(questionsPath, "utf-8");
1463
+ const parsed = JSON.parse(raw);
1464
+ questions = parsed.questions ?? [];
1465
+ } catch {
1466
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1467
+ }
1468
+ if (questions.length === 0) {
1469
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1470
+ }
1471
+ const answersPath = join(cwd, ".stackwright", "answers", `${phase}.json`);
1472
+ let answeredIds = /* @__PURE__ */ new Set();
1473
+ try {
1474
+ const raw = await readFile(answersPath, "utf-8");
1475
+ const parsed = JSON.parse(raw);
1476
+ answeredIds = new Set(Object.keys(parsed.answers ?? {}));
1477
+ } catch {
1478
+ }
1479
+ const next = questions.find((q) => !answeredIds.has(q.id));
1480
+ if (!next) {
1481
+ return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
1482
+ }
1483
+ return {
1484
+ content: [
1485
+ {
1486
+ type: "text",
1487
+ text: JSON.stringify({
1488
+ done: false,
1489
+ questionId: next.id,
1490
+ question: next.question,
1491
+ type: next.type,
1492
+ options: next.options ?? null,
1493
+ help: next.help ?? null,
1494
+ index: questions.indexOf(next) + 1,
1495
+ total: questions.length
1496
+ })
1497
+ }
1498
+ ]
1499
+ };
1500
+ }
1501
+ );
1502
+ server2.tool(
1503
+ "stackwright_pro_record_answer",
1504
+ "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.",
1505
+ {
1506
+ phase: z8.string().describe('Phase name e.g. "designer"'),
1507
+ questionId: z8.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
1508
+ answer: z8.string().describe("The user's free-text answer")
1509
+ },
1510
+ async ({ phase, questionId, answer }) => {
1511
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
1512
+ if (!SAFE_PHASE.test(phase)) {
1513
+ return {
1514
+ content: [
1515
+ { type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
1516
+ ],
1517
+ isError: true
1518
+ };
1519
+ }
1520
+ if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
1521
+ return {
1522
+ content: [
1523
+ { type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
1524
+ ],
1525
+ isError: true
1526
+ };
1527
+ }
1528
+ const safeAnswer = answer.slice(0, 2e3);
1529
+ const cwd = process.cwd();
1530
+ const answersDir = join(cwd, ".stackwright", "answers");
1531
+ const answersPath = join(answersDir, `${phase}.json`);
1532
+ if (existsSync2(answersDir) && lstatSync2(answersDir).isSymbolicLink()) {
1533
+ return {
1534
+ content: [
1535
+ {
1536
+ type: "text",
1537
+ text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
1538
+ }
1539
+ ],
1540
+ isError: true
1541
+ };
1542
+ }
1543
+ mkdirSync(answersDir, { recursive: true });
1544
+ let existing = {
1545
+ version: "1.0",
1546
+ phase,
1547
+ answers: {}
1548
+ };
1549
+ try {
1550
+ if (existsSync2(answersPath)) {
1551
+ if (lstatSync2(answersPath).isSymbolicLink()) {
1552
+ return {
1553
+ content: [
1554
+ {
1555
+ type: "text",
1556
+ text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
1557
+ }
1558
+ ],
1559
+ isError: true
1560
+ };
1561
+ }
1562
+ const raw = await readFile(answersPath, "utf-8");
1563
+ existing = JSON.parse(raw);
1564
+ }
1565
+ } catch {
1566
+ }
1567
+ existing.answers[questionId] = safeAnswer;
1568
+ existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1569
+ const tmp = `${answersPath}.tmp`;
1570
+ await writeFile(tmp, JSON.stringify(existing, null, 2), "utf-8");
1571
+ renameSync(tmp, answersPath);
1572
+ return {
1573
+ content: [
1574
+ { type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
1575
+ ]
1576
+ };
1577
+ }
1578
+ );
1363
1579
  }
1364
1580
 
1365
1581
  // src/tools/orchestration.ts
1366
- import { z as z8 } from "zod";
1367
- import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, mkdirSync, lstatSync as lstatSync2 } from "fs";
1582
+ import { z as z9 } from "zod";
1583
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
1368
1584
  import { join as join2 } from "path";
1369
1585
  var OTTER_NAME_TO_PHASE = [
1370
1586
  ["designer", "designer"],
@@ -1420,9 +1636,9 @@ function handleSaveManifest(input) {
1420
1636
  const dir = join2(cwd, ".stackwright");
1421
1637
  const filePath = join2(dir, "question-manifest.json");
1422
1638
  try {
1423
- mkdirSync(dir, { recursive: true });
1424
- if (existsSync2(filePath)) {
1425
- const stat = lstatSync2(filePath);
1639
+ mkdirSync2(dir, { recursive: true });
1640
+ if (existsSync3(filePath)) {
1641
+ const stat = lstatSync3(filePath);
1426
1642
  if (stat.isSymbolicLink()) {
1427
1643
  const message = `Refusing to write to symlink: ${filePath}`;
1428
1644
  return {
@@ -1454,7 +1670,7 @@ function handleSavePhaseAnswers(input) {
1454
1670
  const dir = join2(cwd, ".stackwright", "answers");
1455
1671
  const filePath = join2(dir, `${input.phase}.json`);
1456
1672
  try {
1457
- mkdirSync(dir, { recursive: true });
1673
+ mkdirSync2(dir, { recursive: true });
1458
1674
  let answers;
1459
1675
  if (input.questions && input.questions.length > 0) {
1460
1676
  answers = answersToManifestFormat(input.rawAnswers, input.questions);
@@ -1469,8 +1685,8 @@ function handleSavePhaseAnswers(input) {
1469
1685
  completedAt: (/* @__PURE__ */ new Date()).toISOString(),
1470
1686
  answers
1471
1687
  };
1472
- if (existsSync2(filePath)) {
1473
- const stat = lstatSync2(filePath);
1688
+ if (existsSync3(filePath)) {
1689
+ const stat = lstatSync3(filePath);
1474
1690
  if (stat.isSymbolicLink()) {
1475
1691
  const message = `Refusing to write to symlink: ${filePath}`;
1476
1692
  return {
@@ -1499,7 +1715,7 @@ function handleSavePhaseAnswers(input) {
1499
1715
  function handleReadPhaseAnswers(input) {
1500
1716
  const cwd = input._cwd ?? process.cwd();
1501
1717
  const filePath = join2(cwd, ".stackwright", "answers", `${input.phase}.json`);
1502
- if (!existsSync2(filePath)) {
1718
+ if (!existsSync3(filePath)) {
1503
1719
  return {
1504
1720
  text: JSON.stringify({ missing: true, phase: input.phase }),
1505
1721
  isError: false
@@ -1531,13 +1747,63 @@ function handleGetOtterName(input) {
1531
1747
  isError: false
1532
1748
  };
1533
1749
  }
1750
+ function handleSaveBuildContext(input) {
1751
+ const cwd = input._cwd ?? process.cwd();
1752
+ const dir = join2(cwd, ".stackwright");
1753
+ const filePath = join2(dir, "build-context.json");
1754
+ try {
1755
+ mkdirSync2(dir, { recursive: true });
1756
+ if (existsSync3(filePath)) {
1757
+ const stat = lstatSync3(filePath);
1758
+ if (stat.isSymbolicLink()) {
1759
+ return {
1760
+ text: JSON.stringify({
1761
+ success: false,
1762
+ error: `Refusing to write to symlink: ${filePath}`
1763
+ }),
1764
+ isError: true
1765
+ };
1766
+ }
1767
+ }
1768
+ const payload = {
1769
+ version: "1.0",
1770
+ savedAt: (/* @__PURE__ */ new Date()).toISOString(),
1771
+ buildContext: input.buildContext
1772
+ };
1773
+ writeFileSync2(filePath, JSON.stringify(payload, null, 2) + "\n");
1774
+ return {
1775
+ text: JSON.stringify({ success: true, path: filePath }),
1776
+ isError: false
1777
+ };
1778
+ } catch (err) {
1779
+ const message = err instanceof Error ? err.message : String(err);
1780
+ return {
1781
+ text: JSON.stringify({ success: false, error: message }),
1782
+ isError: true
1783
+ };
1784
+ }
1785
+ }
1534
1786
  function registerOrchestrationTools(server2) {
1787
+ server2.tool(
1788
+ "stackwright_pro_save_build_context",
1789
+ `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.`,
1790
+ {
1791
+ buildContext: z9.string().describe("Free-text description of what the user wants to build")
1792
+ },
1793
+ async ({ buildContext }) => {
1794
+ const { text, isError } = handleSaveBuildContext({ buildContext });
1795
+ return {
1796
+ content: [{ type: "text", text }],
1797
+ isError
1798
+ };
1799
+ }
1800
+ );
1535
1801
  server2.tool(
1536
1802
  "stackwright_pro_parse_otter_response",
1537
1803
  "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.",
1538
1804
  {
1539
- otterName: z8.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1540
- responseText: z8.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1805
+ otterName: z9.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
1806
+ responseText: z9.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
1541
1807
  },
1542
1808
  async ({ otterName, responseText }) => {
1543
1809
  const { result, isError } = handleParseOtterResponse({ otterName, responseText });
@@ -1551,16 +1817,18 @@ function registerOrchestrationTools(server2) {
1551
1817
  "stackwright_pro_save_manifest",
1552
1818
  "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.",
1553
1819
  {
1554
- phases: z8.array(
1555
- z8.object({
1556
- phase: z8.string(),
1557
- otter: z8.string(),
1558
- questions: z8.array(z8.any()),
1559
- requiredPackages: z8.object({
1560
- dependencies: z8.record(z8.string(), z8.string()).optional(),
1561
- devPackages: z8.record(z8.string(), z8.string()).optional()
1562
- }).optional()
1563
- })
1820
+ phases: jsonCoerce(
1821
+ z9.array(
1822
+ z9.object({
1823
+ phase: z9.string(),
1824
+ otter: z9.string(),
1825
+ questions: z9.array(z9.any()),
1826
+ requiredPackages: z9.object({
1827
+ dependencies: z9.record(z9.string(), z9.string()).optional(),
1828
+ devPackages: z9.record(z9.string(), z9.string()).optional()
1829
+ }).optional()
1830
+ })
1831
+ )
1564
1832
  ).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
1565
1833
  },
1566
1834
  async ({ phases }) => {
@@ -1575,15 +1843,21 @@ function registerOrchestrationTools(server2) {
1575
1843
  "stackwright_pro_save_phase_answers",
1576
1844
  "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.",
1577
1845
  {
1578
- phase: z8.string().describe('Phase name, e.g. "designer"'),
1579
- rawAnswers: z8.array(
1580
- z8.object({
1581
- question_header: z8.string(),
1582
- selected_options: z8.array(z8.string()),
1583
- other_text: z8.string().nullable().optional()
1584
- })
1585
- ).describe("Answers as returned by ask_user_question"),
1586
- questions: z8.array(z8.any()).optional().describe("Original manifest questions for label\u2192value reverse-mapping")
1846
+ phase: z9.string().describe('Phase name, e.g. "designer"'),
1847
+ rawAnswers: jsonCoerce(
1848
+ z9.array(
1849
+ z9.object({
1850
+ question_header: z9.string(),
1851
+ selected_options: z9.array(z9.string()),
1852
+ other_text: z9.string().nullable().optional()
1853
+ })
1854
+ )
1855
+ ).describe(
1856
+ "Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
1857
+ ),
1858
+ questions: jsonCoerce(z9.array(z9.any()).optional()).describe(
1859
+ "Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
1860
+ )
1587
1861
  },
1588
1862
  async ({ phase, rawAnswers, questions }) => {
1589
1863
  const { text, isError } = handleSavePhaseAnswers({
@@ -1601,7 +1875,7 @@ function registerOrchestrationTools(server2) {
1601
1875
  "stackwright_pro_read_phase_answers",
1602
1876
  "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.",
1603
1877
  {
1604
- phase: z8.string().describe('Phase name, e.g. "designer"')
1878
+ phase: z9.string().describe('Phase name, e.g. "designer"')
1605
1879
  },
1606
1880
  async ({ phase }) => {
1607
1881
  const { text, isError } = handleReadPhaseAnswers({ phase });
@@ -1615,7 +1889,7 @@ function registerOrchestrationTools(server2) {
1615
1889
  "stackwright_pro_get_otter_name",
1616
1890
  "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.",
1617
1891
  {
1618
- phase: z8.string().describe('Phase name, e.g. "designer", "api", "pages"')
1892
+ phase: z9.string().describe('Phase name, e.g. "designer", "api", "pages"')
1619
1893
  },
1620
1894
  async ({ phase }) => {
1621
1895
  const { text, isError } = handleGetOtterName({ phase });
@@ -1628,28 +1902,376 @@ function registerOrchestrationTools(server2) {
1628
1902
  }
1629
1903
 
1630
1904
  // src/tools/pipeline.ts
1631
- import { z as z9 } from "zod";
1632
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync3, existsSync as existsSync3, mkdirSync as mkdirSync2, lstatSync as lstatSync3 } from "fs";
1905
+ import { z as z11 } from "zod";
1906
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4, existsSync as existsSync5, mkdirSync as mkdirSync4, lstatSync as lstatSync5 } from "fs";
1907
+ import { join as join4 } from "path";
1908
+ import { createHash as createHash3 } from "crypto";
1909
+
1910
+ // src/artifact-signing.ts
1911
+ import {
1912
+ createHash as createHash2,
1913
+ generateKeyPairSync,
1914
+ createPublicKey,
1915
+ createPrivateKey,
1916
+ sign,
1917
+ verify,
1918
+ timingSafeEqual
1919
+ } from "crypto";
1920
+ import {
1921
+ readFileSync as readFileSync3,
1922
+ writeFileSync as writeFileSync3,
1923
+ existsSync as existsSync4,
1924
+ mkdirSync as mkdirSync3,
1925
+ lstatSync as lstatSync4,
1926
+ unlinkSync,
1927
+ readdirSync
1928
+ } from "fs";
1633
1929
  import { join as join3 } from "path";
1930
+ import { z as z10 } from "zod";
1931
+ var ALGORITHM = "ECDSA-P384-SHA384";
1932
+ var KEY_FILE = "pipeline-keys.json";
1933
+ var KEY_DIR = ".stackwright";
1934
+ var SIGNATURE_MANIFEST = "signatures.json";
1935
+ var ARTIFACTS_DIR = ".stackwright/artifacts";
1936
+ function rejectSymlink(filePath, context) {
1937
+ if (!existsSync4(filePath)) return;
1938
+ const stat = lstatSync4(filePath);
1939
+ if (stat.isSymbolicLink()) {
1940
+ throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
1941
+ }
1942
+ }
1943
+ function computeSha384(data) {
1944
+ return createHash2("sha384").update(data).digest("hex");
1945
+ }
1946
+ function safeDigestEqual(a, b) {
1947
+ if (a.length !== b.length) return false;
1948
+ return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
1949
+ }
1950
+ function emptyManifest() {
1951
+ return {
1952
+ version: "1.0",
1953
+ algorithm: ALGORITHM,
1954
+ signatures: {}
1955
+ };
1956
+ }
1957
+ function initPipelineKeys(cwd) {
1958
+ const keyDir = join3(cwd, KEY_DIR);
1959
+ const keyPath = join3(keyDir, KEY_FILE);
1960
+ rejectSymlink(keyPath, "pipeline-keys.json");
1961
+ mkdirSync3(keyDir, { recursive: true });
1962
+ const { publicKey, privateKey } = generateKeyPairSync("ec", {
1963
+ namedCurve: "P-384"
1964
+ });
1965
+ const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
1966
+ const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
1967
+ const fingerprint = createHash2("sha256").update(publicKeyPem).digest("hex");
1968
+ const keyFile = {
1969
+ version: "1.0",
1970
+ algorithm: ALGORITHM,
1971
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1972
+ publicKeyPem,
1973
+ privateKeyPem
1974
+ };
1975
+ writeFileSync3(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
1976
+ return { publicKeyPem, fingerprint };
1977
+ }
1978
+ function loadPipelineKeys(cwd) {
1979
+ const keyPath = join3(cwd, KEY_DIR, KEY_FILE);
1980
+ rejectSymlink(keyPath, "pipeline-keys.json");
1981
+ if (!existsSync4(keyPath)) {
1982
+ throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
1983
+ }
1984
+ let raw;
1985
+ try {
1986
+ raw = readFileSync3(keyPath, "utf-8");
1987
+ } catch (err) {
1988
+ const msg = err instanceof Error ? err.message : String(err);
1989
+ throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
1990
+ }
1991
+ let parsed;
1992
+ try {
1993
+ parsed = JSON.parse(raw);
1994
+ } catch {
1995
+ throw new Error("Pipeline keys file is not valid JSON");
1996
+ }
1997
+ if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
1998
+ throw new Error("Invalid public key PEM in pipeline keys file");
1999
+ }
2000
+ if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
2001
+ throw new Error("Invalid private key PEM in pipeline keys file");
2002
+ }
2003
+ const publicKey = createPublicKey(parsed.publicKeyPem);
2004
+ const privateKey = createPrivateKey(parsed.privateKeyPem);
2005
+ return { privateKey, publicKey };
2006
+ }
2007
+ function signArtifact(artifactBytes, privateKey) {
2008
+ const digest = computeSha384(artifactBytes);
2009
+ const sig = sign("SHA384", artifactBytes, privateKey);
2010
+ return {
2011
+ digest,
2012
+ signature: sig.toString("base64"),
2013
+ algorithm: ALGORITHM,
2014
+ signedAt: (/* @__PURE__ */ new Date()).toISOString()
2015
+ };
2016
+ }
2017
+ function verifyArtifact(artifactBytes, signature, publicKey) {
2018
+ const sigValid = verify(
2019
+ "SHA384",
2020
+ artifactBytes,
2021
+ publicKey,
2022
+ Buffer.from(signature.signature, "base64")
2023
+ );
2024
+ if (!sigValid) return false;
2025
+ const actualDigest = computeSha384(artifactBytes);
2026
+ return safeDigestEqual(actualDigest, signature.digest);
2027
+ }
2028
+ function loadSignatureManifest(cwd) {
2029
+ const manifestPath = join3(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
2030
+ rejectSymlink(manifestPath, "signatures.json");
2031
+ if (!existsSync4(manifestPath)) return emptyManifest();
2032
+ let raw;
2033
+ try {
2034
+ raw = readFileSync3(manifestPath, "utf-8");
2035
+ } catch (err) {
2036
+ const msg = err instanceof Error ? err.message : String(err);
2037
+ throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
2038
+ }
2039
+ try {
2040
+ const parsed = JSON.parse(raw);
2041
+ if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
2042
+ throw new Error("Malformed signature manifest: invalid version or missing signatures object");
2043
+ }
2044
+ return parsed;
2045
+ } catch (err) {
2046
+ if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
2047
+ throw new Error("Signature manifest is not valid JSON", { cause: err });
2048
+ }
2049
+ }
2050
+ function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
2051
+ const artifactsDir = join3(cwd, ARTIFACTS_DIR);
2052
+ const manifestPath = join3(artifactsDir, SIGNATURE_MANIFEST);
2053
+ rejectSymlink(manifestPath, "signatures.json (save)");
2054
+ mkdirSync3(artifactsDir, { recursive: true });
2055
+ const manifest = loadSignatureManifest(cwd);
2056
+ manifest.signatures[artifactFilename] = {
2057
+ ...sig,
2058
+ signedBy: signerOtter
2059
+ };
2060
+ writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
2061
+ }
2062
+ function getArtifactSignature(cwd, artifactFilename) {
2063
+ const manifest = loadSignatureManifest(cwd);
2064
+ const entry = manifest.signatures[artifactFilename];
2065
+ if (!entry) return null;
2066
+ return {
2067
+ digest: entry.digest,
2068
+ signature: entry.signature,
2069
+ algorithm: entry.algorithm,
2070
+ signedAt: entry.signedAt
2071
+ };
2072
+ }
2073
+ function emitSignatureAuditEvent(params) {
2074
+ const record = JSON.stringify({
2075
+ level: "AUDIT",
2076
+ event: "ARTIFACT_SIGNATURE_FAIL",
2077
+ timestamp: params.timestamp,
2078
+ source: params.source,
2079
+ artifactFilename: params.artifactFilename,
2080
+ expectedDigest: params.expectedDigest,
2081
+ actualDigest: params.actualDigest,
2082
+ phase: params.phase
2083
+ });
2084
+ process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
2085
+ `);
2086
+ }
2087
+ function registerArtifactSigningTools(server2) {
2088
+ server2.tool(
2089
+ "stackwright_pro_verify_artifact_signatures",
2090
+ "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.",
2091
+ {
2092
+ cwd: z10.string().optional().describe("Project root directory. Defaults to process.cwd().")
2093
+ },
2094
+ async ({ cwd: cwdParam }) => {
2095
+ const cwd = cwdParam ?? process.cwd();
2096
+ let publicKey;
2097
+ try {
2098
+ const keys = loadPipelineKeys(cwd);
2099
+ publicKey = keys.publicKey;
2100
+ } catch (err) {
2101
+ const msg = err instanceof Error ? err.message : String(err);
2102
+ return {
2103
+ content: [
2104
+ {
2105
+ type: "text",
2106
+ text: JSON.stringify({
2107
+ error: true,
2108
+ message: `Cannot load pipeline keys: ${msg}`
2109
+ })
2110
+ }
2111
+ ],
2112
+ isError: true
2113
+ };
2114
+ }
2115
+ let manifest;
2116
+ try {
2117
+ manifest = loadSignatureManifest(cwd);
2118
+ } catch (err) {
2119
+ const msg = err instanceof Error ? err.message : String(err);
2120
+ return {
2121
+ content: [
2122
+ {
2123
+ type: "text",
2124
+ text: JSON.stringify({
2125
+ error: true,
2126
+ message: `Cannot load signature manifest: ${msg}`
2127
+ })
2128
+ }
2129
+ ],
2130
+ isError: true
2131
+ };
2132
+ }
2133
+ const artifactsPath = join3(cwd, ARTIFACTS_DIR);
2134
+ let artifactFiles = [];
2135
+ try {
2136
+ if (existsSync4(artifactsPath)) {
2137
+ artifactFiles = readdirSync(artifactsPath).filter(
2138
+ (f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
2139
+ );
2140
+ }
2141
+ } catch {
2142
+ }
2143
+ const results = [];
2144
+ let hasFailure = false;
2145
+ for (const filename of artifactFiles) {
2146
+ const filePath = join3(artifactsPath, filename);
2147
+ try {
2148
+ rejectSymlink(filePath, `artifact ${filename}`);
2149
+ } catch {
2150
+ results.push({
2151
+ filename,
2152
+ verified: false,
2153
+ error: "Refusing to verify symlink"
2154
+ });
2155
+ hasFailure = true;
2156
+ continue;
2157
+ }
2158
+ let artifactBytes;
2159
+ try {
2160
+ artifactBytes = readFileSync3(filePath);
2161
+ } catch (err) {
2162
+ const msg = err instanceof Error ? err.message : String(err);
2163
+ results.push({
2164
+ filename,
2165
+ verified: false,
2166
+ error: `Cannot read artifact: ${msg}`
2167
+ });
2168
+ hasFailure = true;
2169
+ continue;
2170
+ }
2171
+ const entry = manifest.signatures[filename];
2172
+ if (!entry) {
2173
+ results.push({
2174
+ filename,
2175
+ verified: false,
2176
+ error: "No signature found in manifest"
2177
+ });
2178
+ hasFailure = true;
2179
+ continue;
2180
+ }
2181
+ const sig = {
2182
+ digest: entry.digest,
2183
+ signature: entry.signature,
2184
+ algorithm: entry.algorithm,
2185
+ signedAt: entry.signedAt
2186
+ };
2187
+ const verified = verifyArtifact(artifactBytes, sig, publicKey);
2188
+ if (!verified) {
2189
+ const actualDigest = computeSha384(artifactBytes);
2190
+ emitSignatureAuditEvent({
2191
+ artifactFilename: filename,
2192
+ expectedDigest: sig.digest,
2193
+ actualDigest,
2194
+ phase: entry.signedBy ?? "unknown",
2195
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2196
+ source: "stackwright_pro_verify_artifact_signatures"
2197
+ });
2198
+ results.push({
2199
+ filename,
2200
+ verified: false,
2201
+ error: `Signature verification failed \u2014 artifact may have been tampered with`,
2202
+ signedBy: entry.signedBy,
2203
+ signedAt: entry.signedAt
2204
+ });
2205
+ hasFailure = true;
2206
+ } else {
2207
+ results.push({
2208
+ filename,
2209
+ verified: true,
2210
+ signedBy: entry.signedBy,
2211
+ signedAt: entry.signedAt
2212
+ });
2213
+ }
2214
+ }
2215
+ for (const manifestFilename of Object.keys(manifest.signatures)) {
2216
+ if (!artifactFiles.includes(manifestFilename)) {
2217
+ results.push({
2218
+ filename: manifestFilename,
2219
+ verified: false,
2220
+ error: "Artifact referenced in manifest but missing from disk"
2221
+ });
2222
+ hasFailure = true;
2223
+ }
2224
+ }
2225
+ const verifiedCount = results.filter((r) => r.verified).length;
2226
+ const failedCount = results.filter((r) => !r.verified).length;
2227
+ return {
2228
+ content: [
2229
+ {
2230
+ type: "text",
2231
+ text: JSON.stringify({
2232
+ totalArtifacts: artifactFiles.length,
2233
+ verifiedCount,
2234
+ failedCount,
2235
+ results,
2236
+ ...hasFailure ? {
2237
+ error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
2238
+ } : {}
2239
+ })
2240
+ }
2241
+ ],
2242
+ isError: hasFailure
2243
+ };
2244
+ }
2245
+ );
2246
+ }
2247
+
2248
+ // src/tools/pipeline.ts
2249
+ import { WorkflowFileSchema, authConfigSchema } from "@stackwright-pro/types";
1634
2250
  var PHASE_ORDER = [
1635
2251
  "designer",
1636
2252
  "theme",
1637
2253
  "api",
1638
- "auth",
1639
2254
  "data",
2255
+ "workflow",
1640
2256
  "pages",
1641
2257
  "dashboard",
1642
- "workflow"
2258
+ "auth"
1643
2259
  ];
1644
2260
  var PHASE_DEPENDENCIES = {
1645
2261
  designer: [],
1646
2262
  theme: ["designer"],
1647
2263
  api: [],
1648
- auth: [],
1649
2264
  data: ["api"],
1650
- pages: ["designer", "theme", "api", "data", "auth"],
1651
- dashboard: ["designer", "theme", "api", "data"],
1652
- workflow: ["auth"]
2265
+ // workflow: no hard deps uses service: references with Prism mock fallback
2266
+ // when API artifacts aren't available; roles come from user answers
2267
+ workflow: [],
2268
+ // pages: 'api' is transitive through 'data'; auth removed — page-otter has
2269
+ // graceful fallback and reads role names from workflow artifacts instead
2270
+ pages: ["designer", "theme", "data"],
2271
+ dashboard: ["designer", "theme", "data"],
2272
+ // auth is the terminal phase — reads all available artifacts at runtime;
2273
+ // no hard upstream requirements so it always runs and never gets skipped
2274
+ auth: []
1653
2275
  };
1654
2276
  var PHASE_ARTIFACT = {
1655
2277
  designer: "design-language.json",
@@ -1699,11 +2321,11 @@ function createDefaultState() {
1699
2321
  };
1700
2322
  }
1701
2323
  function statePath(cwd) {
1702
- return join3(cwd, ".stackwright", "pipeline-state.json");
2324
+ return join4(cwd, ".stackwright", "pipeline-state.json");
1703
2325
  }
1704
2326
  function readState(cwd) {
1705
2327
  const p = statePath(cwd);
1706
- if (!existsSync3(p)) return createDefaultState();
2328
+ if (!existsSync5(p)) return createDefaultState();
1707
2329
  const raw = JSON.parse(safeReadSync(p));
1708
2330
  if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
1709
2331
  return createDefaultState();
@@ -1711,26 +2333,26 @@ function readState(cwd) {
1711
2333
  return raw;
1712
2334
  }
1713
2335
  function safeWriteSync(filePath, content) {
1714
- if (existsSync3(filePath)) {
1715
- const stat = lstatSync3(filePath);
2336
+ if (existsSync5(filePath)) {
2337
+ const stat = lstatSync5(filePath);
1716
2338
  if (stat.isSymbolicLink()) {
1717
2339
  throw new Error(`Refusing to write to symlink: ${filePath}`);
1718
2340
  }
1719
2341
  }
1720
- writeFileSync3(filePath, content);
2342
+ writeFileSync4(filePath, content);
1721
2343
  }
1722
2344
  function safeReadSync(filePath) {
1723
- if (existsSync3(filePath)) {
1724
- const stat = lstatSync3(filePath);
2345
+ if (existsSync5(filePath)) {
2346
+ const stat = lstatSync5(filePath);
1725
2347
  if (stat.isSymbolicLink()) {
1726
2348
  throw new Error(`Refusing to read symlink: ${filePath}`);
1727
2349
  }
1728
2350
  }
1729
- return readFileSync3(filePath, "utf-8");
2351
+ return readFileSync4(filePath, "utf-8");
1730
2352
  }
1731
2353
  function writeState(cwd, state) {
1732
- const dir = join3(cwd, ".stackwright");
1733
- mkdirSync2(dir, { recursive: true });
2354
+ const dir = join4(cwd, ".stackwright");
2355
+ mkdirSync4(dir, { recursive: true });
1734
2356
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1735
2357
  safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
1736
2358
  }
@@ -1751,6 +2373,15 @@ function handleGetPipelineState(_cwd) {
1751
2373
  const cwd = _cwd ?? process.cwd();
1752
2374
  try {
1753
2375
  const state = readState(cwd);
2376
+ const keyPath = join4(cwd, ".stackwright", "pipeline-keys.json");
2377
+ if (!existsSync5(keyPath)) {
2378
+ try {
2379
+ const { fingerprint } = initPipelineKeys(cwd);
2380
+ state["signingKeyFingerprint"] = fingerprint;
2381
+ writeState(cwd, state);
2382
+ } catch {
2383
+ }
2384
+ }
1754
2385
  return { text: JSON.stringify(state), isError: false };
1755
2386
  } catch (err) {
1756
2387
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
@@ -1802,28 +2433,62 @@ function handleSetPipelineState(input) {
1802
2433
  return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
1803
2434
  }
1804
2435
  }
1805
- function handleCheckExecutionReady(_cwd) {
2436
+ function handleCheckExecutionReady(_cwd, phase) {
1806
2437
  const cwd = _cwd ?? process.cwd();
2438
+ if (phase) {
2439
+ if (!isValidPhase(phase)) {
2440
+ return {
2441
+ text: JSON.stringify({
2442
+ error: true,
2443
+ message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
2444
+ }),
2445
+ isError: true
2446
+ };
2447
+ }
2448
+ const answerFile = join4(cwd, ".stackwright", "answers", `${phase}.json`);
2449
+ if (!existsSync5(answerFile)) {
2450
+ return {
2451
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
2452
+ isError: false
2453
+ };
2454
+ }
2455
+ try {
2456
+ const raw = safeReadSync(answerFile);
2457
+ const parsed = JSON.parse(raw);
2458
+ if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
2459
+ return {
2460
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
2461
+ isError: false
2462
+ };
2463
+ }
2464
+ return { text: JSON.stringify({ ready: true, phase }), isError: false };
2465
+ } catch {
2466
+ return {
2467
+ text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
2468
+ isError: false
2469
+ };
2470
+ }
2471
+ }
1807
2472
  try {
1808
- const answersDir = join3(cwd, ".stackwright", "answers");
2473
+ const answersDir = join4(cwd, ".stackwright", "answers");
1809
2474
  const answeredPhases = [];
1810
2475
  const missingPhases = [];
1811
- for (const phase of PHASE_ORDER) {
1812
- const answerFile = join3(answersDir, `${phase}.json`);
1813
- if (existsSync3(answerFile)) {
2476
+ for (const phase2 of PHASE_ORDER) {
2477
+ const answerFile = join4(answersDir, `${phase2}.json`);
2478
+ if (existsSync5(answerFile)) {
1814
2479
  try {
1815
2480
  const raw = safeReadSync(answerFile);
1816
2481
  const parsed = JSON.parse(raw);
1817
2482
  if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
1818
- missingPhases.push(phase);
2483
+ missingPhases.push(phase2);
1819
2484
  continue;
1820
2485
  }
1821
- answeredPhases.push(phase);
2486
+ answeredPhases.push(phase2);
1822
2487
  } catch {
1823
- missingPhases.push(phase);
2488
+ missingPhases.push(phase2);
1824
2489
  }
1825
2490
  } else {
1826
- missingPhases.push(phase);
2491
+ missingPhases.push(phase2);
1827
2492
  }
1828
2493
  }
1829
2494
  return {
@@ -1842,15 +2507,35 @@ function handleCheckExecutionReady(_cwd) {
1842
2507
  function handleListArtifacts(_cwd) {
1843
2508
  const cwd = _cwd ?? process.cwd();
1844
2509
  try {
1845
- const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2510
+ const artifactsDir = join4(cwd, ".stackwright", "artifacts");
2511
+ let manifest = null;
2512
+ try {
2513
+ manifest = loadSignatureManifest(cwd);
2514
+ } catch {
2515
+ }
1846
2516
  const artifacts = [];
1847
2517
  let completedCount = 0;
1848
2518
  for (const phase of PHASE_ORDER) {
1849
2519
  const expectedFile = PHASE_ARTIFACT[phase];
1850
- const fullPath = join3(artifactsDir, expectedFile);
1851
- const exists = existsSync3(fullPath);
2520
+ const fullPath = join4(artifactsDir, expectedFile);
2521
+ const exists = existsSync5(fullPath);
1852
2522
  if (exists) completedCount++;
1853
- artifacts.push({ phase, expectedFile, exists, path: fullPath });
2523
+ let signed = false;
2524
+ let signatureValid = null;
2525
+ if (exists && manifest) {
2526
+ const entry = manifest.signatures[expectedFile];
2527
+ if (entry) {
2528
+ signed = true;
2529
+ try {
2530
+ const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
2531
+ const { publicKey } = loadPipelineKeys(cwd);
2532
+ signatureValid = verifyArtifact(rawBytes, entry, publicKey);
2533
+ } catch {
2534
+ signatureValid = null;
2535
+ }
2536
+ }
2537
+ }
2538
+ artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
1854
2539
  }
1855
2540
  return {
1856
2541
  text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
@@ -1886,9 +2571,9 @@ function handleWritePhaseQuestions(input) {
1886
2571
  }
1887
2572
  } catch {
1888
2573
  }
1889
- const questionsDir = join3(cwd, ".stackwright", "questions");
1890
- mkdirSync2(questionsDir, { recursive: true });
1891
- const filePath = join3(questionsDir, `${phase}.json`);
2574
+ const questionsDir = join4(cwd, ".stackwright", "questions");
2575
+ mkdirSync4(questionsDir, { recursive: true });
2576
+ const filePath = join4(questionsDir, `${phase}.json`);
1892
2577
  const payload = {
1893
2578
  version: "1.0",
1894
2579
  phase,
@@ -1928,36 +2613,81 @@ function handleBuildSpecialistPrompt(input) {
1928
2613
  };
1929
2614
  }
1930
2615
  try {
1931
- const answersPath = join3(cwd, ".stackwright", "answers", `${phase}.json`);
2616
+ const answersPath = join4(cwd, ".stackwright", "answers", `${phase}.json`);
1932
2617
  let answers = {};
1933
- if (existsSync3(answersPath)) {
2618
+ if (existsSync5(answersPath)) {
1934
2619
  answers = JSON.parse(safeReadSync(answersPath));
1935
2620
  }
2621
+ let buildContextText = "";
2622
+ const buildContextPath = join4(cwd, ".stackwright", "build-context.json");
2623
+ if (existsSync5(buildContextPath)) {
2624
+ try {
2625
+ const bcRaw = JSON.parse(safeReadSync(buildContextPath));
2626
+ if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
2627
+ buildContextText = bcRaw.buildContext.trim();
2628
+ }
2629
+ } catch {
2630
+ }
2631
+ }
1936
2632
  const deps = PHASE_DEPENDENCIES[phase];
1937
2633
  const artifactSections = [];
1938
2634
  const missingDependencies = [];
1939
2635
  for (const dep of deps) {
1940
2636
  const artifactFile = PHASE_ARTIFACT[dep];
1941
- const artifactPath = join3(cwd, ".stackwright", "artifacts", artifactFile);
1942
- if (existsSync3(artifactPath)) {
1943
- const content = JSON.parse(safeReadSync(artifactPath));
2637
+ const artifactPath = join4(cwd, ".stackwright", "artifacts", artifactFile);
2638
+ if (existsSync5(artifactPath)) {
2639
+ const rawContent = safeReadSync(artifactPath);
2640
+ const rawBytes = Buffer.from(rawContent, "utf-8");
2641
+ const content = JSON.parse(rawContent);
2642
+ let signatureVerified = false;
2643
+ let signatureAvailable = false;
2644
+ try {
2645
+ const sig = getArtifactSignature(cwd, artifactFile);
2646
+ if (sig) {
2647
+ signatureAvailable = true;
2648
+ const { publicKey } = loadPipelineKeys(cwd);
2649
+ signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
2650
+ if (!signatureVerified) {
2651
+ const actualDigest = createHash3("sha384").update(rawBytes).digest("hex");
2652
+ emitSignatureAuditEvent({
2653
+ artifactFilename: artifactFile,
2654
+ expectedDigest: sig.digest,
2655
+ actualDigest,
2656
+ phase: dep,
2657
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2658
+ source: "stackwright_pro_build_specialist_prompt"
2659
+ });
2660
+ missingDependencies.push(dep);
2661
+ artifactSections.push(
2662
+ `[${artifactFile}]:
2663
+ (integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
2664
+ );
2665
+ continue;
2666
+ }
2667
+ }
2668
+ } catch {
2669
+ }
1944
2670
  const expectedOtter = PHASE_TO_OTTER2[dep];
1945
2671
  const artifactOtter = content["generatedBy"];
2672
+ const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
1946
2673
  if (!artifactOtter) {
1947
2674
  missingDependencies.push(dep);
1948
2675
  artifactSections.push(
1949
2676
  `[${artifactFile}]:
1950
2677
  (integrity check failed: missing generatedBy field)`
1951
2678
  );
1952
- } else if (artifactOtter !== expectedOtter) {
2679
+ } else if (normalizedOtter !== expectedOtter) {
1953
2680
  missingDependencies.push(dep);
1954
2681
  artifactSections.push(
1955
2682
  `[${artifactFile}]:
1956
2683
  (integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
1957
2684
  );
1958
2685
  } else {
1959
- artifactSections.push(`[${artifactFile}]:
1960
- ${JSON.stringify(content, null, 2)}`);
2686
+ const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
2687
+ artifactSections.push(
2688
+ `[${artifactFile}]${sigStatus}:
2689
+ ${JSON.stringify(content, null, 2)}`
2690
+ );
1961
2691
  }
1962
2692
  } else {
1963
2693
  missingDependencies.push(dep);
@@ -1965,10 +2695,17 @@ ${JSON.stringify(content, null, 2)}`);
1965
2695
  (not yet available)`);
1966
2696
  }
1967
2697
  }
1968
- const parts = ["ANSWERS:", JSON.stringify(answers, null, 2)];
2698
+ const parts = [];
2699
+ if (buildContextText) {
2700
+ parts.push("BUILD_CONTEXT:", buildContextText, "");
2701
+ }
2702
+ parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
1969
2703
  if (artifactSections.length > 0) {
1970
2704
  parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
1971
2705
  }
2706
+ const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
2707
+ parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
2708
+ parts.push(artifactSchema);
1972
2709
  parts.push("", "Execute using these answers and the upstream artifacts provided.");
1973
2710
  const prompt = parts.join("\n");
1974
2711
  const dependenciesSatisfied = missingDependencies.length === 0;
@@ -2003,45 +2740,231 @@ var OFF_SCRIPT_PATTERNS = [
2003
2740
  var PHASE_REQUIRED_KEYS = {
2004
2741
  designer: ["designLanguage", "themeTokenSeeds"],
2005
2742
  theme: ["tokens"],
2006
- api: ["entities"],
2743
+ api: ["entities", "version", "generatedBy"],
2007
2744
  auth: ["version", "generatedBy"],
2008
- data: ["version", "generatedBy"],
2745
+ data: ["version", "generatedBy", "strategy", "collections"],
2009
2746
  pages: ["version", "generatedBy"],
2010
2747
  dashboard: ["version", "generatedBy"],
2011
2748
  workflow: ["version", "generatedBy"]
2012
2749
  };
2750
+ var PHASE_ARTIFACT_SCHEMA = {
2751
+ designer: JSON.stringify(
2752
+ {
2753
+ version: "1.0",
2754
+ generatedBy: "stackwright-pro-designer-otter",
2755
+ application: {
2756
+ type: "<operational|data-explorer|admin|logistics|general>",
2757
+ environment: "<workstation|field|control-room|mixed>",
2758
+ density: "<compact|balanced|spacious>",
2759
+ accessibility: "<wcag-aa|section-508|none>",
2760
+ colorScheme: "<light|dark|both>"
2761
+ },
2762
+ designLanguage: {
2763
+ rationale: "<design rationale>",
2764
+ spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
2765
+ colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
2766
+ typography: {
2767
+ dataFont: "Inter",
2768
+ headingFont: "Inter",
2769
+ monoFont: "monospace",
2770
+ dataSizePx: 12,
2771
+ bodySizePx: 14
2772
+ },
2773
+ contrastRatio: "4.5",
2774
+ borderRadius: "4",
2775
+ shadowElevation: "standard"
2776
+ },
2777
+ themeTokenSeeds: {
2778
+ light: {
2779
+ background: "#ffffff",
2780
+ foreground: "#1a1a1a",
2781
+ primary: "#1a365d",
2782
+ surface: "#f7f7f7",
2783
+ border: "#e2e8f0"
2784
+ },
2785
+ dark: {
2786
+ background: "#1a1a1a",
2787
+ foreground: "#ffffff",
2788
+ primary: "#90cdf4",
2789
+ surface: "#2d2d2d",
2790
+ border: "#4a5568"
2791
+ }
2792
+ },
2793
+ conformsTo: null,
2794
+ operationalNotes: []
2795
+ },
2796
+ null,
2797
+ 2
2798
+ ),
2799
+ theme: JSON.stringify(
2800
+ {
2801
+ version: "1.0",
2802
+ generatedBy: "stackwright-pro-theme-otter",
2803
+ componentLibrary: "shadcn",
2804
+ colorScheme: "<light|dark|both>",
2805
+ tokens: {
2806
+ colors: { "primary-500": "#1a365d", background: "#ffffff" },
2807
+ spacing: { "spacing-1": "8px", "spacing-2": "16px" },
2808
+ typography: { "font-data": "Inter", "text-sm": "12px" },
2809
+ shape: { "radius-sm": "4px", "radius-md": "8px" },
2810
+ shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
2811
+ },
2812
+ cssVariables: {
2813
+ "--background": "0 0% 100%",
2814
+ "--foreground": "222.2 84% 4.9%",
2815
+ "--primary": "222.2 47.4% 11.2%",
2816
+ "--primary-foreground": "210 40% 98%",
2817
+ "--surface": "210 40% 98%",
2818
+ "--border": "214.3 31.8% 91.4%"
2819
+ },
2820
+ dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
2821
+ },
2822
+ null,
2823
+ 2
2824
+ ),
2825
+ api: JSON.stringify(
2826
+ {
2827
+ version: "1.0",
2828
+ generatedBy: "stackwright-pro-api-otter",
2829
+ entities: [
2830
+ {
2831
+ name: "Shipment",
2832
+ endpoint: "/shipments",
2833
+ method: "GET",
2834
+ revalidate: 60,
2835
+ mutationType: null
2836
+ }
2837
+ ],
2838
+ auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
2839
+ baseUrl: "https://api.example.mil/v2",
2840
+ specPath: "./specs/api.yaml"
2841
+ },
2842
+ null,
2843
+ 2
2844
+ ),
2845
+ data: JSON.stringify(
2846
+ {
2847
+ version: "1.0",
2848
+ generatedBy: "stackwright-pro-data-otter",
2849
+ strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
2850
+ pulseMode: false,
2851
+ collections: [{ name: "equipment", revalidate: 60, pulse: false }],
2852
+ endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
2853
+ requiredPackages: { dependencies: {}, devPackages: {} }
2854
+ },
2855
+ null,
2856
+ 2
2857
+ ),
2858
+ workflow: JSON.stringify(
2859
+ {
2860
+ version: "1.0",
2861
+ generatedBy: "stackwright-pro-workflow-otter",
2862
+ workflowConfig: {
2863
+ id: "procurement-approval",
2864
+ route: "/procurement",
2865
+ files: ["workflows/procurement-approval.yml"],
2866
+ serviceDependencies: ["service:workflow-state"],
2867
+ warnings: []
2868
+ }
2869
+ },
2870
+ null,
2871
+ 2
2872
+ ),
2873
+ pages: JSON.stringify(
2874
+ {
2875
+ version: "1.0",
2876
+ generatedBy: "stackwright-pro-page-otter",
2877
+ pages: [
2878
+ {
2879
+ slug: "catalog",
2880
+ type: "collection_listing",
2881
+ collection: "products",
2882
+ themeApplied: true,
2883
+ authRequired: false
2884
+ },
2885
+ {
2886
+ slug: "admin",
2887
+ type: "protected",
2888
+ collection: null,
2889
+ themeApplied: true,
2890
+ authRequired: true
2891
+ }
2892
+ ]
2893
+ },
2894
+ null,
2895
+ 2
2896
+ ),
2897
+ dashboard: JSON.stringify(
2898
+ {
2899
+ version: "1.0",
2900
+ generatedBy: "stackwright-pro-dashboard-otter",
2901
+ pages: [
2902
+ {
2903
+ slug: "dashboard",
2904
+ layout: "<grid|table|mixed>",
2905
+ collections: ["equipment", "supplies"],
2906
+ mode: "<ISR|Pulse>"
2907
+ }
2908
+ ]
2909
+ },
2910
+ null,
2911
+ 2
2912
+ ),
2913
+ auth: JSON.stringify(
2914
+ {
2915
+ version: "1.0",
2916
+ generatedBy: "stackwright-pro-auth-otter",
2917
+ authConfig: {
2918
+ method: "<cac|oidc|oauth2|none>",
2919
+ provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
2920
+ rbacRoles: ["ADMIN", "ANALYST"],
2921
+ rbacDefaultRole: "ANALYST",
2922
+ protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
2923
+ auditEnabled: true,
2924
+ auditRetentionDays: 90
2925
+ }
2926
+ },
2927
+ null,
2928
+ 2
2929
+ )
2930
+ };
2013
2931
  function handleValidateArtifact(input) {
2014
2932
  const cwd = input._cwd ?? process.cwd();
2015
- const { phase, responseText } = input;
2933
+ const { phase, responseText, artifact: directArtifact } = input;
2016
2934
  if (!isValidPhase(phase)) {
2017
2935
  return {
2018
2936
  text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
2019
2937
  isError: true
2020
2938
  };
2021
2939
  }
2022
- for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2023
- if (pattern.test(responseText)) {
2940
+ let artifact;
2941
+ if (directArtifact) {
2942
+ artifact = directArtifact;
2943
+ } else {
2944
+ const text = responseText ?? "";
2945
+ for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
2946
+ if (pattern.test(text)) {
2947
+ const result = {
2948
+ valid: false,
2949
+ phase,
2950
+ violation: "off-script",
2951
+ retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2952
+ };
2953
+ return { text: JSON.stringify(result), isError: false };
2954
+ }
2955
+ }
2956
+ try {
2957
+ artifact = extractJsonFromResponse(text);
2958
+ } catch {
2024
2959
  const result = {
2025
2960
  valid: false,
2026
2961
  phase,
2027
- violation: "off-script",
2028
- retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
2962
+ violation: "invalid-json",
2963
+ retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
2029
2964
  };
2030
2965
  return { text: JSON.stringify(result), isError: false };
2031
2966
  }
2032
2967
  }
2033
- let artifact;
2034
- try {
2035
- artifact = extractJsonFromResponse(responseText);
2036
- } catch {
2037
- const result = {
2038
- valid: false,
2039
- phase,
2040
- violation: "invalid-json",
2041
- retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
2042
- };
2043
- return { text: JSON.stringify(result), isError: false };
2044
- }
2045
2968
  if (!artifact.version || !artifact.generatedBy) {
2046
2969
  const result = {
2047
2970
  valid: false,
@@ -2062,12 +2985,58 @@ function handleValidateArtifact(input) {
2062
2985
  };
2063
2986
  return { text: JSON.stringify(result), isError: false };
2064
2987
  }
2988
+ const PHASE_ZOD_VALIDATORS = {
2989
+ workflow: (artifact2) => {
2990
+ const workflowConfig = artifact2["workflowConfig"];
2991
+ if (!workflowConfig) return { success: true };
2992
+ const result = WorkflowFileSchema.safeParse(workflowConfig);
2993
+ if (!result.success) {
2994
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
2995
+ return { success: false, error: { message: issues } };
2996
+ }
2997
+ return { success: true };
2998
+ },
2999
+ auth: (artifact2) => {
3000
+ const authConfig = artifact2["authConfig"];
3001
+ if (!authConfig) return { success: true };
3002
+ const result = authConfigSchema.safeParse(authConfig);
3003
+ if (!result.success) {
3004
+ const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
3005
+ return { success: false, error: { message: issues } };
3006
+ }
3007
+ return { success: true };
3008
+ }
3009
+ };
3010
+ const zodValidator = PHASE_ZOD_VALIDATORS[phase];
3011
+ if (zodValidator) {
3012
+ const zodResult = zodValidator(artifact);
3013
+ if (!zodResult.success) {
3014
+ const result = {
3015
+ valid: false,
3016
+ phase,
3017
+ violation: "schema-mismatch",
3018
+ retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
3019
+ };
3020
+ return { text: JSON.stringify(result), isError: false };
3021
+ }
3022
+ }
2065
3023
  try {
2066
- const artifactsDir = join3(cwd, ".stackwright", "artifacts");
2067
- mkdirSync2(artifactsDir, { recursive: true });
3024
+ const artifactsDir = join4(cwd, ".stackwright", "artifacts");
3025
+ mkdirSync4(artifactsDir, { recursive: true });
2068
3026
  const artifactFile = PHASE_ARTIFACT[phase];
2069
- const artifactPath = join3(artifactsDir, artifactFile);
2070
- safeWriteSync(artifactPath, JSON.stringify(artifact, null, 2) + "\n");
3027
+ const artifactPath = join4(artifactsDir, artifactFile);
3028
+ const serialized = JSON.stringify(artifact, null, 2) + "\n";
3029
+ const artifactBytes = Buffer.from(serialized, "utf-8");
3030
+ safeWriteSync(artifactPath, serialized);
3031
+ let signed = false;
3032
+ try {
3033
+ const { privateKey } = loadPipelineKeys(cwd);
3034
+ const sig = signArtifact(artifactBytes, privateKey);
3035
+ const signerOtter = PHASE_TO_OTTER2[phase];
3036
+ saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
3037
+ signed = true;
3038
+ } catch {
3039
+ }
2071
3040
  const state = readState(cwd);
2072
3041
  if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
2073
3042
  const ps = state.phases[phase];
@@ -2078,7 +3047,7 @@ function handleValidateArtifact(input) {
2078
3047
  valid: true,
2079
3048
  phase,
2080
3049
  artifactPath,
2081
- summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
3050
+ summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
2082
3051
  };
2083
3052
  return { text: JSON.stringify(result), isError: false };
2084
3053
  } catch (err) {
@@ -2102,11 +3071,15 @@ function registerPipelineTools(server2) {
2102
3071
  "stackwright_pro_set_pipeline_state",
2103
3072
  `Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
2104
3073
  {
2105
- phase: z9.string().optional().describe('Phase to update, e.g. "designer"'),
2106
- field: z9.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
2107
- value: z9.boolean().optional().describe("Value for the field"),
2108
- status: z9.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
2109
- incrementRetry: z9.boolean().optional().describe("Bump retryCount by 1")
3074
+ phase: z11.string().optional().describe('Phase to update, e.g. "designer"'),
3075
+ field: z11.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
3076
+ value: boolCoerce(z11.boolean().optional()).describe(
3077
+ 'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
3078
+ ),
3079
+ status: z11.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
3080
+ incrementRetry: boolCoerce(z11.boolean().optional()).describe(
3081
+ "Bump retryCount by 1 \u2014 must be a JSON boolean"
3082
+ )
2110
3083
  },
2111
3084
  async (args) => res(
2112
3085
  handleSetPipelineState({
@@ -2120,9 +3093,11 @@ function registerPipelineTools(server2) {
2120
3093
  );
2121
3094
  server2.tool(
2122
3095
  "stackwright_pro_check_execution_ready",
2123
- `Check all phases have answer files in .stackwright/answers/. ${DESC}`,
2124
- {},
2125
- async () => res(handleCheckExecutionReady())
3096
+ `Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
3097
+ {
3098
+ phase: z11.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
3099
+ },
3100
+ async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
2126
3101
  );
2127
3102
  server2.tool(
2128
3103
  "stackwright_pro_list_artifacts",
@@ -2132,34 +3107,81 @@ function registerPipelineTools(server2) {
2132
3107
  );
2133
3108
  server2.tool(
2134
3109
  "stackwright_pro_write_phase_questions",
2135
- `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
3110
+ `Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
2136
3111
  {
2137
- phase: z9.string().describe('Phase name, e.g. "designer"'),
2138
- responseText: z9.string().describe("Raw LLM response from QUESTION_COLLECTION_MODE")
3112
+ phase: z11.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
3113
+ responseText: z11.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
3114
+ questions: jsonCoerce(z11.array(z11.any()).optional()).describe(
3115
+ "Questions array for direct specialist write"
3116
+ )
2139
3117
  },
2140
- async ({ phase, responseText }) => res(handleWritePhaseQuestions({ phase, responseText }))
3118
+ async ({ phase, responseText, questions }) => {
3119
+ if (phase && questions && Array.isArray(questions)) {
3120
+ const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
3121
+ if (!SAFE_PHASE.test(phase)) {
3122
+ return {
3123
+ content: [
3124
+ { type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
3125
+ ],
3126
+ isError: true
3127
+ };
3128
+ }
3129
+ const questionsDir = join4(process.cwd(), ".stackwright", "questions");
3130
+ mkdirSync4(questionsDir, { recursive: true });
3131
+ const outPath = join4(questionsDir, `${phase}.json`);
3132
+ writeFileSync4(
3133
+ outPath,
3134
+ JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
3135
+ );
3136
+ return {
3137
+ content: [
3138
+ {
3139
+ type: "text",
3140
+ text: JSON.stringify({
3141
+ written: true,
3142
+ phase,
3143
+ count: questions.length
3144
+ })
3145
+ }
3146
+ ]
3147
+ };
3148
+ }
3149
+ return res(
3150
+ handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
3151
+ );
3152
+ }
2141
3153
  );
2142
3154
  server2.tool(
2143
3155
  "stackwright_pro_build_specialist_prompt",
2144
3156
  `Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
2145
- { phase: z9.string().describe('Phase to build prompt for, e.g. "pages"') },
3157
+ { phase: z11.string().describe('Phase to build prompt for, e.g. "pages"') },
2146
3158
  async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
2147
3159
  );
2148
3160
  server2.tool(
2149
3161
  "stackwright_pro_validate_artifact",
2150
- `Validate specialist response + write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
3162
+ `Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
2151
3163
  {
2152
- phase: z9.string().describe('Phase that produced this artifact, e.g. "designer"'),
2153
- responseText: z9.string().describe("Raw response text from the specialist otter")
3164
+ phase: z11.string().describe('Phase that produced this artifact, e.g. "designer"'),
3165
+ responseText: z11.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
3166
+ artifact: jsonCoerce(z11.record(z11.string(), z11.unknown()).optional()).describe(
3167
+ "Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
3168
+ )
2154
3169
  },
2155
- async ({ phase, responseText }) => res(handleValidateArtifact({ phase, responseText }))
3170
+ async ({ phase, responseText, artifact }) => {
3171
+ if (artifact) {
3172
+ return res(
3173
+ handleValidateArtifact({ phase, artifact })
3174
+ );
3175
+ }
3176
+ return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
3177
+ }
2156
3178
  );
2157
3179
  }
2158
3180
 
2159
3181
  // src/tools/safe-write.ts
2160
- import { z as z10 } from "zod";
2161
- import { writeFileSync as writeFileSync4, existsSync as existsSync4, mkdirSync as mkdirSync3, lstatSync as lstatSync4 } from "fs";
2162
- import { normalize, isAbsolute, dirname, join as join4 } from "path";
3182
+ import { z as z12 } from "zod";
3183
+ import { writeFileSync as writeFileSync5, existsSync as existsSync6, mkdirSync as mkdirSync5, lstatSync as lstatSync6 } from "fs";
3184
+ import { normalize, isAbsolute, dirname, join as join5 } from "path";
2163
3185
  var OTTER_WRITE_ALLOWLISTS = {
2164
3186
  "stackwright-pro-designer-otter": [
2165
3187
  { prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
@@ -2202,9 +3224,25 @@ var OTTER_WRITE_ALLOWLISTS = {
2202
3224
  };
2203
3225
  var PROTECTED_PATH_PREFIXES = [
2204
3226
  ".stackwright/pipeline-state.json",
3227
+ ".stackwright/pipeline-keys.json",
3228
+ // ephemeral signing keys
3229
+ ".stackwright/artifacts/signatures.json",
3230
+ // artifact signature manifest
2205
3231
  ".stackwright/questions/",
2206
3232
  ".stackwright/answers/"
2207
3233
  ];
3234
+ var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
3235
+ var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
3236
+ var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
3237
+ var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
3238
+ function getMaxBytesForPath(filePath) {
3239
+ if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
3240
+ if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
3241
+ return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
3242
+ if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
3243
+ return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
3244
+ return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
3245
+ }
2208
3246
  function checkPathAllowed(callerOtter, filePath) {
2209
3247
  const normalized = normalize(filePath);
2210
3248
  if (normalized.includes("..")) {
@@ -2330,11 +3368,23 @@ function handleSafeWrite(input) {
2330
3368
  };
2331
3369
  return { text: JSON.stringify(result), isError: true };
2332
3370
  }
3371
+ const contentBytes = Buffer.byteLength(content, "utf-8");
3372
+ const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
3373
+ if (contentBytes > maxBytes) {
3374
+ const result = {
3375
+ success: false,
3376
+ error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
3377
+ callerOtter,
3378
+ attemptedPath: filePath,
3379
+ allowedPaths: []
3380
+ };
3381
+ return { text: JSON.stringify(result), isError: true };
3382
+ }
2333
3383
  const normalized = normalize(filePath);
2334
- const fullPath = join4(cwd, normalized);
2335
- if (existsSync4(fullPath)) {
3384
+ const fullPath = join5(cwd, normalized);
3385
+ if (existsSync6(fullPath)) {
2336
3386
  try {
2337
- const stat = lstatSync4(fullPath);
3387
+ const stat = lstatSync6(fullPath);
2338
3388
  if (stat.isSymbolicLink()) {
2339
3389
  const result = {
2340
3390
  success: false,
@@ -2361,9 +3411,9 @@ function handleSafeWrite(input) {
2361
3411
  }
2362
3412
  try {
2363
3413
  if (createDirectories) {
2364
- mkdirSync3(dirname(fullPath), { recursive: true });
3414
+ mkdirSync5(dirname(fullPath), { recursive: true });
2365
3415
  }
2366
- writeFileSync4(fullPath, content, { encoding: "utf-8" });
3416
+ writeFileSync5(fullPath, content, { encoding: "utf-8" });
2367
3417
  const result = {
2368
3418
  success: true,
2369
3419
  path: normalized,
@@ -2389,10 +3439,12 @@ function registerSafeWriteTools(server2) {
2389
3439
  "stackwright_pro_safe_write",
2390
3440
  DESC,
2391
3441
  {
2392
- callerOtter: z10.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
2393
- filePath: z10.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
2394
- content: z10.string().describe("File content to write"),
2395
- createDirectories: z10.boolean().optional().describe("Create parent directories if they don't exist. Default: true")
3442
+ callerOtter: z12.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
3443
+ filePath: z12.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
3444
+ content: z12.string().describe("File content to write"),
3445
+ createDirectories: boolCoerce(z12.boolean().optional().default(true)).describe(
3446
+ "Create parent directories if they don't exist. Default: true"
3447
+ )
2396
3448
  },
2397
3449
  async ({ callerOtter, filePath, content, createDirectories }) => {
2398
3450
  const result = handleSafeWrite({
@@ -2407,9 +3459,9 @@ function registerSafeWriteTools(server2) {
2407
3459
  }
2408
3460
 
2409
3461
  // src/tools/auth.ts
2410
- import { z as z11 } from "zod";
2411
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync5, existsSync as existsSync5 } from "fs";
2412
- import { join as join5 } from "path";
3462
+ import { z as z13 } from "zod";
3463
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync6, existsSync as existsSync7 } from "fs";
3464
+ import { join as join6 } from "path";
2413
3465
  function buildHierarchy(roles) {
2414
3466
  const h = {};
2415
3467
  for (let i = 0; i < roles.length - 1; i++) {
@@ -2671,7 +3723,7 @@ async function configureAuthHandler(params, cwd) {
2671
3723
  auditRetentionDays,
2672
3724
  protectedRoutes
2673
3725
  );
2674
- writeFileSync5(join5(cwd, "middleware.ts"), middlewareContent, "utf8");
3726
+ writeFileSync6(join6(cwd, "middleware.ts"), middlewareContent, "utf8");
2675
3727
  filesWritten.push("middleware.ts");
2676
3728
  } catch (err) {
2677
3729
  const msg = err instanceof Error ? err.message : String(err);
@@ -2687,12 +3739,12 @@ async function configureAuthHandler(params, cwd) {
2687
3739
  }
2688
3740
  try {
2689
3741
  const envBlock = generateEnvBlock(method, params);
2690
- const envPath = join5(cwd, ".env.example");
2691
- if (existsSync5(envPath)) {
2692
- const existing = readFileSync4(envPath, "utf8");
2693
- writeFileSync5(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
3742
+ const envPath = join6(cwd, ".env.example");
3743
+ if (existsSync7(envPath)) {
3744
+ const existing = readFileSync5(envPath, "utf8");
3745
+ writeFileSync6(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
2694
3746
  } else {
2695
- writeFileSync5(envPath, envBlock, "utf8");
3747
+ writeFileSync6(envPath, envBlock, "utf8");
2696
3748
  }
2697
3749
  filesWritten.push(".env.example");
2698
3750
  } catch (err) {
@@ -2718,12 +3770,12 @@ async function configureAuthHandler(params, cwd) {
2718
3770
  auditRetentionDays,
2719
3771
  protectedRoutes
2720
3772
  );
2721
- const ymlPath = join5(cwd, "stackwright.yml");
2722
- if (!existsSync5(ymlPath)) {
2723
- writeFileSync5(ymlPath, authYaml, "utf8");
3773
+ const ymlPath = join6(cwd, "stackwright.yml");
3774
+ if (!existsSync7(ymlPath)) {
3775
+ writeFileSync6(ymlPath, authYaml, "utf8");
2724
3776
  } else {
2725
- const existing = readFileSync4(ymlPath, "utf8");
2726
- writeFileSync5(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
3777
+ const existing = readFileSync5(ymlPath, "utf8");
3778
+ writeFileSync6(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
2727
3779
  }
2728
3780
  filesWritten.push("stackwright.yml");
2729
3781
  } catch (err) {
@@ -2762,35 +3814,35 @@ function registerAuthTools(server2) {
2762
3814
  "stackwright_pro_configure_auth",
2763
3815
  "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.",
2764
3816
  {
2765
- method: z11.enum(["cac", "oidc", "oauth2", "none"]),
2766
- provider: z11.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
3817
+ method: z13.enum(["cac", "oidc", "oauth2", "none"]),
3818
+ provider: z13.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
2767
3819
  // CAC
2768
- cacCaBundle: z11.string().optional(),
2769
- cacEdipiLookup: z11.string().optional(),
2770
- cacOcspEndpoint: z11.string().optional(),
2771
- cacCertHeader: z11.string().optional(),
3820
+ cacCaBundle: z13.string().optional(),
3821
+ cacEdipiLookup: z13.string().optional(),
3822
+ cacOcspEndpoint: z13.string().optional(),
3823
+ cacCertHeader: z13.string().optional(),
2772
3824
  // OIDC
2773
- oidcDiscoveryUrl: z11.string().optional(),
2774
- oidcClientId: z11.string().optional(),
2775
- oidcClientSecret: z11.string().optional(),
2776
- oidcScopes: z11.string().optional(),
2777
- oidcRoleClaim: z11.string().optional(),
3825
+ oidcDiscoveryUrl: z13.string().optional(),
3826
+ oidcClientId: z13.string().optional(),
3827
+ oidcClientSecret: z13.string().optional(),
3828
+ oidcScopes: z13.string().optional(),
3829
+ oidcRoleClaim: z13.string().optional(),
2778
3830
  // OAuth2
2779
- oauth2AuthUrl: z11.string().optional(),
2780
- oauth2TokenUrl: z11.string().optional(),
2781
- oauth2ClientId: z11.string().optional(),
2782
- oauth2ClientSecret: z11.string().optional(),
2783
- oauth2Scopes: z11.string().optional(),
3831
+ oauth2AuthUrl: z13.string().optional(),
3832
+ oauth2TokenUrl: z13.string().optional(),
3833
+ oauth2ClientId: z13.string().optional(),
3834
+ oauth2ClientSecret: z13.string().optional(),
3835
+ oauth2Scopes: z13.string().optional(),
2784
3836
  // RBAC
2785
- rbacRoles: z11.array(z11.string()).optional(),
2786
- rbacDefaultRole: z11.string().optional(),
3837
+ rbacRoles: jsonCoerce(z13.array(z13.string()).optional()),
3838
+ rbacDefaultRole: z13.string().optional(),
2787
3839
  // Audit
2788
- auditEnabled: z11.boolean().optional(),
2789
- auditRetentionDays: z11.number().int().positive().optional(),
3840
+ auditEnabled: boolCoerce(z13.boolean().optional()),
3841
+ auditRetentionDays: numCoerce(z13.number().int().positive().optional()),
2790
3842
  // Routes
2791
- protectedRoutes: z11.array(z11.string()).optional(),
3843
+ protectedRoutes: jsonCoerce(z13.array(z13.string()).optional()),
2792
3844
  // Injection for tests
2793
- _cwd: z11.string().optional()
3845
+ _cwd: z13.string().optional()
2794
3846
  },
2795
3847
  async (params) => {
2796
3848
  const cwd = params._cwd ?? process.cwd();
@@ -2800,45 +3852,45 @@ function registerAuthTools(server2) {
2800
3852
  }
2801
3853
 
2802
3854
  // src/integrity.ts
2803
- import { createHash as createHash2, timingSafeEqual } from "crypto";
2804
- import { readFileSync as readFileSync5, readdirSync, lstatSync as lstatSync5 } from "fs";
2805
- import { join as join6, basename } from "path";
3855
+ import { createHash as createHash4, timingSafeEqual as timingSafeEqual2 } from "crypto";
3856
+ import { readFileSync as readFileSync6, readdirSync as readdirSync2, lstatSync as lstatSync7 } from "fs";
3857
+ import { join as join7, basename } from "path";
2806
3858
  var _checksums = /* @__PURE__ */ new Map([
2807
3859
  [
2808
3860
  "stackwright-pro-api-otter.json",
2809
- "f1cc9edf2dd1df3ebcea1d0ab33d17a358faaf8aa97ee232cd7994042f2eac0d"
3861
+ "9fbaed0ce6116b82d0289f24432037d04637c89b8e73062ed946e5d49b294734"
2810
3862
  ],
2811
3863
  [
2812
3864
  "stackwright-pro-auth-otter.json",
2813
- "a19e06c503209a8a35fe321d30448623545b36b48c47a6ec064d13406ad1f725"
3865
+ "bf0e66e35d15ba818ba6ff1a007df34975565bacbb35cc0c80151fb1da13e573"
2814
3866
  ],
2815
3867
  [
2816
3868
  "stackwright-pro-dashboard-otter.json",
2817
- "b3cb3d7554f2e9eed3b57d5e0e3bf85d6ba5b4db5d3af5514391cf0575fcc001"
3869
+ "89e81ac161147ab532034b40e4f7863dcd7d03ef33fd94c97d19c3e56fb91a9e"
2818
3870
  ],
2819
3871
  [
2820
3872
  "stackwright-pro-data-otter.json",
2821
- "bfacb87ae82867472a75982215554336a105a658d6cd3dd2c8b819fa1e11d7ac"
3873
+ "4769c3fa9b358609159ef2f613ac9d40dc7cb439e916c3344b9500a75d6afa82"
2822
3874
  ],
2823
3875
  [
2824
3876
  "stackwright-pro-designer-otter.json",
2825
- "c58fa7c7ead9e6398074e1c7ce3f31a8ef4eb3679f5fa18cc03cae3a87878c88"
3877
+ "af09ac8f06385bdbac63e2820daa2ff7d38b8ff1ff383c161f07e3fb9d9359c5"
2826
3878
  ],
2827
3879
  [
2828
3880
  "stackwright-pro-foreman-otter.json",
2829
- "84ba692e710ac3efab94d27332bcac19c6785b7f41d9076e8e7c860cdd6f8097"
3881
+ "582a26766a5bc80ef9f3aef53e82794c7f47f618bd28e4e70583bfc2b569398c"
2830
3882
  ],
2831
3883
  [
2832
3884
  "stackwright-pro-page-otter.json",
2833
- "65bec3a3a0dda6b7591bba2de9399f1e3a4fb99cfe1075342f4f4be98d917b67"
3885
+ "2fd8fe6f799e3fb4659979b1c958c64c276632fcfe18f70371543dc8145baa1b"
2834
3886
  ],
2835
3887
  [
2836
3888
  "stackwright-pro-theme-otter.json",
2837
- "64ffaeeceacd739922788a1d074f6feaffc3f91d09706c2c104f0c0281677732"
3889
+ "bc5695ef28164516b3f677cab201de781663c354d187cea28cc41884f1d89414"
2838
3890
  ],
2839
3891
  [
2840
3892
  "stackwright-pro-workflow-otter.json",
2841
- "0eec9d6a731678cf547c2a7b0b6fc338ca143c35501365a1e4e5dd2779dd5510"
3893
+ "c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
2842
3894
  ]
2843
3895
  ]);
2844
3896
  Object.freeze(_checksums);
@@ -2853,11 +3905,11 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
2853
3905
  }
2854
3906
  var MAX_OTTER_BYTES = 1 * 1024 * 1024;
2855
3907
  function computeSha256(data) {
2856
- return createHash2("sha256").update(data).digest("hex");
3908
+ return createHash4("sha256").update(data).digest("hex");
2857
3909
  }
2858
3910
  function safeEqual(a, b) {
2859
3911
  if (a.length !== b.length) return false;
2860
- return timingSafeEqual(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
3912
+ return timingSafeEqual2(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
2861
3913
  }
2862
3914
  function verifyOtterFile(filePath) {
2863
3915
  const filename = basename(filePath);
@@ -2867,7 +3919,7 @@ function verifyOtterFile(filePath) {
2867
3919
  }
2868
3920
  let stat;
2869
3921
  try {
2870
- stat = lstatSync5(filePath);
3922
+ stat = lstatSync7(filePath);
2871
3923
  } catch (err) {
2872
3924
  const msg = err instanceof Error ? err.message : String(err);
2873
3925
  return { verified: false, filename, error: `Cannot stat file: ${msg}` };
@@ -2885,7 +3937,7 @@ function verifyOtterFile(filePath) {
2885
3937
  }
2886
3938
  let raw;
2887
3939
  try {
2888
- raw = readFileSync5(filePath);
3940
+ raw = readFileSync6(filePath);
2889
3941
  } catch (err) {
2890
3942
  const msg = err instanceof Error ? err.message : String(err);
2891
3943
  return { verified: false, filename, error: `Cannot read file: ${msg}` };
@@ -2918,12 +3970,24 @@ function verifyOtterFile(filePath) {
2918
3970
  return { verified: true, filename };
2919
3971
  }
2920
3972
  function verifyAllOtters(otterDir) {
3973
+ if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
3974
+ return {
3975
+ verified: [],
3976
+ failed: [
3977
+ {
3978
+ filename: "<directory>",
3979
+ error: `Security: path traversal sequence detected in otter directory parameter`
3980
+ }
3981
+ ],
3982
+ unknown: []
3983
+ };
3984
+ }
2921
3985
  const verified = [];
2922
3986
  const failed = [];
2923
3987
  const unknown = [];
2924
3988
  let entries;
2925
3989
  try {
2926
- entries = readdirSync(otterDir);
3990
+ entries = readdirSync2(otterDir);
2927
3991
  } catch (err) {
2928
3992
  const msg = err instanceof Error ? err.message : String(err);
2929
3993
  return {
@@ -2934,9 +3998,9 @@ function verifyAllOtters(otterDir) {
2934
3998
  }
2935
3999
  const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
2936
4000
  for (const filename of otterFiles) {
2937
- const filePath = join6(otterDir, filename);
4001
+ const filePath = join7(otterDir, filename);
2938
4002
  try {
2939
- if (lstatSync5(filePath).isSymbolicLink()) {
4003
+ if (lstatSync7(filePath).isSymbolicLink()) {
2940
4004
  failed.push({ filename, error: "Skipped: symlink" });
2941
4005
  continue;
2942
4006
  }
@@ -2962,15 +4026,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
2962
4026
  function resolveOtterDir() {
2963
4027
  const cwd = process.cwd();
2964
4028
  for (const relative of DEFAULT_SEARCH_PATHS) {
2965
- const candidate = join6(cwd, relative);
4029
+ const candidate = join7(cwd, relative);
2966
4030
  try {
2967
- lstatSync5(candidate);
4031
+ lstatSync7(candidate);
2968
4032
  return candidate;
2969
4033
  } catch {
2970
4034
  }
2971
4035
  }
2972
4036
  return null;
2973
4037
  }
4038
+ function emitIntegrityAuditEvent(params) {
4039
+ const record = JSON.stringify({
4040
+ level: "AUDIT",
4041
+ event: "INTEGRITY_FAIL",
4042
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4043
+ source: "stackwright_pro_verify_otter_integrity",
4044
+ otterDir: params.otterDir,
4045
+ failedCount: params.failed.length,
4046
+ unknownCount: params.unknown.length,
4047
+ failures: params.failed,
4048
+ unknown: params.unknown
4049
+ });
4050
+ process.stderr.write(`INTEGRITY_FAIL ${record}
4051
+ `);
4052
+ }
2974
4053
  function registerIntegrityTools(server2) {
2975
4054
  server2.tool(
2976
4055
  "stackwright_pro_verify_otter_integrity",
@@ -2994,6 +4073,13 @@ function registerIntegrityTools(server2) {
2994
4073
  }
2995
4074
  const result = verifyAllOtters(resolved);
2996
4075
  const allGood = result.failed.length === 0 && result.unknown.length === 0;
4076
+ if (!allGood) {
4077
+ emitIntegrityAuditEvent({
4078
+ otterDir: resolved,
4079
+ failed: result.failed,
4080
+ unknown: result.unknown
4081
+ });
4082
+ }
2997
4083
  return {
2998
4084
  content: [
2999
4085
  {
@@ -3006,7 +4092,10 @@ function registerIntegrityTools(server2) {
3006
4092
  unknownCount: result.unknown.length,
3007
4093
  verified: result.verified,
3008
4094
  failed: result.failed,
3009
- unknown: result.unknown
4095
+ unknown: result.unknown,
4096
+ ...allGood ? {} : {
4097
+ error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
4098
+ }
3010
4099
  })
3011
4100
  }
3012
4101
  ],
@@ -3017,14 +4106,14 @@ function registerIntegrityTools(server2) {
3017
4106
  }
3018
4107
 
3019
4108
  // src/tools/domain.ts
3020
- import { z as z12 } from "zod";
3021
- import { readFileSync as readFileSync6, existsSync as existsSync6 } from "fs";
3022
- import { join as join7 } from "path";
4109
+ import { z as z14 } from "zod";
4110
+ import { readFileSync as readFileSync7, existsSync as existsSync8 } from "fs";
4111
+ import { join as join8 } from "path";
3023
4112
  function handleListCollections(input) {
3024
4113
  const cwd = input._cwd ?? process.cwd();
3025
4114
  const sources = [
3026
4115
  {
3027
- path: join7(cwd, ".stackwright", "artifacts", "data-config.json"),
4116
+ path: join8(cwd, ".stackwright", "artifacts", "data-config.json"),
3028
4117
  source: "data-config.json",
3029
4118
  parse: (raw) => {
3030
4119
  const parsed = JSON.parse(raw);
@@ -3035,15 +4124,15 @@ function handleListCollections(input) {
3035
4124
  }
3036
4125
  },
3037
4126
  {
3038
- path: join7(cwd, "stackwright.yml"),
4127
+ path: join8(cwd, "stackwright.yml"),
3039
4128
  source: "stackwright.yml",
3040
4129
  parse: extractCollectionsFromYaml
3041
4130
  }
3042
4131
  ];
3043
4132
  for (const { path: path3, source, parse } of sources) {
3044
- if (!existsSync6(path3)) continue;
4133
+ if (!existsSync8(path3)) continue;
3045
4134
  try {
3046
- const collections = parse(readFileSync6(path3, "utf8"));
4135
+ const collections = parse(readFileSync7(path3, "utf8"));
3047
4136
  return {
3048
4137
  text: JSON.stringify({ collections, source, collectionCount: collections.length }),
3049
4138
  isError: false
@@ -3194,8 +4283,8 @@ function handleValidateWorkflow(input) {
3194
4283
  if (input.workflow && Object.keys(input.workflow).length > 0) {
3195
4284
  raw = input.workflow;
3196
4285
  } else {
3197
- const artifactPath = join7(cwd, ".stackwright", "artifacts", "workflow-config.json");
3198
- if (!existsSync6(artifactPath)) {
4286
+ const artifactPath = join8(cwd, ".stackwright", "artifacts", "workflow-config.json");
4287
+ if (!existsSync8(artifactPath)) {
3199
4288
  return fail([
3200
4289
  {
3201
4290
  code: "NO_WORKFLOW",
@@ -3204,7 +4293,7 @@ function handleValidateWorkflow(input) {
3204
4293
  ]);
3205
4294
  }
3206
4295
  try {
3207
- raw = JSON.parse(readFileSync6(artifactPath, "utf8"));
4296
+ raw = JSON.parse(readFileSync7(artifactPath, "utf8"));
3208
4297
  } catch (err) {
3209
4298
  return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
3210
4299
  }
@@ -3403,7 +4492,7 @@ function registerDomainTools(server2) {
3403
4492
  "stackwright_pro_resolve_data_strategy",
3404
4493
  "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.",
3405
4494
  {
3406
- strategy: z12.string().describe(
4495
+ strategy: z14.string().describe(
3407
4496
  'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
3408
4497
  )
3409
4498
  },
@@ -3413,7 +4502,7 @@ function registerDomainTools(server2) {
3413
4502
  "stackwright_pro_validate_workflow",
3414
4503
  "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.",
3415
4504
  {
3416
- workflow: z12.record(z12.string(), z12.unknown()).optional().describe(
4505
+ workflow: jsonCoerce(z14.record(z14.string(), z14.unknown()).optional()).describe(
3417
4506
  "Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
3418
4507
  )
3419
4508
  },
@@ -3421,20 +4510,112 @@ function registerDomainTools(server2) {
3421
4510
  );
3422
4511
  }
3423
4512
 
4513
+ // src/tools/type-schemas.ts
4514
+ import { z as z15 } from "zod";
4515
+ function buildTypeSchemaSummary() {
4516
+ return {
4517
+ version: "1.0",
4518
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
4519
+ domains: {
4520
+ workflow: {
4521
+ description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
4522
+ schemas: [
4523
+ "WorkflowFileSchema",
4524
+ "WorkflowDefinitionSchema",
4525
+ "WorkflowStepSchema",
4526
+ "WorkflowStepTypeSchema",
4527
+ "WorkflowFieldSchema",
4528
+ "WorkflowActionSchema",
4529
+ "WorkflowAuthSchema",
4530
+ "WorkflowThemeSchema",
4531
+ "TransitionConditionSchema",
4532
+ "PersistenceSchema"
4533
+ ],
4534
+ otter: "stackwright-pro-workflow-otter",
4535
+ artifactKey: "workflowConfig"
4536
+ },
4537
+ auth: {
4538
+ description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
4539
+ schemas: [
4540
+ "authConfigSchema",
4541
+ "pkiConfigSchema",
4542
+ "oidcConfigSchema",
4543
+ "rbacConfigSchema",
4544
+ "componentAuthSchema",
4545
+ "authUserSchema",
4546
+ "authSessionSchema"
4547
+ ],
4548
+ otter: "stackwright-pro-auth-otter",
4549
+ artifactKey: "authConfig"
4550
+ },
4551
+ openapi: {
4552
+ description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
4553
+ interfaces: [
4554
+ "OpenAPIConfig",
4555
+ "ActionConfig",
4556
+ "EndpointFilter",
4557
+ "ApprovedSpec",
4558
+ "PrebuildSecurityConfig",
4559
+ "SiteConfig",
4560
+ "ValidationResult"
4561
+ ],
4562
+ otter: "stackwright-pro-api-otter",
4563
+ artifactKey: "apiConfig"
4564
+ },
4565
+ pulse: {
4566
+ description: "Real-time data polling \u2014 source-agnostic polling options and states",
4567
+ interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
4568
+ note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
4569
+ },
4570
+ enterprise: {
4571
+ description: "Enterprise collection access \u2014 multi-tenant provider extension",
4572
+ interfaces: [
4573
+ "EnterpriseCollectionProvider",
4574
+ "CollectionProvider",
4575
+ "CollectionEntry",
4576
+ "CollectionListOptions",
4577
+ "CollectionListResult",
4578
+ "TenantFilter",
4579
+ "Collection"
4580
+ ],
4581
+ note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
4582
+ }
4583
+ }
4584
+ };
4585
+ }
4586
+ function registerTypeSchemasTool(server2) {
4587
+ server2.tool(
4588
+ "stackwright_pro_get_type_schemas",
4589
+ "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.",
4590
+ {
4591
+ format: z15.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
4592
+ },
4593
+ async ({ format }) => {
4594
+ const summary = buildTypeSchemaSummary();
4595
+ const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
4596
+ return {
4597
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
4598
+ };
4599
+ }
4600
+ );
4601
+ }
4602
+
3424
4603
  // package.json
3425
4604
  var package_default = {
3426
4605
  dependencies: {
4606
+ "@stackwright-pro/types": "workspace:*",
3427
4607
  "@modelcontextprotocol/sdk": "^1.10.0",
3428
4608
  "@stackwright-pro/cli-data-explorer": "workspace:*",
3429
- zod: "^4.3.6"
4609
+ zod: "^4.4.3"
3430
4610
  },
3431
4611
  devDependencies: {
3432
- "@types/node": "^24.1.0",
3433
- tsup: "^8.5.0",
3434
- typescript: "^5.8.3",
3435
- vitest: "^4.0.18"
4612
+ "@types/node": "catalog:",
4613
+ tsup: "catalog:",
4614
+ typescript: "catalog:",
4615
+ vitest: "catalog:"
3436
4616
  },
3437
4617
  scripts: {
4618
+ prepublishOnly: "node scripts/verify-integrity-sync.js",
3438
4619
  build: "tsup",
3439
4620
  dev: "tsup --watch",
3440
4621
  start: "node dist/server.js",
@@ -3442,10 +4623,13 @@ var package_default = {
3442
4623
  "test:coverage": "vitest run --coverage"
3443
4624
  },
3444
4625
  name: "@stackwright-pro/mcp",
3445
- version: "0.2.0-alpha.5",
4626
+ version: "0.2.0-alpha.52",
3446
4627
  description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
3447
- license: "PROPRIETARY",
4628
+ license: "SEE LICENSE IN LICENSE",
3448
4629
  main: "./dist/server.js",
4630
+ bin: {
4631
+ "stackwright-pro-mcp": "./dist/server.js"
4632
+ },
3449
4633
  module: "./dist/server.mjs",
3450
4634
  types: "./dist/server.d.ts",
3451
4635
  exports: {
@@ -3458,6 +4642,11 @@ var package_default = {
3458
4642
  types: "./dist/integrity.d.ts",
3459
4643
  import: "./dist/integrity.mjs",
3460
4644
  require: "./dist/integrity.js"
4645
+ },
4646
+ "./type-schemas": {
4647
+ types: "./dist/tools/type-schemas.d.ts",
4648
+ import: "./dist/tools/type-schemas.mjs",
4649
+ require: "./dist/tools/type-schemas.js"
3461
4650
  }
3462
4651
  },
3463
4652
  files: [
@@ -3485,7 +4674,9 @@ registerPipelineTools(server);
3485
4674
  registerSafeWriteTools(server);
3486
4675
  registerAuthTools(server);
3487
4676
  registerIntegrityTools(server);
4677
+ registerArtifactSigningTools(server);
3488
4678
  registerDomainTools(server);
4679
+ registerTypeSchemasTool(server);
3489
4680
  async function main() {
3490
4681
  const transport = new StdioServerTransport();
3491
4682
  await server.connect(transport);