@stackwright-pro/otters 1.0.0-alpha.11 → 1.0.0-alpha.12

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@stackwright-pro/otters",
3
- "version": "1.0.0-alpha.11",
3
+ "version": "1.0.0-alpha.12",
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": {
@@ -24,7 +24,7 @@
24
24
  "access": "public"
25
25
  },
26
26
  "peerDependencies": {
27
- "@stackwright-pro/mcp": "^0.2.0-alpha.3"
27
+ "@stackwright-pro/mcp": "^0.2.0-alpha.4"
28
28
  },
29
29
  "scripts": {
30
30
  "generate-checksums": "node scripts/generate-checksums.js",
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "version": "1.0",
3
3
  "algorithm": "sha256",
4
- "generated": "2026-04-21T13:37:04.418Z",
4
+ "generated": "2026-04-21T15:16:26.700Z",
5
5
  "files": {
6
6
  "stackwright-pro-api-otter.json": "ad0c3694af41000420229edce4108f860eaa58ab321f8618565d03ebce80bcac",
7
- "stackwright-pro-auth-otter.json": "e8e02ef1389e0d5e55bfa6d960a050ab976bf7960fda4ae805675020874ce4c6",
7
+ "stackwright-pro-auth-otter.json": "53da8d8fd7e922dfaec1f4c8f7a047c81f31fcfed48fd1414a8d4998dac9f826",
8
8
  "stackwright-pro-dashboard-otter.json": "0b4100afef4946bae259f5759aea872d7b1a25a00af191e1ead32bf9ee304d08",
9
9
  "stackwright-pro-data-otter.json": "38ae3a26f064499a5f9773dfea1e2c21f9f358207110224a8e94c19443d236f1",
10
10
  "stackwright-pro-designer-otter.json": "46c9fd94a46f1a3f5267f4cb70c3db0adfc28dc7d4ac50256cbe40ea5363b4f0",
11
- "stackwright-pro-foreman-otter.json": "2e4a13443a8c6bf55d02ab6c0301fa840f63f1df6baaebf14fab06a72d6cc8ca",
11
+ "stackwright-pro-foreman-otter.json": "d6bf6b316c73d47c4a2c6136c46317eb0853b87189e63bb21e16f11ac4b908de",
12
12
  "stackwright-pro-page-otter.json": "0973f1b75a481fd177c5ada1a965f8c32e07f97fc28bbbf03b51d9e6d2af2f74",
13
13
  "stackwright-pro-theme-otter.json": "faa100f0530af75a64ae6e9d0ac8adb370542e5d980468e2d129223cb4aa85d7",
14
14
  "stackwright-pro-workflow-otter.json": "814305e9d170d28b7215ca63730b9fbeb7b9605113d2070cd5925cf92a9e30d3"
@@ -805,6 +805,7 @@
805
805
  " },",
806
806
  " \"devPackages\": {",
807
807
  " }",
808
- " }"
808
+ " }",
809
+ "}"
809
810
  ]
810
811
  }
@@ -24,7 +24,8 @@
24
24
  "stackwright_pro_configure_auth",
25
25
  "stackwright_pro_clarify",
26
26
  "stackwright_pro_detect_conflict",
27
- "stackwright_pro_get_defaults"
27
+ "stackwright_pro_get_defaults",
28
+ "stackwright_pro_present_phase_questions"
28
29
  ],
29
30
  "user_prompt": "",
30
31
  "system_prompt": [
@@ -264,53 +265,35 @@
264
265
  "",
265
266
  "Show the user what packages will be added, then call the tool. Do NOT block on install failures — the user’s workflow should not be interrupted by package plumbing.",
266
267
  "",
267
- "### Step 3: Adapt Questions for ask_user_question",
268
+ "### Step 3: Present Questions to User",
268
269
  "",
269
- "**CRITICAL:** ask_user_question requires options for ALL questions. You MUST adapt:",
270
+ "For each phase, call `stackwright_pro_present_phase_questions` with the raw manifest questions.",
271
+ "The tool handles all adaptation (label truncation, header generation, confirm/text defaults).",
270
272
  "",
271
273
  "```",
272
- "function adaptQuestion(q) {",
273
- " // Generate header from ID (max 12 chars)",
274
- " const parts = q.id.split('-');",
275
- " const prefix = parts[0].toUpperCase().substring(0, 3);",
276
- " const num = parts[1] || '';",
277
- " const header = (prefix + '-' + num).substring(0, 12);",
278
- " ",
279
- " // Determine multi_select",
280
- " const multiSelect = q.type === 'multi-select';",
281
- " ",
282
- " // Handle options - ALWAYS REQUIRED",
283
- " let options;",
284
- " if (q.options && q.options.length >= 2) {",
285
- " // Use provided options",
286
- " options = q.options.map(o => ({ label: o.label.substring(0, 50), description: o.value }));",
287
- " // IMPORTANT: The ask_user_question tool returns label text, not the original value.",
288
- " // Store the value in description so you can reverse-map it later.",
289
- " // When specialist otters receive answers, translate labels back to values:",
290
- " // e.g., user selected label 'Near real-time (minute-level freshness)' → value is in description → 'isr-fast'",
291
- " } else if (q.type === 'confirm') {",
292
- " // Generate Yes/No for confirm",
293
- " options = [",
294
- " { label: 'Yes', description: 'Enable or confirm' },",
295
- " { label: 'No', description: 'Disable or decline' }",
296
- " ];",
297
- " } else {",
298
- " // Generate defaults for text/select without options",
299
- " options = [",
300
- " { label: 'Specify', description: 'I will provide a value' },",
301
- " { label: 'Skip', description: 'Use default or skip' }",
302
- " ];",
303
- " }",
304
- " ",
305
- " return {",
306
- " question: q.question + (q.help ? '\\n\\n' + q.help : ''),",
307
- " header: header,",
308
- " multi_select: multiSelect,",
309
- " options: options.slice(0, 6) // Max 6 options",
310
- " };",
311
- "}",
274
+ "const result = await stackwright_pro_present_phase_questions({",
275
+ " phase: phase.phase,",
276
+ " questions: phase.questions, // Pass verbatim from manifest \u2014 no adaptation needed",
277
+ " answers: previousAnswers // Optional: pass collected answers for dependsOn filtering",
278
+ "});",
279
+ "",
280
+ "// result.adapted_questions is a pre-validated array ready for ask_user_question",
281
+ "const response = await ask_user_question({",
282
+ " questions: result.adapted_questions // Native array \u2014 never stringify this",
283
+ "});",
312
284
  "```",
313
285
  "",
286
+ "\u26a0\ufe0f CRITICAL: Pass result.adapted_questions directly as the questions parameter.",
287
+ "Do NOT JSON.stringify() it. Do NOT rebuild it. Pass the value as-is.",
288
+ "",
289
+ "The tool guarantees:",
290
+ "- All labels truncated to \u226450 chars",
291
+ "- All headers truncated to \u226412 chars",
292
+ "- confirm type questions have Yes/No options",
293
+ "- text type questions have Specify/Skip options",
294
+ "- Options capped at 6",
295
+ "- dependsOn conditions resolved",
296
+ "",
314
297
  "### Step 4: Write Manifest",
315
298
  "",
316
299
  "```",
@@ -338,17 +321,15 @@
338
321
  "const manifest = JSON.parse(read_file('.stackwright/question-manifest.json'));",
339
322
  "",
340
323
  "for (const phase of manifest.phases) {",
341
- " // Adapt questions for this phase",
342
- " const adaptedQuestions = phase.questions.map(adaptQuestion);",
343
- " ",
344
- " if (adaptedQuestions.length === 0) {",
345
- " // No questions for this phase - skip",
346
- " continue;",
347
- " }",
348
- " ",
349
- " // Present to user",
324
+ " const result = await stackwright_pro_present_phase_questions({",
325
+ " phase: phase.phase,",
326
+ " questions: phase.questions, // Pass verbatim from manifest \u2014 no adaptation needed",
327
+ " answers: previousAnswers // Optional: pass collected answers for dependsOn filtering",
328
+ " });",
329
+ "",
330
+ " // result.adapted_questions is a pre-validated array ready for ask_user_question",
350
331
  " const response = await ask_user_question({",
351
- " questions: adaptedQuestions",
332
+ " questions: result.adapted_questions // Native array \u2014 never stringify this",
352
333
  " });",
353
334
  " ",
354
335
  " if (response.cancelled) {",
@@ -1,296 +0,0 @@
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
- }