agent-state-machine 2.0.15 → 2.1.1

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.
Files changed (47) hide show
  1. package/bin/cli.js +1 -1
  2. package/lib/index.js +33 -0
  3. package/lib/remote/client.js +7 -2
  4. package/lib/runtime/agent.js +102 -67
  5. package/lib/runtime/index.js +13 -0
  6. package/lib/runtime/interaction.js +304 -0
  7. package/lib/runtime/prompt.js +39 -12
  8. package/lib/runtime/runtime.js +11 -10
  9. package/package.json +1 -1
  10. package/templates/project-builder/agents/assumptions-clarifier.md +0 -1
  11. package/templates/project-builder/agents/code-reviewer.md +0 -1
  12. package/templates/project-builder/agents/code-writer.md +0 -1
  13. package/templates/project-builder/agents/requirements-clarifier.md +0 -1
  14. package/templates/project-builder/agents/response-interpreter.md +25 -0
  15. package/templates/project-builder/agents/roadmap-generator.md +0 -1
  16. package/templates/project-builder/agents/sanity-checker.md +45 -0
  17. package/templates/project-builder/agents/sanity-runner.js +161 -0
  18. package/templates/project-builder/agents/scope-clarifier.md +0 -1
  19. package/templates/project-builder/agents/security-clarifier.md +0 -1
  20. package/templates/project-builder/agents/security-reviewer.md +0 -1
  21. package/templates/project-builder/agents/task-planner.md +0 -1
  22. package/templates/project-builder/agents/test-planner.md +0 -1
  23. package/templates/project-builder/scripts/interaction-helpers.js +33 -0
  24. package/templates/project-builder/scripts/workflow-helpers.js +2 -47
  25. package/templates/project-builder/workflow.js +214 -54
  26. package/vercel-server/api/session/[token].js +3 -3
  27. package/vercel-server/api/submit/[token].js +5 -3
  28. package/vercel-server/local-server.js +33 -6
  29. package/vercel-server/public/remote/index.html +17 -0
  30. package/vercel-server/ui/index.html +9 -1012
  31. package/vercel-server/ui/package-lock.json +2650 -0
  32. package/vercel-server/ui/package.json +25 -0
  33. package/vercel-server/ui/postcss.config.js +6 -0
  34. package/vercel-server/ui/src/App.jsx +236 -0
  35. package/vercel-server/ui/src/components/ChoiceInteraction.jsx +127 -0
  36. package/vercel-server/ui/src/components/ConfirmInteraction.jsx +51 -0
  37. package/vercel-server/ui/src/components/ContentCard.jsx +161 -0
  38. package/vercel-server/ui/src/components/CopyButton.jsx +27 -0
  39. package/vercel-server/ui/src/components/EventsLog.jsx +82 -0
  40. package/vercel-server/ui/src/components/Footer.jsx +66 -0
  41. package/vercel-server/ui/src/components/Header.jsx +38 -0
  42. package/vercel-server/ui/src/components/InteractionForm.jsx +42 -0
  43. package/vercel-server/ui/src/components/TextInteraction.jsx +72 -0
  44. package/vercel-server/ui/src/index.css +145 -0
  45. package/vercel-server/ui/src/main.jsx +8 -0
  46. package/vercel-server/ui/tailwind.config.js +19 -0
  47. package/vercel-server/ui/vite.config.js +11 -0
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Interaction Schema and Helpers
3
+ *
4
+ * Provides structured interaction types (choice, text, confirm) for human-in-the-loop workflows.
5
+ */
6
+
7
+ // Default schema for interactions
8
+ export const InteractionSchema = {
9
+ type: 'text',
10
+ slug: '',
11
+ prompt: '',
12
+ options: [],
13
+ allowCustom: true,
14
+ multiSelect: false,
15
+ placeholder: '',
16
+ validation: {
17
+ minLength: 0,
18
+ maxLength: 0,
19
+ pattern: ''
20
+ },
21
+ confirmLabel: 'Confirm',
22
+ cancelLabel: 'Cancel',
23
+ default: '',
24
+ context: {}
25
+ };
26
+
27
+ // Default schema for interaction responses
28
+ export const InteractionResponseSchema = {
29
+ slug: '',
30
+ selectedKey: '',
31
+ selectedKeys: [],
32
+ text: '',
33
+ confirmed: false,
34
+ raw: '',
35
+ interpreted: false,
36
+ isCustom: false,
37
+ customText: ''
38
+ };
39
+
40
+ /**
41
+ * Normalize an interaction object by merging with defaults
42
+ */
43
+ export function normalizeInteraction(interaction) {
44
+ if (!interaction || typeof interaction !== 'object') return null;
45
+ return {
46
+ ...InteractionSchema,
47
+ ...interaction,
48
+ validation: {
49
+ ...InteractionSchema.validation,
50
+ ...(interaction.validation || {})
51
+ }
52
+ };
53
+ }
54
+
55
+ /**
56
+ * Validate an interaction object
57
+ */
58
+ export function validateInteraction(interaction) {
59
+ const errors = [];
60
+ if (!interaction || typeof interaction !== 'object') {
61
+ return { valid: false, errors: ['Interaction must be an object'] };
62
+ }
63
+ if (!interaction.type) errors.push('Missing type');
64
+ if (!interaction.slug) errors.push('Missing slug');
65
+ if (!interaction.prompt) errors.push('Missing prompt');
66
+ if (interaction.type === 'choice' && (!Array.isArray(interaction.options) || interaction.options.length === 0)) {
67
+ errors.push('Choice interaction must include options');
68
+ }
69
+ return { valid: errors.length === 0, errors };
70
+ }
71
+
72
+ /**
73
+ * Normalize an interaction response object
74
+ */
75
+ export function normalizeInteractionResponse(response) {
76
+ if (!response || typeof response !== 'object') return null;
77
+ return {
78
+ ...InteractionResponseSchema,
79
+ ...response
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Validate an interaction response
85
+ */
86
+ export function validateInteractionResponse(response) {
87
+ const errors = [];
88
+ if (!response || typeof response !== 'object') {
89
+ return { valid: false, errors: ['Response must be an object'] };
90
+ }
91
+ if (!('raw' in response)) errors.push('Missing raw response');
92
+ return { valid: errors.length === 0, errors };
93
+ }
94
+
95
+ /**
96
+ * Create an interaction object with defaults
97
+ */
98
+ export function createInteraction(type, slug, options = {}) {
99
+ return normalizeInteraction({
100
+ type,
101
+ slug,
102
+ ...options
103
+ });
104
+ }
105
+
106
+ /**
107
+ * Format an interaction as a human-readable prompt string
108
+ */
109
+ export function formatInteractionPrompt(interaction) {
110
+ const prompt = String(interaction?.prompt || interaction?.question || interaction?.content || '').trim();
111
+ const lines = [];
112
+
113
+ if (prompt) lines.push(prompt);
114
+
115
+ if (interaction?.type === 'choice' && Array.isArray(interaction.options)) {
116
+ lines.push('', 'Options:');
117
+ interaction.options.forEach((opt, index) => {
118
+ const letter = String.fromCharCode(65 + index);
119
+ const label = opt.label || opt.key || `Option ${index + 1}`;
120
+ const desc = opt.description ? ` - ${opt.description}` : '';
121
+ lines.push(`- ${letter}: ${label}${desc}`);
122
+ });
123
+ if (interaction.allowCustom) {
124
+ lines.push('- Other: Provide a custom response');
125
+ }
126
+ }
127
+
128
+ if (interaction?.type === 'confirm') {
129
+ const confirmLabel = interaction.confirmLabel || 'Confirm';
130
+ const cancelLabel = interaction.cancelLabel || 'Cancel';
131
+ lines.push('', 'Options:');
132
+ lines.push(`- A: ${confirmLabel}`);
133
+ lines.push(`- B: ${cancelLabel}`);
134
+ }
135
+
136
+ return lines.join('\n').trim();
137
+ }
138
+
139
+ /**
140
+ * Match a single-select response against options
141
+ * Returns the matched option key/label, or null if no match
142
+ */
143
+ export function matchSingleSelect(options, input) {
144
+ const lower = String(input || '').toLowerCase().trim();
145
+
146
+ for (let i = 0; i < options.length; i += 1) {
147
+ const opt = options[i];
148
+ const letter = String.fromCharCode(65 + i).toLowerCase();
149
+ const key = String(opt.key || '').toLowerCase();
150
+ const label = String(opt.label || '').toLowerCase();
151
+ const letterRegex = new RegExp(`^${letter}(\\s|[-:.)\\]])`, 'i');
152
+
153
+ if (
154
+ lower === letter ||
155
+ letterRegex.test(lower) ||
156
+ lower === key ||
157
+ lower === label ||
158
+ lower.startsWith(`${label}:`) ||
159
+ lower.startsWith(`${label} -`)
160
+ ) {
161
+ return opt.key || opt.label || letter.toUpperCase();
162
+ }
163
+ }
164
+ return null;
165
+ }
166
+
167
+ /**
168
+ * Match a multi-select response against options
169
+ * Returns array of matched option keys/labels
170
+ */
171
+ export function matchMultiSelect(options, input) {
172
+ const lower = String(input || '').toLowerCase().trim();
173
+ const tokens = lower.split(/[,\s]+/).filter(Boolean);
174
+ const selections = new Set();
175
+
176
+ // Match letter patterns like "a:", "b)", "c -"
177
+ const letterMatches = lower.matchAll(/(^|[\s,])([a-z])\s*[-:.)\]]/g);
178
+ for (const match of letterMatches) {
179
+ const letter = match[2];
180
+ const index = letter.charCodeAt(0) - 97;
181
+ if (index >= 0 && index < options.length) {
182
+ const opt = options[index];
183
+ selections.add(opt.key || opt.label || letter.toUpperCase());
184
+ }
185
+ }
186
+
187
+ // Match tokens against keys/labels
188
+ tokens.forEach((token) => {
189
+ for (let i = 0; i < options.length; i += 1) {
190
+ const opt = options[i];
191
+ const letter = String.fromCharCode(65 + i).toLowerCase();
192
+ const key = String(opt.key || '').toLowerCase();
193
+ const label = String(opt.label || '').toLowerCase();
194
+ if (token === letter || token === key || token === label) {
195
+ selections.add(opt.key || opt.label || letter.toUpperCase());
196
+ }
197
+ }
198
+ });
199
+
200
+ return Array.from(selections);
201
+ }
202
+
203
+ /**
204
+ * Parse a raw response string into a structured response object
205
+ *
206
+ * @param {object} interaction - The interaction schema
207
+ * @param {string|object} rawResponse - The raw user response
208
+ * @param {function} [interpreter] - Optional async function to interpret ambiguous responses
209
+ * @returns {object} Structured response object
210
+ */
211
+ export async function parseInteractionResponse(interaction, rawResponse, interpreter = null) {
212
+ const normalized = normalizeInteraction(interaction);
213
+ if (!normalized) return { raw: String(rawResponse ?? '') };
214
+
215
+ // If already an object, normalize it
216
+ if (rawResponse && typeof rawResponse === 'object') {
217
+ return normalizeInteractionResponse({
218
+ ...rawResponse,
219
+ raw: rawResponse.raw ?? rawResponse.text ?? ''
220
+ });
221
+ }
222
+
223
+ const raw = String(rawResponse ?? '').trim();
224
+
225
+ // Text type - just return the text
226
+ if (normalized.type === 'text') {
227
+ return { text: raw, raw };
228
+ }
229
+
230
+ // Confirm type - check for yes/no patterns
231
+ if (normalized.type === 'confirm') {
232
+ const lower = raw.toLowerCase();
233
+ const confirmLabel = (normalized.confirmLabel || 'confirm').toLowerCase();
234
+ const cancelLabel = (normalized.cancelLabel || 'cancel').toLowerCase();
235
+
236
+ const confirmed = lower.startsWith('y') || lower.startsWith('a') ||
237
+ lower.startsWith('confirm') || lower === confirmLabel;
238
+ const cancelled = lower.startsWith('n') || lower.startsWith('b') ||
239
+ lower.startsWith('cancel') || lower === cancelLabel;
240
+
241
+ if (confirmed || cancelled) {
242
+ return { confirmed, raw };
243
+ }
244
+
245
+ // Try interpreter if provided
246
+ if (interpreter) {
247
+ const interpreted = await tryInterpreter(interpreter, normalized, raw);
248
+ if (interpreted) return interpreted;
249
+ }
250
+
251
+ // Default to not confirmed for ambiguous responses
252
+ return { confirmed: false, raw };
253
+ }
254
+
255
+ // Choice type - match against options
256
+ if (normalized.type === 'choice') {
257
+ const options = normalized.options || [];
258
+
259
+ if (normalized.multiSelect) {
260
+ const selectedKeys = matchMultiSelect(options, raw);
261
+ if (selectedKeys.length > 0) {
262
+ return { selectedKeys, raw };
263
+ }
264
+ } else {
265
+ const selectedKey = matchSingleSelect(options, raw);
266
+ if (selectedKey) {
267
+ return { selectedKey, raw };
268
+ }
269
+ }
270
+
271
+ // Try interpreter if provided
272
+ if (interpreter) {
273
+ const interpreted = await tryInterpreter(interpreter, normalized, raw);
274
+ if (interpreted) return interpreted;
275
+ }
276
+
277
+ // Treat as custom response if allowed
278
+ if (normalized.allowCustom !== false) {
279
+ return { isCustom: true, customText: raw, raw };
280
+ }
281
+ }
282
+
283
+ return { raw };
284
+ }
285
+
286
+ /**
287
+ * Helper to safely call the interpreter function
288
+ */
289
+ async function tryInterpreter(interpreter, interaction, raw) {
290
+ try {
291
+ const result = await interpreter(interaction, raw);
292
+ if (result && typeof result === 'object') {
293
+ if (result.selectedKey || result.selectedKeys?.length || result.confirmed !== undefined) {
294
+ return { ...result, raw, interpreted: true };
295
+ }
296
+ if (result.isCustom && interaction.allowCustom !== false) {
297
+ return { ...result, raw, interpreted: true };
298
+ }
299
+ }
300
+ } catch (error) {
301
+ // Silently fail - caller can handle uninterpreted responses
302
+ }
303
+ return null;
304
+ }
@@ -35,12 +35,23 @@ export async function askHuman(question, options = {}) {
35
35
 
36
36
  const slug = options.slug || generateSlug(question);
37
37
  const memoryKey = `_interaction_${slug}`;
38
+ const interaction = options.interaction || null;
39
+ const prompt = interaction?.prompt || question;
38
40
 
39
- runtime.prependHistory({
41
+ await runtime.prependHistory({
40
42
  event: 'PROMPT_REQUESTED',
41
43
  slug,
42
44
  targetKey: memoryKey,
43
- question
45
+ question: prompt,
46
+ type: interaction?.type || 'text',
47
+ prompt,
48
+ options: interaction?.options,
49
+ allowCustom: interaction?.allowCustom,
50
+ multiSelect: interaction?.multiSelect,
51
+ validation: interaction?.validation,
52
+ confirmLabel: interaction?.confirmLabel,
53
+ cancelLabel: interaction?.cancelLabel,
54
+ context: interaction?.context
44
55
  });
45
56
 
46
57
  // Check if we're in TTY mode (interactive terminal)
@@ -49,17 +60,19 @@ export async function askHuman(question, options = {}) {
49
60
  const answer = await askQuestionWithRemote(runtime, question, slug, memoryKey);
50
61
  console.log('');
51
62
 
63
+ const normalizedAnswer = normalizePromptAnswer(answer);
64
+
52
65
  // Save the response to memory
53
- runtime._rawMemory[memoryKey] = answer;
66
+ runtime._rawMemory[memoryKey] = normalizedAnswer;
54
67
  runtime.persist();
55
68
 
56
- runtime.prependHistory({
69
+ await runtime.prependHistory({
57
70
  event: 'PROMPT_ANSWERED',
58
71
  slug,
59
- answer: answer.substring(0, 100) + (answer.length > 100 ? '...' : '')
72
+ answer: normalizedAnswer.substring(0, 100) + (normalizedAnswer.length > 100 ? '...' : '')
60
73
  });
61
74
 
62
- return answer;
75
+ return normalizedAnswer;
63
76
  }
64
77
 
65
78
  // Non-TTY mode - create interaction file and wait inline
@@ -73,7 +86,7 @@ ${question}
73
86
 
74
87
  fs.writeFileSync(interactionFile, fileContent);
75
88
 
76
- runtime.prependHistory({
89
+ await runtime.prependHistory({
77
90
  event: 'INTERACTION_REQUESTED',
78
91
  slug,
79
92
  targetKey: memoryKey,
@@ -84,16 +97,18 @@ ${question}
84
97
  // Block and wait for user input (instead of throwing)
85
98
  const answer = await runtime.waitForInteraction(interactionFile, slug, memoryKey);
86
99
 
87
- runtime._rawMemory[memoryKey] = answer;
100
+ const normalizedAnswer = normalizePromptAnswer(answer);
101
+
102
+ runtime._rawMemory[memoryKey] = normalizedAnswer;
88
103
  runtime.persist();
89
104
 
90
- runtime.prependHistory({
105
+ await runtime.prependHistory({
91
106
  event: 'PROMPT_ANSWERED',
92
107
  slug,
93
- answer: answer.substring(0, 100) + (answer.length > 100 ? '...' : '')
108
+ answer: normalizedAnswer.substring(0, 100) + (normalizedAnswer.length > 100 ? '...' : '')
94
109
  });
95
110
 
96
- return answer;
111
+ return normalizedAnswer;
97
112
  }
98
113
 
99
114
  /**
@@ -173,4 +188,16 @@ function generateSlug(question) {
173
188
  .replace(/[^a-z0-9]+/g, '-')
174
189
  .replace(/^-+|-+$/g, '')
175
190
  .substring(0, 30);
176
- }
191
+ }
192
+
193
+ function normalizePromptAnswer(answer) {
194
+ if (typeof answer === 'string') return answer;
195
+ if (answer && typeof answer === 'object') {
196
+ if (typeof answer.raw === 'string') return answer.raw;
197
+ if (typeof answer.text === 'string') return answer.text;
198
+ if (typeof answer.selectedKey === 'string') return answer.selectedKey;
199
+ if (Array.isArray(answer.selectedKeys)) return answer.selectedKeys.join(', ');
200
+ if (typeof answer.confirmed === 'boolean') return answer.confirmed ? 'confirm' : 'cancel';
201
+ }
202
+ return String(answer ?? '');
203
+ }
@@ -88,7 +88,6 @@ export class WorkflowRuntime {
88
88
 
89
89
  // Agent interaction tracking for history logging
90
90
  this._agentResumeFlags = new Set();
91
- this._agentSuppressCompletion = new Set();
92
91
 
93
92
  // Agent error tracking (not persisted to memory, but accessible during run)
94
93
  this._agentErrors = [];
@@ -197,7 +196,7 @@ export class WorkflowRuntime {
197
196
  /**
198
197
  * Prepend an event to history.jsonl (newest first)
199
198
  */
200
- prependHistory(event) {
199
+ async prependHistory(event) {
201
200
  const entry = {
202
201
  timestamp: new Date().toISOString(),
203
202
  ...event
@@ -217,7 +216,7 @@ export class WorkflowRuntime {
217
216
 
218
217
  // Forward to remote if connected
219
218
  if (this.remoteClient && this.remoteEnabled) {
220
- this.remoteClient.sendEvent(entry);
219
+ await this.remoteClient.sendEvent(entry);
221
220
  }
222
221
  }
223
222
 
@@ -305,7 +304,7 @@ export class WorkflowRuntime {
305
304
  if (!this.startedAt) this.startedAt = new Date().toISOString();
306
305
  this.persist();
307
306
 
308
- this.prependHistory({ event: 'WORKFLOW_STARTED' });
307
+ await this.prependHistory({ event: 'WORKFLOW_STARTED' });
309
308
 
310
309
  const configPath = path.join(this.workflowDir, 'config.js');
311
310
  if (!fs.existsSync(configPath)) {
@@ -334,7 +333,7 @@ export class WorkflowRuntime {
334
333
 
335
334
  this.status = 'COMPLETED';
336
335
  this.persist();
337
- this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
336
+ await this.prependHistory({ event: 'WORKFLOW_COMPLETED' });
338
337
 
339
338
  console.log(`\n${C.green}✓ Workflow '${this.workflowName}' completed successfully!${C.reset}`);
340
339
  } catch (err) {
@@ -342,7 +341,7 @@ export class WorkflowRuntime {
342
341
  this._error = err.message;
343
342
  this.persist();
344
343
 
345
- this.prependHistory({
344
+ await this.prependHistory({
346
345
  event: 'WORKFLOW_FAILED',
347
346
  error: err.message
348
347
  });
@@ -419,7 +418,7 @@ export class WorkflowRuntime {
419
418
  const ask = () => {
420
419
  if (resolved) return;
421
420
 
422
- rl.question(`${C.dim}> ${C.reset}`, (answer) => {
421
+ rl.question(`${C.dim}> ${C.reset}`, async (answer) => {
423
422
  if (resolved) return;
424
423
 
425
424
  const a = answer.trim().toLowerCase();
@@ -427,7 +426,7 @@ export class WorkflowRuntime {
427
426
  cleanup();
428
427
  // Read and return the response from file
429
428
  try {
430
- const response = this.readInteractionResponse(filePath, slug, targetKey);
429
+ const response = await this.readInteractionResponse(filePath, slug, targetKey);
431
430
  resolve(response);
432
431
  } catch (err) {
433
432
  reject(err);
@@ -458,7 +457,7 @@ export class WorkflowRuntime {
458
457
  /**
459
458
  * Read the user's response from an interaction file
460
459
  */
461
- readInteractionResponse(filePath, slug, targetKey) {
460
+ async readInteractionResponse(filePath, slug, targetKey) {
462
461
  if (!fs.existsSync(filePath)) {
463
462
  throw new Error(`Interaction file not found: ${filePath}`);
464
463
  }
@@ -474,7 +473,7 @@ export class WorkflowRuntime {
474
473
  this._rawMemory[targetKey] = response;
475
474
  this.persist();
476
475
 
477
- this.prependHistory({
476
+ await this.prependHistory({
478
477
  event: 'INTERACTION_RESOLVED',
479
478
  slug,
480
479
  targetKey
@@ -605,6 +604,7 @@ export class WorkflowRuntime {
605
604
  * @param {string} serverUrl - Base URL of the remote server
606
605
  * @param {object} [options]
607
606
  * @param {string} [options.sessionToken] - Optional session token to reuse
607
+ * @param {boolean} [options.uiBaseUrl] - If true, return base URL for UI instead of /s/{token}
608
608
  * @returns {Promise<string>} The remote URL for browser access
609
609
  */
610
610
  async enableRemote(serverUrl, options = {}) {
@@ -612,6 +612,7 @@ export class WorkflowRuntime {
612
612
  serverUrl,
613
613
  workflowName: this.workflowName,
614
614
  sessionToken: options.sessionToken,
615
+ uiBaseUrl: options.uiBaseUrl,
615
616
  onInteractionResponse: (slug, targetKey, response) => {
616
617
  this.handleRemoteInteraction(slug, targetKey, response);
617
618
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-state-machine",
3
- "version": "2.0.15",
3
+ "version": "2.1.1",
4
4
  "type": "module",
5
5
  "description": "A workflow orchestrator for running agents and scripts in sequence with state management",
6
6
  "main": "lib/index.js",
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  model: med
3
- output: result
4
3
  format: json
5
4
  interaction: true
6
5
  ---
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  model: high
3
- output: result
4
3
  format: json
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  model: high
3
- output: result
4
3
  format: json
5
4
  ---
6
5
 
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  model: med
3
- output: result
4
3
  format: json
5
4
  interaction: true
6
5
  ---
@@ -0,0 +1,25 @@
1
+ ---
2
+ model: fast
3
+ format: json
4
+ ---
5
+
6
+ You are interpreting a user's natural language response against a structured interaction schema.
7
+
8
+ Return JSON only with:
9
+ - selectedKey (string or null)
10
+ - selectedKeys (array, optional)
11
+ - isCustom (boolean)
12
+ - customText (string, optional)
13
+ - confidence ("low" | "medium" | "high")
14
+ - reasoning (short string)
15
+
16
+ Rules:
17
+ - Prefer matching to interaction.options by key or label.
18
+ - If no clear match and allowCustom is true, set isCustom=true and include customText.
19
+ - If ambiguous, set confidence="low" and selectedKey=null.
20
+
21
+ Input:
22
+ {{userResponse}}
23
+
24
+ Schema:
25
+ {{interaction}}
@@ -1,6 +1,5 @@
1
1
  ---
2
2
  model: high
3
- output: result
4
3
  format: json
5
4
  ---
6
5
 
@@ -0,0 +1,45 @@
1
+ ---
2
+ model: fast
3
+ format: json
4
+ ---
5
+
6
+ You generate executable sanity checks for the implemented task.
7
+
8
+ Input:
9
+ - task: { title, description, doneDefinition, sanityCheck }
10
+ - implementation: code-writer output
11
+ - testPlan: test-planner output
12
+
13
+ Return JSON only in this shape:
14
+ {
15
+ "checks": [
16
+ {
17
+ "id": 1,
18
+ "description": "What this verifies",
19
+ "type": "shell" | "file_exists" | "file_contains" | "test_suite",
20
+ "command": "shell command if type=shell/test_suite",
21
+ "expected": "expected output (optional)",
22
+ "comparison": "equals" | "contains" | "not_empty",
23
+ "path": "file path for file checks",
24
+ "pattern": "string or regex source for file_contains"
25
+ }
26
+ ],
27
+ "setup": "optional setup command",
28
+ "teardown": "optional teardown command"
29
+ }
30
+
31
+ Guidelines:
32
+ - Use actual file paths and commands implied by the implementation.
33
+ - Prefer simple, local commands (curl, node, npm, cat, rg).
34
+ - If the task describes a server endpoint, include a curl check.
35
+ - Keep checks short, clear, and runnable.
36
+ - Include at least one file_exists or file_contains check when files are created/modified.
37
+
38
+ Task:
39
+ {{task}}
40
+
41
+ Implementation:
42
+ {{implementation}}
43
+
44
+ Test Plan:
45
+ {{testPlan}}