prompt-language-shell 0.9.4 → 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.
@@ -107,7 +107,18 @@ export const Workflow = ({ initialQueue, debug }) => {
107
107
  setQueue((queue) => [...queue, ...items]);
108
108
  },
109
109
  addToTimeline: (...items) => {
110
- setTimeline((prev) => [...prev, ...items]);
110
+ // Flush pending to timeline first, then add new items
111
+ // Both are added in a SINGLE setTimeline call to guarantee order
112
+ setCurrent((curr) => {
113
+ const { active, pending } = curr;
114
+ if (pending) {
115
+ const donePending = markAsDone(pending);
116
+ setTimeline((prev) => [...prev, donePending, ...items]);
117
+ return { active, pending: null };
118
+ }
119
+ setTimeline((prev) => [...prev, ...items]);
120
+ return curr;
121
+ });
111
122
  },
112
123
  }), []);
113
124
  // Global Esc handler removed - components handle their own Esc individually
@@ -37,8 +37,7 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
37
37
  completedSelections,
38
38
  };
39
39
  requestHandlers.onCompleted(finalState);
40
- // Complete the selection phase - it goes to timeline
41
- // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
40
+ // Move Schedule to pending - callback will flush to timeline
42
41
  lifecycleHandlers.completeActive();
43
42
  void onSelectionConfirmed(concreteTasks);
44
43
  }
@@ -102,9 +101,9 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
102
101
  const options = task.params.options;
103
102
  const selectedIndex = newCompletedSelections[defineGroupIndex];
104
103
  const selectedOption = options[selectedIndex];
105
- // Use Execute as default - LLM will properly classify during refinement
104
+ // Use the command from the selected option
106
105
  refinedTasks.push({
107
- action: selectedOption,
106
+ action: selectedOption.command,
108
107
  type: TaskType.Execute,
109
108
  config: [],
110
109
  });
@@ -122,16 +121,12 @@ export function Schedule({ message, tasks, status, debug = DebugLevel.None, requ
122
121
  completedSelections: newCompletedSelections,
123
122
  };
124
123
  requestHandlers.onCompleted(finalState);
124
+ // Move Schedule to pending - refinement will flush it to timeline
125
+ // before adding Command, ensuring correct order
126
+ lifecycleHandlers.completeActive();
125
127
  if (onSelectionConfirmed) {
126
- // Complete the selection phase - it goes to timeline
127
- // Callback will create a new Plan showing refined tasks (pending) + Confirm (active)
128
- lifecycleHandlers.completeActive();
129
128
  void onSelectionConfirmed(refinedTasks);
130
129
  }
131
- else {
132
- // No selection callback, just complete normally
133
- lifecycleHandlers.completeActive();
134
- }
135
130
  }
136
131
  }
137
132
  }, { isActive: isActive && defineTask !== null });
@@ -3,5 +3,10 @@ import { Box, Text } from 'ink';
3
3
  const CONTENT_WIDTH = 80;
4
4
  const HORIZONTAL_PADDING = 2;
5
5
  export const Debug = ({ title, content, color }) => {
6
- return (_jsxs(Box, { flexDirection: "column", paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: [_jsx(Text, { color: color, wrap: "wrap", children: title }), _jsx(Text, { color: color, wrap: "wrap", children: content })] }));
6
+ // Plain text content - single bordered box
7
+ if (typeof content === 'string') {
8
+ return (_jsxs(Box, { flexDirection: "column", paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: [_jsx(Text, { color: color, wrap: "wrap", children: title }), _jsx(Text, { color: color, wrap: "wrap", children: content })] }));
9
+ }
10
+ // Array content - table with one column, each item in bordered row
11
+ return (_jsxs(Box, { flexDirection: "column", width: CONTENT_WIDTH, children: [_jsx(Box, { paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, children: _jsx(Text, { color: color, wrap: "wrap", children: title }) }), content.map((section, index) => (_jsx(Box, { paddingX: HORIZONTAL_PADDING, paddingY: 1, borderStyle: "single", borderColor: color, width: CONTENT_WIDTH, marginTop: -1, children: _jsx(Text, { color: color, wrap: "wrap", children: section }) }, index)))] }));
7
12
  };
@@ -1,19 +1,8 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { ComponentStatus } from '../../types/components.js';
4
- import { FeedbackType } from '../../types/types.js';
5
4
  import { getFeedbackColor } from '../../services/colors.js';
6
- function getSymbol(type) {
7
- return {
8
- [FeedbackType.Info]: 'ℹ',
9
- [FeedbackType.Warning]: '⚠',
10
- [FeedbackType.Succeeded]: '✓',
11
- [FeedbackType.Aborted]: '⊘',
12
- [FeedbackType.Failed]: '✗',
13
- }[type];
14
- }
15
5
  export function Feedback({ type, message }) {
16
6
  const color = getFeedbackColor(type, ComponentStatus.Done);
17
- const symbol = getSymbol(type);
18
- return (_jsx(Box, { marginLeft: 1, children: _jsxs(Text, { color: color, children: [symbol, " ", message] }) }));
7
+ return (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: color, children: message }) }));
19
8
  }
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { ComponentStatus } from '../../types/components.js';
4
- import { TaskType } from '../../types/types.js';
4
+ import { TaskType, } from '../../types/types.js';
5
5
  import { DebugLevel } from '../../configuration/types.js';
6
6
  import { getTaskColors, getTaskTypeLabel, Palette, } from '../../services/colors.js';
7
7
  import { Label } from './Label.js';
@@ -28,7 +28,8 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
28
28
  }
29
29
  // Add children for Define tasks with options
30
30
  if (task.type === TaskType.Define && Array.isArray(task.params?.options)) {
31
- item.children = task.params.options.map((option, index) => {
31
+ const options = task.params.options;
32
+ item.children = options.map((option, index) => {
32
33
  // Determine the type based on selection state
33
34
  let childType = TaskType.Select;
34
35
  if (highlightedChildIndex !== null) {
@@ -40,7 +41,7 @@ export function taskToListItem(task, highlightedChildIndex = null, isDefineTaskW
40
41
  const planColors = getTaskColors(TaskType.Schedule, status);
41
42
  return {
42
43
  description: {
43
- text: option,
44
+ text: option.name,
44
45
  color: colors.description,
45
46
  highlightedColor: planColors.description,
46
47
  },
@@ -0,0 +1,15 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import React from 'react';
3
+ import { Box, Text } from 'ink';
4
+ const DEFAULT_WIDTH = 80;
5
+ const DEFAULT_PADDING = 1;
6
+ export const Cell = ({ children, padding = DEFAULT_PADDING }) => (_jsx(Box, { paddingX: padding, children: _jsx(Text, { wrap: "wrap", children: children }) }));
7
+ export const Column = ({ children, width }) => (_jsx(Box, { flexDirection: "column", width: width, children: children }));
8
+ const Row = ({ children, color, innerWidth }) => (_jsxs(Box, { children: [_jsx(Text, { color: color, children: '│' }), _jsx(Box, { width: innerWidth, children: children }), _jsx(Text, { color: color, children: '│' })] }));
9
+ export const Table = ({ data, width = DEFAULT_WIDTH, color }) => {
10
+ const innerWidth = width - 2;
11
+ const TOP = '┌' + '─'.repeat(innerWidth) + '┐';
12
+ const DIV = '├' + '─'.repeat(innerWidth) + '┤';
13
+ const BOT = '└' + '─'.repeat(innerWidth) + '┘';
14
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { color: color, children: TOP }), data.map((content, index) => (_jsxs(React.Fragment, { children: [index > 0 && _jsx(Text, { color: color, children: DIV }), _jsx(Row, { color: color, innerWidth: innerWidth, children: _jsx(Cell, { children: content }) })] }, index))), _jsx(Text, { color: color, children: BOT })] }));
15
+ };
@@ -1,7 +1,7 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getAvailableConfigStructure, getConfiguredKeys, } from '../configuration/schema.js';
3
3
  import { logPrompt, logResponse } from './logger.js';
4
- import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
4
+ import { loadSkillsForPrompt } from './skills.js';
5
5
  import { toolRegistry } from './registry.js';
6
6
  import { CommandResultSchema, IntrospectResultSchema, } from '../types/schemas.js';
7
7
  /**
@@ -69,36 +69,32 @@ export class AnthropicService {
69
69
  async processWithTool(command, toolName, customInstructions) {
70
70
  // Load tool from registry
71
71
  const tool = toolRegistry.getSchema(toolName);
72
- // Use custom instructions if provided, otherwise load from registry
73
- let systemPrompt;
74
- if (customInstructions) {
75
- // Custom instructions provided (typically for testing)
76
- systemPrompt = customInstructions;
72
+ // Check if this tool uses skills
73
+ const usesSkills = toolName === 'schedule' ||
74
+ toolName === 'introspect' ||
75
+ toolName === 'execute' ||
76
+ toolName === 'validate';
77
+ // Load base instructions and skills
78
+ const baseInstructions = customInstructions || toolRegistry.getInstructions(toolName);
79
+ let formattedSkills = '';
80
+ let skillDefinitions = [];
81
+ let systemPrompt = baseInstructions;
82
+ if (!customInstructions && usesSkills) {
83
+ const skillsResult = loadSkillsForPrompt();
84
+ formattedSkills = skillsResult.formatted;
85
+ skillDefinitions = skillsResult.definitions;
86
+ systemPrompt += formattedSkills;
77
87
  }
78
- else {
79
- // Load and build system prompt automatically (production)
80
- const instructions = toolRegistry.getInstructions(toolName);
81
- systemPrompt = instructions;
82
- // Add skills section for applicable tools
83
- if (toolName === 'schedule' ||
84
- toolName === 'introspect' ||
85
- toolName === 'execute' ||
86
- toolName === 'validate') {
87
- const skills = loadSkillsWithValidation();
88
- const skillsSection = formatSkillsForPrompt(skills);
89
- systemPrompt += skillsSection;
90
- }
91
- // Add config structure for configure tool only
92
- if (toolName === 'configure') {
93
- const configStructure = getAvailableConfigStructure();
94
- const configuredKeys = getConfiguredKeys();
95
- const configSection = '\n## Available Configuration\n\n' +
96
- 'Config structure (key: description):\n' +
97
- JSON.stringify(configStructure, null, 2) +
98
- '\n\nConfigured keys (keys that exist in config file):\n' +
99
- JSON.stringify(configuredKeys, null, 2);
100
- systemPrompt += configSection;
101
- }
88
+ // Add config structure for configure tool only
89
+ if (!customInstructions && toolName === 'configure') {
90
+ const configStructure = getAvailableConfigStructure();
91
+ const configuredKeys = getConfiguredKeys();
92
+ const configSection = '\n## Available Configuration\n\n' +
93
+ 'Config structure (key: description):\n' +
94
+ JSON.stringify(configStructure, null, 2) +
95
+ '\n\nConfigured keys (keys that exist in config file):\n' +
96
+ JSON.stringify(configuredKeys, null, 2);
97
+ systemPrompt += configSection;
102
98
  }
103
99
  // Build tools array - add web search for answer tool
104
100
  const tools = [tool];
@@ -111,7 +107,7 @@ export class AnthropicService {
111
107
  // Collect debug components
112
108
  const debug = [];
113
109
  // Log prompt at Verbose level
114
- const promptDebug = logPrompt(toolName, command, systemPrompt);
110
+ const promptDebug = logPrompt(toolName, command, baseInstructions, formattedSkills, skillDefinitions);
115
111
  if (promptDebug) {
116
112
  debug.push(promptDebug);
117
113
  }
@@ -20,6 +20,7 @@ export const Palette = {
20
20
  Yellow: '#cccc5c',
21
21
  Orange: '#f48c80',
22
22
  MediumOrange: '#d07560',
23
+ WarmOrange: '#ce985e',
23
24
  DarkOrange: '#ab5e40',
24
25
  BurntOrange: '#cc7a5c',
25
26
  Red: '#cc5c5c',
@@ -132,7 +133,7 @@ const taskColors = {
132
133
  */
133
134
  const feedbackColors = {
134
135
  [FeedbackType.Info]: Colors.Status.Info,
135
- [FeedbackType.Warning]: Palette.Yellow,
136
+ [FeedbackType.Warning]: Palette.WarmOrange,
136
137
  [FeedbackType.Succeeded]: Colors.Status.Success,
137
138
  [FeedbackType.Aborted]: Palette.MediumOrange,
138
139
  [FeedbackType.Failed]: Colors.Status.Error,
@@ -1,7 +1,19 @@
1
1
  import { DebugLevel } from '../configuration/types.js';
2
- import { createDebug } from './components.js';
3
2
  import { loadDebugSetting } from '../configuration/io.js';
4
3
  import { Palette } from './colors.js';
4
+ import { createDebug } from './components.js';
5
+ /**
6
+ * Enum controlling what content is shown in debug prompt output
7
+ * - LLM: Exact prompt as sent to LLM (no display formatting)
8
+ * - Skills: Same content with visual separators for readability
9
+ * - Summary: Condensed view (Name, Steps, Execution only)
10
+ */
11
+ export var PromptDisplay;
12
+ (function (PromptDisplay) {
13
+ PromptDisplay["LLM"] = "llm";
14
+ PromptDisplay["Skills"] = "skills";
15
+ PromptDisplay["Summary"] = "summary";
16
+ })(PromptDisplay || (PromptDisplay = {}));
5
17
  /**
6
18
  * Debug logger for the application
7
19
  * Logs information based on the current debug level setting
@@ -49,24 +61,125 @@ export function getWarnings() {
49
61
  warnings.length = 0;
50
62
  return result;
51
63
  }
64
+ /**
65
+ * Content width for debug display (matches Debug component)
66
+ * Box width 80 - 2 borders - 4 padding = 74 chars
67
+ */
68
+ const DISPLAY_CONTENT_WIDTH = 74;
69
+ /**
70
+ * Join sections with separators matching display width
71
+ */
72
+ function joinWithSeparators(sections) {
73
+ const separator = '-'.repeat(DISPLAY_CONTENT_WIDTH);
74
+ return sections.join('\n\n' + separator + '\n\n');
75
+ }
76
+ /**
77
+ * Format a single skill definition as summary markdown
78
+ */
79
+ function formatSkillSummary(skill) {
80
+ const lines = [];
81
+ lines.push(`### Name`);
82
+ lines.push(skill.name);
83
+ lines.push('');
84
+ if (skill.steps.length > 0) {
85
+ lines.push(`### Steps`);
86
+ for (const step of skill.steps) {
87
+ lines.push(`- ${step}`);
88
+ }
89
+ lines.push('');
90
+ }
91
+ if (skill.execution.length > 0) {
92
+ lines.push(`### Execution`);
93
+ for (const cmd of skill.execution) {
94
+ lines.push(`- ${cmd}`);
95
+ }
96
+ }
97
+ return lines.join('\n').trim();
98
+ }
99
+ /**
100
+ * Format skill definitions as summary for debug display
101
+ * Shows only Name, Steps, and Execution with visual separators
102
+ */
103
+ export function formatSkillsSummary(definitions) {
104
+ if (definitions.length === 0) {
105
+ return '(no skills)';
106
+ }
107
+ const header = '## Available Skills';
108
+ const skillSummaries = definitions.map(formatSkillSummary);
109
+ return joinWithSeparators([header, ...skillSummaries]);
110
+ }
111
+ /**
112
+ * Format skills section with visual separators for debug display
113
+ * Layout: Header description -> separator -> skills separated by lines
114
+ */
115
+ function formatSkillsForDisplay(formattedSkills) {
116
+ if (!formattedSkills) {
117
+ return '(no skills)';
118
+ }
119
+ // Find the header (everything before first ### Name)
120
+ const firstNameIndex = formattedSkills.search(/^###\s+Name/m);
121
+ if (firstNameIndex === -1) {
122
+ return '(no skills)';
123
+ }
124
+ const header = formattedSkills.slice(0, firstNameIndex).trim();
125
+ const skillsContent = formattedSkills.slice(firstNameIndex);
126
+ // Split by ### Name to get individual skills
127
+ const skillParts = skillsContent
128
+ .split(/(?=^###\s+Name)/m)
129
+ .map((s) => s.trim())
130
+ .filter(Boolean);
131
+ if (skillParts.length === 0) {
132
+ return '(no skills)';
133
+ }
134
+ // Join header and skills with separators
135
+ return joinWithSeparators([header, ...skillParts]);
136
+ }
137
+ /**
138
+ * Format prompt content based on the specified detail level
139
+ *
140
+ * - LLM: Returns header + base instructions + formatted skills (as sent to LLM)
141
+ * - Skills: Returns header + skills with visual separators (no base instructions)
142
+ * - Summary: Returns header + skill summaries (Name, Steps, Execution)
143
+ */
144
+ export function formatPromptContent(toolName, command, baseInstructions, formattedSkills, mode, definitions) {
145
+ const header = ['', `Tool: ${toolName}`, `Command: ${command}`];
146
+ switch (mode) {
147
+ case PromptDisplay.LLM:
148
+ return [...header, '', baseInstructions + formattedSkills].join('\n');
149
+ case PromptDisplay.Skills: {
150
+ // Layout: header -> separator -> skills with visual separators
151
+ const headerString = header.join('\n');
152
+ const skillsDisplay = formatSkillsForDisplay(formattedSkills);
153
+ return joinWithSeparators([headerString, skillsDisplay]);
154
+ }
155
+ case PromptDisplay.Summary: {
156
+ const headerString = header.join('\n');
157
+ const summary = definitions
158
+ ? formatSkillsSummary(definitions)
159
+ : '(no skills)';
160
+ return joinWithSeparators([headerString, summary]);
161
+ }
162
+ }
163
+ }
52
164
  /**
53
165
  * Create debug component for system prompts sent to the LLM
54
166
  * Only creates at Verbose level
167
+ *
168
+ * @param toolName - Name of the tool being invoked
169
+ * @param command - User command being processed
170
+ * @param baseInstructions - Base tool instructions (without skills)
171
+ * @param formattedSkills - Formatted skills section (as sent to LLM)
172
+ * @param definitions - Parsed skill definitions for summary display
55
173
  */
56
- export function logPrompt(toolName, command, instructions) {
174
+ export function logPrompt(toolName, command, baseInstructions, formattedSkills, definitions = []) {
57
175
  if (currentDebugLevel !== DebugLevel.Verbose) {
58
176
  return null;
59
177
  }
60
- const content = [
61
- '',
62
- `Tool: ${toolName}`,
63
- `Command: ${command}`,
64
- '',
65
- instructions,
66
- ].join('\n');
67
- // Calculate stats for the instructions
68
- const lines = instructions.split('\n').length;
69
- const bytes = Buffer.byteLength(instructions, 'utf-8');
178
+ const content = formatPromptContent(toolName, command, baseInstructions, formattedSkills, PromptDisplay.Summary, definitions);
179
+ // Calculate stats for the full prompt
180
+ const fullPrompt = baseInstructions + formattedSkills;
181
+ const lines = fullPrompt.split('\n').length;
182
+ const bytes = Buffer.byteLength(fullPrompt, 'utf-8');
70
183
  const title = `SYSTEM PROMPT (${String(lines)} lines, ${String(bytes)} bytes)`;
71
184
  return createDebug({ title, content, color: Palette.Gray });
72
185
  }
@@ -85,5 +198,5 @@ export function logResponse(toolName, response, durationMs) {
85
198
  JSON.stringify(response, null, 2),
86
199
  ].join('\n');
87
200
  const title = `LLM RESPONSE (${String(durationMs)} ms)`;
88
- return createDebug({ title, content, color: Palette.AshGray });
201
+ return createDebug({ title, content, color: Palette.LightGray });
89
202
  }
@@ -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,105 +227,90 @@ 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);
179
231
  }
180
232
  /**
181
233
  * Task types that should appear in the upcoming display
182
234
  */
183
- const UPCOMING_TASK_TYPES = [TaskType.Execute, TaskType.Answer];
235
+ const UPCOMING_TASK_TYPES = [TaskType.Execute, TaskType.Answer, TaskType.Group];
184
236
  /**
185
- * Collect names of all upcoming execution units (groups and standalone tasks)
186
- * for display during task execution
237
+ * Collect action names for tasks that appear in upcoming display.
238
+ * Groups are included with their group name (not individual subtask names).
187
239
  */
188
- function collectUpcomingNames(scheduledTasks) {
189
- const names = [];
190
- for (const task of scheduledTasks) {
191
- if (task.type === TaskType.Group && task.subtasks?.length) {
192
- const subtasks = task.subtasks;
193
- if (UPCOMING_TASK_TYPES.includes(subtasks[0].type)) {
194
- names.push(task.action);
195
- }
196
- }
197
- else if (UPCOMING_TASK_TYPES.includes(task.type)) {
198
- names.push(task.action);
199
- }
200
- }
201
- return names;
240
+ function collectUpcomingNames(tasks) {
241
+ return tasks
242
+ .filter((t) => UPCOMING_TASK_TYPES.includes(t.type))
243
+ .map((t) => t.action);
202
244
  }
203
245
  /**
204
246
  * Route tasks after config is complete (or when no config is needed)
205
- * 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.
206
250
  */
207
- function routeTasksAfterConfig(scheduledTasks, context) {
208
- // Collect all unit names for upcoming display
209
- const allUnitNames = collectUpcomingNames(scheduledTasks);
210
- let currentUnitIndex = 0;
211
- // Process tasks in order, preserving Group boundaries
212
- // Track consecutive standalone tasks to group them by type
213
- let consecutiveStandaloneTasks = [];
214
- const processStandaloneTasks = () => {
215
- if (consecutiveStandaloneTasks.length === 0)
216
- return;
217
- // Group consecutive standalone tasks by type
218
- const tasksByType = {};
219
- for (const type of Object.values(TaskType)) {
220
- tasksByType[type] = [];
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
+ }
221
269
  }
222
- for (const task of consecutiveStandaloneTasks) {
223
- tasksByType[task.type].push(task);
270
+ if (typeTasks.length > 0) {
271
+ routeTasksByType(groupedType, typeTasks, context, []);
224
272
  }
225
- // Route each type group
226
- for (const [type, typeTasks] of Object.entries(tasksByType)) {
227
- const taskType = type;
228
- if (typeTasks.length === 0)
229
- continue;
230
- // For tasks that appear in upcoming, calculate from remaining units
231
- if (UPCOMING_TASK_TYPES.includes(taskType)) {
232
- // Each task advances the unit index
233
- for (const task of typeTasks) {
234
- const upcoming = allUnitNames.slice(currentUnitIndex + 1);
235
- currentUnitIndex++;
236
- routeTasksByType(taskType, [task], context, upcoming);
237
- }
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);
238
291
  }
239
- else {
240
- routeTasksByType(taskType, typeTasks, context, []);
292
+ // Route Answer subtasks individually
293
+ if (answerSubtasks.length > 0) {
294
+ routeAnswerTasks(answerSubtasks, context, upcoming);
241
295
  }
242
296
  }
243
- consecutiveStandaloneTasks = [];
244
- };
245
- // Process tasks in original order
246
- for (const task of scheduledTasks) {
247
- if (task.type === TaskType.Group && task.subtasks) {
248
- // Process any accumulated standalone tasks first
249
- processStandaloneTasks();
250
- // Process Group as separate component
251
- if (task.subtasks.length > 0) {
252
- const subtasks = task.subtasks;
253
- const taskType = subtasks[0].type;
254
- // Calculate upcoming (all units after this one)
255
- const upcoming = UPCOMING_TASK_TYPES.includes(taskType)
256
- ? allUnitNames.slice(currentUnitIndex + 1)
257
- : [];
258
- if (UPCOMING_TASK_TYPES.includes(taskType)) {
259
- currentUnitIndex++;
260
- }
261
- // Pass group name as label for Execute groups
262
- if (taskType === TaskType.Execute) {
263
- routeExecuteTasks(subtasks, context, upcoming, task.action);
264
- }
265
- else {
266
- routeTasksByType(taskType, subtasks, context, upcoming);
267
- }
268
- }
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);
269
308
  }
270
309
  else {
271
- // Accumulate standalone task
272
- consecutiveStandaloneTasks.push(task);
310
+ // For other types (Report, etc.), route without upcoming
311
+ routeTasksByType(taskType, [task], context, []);
273
312
  }
274
313
  }
275
- // Process any remaining standalone tasks
276
- processStandaloneTasks();
277
314
  }
278
315
  /**
279
316
  * Route Answer tasks - creates separate Answer component for each question
@@ -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
@@ -14,6 +14,15 @@ behavior must adapt accordingly:
14
14
  - **Skills present**: Match user requests ONLY against listed skills
15
15
  - **Empty or missing**: Create "ignore" tasks for ALL action verbs
16
16
 
17
+ **CRITICAL - Available Skills Section Takes Precedence**:
18
+
19
+ Information provided in the "Available Skills" section is AUTHORITATIVE
20
+ and OVERRIDES any default behavior in these instructions. When a skill's
21
+ description specifies required parameters, error handling, or specific
22
+ behaviors, those requirements MUST be followed exactly. Skill-specific
23
+ rules always take precedence over general examples or default patterns
24
+ in this document.
25
+
17
26
  All examples in these instructions (e.g., "build", "deploy", "process")
18
27
  are for illustration only. They do NOT represent actual available
19
28
  skills unless they appear in the "Available Skills" section of the
@@ -268,6 +277,152 @@ User request with multiple config expressions
268
277
  - This applies to ALL placeholders in task actions, including those
269
278
  from skill references
270
279
 
280
+ ## Runtime Parameter Placeholders
281
+
282
+ Skills may include runtime parameters in their Execution section using
283
+ angle bracket syntax. These parameters MUST be resolved by the LLM
284
+ during scheduling - they represent values extracted from the user's
285
+ command, NOT from stored configuration.
286
+
287
+ **Parameter Format:**
288
+
289
+ - `<PARAM>` - Required parameter, extract from user command
290
+ - `<PARAM=default>` - Parameter with default, use default if not specified
291
+ - `<PARAM?>` - Optional parameter, omit entirely if not mentioned
292
+
293
+ **Distinction from Config Placeholders:**
294
+
295
+ - `{x.y.z}` - Config placeholder, resolved by system at execution from
296
+ ~/.plsrc
297
+ - `{x.VARIANT.z}` - Variant config, LLM matches variant at schedule time,
298
+ system resolves from ~/.plsrc at execution
299
+ - `<PARAM>` - Runtime parameter, resolved entirely by LLM at schedule time
300
+ from user command
301
+
302
+ **Resolution Rules:**
303
+
304
+ 1. **Full resolution required**: All `<PARAM>` placeholders MUST be
305
+ resolved to concrete values before creating tasks. No angle-bracket
306
+ syntax should remain in task actions or params.
307
+
308
+ 2. **Space normalization**: When optional params are omitted, collapse
309
+ adjacent spaces to single space (e.g., `cmd <OPT?> file` → `cmd file`)
310
+
311
+ 3. **Complete descriptions**: Task actions must be human-readable with
312
+ all parameters filled in:
313
+ - CORRECT: "Process /data/report.csv in batch mode with JSON output"
314
+ - WRONG: "Process <SOURCE> in <MODE> mode"
315
+
316
+ **Parameter Classification:**
317
+
318
+ Runtime parameters fall into two categories:
319
+
320
+ 1. **Key parameters** - Essential to the operation, define WHAT to operate on
321
+ - Input files, paths, URLs, target names, identifiers
322
+ - The primary subject of the command
323
+ - Cannot be guessed or listed as options
324
+ - Examples: `<SOURCE>`, `<FILE>`, `<URL>`, `<TARGET>`
325
+
326
+ 2. **Modifier parameters** - Configure HOW the operation runs
327
+ - Have a finite set of valid options
328
+ - Affect behavior but not the primary subject
329
+ - Examples: `<MODE>`, `<QUALITY>`, `<FORMAT>`, `<VERBOSITY>`
330
+
331
+ **Resolution Outcomes:**
332
+
333
+ When processing runtime parameters, exactly ONE of these outcomes applies.
334
+ **CRITICAL: Evaluate in this EXACT order - key param check MUST happen first:**
335
+
336
+ 1. **Key param missing** → Create IGNORE task (CHECK THIS FIRST!)
337
+ - **PREREQUISITE CHECK**: Before considering ANY other outcome, verify
338
+ ALL key parameters (input files, paths, URLs, targets) are present
339
+ - A key parameter is not specified → ALWAYS create IGNORE task
340
+ - **NEVER create a DEFINE task when key params are missing**, even if
341
+ modifier params could be listed as options
342
+ - NEVER offer options for key parameters - they cannot be guessed
343
+ - Use type `ignore` with descriptive action
344
+ - Action format: "Missing [param]: specify [what's needed]"
345
+ - Examples:
346
+ - "Missing input: specify which file to process"
347
+ - "Missing target: specify which server to deploy to"
348
+ - "Missing URL: specify which page to fetch"
349
+
350
+ 2. **All resolved** → Create normal execute/group task
351
+ - All key parameters are present AND extracted successfully
352
+ - All modifier parameters are extracted or defaulted
353
+ - Task action contains fully resolved description
354
+
355
+ 3. **Modifier param unclear (ALL key params present)** → Create DEFINE task
356
+ - **PREREQUISITE**: ALL key parameters MUST be present in user's command
357
+ - Only a modifier parameter is unclear but has finite options
358
+ - **NEVER use DEFINE when ANY key param is missing** - use IGNORE instead
359
+ - Use type `define` with params.skill and params.options
360
+ - MUST have more than one option (if only one option exists, use it
361
+ directly without refinement)
362
+ - Example: mode (batch/stream/interactive), format (json/xml/csv)
363
+ - Each option is an object: { name: string, command: string }
364
+ - name: readable display text for user selection
365
+ - command: user's natural language command with ALL params resolved
366
+ - Note: command is NOT the shell command - shell commands are generated
367
+ by EXECUTE in the next step
368
+
369
+ **Examples:**
370
+
371
+ Skill execution line:
372
+ - `process <SOURCE> --mode <MODE> --format <FORMAT=json> <VERBOSE?>`
373
+
374
+ Key param missing case (CHECK FIRST):
375
+ - User: "process in batch mode"
376
+ - Problem: SOURCE path not specified (key param, cannot be guessed)
377
+ - Task: type `ignore`, action: "Missing source: specify which file to process"
378
+
379
+ Key param missing with modifier specified:
380
+ - User: "export in JSON format"
381
+ - Problem: SOURCE file not specified (key param, cannot be guessed)
382
+ - Task: type `ignore`, action: "Missing source: specify which data to export"
383
+ - Note: Even though format IS specified, key param is missing → IGNORE, not
384
+ DEFINE. Key param check takes absolute precedence over DEFINE.
385
+
386
+ Success case (all resolved):
387
+ - User: "process /data/report.csv in batch mode"
388
+ - Resolution:
389
+ - `<SOURCE>` → `/data/report.csv` (extracted)
390
+ - `<MODE>` → `batch` (extracted from "batch mode")
391
+ - `<FORMAT=json>` → `json` (default used)
392
+ - `<VERBOSE?>` → omitted (optional, not mentioned)
393
+ - Task action: "Process /data/report.csv in batch mode with JSON format"
394
+
395
+ Define case (modifier unclear, ALL key params present):
396
+ - User: "process /data/report.csv"
397
+ - Key params: SOURCE is present (/data/report.csv) ✓
398
+ - Problem: MODE not specified but can be listed (3 options available)
399
+ - Task: type `define`, params:
400
+ - skill: "Process Data"
401
+ - options:
402
+ - { name: "Process in batch mode",
403
+ command: "process /data/report.csv in batch mode" }
404
+ - { name: "Process in stream mode",
405
+ command: "process /data/report.csv in stream mode" }
406
+ - { name: "Process interactively",
407
+ command: "process /data/report.csv interactively" }
408
+ - User selects "Process in batch mode"
409
+ - SCHEDULE receives: "process /data/report.csv in batch mode"
410
+ - EXECUTE then generates the appropriate shell command
411
+
412
+ **Critical Rules:**
413
+ - **KEY PARAM CHECK IS MANDATORY AND FIRST**: Before creating ANY task type,
414
+ verify ALL key parameters are present. This check takes absolute precedence.
415
+ - IGNORE when ANY key param is missing (input, file, URL, target, etc.)
416
+ - Key params cannot be guessed - always require IGNORE with clear error
417
+ - **DEFINE is ONLY valid when ALL key params are present** - if any key param
418
+ is missing, DEFINE is NOT an option, regardless of modifier params
419
+ - DEFINE tasks MUST have multiple options (2+); single option = use directly
420
+ - NEVER leave `<PARAM>` unresolved in task output
421
+ - NEVER use placeholder values like `<UNKNOWN>` or `<MISSING>`
422
+ - option.command is user's natural language request, NOT shell command
423
+ - Each option.command must include ALL user parameters (original + selected)
424
+ - option.command must preserve exact paths, filenames, URLs (case-sensitive)
425
+
271
426
  ## Grouping Strategy
272
427
 
273
428
  Group subtasks under logical parent tasks based on:
@@ -31,7 +31,7 @@ export const scheduleTool = {
31
31
  },
32
32
  params: {
33
33
  type: 'object',
34
- description: 'Parameters for leaf tasks (e.g., command, path)',
34
+ description: 'Parameters for leaf tasks. For "define" type: { skill: string, options: Array<{ name: string, command: string }> }. "name" is display text, "command" is full resolved command.',
35
35
  },
36
36
  config: {
37
37
  type: 'array',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prompt-language-shell",
3
- "version": "0.9.4",
3
+ "version": "0.9.6",
4
4
  "description": "Your personal command-line concierge. Ask politely, and it gets things done.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -17,9 +17,9 @@
17
17
  "dev": "npm run build && tsc --watch",
18
18
  "prepare": "husky",
19
19
  "prepublishOnly": "npm run check",
20
- "test": "vitest run --exclude 'tests/integration/*.test.tsx'",
21
- "test:watch": "vitest --exclude 'tests/integration/*.test.tsx'",
22
- "test:llm": "vitest run tests/integration/schedule*.test.tsx",
20
+ "test": "vitest run --exclude 'tests/tools/schedule/*.test.tsx'",
21
+ "test:watch": "vitest --exclude 'tests/tools/schedule/*.test.tsx'",
22
+ "test:llm": "vitest run tests/tools/schedule/*.test.tsx",
23
23
  "format": "prettier --write '**/*.{ts,tsx}'",
24
24
  "format:check": "prettier --check '**/*.{ts,tsx}'",
25
25
  "lint": "eslint .",