prompt-language-shell 0.4.9 → 0.5.0

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.
@@ -75,12 +75,13 @@ These MUST appear FIRST, in this EXACT sequence:
75
75
  3. **Answer** ← ALWAYS THIRD
76
76
  4. **Execute** ← ALWAYS FOURTH
77
77
 
78
- ### Position 5-6: Indirect Workflow Capabilities
78
+ ### Position 5-7: Indirect Workflow Capabilities
79
79
 
80
80
  These MUST appear AFTER Execute and BEFORE user skills:
81
81
 
82
82
  5. **Plan** ← NEVER FIRST, ALWAYS position 5 (after Execute)
83
- 6. **Report** ← NEVER FIRST, ALWAYS position 6 (after Plan)
83
+ 6. **Validate** ← ALWAYS position 6 (after Plan)
84
+ 7. **Report** ← NEVER FIRST, ALWAYS position 7 (after Validate)
84
85
 
85
86
  ### 3. User-Defined Skills
86
87
 
@@ -132,8 +133,9 @@ Examples:
132
133
 
133
134
  When user asks "list your skills", create an introductory message like "here
134
135
  are my capabilities:" followed by tasks for built-in capabilities (Introspect,
135
- Config, Answer, Execute), then indirect workflow capabilities (Plan, Report).
136
- Each task uses type "introspect" with an action describing the capability.
136
+ Config, Answer, Execute), then indirect workflow capabilities (Validate, Plan,
137
+ Report). Each task uses type "introspect" with an action describing the
138
+ capability.
137
139
 
138
140
  ### Example 2: Filtered Skills
139
141
 
@@ -146,8 +148,9 @@ with its description.
146
148
 
147
149
  When user asks "what can you do" and user-defined skills like "process data"
148
150
  and "backup files" exist, create an introductory message like "i can help with
149
- these operations:" followed by all built-in capabilities plus the user-defined
150
- skills. Each capability and skill becomes a task with type "introspect".
151
+ these operations:" followed by all built-in capabilities (Introspect, Config,
152
+ Answer, Execute, Validate, Plan, Report) plus the user-defined skills. Each
153
+ capability and skill becomes a task with type "introspect".
151
154
 
152
155
  ## Final Validation
153
156
 
@@ -113,6 +113,26 @@ executable operations.
113
113
  Extract the individual steps from the skill's "Execution" or "Steps"
114
114
  section (prefer Execution if available)
115
115
  - Replace ALL parameter placeholders with the specified value
116
+ - **CRITICAL - Variant Placeholder Resolution**: If the execution commands
117
+ contain variant placeholders (any uppercase word in a placeholder path,
118
+ e.g., {section.VARIANT.property}, {project.TARGET.path}, {env.TYPE.name}),
119
+ you MUST:
120
+ 1. Identify the variant name from the user's request (e.g., "alpha", "beta")
121
+ 2. Normalize the variant to lowercase (e.g., "alpha", "beta")
122
+ 3. Replace the uppercase placeholder component with the actual variant name
123
+ in ALL task actions
124
+ 4. Examples:
125
+ - User says "process alpha target" → variant is "alpha"
126
+ - Execution line: `cd {project.VARIANT.path}`
127
+ - Task action MUST be: `cd {project.alpha.path}` (NOT `cd {project.VARIANT.path}`)
128
+ - User says "deploy to staging environment" → variant is "staging"
129
+ - Execution line: `setup {env.TYPE.config}`
130
+ - Task action MUST be: `setup {env.staging.config}` (NOT `setup {env.TYPE.config}`)
131
+ 5. This applies to ALL placeholders in task actions, whether from direct
132
+ execution lines or from referenced skills (e.g., [Navigate To Target])
133
+ 6. The uppercase word can be ANY name (VARIANT, TARGET, TYPE, PRODUCT, etc.) -
134
+ all uppercase path components indicate variant placeholders that must
135
+ be resolved
116
136
 
117
137
  4. **Handle partial execution:**
118
138
  - Keywords indicating partial execution: "only", "just", specific verbs
@@ -129,11 +149,16 @@ executable operations.
129
149
  - type: category of operation (if the skill specifies it or you can infer it)
130
150
  - params: MUST include:
131
151
  - skill: the skill name (REQUIRED for all skill-based tasks)
132
- - All parameter values used in the step (e.g., target, environment, etc.)
152
+ - variant: the resolved variant value (REQUIRED if skill has variant placeholders)
153
+ - All other parameter values used in the step (e.g., target, environment, etc.)
133
154
  - Any other specific parameters mentioned in the step
134
155
  - NEVER replace the skill's detailed steps with a generic restatement
135
156
  - The params.skill field is CRITICAL for execution to use the skill's
136
157
  Execution section
158
+ - The params.variant field is CRITICAL for config validation to resolve
159
+ variant placeholders in the skill's Execution section
160
+ - Example: If user selects "Deploy to production" and skill has {env.VARIANT.url},
161
+ params must include variant: "production" so validator can resolve to {env.production.url}
137
162
 
138
163
  6. **Handle additional requirements beyond the skill:**
139
164
  - If the user's query includes additional requirements beyond the skill,
@@ -157,6 +182,19 @@ Example 1 - Skill with parameter, variant specified:
157
182
  params: { skill: "Process Data", target: "Alpha" } }
158
183
  - WRONG: Tasks without params.skill or single task "Process Alpha"
159
184
 
185
+ Example 1b - Skill with variant placeholder in config:
186
+ - Skill name: "Navigate To Target"
187
+ - Skill config defines: target.alpha.path, target.beta.path, target.gamma.path
188
+ - Skill execution: "cd {target.VARIANT.path}"
189
+ - User: "navigate to beta"
190
+ - Variant matched: "beta"
191
+ - Correct task: { action: "Navigate to Beta target directory", type: "execute",
192
+ params: { skill: "Navigate To Target", variant: "beta" } }
193
+ - WRONG: params without variant field
194
+ - WRONG: task action "cd {target.VARIANT.path}" (uppercase VARIANT not resolved!)
195
+ - Note: The config validator will use params.variant="beta" to resolve
196
+ {target.VARIANT.path} → {target.beta.path}, then check if it exists in ~/.plsrc
197
+
160
198
  Example 2 - Skill with parameter, variant NOT specified:
161
199
  - Same skill as Example 1
162
200
  - User: "process"
@@ -0,0 +1,139 @@
1
+ ## Overview
2
+
3
+ You are the validation component of "pls" (please), responsible for validating skill requirements and generating natural language descriptions for missing configuration values.
4
+
5
+ Your role is to help users understand what configuration values are needed and why, using context from skill descriptions to create clear, helpful prompts.
6
+
7
+ ## Input
8
+
9
+ You will receive information about missing configuration values:
10
+ - Config path (e.g., "project.alpha.repo")
11
+ - Skill name that requires this config
12
+ - Variant (if applicable)
13
+ - Config type (string, boolean, number)
14
+
15
+ ## Your Task
16
+
17
+ Generate a response with two required fields:
18
+
19
+ 1. **message**: An empty string `""`
20
+ 2. **tasks**: An array of CONFIG tasks, one for each missing config value
21
+
22
+ For each CONFIG task, create a natural language description that:
23
+
24
+ 1. **Explains what the value is for** using context from the skill's description
25
+ 2. **Keeps it SHORT** - one brief phrase (3-6 words max)
26
+ 3. **Does NOT include the config path** - the path will be shown separately in debug mode
27
+
28
+ **CRITICAL**: You MUST include both the `message` field (set to empty string) and the `tasks` array in your response.
29
+
30
+ ## Description Format
31
+
32
+ **Format:** "Brief description" (NO {config.path} at the end!)
33
+
34
+ The description should:
35
+ - Start with what the config value represents (e.g., "Path to...", "URL for...", "Name of...")
36
+ - Be SHORT and direct - no extra details or variant explanations
37
+ - NOT include the config path in curly brackets - that's added automatically
38
+
39
+ ## Examples
40
+
41
+ ### Example 1: Repository Path
42
+
43
+ **Input:**
44
+ - Config path: `project.alpha.repo`
45
+ - Skill: "Navigate To Project"
46
+ - Variant: "alpha"
47
+
48
+ **Correct output:**
49
+ ```
50
+ message: ""
51
+ tasks: [
52
+ {
53
+ action: "Path to Alpha repository {project.alpha.repo}",
54
+ type: "config",
55
+ params: { key: "project.alpha.repo" }
56
+ }
57
+ ]
58
+ ```
59
+
60
+ ### Example 2: Environment URL
61
+
62
+ **Input:**
63
+ - Config path: `env.staging.url`
64
+ - Skill: "Deploy Service"
65
+ - Variant: "staging"
66
+
67
+ **Correct output:**
68
+ ```
69
+ message: ""
70
+ tasks: [
71
+ {
72
+ action: "Staging environment URL {env.staging.url}",
73
+ type: "config",
74
+ params: { key: "env.staging.url" }
75
+ }
76
+ ]
77
+ ```
78
+
79
+ ### Example 3: Project Directory
80
+
81
+ **Input:**
82
+ - Config path: `workspace.beta.path`
83
+ - Skill: "Process Workspace"
84
+ - Variant: "beta"
85
+
86
+ **Correct output:**
87
+ ```
88
+ message: ""
89
+ tasks: [
90
+ {
91
+ action: "Path to Beta workspace {workspace.beta.path}",
92
+ type: "config",
93
+ params: { key: "workspace.beta.path" }
94
+ }
95
+ ]
96
+ ```
97
+
98
+ ## Guidelines
99
+
100
+ 1. **Use skill context**: Read the skill's Description section to understand what the variant represents
101
+ 2. **Be specific**: Don't just say "Repository path" - say "Alpha project repository path"
102
+ 3. **Add helpful details**: Include information from the description (e.g., "legacy implementation")
103
+ 4. **Keep it concise**: One sentence that clearly explains what's needed
104
+ 5. **Always include the path**: End with `{config.path}` for technical reference
105
+
106
+ ## Common Config Types
107
+
108
+ - **repo / repository**: "Path to [name] repository"
109
+ - **path / dir / directory**: "Path to [name] directory"
110
+ - **url**: "[Name] URL"
111
+ - **host**: "[Name] host address"
112
+ - **port**: "[Name] port number"
113
+ - **name**: "Name of [context]"
114
+ - **key / token / secret**: "[Name] authentication key/token/secret"
115
+ - **enabled**: "Enable/disable [feature]"
116
+
117
+ ## Response Format
118
+
119
+ Return a message field (can be empty string) and an array of CONFIG tasks:
120
+
121
+ ```
122
+ message: ""
123
+ tasks: [
124
+ {
125
+ action: "Natural description {config.path}",
126
+ type: "config",
127
+ params: { key: "config.path" }
128
+ },
129
+ // ... more tasks
130
+ ]
131
+ ```
132
+
133
+ ## Important Notes
134
+
135
+ - All tasks must have type "config"
136
+ - All tasks must include params.key with the config path
137
+ - Descriptions should be helpful and contextual, not just technical
138
+ - Use information from Available Skills section to provide context
139
+ - Keep descriptions to one concise sentence
@@ -1,6 +1,6 @@
1
1
  import { ComponentName, FeedbackType } from '../types/types.js';
2
2
  import { createAnthropicService, } from '../services/anthropic.js';
3
- import { createCommandDefinition, createFeedback, markAsDone, } from '../services/components.js';
3
+ import { createCommandDefinition, createExecuteDefinition, createFeedback, markAsDone, } from '../services/components.js';
4
4
  import { saveAnthropicConfig, saveConfig } from '../services/configuration.js';
5
5
  import { FeedbackMessages } from '../services/messages.js';
6
6
  import { exitApp } from '../services/process.js';
@@ -33,18 +33,27 @@ export function createConfigHandlers(ops, handleAborted, command, commandHandler
33
33
  }
34
34
  /**
35
35
  * Creates config execution finished handler for CONFIG skill
36
- * Saves arbitrary config keys and exits
36
+ * Saves arbitrary config keys and optionally continues with execution
37
37
  */
38
- export function createConfigExecutionFinishedHandler(addToTimeline, keys) {
38
+ export function createConfigExecutionFinishedHandler(addToTimeline, keys, tasks, service, executeHandlers) {
39
39
  return (config) => {
40
+ // Group by top-level section
40
41
  const sections = {};
41
42
  for (const fullKey of keys) {
42
43
  const parts = fullKey.split('.');
43
44
  const shortKey = parts[parts.length - 1];
44
- const section = parts.slice(0, -1).join('.');
45
- sections[section] = sections[section] ?? {};
45
+ const topSection = parts[0];
46
+ // Initialize section if needed
47
+ sections[topSection] = sections[topSection] ?? {};
46
48
  if (shortKey in config) {
47
- sections[section][shortKey] = config[shortKey];
49
+ const value = config[shortKey];
50
+ // Build nested structure recursively
51
+ let current = sections[topSection];
52
+ for (let i = 1; i < parts.length - 1; i++) {
53
+ current[parts[i]] = current[parts[i]] ?? {};
54
+ current = current[parts[i]];
55
+ }
56
+ current[parts[parts.length - 1]] = value;
48
57
  }
49
58
  }
50
59
  for (const [section, sectionConfig] of Object.entries(sections)) {
@@ -52,6 +61,14 @@ export function createConfigExecutionFinishedHandler(addToTimeline, keys) {
52
61
  }
53
62
  return withQueueHandler(ComponentName.Config, (first, rest) => {
54
63
  addToTimeline(markAsDone(first), createFeedback(FeedbackType.Succeeded, FeedbackMessages.ConfigurationComplete));
64
+ // If tasks are provided, continue with execution
65
+ if (tasks && service && executeHandlers) {
66
+ return [
67
+ ...rest,
68
+ createExecuteDefinition(tasks, service, executeHandlers.onError, executeHandlers.onComplete, executeHandlers.onAborted),
69
+ ];
70
+ }
71
+ // Otherwise, exit (legacy behavior for initial setup)
55
72
  exitApp(0);
56
73
  return rest;
57
74
  }, false, 0);
@@ -8,6 +8,7 @@ import { withQueueHandler } from '../services/queue.js';
8
8
  * Creates all execute handlers
9
9
  */
10
10
  export function createExecuteHandlers(ops, handleAborted) {
11
+ void handleAborted;
11
12
  const onError = (error) => {
12
13
  ops.setQueue(withQueueHandler(ComponentName.Execute, (first) => {
13
14
  ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Failed, error));
@@ -31,8 +32,15 @@ export function createExecuteHandlers(ops, handleAborted) {
31
32
  return [];
32
33
  }));
33
34
  };
34
- const onAborted = () => {
35
- handleAborted('Execution');
35
+ const onAborted = (elapsedTime) => {
36
+ ops.setQueue(withQueueHandler(ComponentName.Execute, (first) => {
37
+ const message = elapsedTime > 0
38
+ ? `The execution was cancelled after ${formatDuration(elapsedTime)}.`
39
+ : 'The execution was cancelled.';
40
+ ops.addToTimeline(markAsDone(first), createFeedback(FeedbackType.Aborted, message));
41
+ exitApp(0);
42
+ return [];
43
+ }));
36
44
  };
37
45
  return { onError, onComplete, onAborted };
38
46
  }
@@ -1,9 +1,11 @@
1
1
  import { ComponentName, FeedbackType, TaskType } from '../types/types.js';
2
- import { createAnswerDefinition, createConfigDefinitionWithKeys, createExecuteDefinition, createFeedback, createIntrospectDefinition, markAsDone, } from '../services/components.js';
2
+ import { createAnswerDefinition, createConfigDefinitionWithKeys, createExecuteDefinition, createFeedback, createIntrospectDefinition, createValidateDefinition, markAsDone, } from '../services/components.js';
3
+ import { StepType } from '../ui/Config.js';
3
4
  import { getCancellationMessage } from '../services/messages.js';
4
5
  import { exitApp } from '../services/process.js';
5
6
  import { withQueueHandler } from '../services/queue.js';
6
7
  import { createConfigExecutionAbortedHandler, createConfigExecutionFinishedHandler, } from './config.js';
8
+ import { validateExecuteTasks } from '../services/execution-validator.js';
7
9
  /**
8
10
  * Creates all execution handlers
9
11
  */
@@ -49,6 +51,71 @@ export function createExecutionHandlers(ops, taskHandlers) {
49
51
  ];
50
52
  }
51
53
  else if (allExecute && tasks.length > 0) {
54
+ // Validate config requirements before execution
55
+ const missingConfig = validateExecuteTasks(tasks);
56
+ if (missingConfig.length > 0) {
57
+ // Config is missing - call VALIDATE tool to get contextual descriptions
58
+ const keys = missingConfig.map((req) => req.path);
59
+ const userRequest = tasks.map((t) => t.action).join(', ');
60
+ ops.addToTimeline(markAsDone(first));
61
+ // Create handlers for Validate completion
62
+ const handleValidateComplete = (configWithDescriptions) => {
63
+ ops.setQueue(withQueueHandler(ComponentName.Validate, (first) => {
64
+ // Create CONFIG component with descriptions from VALIDATE
65
+ const handleConfigFinished = (config) => {
66
+ ops.setQueue(createConfigExecutionFinishedHandler(ops.addToTimeline, keys, tasks, service, taskHandlers.execute)(config));
67
+ };
68
+ const handleConfigAborted = () => {
69
+ ops.setQueue(createConfigExecutionAbortedHandler(ops.addToTimeline)());
70
+ };
71
+ // Create config steps from validated descriptions
72
+ const steps = configWithDescriptions.map((req) => {
73
+ const keyParts = req.path.split('.');
74
+ const shortKey = keyParts[keyParts.length - 1];
75
+ // Extract description without the {path} suffix
76
+ // Format from VALIDATE: "Description {path}"
77
+ let description = req.description || req.path;
78
+ const pathPattern = /\s*\{[^}]+\}\s*$/;
79
+ description = description.replace(pathPattern, '').trim();
80
+ const step = {
81
+ description,
82
+ key: shortKey,
83
+ path: req.path,
84
+ type: StepType.Text,
85
+ value: null,
86
+ validate: () => true,
87
+ };
88
+ return step;
89
+ });
90
+ // Mark Validate as done and move to timeline
91
+ ops.addToTimeline(markAsDone(first));
92
+ return [
93
+ {
94
+ id: crypto.randomUUID(),
95
+ name: ComponentName.Config,
96
+ state: { done: false },
97
+ props: {
98
+ steps,
99
+ onFinished: handleConfigFinished,
100
+ onAborted: handleConfigAborted,
101
+ },
102
+ },
103
+ ];
104
+ }));
105
+ };
106
+ const handleValidateError = (error) => {
107
+ ops.addToTimeline(createFeedback(FeedbackType.Failed, error));
108
+ exitApp(1);
109
+ };
110
+ const handleValidateAborted = () => {
111
+ ops.addToTimeline(createFeedback(FeedbackType.Aborted, 'Configuration validation cancelled'));
112
+ exitApp(0);
113
+ };
114
+ return [
115
+ createValidateDefinition(missingConfig, userRequest, service, handleValidateError, handleValidateComplete, handleValidateAborted),
116
+ ];
117
+ }
118
+ // No missing config - execute directly
52
119
  ops.addToTimeline(markAsDone(first));
53
120
  return [
54
121
  createExecuteDefinition(tasks, service, taskHandlers.execute.onError, taskHandlers.execute.onComplete, taskHandlers.execute.onAborted),
@@ -18,7 +18,8 @@ export class AnthropicService {
18
18
  // Add skills section for applicable tools
19
19
  if (toolName === 'plan' ||
20
20
  toolName === 'introspect' ||
21
- toolName === 'execute') {
21
+ toolName === 'execute' ||
22
+ toolName === 'validate') {
22
23
  const skills = loadSkills();
23
24
  const skillsSection = formatSkillsForPrompt(skills);
24
25
  systemPrompt += skillsSection;
@@ -114,7 +115,7 @@ export class AnthropicService {
114
115
  };
115
116
  }
116
117
  // Handle plan and introspect tool responses
117
- if (!input.message || typeof input.message !== 'string') {
118
+ if (input.message === undefined || typeof input.message !== 'string') {
118
119
  throw new Error('Invalid tool response: missing or invalid message field');
119
120
  }
120
121
  if (!input.tasks || !Array.isArray(input.tasks)) {
@@ -15,8 +15,8 @@ export const Palette = {
15
15
  BrightGreen: '#3e9a3e',
16
16
  Yellow: '#cccc5c',
17
17
  LightYellow: '#d4d47a',
18
- Orange: '#cc9c5c',
19
- DarkOrange: '#a85c3f',
18
+ Orange: '#f48c80',
19
+ DarkOrange: '#ab5e40',
20
20
  BurntOrange: '#cc7a5c',
21
21
  Red: '#cc5c5c',
22
22
  Cyan: '#5c9ccc',
@@ -65,8 +65,22 @@ export function createConfigStepsFromSchema(keys) {
65
65
  // Config doesn't exist yet, use defaults
66
66
  }
67
67
  return keys.map((key) => {
68
+ // Check if key is in schema (built-in config)
68
69
  if (!(key in schema)) {
69
- throw new Error(`Unknown config key: ${key}`);
70
+ // Key is not in schema - it's from a skill
71
+ // Create a simple text step with placeholder description
72
+ const keyParts = key.split('.');
73
+ const shortKey = keyParts[keyParts.length - 1];
74
+ const description = keyParts
75
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
76
+ .join(' ');
77
+ return {
78
+ description: `${description} {${key}}`,
79
+ key: shortKey,
80
+ type: StepType.Text,
81
+ value: null,
82
+ validate: () => true, // Accept any string for now
83
+ };
70
84
  }
71
85
  const definition = schema[key];
72
86
  const currentValue = getConfigValue(currentConfig, key);
@@ -318,3 +332,21 @@ export function createExecuteDefinition(tasks, service, onError, onComplete, onA
318
332
  },
319
333
  };
320
334
  }
335
+ export function createValidateDefinition(missingConfig, userRequest, service, onError, onComplete, onAborted) {
336
+ return {
337
+ id: randomUUID(),
338
+ name: ComponentName.Validate,
339
+ state: {
340
+ done: false,
341
+ isLoading: true,
342
+ },
343
+ props: {
344
+ missingConfig,
345
+ userRequest,
346
+ service,
347
+ onError,
348
+ onComplete,
349
+ onAborted,
350
+ },
351
+ };
352
+ }
@@ -0,0 +1,67 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { homedir } from 'os';
3
+ import { join } from 'path';
4
+ import YAML from 'yaml';
5
+ /**
6
+ * Load user config from ~/.plsrc
7
+ */
8
+ export function loadUserConfig() {
9
+ const configPath = join(homedir(), '.plsrc');
10
+ if (!existsSync(configPath)) {
11
+ return {};
12
+ }
13
+ try {
14
+ const content = readFileSync(configPath, 'utf-8');
15
+ const parsed = YAML.parse(content);
16
+ if (parsed && typeof parsed === 'object') {
17
+ return parsed;
18
+ }
19
+ return {};
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ /**
26
+ * Check if config has a specific path
27
+ */
28
+ export function hasConfigPath(config, path) {
29
+ const parts = path.split('.');
30
+ let current = config;
31
+ for (const part of parts) {
32
+ if (current === null || current === undefined) {
33
+ return false;
34
+ }
35
+ if (typeof current !== 'object') {
36
+ return false;
37
+ }
38
+ current = current[part];
39
+ }
40
+ return (current !== null &&
41
+ current !== undefined &&
42
+ (typeof current === 'string' ||
43
+ typeof current === 'boolean' ||
44
+ typeof current === 'number'));
45
+ }
46
+ /**
47
+ * Get config value at path
48
+ */
49
+ export function getConfigValue(config, path) {
50
+ const parts = path.split('.');
51
+ let current = config;
52
+ for (const part of parts) {
53
+ if (current === null || current === undefined) {
54
+ return undefined;
55
+ }
56
+ if (typeof current !== 'object') {
57
+ return undefined;
58
+ }
59
+ current = current[part];
60
+ }
61
+ if (typeof current === 'string' ||
62
+ typeof current === 'boolean' ||
63
+ typeof current === 'number') {
64
+ return current;
65
+ }
66
+ return undefined;
67
+ }
@@ -0,0 +1,110 @@
1
+ import { extractPlaceholders, pathToString, resolveVariant, } from './placeholder-resolver.js';
2
+ import { loadUserConfig, hasConfigPath } from './config-loader.js';
3
+ import { loadSkills } from './skills.js';
4
+ import { expandSkillReferences } from './skill-expander.js';
5
+ import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
6
+ /**
7
+ * Validate config requirements for execute tasks
8
+ * Returns missing config requirements
9
+ */
10
+ export function validateExecuteTasks(tasks) {
11
+ const userConfig = loadUserConfig();
12
+ const missing = [];
13
+ const seenPaths = new Set();
14
+ // Load and parse all skills for validation
15
+ const skillContents = loadSkills();
16
+ const parsedSkills = skillContents
17
+ .map((content) => parseSkillMarkdown(content))
18
+ .filter((s) => s !== null);
19
+ const skillLookup = (name) => parsedSkills.find((s) => s.name === name) || null;
20
+ for (const task of tasks) {
21
+ // Check if task originates from a skill
22
+ const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
23
+ if (skillName) {
24
+ // Task comes from a skill - check skill's Execution section
25
+ const skill = skillLookup(skillName);
26
+ if (!skill || !skill.execution) {
27
+ continue;
28
+ }
29
+ // Get variant from task params (if any)
30
+ // Try params.variant first, then look for other param keys that might be the variant
31
+ let variant = null;
32
+ if (typeof task.params?.variant === 'string') {
33
+ variant = task.params.variant.toLowerCase();
34
+ }
35
+ else if (task.params && typeof task.params === 'object') {
36
+ // Look for other params that could be the variant (e.g., product, target, option, etc.)
37
+ // Exclude known non-variant params
38
+ const excludeKeys = new Set(['skill', 'type']);
39
+ for (const [key, value] of Object.entries(task.params)) {
40
+ if (!excludeKeys.has(key) && typeof value === 'string') {
41
+ variant = value.toLowerCase();
42
+ break;
43
+ }
44
+ }
45
+ }
46
+ // Expand skill references to get actual commands
47
+ const expanded = expandSkillReferences(skill.execution, skillLookup);
48
+ // Extract placeholders from actual commands
49
+ for (const line of expanded) {
50
+ const placeholders = extractPlaceholders(line);
51
+ for (const placeholder of placeholders) {
52
+ let resolvedPath;
53
+ if (placeholder.hasVariant) {
54
+ // Variant placeholder - resolve with variant from params
55
+ if (!variant) {
56
+ // No variant provided - skip this placeholder
57
+ continue;
58
+ }
59
+ const resolvedPathArray = resolveVariant(placeholder.path, variant);
60
+ resolvedPath = pathToString(resolvedPathArray);
61
+ }
62
+ else {
63
+ // Strict placeholder - use as-is
64
+ resolvedPath = pathToString(placeholder.path);
65
+ }
66
+ // Skip if already processed
67
+ if (seenPaths.has(resolvedPath)) {
68
+ continue;
69
+ }
70
+ seenPaths.add(resolvedPath);
71
+ // Check if config exists
72
+ if (!hasConfigPath(userConfig, resolvedPath)) {
73
+ // Get type from skill config
74
+ const type = skill.config
75
+ ? getConfigType(skill.config, resolvedPath)
76
+ : undefined;
77
+ missing.push({
78
+ path: resolvedPath,
79
+ type: type || 'string',
80
+ });
81
+ }
82
+ }
83
+ }
84
+ }
85
+ else {
86
+ // Task doesn't come from a skill - check task action for placeholders
87
+ const placeholders = extractPlaceholders(task.action);
88
+ for (const placeholder of placeholders) {
89
+ // Skip variant placeholders - they should have been resolved during planning
90
+ if (placeholder.hasVariant) {
91
+ continue;
92
+ }
93
+ const path = placeholder.path.join('.');
94
+ // Skip if already processed
95
+ if (seenPaths.has(path)) {
96
+ continue;
97
+ }
98
+ seenPaths.add(path);
99
+ // Check if config exists
100
+ if (!hasConfigPath(userConfig, path)) {
101
+ missing.push({
102
+ path,
103
+ type: 'string', // Default to string for now
104
+ });
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return missing;
110
+ }