@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/LICENSE +21 -0
- package/dist/integrity.d.mts +29 -1
- package/dist/integrity.d.ts +29 -1
- package/dist/integrity.js +49 -10
- package/dist/integrity.js.map +1 -1
- package/dist/integrity.mjs +48 -10
- package/dist/integrity.mjs.map +1 -1
- package/dist/server.js +1486 -311
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +1504 -313
- package/dist/server.mjs.map +1 -1
- package/dist/tools/type-schemas.d.mts +51 -0
- package/dist/tools/type-schemas.d.ts +51 -0
- package/dist/tools/type-schemas.js +120 -0
- package/dist/tools/type-schemas.js.map +1 -0
- package/dist/tools/type-schemas.mjs +94 -0
- package/dist/tools/type-schemas.mjs.map +1 -0
- package/package.json +17 -8
package/dist/server.js
CHANGED
|
@@ -27,15 +27,44 @@ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
|
27
27
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
28
28
|
|
|
29
29
|
// src/tools/data-explorer.ts
|
|
30
|
-
var
|
|
30
|
+
var import_zod2 = require("zod");
|
|
31
31
|
var import_cli_data_explorer = require("@stackwright-pro/cli-data-explorer");
|
|
32
|
+
|
|
33
|
+
// src/coerce.ts
|
|
34
|
+
var import_zod = require("zod");
|
|
35
|
+
function jsonCoerce(schema) {
|
|
36
|
+
return import_zod.z.preprocess((v) => {
|
|
37
|
+
if (typeof v === "string") {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(v);
|
|
40
|
+
} catch {
|
|
41
|
+
return v;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return v;
|
|
45
|
+
}, schema);
|
|
46
|
+
}
|
|
47
|
+
function boolCoerce(schema) {
|
|
48
|
+
return import_zod.z.preprocess((v) => v === "true" ? true : v === "false" ? false : v, schema);
|
|
49
|
+
}
|
|
50
|
+
function numCoerce(schema) {
|
|
51
|
+
return import_zod.z.preprocess((v) => {
|
|
52
|
+
if (typeof v === "string" && v.trim() !== "") {
|
|
53
|
+
const n = Number(v);
|
|
54
|
+
if (!Number.isNaN(n)) return n;
|
|
55
|
+
}
|
|
56
|
+
return v;
|
|
57
|
+
}, schema);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/tools/data-explorer.ts
|
|
32
61
|
function registerDataExplorerTools(server2) {
|
|
33
62
|
server2.tool(
|
|
34
63
|
"stackwright_pro_list_entities",
|
|
35
64
|
"List all available API entities from OpenAPI specs or generated Zod schemas. Use this to discover what entities are available before generating endpoint filters. Returns entity names, endpoints, and field counts. Part of the Pro Otter Raft for building API-integrated Stackwright applications.",
|
|
36
65
|
{
|
|
37
|
-
specPath:
|
|
38
|
-
projectRoot:
|
|
66
|
+
specPath: import_zod2.z.string().optional().describe("Path to OpenAPI spec file (YAML or JSON)"),
|
|
67
|
+
projectRoot: import_zod2.z.string().optional().describe("Project root directory (auto-detected if omitted)")
|
|
39
68
|
},
|
|
40
69
|
async ({ specPath, projectRoot }) => {
|
|
41
70
|
const result = (0, import_cli_data_explorer.listEntities)({
|
|
@@ -83,9 +112,13 @@ function registerDataExplorerTools(server2) {
|
|
|
83
112
|
"stackwright_pro_generate_filter",
|
|
84
113
|
"Generate endpoint filter configuration from selected entities. Creates include/exclude patterns for stackwright.yml OpenAPI integration. Use this after stackwright_pro_list_entities to select which API endpoints the application needs. Only selected endpoints will generate client code, reducing bundle size and improving security.",
|
|
85
114
|
{
|
|
86
|
-
selectedEntities:
|
|
87
|
-
|
|
88
|
-
|
|
115
|
+
selectedEntities: jsonCoerce(import_zod2.z.array(import_zod2.z.string())).describe(
|
|
116
|
+
'Entity slugs to include (e.g., ["equipment", "supplies"])'
|
|
117
|
+
),
|
|
118
|
+
excludePatterns: jsonCoerce(import_zod2.z.array(import_zod2.z.string()).optional()).describe(
|
|
119
|
+
'Glob patterns to exclude (e.g., ["/admin/**", "/reports/**"])'
|
|
120
|
+
),
|
|
121
|
+
projectRoot: import_zod2.z.string().optional().describe("Project root directory")
|
|
89
122
|
},
|
|
90
123
|
async ({ selectedEntities, excludePatterns, projectRoot }) => {
|
|
91
124
|
const result = (0, import_cli_data_explorer.generateFilter)({
|
|
@@ -141,7 +174,7 @@ function registerDataExplorerTools(server2) {
|
|
|
141
174
|
}
|
|
142
175
|
|
|
143
176
|
// src/tools/security.ts
|
|
144
|
-
var
|
|
177
|
+
var import_zod3 = require("zod");
|
|
145
178
|
var import_crypto = require("crypto");
|
|
146
179
|
var import_fs = __toESM(require("fs"));
|
|
147
180
|
var import_path = __toESM(require("path"));
|
|
@@ -150,8 +183,8 @@ function registerSecurityTools(server2) {
|
|
|
150
183
|
"stackwright_pro_validate_spec",
|
|
151
184
|
"Validate an OpenAPI spec against the enterprise approved-specs configuration. Checks if the spec URL is on the allowlist and verifies SHA-256 hash integrity. Use this in enterprise environments where only pre-approved API specs are allowed. Fails build if spec is not approved or has been modified.",
|
|
152
185
|
{
|
|
153
|
-
specPath:
|
|
154
|
-
configPath:
|
|
186
|
+
specPath: import_zod3.z.string().describe("URL or file path to the OpenAPI spec to validate"),
|
|
187
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml with prebuild.security config")
|
|
155
188
|
},
|
|
156
189
|
async ({ specPath, configPath }) => {
|
|
157
190
|
let securityEnabled = false;
|
|
@@ -259,16 +292,15 @@ Status: Valid (${allowlist.length} specs on allowlist)`
|
|
|
259
292
|
"stackwright_pro_add_approved_spec",
|
|
260
293
|
"Add an OpenAPI spec to the approved-specs allowlist in stackwright.yml. Computes the SHA-256 hash of the spec and adds it to the security configuration. Use this when onboarding a new API in enterprise environments.",
|
|
261
294
|
{
|
|
262
|
-
name:
|
|
263
|
-
url:
|
|
264
|
-
configPath:
|
|
295
|
+
name: import_zod3.z.string().describe("Human-readable name for the spec"),
|
|
296
|
+
url: import_zod3.z.string().describe("URL or file path to the OpenAPI spec"),
|
|
297
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
265
298
|
},
|
|
266
299
|
async ({ name, url, configPath }) => {
|
|
267
300
|
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
268
301
|
let sha256 = "<computed-at-build>";
|
|
269
302
|
try {
|
|
270
303
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
271
|
-
sha256 = "<computed-at-build>";
|
|
272
304
|
} else if (import_fs.default.existsSync(url)) {
|
|
273
305
|
const specContent = import_fs.default.readFileSync(url, "utf8");
|
|
274
306
|
sha256 = (0, import_crypto.createHash)("sha256").update(specContent).digest("hex");
|
|
@@ -315,7 +347,7 @@ SHA-256 computed: ${sha256}`
|
|
|
315
347
|
"stackwright_pro_list_approved_specs",
|
|
316
348
|
"List all specs currently on the approved-specs allowlist. Shows spec names, URLs, and hash prefixes. Use this to audit what APIs are approved in the project.",
|
|
317
349
|
{
|
|
318
|
-
configPath:
|
|
350
|
+
configPath: import_zod3.z.string().optional().describe("Path to stackwright.yml")
|
|
319
351
|
},
|
|
320
352
|
async ({ configPath }) => {
|
|
321
353
|
const configFile = configPath || import_path.default.join(process.cwd(), "stackwright.yml");
|
|
@@ -384,16 +416,18 @@ SHA-256 computed: ${sha256}`
|
|
|
384
416
|
}
|
|
385
417
|
|
|
386
418
|
// src/tools/isr.ts
|
|
387
|
-
var
|
|
419
|
+
var import_zod4 = require("zod");
|
|
388
420
|
function registerIsrTools(server2) {
|
|
389
421
|
server2.tool(
|
|
390
422
|
"stackwright_pro_configure_isr",
|
|
391
423
|
"Configure Incremental Static Regeneration (ISR) for an API-backed collection. ISR allows API data to be cached and refreshed on a schedule, providing real-time data with the performance of static generation. Use this after stackwright_pro_generate_filter to set revalidation intervals.",
|
|
392
424
|
{
|
|
393
|
-
collection:
|
|
394
|
-
revalidateSeconds:
|
|
395
|
-
|
|
396
|
-
|
|
425
|
+
collection: import_zod4.z.string().describe('Collection name (e.g., "equipment", "supplies")'),
|
|
426
|
+
revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe(
|
|
427
|
+
"Revalidation interval in seconds (default: 60)"
|
|
428
|
+
),
|
|
429
|
+
fallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Fallback behavior for new pages"),
|
|
430
|
+
configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
|
|
397
431
|
},
|
|
398
432
|
async ({ collection, revalidateSeconds = 60, fallback = "blocking", configPath }) => {
|
|
399
433
|
const revalidate = revalidateSeconds;
|
|
@@ -404,7 +438,7 @@ function registerIsrTools(server2) {
|
|
|
404
438
|
isr:
|
|
405
439
|
revalidate: ${revalidate}
|
|
406
440
|
fallback: ${fallback}`;
|
|
407
|
-
let description
|
|
441
|
+
let description;
|
|
408
442
|
if (revalidate < 60) {
|
|
409
443
|
description = "\u26A1 Very fresh data (revalidate every " + revalidate + "s)";
|
|
410
444
|
} else if (revalidate < 300) {
|
|
@@ -436,14 +470,16 @@ ${yamlSnippet}
|
|
|
436
470
|
"stackwright_pro_configure_isr_batch",
|
|
437
471
|
"Configure ISR for multiple collections at once. More efficient than calling stackwright_pro_configure_isr multiple times. Provide different revalidation intervals based on data freshness requirements.",
|
|
438
472
|
{
|
|
439
|
-
collections:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
473
|
+
collections: jsonCoerce(
|
|
474
|
+
import_zod4.z.array(
|
|
475
|
+
import_zod4.z.object({
|
|
476
|
+
name: import_zod4.z.string().describe("Collection name"),
|
|
477
|
+
revalidateSeconds: numCoerce(import_zod4.z.number().optional()).describe("Revalidation interval")
|
|
478
|
+
})
|
|
479
|
+
)
|
|
444
480
|
).describe("Array of collection configurations"),
|
|
445
|
-
defaultFallback:
|
|
446
|
-
configPath:
|
|
481
|
+
defaultFallback: import_zod4.z.enum(["blocking", "true", "false"]).optional().describe("Default fallback behavior"),
|
|
482
|
+
configPath: import_zod4.z.string().optional().describe("Path to stackwright.yml")
|
|
447
483
|
},
|
|
448
484
|
async ({ collections, defaultFallback = "blocking", configPath }) => {
|
|
449
485
|
const lines = [`\u2699\uFE0F Batch ISR Configuration:
|
|
@@ -471,17 +507,17 @@ Fallback: ${defaultFallback}`);
|
|
|
471
507
|
}
|
|
472
508
|
|
|
473
509
|
// src/tools/dashboard.ts
|
|
474
|
-
var
|
|
510
|
+
var import_zod5 = require("zod");
|
|
475
511
|
var import_cli_data_explorer2 = require("@stackwright-pro/cli-data-explorer");
|
|
476
512
|
function registerDashboardTools(server2) {
|
|
477
513
|
server2.tool(
|
|
478
514
|
"stackwright_pro_generate_dashboard",
|
|
479
515
|
"Generate a dashboard page configuration for displaying API data. Creates YAML content for a Stackwright page with grid, metric_card, data_table, and collection_list content types. Use this after stackwright_pro_generate_filter to create pages for your API collections.",
|
|
480
516
|
{
|
|
481
|
-
entities:
|
|
482
|
-
layout:
|
|
483
|
-
pageTitle:
|
|
484
|
-
specPath:
|
|
517
|
+
entities: jsonCoerce(import_zod5.z.array(import_zod5.z.string())).describe("Entity slugs to include in dashboard"),
|
|
518
|
+
layout: import_zod5.z.enum(["grid", "table", "mixed"]).optional().describe("Dashboard layout style"),
|
|
519
|
+
pageTitle: import_zod5.z.string().optional().describe("Page title"),
|
|
520
|
+
specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for entity details")
|
|
485
521
|
},
|
|
486
522
|
async ({ entities, layout = "mixed", pageTitle, specPath }) => {
|
|
487
523
|
let entityDetails = [];
|
|
@@ -567,9 +603,9 @@ ${yaml}
|
|
|
567
603
|
"stackwright_pro_generate_detail_page",
|
|
568
604
|
"Generate a detail view page for a single API entity. Creates YAML content for displaying all fields of an individual record. Use this to create detail pages that complement collection listing pages.",
|
|
569
605
|
{
|
|
570
|
-
entity:
|
|
571
|
-
slugField:
|
|
572
|
-
specPath:
|
|
606
|
+
entity: import_zod5.z.string().describe('Entity slug (e.g., "equipment")'),
|
|
607
|
+
slugField: import_zod5.z.string().optional().describe('Field to use as URL slug (default: "id")'),
|
|
608
|
+
specPath: import_zod5.z.string().optional().describe("Path to OpenAPI spec for field details")
|
|
573
609
|
},
|
|
574
610
|
async ({ entity, slugField = "id", specPath }) => {
|
|
575
611
|
let fields = [];
|
|
@@ -592,39 +628,49 @@ ${yaml}
|
|
|
592
628
|
` title: "${entityName} Details | {{ ${entity}.${slugField} }}"`,
|
|
593
629
|
"",
|
|
594
630
|
" content_items:",
|
|
595
|
-
" -
|
|
596
|
-
'
|
|
597
|
-
"
|
|
598
|
-
`
|
|
599
|
-
'
|
|
600
|
-
"
|
|
601
|
-
`
|
|
602
|
-
'
|
|
603
|
-
' background: "primary"',
|
|
604
|
-
' color: "text"',
|
|
631
|
+
" - type: text_block",
|
|
632
|
+
' label: "detail-header"',
|
|
633
|
+
" heading:",
|
|
634
|
+
` text: "{{ ${entity}.${slugField} }}"`,
|
|
635
|
+
' textSize: "h1"',
|
|
636
|
+
" textBlocks:",
|
|
637
|
+
` - text: "Details for this ${entity}"`,
|
|
638
|
+
' textSize: "body1"',
|
|
605
639
|
"",
|
|
606
|
-
" - grid
|
|
607
|
-
'
|
|
608
|
-
"
|
|
609
|
-
" items:"
|
|
640
|
+
" - type: grid",
|
|
641
|
+
' label: "detail-fields"',
|
|
642
|
+
" columns:"
|
|
610
643
|
];
|
|
611
644
|
const displayFields = fields.length > 0 ? fields.slice(0, 8) : [
|
|
612
645
|
{ name: slugField, type: "string" },
|
|
613
646
|
{ name: "created_at", type: "datetime" },
|
|
614
647
|
{ name: "updated_at", type: "datetime" }
|
|
615
648
|
];
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
yamlLines.push(
|
|
621
|
-
yamlLines.push(
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
649
|
+
const mid = Math.ceil(displayFields.length / 2);
|
|
650
|
+
const col1 = displayFields.slice(0, mid);
|
|
651
|
+
const col2 = displayFields.slice(mid);
|
|
652
|
+
for (const [colIndex, colFields] of [col1, col2].entries()) {
|
|
653
|
+
yamlLines.push(" - width: 1");
|
|
654
|
+
yamlLines.push(" content_items:");
|
|
655
|
+
for (const field of colFields) {
|
|
656
|
+
const label = field.name.replace(/_/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
657
|
+
yamlLines.push(" - type: text_block");
|
|
658
|
+
yamlLines.push(` label: "${field.name}-field"`);
|
|
659
|
+
yamlLines.push(" heading:");
|
|
660
|
+
yamlLines.push(` text: "${label}"`);
|
|
661
|
+
yamlLines.push(' textSize: "h4"');
|
|
662
|
+
yamlLines.push(" textBlocks:");
|
|
663
|
+
yamlLines.push(` - text: "{{ ${entity}.${field.name} }}"`);
|
|
664
|
+
yamlLines.push(' textSize: "body1"');
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
yamlLines.push("");
|
|
668
|
+
yamlLines.push(" - type: action_bar");
|
|
669
|
+
yamlLines.push(" actions:");
|
|
670
|
+
yamlLines.push(` - label: "<- Back to ${entityName}"`);
|
|
671
|
+
yamlLines.push(" action: navigate");
|
|
672
|
+
yamlLines.push(` href: "/${entity}"`);
|
|
673
|
+
yamlLines.push(" style: secondary");
|
|
628
674
|
const yaml = yamlLines.join("\n");
|
|
629
675
|
return {
|
|
630
676
|
content: [
|
|
@@ -645,7 +691,7 @@ ${yaml}
|
|
|
645
691
|
}
|
|
646
692
|
|
|
647
693
|
// src/tools/clarification.ts
|
|
648
|
-
var
|
|
694
|
+
var import_zod6 = require("zod");
|
|
649
695
|
var CONTRADICTION_PATTERNS = [
|
|
650
696
|
{
|
|
651
697
|
keywords: ["minimal", "clean", "simple"],
|
|
@@ -733,12 +779,14 @@ function registerClarificationTools(server2) {
|
|
|
733
779
|
"stackwright_pro_clarify",
|
|
734
780
|
"Ask the user for clarification when a specialist otter encounters ambiguity. This is for MID-EXECUTION questions, NOT upfront question collection (use the Question Manifest Protocol for that). Returns a structured response for the foreman to present to the user via ask_user_question (closed_choice) or directly (open_text).",
|
|
735
781
|
{
|
|
736
|
-
context:
|
|
737
|
-
question_type:
|
|
738
|
-
question:
|
|
739
|
-
choices:
|
|
740
|
-
|
|
741
|
-
|
|
782
|
+
context: import_zod6.z.string().optional().describe("Context about what the otter is trying to do"),
|
|
783
|
+
question_type: import_zod6.z.enum(["closed_choice", "open_text"]).describe("Type of question being asked"),
|
|
784
|
+
question: import_zod6.z.string().describe("The clarification question to ask the user"),
|
|
785
|
+
choices: jsonCoerce(import_zod6.z.array(import_zod6.z.string()).optional()).describe(
|
|
786
|
+
"Options for closed_choice questions"
|
|
787
|
+
),
|
|
788
|
+
priority: import_zod6.z.enum(["blocking", "preferred", "optional"]).optional().default("preferred").describe("How critical is this clarification? Default: preferred"),
|
|
789
|
+
target_field: import_zod6.z.string().optional().describe("What field/config does this clarify?")
|
|
742
790
|
},
|
|
743
791
|
async ({ context, question_type, question, choices, priority, target_field }) => {
|
|
744
792
|
const result = handleClarify({
|
|
@@ -767,8 +815,10 @@ function registerClarificationTools(server2) {
|
|
|
767
815
|
"stackwright_pro_detect_conflict",
|
|
768
816
|
"Detect when a user's stated preference conflicts with their selected choices. Uses keyword heuristics against known contradiction patterns (minimal vs vibrant, dark vs light, etc). Returns conflict details and resolution options.",
|
|
769
817
|
{
|
|
770
|
-
stated_preference:
|
|
771
|
-
selected_values:
|
|
818
|
+
stated_preference: import_zod6.z.string().describe("What the user said they wanted"),
|
|
819
|
+
selected_values: jsonCoerce(import_zod6.z.record(import_zod6.z.string(), import_zod6.z.string())).describe(
|
|
820
|
+
"What the user actually selected (field \u2192 value)"
|
|
821
|
+
)
|
|
772
822
|
},
|
|
773
823
|
async ({ stated_preference, selected_values }) => {
|
|
774
824
|
const result = handleDetectConflict({ stated_preference, selected_values });
|
|
@@ -813,7 +863,7 @@ ${result.resolution_options?.map((o) => ` \u2022 ${o}`).join("\n")}
|
|
|
813
863
|
}
|
|
814
864
|
|
|
815
865
|
// src/tools/packages.ts
|
|
816
|
-
var
|
|
866
|
+
var import_zod7 = require("zod");
|
|
817
867
|
var import_fs2 = require("fs");
|
|
818
868
|
var import_child_process = require("child_process");
|
|
819
869
|
var import_path2 = __toESM(require("path"));
|
|
@@ -834,17 +884,23 @@ function registerPackageTools(server2) {
|
|
|
834
884
|
"Ensures pro packages are present in a project's package.json. Safe to call multiple times \u2014 never overwrites existing version pins. Use this to bootstrap dependencies before specialist otters run. Pass includeBaseline: true to automatically include all required @stackwright-pro/* baseline dependencies. Safe to call on existing projects \u2014 never overwrites pinned versions.",
|
|
835
885
|
{
|
|
836
886
|
// FIX 3 (B-new-1): Zod v4 requires two-arg z.record(keySchema, valueSchema)
|
|
837
|
-
packages:
|
|
838
|
-
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }'
|
|
887
|
+
packages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional().default({})).describe(
|
|
888
|
+
'Dependencies to add. Record<packageName, version>. e.g. { "@stackwright-pro/auth": "latest" }. Omit or pass {} when using includeBaseline: true.'
|
|
889
|
+
),
|
|
890
|
+
devPackages: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
|
|
891
|
+
"devDependencies to add. Same format as packages."
|
|
839
892
|
),
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
893
|
+
scripts: jsonCoerce(import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()).describe(
|
|
894
|
+
"npm scripts to add. Only adds if key does not already exist."
|
|
895
|
+
),
|
|
896
|
+
targetDir: import_zod7.z.string().optional().describe(
|
|
843
897
|
"Project directory containing package.json. Defaults to process.cwd(). Must be an absolute path within the current working directory."
|
|
844
898
|
),
|
|
845
|
-
runInstall:
|
|
846
|
-
|
|
847
|
-
|
|
899
|
+
runInstall: boolCoerce(import_zod7.z.boolean().optional().default(true)).describe(
|
|
900
|
+
"Run pnpm install after writing package.json. Defaults to true. Pass boolean true/false."
|
|
901
|
+
),
|
|
902
|
+
includeBaseline: boolCoerce(import_zod7.z.boolean().optional().default(false)).describe(
|
|
903
|
+
"When true, automatically merges BASELINE_DEPS and BASELINE_DEV_DEPS before applying packages/devPackages args. Safe to call on existing projects \u2014 never overwrites pinned versions. Pass boolean true/false."
|
|
848
904
|
)
|
|
849
905
|
},
|
|
850
906
|
async ({ packages, devPackages, scripts, targetDir, runInstall, includeBaseline }) => {
|
|
@@ -1002,11 +1058,11 @@ function setupPackages(opts) {
|
|
|
1002
1058
|
}
|
|
1003
1059
|
}
|
|
1004
1060
|
const raw = (0, import_fs2.readFileSync)(realPackageJsonPath, "utf8");
|
|
1005
|
-
const PackageJsonSchema =
|
|
1061
|
+
const PackageJsonSchema = import_zod7.z.object({
|
|
1006
1062
|
// Zod v4: z.record(keySchema, valueSchema) — two-arg form required
|
|
1007
|
-
dependencies:
|
|
1008
|
-
devDependencies:
|
|
1009
|
-
scripts:
|
|
1063
|
+
dependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
|
|
1064
|
+
devDependencies: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional(),
|
|
1065
|
+
scripts: import_zod7.z.record(import_zod7.z.string(), import_zod7.z.string()).optional()
|
|
1010
1066
|
}).passthrough();
|
|
1011
1067
|
const schemaResult = PackageJsonSchema.safeParse(JSON.parse(raw));
|
|
1012
1068
|
if (!schemaResult.success) {
|
|
@@ -1088,8 +1144,9 @@ function setupPackages(opts) {
|
|
|
1088
1144
|
|
|
1089
1145
|
// src/tools/questions.ts
|
|
1090
1146
|
var import_promises = require("fs/promises");
|
|
1147
|
+
var import_node_fs = require("fs");
|
|
1091
1148
|
var import_node_path = require("path");
|
|
1092
|
-
var
|
|
1149
|
+
var import_zod8 = require("zod");
|
|
1093
1150
|
|
|
1094
1151
|
// src/question-adapter.ts
|
|
1095
1152
|
function truncate(str, maxLength) {
|
|
@@ -1275,22 +1332,22 @@ function answersToManifestFormat(answers, questions) {
|
|
|
1275
1332
|
}
|
|
1276
1333
|
|
|
1277
1334
|
// src/tools/questions.ts
|
|
1278
|
-
var ManifestQuestionSchema =
|
|
1279
|
-
id:
|
|
1280
|
-
question:
|
|
1281
|
-
type:
|
|
1282
|
-
required:
|
|
1283
|
-
options:
|
|
1284
|
-
|
|
1285
|
-
label:
|
|
1286
|
-
value:
|
|
1335
|
+
var ManifestQuestionSchema = import_zod8.z.object({
|
|
1336
|
+
id: import_zod8.z.string(),
|
|
1337
|
+
question: import_zod8.z.string(),
|
|
1338
|
+
type: import_zod8.z.enum(["text", "select", "multi-select", "confirm"]),
|
|
1339
|
+
required: import_zod8.z.boolean().optional(),
|
|
1340
|
+
options: import_zod8.z.array(
|
|
1341
|
+
import_zod8.z.object({
|
|
1342
|
+
label: import_zod8.z.string(),
|
|
1343
|
+
value: import_zod8.z.string()
|
|
1287
1344
|
})
|
|
1288
1345
|
).optional(),
|
|
1289
|
-
default:
|
|
1290
|
-
help:
|
|
1291
|
-
dependsOn:
|
|
1292
|
-
questionId:
|
|
1293
|
-
value:
|
|
1346
|
+
default: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.boolean(), import_zod8.z.array(import_zod8.z.string())]).optional(),
|
|
1347
|
+
help: import_zod8.z.string().optional(),
|
|
1348
|
+
dependsOn: import_zod8.z.object({
|
|
1349
|
+
questionId: import_zod8.z.string(),
|
|
1350
|
+
value: import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string())])
|
|
1294
1351
|
}).optional()
|
|
1295
1352
|
});
|
|
1296
1353
|
function registerQuestionTools(server2) {
|
|
@@ -1298,11 +1355,13 @@ function registerQuestionTools(server2) {
|
|
|
1298
1355
|
"stackwright_pro_present_phase_questions",
|
|
1299
1356
|
"Adapt manifest-format questions from a specialist otter and present them to the user via ask_user_question. Pass only the phase name \u2014 this tool reads questions from .stackwright/question-manifest.json automatically. The questions parameter is optional and only needed if the manifest has not been written yet. Use this instead of calling ask_user_question directly \u2014 it handles label truncation (50-char limit), header generation, confirm/text defaults, and correct array formatting automatically. IMPORTANT: This is the ONLY approved way to prepare questions before calling ask_user_question. Never call ask_user_question with raw manifest questions. Never retry ask_user_question validation errors by calling it directly \u2014 always re-call this tool.",
|
|
1300
1357
|
{
|
|
1301
|
-
phase:
|
|
1302
|
-
questions:
|
|
1358
|
+
phase: import_zod8.z.string().describe('Phase name for display context, e.g. "designer", "api", "auth"'),
|
|
1359
|
+
questions: jsonCoerce(import_zod8.z.array(ManifestQuestionSchema).optional()).describe(
|
|
1303
1360
|
"Questions in Question Manifest format. If omitted, questions are read from .stackwright/question-manifest.json using the phase name."
|
|
1304
1361
|
),
|
|
1305
|
-
answers:
|
|
1362
|
+
answers: jsonCoerce(
|
|
1363
|
+
import_zod8.z.record(import_zod8.z.string(), import_zod8.z.union([import_zod8.z.string(), import_zod8.z.array(import_zod8.z.string()), import_zod8.z.boolean()])).optional()
|
|
1364
|
+
).describe("Previously collected answers used to resolve dependsOn conditions")
|
|
1306
1365
|
},
|
|
1307
1366
|
async ({ phase, questions, answers }) => {
|
|
1308
1367
|
let resolvedQuestions;
|
|
@@ -1353,17 +1412,36 @@ function registerQuestionTools(server2) {
|
|
|
1353
1412
|
}
|
|
1354
1413
|
}
|
|
1355
1414
|
const adapted = adaptQuestions(resolvedQuestions, answers ?? {});
|
|
1415
|
+
const labelSchema = import_zod8.z.string().max(50, "Value should have at most 50 characters");
|
|
1416
|
+
for (const q of adapted) {
|
|
1417
|
+
for (const opt of q.options) {
|
|
1418
|
+
const check = labelSchema.safeParse(opt.label);
|
|
1419
|
+
if (!check.success) {
|
|
1420
|
+
return {
|
|
1421
|
+
content: [
|
|
1422
|
+
{
|
|
1423
|
+
type: "text",
|
|
1424
|
+
text: JSON.stringify({
|
|
1425
|
+
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.`
|
|
1426
|
+
})
|
|
1427
|
+
}
|
|
1428
|
+
],
|
|
1429
|
+
isError: true
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1356
1434
|
if (adapted.length === 0) {
|
|
1357
1435
|
return {
|
|
1358
1436
|
content: [
|
|
1359
1437
|
{
|
|
1360
1438
|
type: "text",
|
|
1361
|
-
text:
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1439
|
+
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.`
|
|
1440
|
+
},
|
|
1441
|
+
{
|
|
1442
|
+
type: "text",
|
|
1443
|
+
// Empty array — second block always present so the foreman's two-block contract holds
|
|
1444
|
+
text: JSON.stringify([])
|
|
1367
1445
|
}
|
|
1368
1446
|
],
|
|
1369
1447
|
isError: false
|
|
@@ -1384,10 +1462,148 @@ function registerQuestionTools(server2) {
|
|
|
1384
1462
|
};
|
|
1385
1463
|
}
|
|
1386
1464
|
);
|
|
1465
|
+
server2.tool(
|
|
1466
|
+
"stackwright_pro_get_next_question",
|
|
1467
|
+
"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.",
|
|
1468
|
+
{ phase: import_zod8.z.string().describe('Phase name e.g. "designer", "api", "auth"') },
|
|
1469
|
+
async ({ phase }) => {
|
|
1470
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1471
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1472
|
+
return {
|
|
1473
|
+
content: [
|
|
1474
|
+
{
|
|
1475
|
+
type: "text",
|
|
1476
|
+
text: JSON.stringify({ error: `Invalid phase name: "${phase.slice(0, 50)}"` })
|
|
1477
|
+
}
|
|
1478
|
+
],
|
|
1479
|
+
isError: true
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
const cwd = process.cwd();
|
|
1483
|
+
const questionsPath = (0, import_node_path.join)(cwd, ".stackwright", "questions", `${phase}.json`);
|
|
1484
|
+
let questions = [];
|
|
1485
|
+
try {
|
|
1486
|
+
const raw = await (0, import_promises.readFile)(questionsPath, "utf-8");
|
|
1487
|
+
const parsed = JSON.parse(raw);
|
|
1488
|
+
questions = parsed.questions ?? [];
|
|
1489
|
+
} catch {
|
|
1490
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1491
|
+
}
|
|
1492
|
+
if (questions.length === 0) {
|
|
1493
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1494
|
+
}
|
|
1495
|
+
const answersPath = (0, import_node_path.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1496
|
+
let answeredIds = /* @__PURE__ */ new Set();
|
|
1497
|
+
try {
|
|
1498
|
+
const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
|
|
1499
|
+
const parsed = JSON.parse(raw);
|
|
1500
|
+
answeredIds = new Set(Object.keys(parsed.answers ?? {}));
|
|
1501
|
+
} catch {
|
|
1502
|
+
}
|
|
1503
|
+
const next = questions.find((q) => !answeredIds.has(q.id));
|
|
1504
|
+
if (!next) {
|
|
1505
|
+
return { content: [{ type: "text", text: JSON.stringify({ done: true }) }] };
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
content: [
|
|
1509
|
+
{
|
|
1510
|
+
type: "text",
|
|
1511
|
+
text: JSON.stringify({
|
|
1512
|
+
done: false,
|
|
1513
|
+
questionId: next.id,
|
|
1514
|
+
question: next.question,
|
|
1515
|
+
type: next.type,
|
|
1516
|
+
options: next.options ?? null,
|
|
1517
|
+
help: next.help ?? null,
|
|
1518
|
+
index: questions.indexOf(next) + 1,
|
|
1519
|
+
total: questions.length
|
|
1520
|
+
})
|
|
1521
|
+
}
|
|
1522
|
+
]
|
|
1523
|
+
};
|
|
1524
|
+
}
|
|
1525
|
+
);
|
|
1526
|
+
server2.tool(
|
|
1527
|
+
"stackwright_pro_record_answer",
|
|
1528
|
+
"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.",
|
|
1529
|
+
{
|
|
1530
|
+
phase: import_zod8.z.string().describe('Phase name e.g. "designer"'),
|
|
1531
|
+
questionId: import_zod8.z.string().describe('The question ID from get_next_question, e.g. "designer-1"'),
|
|
1532
|
+
answer: import_zod8.z.string().describe("The user's free-text answer")
|
|
1533
|
+
},
|
|
1534
|
+
async ({ phase, questionId, answer }) => {
|
|
1535
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
1536
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
1537
|
+
return {
|
|
1538
|
+
content: [
|
|
1539
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid phase name" }) }
|
|
1540
|
+
],
|
|
1541
|
+
isError: true
|
|
1542
|
+
};
|
|
1543
|
+
}
|
|
1544
|
+
if (!/^[a-z0-9][a-z0-9-]{0,63}$/i.test(questionId)) {
|
|
1545
|
+
return {
|
|
1546
|
+
content: [
|
|
1547
|
+
{ type: "text", text: JSON.stringify({ error: "Invalid questionId" }) }
|
|
1548
|
+
],
|
|
1549
|
+
isError: true
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
const safeAnswer = answer.slice(0, 2e3);
|
|
1553
|
+
const cwd = process.cwd();
|
|
1554
|
+
const answersDir = (0, import_node_path.join)(cwd, ".stackwright", "answers");
|
|
1555
|
+
const answersPath = (0, import_node_path.join)(answersDir, `${phase}.json`);
|
|
1556
|
+
if ((0, import_node_fs.existsSync)(answersDir) && (0, import_node_fs.lstatSync)(answersDir).isSymbolicLink()) {
|
|
1557
|
+
return {
|
|
1558
|
+
content: [
|
|
1559
|
+
{
|
|
1560
|
+
type: "text",
|
|
1561
|
+
text: JSON.stringify({ error: "answers dir is a symlink \u2014 refusing to write" })
|
|
1562
|
+
}
|
|
1563
|
+
],
|
|
1564
|
+
isError: true
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
(0, import_node_fs.mkdirSync)(answersDir, { recursive: true });
|
|
1568
|
+
let existing = {
|
|
1569
|
+
version: "1.0",
|
|
1570
|
+
phase,
|
|
1571
|
+
answers: {}
|
|
1572
|
+
};
|
|
1573
|
+
try {
|
|
1574
|
+
if ((0, import_node_fs.existsSync)(answersPath)) {
|
|
1575
|
+
if ((0, import_node_fs.lstatSync)(answersPath).isSymbolicLink()) {
|
|
1576
|
+
return {
|
|
1577
|
+
content: [
|
|
1578
|
+
{
|
|
1579
|
+
type: "text",
|
|
1580
|
+
text: JSON.stringify({ error: "answers file is a symlink \u2014 refusing to write" })
|
|
1581
|
+
}
|
|
1582
|
+
],
|
|
1583
|
+
isError: true
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
const raw = await (0, import_promises.readFile)(answersPath, "utf-8");
|
|
1587
|
+
existing = JSON.parse(raw);
|
|
1588
|
+
}
|
|
1589
|
+
} catch {
|
|
1590
|
+
}
|
|
1591
|
+
existing.answers[questionId] = safeAnswer;
|
|
1592
|
+
existing.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1593
|
+
const tmp = `${answersPath}.tmp`;
|
|
1594
|
+
await (0, import_promises.writeFile)(tmp, JSON.stringify(existing, null, 2), "utf-8");
|
|
1595
|
+
(0, import_node_fs.renameSync)(tmp, answersPath);
|
|
1596
|
+
return {
|
|
1597
|
+
content: [
|
|
1598
|
+
{ type: "text", text: JSON.stringify({ recorded: true, phase, questionId }) }
|
|
1599
|
+
]
|
|
1600
|
+
};
|
|
1601
|
+
}
|
|
1602
|
+
);
|
|
1387
1603
|
}
|
|
1388
1604
|
|
|
1389
1605
|
// src/tools/orchestration.ts
|
|
1390
|
-
var
|
|
1606
|
+
var import_zod9 = require("zod");
|
|
1391
1607
|
var import_fs3 = require("fs");
|
|
1392
1608
|
var import_path3 = require("path");
|
|
1393
1609
|
var OTTER_NAME_TO_PHASE = [
|
|
@@ -1555,13 +1771,63 @@ function handleGetOtterName(input) {
|
|
|
1555
1771
|
isError: false
|
|
1556
1772
|
};
|
|
1557
1773
|
}
|
|
1774
|
+
function handleSaveBuildContext(input) {
|
|
1775
|
+
const cwd = input._cwd ?? process.cwd();
|
|
1776
|
+
const dir = (0, import_path3.join)(cwd, ".stackwright");
|
|
1777
|
+
const filePath = (0, import_path3.join)(dir, "build-context.json");
|
|
1778
|
+
try {
|
|
1779
|
+
(0, import_fs3.mkdirSync)(dir, { recursive: true });
|
|
1780
|
+
if ((0, import_fs3.existsSync)(filePath)) {
|
|
1781
|
+
const stat = (0, import_fs3.lstatSync)(filePath);
|
|
1782
|
+
if (stat.isSymbolicLink()) {
|
|
1783
|
+
return {
|
|
1784
|
+
text: JSON.stringify({
|
|
1785
|
+
success: false,
|
|
1786
|
+
error: `Refusing to write to symlink: ${filePath}`
|
|
1787
|
+
}),
|
|
1788
|
+
isError: true
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
const payload = {
|
|
1793
|
+
version: "1.0",
|
|
1794
|
+
savedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1795
|
+
buildContext: input.buildContext
|
|
1796
|
+
};
|
|
1797
|
+
(0, import_fs3.writeFileSync)(filePath, JSON.stringify(payload, null, 2) + "\n");
|
|
1798
|
+
return {
|
|
1799
|
+
text: JSON.stringify({ success: true, path: filePath }),
|
|
1800
|
+
isError: false
|
|
1801
|
+
};
|
|
1802
|
+
} catch (err) {
|
|
1803
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1804
|
+
return {
|
|
1805
|
+
text: JSON.stringify({ success: false, error: message }),
|
|
1806
|
+
isError: true
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1558
1810
|
function registerOrchestrationTools(server2) {
|
|
1811
|
+
server2.tool(
|
|
1812
|
+
"stackwright_pro_save_build_context",
|
|
1813
|
+
`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.`,
|
|
1814
|
+
{
|
|
1815
|
+
buildContext: import_zod9.z.string().describe("Free-text description of what the user wants to build")
|
|
1816
|
+
},
|
|
1817
|
+
async ({ buildContext }) => {
|
|
1818
|
+
const { text, isError } = handleSaveBuildContext({ buildContext });
|
|
1819
|
+
return {
|
|
1820
|
+
content: [{ type: "text", text }],
|
|
1821
|
+
isError
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
);
|
|
1559
1825
|
server2.tool(
|
|
1560
1826
|
"stackwright_pro_parse_otter_response",
|
|
1561
1827
|
"Parse and validate a specialist otter's QUESTION_COLLECTION_MODE JSON response. Handles JSON extraction from LLM responses (strips markdown, fixes single quotes, trailing commas). Detects the phase from the otter name. Use this immediately after invoke_agent() to get a validated manifest phase object.",
|
|
1562
1828
|
{
|
|
1563
|
-
otterName:
|
|
1564
|
-
responseText:
|
|
1829
|
+
otterName: import_zod9.z.string().describe('The agent name, e.g. "stackwright-pro-api-otter"'),
|
|
1830
|
+
responseText: import_zod9.z.string().describe("Raw text response from the otter's QUESTION_COLLECTION_MODE invocation")
|
|
1565
1831
|
},
|
|
1566
1832
|
async ({ otterName, responseText }) => {
|
|
1567
1833
|
const { result, isError } = handleParseOtterResponse({ otterName, responseText });
|
|
@@ -1575,16 +1841,18 @@ function registerOrchestrationTools(server2) {
|
|
|
1575
1841
|
"stackwright_pro_save_manifest",
|
|
1576
1842
|
"Write the question manifest to .stackwright/question-manifest.json. Call this after collecting and parsing questions from all otters via stackwright_pro_parse_otter_response.",
|
|
1577
1843
|
{
|
|
1578
|
-
phases:
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1844
|
+
phases: jsonCoerce(
|
|
1845
|
+
import_zod9.z.array(
|
|
1846
|
+
import_zod9.z.object({
|
|
1847
|
+
phase: import_zod9.z.string(),
|
|
1848
|
+
otter: import_zod9.z.string(),
|
|
1849
|
+
questions: import_zod9.z.array(import_zod9.z.any()),
|
|
1850
|
+
requiredPackages: import_zod9.z.object({
|
|
1851
|
+
dependencies: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional(),
|
|
1852
|
+
devPackages: import_zod9.z.record(import_zod9.z.string(), import_zod9.z.string()).optional()
|
|
1853
|
+
}).optional()
|
|
1854
|
+
})
|
|
1855
|
+
)
|
|
1588
1856
|
).describe("Array of parsed phase objects from stackwright_pro_parse_otter_response")
|
|
1589
1857
|
},
|
|
1590
1858
|
async ({ phases }) => {
|
|
@@ -1599,15 +1867,21 @@ function registerOrchestrationTools(server2) {
|
|
|
1599
1867
|
"stackwright_pro_save_phase_answers",
|
|
1600
1868
|
"Save user answers for a phase to .stackwright/answers/{phase}.json. Pass rawAnswers directly from ask_user_question and the original manifest questions for label-to-value reverse mapping.",
|
|
1601
1869
|
{
|
|
1602
|
-
phase:
|
|
1603
|
-
rawAnswers:
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1870
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer"'),
|
|
1871
|
+
rawAnswers: jsonCoerce(
|
|
1872
|
+
import_zod9.z.array(
|
|
1873
|
+
import_zod9.z.object({
|
|
1874
|
+
question_header: import_zod9.z.string(),
|
|
1875
|
+
selected_options: import_zod9.z.array(import_zod9.z.string()),
|
|
1876
|
+
other_text: import_zod9.z.string().nullable().optional()
|
|
1877
|
+
})
|
|
1878
|
+
)
|
|
1879
|
+
).describe(
|
|
1880
|
+
"Answers as returned by ask_user_question \u2014 pass the native array, not a JSON string"
|
|
1881
|
+
),
|
|
1882
|
+
questions: jsonCoerce(import_zod9.z.array(import_zod9.z.any()).optional()).describe(
|
|
1883
|
+
"Original manifest questions for label\u2192value reverse-mapping \u2014 pass the native array, not a JSON string"
|
|
1884
|
+
)
|
|
1611
1885
|
},
|
|
1612
1886
|
async ({ phase, rawAnswers, questions }) => {
|
|
1613
1887
|
const { text, isError } = handleSavePhaseAnswers({
|
|
@@ -1625,7 +1899,7 @@ function registerOrchestrationTools(server2) {
|
|
|
1625
1899
|
"stackwright_pro_read_phase_answers",
|
|
1626
1900
|
"Read saved answers for a phase from .stackwright/answers/{phase}.json. Returns { missing: true } when no answers exist yet \u2014 use this to skip phases safely in the execution loop.",
|
|
1627
1901
|
{
|
|
1628
|
-
phase:
|
|
1902
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer"')
|
|
1629
1903
|
},
|
|
1630
1904
|
async ({ phase }) => {
|
|
1631
1905
|
const { text, isError } = handleReadPhaseAnswers({ phase });
|
|
@@ -1639,41 +1913,373 @@ function registerOrchestrationTools(server2) {
|
|
|
1639
1913
|
"stackwright_pro_get_otter_name",
|
|
1640
1914
|
"Get the agent name for a phase (e.g. 'designer' \u2192 'stackwright-pro-designer-otter'). Use this in the execution loop to invoke the correct specialist otter without hardcoding names in the prompt.",
|
|
1641
1915
|
{
|
|
1642
|
-
phase:
|
|
1916
|
+
phase: import_zod9.z.string().describe('Phase name, e.g. "designer", "api", "pages"')
|
|
1643
1917
|
},
|
|
1644
1918
|
async ({ phase }) => {
|
|
1645
1919
|
const { text, isError } = handleGetOtterName({ phase });
|
|
1646
1920
|
return {
|
|
1647
|
-
content: [{ type: "text", text }],
|
|
1648
|
-
isError
|
|
1921
|
+
content: [{ type: "text", text }],
|
|
1922
|
+
isError
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// src/tools/pipeline.ts
|
|
1929
|
+
var import_zod11 = require("zod");
|
|
1930
|
+
var import_fs5 = require("fs");
|
|
1931
|
+
var import_path5 = require("path");
|
|
1932
|
+
var import_crypto3 = require("crypto");
|
|
1933
|
+
|
|
1934
|
+
// src/artifact-signing.ts
|
|
1935
|
+
var import_crypto2 = require("crypto");
|
|
1936
|
+
var import_fs4 = require("fs");
|
|
1937
|
+
var import_path4 = require("path");
|
|
1938
|
+
var import_zod10 = require("zod");
|
|
1939
|
+
var ALGORITHM = "ECDSA-P384-SHA384";
|
|
1940
|
+
var KEY_FILE = "pipeline-keys.json";
|
|
1941
|
+
var KEY_DIR = ".stackwright";
|
|
1942
|
+
var SIGNATURE_MANIFEST = "signatures.json";
|
|
1943
|
+
var ARTIFACTS_DIR = ".stackwright/artifacts";
|
|
1944
|
+
function rejectSymlink(filePath, context) {
|
|
1945
|
+
if (!(0, import_fs4.existsSync)(filePath)) return;
|
|
1946
|
+
const stat = (0, import_fs4.lstatSync)(filePath);
|
|
1947
|
+
if (stat.isSymbolicLink()) {
|
|
1948
|
+
throw new Error(`Security: refusing to follow symlink at ${context}: ${filePath}`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
function computeSha384(data) {
|
|
1952
|
+
return (0, import_crypto2.createHash)("sha384").update(data).digest("hex");
|
|
1953
|
+
}
|
|
1954
|
+
function safeDigestEqual(a, b) {
|
|
1955
|
+
if (a.length !== b.length) return false;
|
|
1956
|
+
return (0, import_crypto2.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
1957
|
+
}
|
|
1958
|
+
function emptyManifest() {
|
|
1959
|
+
return {
|
|
1960
|
+
version: "1.0",
|
|
1961
|
+
algorithm: ALGORITHM,
|
|
1962
|
+
signatures: {}
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function initPipelineKeys(cwd) {
|
|
1966
|
+
const keyDir = (0, import_path4.join)(cwd, KEY_DIR);
|
|
1967
|
+
const keyPath = (0, import_path4.join)(keyDir, KEY_FILE);
|
|
1968
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1969
|
+
(0, import_fs4.mkdirSync)(keyDir, { recursive: true });
|
|
1970
|
+
const { publicKey, privateKey } = (0, import_crypto2.generateKeyPairSync)("ec", {
|
|
1971
|
+
namedCurve: "P-384"
|
|
1972
|
+
});
|
|
1973
|
+
const publicKeyPem = publicKey.export({ type: "spki", format: "pem" });
|
|
1974
|
+
const privateKeyPem = privateKey.export({ type: "pkcs8", format: "pem" });
|
|
1975
|
+
const fingerprint = (0, import_crypto2.createHash)("sha256").update(publicKeyPem).digest("hex");
|
|
1976
|
+
const keyFile = {
|
|
1977
|
+
version: "1.0",
|
|
1978
|
+
algorithm: ALGORITHM,
|
|
1979
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1980
|
+
publicKeyPem,
|
|
1981
|
+
privateKeyPem
|
|
1982
|
+
};
|
|
1983
|
+
(0, import_fs4.writeFileSync)(keyPath, JSON.stringify(keyFile, null, 2), { encoding: "utf-8" });
|
|
1984
|
+
return { publicKeyPem, fingerprint };
|
|
1985
|
+
}
|
|
1986
|
+
function loadPipelineKeys(cwd) {
|
|
1987
|
+
const keyPath = (0, import_path4.join)(cwd, KEY_DIR, KEY_FILE);
|
|
1988
|
+
rejectSymlink(keyPath, "pipeline-keys.json");
|
|
1989
|
+
if (!(0, import_fs4.existsSync)(keyPath)) {
|
|
1990
|
+
throw new Error("Pipeline keys not found \u2014 call initPipelineKeys() first");
|
|
1991
|
+
}
|
|
1992
|
+
let raw;
|
|
1993
|
+
try {
|
|
1994
|
+
raw = (0, import_fs4.readFileSync)(keyPath, "utf-8");
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1997
|
+
throw new Error(`Cannot read pipeline keys: ${msg}`, { cause: err });
|
|
1998
|
+
}
|
|
1999
|
+
let parsed;
|
|
2000
|
+
try {
|
|
2001
|
+
parsed = JSON.parse(raw);
|
|
2002
|
+
} catch {
|
|
2003
|
+
throw new Error("Pipeline keys file is not valid JSON");
|
|
2004
|
+
}
|
|
2005
|
+
if (typeof parsed.publicKeyPem !== "string" || !parsed.publicKeyPem.includes("-----BEGIN PUBLIC KEY-----")) {
|
|
2006
|
+
throw new Error("Invalid public key PEM in pipeline keys file");
|
|
2007
|
+
}
|
|
2008
|
+
if (typeof parsed.privateKeyPem !== "string" || !parsed.privateKeyPem.includes("-----BEGIN")) {
|
|
2009
|
+
throw new Error("Invalid private key PEM in pipeline keys file");
|
|
2010
|
+
}
|
|
2011
|
+
const publicKey = (0, import_crypto2.createPublicKey)(parsed.publicKeyPem);
|
|
2012
|
+
const privateKey = (0, import_crypto2.createPrivateKey)(parsed.privateKeyPem);
|
|
2013
|
+
return { privateKey, publicKey };
|
|
2014
|
+
}
|
|
2015
|
+
function signArtifact(artifactBytes, privateKey) {
|
|
2016
|
+
const digest = computeSha384(artifactBytes);
|
|
2017
|
+
const sig = (0, import_crypto2.sign)("SHA384", artifactBytes, privateKey);
|
|
2018
|
+
return {
|
|
2019
|
+
digest,
|
|
2020
|
+
signature: sig.toString("base64"),
|
|
2021
|
+
algorithm: ALGORITHM,
|
|
2022
|
+
signedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2023
|
+
};
|
|
2024
|
+
}
|
|
2025
|
+
function verifyArtifact(artifactBytes, signature, publicKey) {
|
|
2026
|
+
const sigValid = (0, import_crypto2.verify)(
|
|
2027
|
+
"SHA384",
|
|
2028
|
+
artifactBytes,
|
|
2029
|
+
publicKey,
|
|
2030
|
+
Buffer.from(signature.signature, "base64")
|
|
2031
|
+
);
|
|
2032
|
+
if (!sigValid) return false;
|
|
2033
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2034
|
+
return safeDigestEqual(actualDigest, signature.digest);
|
|
2035
|
+
}
|
|
2036
|
+
function loadSignatureManifest(cwd) {
|
|
2037
|
+
const manifestPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR, SIGNATURE_MANIFEST);
|
|
2038
|
+
rejectSymlink(manifestPath, "signatures.json");
|
|
2039
|
+
if (!(0, import_fs4.existsSync)(manifestPath)) return emptyManifest();
|
|
2040
|
+
let raw;
|
|
2041
|
+
try {
|
|
2042
|
+
raw = (0, import_fs4.readFileSync)(manifestPath, "utf-8");
|
|
2043
|
+
} catch (err) {
|
|
2044
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2045
|
+
throw new Error(`Cannot read signature manifest: ${msg}`, { cause: err });
|
|
2046
|
+
}
|
|
2047
|
+
try {
|
|
2048
|
+
const parsed = JSON.parse(raw);
|
|
2049
|
+
if (parsed.version !== "1.0" || typeof parsed.signatures !== "object") {
|
|
2050
|
+
throw new Error("Malformed signature manifest: invalid version or missing signatures object");
|
|
2051
|
+
}
|
|
2052
|
+
return parsed;
|
|
2053
|
+
} catch (err) {
|
|
2054
|
+
if (err instanceof Error && err.message.startsWith("Malformed")) throw err;
|
|
2055
|
+
throw new Error("Signature manifest is not valid JSON", { cause: err });
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
function saveArtifactSignature(cwd, artifactFilename, sig, signerOtter) {
|
|
2059
|
+
const artifactsDir = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2060
|
+
const manifestPath = (0, import_path4.join)(artifactsDir, SIGNATURE_MANIFEST);
|
|
2061
|
+
rejectSymlink(manifestPath, "signatures.json (save)");
|
|
2062
|
+
(0, import_fs4.mkdirSync)(artifactsDir, { recursive: true });
|
|
2063
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2064
|
+
manifest.signatures[artifactFilename] = {
|
|
2065
|
+
...sig,
|
|
2066
|
+
signedBy: signerOtter
|
|
2067
|
+
};
|
|
2068
|
+
(0, import_fs4.writeFileSync)(manifestPath, JSON.stringify(manifest, null, 2), { encoding: "utf-8" });
|
|
2069
|
+
}
|
|
2070
|
+
function getArtifactSignature(cwd, artifactFilename) {
|
|
2071
|
+
const manifest = loadSignatureManifest(cwd);
|
|
2072
|
+
const entry = manifest.signatures[artifactFilename];
|
|
2073
|
+
if (!entry) return null;
|
|
2074
|
+
return {
|
|
2075
|
+
digest: entry.digest,
|
|
2076
|
+
signature: entry.signature,
|
|
2077
|
+
algorithm: entry.algorithm,
|
|
2078
|
+
signedAt: entry.signedAt
|
|
2079
|
+
};
|
|
2080
|
+
}
|
|
2081
|
+
function emitSignatureAuditEvent(params) {
|
|
2082
|
+
const record = JSON.stringify({
|
|
2083
|
+
level: "AUDIT",
|
|
2084
|
+
event: "ARTIFACT_SIGNATURE_FAIL",
|
|
2085
|
+
timestamp: params.timestamp,
|
|
2086
|
+
source: params.source,
|
|
2087
|
+
artifactFilename: params.artifactFilename,
|
|
2088
|
+
expectedDigest: params.expectedDigest,
|
|
2089
|
+
actualDigest: params.actualDigest,
|
|
2090
|
+
phase: params.phase
|
|
2091
|
+
});
|
|
2092
|
+
process.stderr.write(`ARTIFACT_SIGNATURE_FAIL ${record}
|
|
2093
|
+
`);
|
|
2094
|
+
}
|
|
2095
|
+
function registerArtifactSigningTools(server2) {
|
|
2096
|
+
server2.tool(
|
|
2097
|
+
"stackwright_pro_verify_artifact_signatures",
|
|
2098
|
+
"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.",
|
|
2099
|
+
{
|
|
2100
|
+
cwd: import_zod10.z.string().optional().describe("Project root directory. Defaults to process.cwd().")
|
|
2101
|
+
},
|
|
2102
|
+
async ({ cwd: cwdParam }) => {
|
|
2103
|
+
const cwd = cwdParam ?? process.cwd();
|
|
2104
|
+
let publicKey;
|
|
2105
|
+
try {
|
|
2106
|
+
const keys = loadPipelineKeys(cwd);
|
|
2107
|
+
publicKey = keys.publicKey;
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2110
|
+
return {
|
|
2111
|
+
content: [
|
|
2112
|
+
{
|
|
2113
|
+
type: "text",
|
|
2114
|
+
text: JSON.stringify({
|
|
2115
|
+
error: true,
|
|
2116
|
+
message: `Cannot load pipeline keys: ${msg}`
|
|
2117
|
+
})
|
|
2118
|
+
}
|
|
2119
|
+
],
|
|
2120
|
+
isError: true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2123
|
+
let manifest;
|
|
2124
|
+
try {
|
|
2125
|
+
manifest = loadSignatureManifest(cwd);
|
|
2126
|
+
} catch (err) {
|
|
2127
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2128
|
+
return {
|
|
2129
|
+
content: [
|
|
2130
|
+
{
|
|
2131
|
+
type: "text",
|
|
2132
|
+
text: JSON.stringify({
|
|
2133
|
+
error: true,
|
|
2134
|
+
message: `Cannot load signature manifest: ${msg}`
|
|
2135
|
+
})
|
|
2136
|
+
}
|
|
2137
|
+
],
|
|
2138
|
+
isError: true
|
|
2139
|
+
};
|
|
2140
|
+
}
|
|
2141
|
+
const artifactsPath = (0, import_path4.join)(cwd, ARTIFACTS_DIR);
|
|
2142
|
+
let artifactFiles = [];
|
|
2143
|
+
try {
|
|
2144
|
+
if ((0, import_fs4.existsSync)(artifactsPath)) {
|
|
2145
|
+
artifactFiles = (0, import_fs4.readdirSync)(artifactsPath).filter(
|
|
2146
|
+
(f) => f.endsWith(".json") && f !== SIGNATURE_MANIFEST
|
|
2147
|
+
);
|
|
2148
|
+
}
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
const results = [];
|
|
2152
|
+
let hasFailure = false;
|
|
2153
|
+
for (const filename of artifactFiles) {
|
|
2154
|
+
const filePath = (0, import_path4.join)(artifactsPath, filename);
|
|
2155
|
+
try {
|
|
2156
|
+
rejectSymlink(filePath, `artifact ${filename}`);
|
|
2157
|
+
} catch {
|
|
2158
|
+
results.push({
|
|
2159
|
+
filename,
|
|
2160
|
+
verified: false,
|
|
2161
|
+
error: "Refusing to verify symlink"
|
|
2162
|
+
});
|
|
2163
|
+
hasFailure = true;
|
|
2164
|
+
continue;
|
|
2165
|
+
}
|
|
2166
|
+
let artifactBytes;
|
|
2167
|
+
try {
|
|
2168
|
+
artifactBytes = (0, import_fs4.readFileSync)(filePath);
|
|
2169
|
+
} catch (err) {
|
|
2170
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2171
|
+
results.push({
|
|
2172
|
+
filename,
|
|
2173
|
+
verified: false,
|
|
2174
|
+
error: `Cannot read artifact: ${msg}`
|
|
2175
|
+
});
|
|
2176
|
+
hasFailure = true;
|
|
2177
|
+
continue;
|
|
2178
|
+
}
|
|
2179
|
+
const entry = manifest.signatures[filename];
|
|
2180
|
+
if (!entry) {
|
|
2181
|
+
results.push({
|
|
2182
|
+
filename,
|
|
2183
|
+
verified: false,
|
|
2184
|
+
error: "No signature found in manifest"
|
|
2185
|
+
});
|
|
2186
|
+
hasFailure = true;
|
|
2187
|
+
continue;
|
|
2188
|
+
}
|
|
2189
|
+
const sig = {
|
|
2190
|
+
digest: entry.digest,
|
|
2191
|
+
signature: entry.signature,
|
|
2192
|
+
algorithm: entry.algorithm,
|
|
2193
|
+
signedAt: entry.signedAt
|
|
2194
|
+
};
|
|
2195
|
+
const verified = verifyArtifact(artifactBytes, sig, publicKey);
|
|
2196
|
+
if (!verified) {
|
|
2197
|
+
const actualDigest = computeSha384(artifactBytes);
|
|
2198
|
+
emitSignatureAuditEvent({
|
|
2199
|
+
artifactFilename: filename,
|
|
2200
|
+
expectedDigest: sig.digest,
|
|
2201
|
+
actualDigest,
|
|
2202
|
+
phase: entry.signedBy ?? "unknown",
|
|
2203
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2204
|
+
source: "stackwright_pro_verify_artifact_signatures"
|
|
2205
|
+
});
|
|
2206
|
+
results.push({
|
|
2207
|
+
filename,
|
|
2208
|
+
verified: false,
|
|
2209
|
+
error: `Signature verification failed \u2014 artifact may have been tampered with`,
|
|
2210
|
+
signedBy: entry.signedBy,
|
|
2211
|
+
signedAt: entry.signedAt
|
|
2212
|
+
});
|
|
2213
|
+
hasFailure = true;
|
|
2214
|
+
} else {
|
|
2215
|
+
results.push({
|
|
2216
|
+
filename,
|
|
2217
|
+
verified: true,
|
|
2218
|
+
signedBy: entry.signedBy,
|
|
2219
|
+
signedAt: entry.signedAt
|
|
2220
|
+
});
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
for (const manifestFilename of Object.keys(manifest.signatures)) {
|
|
2224
|
+
if (!artifactFiles.includes(manifestFilename)) {
|
|
2225
|
+
results.push({
|
|
2226
|
+
filename: manifestFilename,
|
|
2227
|
+
verified: false,
|
|
2228
|
+
error: "Artifact referenced in manifest but missing from disk"
|
|
2229
|
+
});
|
|
2230
|
+
hasFailure = true;
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
const verifiedCount = results.filter((r) => r.verified).length;
|
|
2234
|
+
const failedCount = results.filter((r) => !r.verified).length;
|
|
2235
|
+
return {
|
|
2236
|
+
content: [
|
|
2237
|
+
{
|
|
2238
|
+
type: "text",
|
|
2239
|
+
text: JSON.stringify({
|
|
2240
|
+
totalArtifacts: artifactFiles.length,
|
|
2241
|
+
verifiedCount,
|
|
2242
|
+
failedCount,
|
|
2243
|
+
results,
|
|
2244
|
+
...hasFailure ? {
|
|
2245
|
+
error: "SIGNATURE VERIFICATION FAILED: One or more artifact signatures are invalid. Do not proceed \u2014 artifacts may have been tampered with."
|
|
2246
|
+
} : {}
|
|
2247
|
+
})
|
|
2248
|
+
}
|
|
2249
|
+
],
|
|
2250
|
+
isError: hasFailure
|
|
1649
2251
|
};
|
|
1650
2252
|
}
|
|
1651
2253
|
);
|
|
1652
2254
|
}
|
|
1653
2255
|
|
|
1654
2256
|
// src/tools/pipeline.ts
|
|
1655
|
-
var
|
|
1656
|
-
var import_fs4 = require("fs");
|
|
1657
|
-
var import_path4 = require("path");
|
|
2257
|
+
var import_types = require("@stackwright-pro/types");
|
|
1658
2258
|
var PHASE_ORDER = [
|
|
1659
2259
|
"designer",
|
|
1660
2260
|
"theme",
|
|
1661
2261
|
"api",
|
|
1662
|
-
"auth",
|
|
1663
2262
|
"data",
|
|
2263
|
+
"workflow",
|
|
1664
2264
|
"pages",
|
|
1665
2265
|
"dashboard",
|
|
1666
|
-
"
|
|
2266
|
+
"auth"
|
|
1667
2267
|
];
|
|
1668
2268
|
var PHASE_DEPENDENCIES = {
|
|
1669
2269
|
designer: [],
|
|
1670
2270
|
theme: ["designer"],
|
|
1671
2271
|
api: [],
|
|
1672
|
-
auth: [],
|
|
1673
2272
|
data: ["api"],
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
workflow: [
|
|
2273
|
+
// workflow: no hard deps — uses service: references with Prism mock fallback
|
|
2274
|
+
// when API artifacts aren't available; roles come from user answers
|
|
2275
|
+
workflow: [],
|
|
2276
|
+
// pages: 'api' is transitive through 'data'; auth removed — page-otter has
|
|
2277
|
+
// graceful fallback and reads role names from workflow artifacts instead
|
|
2278
|
+
pages: ["designer", "theme", "data"],
|
|
2279
|
+
dashboard: ["designer", "theme", "data"],
|
|
2280
|
+
// auth is the terminal phase — reads all available artifacts at runtime;
|
|
2281
|
+
// no hard upstream requirements so it always runs and never gets skipped
|
|
2282
|
+
auth: []
|
|
1677
2283
|
};
|
|
1678
2284
|
var PHASE_ARTIFACT = {
|
|
1679
2285
|
designer: "design-language.json",
|
|
@@ -1723,11 +2329,11 @@ function createDefaultState() {
|
|
|
1723
2329
|
};
|
|
1724
2330
|
}
|
|
1725
2331
|
function statePath(cwd) {
|
|
1726
|
-
return (0,
|
|
2332
|
+
return (0, import_path5.join)(cwd, ".stackwright", "pipeline-state.json");
|
|
1727
2333
|
}
|
|
1728
2334
|
function readState(cwd) {
|
|
1729
2335
|
const p = statePath(cwd);
|
|
1730
|
-
if (!(0,
|
|
2336
|
+
if (!(0, import_fs5.existsSync)(p)) return createDefaultState();
|
|
1731
2337
|
const raw = JSON.parse(safeReadSync(p));
|
|
1732
2338
|
if (typeof raw !== "object" || raw === null || raw.version !== "1.0") {
|
|
1733
2339
|
return createDefaultState();
|
|
@@ -1735,26 +2341,26 @@ function readState(cwd) {
|
|
|
1735
2341
|
return raw;
|
|
1736
2342
|
}
|
|
1737
2343
|
function safeWriteSync(filePath, content) {
|
|
1738
|
-
if ((0,
|
|
1739
|
-
const stat = (0,
|
|
2344
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2345
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
1740
2346
|
if (stat.isSymbolicLink()) {
|
|
1741
2347
|
throw new Error(`Refusing to write to symlink: ${filePath}`);
|
|
1742
2348
|
}
|
|
1743
2349
|
}
|
|
1744
|
-
(0,
|
|
2350
|
+
(0, import_fs5.writeFileSync)(filePath, content);
|
|
1745
2351
|
}
|
|
1746
2352
|
function safeReadSync(filePath) {
|
|
1747
|
-
if ((0,
|
|
1748
|
-
const stat = (0,
|
|
2353
|
+
if ((0, import_fs5.existsSync)(filePath)) {
|
|
2354
|
+
const stat = (0, import_fs5.lstatSync)(filePath);
|
|
1749
2355
|
if (stat.isSymbolicLink()) {
|
|
1750
2356
|
throw new Error(`Refusing to read symlink: ${filePath}`);
|
|
1751
2357
|
}
|
|
1752
2358
|
}
|
|
1753
|
-
return (0,
|
|
2359
|
+
return (0, import_fs5.readFileSync)(filePath, "utf-8");
|
|
1754
2360
|
}
|
|
1755
2361
|
function writeState(cwd, state) {
|
|
1756
|
-
const dir = (0,
|
|
1757
|
-
(0,
|
|
2362
|
+
const dir = (0, import_path5.join)(cwd, ".stackwright");
|
|
2363
|
+
(0, import_fs5.mkdirSync)(dir, { recursive: true });
|
|
1758
2364
|
state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
1759
2365
|
safeWriteSync(statePath(cwd), JSON.stringify(state, null, 2) + "\n");
|
|
1760
2366
|
}
|
|
@@ -1775,6 +2381,15 @@ function handleGetPipelineState(_cwd) {
|
|
|
1775
2381
|
const cwd = _cwd ?? process.cwd();
|
|
1776
2382
|
try {
|
|
1777
2383
|
const state = readState(cwd);
|
|
2384
|
+
const keyPath = (0, import_path5.join)(cwd, ".stackwright", "pipeline-keys.json");
|
|
2385
|
+
if (!(0, import_fs5.existsSync)(keyPath)) {
|
|
2386
|
+
try {
|
|
2387
|
+
const { fingerprint } = initPipelineKeys(cwd);
|
|
2388
|
+
state["signingKeyFingerprint"] = fingerprint;
|
|
2389
|
+
writeState(cwd, state);
|
|
2390
|
+
} catch {
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
1778
2393
|
return { text: JSON.stringify(state), isError: false };
|
|
1779
2394
|
} catch (err) {
|
|
1780
2395
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
@@ -1826,28 +2441,62 @@ function handleSetPipelineState(input) {
|
|
|
1826
2441
|
return { text: JSON.stringify({ error: true, message: String(err) }), isError: true };
|
|
1827
2442
|
}
|
|
1828
2443
|
}
|
|
1829
|
-
function handleCheckExecutionReady(_cwd) {
|
|
2444
|
+
function handleCheckExecutionReady(_cwd, phase) {
|
|
1830
2445
|
const cwd = _cwd ?? process.cwd();
|
|
2446
|
+
if (phase) {
|
|
2447
|
+
if (!isValidPhase(phase)) {
|
|
2448
|
+
return {
|
|
2449
|
+
text: JSON.stringify({
|
|
2450
|
+
error: true,
|
|
2451
|
+
message: `Invalid phase: ${phase}. Valid phases are: ${PHASE_ORDER.join(", ")}`
|
|
2452
|
+
}),
|
|
2453
|
+
isError: true
|
|
2454
|
+
};
|
|
2455
|
+
}
|
|
2456
|
+
const answerFile = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
2457
|
+
if (!(0, import_fs5.existsSync)(answerFile)) {
|
|
2458
|
+
return {
|
|
2459
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file not found" }),
|
|
2460
|
+
isError: false
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
try {
|
|
2464
|
+
const raw = safeReadSync(answerFile);
|
|
2465
|
+
const parsed = JSON.parse(raw);
|
|
2466
|
+
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
2467
|
+
return {
|
|
2468
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file is malformed" }),
|
|
2469
|
+
isError: false
|
|
2470
|
+
};
|
|
2471
|
+
}
|
|
2472
|
+
return { text: JSON.stringify({ ready: true, phase }), isError: false };
|
|
2473
|
+
} catch {
|
|
2474
|
+
return {
|
|
2475
|
+
text: JSON.stringify({ ready: false, phase, reason: "Answer file could not be parsed" }),
|
|
2476
|
+
isError: false
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
1831
2480
|
try {
|
|
1832
|
-
const answersDir = (0,
|
|
2481
|
+
const answersDir = (0, import_path5.join)(cwd, ".stackwright", "answers");
|
|
1833
2482
|
const answeredPhases = [];
|
|
1834
2483
|
const missingPhases = [];
|
|
1835
|
-
for (const
|
|
1836
|
-
const answerFile = (0,
|
|
1837
|
-
if ((0,
|
|
2484
|
+
for (const phase2 of PHASE_ORDER) {
|
|
2485
|
+
const answerFile = (0, import_path5.join)(answersDir, `${phase2}.json`);
|
|
2486
|
+
if ((0, import_fs5.existsSync)(answerFile)) {
|
|
1838
2487
|
try {
|
|
1839
2488
|
const raw = safeReadSync(answerFile);
|
|
1840
2489
|
const parsed = JSON.parse(raw);
|
|
1841
2490
|
if (typeof parsed["version"] !== "string" || typeof parsed["phase"] !== "string" || typeof parsed["answers"] !== "object" || parsed["answers"] === null) {
|
|
1842
|
-
missingPhases.push(
|
|
2491
|
+
missingPhases.push(phase2);
|
|
1843
2492
|
continue;
|
|
1844
2493
|
}
|
|
1845
|
-
answeredPhases.push(
|
|
2494
|
+
answeredPhases.push(phase2);
|
|
1846
2495
|
} catch {
|
|
1847
|
-
missingPhases.push(
|
|
2496
|
+
missingPhases.push(phase2);
|
|
1848
2497
|
}
|
|
1849
2498
|
} else {
|
|
1850
|
-
missingPhases.push(
|
|
2499
|
+
missingPhases.push(phase2);
|
|
1851
2500
|
}
|
|
1852
2501
|
}
|
|
1853
2502
|
return {
|
|
@@ -1866,15 +2515,35 @@ function handleCheckExecutionReady(_cwd) {
|
|
|
1866
2515
|
function handleListArtifacts(_cwd) {
|
|
1867
2516
|
const cwd = _cwd ?? process.cwd();
|
|
1868
2517
|
try {
|
|
1869
|
-
const artifactsDir = (0,
|
|
2518
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
2519
|
+
let manifest = null;
|
|
2520
|
+
try {
|
|
2521
|
+
manifest = loadSignatureManifest(cwd);
|
|
2522
|
+
} catch {
|
|
2523
|
+
}
|
|
1870
2524
|
const artifacts = [];
|
|
1871
2525
|
let completedCount = 0;
|
|
1872
2526
|
for (const phase of PHASE_ORDER) {
|
|
1873
2527
|
const expectedFile = PHASE_ARTIFACT[phase];
|
|
1874
|
-
const fullPath = (0,
|
|
1875
|
-
const exists = (0,
|
|
2528
|
+
const fullPath = (0, import_path5.join)(artifactsDir, expectedFile);
|
|
2529
|
+
const exists = (0, import_fs5.existsSync)(fullPath);
|
|
1876
2530
|
if (exists) completedCount++;
|
|
1877
|
-
|
|
2531
|
+
let signed = false;
|
|
2532
|
+
let signatureValid = null;
|
|
2533
|
+
if (exists && manifest) {
|
|
2534
|
+
const entry = manifest.signatures[expectedFile];
|
|
2535
|
+
if (entry) {
|
|
2536
|
+
signed = true;
|
|
2537
|
+
try {
|
|
2538
|
+
const rawBytes = Buffer.from(safeReadSync(fullPath), "utf-8");
|
|
2539
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2540
|
+
signatureValid = verifyArtifact(rawBytes, entry, publicKey);
|
|
2541
|
+
} catch {
|
|
2542
|
+
signatureValid = null;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
artifacts.push({ phase, expectedFile, exists, path: fullPath, signed, signatureValid });
|
|
1878
2547
|
}
|
|
1879
2548
|
return {
|
|
1880
2549
|
text: JSON.stringify({ artifacts, completedCount, totalCount: PHASE_ORDER.length }),
|
|
@@ -1910,9 +2579,9 @@ function handleWritePhaseQuestions(input) {
|
|
|
1910
2579
|
}
|
|
1911
2580
|
} catch {
|
|
1912
2581
|
}
|
|
1913
|
-
const questionsDir = (0,
|
|
1914
|
-
(0,
|
|
1915
|
-
const filePath = (0,
|
|
2582
|
+
const questionsDir = (0, import_path5.join)(cwd, ".stackwright", "questions");
|
|
2583
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
2584
|
+
const filePath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
1916
2585
|
const payload = {
|
|
1917
2586
|
version: "1.0",
|
|
1918
2587
|
phase,
|
|
@@ -1952,36 +2621,81 @@ function handleBuildSpecialistPrompt(input) {
|
|
|
1952
2621
|
};
|
|
1953
2622
|
}
|
|
1954
2623
|
try {
|
|
1955
|
-
const answersPath = (0,
|
|
2624
|
+
const answersPath = (0, import_path5.join)(cwd, ".stackwright", "answers", `${phase}.json`);
|
|
1956
2625
|
let answers = {};
|
|
1957
|
-
if ((0,
|
|
2626
|
+
if ((0, import_fs5.existsSync)(answersPath)) {
|
|
1958
2627
|
answers = JSON.parse(safeReadSync(answersPath));
|
|
1959
2628
|
}
|
|
2629
|
+
let buildContextText = "";
|
|
2630
|
+
const buildContextPath = (0, import_path5.join)(cwd, ".stackwright", "build-context.json");
|
|
2631
|
+
if ((0, import_fs5.existsSync)(buildContextPath)) {
|
|
2632
|
+
try {
|
|
2633
|
+
const bcRaw = JSON.parse(safeReadSync(buildContextPath));
|
|
2634
|
+
if (typeof bcRaw.buildContext === "string" && bcRaw.buildContext.trim().length > 0) {
|
|
2635
|
+
buildContextText = bcRaw.buildContext.trim();
|
|
2636
|
+
}
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
1960
2640
|
const deps = PHASE_DEPENDENCIES[phase];
|
|
1961
2641
|
const artifactSections = [];
|
|
1962
2642
|
const missingDependencies = [];
|
|
1963
2643
|
for (const dep of deps) {
|
|
1964
2644
|
const artifactFile = PHASE_ARTIFACT[dep];
|
|
1965
|
-
const artifactPath = (0,
|
|
1966
|
-
if ((0,
|
|
1967
|
-
const
|
|
2645
|
+
const artifactPath = (0, import_path5.join)(cwd, ".stackwright", "artifacts", artifactFile);
|
|
2646
|
+
if ((0, import_fs5.existsSync)(artifactPath)) {
|
|
2647
|
+
const rawContent = safeReadSync(artifactPath);
|
|
2648
|
+
const rawBytes = Buffer.from(rawContent, "utf-8");
|
|
2649
|
+
const content = JSON.parse(rawContent);
|
|
2650
|
+
let signatureVerified = false;
|
|
2651
|
+
let signatureAvailable = false;
|
|
2652
|
+
try {
|
|
2653
|
+
const sig = getArtifactSignature(cwd, artifactFile);
|
|
2654
|
+
if (sig) {
|
|
2655
|
+
signatureAvailable = true;
|
|
2656
|
+
const { publicKey } = loadPipelineKeys(cwd);
|
|
2657
|
+
signatureVerified = verifyArtifact(rawBytes, sig, publicKey);
|
|
2658
|
+
if (!signatureVerified) {
|
|
2659
|
+
const actualDigest = (0, import_crypto3.createHash)("sha384").update(rawBytes).digest("hex");
|
|
2660
|
+
emitSignatureAuditEvent({
|
|
2661
|
+
artifactFilename: artifactFile,
|
|
2662
|
+
expectedDigest: sig.digest,
|
|
2663
|
+
actualDigest,
|
|
2664
|
+
phase: dep,
|
|
2665
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2666
|
+
source: "stackwright_pro_build_specialist_prompt"
|
|
2667
|
+
});
|
|
2668
|
+
missingDependencies.push(dep);
|
|
2669
|
+
artifactSections.push(
|
|
2670
|
+
`[${artifactFile}]:
|
|
2671
|
+
(integrity check failed: ECDSA-P384 signature verification failed \u2014 artifact may have been tampered with)`
|
|
2672
|
+
);
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
}
|
|
2676
|
+
} catch {
|
|
2677
|
+
}
|
|
1968
2678
|
const expectedOtter = PHASE_TO_OTTER2[dep];
|
|
1969
2679
|
const artifactOtter = content["generatedBy"];
|
|
2680
|
+
const normalizedOtter = artifactOtter?.replace(/-[a-f0-9]{6}$/, "");
|
|
1970
2681
|
if (!artifactOtter) {
|
|
1971
2682
|
missingDependencies.push(dep);
|
|
1972
2683
|
artifactSections.push(
|
|
1973
2684
|
`[${artifactFile}]:
|
|
1974
2685
|
(integrity check failed: missing generatedBy field)`
|
|
1975
2686
|
);
|
|
1976
|
-
} else if (
|
|
2687
|
+
} else if (normalizedOtter !== expectedOtter) {
|
|
1977
2688
|
missingDependencies.push(dep);
|
|
1978
2689
|
artifactSections.push(
|
|
1979
2690
|
`[${artifactFile}]:
|
|
1980
2691
|
(integrity check failed: artifact claims generatedBy="${artifactOtter}" but expected="${expectedOtter}")`
|
|
1981
2692
|
);
|
|
1982
2693
|
} else {
|
|
1983
|
-
|
|
1984
|
-
|
|
2694
|
+
const sigStatus = signatureAvailable ? signatureVerified ? " [signature verified]" : " [signature check skipped]" : " [unsigned]";
|
|
2695
|
+
artifactSections.push(
|
|
2696
|
+
`[${artifactFile}]${sigStatus}:
|
|
2697
|
+
${JSON.stringify(content, null, 2)}`
|
|
2698
|
+
);
|
|
1985
2699
|
}
|
|
1986
2700
|
} else {
|
|
1987
2701
|
missingDependencies.push(dep);
|
|
@@ -1989,10 +2703,17 @@ ${JSON.stringify(content, null, 2)}`);
|
|
|
1989
2703
|
(not yet available)`);
|
|
1990
2704
|
}
|
|
1991
2705
|
}
|
|
1992
|
-
const parts = [
|
|
2706
|
+
const parts = [];
|
|
2707
|
+
if (buildContextText) {
|
|
2708
|
+
parts.push("BUILD_CONTEXT:", buildContextText, "");
|
|
2709
|
+
}
|
|
2710
|
+
parts.push("ANSWERS:", JSON.stringify(answers, null, 2));
|
|
1993
2711
|
if (artifactSections.length > 0) {
|
|
1994
2712
|
parts.push("", "UPSTREAM ARTIFACTS:", "", ...artifactSections);
|
|
1995
2713
|
}
|
|
2714
|
+
const artifactSchema = PHASE_ARTIFACT_SCHEMA[phase];
|
|
2715
|
+
parts.push("", "REQUIRED_ARTIFACT_SCHEMA:");
|
|
2716
|
+
parts.push(artifactSchema);
|
|
1996
2717
|
parts.push("", "Execute using these answers and the upstream artifacts provided.");
|
|
1997
2718
|
const prompt = parts.join("\n");
|
|
1998
2719
|
const dependenciesSatisfied = missingDependencies.length === 0;
|
|
@@ -2027,45 +2748,231 @@ var OFF_SCRIPT_PATTERNS = [
|
|
|
2027
2748
|
var PHASE_REQUIRED_KEYS = {
|
|
2028
2749
|
designer: ["designLanguage", "themeTokenSeeds"],
|
|
2029
2750
|
theme: ["tokens"],
|
|
2030
|
-
api: ["entities"],
|
|
2751
|
+
api: ["entities", "version", "generatedBy"],
|
|
2031
2752
|
auth: ["version", "generatedBy"],
|
|
2032
|
-
data: ["version", "generatedBy"],
|
|
2753
|
+
data: ["version", "generatedBy", "strategy", "collections"],
|
|
2033
2754
|
pages: ["version", "generatedBy"],
|
|
2034
2755
|
dashboard: ["version", "generatedBy"],
|
|
2035
2756
|
workflow: ["version", "generatedBy"]
|
|
2036
2757
|
};
|
|
2758
|
+
var PHASE_ARTIFACT_SCHEMA = {
|
|
2759
|
+
designer: JSON.stringify(
|
|
2760
|
+
{
|
|
2761
|
+
version: "1.0",
|
|
2762
|
+
generatedBy: "stackwright-pro-designer-otter",
|
|
2763
|
+
application: {
|
|
2764
|
+
type: "<operational|data-explorer|admin|logistics|general>",
|
|
2765
|
+
environment: "<workstation|field|control-room|mixed>",
|
|
2766
|
+
density: "<compact|balanced|spacious>",
|
|
2767
|
+
accessibility: "<wcag-aa|section-508|none>",
|
|
2768
|
+
colorScheme: "<light|dark|both>"
|
|
2769
|
+
},
|
|
2770
|
+
designLanguage: {
|
|
2771
|
+
rationale: "<design rationale>",
|
|
2772
|
+
spacingScale: { base: 8, scale: [0, 4, 8, 16, 24, 32, 48, 64] },
|
|
2773
|
+
colorSemantics: { primary: "#1a365d", accent: "#e53e3e" },
|
|
2774
|
+
typography: {
|
|
2775
|
+
dataFont: "Inter",
|
|
2776
|
+
headingFont: "Inter",
|
|
2777
|
+
monoFont: "monospace",
|
|
2778
|
+
dataSizePx: 12,
|
|
2779
|
+
bodySizePx: 14
|
|
2780
|
+
},
|
|
2781
|
+
contrastRatio: "4.5",
|
|
2782
|
+
borderRadius: "4",
|
|
2783
|
+
shadowElevation: "standard"
|
|
2784
|
+
},
|
|
2785
|
+
themeTokenSeeds: {
|
|
2786
|
+
light: {
|
|
2787
|
+
background: "#ffffff",
|
|
2788
|
+
foreground: "#1a1a1a",
|
|
2789
|
+
primary: "#1a365d",
|
|
2790
|
+
surface: "#f7f7f7",
|
|
2791
|
+
border: "#e2e8f0"
|
|
2792
|
+
},
|
|
2793
|
+
dark: {
|
|
2794
|
+
background: "#1a1a1a",
|
|
2795
|
+
foreground: "#ffffff",
|
|
2796
|
+
primary: "#90cdf4",
|
|
2797
|
+
surface: "#2d2d2d",
|
|
2798
|
+
border: "#4a5568"
|
|
2799
|
+
}
|
|
2800
|
+
},
|
|
2801
|
+
conformsTo: null,
|
|
2802
|
+
operationalNotes: []
|
|
2803
|
+
},
|
|
2804
|
+
null,
|
|
2805
|
+
2
|
|
2806
|
+
),
|
|
2807
|
+
theme: JSON.stringify(
|
|
2808
|
+
{
|
|
2809
|
+
version: "1.0",
|
|
2810
|
+
generatedBy: "stackwright-pro-theme-otter",
|
|
2811
|
+
componentLibrary: "shadcn",
|
|
2812
|
+
colorScheme: "<light|dark|both>",
|
|
2813
|
+
tokens: {
|
|
2814
|
+
colors: { "primary-500": "#1a365d", background: "#ffffff" },
|
|
2815
|
+
spacing: { "spacing-1": "8px", "spacing-2": "16px" },
|
|
2816
|
+
typography: { "font-data": "Inter", "text-sm": "12px" },
|
|
2817
|
+
shape: { "radius-sm": "4px", "radius-md": "8px" },
|
|
2818
|
+
shadows: { "shadow-sm": "0 1px 2px rgba(0,0,0,0.08)" }
|
|
2819
|
+
},
|
|
2820
|
+
cssVariables: {
|
|
2821
|
+
"--background": "0 0% 100%",
|
|
2822
|
+
"--foreground": "222.2 84% 4.9%",
|
|
2823
|
+
"--primary": "222.2 47.4% 11.2%",
|
|
2824
|
+
"--primary-foreground": "210 40% 98%",
|
|
2825
|
+
"--surface": "210 40% 98%",
|
|
2826
|
+
"--border": "214.3 31.8% 91.4%"
|
|
2827
|
+
},
|
|
2828
|
+
dark: { "--background": "222.2 84% 4.9%", "--foreground": "210 40% 98%" }
|
|
2829
|
+
},
|
|
2830
|
+
null,
|
|
2831
|
+
2
|
|
2832
|
+
),
|
|
2833
|
+
api: JSON.stringify(
|
|
2834
|
+
{
|
|
2835
|
+
version: "1.0",
|
|
2836
|
+
generatedBy: "stackwright-pro-api-otter",
|
|
2837
|
+
entities: [
|
|
2838
|
+
{
|
|
2839
|
+
name: "Shipment",
|
|
2840
|
+
endpoint: "/shipments",
|
|
2841
|
+
method: "GET",
|
|
2842
|
+
revalidate: 60,
|
|
2843
|
+
mutationType: null
|
|
2844
|
+
}
|
|
2845
|
+
],
|
|
2846
|
+
auth: { type: "bearer", header: "Authorization", envVar: "API_TOKEN" },
|
|
2847
|
+
baseUrl: "https://api.example.mil/v2",
|
|
2848
|
+
specPath: "./specs/api.yaml"
|
|
2849
|
+
},
|
|
2850
|
+
null,
|
|
2851
|
+
2
|
|
2852
|
+
),
|
|
2853
|
+
data: JSON.stringify(
|
|
2854
|
+
{
|
|
2855
|
+
version: "1.0",
|
|
2856
|
+
generatedBy: "stackwright-pro-data-otter",
|
|
2857
|
+
strategy: "<pulse-fast|isr-fast|isr-standard|isr-slow>",
|
|
2858
|
+
pulseMode: false,
|
|
2859
|
+
collections: [{ name: "equipment", revalidate: 60, pulse: false }],
|
|
2860
|
+
endpoints: { included: ["/equipment/**"], excluded: ["/admin/**"] },
|
|
2861
|
+
requiredPackages: { dependencies: {}, devPackages: {} }
|
|
2862
|
+
},
|
|
2863
|
+
null,
|
|
2864
|
+
2
|
|
2865
|
+
),
|
|
2866
|
+
workflow: JSON.stringify(
|
|
2867
|
+
{
|
|
2868
|
+
version: "1.0",
|
|
2869
|
+
generatedBy: "stackwright-pro-workflow-otter",
|
|
2870
|
+
workflowConfig: {
|
|
2871
|
+
id: "procurement-approval",
|
|
2872
|
+
route: "/procurement",
|
|
2873
|
+
files: ["workflows/procurement-approval.yml"],
|
|
2874
|
+
serviceDependencies: ["service:workflow-state"],
|
|
2875
|
+
warnings: []
|
|
2876
|
+
}
|
|
2877
|
+
},
|
|
2878
|
+
null,
|
|
2879
|
+
2
|
|
2880
|
+
),
|
|
2881
|
+
pages: JSON.stringify(
|
|
2882
|
+
{
|
|
2883
|
+
version: "1.0",
|
|
2884
|
+
generatedBy: "stackwright-pro-page-otter",
|
|
2885
|
+
pages: [
|
|
2886
|
+
{
|
|
2887
|
+
slug: "catalog",
|
|
2888
|
+
type: "collection_listing",
|
|
2889
|
+
collection: "products",
|
|
2890
|
+
themeApplied: true,
|
|
2891
|
+
authRequired: false
|
|
2892
|
+
},
|
|
2893
|
+
{
|
|
2894
|
+
slug: "admin",
|
|
2895
|
+
type: "protected",
|
|
2896
|
+
collection: null,
|
|
2897
|
+
themeApplied: true,
|
|
2898
|
+
authRequired: true
|
|
2899
|
+
}
|
|
2900
|
+
]
|
|
2901
|
+
},
|
|
2902
|
+
null,
|
|
2903
|
+
2
|
|
2904
|
+
),
|
|
2905
|
+
dashboard: JSON.stringify(
|
|
2906
|
+
{
|
|
2907
|
+
version: "1.0",
|
|
2908
|
+
generatedBy: "stackwright-pro-dashboard-otter",
|
|
2909
|
+
pages: [
|
|
2910
|
+
{
|
|
2911
|
+
slug: "dashboard",
|
|
2912
|
+
layout: "<grid|table|mixed>",
|
|
2913
|
+
collections: ["equipment", "supplies"],
|
|
2914
|
+
mode: "<ISR|Pulse>"
|
|
2915
|
+
}
|
|
2916
|
+
]
|
|
2917
|
+
},
|
|
2918
|
+
null,
|
|
2919
|
+
2
|
|
2920
|
+
),
|
|
2921
|
+
auth: JSON.stringify(
|
|
2922
|
+
{
|
|
2923
|
+
version: "1.0",
|
|
2924
|
+
generatedBy: "stackwright-pro-auth-otter",
|
|
2925
|
+
authConfig: {
|
|
2926
|
+
method: "<cac|oidc|oauth2|none>",
|
|
2927
|
+
provider: "<azure-ad|okta|ping|cognito \u2014 if OIDC, else null>",
|
|
2928
|
+
rbacRoles: ["ADMIN", "ANALYST"],
|
|
2929
|
+
rbacDefaultRole: "ANALYST",
|
|
2930
|
+
protectedRoutes: ["/dashboard/:path*", "/procurement/:path*"],
|
|
2931
|
+
auditEnabled: true,
|
|
2932
|
+
auditRetentionDays: 90
|
|
2933
|
+
}
|
|
2934
|
+
},
|
|
2935
|
+
null,
|
|
2936
|
+
2
|
|
2937
|
+
)
|
|
2938
|
+
};
|
|
2037
2939
|
function handleValidateArtifact(input) {
|
|
2038
2940
|
const cwd = input._cwd ?? process.cwd();
|
|
2039
|
-
const { phase, responseText } = input;
|
|
2941
|
+
const { phase, responseText, artifact: directArtifact } = input;
|
|
2040
2942
|
if (!isValidPhase(phase)) {
|
|
2041
2943
|
return {
|
|
2042
2944
|
text: JSON.stringify({ error: true, message: `Unknown phase: ${phase}` }),
|
|
2043
2945
|
isError: true
|
|
2044
2946
|
};
|
|
2045
2947
|
}
|
|
2046
|
-
|
|
2047
|
-
|
|
2948
|
+
let artifact;
|
|
2949
|
+
if (directArtifact) {
|
|
2950
|
+
artifact = directArtifact;
|
|
2951
|
+
} else {
|
|
2952
|
+
const text = responseText ?? "";
|
|
2953
|
+
for (const { pattern, label } of OFF_SCRIPT_PATTERNS) {
|
|
2954
|
+
if (pattern.test(text)) {
|
|
2955
|
+
const result = {
|
|
2956
|
+
valid: false,
|
|
2957
|
+
phase,
|
|
2958
|
+
violation: "off-script",
|
|
2959
|
+
retryPrompt: `You returned code output (detected: ${label}). Return ONLY a JSON artifact \u2014 no code, no files.`
|
|
2960
|
+
};
|
|
2961
|
+
return { text: JSON.stringify(result), isError: false };
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
try {
|
|
2965
|
+
artifact = extractJsonFromResponse(text);
|
|
2966
|
+
} catch {
|
|
2048
2967
|
const result = {
|
|
2049
2968
|
valid: false,
|
|
2050
2969
|
phase,
|
|
2051
|
-
violation: "
|
|
2052
|
-
retryPrompt:
|
|
2970
|
+
violation: "invalid-json",
|
|
2971
|
+
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2053
2972
|
};
|
|
2054
2973
|
return { text: JSON.stringify(result), isError: false };
|
|
2055
2974
|
}
|
|
2056
2975
|
}
|
|
2057
|
-
let artifact;
|
|
2058
|
-
try {
|
|
2059
|
-
artifact = extractJsonFromResponse(responseText);
|
|
2060
|
-
} catch {
|
|
2061
|
-
const result = {
|
|
2062
|
-
valid: false,
|
|
2063
|
-
phase,
|
|
2064
|
-
violation: "invalid-json",
|
|
2065
|
-
retryPrompt: "Your response did not contain valid JSON. Return a single JSON object with no surrounding text."
|
|
2066
|
-
};
|
|
2067
|
-
return { text: JSON.stringify(result), isError: false };
|
|
2068
|
-
}
|
|
2069
2976
|
if (!artifact.version || !artifact.generatedBy) {
|
|
2070
2977
|
const result = {
|
|
2071
2978
|
valid: false,
|
|
@@ -2086,12 +2993,58 @@ function handleValidateArtifact(input) {
|
|
|
2086
2993
|
};
|
|
2087
2994
|
return { text: JSON.stringify(result), isError: false };
|
|
2088
2995
|
}
|
|
2996
|
+
const PHASE_ZOD_VALIDATORS = {
|
|
2997
|
+
workflow: (artifact2) => {
|
|
2998
|
+
const workflowConfig = artifact2["workflowConfig"];
|
|
2999
|
+
if (!workflowConfig) return { success: true };
|
|
3000
|
+
const result = import_types.WorkflowFileSchema.safeParse(workflowConfig);
|
|
3001
|
+
if (!result.success) {
|
|
3002
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
3003
|
+
return { success: false, error: { message: issues } };
|
|
3004
|
+
}
|
|
3005
|
+
return { success: true };
|
|
3006
|
+
},
|
|
3007
|
+
auth: (artifact2) => {
|
|
3008
|
+
const authConfig = artifact2["authConfig"];
|
|
3009
|
+
if (!authConfig) return { success: true };
|
|
3010
|
+
const result = import_types.authConfigSchema.safeParse(authConfig);
|
|
3011
|
+
if (!result.success) {
|
|
3012
|
+
const issues = result.error.issues.slice(0, 3).map((i) => `${i.path.join(".")}: ${i.message}`).join("; ");
|
|
3013
|
+
return { success: false, error: { message: issues } };
|
|
3014
|
+
}
|
|
3015
|
+
return { success: true };
|
|
3016
|
+
}
|
|
3017
|
+
};
|
|
3018
|
+
const zodValidator = PHASE_ZOD_VALIDATORS[phase];
|
|
3019
|
+
if (zodValidator) {
|
|
3020
|
+
const zodResult = zodValidator(artifact);
|
|
3021
|
+
if (!zodResult.success) {
|
|
3022
|
+
const result = {
|
|
3023
|
+
valid: false,
|
|
3024
|
+
phase,
|
|
3025
|
+
violation: "schema-mismatch",
|
|
3026
|
+
retryPrompt: `Your artifact failed schema validation: ${zodResult.error?.message}. Fix these fields and return the corrected JSON artifact.`
|
|
3027
|
+
};
|
|
3028
|
+
return { text: JSON.stringify(result), isError: false };
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
2089
3031
|
try {
|
|
2090
|
-
const artifactsDir = (0,
|
|
2091
|
-
(0,
|
|
3032
|
+
const artifactsDir = (0, import_path5.join)(cwd, ".stackwright", "artifacts");
|
|
3033
|
+
(0, import_fs5.mkdirSync)(artifactsDir, { recursive: true });
|
|
2092
3034
|
const artifactFile = PHASE_ARTIFACT[phase];
|
|
2093
|
-
const artifactPath = (0,
|
|
2094
|
-
|
|
3035
|
+
const artifactPath = (0, import_path5.join)(artifactsDir, artifactFile);
|
|
3036
|
+
const serialized = JSON.stringify(artifact, null, 2) + "\n";
|
|
3037
|
+
const artifactBytes = Buffer.from(serialized, "utf-8");
|
|
3038
|
+
safeWriteSync(artifactPath, serialized);
|
|
3039
|
+
let signed = false;
|
|
3040
|
+
try {
|
|
3041
|
+
const { privateKey } = loadPipelineKeys(cwd);
|
|
3042
|
+
const sig = signArtifact(artifactBytes, privateKey);
|
|
3043
|
+
const signerOtter = PHASE_TO_OTTER2[phase];
|
|
3044
|
+
saveArtifactSignature(cwd, artifactFile, sig, signerOtter);
|
|
3045
|
+
signed = true;
|
|
3046
|
+
} catch {
|
|
3047
|
+
}
|
|
2095
3048
|
const state = readState(cwd);
|
|
2096
3049
|
if (!state.phases[phase]) state.phases[phase] = defaultPhaseStatus();
|
|
2097
3050
|
const ps = state.phases[phase];
|
|
@@ -2102,7 +3055,7 @@ function handleValidateArtifact(input) {
|
|
|
2102
3055
|
valid: true,
|
|
2103
3056
|
phase,
|
|
2104
3057
|
artifactPath,
|
|
2105
|
-
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})`
|
|
3058
|
+
summary: `Wrote ${artifactFile} (keys: ${topKeys}${Object.keys(artifact).length > 5 ? ", ..." : ""})${signed ? " [signed]" : ""}`
|
|
2106
3059
|
};
|
|
2107
3060
|
return { text: JSON.stringify(result), isError: false };
|
|
2108
3061
|
} catch (err) {
|
|
@@ -2126,11 +3079,15 @@ function registerPipelineTools(server2) {
|
|
|
2126
3079
|
"stackwright_pro_set_pipeline_state",
|
|
2127
3080
|
`Atomic read\u2192modify\u2192write pipeline state. ${DESC}`,
|
|
2128
3081
|
{
|
|
2129
|
-
phase:
|
|
2130
|
-
field:
|
|
2131
|
-
value:
|
|
2132
|
-
|
|
2133
|
-
|
|
3082
|
+
phase: import_zod11.z.string().optional().describe('Phase to update, e.g. "designer"'),
|
|
3083
|
+
field: import_zod11.z.enum(["questionsCollected", "answered", "executed", "artifactWritten"]).optional().describe("Boolean field to set"),
|
|
3084
|
+
value: boolCoerce(import_zod11.z.boolean().optional()).describe(
|
|
3085
|
+
'Value for the field \u2014 must be a JSON boolean (true/false), NOT the string "true"/"false"'
|
|
3086
|
+
),
|
|
3087
|
+
status: import_zod11.z.enum(["setup", "questions", "execution", "done"]).optional().describe("Top-level status override"),
|
|
3088
|
+
incrementRetry: boolCoerce(import_zod11.z.boolean().optional()).describe(
|
|
3089
|
+
"Bump retryCount by 1 \u2014 must be a JSON boolean"
|
|
3090
|
+
)
|
|
2134
3091
|
},
|
|
2135
3092
|
async (args) => res(
|
|
2136
3093
|
handleSetPipelineState({
|
|
@@ -2144,9 +3101,11 @@ function registerPipelineTools(server2) {
|
|
|
2144
3101
|
);
|
|
2145
3102
|
server2.tool(
|
|
2146
3103
|
"stackwright_pro_check_execution_ready",
|
|
2147
|
-
`Check all phases have answer files in .stackwright/answers/. ${DESC}`,
|
|
2148
|
-
{
|
|
2149
|
-
|
|
3104
|
+
`Check all phases have answer files in .stackwright/answers/. If phase is provided, check only that phase. ${DESC}`,
|
|
3105
|
+
{
|
|
3106
|
+
phase: import_zod11.z.string().optional().describe("If provided, check only this phase's readiness. Omit to check all phases.")
|
|
3107
|
+
},
|
|
3108
|
+
async ({ phase }) => res(handleCheckExecutionReady(void 0, phase))
|
|
2150
3109
|
);
|
|
2151
3110
|
server2.tool(
|
|
2152
3111
|
"stackwright_pro_list_artifacts",
|
|
@@ -2156,34 +3115,81 @@ function registerPipelineTools(server2) {
|
|
|
2156
3115
|
);
|
|
2157
3116
|
server2.tool(
|
|
2158
3117
|
"stackwright_pro_write_phase_questions",
|
|
2159
|
-
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. ${DESC}`,
|
|
3118
|
+
`Parse otter question-collection response \u2192 .stackwright/questions/{phase}.json. Specialists may also call this directly with a parsed questions array. ${DESC}`,
|
|
2160
3119
|
{
|
|
2161
|
-
phase:
|
|
2162
|
-
responseText:
|
|
3120
|
+
phase: import_zod11.z.string().optional().describe('Phase name, e.g. "designer" (required for direct write)'),
|
|
3121
|
+
responseText: import_zod11.z.string().optional().describe("Raw LLM response from QUESTION_COLLECTION_MODE"),
|
|
3122
|
+
questions: jsonCoerce(import_zod11.z.array(import_zod11.z.any()).optional()).describe(
|
|
3123
|
+
"Questions array for direct specialist write"
|
|
3124
|
+
)
|
|
2163
3125
|
},
|
|
2164
|
-
async ({ phase, responseText }) =>
|
|
3126
|
+
async ({ phase, responseText, questions }) => {
|
|
3127
|
+
if (phase && questions && Array.isArray(questions)) {
|
|
3128
|
+
const SAFE_PHASE = /^[a-z][a-z0-9-]{0,30}$/;
|
|
3129
|
+
if (!SAFE_PHASE.test(phase)) {
|
|
3130
|
+
return {
|
|
3131
|
+
content: [
|
|
3132
|
+
{ type: "text", text: JSON.stringify({ error: `Invalid phase name` }) }
|
|
3133
|
+
],
|
|
3134
|
+
isError: true
|
|
3135
|
+
};
|
|
3136
|
+
}
|
|
3137
|
+
const questionsDir = (0, import_path5.join)(process.cwd(), ".stackwright", "questions");
|
|
3138
|
+
(0, import_fs5.mkdirSync)(questionsDir, { recursive: true });
|
|
3139
|
+
const outPath = (0, import_path5.join)(questionsDir, `${phase}.json`);
|
|
3140
|
+
(0, import_fs5.writeFileSync)(
|
|
3141
|
+
outPath,
|
|
3142
|
+
JSON.stringify({ phase, questions, writtenAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2)
|
|
3143
|
+
);
|
|
3144
|
+
return {
|
|
3145
|
+
content: [
|
|
3146
|
+
{
|
|
3147
|
+
type: "text",
|
|
3148
|
+
text: JSON.stringify({
|
|
3149
|
+
written: true,
|
|
3150
|
+
phase,
|
|
3151
|
+
count: questions.length
|
|
3152
|
+
})
|
|
3153
|
+
}
|
|
3154
|
+
]
|
|
3155
|
+
};
|
|
3156
|
+
}
|
|
3157
|
+
return res(
|
|
3158
|
+
handleWritePhaseQuestions({ phase: phase ?? "", responseText: responseText ?? "" })
|
|
3159
|
+
);
|
|
3160
|
+
}
|
|
2165
3161
|
);
|
|
2166
3162
|
server2.tool(
|
|
2167
3163
|
"stackwright_pro_build_specialist_prompt",
|
|
2168
3164
|
`Assemble execution prompt from answers + upstream artifacts. Foreman passes verbatim. ${DESC}`,
|
|
2169
|
-
{ phase:
|
|
3165
|
+
{ phase: import_zod11.z.string().describe('Phase to build prompt for, e.g. "pages"') },
|
|
2170
3166
|
async ({ phase }) => res(handleBuildSpecialistPrompt({ phase }))
|
|
2171
3167
|
);
|
|
2172
3168
|
server2.tool(
|
|
2173
3169
|
"stackwright_pro_validate_artifact",
|
|
2174
|
-
`Validate
|
|
3170
|
+
`Validate and write artifact to .stackwright/artifacts/. Returns retryPrompt on failure. ${DESC}`,
|
|
2175
3171
|
{
|
|
2176
|
-
phase:
|
|
2177
|
-
responseText:
|
|
3172
|
+
phase: import_zod11.z.string().describe('Phase that produced this artifact, e.g. "designer"'),
|
|
3173
|
+
responseText: import_zod11.z.string().optional().describe("Raw response text from the specialist otter (Foreman-mediated path, legacy)"),
|
|
3174
|
+
artifact: jsonCoerce(import_zod11.z.record(import_zod11.z.string(), import_zod11.z.unknown()).optional()).describe(
|
|
3175
|
+
"Artifact object to validate and write directly (specialist direct path \u2014 skips off-script detection and JSON parsing)"
|
|
3176
|
+
)
|
|
2178
3177
|
},
|
|
2179
|
-
async ({ phase, responseText }) =>
|
|
3178
|
+
async ({ phase, responseText, artifact }) => {
|
|
3179
|
+
if (artifact) {
|
|
3180
|
+
return res(
|
|
3181
|
+
handleValidateArtifact({ phase, artifact })
|
|
3182
|
+
);
|
|
3183
|
+
}
|
|
3184
|
+
return res(handleValidateArtifact({ phase, responseText: responseText ?? "" }));
|
|
3185
|
+
}
|
|
2180
3186
|
);
|
|
2181
3187
|
}
|
|
2182
3188
|
|
|
2183
3189
|
// src/tools/safe-write.ts
|
|
2184
|
-
var
|
|
2185
|
-
var
|
|
2186
|
-
var
|
|
3190
|
+
var import_zod12 = require("zod");
|
|
3191
|
+
var import_fs6 = require("fs");
|
|
3192
|
+
var import_path6 = require("path");
|
|
2187
3193
|
var OTTER_WRITE_ALLOWLISTS = {
|
|
2188
3194
|
"stackwright-pro-designer-otter": [
|
|
2189
3195
|
{ prefix: ".stackwright/artifacts/", suffix: ".json", description: "Design language artifact" }
|
|
@@ -2226,15 +3232,31 @@ var OTTER_WRITE_ALLOWLISTS = {
|
|
|
2226
3232
|
};
|
|
2227
3233
|
var PROTECTED_PATH_PREFIXES = [
|
|
2228
3234
|
".stackwright/pipeline-state.json",
|
|
3235
|
+
".stackwright/pipeline-keys.json",
|
|
3236
|
+
// ephemeral signing keys
|
|
3237
|
+
".stackwright/artifacts/signatures.json",
|
|
3238
|
+
// artifact signature manifest
|
|
2229
3239
|
".stackwright/questions/",
|
|
2230
3240
|
".stackwright/answers/"
|
|
2231
3241
|
];
|
|
3242
|
+
var MAX_SAFE_WRITE_BYTES_JSON = 512 * 1024;
|
|
3243
|
+
var MAX_SAFE_WRITE_BYTES_YAML = 256 * 1024;
|
|
3244
|
+
var MAX_SAFE_WRITE_BYTES_ENV = 4 * 1024;
|
|
3245
|
+
var MAX_SAFE_WRITE_BYTES_DEFAULT = 256 * 1024;
|
|
3246
|
+
function getMaxBytesForPath(filePath) {
|
|
3247
|
+
if (filePath.endsWith(".json")) return { limit: MAX_SAFE_WRITE_BYTES_JSON, label: "JSON" };
|
|
3248
|
+
if (filePath.endsWith(".yml") || filePath.endsWith(".yaml"))
|
|
3249
|
+
return { limit: MAX_SAFE_WRITE_BYTES_YAML, label: "YAML" };
|
|
3250
|
+
if (filePath === ".env" || /^\.env\.[a-zA-Z0-9]+/.test(filePath))
|
|
3251
|
+
return { limit: MAX_SAFE_WRITE_BYTES_ENV, label: "env" };
|
|
3252
|
+
return { limit: MAX_SAFE_WRITE_BYTES_DEFAULT, label: "default" };
|
|
3253
|
+
}
|
|
2232
3254
|
function checkPathAllowed(callerOtter, filePath) {
|
|
2233
|
-
const normalized = (0,
|
|
3255
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
2234
3256
|
if (normalized.includes("..")) {
|
|
2235
3257
|
return { allowed: false, error: 'Path traversal detected: ".." segments are not allowed' };
|
|
2236
3258
|
}
|
|
2237
|
-
if ((0,
|
|
3259
|
+
if ((0, import_path6.isAbsolute)(normalized)) {
|
|
2238
3260
|
return {
|
|
2239
3261
|
allowed: false,
|
|
2240
3262
|
error: "Absolute paths are not allowed \u2014 use paths relative to project root"
|
|
@@ -2354,11 +3376,23 @@ function handleSafeWrite(input) {
|
|
|
2354
3376
|
};
|
|
2355
3377
|
return { text: JSON.stringify(result), isError: true };
|
|
2356
3378
|
}
|
|
2357
|
-
const
|
|
2358
|
-
const
|
|
2359
|
-
if (
|
|
3379
|
+
const contentBytes = Buffer.byteLength(content, "utf-8");
|
|
3380
|
+
const { limit: maxBytes, label: fileTypeLabel } = getMaxBytesForPath(filePath);
|
|
3381
|
+
if (contentBytes > maxBytes) {
|
|
3382
|
+
const result = {
|
|
3383
|
+
success: false,
|
|
3384
|
+
error: `Content size ${contentBytes} bytes exceeds ${fileTypeLabel} limit of ${maxBytes} bytes (${maxBytes / 1024} KB)`,
|
|
3385
|
+
callerOtter,
|
|
3386
|
+
attemptedPath: filePath,
|
|
3387
|
+
allowedPaths: []
|
|
3388
|
+
};
|
|
3389
|
+
return { text: JSON.stringify(result), isError: true };
|
|
3390
|
+
}
|
|
3391
|
+
const normalized = (0, import_path6.normalize)(filePath);
|
|
3392
|
+
const fullPath = (0, import_path6.join)(cwd, normalized);
|
|
3393
|
+
if ((0, import_fs6.existsSync)(fullPath)) {
|
|
2360
3394
|
try {
|
|
2361
|
-
const stat = (0,
|
|
3395
|
+
const stat = (0, import_fs6.lstatSync)(fullPath);
|
|
2362
3396
|
if (stat.isSymbolicLink()) {
|
|
2363
3397
|
const result = {
|
|
2364
3398
|
success: false,
|
|
@@ -2385,9 +3419,9 @@ function handleSafeWrite(input) {
|
|
|
2385
3419
|
}
|
|
2386
3420
|
try {
|
|
2387
3421
|
if (createDirectories) {
|
|
2388
|
-
(0,
|
|
3422
|
+
(0, import_fs6.mkdirSync)((0, import_path6.dirname)(fullPath), { recursive: true });
|
|
2389
3423
|
}
|
|
2390
|
-
(0,
|
|
3424
|
+
(0, import_fs6.writeFileSync)(fullPath, content, { encoding: "utf-8" });
|
|
2391
3425
|
const result = {
|
|
2392
3426
|
success: true,
|
|
2393
3427
|
path: normalized,
|
|
@@ -2413,10 +3447,12 @@ function registerSafeWriteTools(server2) {
|
|
|
2413
3447
|
"stackwright_pro_safe_write",
|
|
2414
3448
|
DESC,
|
|
2415
3449
|
{
|
|
2416
|
-
callerOtter:
|
|
2417
|
-
filePath:
|
|
2418
|
-
content:
|
|
2419
|
-
createDirectories:
|
|
3450
|
+
callerOtter: import_zod12.z.string().describe('The otter agent name requesting the write, e.g. "stackwright-pro-page-otter"'),
|
|
3451
|
+
filePath: import_zod12.z.string().describe('Relative path from project root, e.g. "pages/dashboard/content.yml"'),
|
|
3452
|
+
content: import_zod12.z.string().describe("File content to write"),
|
|
3453
|
+
createDirectories: boolCoerce(import_zod12.z.boolean().optional().default(true)).describe(
|
|
3454
|
+
"Create parent directories if they don't exist. Default: true"
|
|
3455
|
+
)
|
|
2420
3456
|
},
|
|
2421
3457
|
async ({ callerOtter, filePath, content, createDirectories }) => {
|
|
2422
3458
|
const result = handleSafeWrite({
|
|
@@ -2431,9 +3467,9 @@ function registerSafeWriteTools(server2) {
|
|
|
2431
3467
|
}
|
|
2432
3468
|
|
|
2433
3469
|
// src/tools/auth.ts
|
|
2434
|
-
var
|
|
2435
|
-
var
|
|
2436
|
-
var
|
|
3470
|
+
var import_zod13 = require("zod");
|
|
3471
|
+
var import_fs7 = require("fs");
|
|
3472
|
+
var import_path7 = require("path");
|
|
2437
3473
|
function buildHierarchy(roles) {
|
|
2438
3474
|
const h = {};
|
|
2439
3475
|
for (let i = 0; i < roles.length - 1; i++) {
|
|
@@ -2695,7 +3731,7 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2695
3731
|
auditRetentionDays,
|
|
2696
3732
|
protectedRoutes
|
|
2697
3733
|
);
|
|
2698
|
-
(0,
|
|
3734
|
+
(0, import_fs7.writeFileSync)((0, import_path7.join)(cwd, "middleware.ts"), middlewareContent, "utf8");
|
|
2699
3735
|
filesWritten.push("middleware.ts");
|
|
2700
3736
|
} catch (err) {
|
|
2701
3737
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -2711,12 +3747,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2711
3747
|
}
|
|
2712
3748
|
try {
|
|
2713
3749
|
const envBlock = generateEnvBlock(method, params);
|
|
2714
|
-
const envPath = (0,
|
|
2715
|
-
if ((0,
|
|
2716
|
-
const existing = (0,
|
|
2717
|
-
(0,
|
|
3750
|
+
const envPath = (0, import_path7.join)(cwd, ".env.example");
|
|
3751
|
+
if ((0, import_fs7.existsSync)(envPath)) {
|
|
3752
|
+
const existing = (0, import_fs7.readFileSync)(envPath, "utf8");
|
|
3753
|
+
(0, import_fs7.writeFileSync)(envPath, existing.trimEnd() + "\n\n" + envBlock, "utf8");
|
|
2718
3754
|
} else {
|
|
2719
|
-
(0,
|
|
3755
|
+
(0, import_fs7.writeFileSync)(envPath, envBlock, "utf8");
|
|
2720
3756
|
}
|
|
2721
3757
|
filesWritten.push(".env.example");
|
|
2722
3758
|
} catch (err) {
|
|
@@ -2742,12 +3778,12 @@ async function configureAuthHandler(params, cwd) {
|
|
|
2742
3778
|
auditRetentionDays,
|
|
2743
3779
|
protectedRoutes
|
|
2744
3780
|
);
|
|
2745
|
-
const ymlPath = (0,
|
|
2746
|
-
if (!(0,
|
|
2747
|
-
(0,
|
|
3781
|
+
const ymlPath = (0, import_path7.join)(cwd, "stackwright.yml");
|
|
3782
|
+
if (!(0, import_fs7.existsSync)(ymlPath)) {
|
|
3783
|
+
(0, import_fs7.writeFileSync)(ymlPath, authYaml, "utf8");
|
|
2748
3784
|
} else {
|
|
2749
|
-
const existing = (0,
|
|
2750
|
-
(0,
|
|
3785
|
+
const existing = (0, import_fs7.readFileSync)(ymlPath, "utf8");
|
|
3786
|
+
(0, import_fs7.writeFileSync)(ymlPath, upsertAuthBlock(existing, authYaml), "utf8");
|
|
2751
3787
|
}
|
|
2752
3788
|
filesWritten.push("stackwright.yml");
|
|
2753
3789
|
} catch (err) {
|
|
@@ -2786,35 +3822,35 @@ function registerAuthTools(server2) {
|
|
|
2786
3822
|
"stackwright_pro_configure_auth",
|
|
2787
3823
|
"Generate authentication middleware and configuration for a Next.js Stackwright application. Writes `middleware.ts` from a secure template, appends/updates the `auth:` section in `stackwright.yml`, and generates `.env.example` with required environment variables. \u26A0\uFE0F For CAC/PKI: generated `middleware.ts` carries a SECURITY REVIEW REQUIRED comment \u2014 certificate chain validation must be verified by a DoD security officer before production deployment. This is the ONLY approved path to generating `middleware.ts`. Never write TypeScript auth files directly.",
|
|
2788
3824
|
{
|
|
2789
|
-
method:
|
|
2790
|
-
provider:
|
|
3825
|
+
method: import_zod13.z.enum(["cac", "oidc", "oauth2", "none"]),
|
|
3826
|
+
provider: import_zod13.z.enum(["azure-ad", "okta", "ping", "cognito", "custom"]).optional(),
|
|
2791
3827
|
// CAC
|
|
2792
|
-
cacCaBundle:
|
|
2793
|
-
cacEdipiLookup:
|
|
2794
|
-
cacOcspEndpoint:
|
|
2795
|
-
cacCertHeader:
|
|
3828
|
+
cacCaBundle: import_zod13.z.string().optional(),
|
|
3829
|
+
cacEdipiLookup: import_zod13.z.string().optional(),
|
|
3830
|
+
cacOcspEndpoint: import_zod13.z.string().optional(),
|
|
3831
|
+
cacCertHeader: import_zod13.z.string().optional(),
|
|
2796
3832
|
// OIDC
|
|
2797
|
-
oidcDiscoveryUrl:
|
|
2798
|
-
oidcClientId:
|
|
2799
|
-
oidcClientSecret:
|
|
2800
|
-
oidcScopes:
|
|
2801
|
-
oidcRoleClaim:
|
|
3833
|
+
oidcDiscoveryUrl: import_zod13.z.string().optional(),
|
|
3834
|
+
oidcClientId: import_zod13.z.string().optional(),
|
|
3835
|
+
oidcClientSecret: import_zod13.z.string().optional(),
|
|
3836
|
+
oidcScopes: import_zod13.z.string().optional(),
|
|
3837
|
+
oidcRoleClaim: import_zod13.z.string().optional(),
|
|
2802
3838
|
// OAuth2
|
|
2803
|
-
oauth2AuthUrl:
|
|
2804
|
-
oauth2TokenUrl:
|
|
2805
|
-
oauth2ClientId:
|
|
2806
|
-
oauth2ClientSecret:
|
|
2807
|
-
oauth2Scopes:
|
|
3839
|
+
oauth2AuthUrl: import_zod13.z.string().optional(),
|
|
3840
|
+
oauth2TokenUrl: import_zod13.z.string().optional(),
|
|
3841
|
+
oauth2ClientId: import_zod13.z.string().optional(),
|
|
3842
|
+
oauth2ClientSecret: import_zod13.z.string().optional(),
|
|
3843
|
+
oauth2Scopes: import_zod13.z.string().optional(),
|
|
2808
3844
|
// RBAC
|
|
2809
|
-
rbacRoles:
|
|
2810
|
-
rbacDefaultRole:
|
|
3845
|
+
rbacRoles: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
|
|
3846
|
+
rbacDefaultRole: import_zod13.z.string().optional(),
|
|
2811
3847
|
// Audit
|
|
2812
|
-
auditEnabled:
|
|
2813
|
-
auditRetentionDays:
|
|
3848
|
+
auditEnabled: boolCoerce(import_zod13.z.boolean().optional()),
|
|
3849
|
+
auditRetentionDays: numCoerce(import_zod13.z.number().int().positive().optional()),
|
|
2814
3850
|
// Routes
|
|
2815
|
-
protectedRoutes:
|
|
3851
|
+
protectedRoutes: jsonCoerce(import_zod13.z.array(import_zod13.z.string()).optional()),
|
|
2816
3852
|
// Injection for tests
|
|
2817
|
-
_cwd:
|
|
3853
|
+
_cwd: import_zod13.z.string().optional()
|
|
2818
3854
|
},
|
|
2819
3855
|
async (params) => {
|
|
2820
3856
|
const cwd = params._cwd ?? process.cwd();
|
|
@@ -2824,45 +3860,45 @@ function registerAuthTools(server2) {
|
|
|
2824
3860
|
}
|
|
2825
3861
|
|
|
2826
3862
|
// src/integrity.ts
|
|
2827
|
-
var
|
|
2828
|
-
var
|
|
2829
|
-
var
|
|
3863
|
+
var import_crypto4 = require("crypto");
|
|
3864
|
+
var import_fs8 = require("fs");
|
|
3865
|
+
var import_path8 = require("path");
|
|
2830
3866
|
var _checksums = /* @__PURE__ */ new Map([
|
|
2831
3867
|
[
|
|
2832
3868
|
"stackwright-pro-api-otter.json",
|
|
2833
|
-
"
|
|
3869
|
+
"9fbaed0ce6116b82d0289f24432037d04637c89b8e73062ed946e5d49b294734"
|
|
2834
3870
|
],
|
|
2835
3871
|
[
|
|
2836
3872
|
"stackwright-pro-auth-otter.json",
|
|
2837
|
-
"
|
|
3873
|
+
"bf0e66e35d15ba818ba6ff1a007df34975565bacbb35cc0c80151fb1da13e573"
|
|
2838
3874
|
],
|
|
2839
3875
|
[
|
|
2840
3876
|
"stackwright-pro-dashboard-otter.json",
|
|
2841
|
-
"
|
|
3877
|
+
"89e81ac161147ab532034b40e4f7863dcd7d03ef33fd94c97d19c3e56fb91a9e"
|
|
2842
3878
|
],
|
|
2843
3879
|
[
|
|
2844
3880
|
"stackwright-pro-data-otter.json",
|
|
2845
|
-
"
|
|
3881
|
+
"4769c3fa9b358609159ef2f613ac9d40dc7cb439e916c3344b9500a75d6afa82"
|
|
2846
3882
|
],
|
|
2847
3883
|
[
|
|
2848
3884
|
"stackwright-pro-designer-otter.json",
|
|
2849
|
-
"
|
|
3885
|
+
"af09ac8f06385bdbac63e2820daa2ff7d38b8ff1ff383c161f07e3fb9d9359c5"
|
|
2850
3886
|
],
|
|
2851
3887
|
[
|
|
2852
3888
|
"stackwright-pro-foreman-otter.json",
|
|
2853
|
-
"
|
|
3889
|
+
"582a26766a5bc80ef9f3aef53e82794c7f47f618bd28e4e70583bfc2b569398c"
|
|
2854
3890
|
],
|
|
2855
3891
|
[
|
|
2856
3892
|
"stackwright-pro-page-otter.json",
|
|
2857
|
-
"
|
|
3893
|
+
"2fd8fe6f799e3fb4659979b1c958c64c276632fcfe18f70371543dc8145baa1b"
|
|
2858
3894
|
],
|
|
2859
3895
|
[
|
|
2860
3896
|
"stackwright-pro-theme-otter.json",
|
|
2861
|
-
"
|
|
3897
|
+
"bc5695ef28164516b3f677cab201de781663c354d187cea28cc41884f1d89414"
|
|
2862
3898
|
],
|
|
2863
3899
|
[
|
|
2864
3900
|
"stackwright-pro-workflow-otter.json",
|
|
2865
|
-
"
|
|
3901
|
+
"c90d6773b2287aa9a640c2715ca0e75f44c13e99fddcfb89ced36603f38930ce"
|
|
2866
3902
|
]
|
|
2867
3903
|
]);
|
|
2868
3904
|
Object.freeze(_checksums);
|
|
@@ -2877,21 +3913,21 @@ for (const [name, digest] of CANONICAL_CHECKSUMS) {
|
|
|
2877
3913
|
}
|
|
2878
3914
|
var MAX_OTTER_BYTES = 1 * 1024 * 1024;
|
|
2879
3915
|
function computeSha256(data) {
|
|
2880
|
-
return (0,
|
|
3916
|
+
return (0, import_crypto4.createHash)("sha256").update(data).digest("hex");
|
|
2881
3917
|
}
|
|
2882
3918
|
function safeEqual(a, b) {
|
|
2883
3919
|
if (a.length !== b.length) return false;
|
|
2884
|
-
return (0,
|
|
3920
|
+
return (0, import_crypto4.timingSafeEqual)(Buffer.from(a, "utf8"), Buffer.from(b, "utf8"));
|
|
2885
3921
|
}
|
|
2886
3922
|
function verifyOtterFile(filePath) {
|
|
2887
|
-
const filename = (0,
|
|
3923
|
+
const filename = (0, import_path8.basename)(filePath);
|
|
2888
3924
|
const expected = CANONICAL_CHECKSUMS.get(filename);
|
|
2889
3925
|
if (expected === void 0) {
|
|
2890
3926
|
return { verified: false, filename, error: `Unknown otter file: not in canonical set` };
|
|
2891
3927
|
}
|
|
2892
3928
|
let stat;
|
|
2893
3929
|
try {
|
|
2894
|
-
stat = (0,
|
|
3930
|
+
stat = (0, import_fs8.lstatSync)(filePath);
|
|
2895
3931
|
} catch (err) {
|
|
2896
3932
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2897
3933
|
return { verified: false, filename, error: `Cannot stat file: ${msg}` };
|
|
@@ -2909,7 +3945,7 @@ function verifyOtterFile(filePath) {
|
|
|
2909
3945
|
}
|
|
2910
3946
|
let raw;
|
|
2911
3947
|
try {
|
|
2912
|
-
raw = (0,
|
|
3948
|
+
raw = (0, import_fs8.readFileSync)(filePath);
|
|
2913
3949
|
} catch (err) {
|
|
2914
3950
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2915
3951
|
return { verified: false, filename, error: `Cannot read file: ${msg}` };
|
|
@@ -2942,12 +3978,24 @@ function verifyOtterFile(filePath) {
|
|
|
2942
3978
|
return { verified: true, filename };
|
|
2943
3979
|
}
|
|
2944
3980
|
function verifyAllOtters(otterDir) {
|
|
3981
|
+
if (/(?:^|[/\\])\.\.(?:[/\\]|$)/.test(otterDir) || otterDir.includes("..")) {
|
|
3982
|
+
return {
|
|
3983
|
+
verified: [],
|
|
3984
|
+
failed: [
|
|
3985
|
+
{
|
|
3986
|
+
filename: "<directory>",
|
|
3987
|
+
error: `Security: path traversal sequence detected in otter directory parameter`
|
|
3988
|
+
}
|
|
3989
|
+
],
|
|
3990
|
+
unknown: []
|
|
3991
|
+
};
|
|
3992
|
+
}
|
|
2945
3993
|
const verified = [];
|
|
2946
3994
|
const failed = [];
|
|
2947
3995
|
const unknown = [];
|
|
2948
3996
|
let entries;
|
|
2949
3997
|
try {
|
|
2950
|
-
entries = (0,
|
|
3998
|
+
entries = (0, import_fs8.readdirSync)(otterDir);
|
|
2951
3999
|
} catch (err) {
|
|
2952
4000
|
const msg = err instanceof Error ? err.message : String(err);
|
|
2953
4001
|
return {
|
|
@@ -2958,9 +4006,9 @@ function verifyAllOtters(otterDir) {
|
|
|
2958
4006
|
}
|
|
2959
4007
|
const otterFiles = entries.filter((f) => f.endsWith("-otter.json"));
|
|
2960
4008
|
for (const filename of otterFiles) {
|
|
2961
|
-
const filePath = (0,
|
|
4009
|
+
const filePath = (0, import_path8.join)(otterDir, filename);
|
|
2962
4010
|
try {
|
|
2963
|
-
if ((0,
|
|
4011
|
+
if ((0, import_fs8.lstatSync)(filePath).isSymbolicLink()) {
|
|
2964
4012
|
failed.push({ filename, error: "Skipped: symlink" });
|
|
2965
4013
|
continue;
|
|
2966
4014
|
}
|
|
@@ -2986,15 +4034,30 @@ var DEFAULT_SEARCH_PATHS = ["node_modules/@stackwright-pro/otters/src/", "packag
|
|
|
2986
4034
|
function resolveOtterDir() {
|
|
2987
4035
|
const cwd = process.cwd();
|
|
2988
4036
|
for (const relative of DEFAULT_SEARCH_PATHS) {
|
|
2989
|
-
const candidate = (0,
|
|
4037
|
+
const candidate = (0, import_path8.join)(cwd, relative);
|
|
2990
4038
|
try {
|
|
2991
|
-
(0,
|
|
4039
|
+
(0, import_fs8.lstatSync)(candidate);
|
|
2992
4040
|
return candidate;
|
|
2993
4041
|
} catch {
|
|
2994
4042
|
}
|
|
2995
4043
|
}
|
|
2996
4044
|
return null;
|
|
2997
4045
|
}
|
|
4046
|
+
function emitIntegrityAuditEvent(params) {
|
|
4047
|
+
const record = JSON.stringify({
|
|
4048
|
+
level: "AUDIT",
|
|
4049
|
+
event: "INTEGRITY_FAIL",
|
|
4050
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4051
|
+
source: "stackwright_pro_verify_otter_integrity",
|
|
4052
|
+
otterDir: params.otterDir,
|
|
4053
|
+
failedCount: params.failed.length,
|
|
4054
|
+
unknownCount: params.unknown.length,
|
|
4055
|
+
failures: params.failed,
|
|
4056
|
+
unknown: params.unknown
|
|
4057
|
+
});
|
|
4058
|
+
process.stderr.write(`INTEGRITY_FAIL ${record}
|
|
4059
|
+
`);
|
|
4060
|
+
}
|
|
2998
4061
|
function registerIntegrityTools(server2) {
|
|
2999
4062
|
server2.tool(
|
|
3000
4063
|
"stackwright_pro_verify_otter_integrity",
|
|
@@ -3018,6 +4081,13 @@ function registerIntegrityTools(server2) {
|
|
|
3018
4081
|
}
|
|
3019
4082
|
const result = verifyAllOtters(resolved);
|
|
3020
4083
|
const allGood = result.failed.length === 0 && result.unknown.length === 0;
|
|
4084
|
+
if (!allGood) {
|
|
4085
|
+
emitIntegrityAuditEvent({
|
|
4086
|
+
otterDir: resolved,
|
|
4087
|
+
failed: result.failed,
|
|
4088
|
+
unknown: result.unknown
|
|
4089
|
+
});
|
|
4090
|
+
}
|
|
3021
4091
|
return {
|
|
3022
4092
|
content: [
|
|
3023
4093
|
{
|
|
@@ -3030,7 +4100,10 @@ function registerIntegrityTools(server2) {
|
|
|
3030
4100
|
unknownCount: result.unknown.length,
|
|
3031
4101
|
verified: result.verified,
|
|
3032
4102
|
failed: result.failed,
|
|
3033
|
-
unknown: result.unknown
|
|
4103
|
+
unknown: result.unknown,
|
|
4104
|
+
...allGood ? {} : {
|
|
4105
|
+
error: "INTEGRITY CHECK FAILED: SHA-256 mismatch detected in otter agent definitions. Do not proceed \u2014 otter files may have been tampered with."
|
|
4106
|
+
}
|
|
3034
4107
|
})
|
|
3035
4108
|
}
|
|
3036
4109
|
],
|
|
@@ -3041,14 +4114,14 @@ function registerIntegrityTools(server2) {
|
|
|
3041
4114
|
}
|
|
3042
4115
|
|
|
3043
4116
|
// src/tools/domain.ts
|
|
3044
|
-
var
|
|
3045
|
-
var
|
|
3046
|
-
var
|
|
4117
|
+
var import_zod14 = require("zod");
|
|
4118
|
+
var import_fs9 = require("fs");
|
|
4119
|
+
var import_path9 = require("path");
|
|
3047
4120
|
function handleListCollections(input) {
|
|
3048
4121
|
const cwd = input._cwd ?? process.cwd();
|
|
3049
4122
|
const sources = [
|
|
3050
4123
|
{
|
|
3051
|
-
path: (0,
|
|
4124
|
+
path: (0, import_path9.join)(cwd, ".stackwright", "artifacts", "data-config.json"),
|
|
3052
4125
|
source: "data-config.json",
|
|
3053
4126
|
parse: (raw) => {
|
|
3054
4127
|
const parsed = JSON.parse(raw);
|
|
@@ -3059,15 +4132,15 @@ function handleListCollections(input) {
|
|
|
3059
4132
|
}
|
|
3060
4133
|
},
|
|
3061
4134
|
{
|
|
3062
|
-
path: (0,
|
|
4135
|
+
path: (0, import_path9.join)(cwd, "stackwright.yml"),
|
|
3063
4136
|
source: "stackwright.yml",
|
|
3064
4137
|
parse: extractCollectionsFromYaml
|
|
3065
4138
|
}
|
|
3066
4139
|
];
|
|
3067
4140
|
for (const { path: path3, source, parse } of sources) {
|
|
3068
|
-
if (!(0,
|
|
4141
|
+
if (!(0, import_fs9.existsSync)(path3)) continue;
|
|
3069
4142
|
try {
|
|
3070
|
-
const collections = parse((0,
|
|
4143
|
+
const collections = parse((0, import_fs9.readFileSync)(path3, "utf8"));
|
|
3071
4144
|
return {
|
|
3072
4145
|
text: JSON.stringify({ collections, source, collectionCount: collections.length }),
|
|
3073
4146
|
isError: false
|
|
@@ -3218,8 +4291,8 @@ function handleValidateWorkflow(input) {
|
|
|
3218
4291
|
if (input.workflow && Object.keys(input.workflow).length > 0) {
|
|
3219
4292
|
raw = input.workflow;
|
|
3220
4293
|
} else {
|
|
3221
|
-
const artifactPath = (0,
|
|
3222
|
-
if (!(0,
|
|
4294
|
+
const artifactPath = (0, import_path9.join)(cwd, ".stackwright", "artifacts", "workflow-config.json");
|
|
4295
|
+
if (!(0, import_fs9.existsSync)(artifactPath)) {
|
|
3223
4296
|
return fail([
|
|
3224
4297
|
{
|
|
3225
4298
|
code: "NO_WORKFLOW",
|
|
@@ -3228,7 +4301,7 @@ function handleValidateWorkflow(input) {
|
|
|
3228
4301
|
]);
|
|
3229
4302
|
}
|
|
3230
4303
|
try {
|
|
3231
|
-
raw = JSON.parse((0,
|
|
4304
|
+
raw = JSON.parse((0, import_fs9.readFileSync)(artifactPath, "utf8"));
|
|
3232
4305
|
} catch (err) {
|
|
3233
4306
|
return fail([{ code: "INVALID_JSON", message: `Failed to parse workflow artifact: ${err}` }]);
|
|
3234
4307
|
}
|
|
@@ -3427,7 +4500,7 @@ function registerDomainTools(server2) {
|
|
|
3427
4500
|
"stackwright_pro_resolve_data_strategy",
|
|
3428
4501
|
"Look up the data freshness strategy configuration from the user's answer. Returns mechanism, revalidation seconds, required packages, and the exact MCP tool call to make. Replaces the strategy table in the data-otter prompt.",
|
|
3429
4502
|
{
|
|
3430
|
-
strategy:
|
|
4503
|
+
strategy: import_zod14.z.string().describe(
|
|
3431
4504
|
'The data-1 answer value: "pulse-fast", "isr-fast", "isr-standard", or "isr-slow"'
|
|
3432
4505
|
)
|
|
3433
4506
|
},
|
|
@@ -3437,7 +4510,7 @@ function registerDomainTools(server2) {
|
|
|
3437
4510
|
"stackwright_pro_validate_workflow",
|
|
3438
4511
|
"Validate a workflow definition against the Stackwright workflow schema. Checks step ID uniqueness, transition targets, terminal state existence, and service references. Call this after the workflow otter produces output.",
|
|
3439
4512
|
{
|
|
3440
|
-
workflow:
|
|
4513
|
+
workflow: jsonCoerce(import_zod14.z.record(import_zod14.z.string(), import_zod14.z.unknown()).optional()).describe(
|
|
3441
4514
|
"Parsed workflow object. If omitted, reads from .stackwright/artifacts/workflow-config.json"
|
|
3442
4515
|
)
|
|
3443
4516
|
},
|
|
@@ -3445,20 +4518,112 @@ function registerDomainTools(server2) {
|
|
|
3445
4518
|
);
|
|
3446
4519
|
}
|
|
3447
4520
|
|
|
4521
|
+
// src/tools/type-schemas.ts
|
|
4522
|
+
var import_zod15 = require("zod");
|
|
4523
|
+
function buildTypeSchemaSummary() {
|
|
4524
|
+
return {
|
|
4525
|
+
version: "1.0",
|
|
4526
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
4527
|
+
domains: {
|
|
4528
|
+
workflow: {
|
|
4529
|
+
description: "Workflow DSL \u2014 step definitions, auth blocks, field types, conditions",
|
|
4530
|
+
schemas: [
|
|
4531
|
+
"WorkflowFileSchema",
|
|
4532
|
+
"WorkflowDefinitionSchema",
|
|
4533
|
+
"WorkflowStepSchema",
|
|
4534
|
+
"WorkflowStepTypeSchema",
|
|
4535
|
+
"WorkflowFieldSchema",
|
|
4536
|
+
"WorkflowActionSchema",
|
|
4537
|
+
"WorkflowAuthSchema",
|
|
4538
|
+
"WorkflowThemeSchema",
|
|
4539
|
+
"TransitionConditionSchema",
|
|
4540
|
+
"PersistenceSchema"
|
|
4541
|
+
],
|
|
4542
|
+
otter: "stackwright-pro-workflow-otter",
|
|
4543
|
+
artifactKey: "workflowConfig"
|
|
4544
|
+
},
|
|
4545
|
+
auth: {
|
|
4546
|
+
description: "Authentication providers \u2014 PKI/CAC, OIDC, RBAC configuration",
|
|
4547
|
+
schemas: [
|
|
4548
|
+
"authConfigSchema",
|
|
4549
|
+
"pkiConfigSchema",
|
|
4550
|
+
"oidcConfigSchema",
|
|
4551
|
+
"rbacConfigSchema",
|
|
4552
|
+
"componentAuthSchema",
|
|
4553
|
+
"authUserSchema",
|
|
4554
|
+
"authSessionSchema"
|
|
4555
|
+
],
|
|
4556
|
+
otter: "stackwright-pro-auth-otter",
|
|
4557
|
+
artifactKey: "authConfig"
|
|
4558
|
+
},
|
|
4559
|
+
openapi: {
|
|
4560
|
+
description: "OpenAPI spec integration \u2014 collection config, endpoint filters, actions",
|
|
4561
|
+
interfaces: [
|
|
4562
|
+
"OpenAPIConfig",
|
|
4563
|
+
"ActionConfig",
|
|
4564
|
+
"EndpointFilter",
|
|
4565
|
+
"ApprovedSpec",
|
|
4566
|
+
"PrebuildSecurityConfig",
|
|
4567
|
+
"SiteConfig",
|
|
4568
|
+
"ValidationResult"
|
|
4569
|
+
],
|
|
4570
|
+
otter: "stackwright-pro-api-otter",
|
|
4571
|
+
artifactKey: "apiConfig"
|
|
4572
|
+
},
|
|
4573
|
+
pulse: {
|
|
4574
|
+
description: "Real-time data polling \u2014 source-agnostic polling options and states",
|
|
4575
|
+
interfaces: ["PulseOptions", "PulseMeta", "PulseState"],
|
|
4576
|
+
note: "React-bound types (PulseProps, PulseIndicatorProps) remain in @stackwright-pro/pulse"
|
|
4577
|
+
},
|
|
4578
|
+
enterprise: {
|
|
4579
|
+
description: "Enterprise collection access \u2014 multi-tenant provider extension",
|
|
4580
|
+
interfaces: [
|
|
4581
|
+
"EnterpriseCollectionProvider",
|
|
4582
|
+
"CollectionProvider",
|
|
4583
|
+
"CollectionEntry",
|
|
4584
|
+
"CollectionListOptions",
|
|
4585
|
+
"CollectionListResult",
|
|
4586
|
+
"TenantFilter",
|
|
4587
|
+
"Collection"
|
|
4588
|
+
],
|
|
4589
|
+
note: "CollectionProvider, CollectionEntry, CollectionListOptions, and CollectionListResult are re-exported from @stackwright/types (^1.5.0). EnterpriseCollectionProvider, TenantFilter, and Collection are Pro-only extensions."
|
|
4590
|
+
}
|
|
4591
|
+
}
|
|
4592
|
+
};
|
|
4593
|
+
}
|
|
4594
|
+
function registerTypeSchemasTool(server2) {
|
|
4595
|
+
server2.tool(
|
|
4596
|
+
"stackwright_pro_get_type_schemas",
|
|
4597
|
+
"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.",
|
|
4598
|
+
{
|
|
4599
|
+
format: import_zod15.z.enum(["full", "domains-only"]).optional().default("full").describe("full = complete summary with all fields; domains-only = just domain names")
|
|
4600
|
+
},
|
|
4601
|
+
async ({ format }) => {
|
|
4602
|
+
const summary = buildTypeSchemaSummary();
|
|
4603
|
+
const output = format === "domains-only" ? Object.keys(summary.domains) : summary;
|
|
4604
|
+
return {
|
|
4605
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
|
|
4606
|
+
};
|
|
4607
|
+
}
|
|
4608
|
+
);
|
|
4609
|
+
}
|
|
4610
|
+
|
|
3448
4611
|
// package.json
|
|
3449
4612
|
var package_default = {
|
|
3450
4613
|
dependencies: {
|
|
4614
|
+
"@stackwright-pro/types": "workspace:*",
|
|
3451
4615
|
"@modelcontextprotocol/sdk": "^1.10.0",
|
|
3452
4616
|
"@stackwright-pro/cli-data-explorer": "workspace:*",
|
|
3453
|
-
zod: "^4.3
|
|
4617
|
+
zod: "^4.4.3"
|
|
3454
4618
|
},
|
|
3455
4619
|
devDependencies: {
|
|
3456
|
-
"@types/node": "
|
|
3457
|
-
tsup: "
|
|
3458
|
-
typescript: "
|
|
3459
|
-
vitest: "
|
|
4620
|
+
"@types/node": "catalog:",
|
|
4621
|
+
tsup: "catalog:",
|
|
4622
|
+
typescript: "catalog:",
|
|
4623
|
+
vitest: "catalog:"
|
|
3460
4624
|
},
|
|
3461
4625
|
scripts: {
|
|
4626
|
+
prepublishOnly: "node scripts/verify-integrity-sync.js",
|
|
3462
4627
|
build: "tsup",
|
|
3463
4628
|
dev: "tsup --watch",
|
|
3464
4629
|
start: "node dist/server.js",
|
|
@@ -3466,10 +4631,13 @@ var package_default = {
|
|
|
3466
4631
|
"test:coverage": "vitest run --coverage"
|
|
3467
4632
|
},
|
|
3468
4633
|
name: "@stackwright-pro/mcp",
|
|
3469
|
-
version: "0.2.0-alpha.
|
|
4634
|
+
version: "0.2.0-alpha.52",
|
|
3470
4635
|
description: "MCP tools for Stackwright Pro - Data Explorer, Security, ISR, and Dashboard generation",
|
|
3471
|
-
license: "
|
|
4636
|
+
license: "SEE LICENSE IN LICENSE",
|
|
3472
4637
|
main: "./dist/server.js",
|
|
4638
|
+
bin: {
|
|
4639
|
+
"stackwright-pro-mcp": "./dist/server.js"
|
|
4640
|
+
},
|
|
3473
4641
|
module: "./dist/server.mjs",
|
|
3474
4642
|
types: "./dist/server.d.ts",
|
|
3475
4643
|
exports: {
|
|
@@ -3482,6 +4650,11 @@ var package_default = {
|
|
|
3482
4650
|
types: "./dist/integrity.d.ts",
|
|
3483
4651
|
import: "./dist/integrity.mjs",
|
|
3484
4652
|
require: "./dist/integrity.js"
|
|
4653
|
+
},
|
|
4654
|
+
"./type-schemas": {
|
|
4655
|
+
types: "./dist/tools/type-schemas.d.ts",
|
|
4656
|
+
import: "./dist/tools/type-schemas.mjs",
|
|
4657
|
+
require: "./dist/tools/type-schemas.js"
|
|
3485
4658
|
}
|
|
3486
4659
|
},
|
|
3487
4660
|
files: [
|
|
@@ -3509,7 +4682,9 @@ registerPipelineTools(server);
|
|
|
3509
4682
|
registerSafeWriteTools(server);
|
|
3510
4683
|
registerAuthTools(server);
|
|
3511
4684
|
registerIntegrityTools(server);
|
|
4685
|
+
registerArtifactSigningTools(server);
|
|
3512
4686
|
registerDomainTools(server);
|
|
4687
|
+
registerTypeSchemasTool(server);
|
|
3513
4688
|
async function main() {
|
|
3514
4689
|
const transport = new import_stdio.StdioServerTransport();
|
|
3515
4690
|
await server.connect(transport);
|