prompt-language-shell 0.8.8 → 0.9.2

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 (45) hide show
  1. package/README.md +0 -1
  2. package/dist/configuration/io.js +22 -1
  3. package/dist/{services/config-labels.js → configuration/labels.js} +1 -1
  4. package/dist/configuration/schema.js +2 -2
  5. package/dist/configuration/steps.js +171 -0
  6. package/dist/configuration/transformation.js +17 -0
  7. package/dist/configuration/types.js +3 -4
  8. package/dist/execution/handlers.js +20 -35
  9. package/dist/execution/hooks.js +291 -0
  10. package/dist/execution/processing.js +15 -2
  11. package/dist/execution/reducer.js +30 -48
  12. package/dist/execution/runner.js +81 -0
  13. package/dist/execution/types.js +1 -0
  14. package/dist/execution/utils.js +28 -0
  15. package/dist/services/components.js +109 -395
  16. package/dist/services/filesystem.js +21 -1
  17. package/dist/services/logger.js +3 -3
  18. package/dist/services/messages.js +10 -16
  19. package/dist/services/process.js +7 -2
  20. package/dist/services/refinement.js +5 -2
  21. package/dist/services/router.js +120 -67
  22. package/dist/services/shell.js +179 -10
  23. package/dist/services/skills.js +2 -1
  24. package/dist/skills/answer.md +14 -12
  25. package/dist/skills/execute.md +98 -39
  26. package/dist/skills/introspect.md +9 -9
  27. package/dist/skills/schedule.md +0 -6
  28. package/dist/types/errors.js +47 -0
  29. package/dist/types/result.js +40 -0
  30. package/dist/ui/Command.js +11 -7
  31. package/dist/ui/Component.js +6 -3
  32. package/dist/ui/Config.js +9 -3
  33. package/dist/ui/Execute.js +249 -163
  34. package/dist/ui/Introspect.js +13 -14
  35. package/dist/ui/List.js +2 -2
  36. package/dist/ui/Main.js +14 -7
  37. package/dist/ui/Output.js +54 -0
  38. package/dist/ui/Schedule.js +3 -1
  39. package/dist/ui/Subtask.js +6 -3
  40. package/dist/ui/Task.js +10 -85
  41. package/dist/ui/Validate.js +26 -21
  42. package/dist/ui/Workflow.js +21 -4
  43. package/package.json +1 -1
  44. package/dist/parser.js +0 -13
  45. package/dist/services/config-utils.js +0 -20
@@ -2,10 +2,11 @@ import { asScheduledTasks } from '../types/guards.js';
2
2
  import { FeedbackType, TaskType } from '../types/types.js';
3
3
  import { saveConfig } from '../configuration/io.js';
4
4
  import { getConfigSchema } from '../configuration/schema.js';
5
+ import { createConfigStepsFromSchema } from '../configuration/steps.js';
5
6
  import { unflattenConfig } from '../configuration/transformation.js';
6
- import { saveConfigLabels } from './config-labels.js';
7
- import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createScheduleDefinition, createValidateDefinition, } from './components.js';
8
- import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
7
+ import { saveConfigLabels } from '../configuration/labels.js';
8
+ import { createAnswer, createConfig, createConfirm, createExecute, createFeedback, createIntrospect, createMessage, createSchedule, createValidate, } from './components.js';
9
+ import { getCancellationMessage, getConfirmationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
9
10
  import { validateExecuteTasks } from './validator.js';
10
11
  /**
11
12
  * Determine the operation name based on task types
@@ -30,15 +31,22 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
30
31
  const validTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
31
32
  // Check if no valid tasks remain after filtering
32
33
  if (validTasks.length === 0) {
33
- const message = createMessage(getUnknownRequestMessage());
34
- workflowHandlers.addToQueue(message);
34
+ const msg = createMessage({ text: getUnknownRequestMessage() });
35
+ workflowHandlers.addToQueue(msg);
35
36
  return;
36
37
  }
37
38
  const operation = getOperationName(validTasks);
39
+ // Create routing context for downstream functions
40
+ const context = {
41
+ service,
42
+ userRequest,
43
+ workflowHandlers,
44
+ requestHandlers: requestHandlers,
45
+ };
38
46
  if (hasDefineTask) {
39
47
  // Has DEFINE tasks - add Schedule to queue for user selection
40
48
  // Refinement flow will call this function again with refined tasks
41
- const scheduleDefinition = createScheduleDefinition(message, validTasks);
49
+ const scheduleDefinition = createSchedule({ message, tasks: validTasks });
42
50
  workflowHandlers.addToQueue(scheduleDefinition);
43
51
  }
44
52
  else {
@@ -46,19 +54,27 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
46
54
  // When Schedule activates, Command moves to timeline
47
55
  // When Schedule completes, it moves to pending
48
56
  // When Confirm activates, Schedule stays pending (visible for context)
49
- const scheduleDefinition = createScheduleDefinition(message, validTasks, () => {
50
- // Schedule completed - add Confirm to queue
51
- const confirmDefinition = createConfirmDefinition(() => {
52
- // User confirmed - complete both Confirm and Schedule, then route to appropriate component
53
- lifecycleHandlers.completeActiveAndPending();
54
- executeTasksAfterConfirm(validTasks, service, userRequest, workflowHandlers, requestHandlers);
55
- }, () => {
56
- // User cancelled - complete both Confirm and Schedule, then show cancellation
57
- lifecycleHandlers.completeActiveAndPending();
58
- const message = getCancellationMessage(operation);
59
- workflowHandlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
60
- });
61
- workflowHandlers.addToQueue(confirmDefinition);
57
+ const scheduleDefinition = createSchedule({
58
+ message,
59
+ tasks: validTasks,
60
+ onSelectionConfirmed: () => {
61
+ // Schedule completed - add Confirm to queue
62
+ const confirmDefinition = createConfirm({
63
+ message: getConfirmationMessage(),
64
+ onConfirmed: () => {
65
+ // User confirmed - complete both Confirm and Schedule, then route
66
+ lifecycleHandlers.completeActiveAndPending();
67
+ executeTasksAfterConfirm(validTasks, context);
68
+ },
69
+ onCancelled: () => {
70
+ // User cancelled - complete both Confirm and Schedule, then show cancellation
71
+ lifecycleHandlers.completeActiveAndPending();
72
+ const message = getCancellationMessage(operation);
73
+ workflowHandlers.addToQueue(createFeedback({ type: FeedbackType.Aborted, message }));
74
+ },
75
+ });
76
+ workflowHandlers.addToQueue(confirmDefinition);
77
+ },
62
78
  });
63
79
  workflowHandlers.addToQueue(scheduleDefinition);
64
80
  }
@@ -91,7 +107,8 @@ function validateTaskTypes(tasks) {
91
107
  * Validates task types and routes each type appropriately
92
108
  * Supports mixed types at top level with Groups
93
109
  */
94
- function executeTasksAfterConfirm(tasks, service, userRequest, workflowHandlers, requestHandlers) {
110
+ function executeTasksAfterConfirm(tasks, context) {
111
+ const { service, userRequest, workflowHandlers, requestHandlers } = context;
95
112
  // Validate task types (Groups must have uniform subtasks)
96
113
  try {
97
114
  validateTaskTypes(tasks);
@@ -126,18 +143,28 @@ function executeTasksAfterConfirm(tasks, service, userRequest, workflowHandlers,
126
143
  .join('\n');
127
144
  return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
128
145
  });
129
- workflowHandlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
146
+ workflowHandlers.addToQueue(createFeedback({
147
+ type: FeedbackType.Failed,
148
+ message: errorMessages.join('\n\n'),
149
+ }));
130
150
  return;
131
151
  }
132
152
  else if (validation.missingConfig.length > 0) {
133
153
  // Missing config detected - create ONE Validate component for ALL missing config
134
- workflowHandlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
135
- requestHandlers.onError(error);
136
- }, () => {
137
- // After config is complete, resume task routing
138
- routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHandlers, requestHandlers);
139
- }, (operation) => {
140
- requestHandlers.onAborted(operation);
154
+ workflowHandlers.addToQueue(createValidate({
155
+ missingConfig: validation.missingConfig,
156
+ userRequest,
157
+ service,
158
+ onError: (error) => {
159
+ requestHandlers.onError(error);
160
+ },
161
+ onValidationComplete: () => {
162
+ // After config is complete, resume task routing
163
+ routeTasksAfterConfig(scheduledTasks, context);
164
+ },
165
+ onAborted: (operation) => {
166
+ requestHandlers.onAborted(operation);
167
+ },
141
168
  }));
142
169
  return;
143
170
  }
@@ -148,13 +175,13 @@ function executeTasksAfterConfirm(tasks, service, userRequest, workflowHandlers,
148
175
  }
149
176
  }
150
177
  // No missing config - proceed with normal routing
151
- routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHandlers, requestHandlers);
178
+ routeTasksAfterConfig(scheduledTasks, context);
152
179
  }
153
180
  /**
154
181
  * Route tasks after config is complete (or when no config is needed)
155
182
  * Processes tasks in order, grouping by type
156
183
  */
157
- function routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHandlers, requestHandlers) {
184
+ function routeTasksAfterConfig(scheduledTasks, context) {
158
185
  // Process tasks in order, preserving Group boundaries
159
186
  // Track consecutive standalone tasks to group them by type
160
187
  let consecutiveStandaloneTasks = [];
@@ -174,7 +201,7 @@ function routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHan
174
201
  const taskType = type;
175
202
  if (typeTasks.length === 0)
176
203
  continue;
177
- routeTasksByType(taskType, typeTasks, service, userRequest, workflowHandlers, requestHandlers);
204
+ routeTasksByType(taskType, typeTasks, context);
178
205
  }
179
206
  consecutiveStandaloneTasks = [];
180
207
  };
@@ -187,7 +214,7 @@ function routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHan
187
214
  if (task.subtasks.length > 0) {
188
215
  const subtasks = task.subtasks;
189
216
  const taskType = subtasks[0].type;
190
- routeTasksByType(taskType, subtasks, service, userRequest, workflowHandlers, requestHandlers);
217
+ routeTasksByType(taskType, subtasks, context);
191
218
  }
192
219
  }
193
220
  else {
@@ -199,38 +226,42 @@ function routeTasksAfterConfig(scheduledTasks, service, userRequest, workflowHan
199
226
  processStandaloneTasks();
200
227
  }
201
228
  /**
202
- * Route tasks by type to appropriate components
203
- * Extracted to allow reuse for both Groups and standalone tasks
229
+ * Route Answer tasks - creates separate Answer component for each question
230
+ */
231
+ function routeAnswerTasks(tasks, context) {
232
+ for (const task of tasks) {
233
+ context.workflowHandlers.addToQueue(createAnswer({ question: task.action, service: context.service }));
234
+ }
235
+ }
236
+ /**
237
+ * Route Introspect tasks - creates single Introspect component for all tasks
238
+ */
239
+ function routeIntrospectTasks(tasks, context) {
240
+ context.workflowHandlers.addToQueue(createIntrospect({ tasks, service: context.service }));
241
+ }
242
+ /**
243
+ * Route Config tasks - extracts keys, caches labels, creates Config component
204
244
  */
205
- function routeTasksByType(taskType, typeTasks, service, userRequest, workflowHandlers, requestHandlers) {
206
- if (taskType === TaskType.Answer) {
207
- // Create separate Answer component for each question
208
- for (const task of typeTasks) {
209
- workflowHandlers.addToQueue(createAnswerDefinition(task.action, service));
245
+ function routeConfigTasks(tasks, context) {
246
+ const configKeys = tasks
247
+ .map((task) => task.params?.key)
248
+ .filter((key) => key !== undefined);
249
+ // Extract and cache labels from task descriptions
250
+ // Only cache labels for dynamically discovered keys (not in schema)
251
+ const schema = getConfigSchema();
252
+ const labels = {};
253
+ for (const task of tasks) {
254
+ const key = task.params?.key;
255
+ if (key && task.action && !(key in schema)) {
256
+ labels[key] = task.action;
210
257
  }
211
258
  }
212
- else if (taskType === TaskType.Introspect) {
213
- workflowHandlers.addToQueue(createIntrospectDefinition(typeTasks, service));
259
+ if (Object.keys(labels).length > 0) {
260
+ saveConfigLabels(labels);
214
261
  }
215
- else if (taskType === TaskType.Config) {
216
- // Route to Config flow - extract keys and descriptions from task params
217
- const configKeys = typeTasks
218
- .map((task) => task.params?.key)
219
- .filter((key) => key !== undefined);
220
- // Extract and cache labels from task descriptions
221
- // Only cache labels for dynamically discovered keys (not in schema)
222
- const schema = getConfigSchema();
223
- const labels = {};
224
- for (const task of typeTasks) {
225
- const key = task.params?.key;
226
- if (key && task.action && !(key in schema)) {
227
- labels[key] = task.action;
228
- }
229
- }
230
- if (Object.keys(labels).length > 0) {
231
- saveConfigLabels(labels);
232
- }
233
- workflowHandlers.addToQueue(createConfigDefinitionWithKeys(configKeys, (config) => {
262
+ context.workflowHandlers.addToQueue(createConfig({
263
+ steps: createConfigStepsFromSchema(configKeys),
264
+ onFinished: (config) => {
234
265
  // Save config - Config component will handle completion and feedback
235
266
  try {
236
267
  // Convert flat dotted keys to nested structure grouped by section
@@ -246,12 +277,34 @@ function routeTasksByType(taskType, typeTasks, service, userRequest, workflowHan
246
277
  : 'Failed to save configuration';
247
278
  throw new Error(errorMessage);
248
279
  }
249
- }, (operation) => {
250
- requestHandlers.onAborted(operation);
251
- }));
252
- }
253
- else if (taskType === TaskType.Execute) {
254
- // Execute tasks (validation already happened upfront in executeTasksAfterConfirm)
255
- workflowHandlers.addToQueue(createExecuteDefinition(typeTasks, service));
280
+ },
281
+ onAborted: (operation) => {
282
+ context.requestHandlers.onAborted(operation);
283
+ },
284
+ }));
285
+ }
286
+ /**
287
+ * Route Execute tasks - creates Execute component (validation already done)
288
+ */
289
+ function routeExecuteTasks(tasks, context) {
290
+ context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service }));
291
+ }
292
+ /**
293
+ * Registry mapping task types to their route handlers
294
+ */
295
+ const taskRouteHandlers = {
296
+ [TaskType.Answer]: routeAnswerTasks,
297
+ [TaskType.Introspect]: routeIntrospectTasks,
298
+ [TaskType.Config]: routeConfigTasks,
299
+ [TaskType.Execute]: routeExecuteTasks,
300
+ };
301
+ /**
302
+ * Route tasks by type to appropriate components
303
+ * Uses registry pattern for extensibility
304
+ */
305
+ function routeTasksByType(taskType, tasks, context) {
306
+ const handler = taskRouteHandlers[taskType];
307
+ if (handler) {
308
+ handler(tasks, context);
256
309
  }
257
310
  }
@@ -1,3 +1,4 @@
1
+ import { spawn } from 'child_process';
1
2
  export var ExecutionStatus;
2
3
  (function (ExecutionStatus) {
3
4
  ExecutionStatus["Pending"] = "pending";
@@ -58,18 +59,186 @@ export class DummyExecutor {
58
59
  });
59
60
  }
60
61
  }
62
+ // Marker for extracting pwd from command output
63
+ const PWD_MARKER = '__PWD_MARKER_7x9k2m__';
61
64
  /**
62
- * Default executor uses DummyExecutor for development and testing.
63
- * To implement real shell execution, create a RealExecutor class that:
64
- * - Spawns process with cmd.command in shell mode using child_process.spawn()
65
- * - Sets working directory from cmd.workdir
66
- * - Handles cmd.timeout for command timeout
67
- * - Captures stdout and stderr streams
68
- * - Calls onProgress with Running/Success/Failed status
69
- * - Returns CommandOutput with actual stdout, stderr, exitCode
70
- * - Handles errors (spawn failures, timeouts, non-zero exit codes)
65
+ * Parse stdout to extract workdir and clean output.
66
+ * Returns the cleaned output and the extracted workdir.
71
67
  */
72
- const executor = new DummyExecutor();
68
+ function parseWorkdir(rawOutput) {
69
+ const markerIndex = rawOutput.lastIndexOf(PWD_MARKER);
70
+ if (markerIndex === -1) {
71
+ return { output: rawOutput };
72
+ }
73
+ const output = rawOutput.slice(0, markerIndex).trimEnd();
74
+ const pwdPart = rawOutput.slice(markerIndex + PWD_MARKER.length).trim();
75
+ const lines = pwdPart.split('\n').filter((l) => l.trim());
76
+ const workdir = lines[0];
77
+ return { output, workdir };
78
+ }
79
+ /**
80
+ * Manages streaming output while filtering out the PWD marker.
81
+ * Buffers output to avoid emitting partial markers to the callback.
82
+ */
83
+ class OutputStreamer {
84
+ chunks = [];
85
+ emittedLength = 0;
86
+ callback;
87
+ constructor(callback) {
88
+ this.callback = callback;
89
+ }
90
+ /**
91
+ * Add new stdout data and emit safe content to callback.
92
+ * Buffers data to avoid emitting partial PWD markers.
93
+ */
94
+ pushStdout(data) {
95
+ this.chunks.push(data);
96
+ if (!this.callback)
97
+ return;
98
+ const accumulated = this.chunks.join('');
99
+ const markerIndex = accumulated.indexOf(PWD_MARKER);
100
+ if (markerIndex !== -1) {
101
+ // Marker found - emit everything before it (trimmed)
102
+ this.emitUpTo(accumulated.slice(0, markerIndex).trimEnd().length);
103
+ }
104
+ else {
105
+ // No marker yet - emit all but buffer for potential partial marker
106
+ const bufferSize = PWD_MARKER.length + 5;
107
+ const safeLength = Math.max(this.emittedLength, accumulated.length - bufferSize);
108
+ this.emitUpTo(safeLength);
109
+ }
110
+ }
111
+ /**
112
+ * Emit content up to the specified length if there's new content.
113
+ */
114
+ emitUpTo(length) {
115
+ if (length > this.emittedLength && this.callback) {
116
+ const accumulated = this.chunks.join('');
117
+ const newContent = accumulated.slice(this.emittedLength, length);
118
+ this.callback(newContent, 'stdout');
119
+ this.emittedLength = length;
120
+ }
121
+ }
122
+ /**
123
+ * Get the accumulated raw output.
124
+ */
125
+ getAccumulated() {
126
+ return this.chunks.join('');
127
+ }
128
+ }
129
+ /**
130
+ * Real executor that spawns shell processes and captures output.
131
+ */
132
+ export class RealExecutor {
133
+ outputCallback;
134
+ constructor(outputCallback) {
135
+ this.outputCallback = outputCallback;
136
+ }
137
+ /**
138
+ * Set or update the output callback
139
+ */
140
+ setOutputCallback(callback) {
141
+ this.outputCallback = callback;
142
+ }
143
+ execute(cmd, onProgress, _ = 0) {
144
+ return new Promise((resolve) => {
145
+ onProgress?.(ExecutionStatus.Running);
146
+ const stderr = [];
147
+ // Wrap command to capture final working directory
148
+ const wrappedCommand = `${cmd.command}; __exit=$?; echo ""; echo "${PWD_MARKER}"; pwd; exit $__exit`;
149
+ // Wrap spawn in try/catch to handle synchronous errors
150
+ let child;
151
+ try {
152
+ child = spawn(wrappedCommand, {
153
+ shell: true,
154
+ cwd: cmd.workdir || process.cwd(),
155
+ });
156
+ }
157
+ catch (error) {
158
+ const errorMessage = error instanceof Error ? error.message : 'Failed to spawn process';
159
+ const commandResult = {
160
+ description: cmd.description,
161
+ command: cmd.command,
162
+ output: '',
163
+ errors: errorMessage,
164
+ result: ExecutionResult.Error,
165
+ error: errorMessage,
166
+ };
167
+ onProgress?.(ExecutionStatus.Failed);
168
+ resolve(commandResult);
169
+ return;
170
+ }
171
+ // Handle timeout if specified
172
+ const SIGKILL_GRACE_PERIOD = 3000;
173
+ let timeoutId;
174
+ let killTimeoutId;
175
+ if (cmd.timeout && cmd.timeout > 0) {
176
+ timeoutId = setTimeout(() => {
177
+ child.kill('SIGTERM');
178
+ // Escalate to SIGKILL if process doesn't terminate
179
+ killTimeoutId = setTimeout(() => {
180
+ child.kill('SIGKILL');
181
+ }, SIGKILL_GRACE_PERIOD);
182
+ }, cmd.timeout);
183
+ }
184
+ // Use OutputStreamer for buffered stdout streaming
185
+ const stdoutStreamer = new OutputStreamer(this.outputCallback);
186
+ child.stdout.on('data', (data) => {
187
+ stdoutStreamer.pushStdout(data.toString());
188
+ });
189
+ child.stderr.on('data', (data) => {
190
+ const text = data.toString();
191
+ stderr.push(text);
192
+ this.outputCallback?.(text, 'stderr');
193
+ });
194
+ child.on('error', (error) => {
195
+ if (timeoutId)
196
+ clearTimeout(timeoutId);
197
+ if (killTimeoutId)
198
+ clearTimeout(killTimeoutId);
199
+ const commandResult = {
200
+ description: cmd.description,
201
+ command: cmd.command,
202
+ output: stdoutStreamer.getAccumulated(),
203
+ errors: error.message,
204
+ result: ExecutionResult.Error,
205
+ error: error.message,
206
+ };
207
+ onProgress?.(ExecutionStatus.Failed);
208
+ resolve(commandResult);
209
+ });
210
+ child.on('close', (code) => {
211
+ if (timeoutId)
212
+ clearTimeout(timeoutId);
213
+ if (killTimeoutId)
214
+ clearTimeout(killTimeoutId);
215
+ const success = code === 0;
216
+ const { output, workdir } = parseWorkdir(stdoutStreamer.getAccumulated());
217
+ const commandResult = {
218
+ description: cmd.description,
219
+ command: cmd.command,
220
+ output,
221
+ errors: stderr.join(''),
222
+ result: success ? ExecutionResult.Success : ExecutionResult.Error,
223
+ error: success ? undefined : `Exit code: ${code}`,
224
+ workdir,
225
+ };
226
+ onProgress?.(success ? ExecutionStatus.Success : ExecutionStatus.Failed);
227
+ resolve(commandResult);
228
+ });
229
+ });
230
+ }
231
+ }
232
+ // Create real executor instance
233
+ const realExecutor = new RealExecutor();
234
+ // Default executor for production use
235
+ const executor = realExecutor;
236
+ /**
237
+ * Set a callback to receive command output in real-time
238
+ */
239
+ export function setOutputCallback(callback) {
240
+ realExecutor.setOutputCallback(callback);
241
+ }
73
242
  /**
74
243
  * Execute a single shell command
75
244
  */
@@ -1,5 +1,6 @@
1
1
  import { homedir } from 'os';
2
2
  import { join } from 'path';
3
+ import { AppError, ErrorCode } from '../types/errors.js';
3
4
  import { defaultFileSystem } from './filesystem.js';
4
5
  import { displayWarning } from './logger.js';
5
6
  import { getUnknownSkillMessage } from './messages.js';
@@ -189,7 +190,7 @@ export function expandSkillReferences(execution, skillLookup, visited = new Set(
189
190
  }
190
191
  // Check for circular reference
191
192
  if (visited.has(skillName)) {
192
- throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
193
+ throw new AppError(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`, ErrorCode.CircularReference);
193
194
  }
194
195
  // Second: Match against skill name
195
196
  const skill = skillLookup(skillName);
@@ -110,15 +110,17 @@ They enable cleaner, more reusable component logic.
110
110
 
111
111
  ## Common Mistakes to Avoid
112
112
 
113
- Starting with "Here's the answer:" or "Let me explain:"
114
- Exceeding 4 lines or 80 characters per line
115
- Including unnecessary details
116
- Using overly technical jargon without explanation
117
- Repeating the question in the answer
118
- Using citation tags like `<cite>` or any HTML/XML markup
119
-
120
- ✅ Direct, concise answers
121
- ✅ Proper line breaks at natural phrase boundaries
122
- Essential information only
123
- Clear, accessible language
124
- Plain text only - no markup tags
113
+ **DO NOT:**
114
+ - Start with "Here's the answer:" or "Let me explain:"
115
+ - Exceed 4 lines or 80 characters per line
116
+ - Include unnecessary details
117
+ - Use overly technical jargon without explanation
118
+ - Repeat the question in the answer
119
+ - Use citation tags like `<cite>` or any HTML/XML markup
120
+
121
+ **DO:**
122
+ - Give direct, concise answers
123
+ - Use proper line breaks at natural phrase boundaries
124
+ - Include essential information only
125
+ - Use clear, accessible language
126
+ - Use plain text only - no markup tags