prompt-language-shell 0.6.2 → 0.6.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/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Your personal command-line concierge. Ask politely, and it gets things done.
4
4
 
5
- > **Note:** This project is in early preview. Features and APIs may change as
6
- > development continues.
5
+ > **Note:** This project is in early preview. Features and APIs will change.
6
+ > See [roadmap](#roadmap).
7
7
 
8
8
  ## Installation
9
9
 
@@ -13,14 +13,14 @@ npm install -g prompt-language-shell
13
13
 
14
14
  ## Setup
15
15
 
16
- On first run, `pls` walks you through a quick setup. Your settings will be saved to `~/.plsrc`.
16
+ On first run, `pls` walks you through a quick setup.
17
+ Your settings will be saved to `~/.plsrc`.
17
18
 
18
19
  ## Usage
19
20
 
20
21
  Type `pls` followed by your request in natural language.
21
22
 
22
- To see what `pls` can
23
- do, start by listing available capabilities:
23
+ To see what `pls` can do, start by listing available capabilities:
24
24
 
25
25
  ```
26
26
  $ pls list skills
@@ -98,6 +98,8 @@ Skills let you teach `pls` about your project-specific workflows. Create
98
98
  markdown files in `~/.pls/skills/` to define custom operations that `pls` can
99
99
  understand and execute.
100
100
 
101
+ For complete documentation, see [docs/SKILLS.md](./docs/SKILLS.md).
102
+
101
103
  ### Structure
102
104
 
103
105
  Each skill file uses a simple markdown format:
@@ -155,6 +157,13 @@ $ pls build dev
155
157
  $ pls build test
156
158
  ```
157
159
 
160
+ ## Roadmap
161
+
162
+ - **0.7** - Comprehend skill, simplified prompts, better debugging
163
+ - **0.8** - Sequential and interlaced skill execution
164
+ - **0.9** - Learn skill, codebase refinement, complex dependency handling
165
+ - **1.0** - Production release
166
+
158
167
  ## Development
159
168
 
160
169
  See [CLAUDE.md](./CLAUDE.md) for development guidelines and architecture.
@@ -88,6 +88,8 @@ These MUST appear AFTER Execute and BEFORE user skills:
88
88
  If skills are provided in the "Available Skills" section below, include them
89
89
  in the response. For each skill:
90
90
  - Extract the skill name from the first heading (# Skill Name)
91
+ - If the skill name contains "(INCOMPLETE)", preserve it exactly in the task
92
+ action
91
93
  - Extract a brief description from the Description or Overview section
92
94
  - Keep descriptions concise (1-2 lines maximum)
93
95
  - If the user specified a filter (e.g., "skills for deployment"), only include
@@ -1,6 +1,6 @@
1
1
  import Anthropic from '@anthropic-ai/sdk';
2
2
  import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
3
- import { formatSkillsForPrompt, loadSkills } from './skills.js';
3
+ import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
4
4
  import { toolRegistry } from './tool-registry.js';
5
5
  /**
6
6
  * Wraps text to ensure no line exceeds the specified width.
@@ -62,7 +62,7 @@ export class AnthropicService {
62
62
  toolName === 'introspect' ||
63
63
  toolName === 'execute' ||
64
64
  toolName === 'validate') {
65
- const skills = loadSkills();
65
+ const skills = loadSkillsWithValidation();
66
66
  const skillsSection = formatSkillsForPrompt(skills);
67
67
  systemPrompt += skillsSection;
68
68
  }
@@ -1,8 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { existsSync, readFileSync } from 'node:fs';
3
3
  import { ComponentName } from '../types/types.js';
4
+ import { ComponentStatus, } from '../types/components.js';
4
5
  import { parse as parseYaml } from 'yaml';
5
- import { getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
6
+ import { ConfigDefinitionType, getConfigPath, getConfigSchema, loadConfig, } from './configuration.js';
6
7
  import { getConfirmationMessage } from './messages.js';
7
8
  import { StepType } from '../ui/Config.js';
8
9
  export function createWelcomeDefinition(app) {
@@ -39,15 +40,15 @@ function getConfigValue(config, key) {
39
40
  */
40
41
  function getValidator(definition) {
41
42
  switch (definition.type) {
42
- case 'regexp':
43
+ case ConfigDefinitionType.RegExp:
43
44
  return (value) => definition.pattern.test(value);
44
- case 'string':
45
+ case ConfigDefinitionType.String:
45
46
  return () => true; // Strings are always valid
46
- case 'enum':
47
+ case ConfigDefinitionType.Enum:
47
48
  return (value) => definition.values.includes(value);
48
- case 'number':
49
+ case ConfigDefinitionType.Number:
49
50
  return (value) => !isNaN(Number(value));
50
- case 'boolean':
51
+ case ConfigDefinitionType.Boolean:
51
52
  return (value) => value === 'true' || value === 'false';
52
53
  }
53
54
  }
@@ -103,11 +104,11 @@ export function createConfigStepsFromSchema(keys) {
103
104
  const shortKey = keyParts[keyParts.length - 1];
104
105
  // Map definition to ConfigStep based on type
105
106
  switch (definition.type) {
106
- case 'regexp':
107
- case 'string': {
107
+ case ConfigDefinitionType.RegExp:
108
+ case ConfigDefinitionType.String: {
108
109
  const value = currentValue !== undefined && typeof currentValue === 'string'
109
110
  ? currentValue
110
- : definition.type === 'string'
111
+ : definition.type === ConfigDefinitionType.String
111
112
  ? (definition.default ?? '')
112
113
  : null;
113
114
  return {
@@ -119,7 +120,7 @@ export function createConfigStepsFromSchema(keys) {
119
120
  validate: getValidator(definition),
120
121
  };
121
122
  }
122
- case 'number': {
123
+ case ConfigDefinitionType.Number: {
123
124
  const value = currentValue !== undefined && typeof currentValue === 'number'
124
125
  ? String(currentValue)
125
126
  : definition.default !== undefined
@@ -134,7 +135,7 @@ export function createConfigStepsFromSchema(keys) {
134
135
  validate: getValidator(definition),
135
136
  };
136
137
  }
137
- case 'enum': {
138
+ case ConfigDefinitionType.Enum: {
138
139
  const currentStr = currentValue !== undefined && typeof currentValue === 'string'
139
140
  ? currentValue
140
141
  : definition.default;
@@ -154,7 +155,7 @@ export function createConfigStepsFromSchema(keys) {
154
155
  validate: getValidator(definition),
155
156
  };
156
157
  }
157
- case 'boolean': {
158
+ case ConfigDefinitionType.Boolean: {
158
159
  const currentBool = currentValue !== undefined && typeof currentValue === 'boolean'
159
160
  ? currentValue
160
161
  : undefined;
@@ -178,6 +179,7 @@ export function createConfigDefinition(onFinished, onAborted) {
178
179
  return {
179
180
  id: randomUUID(),
180
181
  name: ComponentName.Config,
182
+ status: ComponentStatus.Awaiting,
181
183
  state: {},
182
184
  props: {
183
185
  steps: createConfigSteps(),
@@ -193,6 +195,7 @@ export function createConfigDefinitionWithKeys(keys, onFinished, onAborted) {
193
195
  return {
194
196
  id: randomUUID(),
195
197
  name: ComponentName.Config,
198
+ status: ComponentStatus.Awaiting,
196
199
  state: {},
197
200
  props: {
198
201
  steps: createConfigStepsFromSchema(keys),
@@ -205,6 +208,7 @@ export function createCommandDefinition(command, service) {
205
208
  return {
206
209
  id: randomUUID(),
207
210
  name: ComponentName.Command,
211
+ status: ComponentStatus.Awaiting,
208
212
  state: {},
209
213
  props: {
210
214
  command,
@@ -216,6 +220,7 @@ export function createPlanDefinition(message, tasks, onSelectionConfirmed) {
216
220
  return {
217
221
  id: randomUUID(),
218
222
  name: ComponentName.Plan,
223
+ status: ComponentStatus.Awaiting,
219
224
  state: {
220
225
  highlightedIndex: null,
221
226
  currentDefineGroupIndex: 0,
@@ -251,6 +256,7 @@ export function createRefinement(text, onAborted) {
251
256
  return {
252
257
  id: randomUUID(),
253
258
  name: ComponentName.Refinement,
259
+ status: ComponentStatus.Awaiting,
254
260
  state: {},
255
261
  props: {
256
262
  text,
@@ -262,6 +268,7 @@ export function createConfirmDefinition(onConfirmed, onCancelled) {
262
268
  return {
263
269
  id: randomUUID(),
264
270
  name: ComponentName.Confirm,
271
+ status: ComponentStatus.Awaiting,
265
272
  state: {},
266
273
  props: {
267
274
  message: getConfirmationMessage(),
@@ -274,6 +281,7 @@ export function createIntrospectDefinition(tasks, service) {
274
281
  return {
275
282
  id: randomUUID(),
276
283
  name: ComponentName.Introspect,
284
+ status: ComponentStatus.Awaiting,
277
285
  state: {},
278
286
  props: {
279
287
  tasks,
@@ -295,6 +303,7 @@ export function createAnswerDefinition(question, service) {
295
303
  return {
296
304
  id: randomUUID(),
297
305
  name: ComponentName.Answer,
306
+ status: ComponentStatus.Awaiting,
298
307
  state: {},
299
308
  props: {
300
309
  question,
@@ -308,16 +317,16 @@ export function isStateless(component) {
308
317
  /**
309
318
  * Mark a component as done. Returns the component to be added to timeline.
310
319
  * Components use handlers.updateState to save their state before completion,
311
- * so this function simply returns the component as-is.
320
+ * so this function sets the status to Done and returns the updated component.
312
321
  */
313
322
  export function markAsDone(component) {
314
- // State already updated via handlers.updateState
315
- return component;
323
+ return { ...component, status: ComponentStatus.Done };
316
324
  }
317
325
  export function createExecuteDefinition(tasks, service) {
318
326
  return {
319
327
  id: randomUUID(),
320
328
  name: ComponentName.Execute,
329
+ status: ComponentStatus.Awaiting,
321
330
  state: {},
322
331
  props: {
323
332
  tasks,
@@ -329,6 +338,7 @@ export function createValidateDefinition(missingConfig, userRequest, service, on
329
338
  return {
330
339
  id: randomUUID(),
331
340
  name: ComponentName.Validate,
341
+ status: ComponentStatus.Awaiting,
332
342
  state: {},
333
343
  props: {
334
344
  missingConfig,
@@ -9,6 +9,14 @@ export var AnthropicModel;
9
9
  AnthropicModel["Opus"] = "claude-opus-4-1";
10
10
  })(AnthropicModel || (AnthropicModel = {}));
11
11
  export const SUPPORTED_MODELS = Object.values(AnthropicModel);
12
+ export var ConfigDefinitionType;
13
+ (function (ConfigDefinitionType) {
14
+ ConfigDefinitionType["RegExp"] = "regexp";
15
+ ConfigDefinitionType["String"] = "string";
16
+ ConfigDefinitionType["Enum"] = "enum";
17
+ ConfigDefinitionType["Number"] = "number";
18
+ ConfigDefinitionType["Boolean"] = "boolean";
19
+ })(ConfigDefinitionType || (ConfigDefinitionType = {}));
12
20
  export class ConfigError extends Error {
13
21
  origin;
14
22
  constructor(message, origin) {
@@ -172,20 +180,20 @@ export function getConfigurationRequiredMessage(forFutureUse = false) {
172
180
  */
173
181
  const coreConfigSchema = {
174
182
  'anthropic.key': {
175
- type: 'regexp',
183
+ type: ConfigDefinitionType.RegExp,
176
184
  required: true,
177
185
  pattern: /^sk-ant-api03-[A-Za-z0-9_-]{95}$/,
178
186
  description: 'Anthropic API key',
179
187
  },
180
188
  'anthropic.model': {
181
- type: 'enum',
189
+ type: ConfigDefinitionType.Enum,
182
190
  required: true,
183
191
  values: SUPPORTED_MODELS,
184
192
  default: AnthropicModel.Haiku,
185
193
  description: 'Anthropic model',
186
194
  },
187
195
  'settings.debug': {
188
- type: 'boolean',
196
+ type: ConfigDefinitionType.Boolean,
189
197
  required: false,
190
198
  description: 'Debug mode',
191
199
  },
@@ -239,20 +247,20 @@ export function getMissingConfigKeys() {
239
247
  // Validate based on type
240
248
  let isValid = false;
241
249
  switch (definition.type) {
242
- case 'regexp':
250
+ case ConfigDefinitionType.RegExp:
243
251
  isValid = typeof value === 'string' && definition.pattern.test(value);
244
252
  break;
245
- case 'string':
253
+ case ConfigDefinitionType.String:
246
254
  isValid = typeof value === 'string';
247
255
  break;
248
- case 'enum':
256
+ case ConfigDefinitionType.Enum:
249
257
  isValid =
250
258
  typeof value === 'string' && definition.values.includes(value);
251
259
  break;
252
- case 'number':
260
+ case ConfigDefinitionType.Number:
253
261
  isValid = typeof value === 'number';
254
262
  break;
255
- case 'boolean':
263
+ case ConfigDefinitionType.Boolean:
256
264
  isValid = typeof value === 'boolean';
257
265
  break;
258
266
  }
@@ -357,13 +365,13 @@ function parseConfigValue(key, stringValue, schema) {
357
365
  if (key in schema) {
358
366
  const definition = schema[key];
359
367
  switch (definition.type) {
360
- case 'boolean':
368
+ case ConfigDefinitionType.Boolean:
361
369
  return stringValue === 'true';
362
- case 'number':
370
+ case ConfigDefinitionType.Number:
363
371
  return Number(stringValue);
364
- case 'string':
365
- case 'regexp':
366
- case 'enum':
372
+ case ConfigDefinitionType.String:
373
+ case ConfigDefinitionType.RegExp:
374
+ case ConfigDefinitionType.Enum:
367
375
  return stringValue;
368
376
  }
369
377
  }
@@ -5,25 +5,47 @@ import { expandSkillReferences } from './skill-expander.js';
5
5
  import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
6
6
  /**
7
7
  * Validate config requirements for execute tasks
8
- * Returns missing config requirements
8
+ * Returns validation result with missing config and validation errors
9
9
  */
10
10
  export function validateExecuteTasks(tasks) {
11
11
  const userConfig = loadUserConfig();
12
12
  const missing = [];
13
13
  const seenPaths = new Set();
14
- // Load and parse all skills for validation
14
+ const validationErrors = [];
15
+ const seenSkills = new Set();
16
+ // Load all skills (including invalid ones for validation)
15
17
  const skillContents = loadSkills();
16
- const parsedSkills = skillContents
17
- .map((content) => parseSkillMarkdown(content))
18
- .filter((s) => s !== null);
18
+ const parsedSkills = skillContents.map((content) => parseSkillMarkdown(content));
19
19
  const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
20
+ // Check for invalid skills being used in tasks
21
+ for (const task of tasks) {
22
+ const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
23
+ if (skillName && !seenSkills.has(skillName)) {
24
+ seenSkills.add(skillName);
25
+ // Check if this skill is invalid
26
+ const skill = skillLookup(skillName);
27
+ if (skill && !skill.isValid) {
28
+ validationErrors.push({
29
+ skill: skill.name,
30
+ issues: [skill.validationError || 'Unknown validation error'],
31
+ });
32
+ }
33
+ }
34
+ }
35
+ // If there are validation errors, return early
36
+ if (validationErrors.length > 0) {
37
+ return {
38
+ missingConfig: [],
39
+ validationErrors,
40
+ };
41
+ }
20
42
  for (const task of tasks) {
21
43
  // Check if task originates from a skill
22
44
  const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
23
45
  if (skillName) {
24
46
  // Task comes from a skill - check skill's Execution section
25
47
  const skill = skillLookup(skillName);
26
- if (!skill || !skill.execution) {
48
+ if (!skill) {
27
49
  continue;
28
50
  }
29
51
  // Get variant from task params (if any)
@@ -106,5 +128,8 @@ export function validateExecuteTasks(tasks) {
106
128
  }
107
129
  }
108
130
  }
109
- return missing;
131
+ return {
132
+ missingConfig: missing,
133
+ validationErrors: [],
134
+ };
110
135
  }
@@ -25,8 +25,7 @@ export async function handleRefinement(selectedTasks, service, originalCommand,
25
25
  // Complete the Refinement component
26
26
  handlers.completeActive();
27
27
  // Route refined tasks to appropriate components
28
- routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false, // No DEFINE tasks in refined result
29
- undefined // No commandComponent - use normal flow
28
+ routeTasksWithConfirm(refinedResult.tasks, refinedResult.message, service, originalCommand, handlers, false // No DEFINE tasks in refined result
30
29
  );
31
30
  }
32
31
  catch (err) {
@@ -37,10 +37,6 @@ export function expandSkillReferences(execution, skillLookup, visited = new Set(
37
37
  expanded.push(line);
38
38
  continue;
39
39
  }
40
- if (!skill.execution || skill.execution.length === 0) {
41
- // Referenced skill has no execution, skip
42
- continue;
43
- }
44
40
  // Recursively expand referenced skill's execution
45
41
  const newVisited = new Set(visited);
46
42
  newVisited.add(skillName);
@@ -62,7 +58,7 @@ export function getReferencedSkills(execution, skillLookup, visited = new Set())
62
58
  }
63
59
  referenced.add(skillName);
64
60
  const skill = skillLookup(skillName);
65
- if (skill && skill.execution) {
61
+ if (skill) {
66
62
  const newVisited = new Set(visited);
67
63
  newVisited.add(skillName);
68
64
  const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
@@ -1,40 +1,85 @@
1
1
  import YAML from 'yaml';
2
2
  /**
3
- * Parse a skill markdown file into structured definition
3
+ * Validate a skill without parsing it fully
4
+ * Returns validation error if skill is invalid, null if valid
4
5
  */
5
- export function parseSkillMarkdown(content) {
6
+ export function validateSkill(content) {
6
7
  const sections = extractSections(content);
7
- // Name is required
8
+ // Name is required for error reporting
9
+ const skillName = sections.name || 'Unknown skill';
10
+ // Check required sections
8
11
  if (!sections.name) {
9
- return null;
12
+ return {
13
+ skillName,
14
+ error: 'The skill file is missing a Name section',
15
+ };
10
16
  }
11
- // Description is required
12
17
  if (!sections.description) {
13
- return null;
18
+ return {
19
+ skillName,
20
+ error: 'The skill file is missing a Description section',
21
+ };
14
22
  }
15
- // Steps are required
16
23
  if (!sections.steps || sections.steps.length === 0) {
17
- return null;
18
- }
19
- // Validate execution and steps have same count (if execution exists)
20
- if (sections.execution &&
21
- sections.execution.length !== sections.steps.length) {
22
- return null;
23
- }
24
+ return {
25
+ skillName,
26
+ error: 'The skill file is missing a Steps section',
27
+ };
28
+ }
29
+ if (!sections.execution || sections.execution.length === 0) {
30
+ return {
31
+ skillName,
32
+ error: 'The skill file is missing an Execution section',
33
+ };
34
+ }
35
+ // Execution and steps must have same count
36
+ if (sections.execution.length !== sections.steps.length) {
37
+ return {
38
+ skillName,
39
+ error: `The skill has ${String(sections.steps.length)} steps but ${String(sections.execution.length)} execution lines`,
40
+ };
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Parse a skill markdown file into structured definition
46
+ */
47
+ export function parseSkillMarkdown(content) {
48
+ const sections = extractSections(content);
49
+ // Validate the skill
50
+ const validationError = validateSkill(content);
51
+ // For invalid skills, return minimal definition with error
52
+ if (validationError) {
53
+ return {
54
+ name: sections.name || 'Unknown skill',
55
+ description: sections.description || '',
56
+ steps: sections.steps || [],
57
+ execution: sections.execution || [],
58
+ isValid: false,
59
+ validationError: validationError.error,
60
+ isIncomplete: true,
61
+ };
62
+ }
63
+ // Valid skill - all required fields are present (validation passed)
64
+ const description = sections.description;
24
65
  const skill = {
25
66
  name: sections.name,
26
- description: sections.description,
67
+ description,
27
68
  steps: sections.steps,
69
+ execution: sections.execution,
70
+ isValid: true,
28
71
  };
72
+ // Check if skill is incomplete (valid but needs more documentation)
73
+ const MIN_DESCRIPTION_LENGTH = 20;
74
+ if (description.trim().length < MIN_DESCRIPTION_LENGTH) {
75
+ skill.isIncomplete = true;
76
+ }
29
77
  if (sections.aliases && sections.aliases.length > 0) {
30
78
  skill.aliases = sections.aliases;
31
79
  }
32
80
  if (sections.config) {
33
81
  skill.config = sections.config;
34
82
  }
35
- if (sections.execution && sections.execution.length > 0) {
36
- skill.execution = sections.execution;
37
- }
38
83
  return skill;
39
84
  }
40
85
  /**
@@ -46,8 +91,8 @@ function extractSections(content) {
46
91
  let currentSection = null;
47
92
  let sectionLines = [];
48
93
  for (const line of lines) {
49
- // Check for section headers (### SectionName)
50
- const headerMatch = line.match(/^###\s+(.+)$/);
94
+ // Check for section headers (any valid markdown header: #, ##, ###, etc.)
95
+ const headerMatch = line.match(/^#{1,6}\s+(.+)$/);
51
96
  if (headerMatch) {
52
97
  // Process previous section
53
98
  if (currentSection) {
@@ -35,18 +35,26 @@ export function loadSkills() {
35
35
  }
36
36
  /**
37
37
  * Load and parse all skill definitions
38
- * Returns structured skill definitions
38
+ * Returns structured skill definitions (including invalid skills)
39
39
  */
40
40
  export function loadSkillDefinitions() {
41
41
  const skillContents = loadSkills();
42
- const definitions = [];
43
- for (const content of skillContents) {
42
+ return skillContents.map((content) => parseSkillMarkdown(content));
43
+ }
44
+ /**
45
+ * Load skills and mark incomplete ones in their markdown
46
+ * Returns array of skill markdown with status markers
47
+ */
48
+ export function loadSkillsWithValidation() {
49
+ const skillContents = loadSkills();
50
+ return skillContents.map((content) => {
44
51
  const parsed = parseSkillMarkdown(content);
45
- if (parsed) {
46
- definitions.push(parsed);
52
+ // If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
53
+ if (parsed.isIncomplete) {
54
+ return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
47
55
  }
48
- }
49
- return definitions;
56
+ return content;
57
+ });
50
58
  }
51
59
  /**
52
60
  * Create skill lookup function from definitions
@@ -72,6 +80,10 @@ export function formatSkillsForPrompt(skills) {
72
80
  The following skills define domain-specific workflows. When the user's
73
81
  query matches a skill, incorporate the skill's steps into your plan.
74
82
 
83
+ Skills marked with (INCOMPLETE) have validation errors or need more
84
+ documentation, and cannot be executed. These should be listed in
85
+ introspection with their markers.
86
+
75
87
  **IMPORTANT**: When creating options from skill descriptions, do NOT use
76
88
  brackets for additional information. Use commas instead. For example:
77
89
  - CORRECT: "Build project Alpha, the legacy version"
@@ -3,7 +3,7 @@ import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDe
3
3
  import { saveConfig, unflattenConfig } from './configuration.js';
4
4
  import { FeedbackType } from '../types/types.js';
5
5
  import { validateExecuteTasks } from './execution-validator.js';
6
- import { getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
6
+ import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
7
7
  /**
8
8
  * Determine the operation name based on task types
9
9
  */
@@ -20,7 +20,7 @@ export function getOperationName(tasks) {
20
20
  * Route tasks to appropriate components with Confirm flow
21
21
  * Handles the complete flow: Plan → Confirm → Execute/Answer/Introspect
22
22
  */
23
- export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false, commandComponent) {
23
+ export function routeTasksWithConfirm(tasks, message, service, userRequest, handlers, hasDefineTask = false) {
24
24
  if (tasks.length === 0)
25
25
  return;
26
26
  // Filter out ignore and discard tasks early
@@ -32,31 +32,32 @@ export function routeTasksWithConfirm(tasks, message, service, userRequest, hand
32
32
  return;
33
33
  }
34
34
  const operation = getOperationName(validTasks);
35
- // Create plan definition with valid tasks only
36
- const planDefinition = createPlanDefinition(message, validTasks);
37
35
  if (hasDefineTask) {
38
36
  // Has DEFINE tasks - add Plan to queue for user selection
39
37
  // Refinement flow will call this function again with refined tasks
38
+ const planDefinition = createPlanDefinition(message, validTasks);
40
39
  handlers.addToQueue(planDefinition);
41
40
  }
42
41
  else {
43
- // No DEFINE tasks - add Plan to timeline, create Confirm
44
- const confirmDefinition = createConfirmDefinition(() => {
45
- // User confirmed - route to appropriate component
46
- handlers.completeActive();
47
- executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
48
- }, () => {
49
- // User cancelled
50
- handlers.onAborted(operation);
42
+ // No DEFINE tasks - Plan auto-completes and adds Confirm to queue
43
+ // When Plan activates, Command moves to timeline
44
+ // When Plan completes, it moves to pending
45
+ // When Confirm activates, Plan stays pending (visible for context)
46
+ const planDefinition = createPlanDefinition(message, validTasks, () => {
47
+ // Plan completed - add Confirm to queue
48
+ const confirmDefinition = createConfirmDefinition(() => {
49
+ // User confirmed - complete both Confirm and Plan, then route to appropriate component
50
+ handlers.completeActiveAndPending();
51
+ executeTasksAfterConfirm(validTasks, service, userRequest, handlers);
52
+ }, () => {
53
+ // User cancelled - complete both Confirm and Plan, then show cancellation
54
+ handlers.completeActiveAndPending();
55
+ const message = getCancellationMessage(operation);
56
+ handlers.addToQueue(createFeedback(FeedbackType.Aborted, message));
57
+ });
58
+ handlers.addToQueue(confirmDefinition);
51
59
  });
52
- // Use atomic update if commandComponent provided, else normal flow
53
- if (commandComponent) {
54
- handlers.completeActive(planDefinition);
55
- }
56
- else {
57
- handlers.addToTimeline(planDefinition);
58
- }
59
- handlers.addToQueue(confirmDefinition);
60
+ handlers.addToQueue(planDefinition);
60
61
  }
61
62
  }
62
63
  /**
@@ -124,9 +125,19 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
124
125
  }
125
126
  else {
126
127
  // Execute tasks with validation
127
- const missingConfig = validateExecuteTasks(tasks);
128
- if (missingConfig.length > 0) {
129
- handlers.addToQueue(createValidateDefinition(missingConfig, userRequest, service, (error) => {
128
+ const validation = validateExecuteTasks(tasks);
129
+ if (validation.validationErrors.length > 0) {
130
+ // Show error feedback for invalid skills
131
+ const errorMessages = validation.validationErrors.map((error) => {
132
+ const issuesList = error.issues
133
+ .map((issue) => ` - ${issue}`)
134
+ .join('\n');
135
+ return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
136
+ });
137
+ handlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
138
+ }
139
+ else if (validation.missingConfig.length > 0) {
140
+ handlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
130
141
  handlers.onError(error);
131
142
  }, () => {
132
143
  handlers.addToQueue(createExecuteDefinition(tasks, service));