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.
- package/dist/components/Workflow.js +12 -1
- package/dist/components/controllers/Schedule.js +6 -11
- package/dist/components/views/Debug.js +6 -1
- package/dist/components/views/Feedback.js +2 -13
- package/dist/components/views/Schedule.js +4 -3
- package/dist/components/views/Table.js +15 -0
- package/dist/services/anthropic.js +27 -31
- package/dist/services/colors.js +2 -1
- package/dist/services/logger.js +126 -13
- package/dist/services/parser.js +13 -5
- package/dist/services/refinement.js +8 -2
- package/dist/services/router.js +159 -122
- package/dist/services/skills.js +35 -7
- package/dist/skills/schedule.md +155 -0
- package/dist/tools/schedule.tool.js +1 -1
- package/package.json +4 -4
|
@@ -107,7 +107,18 @@ export const Workflow = ({ initialQueue, debug }) => {
|
|
|
107
107
|
setQueue((queue) => [...queue, ...items]);
|
|
108
108
|
},
|
|
109
109
|
addToTimeline: (...items) => {
|
|
110
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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,
|
|
110
|
+
const promptDebug = logPrompt(toolName, command, baseInstructions, formattedSkills, skillDefinitions);
|
|
115
111
|
if (promptDebug) {
|
|
116
112
|
debug.push(promptDebug);
|
|
117
113
|
}
|
package/dist/services/colors.js
CHANGED
|
@@ -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.
|
|
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,
|
package/dist/services/logger.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
63
|
-
|
|
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.
|
|
201
|
+
return createDebug({ title, content, color: Palette.LightGray });
|
|
89
202
|
}
|
package/dist/services/parser.js
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
import { displayWarning } from './logger.js';
|
|
3
3
|
/**
|
|
4
|
-
* Validate
|
|
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
|
-
|
|
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
|
|
68
|
-
const validationError =
|
|
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.
|
|
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) {
|
package/dist/services/router.js
CHANGED
|
@@ -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,
|
|
9
|
-
import { getCancellationMessage, getConfirmationMessage,
|
|
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
|
-
|
|
35
|
-
|
|
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
|
|
84
|
-
*
|
|
152
|
+
* Validate task structure after flattening.
|
|
153
|
+
* Currently no-op since flattening removes Groups and mixed types are allowed.
|
|
85
154
|
*/
|
|
86
|
-
function validateTaskTypes(
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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(
|
|
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
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
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
|
-
|
|
180
|
+
executeTasks.push(task);
|
|
126
181
|
}
|
|
127
182
|
else if (task.type === TaskType.Group && task.subtasks) {
|
|
128
|
-
|
|
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
|
|
135
|
-
if (
|
|
186
|
+
// Validate Execute tasks to collect missing config upfront
|
|
187
|
+
if (executeTasks.length > 0) {
|
|
136
188
|
try {
|
|
137
|
-
const validation = validateExecuteTasks(
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
186
|
-
*
|
|
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(
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
|
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(
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
223
|
-
|
|
270
|
+
if (typeTasks.length > 0) {
|
|
271
|
+
routeTasksByType(groupedType, typeTasks, context, []);
|
|
224
272
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
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
|
-
|
|
240
|
-
|
|
292
|
+
// Route Answer subtasks individually
|
|
293
|
+
if (answerSubtasks.length > 0) {
|
|
294
|
+
routeAnswerTasks(answerSubtasks, context, upcoming);
|
|
241
295
|
}
|
|
242
296
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
//
|
|
272
|
-
|
|
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
|
package/dist/services/skills.js
CHANGED
|
@@ -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
|
|
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
|
|
152
|
-
|
|
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
|
package/dist/skills/schedule.md
CHANGED
|
@@ -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
|
|
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.
|
|
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/
|
|
21
|
-
"test:watch": "vitest --exclude 'tests/
|
|
22
|
-
"test:llm": "vitest run tests/
|
|
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 .",
|