@compilr-dev/sdk 0.10.10 → 0.10.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.
@@ -53,11 +53,17 @@ export const CAPABILITY_PACKS = {
53
53
  interaction: {
54
54
  id: 'interaction',
55
55
  label: 'User Interaction',
56
- tools: ['ask_user', 'ask_user_simple', 'propose_alternatives', 'suggest'],
56
+ tools: [
57
+ 'ask_user',
58
+ 'ask_user_simple',
59
+ 'propose_alternatives',
60
+ 'suggest',
61
+ 'build_interactive_flow',
62
+ ],
57
63
  readOnly: true,
58
64
  promptModules: [],
59
65
  estimatedPromptTokens: 0,
60
- estimatedToolTokens: 900,
66
+ estimatedToolTokens: 1100,
61
67
  },
62
68
  coordinator: {
63
69
  id: 'coordinator',
@@ -148,13 +148,14 @@ For complex features:
148
148
 
149
149
  - **ask_user_simple**: For single-choice questions (preferred for most interactions)
150
150
  - **ask_user**: For multi-question batches (when gathering related info)
151
+ - **build_interactive_flow**: For decisions with 2+ branching considerations the user benefits from exploring visually (navigable tree with Back/forward; agent receives the full reasoning path back)
151
152
  - **backlog_read**: Use id param to get specific item, use search/filters otherwise
152
153
  - **backlog_write**: Update items after refinement decisions
153
154
  - **todo_write**: Track your progress through refinement
154
155
 
155
156
  ## RULES
156
157
 
157
- 1. ALWAYS use ask_user_simple or ask_user for decisions - NEVER ask questions in plain text
158
+ 1. ALWAYS use ask_user_simple, ask_user, or build_interactive_flow for decisions - NEVER ask questions in plain text
158
159
  2. Keep backlog reads efficient - use filters and limits
159
160
  3. One refinement focus at a time
160
161
  4. Update backlog with backlog_write after each decision
@@ -140,7 +140,7 @@ IMPORTANT: Tool names are lowercase with underscores.
140
140
  - **File operations**: read_file, write_file, edit, glob, grep
141
141
  - **Shell**: bash (use run_in_background=true for long-running), bash_output, kill_shell
142
142
  - **Task management**: todo_write, todo_read
143
- - **User interaction**: ask_user, ask_user_simple, propose_alternatives (present 2-3 options for comparison)
143
+ - **User interaction**: ask_user_simple (single question, small models), ask_user (1-5 questions), propose_alternatives (2-3 options with pros/cons), build_interactive_flow (navigable decision tree — use when a choice has 2+ branching considerations the user benefits from exploring visually with Back/forward)
144
144
  - **Sub-agents**: task (types: explore, code-review, plan, debug, test-runner, refactor, security-audit, general)`,
145
145
  };
146
146
  /**
@@ -46,12 +46,18 @@ export const SKILL_REQUIREMENTS = {
46
46
  // Planning skills
47
47
  planning: {
48
48
  required: ['read_file', 'glob'],
49
- optional: ['ask_user', 'ask_user_simple', 'todo_write'],
49
+ optional: ['ask_user', 'ask_user_simple', 'build_interactive_flow', 'todo_write'],
50
50
  reason: 'Needs to understand codebase to plan effectively',
51
51
  },
52
52
  design: {
53
53
  required: ['ask_user', 'workitem_add'],
54
- optional: ['propose_alternatives', 'detect_project', 'document_save', 'edit'],
54
+ optional: [
55
+ 'propose_alternatives',
56
+ 'build_interactive_flow',
57
+ 'detect_project',
58
+ 'document_save',
59
+ 'edit',
60
+ ],
55
61
  reason: 'Needs to gather requirements and create backlog',
56
62
  },
57
63
  refine: {
@@ -72,7 +78,13 @@ export const SKILL_REQUIREMENTS = {
72
78
  // Documentation skills
73
79
  architecture: {
74
80
  required: ['read_file', 'write_file', 'edit'],
75
- optional: ['backlog_read', 'ask_user', 'propose_alternatives', 'glob'],
81
+ optional: [
82
+ 'backlog_read',
83
+ 'ask_user',
84
+ 'propose_alternatives',
85
+ 'build_interactive_flow',
86
+ 'glob',
87
+ ],
76
88
  reason: 'Creates architecture documents',
77
89
  },
78
90
  prd: {
@@ -22,6 +22,7 @@ const TOOL_NAMES = {
22
22
  ASK_USER: 'ask_user',
23
23
  ASK_USER_SIMPLE: 'ask_user_simple',
24
24
  PROPOSE_ALTERNATIVES: 'propose_alternatives',
25
+ BUILD_INTERACTIVE_FLOW: 'build_interactive_flow',
25
26
  // Delegation
26
27
  DELEGATE: 'delegate',
27
28
  DELEGATE_BACKGROUND: 'delegate_background',
@@ -115,7 +115,7 @@ You are the **Project Manager (PM)** in this multi-agent development team. You s
115
115
 
116
116
  **Direct tools** (call by name):
117
117
  - \`todo_write\`, \`todo_read\` - Task tracking
118
- - \`ask_user\`, \`suggest\` - User interaction
118
+ - \`ask_user_simple\`, \`ask_user\`, \`propose_alternatives\`, \`build_interactive_flow\`, \`suggest\` - User interaction
119
119
  - \`handoff\` - Hand off to another specialist
120
120
 
121
121
  **Specialized tools** (call via \`use_tool\`):
@@ -601,7 +601,7 @@ You are the **Business Analyst** in this multi-agent development team. You trans
601
601
 
602
602
  **Direct tools** (call by name):
603
603
  - \`todo_write\`, \`todo_read\` - Task tracking
604
- - \`ask_user\`, \`suggest\` - User interaction
604
+ - \`ask_user_simple\`, \`ask_user\`, \`propose_alternatives\`, \`build_interactive_flow\`, \`suggest\` - User interaction
605
605
  - \`handoff\` - Hand off to another specialist
606
606
 
607
607
  **Specialized tools** (call via \`use_tool\`):
@@ -38,6 +38,16 @@ const SUMMARY_RENDERERS = ['compact', 'detailed'];
38
38
  * slug doesn't map to a component.
39
39
  */
40
40
  const ICON_NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
41
+ /* eslint-disable @typescript-eslint/no-unnecessary-condition, @typescript-eslint/no-unnecessary-type-conversion --
42
+ * The entire validator section runs DEFENSIVE runtime checks against agent
43
+ * JSON arriving over IPC. TypeScript types are lies at this boundary — an
44
+ * agent's malformed input can have any field missing or wrong-typed, and we
45
+ * must return structured errors rather than throw. The "always-falsy" and
46
+ * "redundant String()" warnings would be true if we only saw type-safe
47
+ * callers, but our caller is `JSON.parse(IPC payload)` cast to Flow. The
48
+ * pragmatic alternative (passing `unknown` through every helper signature)
49
+ * would be a much larger refactor for no behavioral gain.
50
+ */
41
51
  /**
42
52
  * Validate a flow. Accepts `unknown` because inputs flow in as JSON from the
43
53
  * agent and TS types are lies at the boundary — runtime defensive checks
@@ -75,10 +85,18 @@ export function validateFlow(flowInput) {
75
85
  // 2. Each node's id must match the key it lives under
76
86
  for (const id of nodeIds) {
77
87
  const node = flow.nodes[id];
88
+ if (!node || typeof node !== 'object') {
89
+ errors.push({
90
+ code: 'INVALID_NODE_TYPE',
91
+ message: `flow.nodes['${id}'] is null or not an object`,
92
+ nodeId: id,
93
+ });
94
+ continue;
95
+ }
78
96
  if (node.id !== id) {
79
97
  errors.push({
80
98
  code: 'DUPLICATE_NODE_ID',
81
- message: `Node key '${id}' does not match node.id '${node.id}'`,
99
+ message: `Node key '${id}' does not match node.id '${String(node.id)}'`,
82
100
  nodeId: id,
83
101
  });
84
102
  }
@@ -86,6 +104,8 @@ export function validateFlow(flowInput) {
86
104
  // 3. Per-node validation: type, references, icons, renderers, conditions
87
105
  for (const id of nodeIds) {
88
106
  const node = flow.nodes[id];
107
+ if (!node || typeof node !== 'object')
108
+ continue; // already flagged above
89
109
  validateNode(node, flow.nodes, errors);
90
110
  }
91
111
  // 4. Reachability — warn on orphans
@@ -125,6 +145,16 @@ function validateNode(node, allNodes, errors) {
125
145
  }
126
146
  }
127
147
  function validateQuestion(node, allNodes, errors) {
148
+ // Defensive: agent JSON may omit required nested fields. Surface as
149
+ // structured errors rather than letting the validator throw.
150
+ if (!node.input || typeof node.input !== 'object') {
151
+ errors.push({
152
+ code: 'INVALID_NODE_TYPE',
153
+ message: `Question node '${node.id}' is missing the required 'input' object (must contain 'mode' and choices/options/etc.)`,
154
+ nodeId: node.id,
155
+ });
156
+ return;
157
+ }
128
158
  // Input mode must have a valid renderer if specified. The lookup CAN miss
129
159
  // at runtime if the agent passed an unknown mode in the JSON — TS narrowing
130
160
  // doesn't help us across the boundary, hence the explicit widened type.
@@ -132,7 +162,7 @@ function validateQuestion(node, allNodes, errors) {
132
162
  if (!validRenderers) {
133
163
  errors.push({
134
164
  code: 'INVALID_NODE_TYPE',
135
- message: `Unknown question input mode '${node.input.mode}'`,
165
+ message: `Unknown question input mode '${String(node.input.mode)}' for node '${node.id}' (expected: single, multi, text, or proposal)`,
136
166
  nodeId: node.id,
137
167
  });
138
168
  return;
@@ -144,23 +174,67 @@ function validateQuestion(node, allNodes, errors) {
144
174
  nodeId: node.id,
145
175
  });
146
176
  }
147
- // Validate icons on choices/proposals
177
+ // Validate choices/options arrays are present and well-formed
148
178
  if (node.input.mode === 'single' || node.input.mode === 'multi') {
149
- for (const choice of node.input.choices) {
150
- if (choice.icon !== undefined)
151
- checkIcon(choice.icon, node.id, errors);
179
+ if (!Array.isArray(node.input.choices)) {
180
+ errors.push({
181
+ code: 'INVALID_NODE_TYPE',
182
+ message: `Question '${node.id}' with mode '${node.input.mode}' requires a 'choices' array`,
183
+ nodeId: node.id,
184
+ });
185
+ }
186
+ else {
187
+ for (const choice of node.input.choices) {
188
+ if (!choice || typeof choice !== 'object')
189
+ continue;
190
+ if (choice.icon !== undefined)
191
+ checkIcon(choice.icon, node.id, errors);
192
+ }
152
193
  }
153
194
  }
154
195
  else if (node.input.mode === 'proposal') {
155
- for (const opt of node.input.options) {
156
- if (opt.icon !== undefined)
157
- checkIcon(opt.icon, node.id, errors);
196
+ if (!Array.isArray(node.input.options)) {
197
+ errors.push({
198
+ code: 'INVALID_NODE_TYPE',
199
+ message: `Question '${node.id}' with mode 'proposal' requires an 'options' array`,
200
+ nodeId: node.id,
201
+ });
202
+ }
203
+ else {
204
+ for (const opt of node.input.options) {
205
+ if (!opt || typeof opt !== 'object')
206
+ continue;
207
+ if (opt.icon !== undefined)
208
+ checkIcon(opt.icon, node.id, errors);
209
+ }
158
210
  }
159
211
  }
160
- // Validate next references
212
+ // Validate next references (defensive — `next` might be undefined)
213
+ if (node.next === undefined || node.next === null) {
214
+ errors.push({
215
+ code: 'UNRESOLVED_NEXT',
216
+ message: `Question node '${node.id}' is missing the required 'next' field (string nodeId or { byAnswer: ... })`,
217
+ nodeId: node.id,
218
+ });
219
+ return;
220
+ }
161
221
  validateNextRef(node.next, node, allNodes, errors);
162
222
  }
163
223
  function validateInfo(node, allNodes, errors) {
224
+ if (typeof node.title !== 'string' || node.title.length === 0) {
225
+ errors.push({
226
+ code: 'INVALID_NODE_TYPE',
227
+ message: `Info node '${node.id}' is missing required 'title' string`,
228
+ nodeId: node.id,
229
+ });
230
+ }
231
+ if (typeof node.body !== 'string') {
232
+ errors.push({
233
+ code: 'INVALID_NODE_TYPE',
234
+ message: `Info node '${node.id}' is missing required 'body' string (markdown content)`,
235
+ nodeId: node.id,
236
+ });
237
+ }
164
238
  if (node.render && !INFO_RENDERERS.includes(node.render)) {
165
239
  errors.push({
166
240
  code: 'INVALID_RENDERER',
@@ -168,31 +242,56 @@ function validateInfo(node, allNodes, errors) {
168
242
  nodeId: node.id,
169
243
  });
170
244
  }
245
+ if (node.next === undefined || node.next === null) {
246
+ errors.push({
247
+ code: 'UNRESOLVED_NEXT',
248
+ message: `Info node '${node.id}' is missing the required 'next' field`,
249
+ nodeId: node.id,
250
+ });
251
+ return;
252
+ }
171
253
  validateNextRef(node.next, node, allNodes, errors);
172
254
  }
173
255
  function validateBranch(node, allNodes, errors) {
174
- // All routes must reference existing nodes and use valid conditions
175
- for (let i = 0; i < node.routes.length; i++) {
176
- const route = node.routes[i];
177
- if (!isValidCondition(route.when)) {
178
- errors.push({
179
- code: 'INVALID_CONDITION',
180
- message: `Branch route ${String(i)} has an invalid condition shape`,
181
- nodeId: node.id,
182
- });
183
- }
184
- if (!(route.goto in allNodes)) {
185
- errors.push({
186
- code: 'UNRESOLVED_BRANCH',
187
- message: `Branch route ${String(i)} goto '${route.goto}' is not defined in flow.nodes`,
188
- nodeId: node.id,
189
- });
256
+ // Defensive: routes might be missing or non-array
257
+ if (!Array.isArray(node.routes)) {
258
+ errors.push({
259
+ code: 'INVALID_NODE_TYPE',
260
+ message: `Branch node '${node.id}' requires a 'routes' array (each item: { when: ..., goto: nodeId })`,
261
+ nodeId: node.id,
262
+ });
263
+ }
264
+ else {
265
+ for (let i = 0; i < node.routes.length; i++) {
266
+ const route = node.routes[i];
267
+ if (!route || typeof route !== 'object') {
268
+ errors.push({
269
+ code: 'INVALID_CONDITION',
270
+ message: `Branch route ${String(i)} on node '${node.id}' is null or not an object`,
271
+ nodeId: node.id,
272
+ });
273
+ continue;
274
+ }
275
+ if (!isValidCondition(route.when)) {
276
+ errors.push({
277
+ code: 'INVALID_CONDITION',
278
+ message: `Branch route ${String(i)} has an invalid condition shape`,
279
+ nodeId: node.id,
280
+ });
281
+ }
282
+ if (typeof route.goto !== 'string' || !(route.goto in allNodes)) {
283
+ errors.push({
284
+ code: 'UNRESOLVED_BRANCH',
285
+ message: `Branch route ${String(i)} goto '${String(route.goto)}' is not defined in flow.nodes`,
286
+ nodeId: node.id,
287
+ });
288
+ }
190
289
  }
191
290
  }
192
- if (!node.default || !(node.default in allNodes)) {
291
+ if (!node.default || typeof node.default !== 'string' || !(node.default in allNodes)) {
193
292
  errors.push({
194
293
  code: 'UNRESOLVED_BRANCH',
195
- message: `Branch default '${node.default}' is not defined in flow.nodes`,
294
+ message: `Branch default '${String(node.default)}' is not defined in flow.nodes`,
196
295
  nodeId: node.id,
197
296
  });
198
297
  }
@@ -218,12 +317,20 @@ function validateNextRef(next, node, allNodes, errors) {
218
317
  }
219
318
  return;
220
319
  }
221
- // byAnswer object
320
+ // byAnswer object — defensive on shape
321
+ if (!next || typeof next !== 'object' || !next.byAnswer || typeof next.byAnswer !== 'object') {
322
+ errors.push({
323
+ code: 'UNRESOLVED_NEXT',
324
+ message: `Node '${node.id}' next is not a valid NextRef (expected string nodeId or { byAnswer: { ... } })`,
325
+ nodeId: node.id,
326
+ });
327
+ return;
328
+ }
222
329
  for (const [answer, target] of Object.entries(next.byAnswer)) {
223
- if (!(target in allNodes)) {
330
+ if (typeof target !== 'string' || !(target in allNodes)) {
224
331
  errors.push({
225
332
  code: 'UNRESOLVED_NEXT',
226
- message: `Node '${node.id}' next.byAnswer['${answer}'] references '${target}' which is not defined in flow.nodes`,
333
+ message: `Node '${node.id}' next.byAnswer['${answer}'] references '${String(target)}' which is not defined in flow.nodes`,
227
334
  nodeId: node.id,
228
335
  });
229
336
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compilr-dev/sdk",
3
- "version": "0.10.10",
3
+ "version": "0.10.12",
4
4
  "description": "Universal agent runtime for building AI-powered applications",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",