@stackwright-pro/otters 0.3.0-alpha.1 → 1.0.0-alpha.3

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/package.json CHANGED
@@ -1,12 +1,16 @@
1
1
  {
2
2
  "name": "@stackwright-pro/otters",
3
- "version": "0.3.0-alpha.1",
3
+ "version": "1.0.0-alpha.3",
4
4
  "description": "Stackwright Pro Otter Raft - AI agents for enterprise features (CAC auth, API dashboards, government use cases)",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "https://github.com/Per-Aspera-LLC/stackwright-pro"
9
9
  },
10
+ "devDependencies": {
11
+ "vitest": "^1.4.0",
12
+ "zod": "^3.22.4"
13
+ },
10
14
  "exports": {
11
15
  "./src": "./src",
12
16
  "./pro-foreman": "./src/stackwright-pro-foreman-otter.json"
@@ -16,10 +20,14 @@
16
20
  "src"
17
21
  ],
18
22
  "peerDependencies": {
19
- "@stackwright-pro/mcp": "*"
23
+ "@stackwright-pro/mcp": "0.2.0-alpha.0"
20
24
  },
21
25
  "scripts": {
26
+ "generate-checksums": "node scripts/generate-checksums.js",
27
+ "verify-checksums": "node scripts/verify-checksums.js",
22
28
  "postinstall": "node scripts/install-agents.js",
23
- "test": "echo 'Pro otters are configuration-only'"
29
+ "test": "vitest run",
30
+ "test:watch": "vitest",
31
+ "test:coverage": "vitest run --coverage"
24
32
  }
25
33
  }
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * generate-checksums.js
4
+ * Computes SHA-256 for every *-otter.json in src/ and writes src/checksums.json
5
+ * Run: node scripts/generate-checksums.js
6
+ * Auto-run: npm prepare (before publish), npm pretest (before tests)
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const crypto = require('crypto');
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+
15
+ const scriptDir = __dirname;
16
+ const packageRoot = path.resolve(scriptDir, '..');
17
+ const srcDir = path.join(packageRoot, 'src');
18
+ const outputFile = path.join(srcDir, 'checksums.json');
19
+
20
+ async function generateChecksums() {
21
+ const entries = await fs.promises.readdir(srcDir);
22
+
23
+ const otterFiles = entries
24
+ .filter((f) => f.endsWith('-otter.json'))
25
+ .sort(); // alphabetical — deterministic output, no excuses
26
+
27
+ const files = {};
28
+ for (const filename of otterFiles) {
29
+ const filePath = path.join(srcDir, filename);
30
+ const raw = await fs.promises.readFile(filePath); // raw Buffer → utf-8 bytes, no funny business
31
+ const digest = crypto.createHash('sha256').update(raw).digest('hex');
32
+ files[filename] = digest;
33
+ }
34
+
35
+ const manifest = {
36
+ version: '1.0',
37
+ algorithm: 'sha256',
38
+ generated: new Date().toISOString(),
39
+ files,
40
+ };
41
+
42
+ await fs.promises.writeFile(outputFile, JSON.stringify(manifest, null, 2) + '\n', 'utf8');
43
+
44
+ console.log(`✅ checksums.json written with ${otterFiles.length} entr${otterFiles.length === 1 ? 'y' : 'ies'}:`);
45
+ for (const [name, hash] of Object.entries(files)) {
46
+ console.log(` ${name}: ${hash}`);
47
+ }
48
+ }
49
+
50
+ generateChecksums().catch((err) => {
51
+ console.error('❌ generate-checksums failed:', err.message);
52
+ process.exit(1);
53
+ });
@@ -12,12 +12,10 @@ const AGENTS_DIR = path.join(os.homedir(), '.code_puppy', 'agents');
12
12
 
13
13
  // Resolve package root relative to this script's location
14
14
  const scriptDir = __dirname;
15
- const packageRoot = scriptDir.endsWith('/scripts')
16
- ? path.dirname(scriptDir) // Running from within the package
17
- : path.join(scriptDir, 'packages', 'otters'); // Running from monorepo root
15
+ const packageRoot = path.resolve(scriptDir, '..');
18
16
 
19
17
  // All Pro otters are in the src/ directory
20
- const OTTERS_DIR = path.join(packageRoot, 'src');
18
+ const srcDir = path.join(packageRoot, 'src');
21
19
 
22
20
  async function installAgents() {
23
21
  try {
@@ -25,29 +23,37 @@ async function installAgents() {
25
23
  await fs.promises.mkdir(AGENTS_DIR, { recursive: true });
26
24
 
27
25
  // Copy all JSON files from src/ to ~/.code_puppy/agents/
28
- const files = await fs.promises.readdir(OTTERS_DIR);
26
+ const files = await fs.promises.readdir(srcDir);
29
27
 
30
28
  let installedCount = 0;
31
29
  for (const file of files) {
32
30
  if (file.endsWith('-otter.json')) {
33
- const srcPath = path.join(OTTERS_DIR, file);
31
+ const srcPath = path.join(srcDir, file);
34
32
  const destPath = path.join(AGENTS_DIR, file);
35
33
 
36
34
  await fs.promises.copyFile(srcPath, destPath);
37
- console.log(`✅ Installed Pro agent: ${file}`);
35
+ console.log(`✅ Installed: ${file}`);
38
36
  installedCount++;
39
37
  }
40
38
  }
41
39
 
40
+ // Also install checksums manifest (informational — Python coordinator uses hardcoded constants)
41
+ const checksumsSource = path.join(packageRoot, 'src', 'checksums.json');
42
+ const checksumsDest = path.join(AGENTS_DIR, 'otter-checksums.REFERENCE-ONLY.json');
43
+ if (fs.existsSync(checksumsSource)) {
44
+ await fs.promises.copyFile(checksumsSource, checksumsDest);
45
+ console.log('✅ Installed: otter-checksums.REFERENCE-ONLY.json');
46
+ }
47
+
42
48
  if (installedCount > 0) {
49
+ console.log(`Installing otters from: ${srcDir}`);
43
50
  console.log(`\n🦦🦦 Pro otters installed to ${AGENTS_DIR}`);
44
- console.log(' Run "code-puppy -i -a stackwright-pro-foreman-otter" to start!');
45
51
  } else {
46
52
  console.log('⚠️ No Pro otter files found to install');
47
53
  }
48
54
  } catch (error) {
49
- // Don't fail the install if this script has issues
50
- console.warn(`⚠️ Failed to install Pro otters: ${error.message}`);
55
+ console.error(`❌ FATAL: Failed to install Pro otters: ${error.message}`);
56
+ process.exit(1); // Fail the npm install surface it early
51
57
  }
52
58
  }
53
59
 
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * verify-checksums.js
4
+ * Read-only: verifies that *-otter.json files match the committed checksums.json
5
+ * Does NOT regenerate. For regeneration: node scripts/generate-checksums.js
6
+ *
7
+ * Exit codes:
8
+ * 0 = all checksums match
9
+ * 1 = one or more mismatches detected (tamper detected or file missing)
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ const crypto = require('crypto');
15
+ const fs = require('fs');
16
+ const path = require('path');
17
+
18
+ const packageRoot = path.resolve(__dirname, '..');
19
+ const srcDir = path.join(packageRoot, 'src');
20
+ const checksumsPath = path.join(srcDir, 'checksums.json');
21
+
22
+ // Read checksums manifest
23
+ let manifest;
24
+ try {
25
+ manifest = JSON.parse(fs.readFileSync(checksumsPath, 'utf8'));
26
+ } catch (err) {
27
+ console.error(`❌ Cannot read checksums.json: ${err.message}`);
28
+ console.error('Run: node scripts/generate-checksums.js');
29
+ process.exit(1);
30
+ }
31
+
32
+ const expected = manifest.files || {};
33
+ let failures = 0;
34
+ let verified = 0;
35
+
36
+ for (const [filename, expectedHash] of Object.entries(expected)) {
37
+ const filePath = path.join(srcDir, filename);
38
+ try {
39
+ const raw = fs.readFileSync(filePath);
40
+ const actual = crypto.createHash('sha256').update(raw).digest('hex');
41
+ if (actual !== expectedHash) {
42
+ console.error(`❌ MISMATCH: ${filename}`);
43
+ console.error(` Expected: ${expectedHash.substring(0, 16)}...`);
44
+ console.error(` Actual: ${actual.substring(0, 16)}...`);
45
+ failures++;
46
+ } else {
47
+ console.log(`✅ ${filename}`);
48
+ verified++;
49
+ }
50
+ } catch (err) {
51
+ console.error(`❌ MISSING: ${filename} — ${err.message}`);
52
+ failures++;
53
+ }
54
+ }
55
+
56
+ if (failures > 0) {
57
+ console.error(`\n🚨 ${failures} checksum failure(s) detected. Run \`npm install @stackwright-pro/otters\` to restore.`);
58
+ process.exit(1);
59
+ } else {
60
+ console.log(`\n✅ All ${verified} otter checksums verified.`);
61
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "version": "1.0",
3
+ "algorithm": "sha256",
4
+ "generated": "2026-04-15T13:32:12.453Z",
5
+ "files": {
6
+ "stackwright-pro-api-otter.json": "ad0c3694af41000420229edce4108f860eaa58ab321f8618565d03ebce80bcac",
7
+ "stackwright-pro-auth-otter.json": "e8e02ef1389e0d5e55bfa6d960a050ab976bf7960fda4ae805675020874ce4c6",
8
+ "stackwright-pro-dashboard-otter.json": "0b4100afef4946bae259f5759aea872d7b1a25a00af191e1ead32bf9ee304d08",
9
+ "stackwright-pro-data-otter.json": "38ae3a26f064499a5f9773dfea1e2c21f9f358207110224a8e94c19443d236f1",
10
+ "stackwright-pro-foreman-otter.json": "bf8f0b411c5415afe0766dcfa051a0c3cb0eeea8f248a9ce44d017c104a9a314",
11
+ "stackwright-pro-page-otter.json": "0973f1b75a481fd177c5ada1a965f8c32e07f97fc28bbbf03b51d9e6d2af2f74"
12
+ }
13
+ }
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Question Adapter - Converts between Question Manifest format and ask_user_question format
3
+ *
4
+ * The ask_user_question MCP tool requires:
5
+ * - question: string (full text)
6
+ * - header: string (max 12 chars, short label)
7
+ * - multi_select: boolean
8
+ * - options: {label, description}[] (REQUIRED, min 2)
9
+ *
10
+ * Our Question Manifest format has:
11
+ * - id: string (e.g., "api-1")
12
+ * - question: string
13
+ * - type: 'text' | 'select' | 'multi-select' | 'confirm'
14
+ * - options?: {label, value}[] (optional for text/confirm)
15
+ * - required?: boolean
16
+ * - default?: string | boolean | string[]
17
+ * - dependsOn?: {questionId, value}
18
+ */
19
+
20
+ export interface QuestionManifestQuestion {
21
+ id: string;
22
+ question: string;
23
+ type: 'text' | 'select' | 'multi-select' | 'confirm';
24
+ required?: boolean;
25
+ options?: { label: string; value: string }[];
26
+ default?: string | boolean | string[];
27
+ help?: string;
28
+ dependsOn?: {
29
+ questionId: string;
30
+ value: string | string[];
31
+ };
32
+ }
33
+
34
+ export interface AskUserQuestionOption {
35
+ label: string;
36
+ description?: string;
37
+ }
38
+
39
+ export interface AskUserQuestion {
40
+ question: string;
41
+ header: string;
42
+ multi_select: boolean;
43
+ options: AskUserQuestionOption[];
44
+ }
45
+
46
+ /**
47
+ * Truncate string to max length, adding ellipsis if needed
48
+ */
49
+ function truncate(str: string, maxLength: number): string {
50
+ if (str.length <= maxLength) return str;
51
+ return str.substring(0, maxLength - 1) + '…';
52
+ }
53
+
54
+ /**
55
+ * Generate a valid header from a question ID
56
+ * Headers must be max 12 chars, alphanumeric with hyphens
57
+ */
58
+ function generateHeader(id: string): string {
59
+ // Remove numbers and prefix, keep meaningful part
60
+ // e.g., "api-1" -> "API-1", "auth-3" -> "AUTH-3"
61
+ const parts = id.split('-');
62
+ if (parts.length >= 2) {
63
+ const prefix = parts[0].toUpperCase().substring(0, 4);
64
+ const num = parts[1];
65
+ return truncate(`${prefix}-${num}`, 12);
66
+ }
67
+ return truncate(
68
+ id
69
+ .toUpperCase()
70
+ .replace(/[^A-Z0-9]/g, '')
71
+ .substring(0, 12),
72
+ 12
73
+ );
74
+ }
75
+
76
+ /**
77
+ * Generate default options for question types that don't have options
78
+ */
79
+ function generateDefaultOptions(type: string): AskUserQuestionOption[] {
80
+ switch (type) {
81
+ case 'confirm':
82
+ return [
83
+ { label: 'Yes', description: 'Enable or confirm this option' },
84
+ { label: 'No', description: 'Disable or decline this option' },
85
+ ];
86
+ case 'text':
87
+ return [
88
+ { label: 'Specify', description: 'I will provide a specific value' },
89
+ { label: 'Skip', description: 'Use default or skip this question' },
90
+ ];
91
+ default:
92
+ return [
93
+ { label: 'Option 1', description: 'First option' },
94
+ { label: 'Option 2', description: 'Second option' },
95
+ ];
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Convert a single QuestionManifest question to ask_user_question format
101
+ */
102
+ export function adaptQuestion(q: QuestionManifestQuestion): AskUserQuestion {
103
+ // Generate header from ID
104
+ const header = generateHeader(q.id);
105
+
106
+ // Determine multi_select
107
+ const multiSelect = q.type === 'multi-select';
108
+
109
+ // Handle options - use provided or generate defaults
110
+ let options: AskUserQuestionOption[];
111
+
112
+ if (q.options && q.options.length >= 2) {
113
+ // Use provided options, adapt format
114
+ options = q.options.map((opt) => ({
115
+ label: truncate(opt.label, 50),
116
+ description: opt.value !== opt.label ? opt.value : undefined,
117
+ }));
118
+ } else if (q.options && q.options.length === 1) {
119
+ // Single option - add a default
120
+ options = [
121
+ ...q.options.map((opt) => ({ label: truncate(opt.label, 50), description: opt.value })),
122
+ { label: 'Other', description: 'Specify a different value' },
123
+ ];
124
+ } else {
125
+ // No options - generate defaults based on type
126
+ options = generateDefaultOptions(q.type);
127
+ }
128
+
129
+ // Ensure minimum 2 options (MCP tool requirement)
130
+ if (options.length < 2) {
131
+ options.push({ label: 'Other', description: 'Alternative option' });
132
+ }
133
+
134
+ // Limit to 6 options (MCP tool max)
135
+ options = options.slice(0, 6);
136
+
137
+ return {
138
+ question: q.question + (q.help ? `\n\n${q.help}` : ''),
139
+ header,
140
+ multi_select: multiSelect,
141
+ options,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Adapt multiple questions, filtering by dependsOn
147
+ *
148
+ * @param questions - All questions from manifest
149
+ * @param answers - Previously answered questions (for filtering conditionals)
150
+ * @returns Questions adapted for ask_user_question, with conditionals resolved
151
+ */
152
+ export function adaptQuestions(
153
+ questions: QuestionManifestQuestion[],
154
+ answers: Record<string, string | string[] | boolean> = {}
155
+ ): AskUserQuestion[] {
156
+ const adapted: AskUserQuestion[] = [];
157
+
158
+ for (const q of questions) {
159
+ // Check dependsOn condition
160
+ if (q.dependsOn) {
161
+ const dependsAnswer = answers[q.dependsOn.questionId];
162
+ if (dependsAnswer === undefined) {
163
+ // Parent question not answered yet - skip this conditional question
164
+ continue;
165
+ }
166
+
167
+ // Check if the answer matches the condition
168
+ const expectedValues = Array.isArray(q.dependsOn.value)
169
+ ? q.dependsOn.value
170
+ : [q.dependsOn.value];
171
+
172
+ const answerValue = Array.isArray(dependsAnswer) ? dependsAnswer[0] : dependsAnswer;
173
+
174
+ if (!expectedValues.includes(answerValue as string)) {
175
+ // Condition not met - skip this question
176
+ continue;
177
+ }
178
+ }
179
+
180
+ adapted.push(adaptQuestion(q));
181
+ }
182
+
183
+ return adapted;
184
+ }
185
+
186
+ /**
187
+ * Parse JSON response from LLM (handles various formats)
188
+ */
189
+ export function parseLLMQuestionsResponse(text: string): QuestionManifestQuestion[] {
190
+ // Try to extract JSON from the response
191
+ let jsonStr = text;
192
+
193
+ // Remove markdown code blocks
194
+ jsonStr = jsonStr.replace(/```(?:json|javascript)?\s*/gi, '');
195
+ jsonStr = jsonStr.replace(/```\s*$/gm, '');
196
+
197
+ // Find JSON object or array
198
+ const firstBrace = jsonStr.indexOf('{');
199
+ const firstBracket = jsonStr.indexOf('[');
200
+
201
+ let start = -1;
202
+ if (firstBrace !== -1 && firstBracket !== -1) {
203
+ start = Math.min(firstBrace, firstBracket);
204
+ } else if (firstBrace !== -1) {
205
+ start = firstBrace;
206
+ } else if (firstBracket !== -1) {
207
+ start = firstBracket;
208
+ }
209
+
210
+ if (start === -1) {
211
+ throw new Error('No JSON found in response');
212
+ }
213
+
214
+ jsonStr = jsonStr.substring(start);
215
+
216
+ // Handle trailing text after JSON
217
+ const lastBrace = jsonStr.lastIndexOf('}');
218
+ const lastBracket = jsonStr.lastIndexOf(']');
219
+ const end = Math.max(lastBrace, lastBracket);
220
+
221
+ if (end === -1) {
222
+ throw new Error('Invalid JSON structure');
223
+ }
224
+
225
+ jsonStr = jsonStr.substring(0, end + 1);
226
+
227
+ // Fix common issues
228
+ jsonStr = jsonStr.replace(/,(\s*[}\]])/g, '$1'); // Remove trailing commas
229
+ jsonStr = jsonStr.replace(/'/g, '"'); // Single quotes to double
230
+
231
+ const parsed = JSON.parse(jsonStr);
232
+
233
+ // Handle various JSON structures
234
+ let questions: unknown[];
235
+
236
+ if (Array.isArray(parsed)) {
237
+ questions = parsed;
238
+ } else if (parsed.questions && Array.isArray(parsed.questions)) {
239
+ questions = parsed.questions;
240
+ } else if (parsed.data && Array.isArray((parsed.data as Record<string, unknown>).questions)) {
241
+ questions = (parsed.data as Record<string, unknown>).questions as unknown[];
242
+ } else {
243
+ throw new Error('No questions array found in response');
244
+ }
245
+
246
+ return questions as QuestionManifestQuestion[];
247
+ }
248
+
249
+ /**
250
+ * Convert ask_user_question answers back to manifest format
251
+ */
252
+ export function answersToManifestFormat(
253
+ answers: { question_header: string; selected_options: string[]; other_text?: string | null }[],
254
+ questions: QuestionManifestQuestion[]
255
+ ): Record<string, string | string[] | boolean> {
256
+ const result: Record<string, string | string[] | boolean> = {};
257
+
258
+ for (const answer of answers) {
259
+ // Find matching question by header
260
+ const headerLower = answer.question_header.toLowerCase();
261
+ const question = questions.find((q) => {
262
+ const qHeader = generateHeader(q.id).toLowerCase();
263
+ return qHeader === headerLower || q.id.toLowerCase().includes(headerLower);
264
+ });
265
+
266
+ if (!question) {
267
+ // Try to match by header prefix
268
+ const matched = questions.find((q) => {
269
+ const qHeader = generateHeader(q.id).toLowerCase();
270
+ return qHeader.startsWith(headerLower.split('-')[0]);
271
+ });
272
+ if (matched) {
273
+ result[matched.id] = answer.selected_options[0] || '';
274
+ }
275
+ continue;
276
+ }
277
+
278
+ // Handle multi-select vs single select
279
+ if (question.type === 'multi-select' || answer.selected_options.length > 1) {
280
+ result[question.id] = answer.selected_options;
281
+ } else if (question.type === 'confirm') {
282
+ result[question.id] = answer.selected_options[0] === 'Yes';
283
+ } else {
284
+ // For text or single select, use first option or other_text
285
+ if (answer.other_text) {
286
+ result[question.id] = answer.other_text;
287
+ } else if (answer.selected_options.length > 0) {
288
+ // Map label back to value if possible
289
+ const option = question.options?.find((o) => o.label === answer.selected_options[0]);
290
+ result[question.id] = option?.value ?? answer.selected_options[0];
291
+ }
292
+ }
293
+ }
294
+
295
+ return result;
296
+ }
@@ -21,12 +21,138 @@
21
21
  "4. List available entities for user selection",
22
22
  "",
23
23
  "## OUTPUT FORMAT",
24
- "Report findings as a simple list:",
25
- "- Entities: {list}",
26
- "- Auth: {type}",
27
- "- Base URL: {url}",
28
24
  "",
29
- "## PASS TO DATA OTTER",
30
- "After analysis, invoke pro-data-otter to wire collections."
25
+ "**You return a JSON artifact to the Foreman. You do NOT create files.**",
26
+ "",
27
+ "Return ONLY valid JSON \u2014 no markdown fences, no prose, no comments. Use one of these two shapes:",
28
+ "",
29
+ "SUCCESS SHAPE:",
30
+ "```json",
31
+ "{",
32
+ " \"entities\": [",
33
+ " {",
34
+ " \"name\": \"Shipment\",",
35
+ " \"endpoint\": \"/shipments\",",
36
+ " \"method\": \"GET\",",
37
+ " \"revalidate\": 60,",
38
+ " \"mutationType\": null",
39
+ " }",
40
+ " ],",
41
+ " \"auth\": {",
42
+ " \"type\": \"bearer\",",
43
+ " \"header\": \"Authorization\",",
44
+ " \"envVar\": \"MARINE_LOGISTICS_API_TOKEN\"",
45
+ " },",
46
+ " \"baseUrl\": \"https://api.marine-logistics.mil/v2\",",
47
+ " \"specPath\": \"./specs/marine-combat-logistics.yaml\"",
48
+ "}",
49
+ "```",
50
+ "",
51
+ "ERROR SHAPE (use if spec is missing, unreadable, or unsupported format):",
52
+ "```json",
53
+ "{",
54
+ " \"error\": \"Spec file not found at ./specs/missing.yaml\",",
55
+ " \"specPath\": \"./specs/missing.yaml\"",
56
+ "}",
57
+ "```",
58
+ "",
59
+ "Field notes:",
60
+ "- entities[].revalidate: seconds for ISR cache (from x-revalidate extension), or null",
61
+ "- entities[].mutationType: \"create\"|\"update\"|\"delete\" for mutations, null for GET",
62
+ "- auth.type: one of \"none\" | \"api-key\" | \"bearer\" | \"oauth2\" | \"cac\"",
63
+ " (cac = DoD Common Access Card / PKI certificate authentication)",
64
+ "- auth.envVar: environment variable name that will hold the credential",
65
+ "",
66
+ "---",
67
+ "",
68
+ "## SCOPE BOUNDARIES",
69
+ "",
70
+ "✅ **You DO:**",
71
+ "- Read and parse OpenAPI/GraphQL/AsyncAPI specs",
72
+ "- Extract entity names, endpoint paths, auth schemes, and base URLs",
73
+ "- Return a structured JSON artifact to the Foreman",
74
+ "",
75
+ "❌ **You DON'T:**",
76
+ "- Create any files (no .ts, no .js, no .json files on disk)",
77
+ "- Write TypeScript types, interfaces, or classes",
78
+ "- Write Zod schemas",
79
+ "- Generate API client classes (BaseApiClient, etc.)",
80
+ "- Write to src/generated/ or any other directory",
81
+ "",
82
+ "**WHY:** TypeScript type generation is @stackwright-pro/openapi's job at build time.",
83
+ "The otter's job is discovery and configuration — not code generation.",
84
+ "If you find yourself about to call create_file or replace_in_file, STOP.",
85
+ "Return your JSON artifact to the Foreman instead.",
86
+ "---",
87
+ "",
88
+ "## TERMINATION",
89
+ "",
90
+ "Once you have returned your JSON artifact, your job is complete.",
91
+ "Do NOT invoke other agents. Do NOT create files. Do NOT send follow-up messages.",
92
+ "The Foreman handles all routing after receiving your artifact.",
93
+ "",
94
+ "You are invoked ONE TIME by the Foreman with full context in this prompt.",
95
+ "The spec path will be provided in the prompt \u2014 you do not need to ask the user for it.",
96
+ "",
97
+ "---",
98
+ "",
99
+ "## QUESTION_COLLECTION_MODE",
100
+ "",
101
+ "When invoked with QUESTION_COLLECTION_MODE=true, return questions for the user INSTEAD of doing work.",
102
+ "",
103
+ "If the prompt contains \"QUESTION_COLLECTION_MODE=true\", respond ONLY with this JSON (no other text):",
104
+ "",
105
+ "**IMPORTANT**: Your response MUST include a `requiredPackages` field alongside the `questions` array. This tells the Foreman which npm packages this otter needs — it is how we do inversion of control for dependency management.",
106
+ "",
107
+ "{",
108
+ " \"questions\": [",
109
+ " {",
110
+ " \"id\": \"api-1\",",
111
+ " \"question\": \"Do you have an existing OpenAPI spec?\",",
112
+ " \"type\": \"confirm\",",
113
+ " \"required\": true,",
114
+ " \"default\": \"no\"",
115
+ " },",
116
+ " {",
117
+ " \"id\": \"api-2\",",
118
+ " \"question\": \"What is the URL or path to your OpenAPI spec?\",",
119
+ " \"type\": \"text\",",
120
+ " \"required\": true,",
121
+ " \"dependsOn\": { \"questionId\": \"api-1\", \"value\": \"yes\" }",
122
+ " },",
123
+ " {",
124
+ " \"id\": \"api-3\",",
125
+ " \"question\": \"What is your API authentication method?\",",
126
+ " \"type\": \"select\",",
127
+ " \"options\": [",
128
+ " { \"label\": \"No auth\", \"value\": \"none\" },",
129
+ " { \"label\": \"API Key\", \"value\": \"api-key\" },",
130
+ " { \"label\": \"OAuth2\", \"value\": \"oauth2\" },",
131
+ " { \"label\": \"OIDC/SAML\", \"value\": \"oidc\" },",
132
+ " { \"label\": \"CAC/PIV (DoD)\", \"value\": \"cac\" }",
133
+ " ],",
134
+ " \"required\": true",
135
+ " },",
136
+ " {",
137
+ " \"id\": \"api-4\",",
138
+ " \"question\": \"Which entities from the spec do you need?\",",
139
+ " \"type\": \"multi-select\",",
140
+ " \"options\": [",
141
+ " { \"label\": \"I will tell you after you parse it\", \"value\": \"discover\" }",
142
+ " ],",
143
+ " \"required\": false,",
144
+ " \"help\": \"You can select specific entities or let me discover them after parsing the spec.\"",
145
+ " }",
146
+ " ],",
147
+ " \"requiredPackages\": {",
148
+ " \"dependencies\": {",
149
+ " \"@stackwright-pro/openapi\": \"latest\",",
150
+ " \"zod\": \"^3.23.0\"",
151
+ " },",
152
+ " \"devPackages\": {",
153
+ " \"@stoplight/prism-cli\": \"^5.14.2\"",
154
+ " }",
155
+ " }",
156
+ "}"
31
157
  ]
32
158
  }