prompt-language-shell 0.9.2 → 0.9.6

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 (67) hide show
  1. package/dist/{ui/Main.js → Main.js} +12 -12
  2. package/dist/{ui → components}/Component.js +28 -26
  3. package/dist/{ui → components}/Workflow.js +14 -4
  4. package/dist/{ui → components/controllers}/Answer.js +18 -17
  5. package/dist/{ui → components/controllers}/Command.js +11 -18
  6. package/dist/{ui → components/controllers}/Config.js +8 -116
  7. package/dist/components/controllers/Confirm.js +42 -0
  8. package/dist/{ui → components/controllers}/Execute.js +75 -144
  9. package/dist/{ui → components/controllers}/Introspect.js +12 -28
  10. package/dist/components/controllers/Refinement.js +18 -0
  11. package/dist/components/controllers/Schedule.js +134 -0
  12. package/dist/{ui → components/controllers}/Validate.js +14 -32
  13. package/dist/components/views/Answer.js +28 -0
  14. package/dist/components/views/Command.js +11 -0
  15. package/dist/components/views/Config.js +115 -0
  16. package/dist/components/views/Confirm.js +24 -0
  17. package/dist/components/views/Debug.js +12 -0
  18. package/dist/components/views/Execute.js +60 -0
  19. package/dist/components/views/Feedback.js +8 -0
  20. package/dist/components/views/Introspect.js +17 -0
  21. package/dist/{ui → components/views}/Label.js +3 -3
  22. package/dist/{ui → components/views}/List.js +1 -1
  23. package/dist/{ui → components/views}/Output.js +2 -2
  24. package/dist/components/views/Refinement.js +9 -0
  25. package/dist/{ui → components/views}/Report.js +1 -1
  26. package/dist/components/views/Schedule.js +121 -0
  27. package/dist/{ui → components/views}/Separator.js +1 -1
  28. package/dist/{ui → components/views}/Spinner.js +1 -1
  29. package/dist/{ui → components/views}/Subtask.js +4 -4
  30. package/dist/components/views/Table.js +15 -0
  31. package/dist/components/views/Task.js +18 -0
  32. package/dist/components/views/Upcoming.js +30 -0
  33. package/dist/{ui → components/views}/UserQuery.js +1 -1
  34. package/dist/components/views/Validate.js +17 -0
  35. package/dist/{ui → components/views}/Welcome.js +1 -1
  36. package/dist/configuration/steps.js +1 -1
  37. package/dist/execution/handlers.js +19 -53
  38. package/dist/execution/reducer.js +26 -38
  39. package/dist/execution/runner.js +43 -25
  40. package/dist/execution/types.js +3 -4
  41. package/dist/execution/utils.js +1 -1
  42. package/dist/index.js +1 -1
  43. package/dist/services/anthropic.js +27 -31
  44. package/dist/services/colors.js +2 -1
  45. package/dist/services/logger.js +126 -13
  46. package/dist/services/messages.js +19 -0
  47. package/dist/services/parser.js +13 -5
  48. package/dist/services/refinement.js +8 -2
  49. package/dist/services/router.js +184 -89
  50. package/dist/services/shell.js +26 -6
  51. package/dist/services/skills.js +35 -7
  52. package/dist/services/timing.js +1 -0
  53. package/dist/skills/execute.md +15 -7
  54. package/dist/skills/schedule.md +155 -0
  55. package/dist/tools/execute.tool.js +0 -4
  56. package/dist/tools/schedule.tool.js +1 -1
  57. package/dist/types/schemas.js +0 -1
  58. package/package.json +4 -4
  59. package/dist/execution/hooks.js +0 -291
  60. package/dist/ui/Confirm.js +0 -62
  61. package/dist/ui/Debug.js +0 -7
  62. package/dist/ui/Feedback.js +0 -19
  63. package/dist/ui/Refinement.js +0 -23
  64. package/dist/ui/Schedule.js +0 -257
  65. package/dist/ui/Task.js +0 -11
  66. /package/dist/{ui → components/views}/Message.js +0 -0
  67. /package/dist/{ui → components/views}/Panel.js +0 -0
@@ -1,12 +1,11 @@
1
1
  import YAML from 'yaml';
2
2
  import { displayWarning } from './logger.js';
3
3
  /**
4
- * Validate a skill without parsing it fully
4
+ * Validate extracted sections from a skill
5
5
  * Returns validation error if skill is invalid, null if valid
6
6
  * Note: Name section is optional - key from filename is used as fallback
7
7
  */
8
- export function validateSkillStructure(content, key) {
9
- const sections = extractSections(content);
8
+ function validateSections(sections, key) {
10
9
  // Use key for error reporting if name not present
11
10
  const skillName = sections.name || key;
12
11
  // Check required sections (Name is now optional)
@@ -37,6 +36,15 @@ export function validateSkillStructure(content, key) {
37
36
  }
38
37
  return null;
39
38
  }
39
+ /**
40
+ * Validate a skill without parsing it fully
41
+ * Returns validation error if skill is invalid, null if valid
42
+ * Note: Name section is optional - key from filename is used as fallback
43
+ */
44
+ export function validateSkillStructure(content, key) {
45
+ const sections = extractSections(content);
46
+ return validateSections(sections, key);
47
+ }
40
48
  /**
41
49
  * Convert kebab-case key to Title Case display name
42
50
  * Examples: "deploy-app" -> "Deploy App", "build-project-2" -> "Build Project 2"
@@ -64,8 +72,8 @@ export function parseSkillMarkdown(key, content) {
64
72
  const sections = extractSections(content);
65
73
  // Determine display name: prefer Name section, otherwise derive from key
66
74
  const displayName = sections.name || keyToDisplayName(key);
67
- // Validate the skill (Name is no longer required since we have key)
68
- const validationError = validateSkillStructure(content, key);
75
+ // Validate using already-extracted sections (avoids re-parsing)
76
+ const validationError = validateSections(sections, key);
69
77
  // For invalid skills, return minimal definition with error
70
78
  if (validationError) {
71
79
  return {
@@ -1,5 +1,6 @@
1
+ import { ComponentStatus, } from '../types/components.js';
1
2
  import { TaskType } from '../types/types.js';
2
- import { createRefinement } from './components.js';
3
+ import { createCommand, createRefinement } from './components.js';
3
4
  import { formatErrorMessage, getRefiningMessage } from './messages.js';
4
5
  import { routeTasksWithConfirm } from './router.js';
5
6
  /**
@@ -7,6 +8,11 @@ import { routeTasksWithConfirm } from './router.js';
7
8
  * Called when user selects options from a plan with DEFINE tasks
8
9
  */
9
10
  export async function handleRefinement(selectedTasks, service, originalCommand, lifecycleHandlers, workflowHandlers, requestHandlers) {
11
+ // Display the resolved command (from user's selection)
12
+ // The first task's action contains the full resolved command
13
+ const resolvedCommand = selectedTasks[0]?.action || originalCommand;
14
+ const commandDisplay = createCommand({ command: resolvedCommand, service, onAborted: requestHandlers.onAborted }, ComponentStatus.Done);
15
+ workflowHandlers.addToTimeline(commandDisplay);
10
16
  // Create and add refinement component to queue
11
17
  const refinementDef = createRefinement({
12
18
  text: getRefiningMessage(),
@@ -19,7 +25,7 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
19
25
  // Build refined command from selected tasks
20
26
  const refinedCommand = selectedTasks
21
27
  .map((task) => {
22
- const action = task.action.toLowerCase().replace(/,/g, ' -');
28
+ const action = task.action.replace(/,/g, ' -');
23
29
  const type = task.type;
24
30
  // For execute/group tasks, use generic hint - let LLM decide based on skill
25
31
  if (type === TaskType.Execute || type === TaskType.Group) {
@@ -5,9 +5,74 @@ import { getConfigSchema } from '../configuration/schema.js';
5
5
  import { createConfigStepsFromSchema } from '../configuration/steps.js';
6
6
  import { unflattenConfig } from '../configuration/transformation.js';
7
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';
8
+ import { createAnswer, createConfig, createConfirm, createExecute, createFeedback, createIntrospect, createSchedule, createValidate, } from './components.js';
9
+ import { getCancellationMessage, getConfirmationMessage, getUnknownRequestMessage, } from './messages.js';
10
10
  import { validateExecuteTasks } from './validator.js';
11
+ /**
12
+ * Flatten inner task structure completely - removes all nested groups.
13
+ * Used internally to flatten subtasks within a top-level group.
14
+ */
15
+ function flattenInnerTasks(tasks) {
16
+ const result = [];
17
+ for (const task of tasks) {
18
+ if (task.type === TaskType.Group &&
19
+ task.subtasks &&
20
+ task.subtasks.length > 0) {
21
+ // Recursively flatten inner group
22
+ result.push(...flattenInnerTasks(task.subtasks));
23
+ }
24
+ else if (task.type !== TaskType.Group) {
25
+ // Leaf task - add as-is
26
+ const leafTask = {
27
+ action: task.action,
28
+ type: task.type,
29
+ };
30
+ if (task.params)
31
+ leafTask.params = task.params;
32
+ if (task.config)
33
+ leafTask.config = task.config;
34
+ result.push(leafTask);
35
+ }
36
+ // Skip empty groups
37
+ }
38
+ return result;
39
+ }
40
+ /**
41
+ * Flatten hierarchical task structure, preserving top-level groups.
42
+ * Top-level groups are kept with their subtasks flattened.
43
+ * Inner nested groups are removed and their subtasks extracted recursively.
44
+ */
45
+ export function flattenTasks(tasks) {
46
+ const result = [];
47
+ for (const task of tasks) {
48
+ if (task.type === TaskType.Group &&
49
+ task.subtasks &&
50
+ task.subtasks.length > 0) {
51
+ // Preserve top-level group but flatten its subtasks
52
+ const flattenedSubtasks = flattenInnerTasks(task.subtasks);
53
+ const groupTask = {
54
+ action: task.action,
55
+ type: task.type,
56
+ subtasks: flattenedSubtasks,
57
+ };
58
+ result.push(groupTask);
59
+ }
60
+ else if (task.type !== TaskType.Group) {
61
+ // Non-group task - add as-is
62
+ const leafTask = {
63
+ action: task.action,
64
+ type: task.type,
65
+ };
66
+ if (task.params)
67
+ leafTask.params = task.params;
68
+ if (task.config)
69
+ leafTask.config = task.config;
70
+ result.push(leafTask);
71
+ }
72
+ // Skip empty groups (group with no subtasks)
73
+ }
74
+ return result;
75
+ }
11
76
  /**
12
77
  * Determine the operation name based on task types
13
78
  */
@@ -31,8 +96,12 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
31
96
  const validTasks = tasks.filter((task) => task.type !== TaskType.Ignore && task.type !== TaskType.Discard);
32
97
  // Check if no valid tasks remain after filtering
33
98
  if (validTasks.length === 0) {
34
- const msg = createMessage({ text: getUnknownRequestMessage() });
35
- workflowHandlers.addToQueue(msg);
99
+ // Use action from first ignore task if available, otherwise generic message
100
+ const ignoreTask = tasks.find((task) => task.type === TaskType.Ignore);
101
+ const message = ignoreTask?.action
102
+ ? `${ignoreTask.action}.`
103
+ : getUnknownRequestMessage();
104
+ workflowHandlers.addToQueue(createFeedback({ type: FeedbackType.Warning, message }));
36
105
  return;
37
106
  }
38
107
  const operation = getOperationName(validTasks);
@@ -80,61 +149,44 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, life
80
149
  }
81
150
  }
82
151
  /**
83
- * Validate task types - allows mixed types at top level with Groups,
84
- * but each Group must have uniform subtask types
152
+ * Validate task structure after flattening.
153
+ * Currently no-op since flattening removes Groups and mixed types are allowed.
85
154
  */
86
- function validateTaskTypes(tasks) {
87
- if (tasks.length === 0)
88
- return;
89
- // Convert to ScheduledTask to access subtasks property
90
- const scheduledTasks = asScheduledTasks(tasks);
91
- // Check each Group task's subtasks for uniform types
92
- for (const task of scheduledTasks) {
93
- if (task.type === TaskType.Group &&
94
- task.subtasks &&
95
- task.subtasks.length > 0) {
96
- const subtaskTypes = new Set(task.subtasks.map((t) => t.type));
97
- if (subtaskTypes.size > 1) {
98
- throw new Error(getMixedTaskTypesError(Array.from(subtaskTypes)));
99
- }
100
- // Recursively validate nested groups
101
- validateTaskTypes(task.subtasks);
102
- }
103
- }
155
+ function validateTaskTypes(_tasks) {
156
+ // After flattening, Groups are removed and mixed leaf types are allowed.
157
+ // The router handles different task types by routing each to its handler.
104
158
  }
105
159
  /**
106
160
  * Execute tasks after confirmation (internal helper)
107
- * Validates task types and routes each type appropriately
108
- * Supports mixed types at top level with Groups
161
+ * Flattens hierarchical structure, validates task types, and routes appropriately
109
162
  */
110
163
  function executeTasksAfterConfirm(tasks, context) {
111
164
  const { service, userRequest, workflowHandlers, requestHandlers } = context;
112
- // Validate task types (Groups must have uniform subtasks)
165
+ // Flatten hierarchical structure into flat list of leaf tasks
166
+ const scheduledTasks = asScheduledTasks(tasks);
167
+ const flatTasks = flattenTasks(scheduledTasks);
168
+ // Validate that all tasks have uniform type
113
169
  try {
114
- validateTaskTypes(tasks);
170
+ validateTaskTypes(flatTasks);
115
171
  }
116
172
  catch (error) {
117
173
  requestHandlers.onError(error instanceof Error ? error.message : String(error));
118
174
  return;
119
175
  }
120
- const scheduledTasks = asScheduledTasks(tasks);
121
- // Collect ALL Execute tasks (standalone and from groups) for upfront validation
122
- const allExecuteTasks = [];
123
- for (const task of scheduledTasks) {
176
+ // Collect all Execute tasks for validation (including those inside groups)
177
+ const executeTasks = [];
178
+ for (const task of flatTasks) {
124
179
  if (task.type === TaskType.Execute) {
125
- allExecuteTasks.push(task);
180
+ executeTasks.push(task);
126
181
  }
127
182
  else if (task.type === TaskType.Group && task.subtasks) {
128
- const subtasks = task.subtasks;
129
- if (subtasks.length > 0 && subtasks[0].type === TaskType.Execute) {
130
- allExecuteTasks.push(...subtasks);
131
- }
183
+ executeTasks.push(...task.subtasks.filter((t) => t.type === TaskType.Execute));
132
184
  }
133
185
  }
134
- // Validate ALL Execute tasks together to collect ALL missing config upfront
135
- if (allExecuteTasks.length > 0) {
186
+ // Validate Execute tasks to collect missing config upfront
187
+ if (executeTasks.length > 0) {
136
188
  try {
137
- const validation = validateExecuteTasks(allExecuteTasks);
189
+ const validation = validateExecuteTasks(executeTasks);
138
190
  if (validation.validationErrors.length > 0) {
139
191
  // Show error feedback for invalid skills
140
192
  const errorMessages = validation.validationErrors.map((error) => {
@@ -150,7 +202,7 @@ function executeTasksAfterConfirm(tasks, context) {
150
202
  return;
151
203
  }
152
204
  else if (validation.missingConfig.length > 0) {
153
- // Missing config detected - create ONE Validate component for ALL missing config
205
+ // Missing config detected - create Validate component for all missing config
154
206
  workflowHandlers.addToQueue(createValidate({
155
207
  missingConfig: validation.missingConfig,
156
208
  userRequest,
@@ -160,7 +212,7 @@ function executeTasksAfterConfirm(tasks, context) {
160
212
  },
161
213
  onValidationComplete: () => {
162
214
  // After config is complete, resume task routing
163
- routeTasksAfterConfig(scheduledTasks, context);
215
+ routeTasksAfterConfig(flatTasks, context);
164
216
  },
165
217
  onAborted: (operation) => {
166
218
  requestHandlers.onAborted(operation);
@@ -175,74 +227,117 @@ function executeTasksAfterConfirm(tasks, context) {
175
227
  }
176
228
  }
177
229
  // No missing config - proceed with normal routing
178
- routeTasksAfterConfig(scheduledTasks, context);
230
+ routeTasksAfterConfig(flatTasks, context);
231
+ }
232
+ /**
233
+ * Task types that should appear in the upcoming display
234
+ */
235
+ const UPCOMING_TASK_TYPES = [TaskType.Execute, TaskType.Answer, TaskType.Group];
236
+ /**
237
+ * Collect action names for tasks that appear in upcoming display.
238
+ * Groups are included with their group name (not individual subtask names).
239
+ */
240
+ function collectUpcomingNames(tasks) {
241
+ return tasks
242
+ .filter((t) => UPCOMING_TASK_TYPES.includes(t.type))
243
+ .map((t) => t.action);
179
244
  }
180
245
  /**
181
246
  * Route tasks after config is complete (or when no config is needed)
182
- * Processes tasks in order, grouping by type
247
+ * Processes task list, routing each task type to its handler.
248
+ * Top-level groups are preserved: their subtasks are routed with the group name.
249
+ * Config tasks are grouped together; Execute/Answer are routed individually.
183
250
  */
184
- function routeTasksAfterConfig(scheduledTasks, context) {
185
- // Process tasks in order, preserving Group boundaries
186
- // Track consecutive standalone tasks to group them by type
187
- let consecutiveStandaloneTasks = [];
188
- const processStandaloneTasks = () => {
189
- if (consecutiveStandaloneTasks.length === 0)
190
- return;
191
- // Group consecutive standalone tasks by type
192
- const tasksByType = {};
193
- for (const type of Object.values(TaskType)) {
194
- tasksByType[type] = [];
195
- }
196
- for (const task of consecutiveStandaloneTasks) {
197
- tasksByType[task.type].push(task);
251
+ function routeTasksAfterConfig(tasks, context) {
252
+ if (tasks.length === 0)
253
+ return;
254
+ // Collect all upcoming names for display (Execute, Answer, and Group tasks)
255
+ const allUpcomingNames = collectUpcomingNames(tasks);
256
+ let upcomingIndex = 0;
257
+ // Task types that should be grouped together (one component for all tasks)
258
+ const groupedTypes = [TaskType.Config, TaskType.Introspect];
259
+ // Route grouped task types together (collect from all tasks including subtasks)
260
+ for (const groupedType of groupedTypes) {
261
+ const typeTasks = [];
262
+ for (const task of tasks) {
263
+ if (task.type === groupedType) {
264
+ typeTasks.push(task);
265
+ }
266
+ else if (task.type === TaskType.Group && task.subtasks) {
267
+ typeTasks.push(...task.subtasks.filter((t) => t.type === groupedType));
268
+ }
198
269
  }
199
- // Route each type group
200
- for (const [type, typeTasks] of Object.entries(tasksByType)) {
201
- const taskType = type;
202
- if (typeTasks.length === 0)
203
- continue;
204
- routeTasksByType(taskType, typeTasks, context);
270
+ if (typeTasks.length > 0) {
271
+ routeTasksByType(groupedType, typeTasks, context, []);
205
272
  }
206
- consecutiveStandaloneTasks = [];
207
- };
208
- // Process tasks in original order
209
- for (const task of scheduledTasks) {
210
- if (task.type === TaskType.Group && task.subtasks) {
211
- // Process any accumulated standalone tasks first
212
- processStandaloneTasks();
213
- // Process Group as separate component
214
- if (task.subtasks.length > 0) {
215
- const subtasks = task.subtasks;
216
- const taskType = subtasks[0].type;
217
- routeTasksByType(taskType, subtasks, context);
273
+ }
274
+ // Process Execute, Answer, and Group tasks individually (with upcoming support)
275
+ for (let i = 0; i < tasks.length; i++) {
276
+ const task = tasks[i];
277
+ const taskType = task.type;
278
+ // Skip grouped task types (already routed above)
279
+ if (groupedTypes.includes(taskType))
280
+ continue;
281
+ if (taskType === TaskType.Group && task.subtasks) {
282
+ // Route group's subtasks - Execute tasks get group label, others routed normally
283
+ const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
284
+ upcomingIndex++;
285
+ // Separate subtasks by type
286
+ const executeSubtasks = task.subtasks.filter((t) => t.type === TaskType.Execute);
287
+ const answerSubtasks = task.subtasks.filter((t) => t.type === TaskType.Answer);
288
+ // Route Execute subtasks with group name as label
289
+ if (executeSubtasks.length > 0) {
290
+ routeExecuteTasks(executeSubtasks, context, upcoming, task.action);
291
+ }
292
+ // Route Answer subtasks individually
293
+ if (answerSubtasks.length > 0) {
294
+ routeAnswerTasks(answerSubtasks, context, upcoming);
218
295
  }
219
296
  }
297
+ else if (taskType === TaskType.Execute) {
298
+ // Calculate upcoming for this Execute task
299
+ const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
300
+ upcomingIndex++;
301
+ routeExecuteTasks([task], context, upcoming);
302
+ }
303
+ else if (taskType === TaskType.Answer) {
304
+ // Calculate upcoming for this Answer task
305
+ const upcoming = allUpcomingNames.slice(upcomingIndex + 1);
306
+ upcomingIndex++;
307
+ routeTasksByType(taskType, [task], context, upcoming);
308
+ }
220
309
  else {
221
- // Accumulate standalone task
222
- consecutiveStandaloneTasks.push(task);
310
+ // For other types (Report, etc.), route without upcoming
311
+ routeTasksByType(taskType, [task], context, []);
223
312
  }
224
313
  }
225
- // Process any remaining standalone tasks
226
- processStandaloneTasks();
227
314
  }
228
315
  /**
229
316
  * Route Answer tasks - creates separate Answer component for each question
230
317
  */
231
- function routeAnswerTasks(tasks, context) {
232
- for (const task of tasks) {
233
- context.workflowHandlers.addToQueue(createAnswer({ question: task.action, service: context.service }));
318
+ function routeAnswerTasks(tasks, context, upcoming) {
319
+ for (let i = 0; i < tasks.length; i++) {
320
+ const task = tasks[i];
321
+ // Calculate upcoming: remaining answer tasks + original upcoming
322
+ const remainingAnswers = tasks.slice(i + 1).map((t) => t.action);
323
+ const taskUpcoming = [...remainingAnswers, ...upcoming];
324
+ context.workflowHandlers.addToQueue(createAnswer({
325
+ question: task.action,
326
+ service: context.service,
327
+ upcoming: taskUpcoming,
328
+ }));
234
329
  }
235
330
  }
236
331
  /**
237
332
  * Route Introspect tasks - creates single Introspect component for all tasks
238
333
  */
239
- function routeIntrospectTasks(tasks, context) {
334
+ function routeIntrospectTasks(tasks, context, _upcoming) {
240
335
  context.workflowHandlers.addToQueue(createIntrospect({ tasks, service: context.service }));
241
336
  }
242
337
  /**
243
338
  * Route Config tasks - extracts keys, caches labels, creates Config component
244
339
  */
245
- function routeConfigTasks(tasks, context) {
340
+ function routeConfigTasks(tasks, context, _upcoming) {
246
341
  const configKeys = tasks
247
342
  .map((task) => task.params?.key)
248
343
  .filter((key) => key !== undefined);
@@ -286,8 +381,8 @@ function routeConfigTasks(tasks, context) {
286
381
  /**
287
382
  * Route Execute tasks - creates Execute component (validation already done)
288
383
  */
289
- function routeExecuteTasks(tasks, context) {
290
- context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service }));
384
+ function routeExecuteTasks(tasks, context, upcoming, label) {
385
+ context.workflowHandlers.addToQueue(createExecute({ tasks, service: context.service, upcoming, label }));
291
386
  }
292
387
  /**
293
388
  * Registry mapping task types to their route handlers
@@ -302,9 +397,9 @@ const taskRouteHandlers = {
302
397
  * Route tasks by type to appropriate components
303
398
  * Uses registry pattern for extensibility
304
399
  */
305
- function routeTasksByType(taskType, tasks, context) {
400
+ function routeTasksByType(taskType, tasks, context, upcoming) {
306
401
  const handler = taskRouteHandlers[taskType];
307
402
  if (handler) {
308
- handler(tasks, context);
403
+ handler(tasks, context, upcoming);
309
404
  }
310
405
  }
@@ -61,6 +61,14 @@ export class DummyExecutor {
61
61
  }
62
62
  // Marker for extracting pwd from command output
63
63
  const PWD_MARKER = '__PWD_MARKER_7x9k2m__';
64
+ const MAX_OUTPUT_LINES = 128;
65
+ /**
66
+ * Limit output to last MAX_OUTPUT_LINES lines.
67
+ */
68
+ function limitLines(output) {
69
+ const lines = output.split('\n');
70
+ return lines.slice(-MAX_OUTPUT_LINES).join('\n');
71
+ }
64
72
  /**
65
73
  * Parse stdout to extract workdir and clean output.
66
74
  * Returns the cleaned output and the extracted workdir.
@@ -93,6 +101,12 @@ class OutputStreamer {
93
101
  */
94
102
  pushStdout(data) {
95
103
  this.chunks.push(data);
104
+ // Collapse when we have too many chunks to prevent memory growth
105
+ if (this.chunks.length > 16) {
106
+ const accumulated = this.chunks.join('');
107
+ this.chunks = [limitLines(accumulated)];
108
+ this.emittedLength = 0;
109
+ }
96
110
  if (!this.callback)
97
111
  return;
98
112
  const accumulated = this.chunks.join('');
@@ -123,7 +137,7 @@ class OutputStreamer {
123
137
  * Get the accumulated raw output.
124
138
  */
125
139
  getAccumulated() {
126
- return this.chunks.join('');
140
+ return limitLines(this.chunks.join(''));
127
141
  }
128
142
  }
129
143
  /**
@@ -189,6 +203,13 @@ export class RealExecutor {
189
203
  child.stderr.on('data', (data) => {
190
204
  const text = data.toString();
191
205
  stderr.push(text);
206
+ // Collapse when we have too many chunks to prevent memory growth
207
+ if (stderr.length > 16) {
208
+ const accumulated = stderr.join('');
209
+ const limited = limitLines(accumulated);
210
+ stderr.length = 0;
211
+ stderr.push(limited);
212
+ }
192
213
  this.outputCallback?.(text, 'stderr');
193
214
  });
194
215
  child.on('error', (error) => {
@@ -200,7 +221,7 @@ export class RealExecutor {
200
221
  description: cmd.description,
201
222
  command: cmd.command,
202
223
  output: stdoutStreamer.getAccumulated(),
203
- errors: error.message,
224
+ errors: limitLines(stderr.join('')) || error.message,
204
225
  result: ExecutionResult.Error,
205
226
  error: error.message,
206
227
  };
@@ -218,7 +239,7 @@ export class RealExecutor {
218
239
  description: cmd.description,
219
240
  command: cmd.command,
220
241
  output,
221
- errors: stderr.join(''),
242
+ errors: limitLines(stderr.join('')),
222
243
  result: success ? ExecutionResult.Success : ExecutionResult.Error,
223
244
  error: success ? undefined : `Exit code: ${code}`,
224
245
  workdir,
@@ -278,9 +299,8 @@ export async function executeCommands(commands, onProgress) {
278
299
  : ExecutionStatus.Failed,
279
300
  output,
280
301
  });
281
- // Stop if critical command failed
282
- const isCritical = cmd.critical !== false;
283
- if (output.result !== ExecutionResult.Success && isCritical) {
302
+ // Stop on failure
303
+ if (output.result !== ExecutionResult.Success) {
284
304
  break;
285
305
  }
286
306
  }
@@ -94,17 +94,47 @@ export function loadSkillDefinitions(fs = defaultFileSystem) {
94
94
  const skills = loadSkills(fs);
95
95
  return skills.map(({ key, content }) => parseSkillMarkdown(key, content));
96
96
  }
97
+ /**
98
+ * Mark incomplete skill in markdown by appending (INCOMPLETE) to name
99
+ */
100
+ function markIncompleteSkill(content) {
101
+ return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
102
+ }
103
+ /**
104
+ * Load skills with both formatted prompt section and parsed definitions
105
+ * Single source of truth for both LLM prompts and debug display
106
+ * Parses each skill only once for efficiency
107
+ */
108
+ export function loadSkillsForPrompt(fs = defaultFileSystem) {
109
+ const skills = loadSkills(fs);
110
+ // Parse each skill once and build both outputs
111
+ const definitions = [];
112
+ const markedContent = [];
113
+ for (const { key, content } of skills) {
114
+ const parsed = parseSkillMarkdown(key, content);
115
+ definitions.push(parsed);
116
+ // Mark incomplete skills in markdown for LLM
117
+ if (parsed.isIncomplete) {
118
+ markedContent.push(markIncompleteSkill(content));
119
+ }
120
+ else {
121
+ markedContent.push(content);
122
+ }
123
+ }
124
+ const formatted = formatSkillsForPrompt(markedContent);
125
+ return { formatted, definitions };
126
+ }
97
127
  /**
98
128
  * Load skills and mark incomplete ones in their markdown
99
129
  * Returns array of skill markdown with status markers
130
+ * Uses loadSkillsForPrompt internally to avoid duplicating logic
100
131
  */
101
132
  export function loadSkillsWithValidation(fs = defaultFileSystem) {
102
133
  const skills = loadSkills(fs);
103
134
  return skills.map(({ key, content }) => {
104
135
  const parsed = parseSkillMarkdown(key, content);
105
- // If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
106
136
  if (parsed.isIncomplete) {
107
- return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
137
+ return markIncompleteSkill(content);
108
138
  }
109
139
  return content;
110
140
  });
@@ -127,6 +157,7 @@ export function createSkillLookup(definitions) {
127
157
  }
128
158
  /**
129
159
  * Format skills for inclusion in the planning prompt
160
+ * Skills are joined with double newlines (skill headers provide separation)
130
161
  */
131
162
  export function formatSkillsForPrompt(skills) {
132
163
  if (skills.length === 0) {
@@ -148,11 +179,8 @@ brackets for additional information. Use commas instead. For example:
148
179
  - WRONG: "Build project Alpha (the legacy version)"
149
180
 
150
181
  `;
151
- const separator = '-'.repeat(64);
152
- const skillsContent = skills
153
- .map((s) => s.trim())
154
- .join('\n\n' + separator + '\n\n');
155
- return header + separator + '\n\n' + skillsContent;
182
+ const skillsContent = skills.map((s) => s.trim()).join('\n\n');
183
+ return header + skillsContent;
156
184
  }
157
185
  /**
158
186
  * Parse skill reference from execution line
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Timing utilities for UI components
3
3
  */
4
+ export const ELAPSED_UPDATE_INTERVAL = 250;
4
5
  /**
5
6
  * Waits for at least the minimum processing time.
6
7
  * Ensures async operations don't complete too quickly for good UX.
@@ -139,6 +139,18 @@ Given tasks from this skill:
139
139
  Do NOT invent different commands - use exactly what the skill specifies,
140
140
  with parameter placeholders replaced by actual values.
141
141
 
142
+ ### Handling Skipped Steps
143
+
144
+ **CRITICAL - STEP ORDER PRESERVATION**: When some steps from a skill are
145
+ omitted during scheduling, you MUST maintain alignment with the
146
+ original step positions in both the Steps and Execution sections. Each
147
+ task corresponds to a specific line number in the skill definition, NOT
148
+ to its sequential position in the task list. If you receive tasks for
149
+ steps 1 and 3 (with step 2 skipped), use Execution lines 1 and 3
150
+ (NOT lines 1 and 2). The step numbers in the task actions indicate
151
+ which Execution line to use - always match by original position, never
152
+ by sequential task index.
153
+
142
154
  **CRITICAL - VERBATIM EXECUTION**: Run shell commands EXACTLY as written in
143
155
  the ### Execution section. Do NOT:
144
156
  - Modify the command string in any way
@@ -180,7 +192,6 @@ Return a structured response with commands to execute:
180
192
  - **workdir**: Optional working directory for the command (defaults to
181
193
  current)
182
194
  - **timeout**: Optional timeout in milliseconds (defaults to 30000)
183
- - **critical**: Whether failure should stop execution (defaults to true)
184
195
 
185
196
  ## Command Generation Guidelines
186
197
 
@@ -364,14 +375,12 @@ commands:
364
375
 
365
376
  For complex multi-step operations:
366
377
 
367
- 1. **Sequential dependencies**: Mark early commands as critical so failure
368
- stops the chain
378
+ 1. **Sequential dependencies**: Commands execute in order; any failure stops
379
+ the chain
369
380
  2. **Long-running processes**: Set appropriate timeouts (build processes
370
381
  may need 10+ minutes)
371
382
  3. **Working directories**: Use workdir to ensure commands run in the
372
383
  right location
373
- 4. **Error handling**: For non-critical cleanup steps, set critical:
374
- false
375
384
 
376
385
  ## Handling Config Placeholders
377
386
 
@@ -444,8 +453,7 @@ Before returning commands:
444
453
  7. Check that all task params are incorporated
445
454
  8. Ensure paths are properly quoted
446
455
  9. Confirm timeouts are reasonable for each operation
447
- 10. Validate that critical flags are set appropriately
448
- 11. Review for any safety concerns
456
+ 10. Review for any safety concerns
449
457
 
450
458
  ## Confirmed Schedule
451
459