prompt-language-shell 0.6.4 → 0.6.8
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 +14 -5
- package/dist/services/anthropic.js +3 -3
- package/dist/services/messages.js +15 -0
- package/dist/services/{skill-parser.js → parser.js} +87 -23
- package/dist/services/refinement.js +1 -1
- package/dist/services/{tool-registry.js → registry.js} +14 -24
- package/dist/services/{task-router.js → router.js} +29 -11
- package/dist/services/skills.js +180 -19
- package/dist/services/{execution-validator.js → validator.js} +37 -14
- package/dist/{config/INTROSPECT.md → skills/introspect.md} +2 -0
- package/dist/ui/Command.js +1 -1
- package/dist/ui/Execute.js +2 -2
- package/dist/ui/Introspect.js +11 -3
- package/dist/ui/Report.js +3 -3
- package/dist/ui/Validate.js +6 -0
- package/package.json +2 -2
- package/dist/services/skill-expander.js +0 -91
- /package/dist/services/{config-loader.js → loader.js} +0 -0
- /package/dist/services/{placeholder-resolver.js → resolver.js} +0 -0
- /package/dist/{config/ANSWER.md → skills/answer.md} +0 -0
- /package/dist/{config/CONFIG.md → skills/config.md} +0 -0
- /package/dist/{config/EXECUTE.md → skills/execute.md} +0 -0
- /package/dist/{config/PLAN.md → skills/plan.md} +0 -0
- /package/dist/{config/VALIDATE.md → skills/validate.md} +0 -0
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
|
|
6
|
-
>
|
|
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.
|
|
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.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Anthropic from '@anthropic-ai/sdk';
|
|
2
2
|
import { getAvailableConfigStructure, getConfiguredKeys, } from './configuration.js';
|
|
3
|
-
import { formatSkillsForPrompt,
|
|
4
|
-
import { toolRegistry } from './
|
|
3
|
+
import { formatSkillsForPrompt, loadSkillsWithValidation } from './skills.js';
|
|
4
|
+
import { toolRegistry } from './registry.js';
|
|
5
5
|
/**
|
|
6
6
|
* Wraps text to ensure no line exceeds the specified width.
|
|
7
7
|
* Breaks at word boundaries to maintain readability.
|
|
@@ -62,7 +62,7 @@ export class AnthropicService {
|
|
|
62
62
|
toolName === 'introspect' ||
|
|
63
63
|
toolName === 'execute' ||
|
|
64
64
|
toolName === 'validate') {
|
|
65
|
-
const skills =
|
|
65
|
+
const skills = loadSkillsWithValidation();
|
|
66
66
|
const skillsSection = formatSkillsForPrompt(skills);
|
|
67
67
|
systemPrompt += skillsSection;
|
|
68
68
|
}
|
|
@@ -58,6 +58,21 @@ export function getUnknownRequestMessage() {
|
|
|
58
58
|
];
|
|
59
59
|
return messages[Math.floor(Math.random() * messages.length)];
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Returns an error message for unknown skill references.
|
|
63
|
+
* Randomly selects from variations to sound natural.
|
|
64
|
+
* Skill name should be a verb with details (e.g., "Deploy Application")
|
|
65
|
+
*/
|
|
66
|
+
export function getUnknownSkillMessage(skillName) {
|
|
67
|
+
const templates = [
|
|
68
|
+
`I don't know how to "${skillName}".`,
|
|
69
|
+
`I'm not familiar with the "${skillName}" command.`,
|
|
70
|
+
`I haven't learned how to "${skillName}" yet.`,
|
|
71
|
+
`I can't "${skillName}".`,
|
|
72
|
+
`I'm unable to "${skillName}".`,
|
|
73
|
+
];
|
|
74
|
+
return templates[Math.floor(Math.random() * templates.length)];
|
|
75
|
+
}
|
|
61
76
|
/**
|
|
62
77
|
* Returns an error message for mixed task types.
|
|
63
78
|
*/
|
|
@@ -1,40 +1,104 @@
|
|
|
1
1
|
import YAML from 'yaml';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Validate a skill without parsing it fully
|
|
4
|
+
* Returns validation error if skill is invalid, null if valid
|
|
5
|
+
* Note: Name section is optional - key from filename is used as fallback
|
|
4
6
|
*/
|
|
5
|
-
export function
|
|
7
|
+
export function validateSkillStructure(content, key) {
|
|
6
8
|
const sections = extractSections(content);
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
// Description is required
|
|
9
|
+
// Use key for error reporting if name not present
|
|
10
|
+
const skillName = sections.name || key;
|
|
11
|
+
// Check required sections (Name is now optional)
|
|
12
12
|
if (!sections.description) {
|
|
13
|
-
return
|
|
13
|
+
return {
|
|
14
|
+
skillName,
|
|
15
|
+
error: 'The skill file is missing a Description section',
|
|
16
|
+
};
|
|
14
17
|
}
|
|
15
|
-
// Steps are required
|
|
16
18
|
if (!sections.steps || sections.steps.length === 0) {
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
return {
|
|
20
|
+
skillName,
|
|
21
|
+
error: 'The skill file is missing a Steps section',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (!sections.execution || sections.execution.length === 0) {
|
|
25
|
+
return {
|
|
26
|
+
skillName,
|
|
27
|
+
error: 'The skill file is missing an Execution section',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Execution and steps must have same count
|
|
31
|
+
if (sections.execution.length !== sections.steps.length) {
|
|
32
|
+
return {
|
|
33
|
+
skillName,
|
|
34
|
+
error: `The skill has ${String(sections.steps.length)} steps but ${String(sections.execution.length)} execution lines`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Convert kebab-case key to Title Case display name
|
|
41
|
+
* Examples: "deploy-app" -> "Deploy App", "build-project-2" -> "Build Project 2"
|
|
42
|
+
*/
|
|
43
|
+
function keyToDisplayName(key) {
|
|
44
|
+
return key
|
|
45
|
+
.split('-')
|
|
46
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
47
|
+
.join(' ');
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert display name to kebab-case key
|
|
51
|
+
* Examples: "Deploy App" -> "deploy-app", "Navigate To Product" -> "navigate-to-product"
|
|
52
|
+
*/
|
|
53
|
+
export function displayNameToKey(name) {
|
|
54
|
+
return name
|
|
55
|
+
.toLowerCase()
|
|
56
|
+
.replace(/\s+/g, '-')
|
|
57
|
+
.replace(/[^a-z0-9-]/g, '');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Parse a skill markdown file into structured definition
|
|
61
|
+
*/
|
|
62
|
+
export function parseSkillMarkdown(key, content) {
|
|
63
|
+
const sections = extractSections(content);
|
|
64
|
+
// Determine display name: prefer Name section, otherwise derive from key
|
|
65
|
+
const displayName = sections.name || keyToDisplayName(key);
|
|
66
|
+
// Validate the skill (Name is no longer required since we have key)
|
|
67
|
+
const validationError = validateSkillStructure(content, key);
|
|
68
|
+
// For invalid skills, return minimal definition with error
|
|
69
|
+
if (validationError) {
|
|
70
|
+
return {
|
|
71
|
+
key,
|
|
72
|
+
name: displayName,
|
|
73
|
+
description: sections.description || '',
|
|
74
|
+
steps: sections.steps || [],
|
|
75
|
+
execution: sections.execution || [],
|
|
76
|
+
isValid: false,
|
|
77
|
+
validationError: validationError.error,
|
|
78
|
+
isIncomplete: true,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// Valid skill - all required fields are present (validation passed)
|
|
82
|
+
const description = sections.description;
|
|
24
83
|
const skill = {
|
|
25
|
-
|
|
26
|
-
|
|
84
|
+
key,
|
|
85
|
+
name: displayName,
|
|
86
|
+
description,
|
|
27
87
|
steps: sections.steps,
|
|
88
|
+
execution: sections.execution,
|
|
89
|
+
isValid: true,
|
|
28
90
|
};
|
|
91
|
+
// Check if skill is incomplete (valid but needs more documentation)
|
|
92
|
+
const MIN_DESCRIPTION_LENGTH = 20;
|
|
93
|
+
if (description.trim().length < MIN_DESCRIPTION_LENGTH) {
|
|
94
|
+
skill.isIncomplete = true;
|
|
95
|
+
}
|
|
29
96
|
if (sections.aliases && sections.aliases.length > 0) {
|
|
30
97
|
skill.aliases = sections.aliases;
|
|
31
98
|
}
|
|
32
99
|
if (sections.config) {
|
|
33
100
|
skill.config = sections.config;
|
|
34
101
|
}
|
|
35
|
-
if (sections.execution && sections.execution.length > 0) {
|
|
36
|
-
skill.execution = sections.execution;
|
|
37
|
-
}
|
|
38
102
|
return skill;
|
|
39
103
|
}
|
|
40
104
|
/**
|
|
@@ -46,8 +110,8 @@ function extractSections(content) {
|
|
|
46
110
|
let currentSection = null;
|
|
47
111
|
let sectionLines = [];
|
|
48
112
|
for (const line of lines) {
|
|
49
|
-
// Check for section headers (
|
|
50
|
-
const headerMatch = line.match(
|
|
113
|
+
// Check for section headers (any valid markdown header: #, ##, ###, etc.)
|
|
114
|
+
const headerMatch = line.match(/^#{1,6}\s+(.+)$/);
|
|
51
115
|
if (headerMatch) {
|
|
52
116
|
// Process previous section
|
|
53
117
|
if (currentSection) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createRefinement } from './components.js';
|
|
2
2
|
import { formatErrorMessage, getRefiningMessage } from './messages.js';
|
|
3
|
-
import { routeTasksWithConfirm } from './
|
|
3
|
+
import { routeTasksWithConfirm } from './router.js';
|
|
4
4
|
/**
|
|
5
5
|
* Handle refinement flow for DEFINE tasks
|
|
6
6
|
* Called when user selects options from a plan with DEFINE tasks
|
|
@@ -39,27 +39,17 @@ import { executeTool } from '../tools/execute.tool.js';
|
|
|
39
39
|
import { introspectTool } from '../tools/introspect.tool.js';
|
|
40
40
|
import { planTool } from '../tools/plan.tool.js';
|
|
41
41
|
import { validateTool } from '../tools/validate.tool.js';
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
instructionsPath: 'config/CONFIG.md',
|
|
57
|
-
});
|
|
58
|
-
toolRegistry.register('execute', {
|
|
59
|
-
schema: executeTool,
|
|
60
|
-
instructionsPath: 'config/EXECUTE.md',
|
|
61
|
-
});
|
|
62
|
-
toolRegistry.register('validate', {
|
|
63
|
-
schema: validateTool,
|
|
64
|
-
instructionsPath: 'config/VALIDATE.md',
|
|
65
|
-
});
|
|
42
|
+
const tools = {
|
|
43
|
+
answer: answerTool,
|
|
44
|
+
config: configTool,
|
|
45
|
+
execute: executeTool,
|
|
46
|
+
introspect: introspectTool,
|
|
47
|
+
plan: planTool,
|
|
48
|
+
validate: validateTool,
|
|
49
|
+
};
|
|
50
|
+
for (const [name, schema] of Object.entries(tools)) {
|
|
51
|
+
toolRegistry.register(name, {
|
|
52
|
+
schema,
|
|
53
|
+
instructionsPath: `skills/${name}.md`,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
@@ -2,7 +2,7 @@ import { TaskType } from '../types/types.js';
|
|
|
2
2
|
import { createAnswerDefinition, createConfigDefinitionWithKeys, createConfirmDefinition, createExecuteDefinition, createFeedback, createIntrospectDefinition, createMessage, createPlanDefinition, createValidateDefinition, } from './components.js';
|
|
3
3
|
import { saveConfig, unflattenConfig } from './configuration.js';
|
|
4
4
|
import { FeedbackType } from '../types/types.js';
|
|
5
|
-
import { validateExecuteTasks } from './
|
|
5
|
+
import { validateExecuteTasks } from './validator.js';
|
|
6
6
|
import { getCancellationMessage, getMixedTaskTypesError, getUnknownRequestMessage, } from './messages.js';
|
|
7
7
|
/**
|
|
8
8
|
* Determine the operation name based on task types
|
|
@@ -125,18 +125,36 @@ function executeTasksAfterConfirm(tasks, service, userRequest, handlers) {
|
|
|
125
125
|
}
|
|
126
126
|
else {
|
|
127
127
|
// Execute tasks with validation
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
128
|
+
try {
|
|
129
|
+
const validation = validateExecuteTasks(tasks);
|
|
130
|
+
if (validation.validationErrors.length > 0) {
|
|
131
|
+
// Show error feedback for invalid skills
|
|
132
|
+
const errorMessages = validation.validationErrors.map((error) => {
|
|
133
|
+
const issuesList = error.issues
|
|
134
|
+
.map((issue) => ` - ${issue}`)
|
|
135
|
+
.join('\n');
|
|
136
|
+
return `Invalid skill definition "${error.skill}":\n\n${issuesList}`;
|
|
137
|
+
});
|
|
138
|
+
handlers.addToQueue(createFeedback(FeedbackType.Failed, errorMessages.join('\n\n')));
|
|
139
|
+
}
|
|
140
|
+
else if (validation.missingConfig.length > 0) {
|
|
141
|
+
handlers.addToQueue(createValidateDefinition(validation.missingConfig, userRequest, service, (error) => {
|
|
142
|
+
handlers.onError(error);
|
|
143
|
+
}, () => {
|
|
144
|
+
handlers.addToQueue(createExecuteDefinition(tasks, service));
|
|
145
|
+
}, (operation) => {
|
|
146
|
+
handlers.onAborted(operation);
|
|
147
|
+
}));
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
133
150
|
handlers.addToQueue(createExecuteDefinition(tasks, service));
|
|
134
|
-
}
|
|
135
|
-
handlers.onAborted(operation);
|
|
136
|
-
}));
|
|
151
|
+
}
|
|
137
152
|
}
|
|
138
|
-
|
|
139
|
-
|
|
153
|
+
catch (error) {
|
|
154
|
+
// Handle skill reference errors (e.g., unknown skills)
|
|
155
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
156
|
+
const message = createMessage(errorMessage);
|
|
157
|
+
handlers.addToQueue(message);
|
|
140
158
|
}
|
|
141
159
|
}
|
|
142
160
|
}
|
package/dist/services/skills.js
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
2
|
import { homedir } from 'os';
|
|
3
3
|
import { join } from 'path';
|
|
4
|
-
import {
|
|
4
|
+
import { getUnknownSkillMessage } from './messages.js';
|
|
5
|
+
import { parseSkillMarkdown, displayNameToKey } from './parser.js';
|
|
6
|
+
/**
|
|
7
|
+
* Built-in skill names that user skills cannot override
|
|
8
|
+
*/
|
|
9
|
+
const BUILT_IN_SKILLS = new Set([
|
|
10
|
+
'plan',
|
|
11
|
+
'execute',
|
|
12
|
+
'answer',
|
|
13
|
+
'config',
|
|
14
|
+
'validate',
|
|
15
|
+
'introspect',
|
|
16
|
+
]);
|
|
17
|
+
/**
|
|
18
|
+
* Validate filename follows kebab-case pattern
|
|
19
|
+
* Valid: deploy-app.md, build-project-2.md, copy-files.md
|
|
20
|
+
* Invalid: Deploy_App.md, buildProject.md, DEPLOY.md, file name.md
|
|
21
|
+
*/
|
|
22
|
+
export function isValidSkillFilename(filename) {
|
|
23
|
+
// Must end with .md or .MD extension
|
|
24
|
+
if (!filename.endsWith('.md') && !filename.endsWith('.MD')) {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
// Extract name without extension
|
|
28
|
+
const name = filename.slice(0, -3);
|
|
29
|
+
// Must match kebab-case pattern: lowercase letters, numbers, and hyphens only
|
|
30
|
+
// Must start with a letter, and not start or end with a hyphen
|
|
31
|
+
const kebabCasePattern = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
32
|
+
return kebabCasePattern.test(name);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Check if skill key conflicts with built-in skills
|
|
36
|
+
*/
|
|
37
|
+
export function conflictsWithBuiltIn(key) {
|
|
38
|
+
return BUILT_IN_SKILLS.has(key);
|
|
39
|
+
}
|
|
5
40
|
/**
|
|
6
41
|
* Get the path to the skills directory
|
|
7
42
|
*/
|
|
@@ -10,7 +45,8 @@ export function getSkillsDirectory() {
|
|
|
10
45
|
}
|
|
11
46
|
/**
|
|
12
47
|
* Load all skill markdown files from the skills directory
|
|
13
|
-
* Returns an array of
|
|
48
|
+
* Returns an array of objects with filename (key) and content
|
|
49
|
+
* Filters out invalid filenames and conflicts with built-in skills
|
|
14
50
|
*/
|
|
15
51
|
export function loadSkills() {
|
|
16
52
|
const skillsDir = getSkillsDirectory();
|
|
@@ -20,12 +56,27 @@ export function loadSkills() {
|
|
|
20
56
|
}
|
|
21
57
|
try {
|
|
22
58
|
const files = readdirSync(skillsDir);
|
|
23
|
-
// Filter
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
59
|
+
// Filter and map valid skill files
|
|
60
|
+
return files
|
|
61
|
+
.filter((file) => {
|
|
62
|
+
// Must follow kebab-case naming convention
|
|
63
|
+
if (!isValidSkillFilename(file)) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
// Extract key (filename without extension, handles both .md and .MD)
|
|
67
|
+
const key = file.slice(0, -3);
|
|
68
|
+
// Must not conflict with built-in skills
|
|
69
|
+
if (conflictsWithBuiltIn(key)) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
})
|
|
74
|
+
.map((file) => {
|
|
75
|
+
// Extract key (filename without extension, handles both .md and .MD)
|
|
76
|
+
const key = file.slice(0, -3);
|
|
27
77
|
const filePath = join(skillsDir, file);
|
|
28
|
-
|
|
78
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
79
|
+
return { key, content };
|
|
29
80
|
});
|
|
30
81
|
}
|
|
31
82
|
catch {
|
|
@@ -35,28 +86,42 @@ export function loadSkills() {
|
|
|
35
86
|
}
|
|
36
87
|
/**
|
|
37
88
|
* Load and parse all skill definitions
|
|
38
|
-
* Returns structured skill definitions
|
|
89
|
+
* Returns structured skill definitions (including invalid skills)
|
|
39
90
|
*/
|
|
40
91
|
export function loadSkillDefinitions() {
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
92
|
+
const skills = loadSkills();
|
|
93
|
+
return skills.map(({ key, content }) => parseSkillMarkdown(key, content));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Load skills and mark incomplete ones in their markdown
|
|
97
|
+
* Returns array of skill markdown with status markers
|
|
98
|
+
*/
|
|
99
|
+
export function loadSkillsWithValidation() {
|
|
100
|
+
const skills = loadSkills();
|
|
101
|
+
return skills.map(({ key, content }) => {
|
|
102
|
+
const parsed = parseSkillMarkdown(key, content);
|
|
103
|
+
// If skill is incomplete (either validation failed or needs more documentation), append (INCOMPLETE) to the name
|
|
104
|
+
if (parsed.isIncomplete) {
|
|
105
|
+
return content.replace(/^(#{1,6}\s+Name\s*\n+)(.+?)(\n|$)/im, `$1$2 (INCOMPLETE)$3`);
|
|
47
106
|
}
|
|
48
|
-
|
|
49
|
-
|
|
107
|
+
return content;
|
|
108
|
+
});
|
|
50
109
|
}
|
|
51
110
|
/**
|
|
52
111
|
* Create skill lookup function from definitions
|
|
112
|
+
* Lookup by key (display name is converted to kebab-case for matching)
|
|
113
|
+
* Example: "Deploy App" -> "deploy-app" -> matches skill with key "deploy-app"
|
|
53
114
|
*/
|
|
54
115
|
export function createSkillLookup(definitions) {
|
|
55
|
-
const
|
|
116
|
+
const keyMap = new Map();
|
|
56
117
|
for (const definition of definitions) {
|
|
57
|
-
|
|
118
|
+
keyMap.set(definition.key, definition);
|
|
58
119
|
}
|
|
59
|
-
return (name) =>
|
|
120
|
+
return (name) => {
|
|
121
|
+
// Convert display name to kebab-case key for lookup
|
|
122
|
+
const key = displayNameToKey(name);
|
|
123
|
+
return keyMap.get(key) || null;
|
|
124
|
+
};
|
|
60
125
|
}
|
|
61
126
|
/**
|
|
62
127
|
* Format skills for inclusion in the planning prompt
|
|
@@ -72,6 +137,10 @@ export function formatSkillsForPrompt(skills) {
|
|
|
72
137
|
The following skills define domain-specific workflows. When the user's
|
|
73
138
|
query matches a skill, incorporate the skill's steps into your plan.
|
|
74
139
|
|
|
140
|
+
Skills marked with (INCOMPLETE) have validation errors or need more
|
|
141
|
+
documentation, and cannot be executed. These should be listed in
|
|
142
|
+
introspection with their markers.
|
|
143
|
+
|
|
75
144
|
**IMPORTANT**: When creating options from skill descriptions, do NOT use
|
|
76
145
|
brackets for additional information. Use commas instead. For example:
|
|
77
146
|
- CORRECT: "Build project Alpha, the legacy version"
|
|
@@ -81,3 +150,95 @@ brackets for additional information. Use commas instead. For example:
|
|
|
81
150
|
const skillsContent = skills.join('\n\n');
|
|
82
151
|
return header + skillsContent;
|
|
83
152
|
}
|
|
153
|
+
/**
|
|
154
|
+
* Parse skill reference from execution line
|
|
155
|
+
* Format: [ Display Name ] with mandatory spaces
|
|
156
|
+
* Returns display name if line matches format, otherwise null
|
|
157
|
+
* Example: "[ My Skill ]" -> "My Skill"
|
|
158
|
+
*/
|
|
159
|
+
export function parseSkillReference(line) {
|
|
160
|
+
// Must match format: [ content ] with at least one space before and after
|
|
161
|
+
const match = line.trim().match(/^\[\s+(.+?)\s+\]$/);
|
|
162
|
+
return match ? match[1] : null;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Check if execution line is a skill reference
|
|
166
|
+
* Must have format: [ content ] with spaces
|
|
167
|
+
*/
|
|
168
|
+
export function isSkillReference(line) {
|
|
169
|
+
return /^\[\s+.+?\s+\]$/.test(line.trim());
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Expand skill references in execution commands
|
|
173
|
+
* Returns expanded execution lines with references replaced
|
|
174
|
+
* Throws error if circular reference detected or skill not found
|
|
175
|
+
* Reference format: [ Skill Name ]
|
|
176
|
+
*/
|
|
177
|
+
export function expandSkillReferences(execution, skillLookup, visited = new Set()) {
|
|
178
|
+
const expanded = [];
|
|
179
|
+
for (const line of execution) {
|
|
180
|
+
// First: Detect if line matches [ XXX ] format
|
|
181
|
+
const skillName = parseSkillReference(line);
|
|
182
|
+
if (!skillName) {
|
|
183
|
+
// Not a reference, keep command as-is
|
|
184
|
+
expanded.push(line);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
// Check for circular reference
|
|
188
|
+
if (visited.has(skillName)) {
|
|
189
|
+
throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
|
|
190
|
+
}
|
|
191
|
+
// Second: Match against skill name
|
|
192
|
+
const skill = skillLookup(skillName);
|
|
193
|
+
if (!skill) {
|
|
194
|
+
// Referenced skill not found - throw error to break execution
|
|
195
|
+
throw new Error(getUnknownSkillMessage(skillName));
|
|
196
|
+
}
|
|
197
|
+
// Recursively expand referenced skill's execution
|
|
198
|
+
const newVisited = new Set(visited);
|
|
199
|
+
newVisited.add(skillName);
|
|
200
|
+
const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
|
|
201
|
+
expanded.push(...referencedExecution);
|
|
202
|
+
}
|
|
203
|
+
return expanded;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Get all skill names referenced in execution (including nested)
|
|
207
|
+
* Returns unique set of skill names
|
|
208
|
+
*/
|
|
209
|
+
export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
|
|
210
|
+
const referenced = new Set();
|
|
211
|
+
for (const line of execution) {
|
|
212
|
+
const skillName = parseSkillReference(line);
|
|
213
|
+
if (!skillName || visited.has(skillName)) {
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
referenced.add(skillName);
|
|
217
|
+
const skill = skillLookup(skillName);
|
|
218
|
+
if (skill) {
|
|
219
|
+
const newVisited = new Set(visited);
|
|
220
|
+
newVisited.add(skillName);
|
|
221
|
+
const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
|
|
222
|
+
for (const name of nested) {
|
|
223
|
+
referenced.add(name);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return referenced;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Validate skill references don't form cycles
|
|
231
|
+
* Returns true if valid, false if circular reference detected
|
|
232
|
+
*/
|
|
233
|
+
export function validateNoCycles(execution, skillLookup, visited = new Set()) {
|
|
234
|
+
try {
|
|
235
|
+
expandSkillReferences(execution, skillLookup, visited);
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
if (error instanceof Error && error.message.includes('Circular')) {
|
|
240
|
+
return false;
|
|
241
|
+
}
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -1,29 +1,49 @@
|
|
|
1
|
-
import { extractPlaceholders, pathToString, resolveVariant, } from './
|
|
2
|
-
import { loadUserConfig, hasConfigPath } from './
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { getConfigType, parseSkillMarkdown } from './skill-parser.js';
|
|
1
|
+
import { extractPlaceholders, pathToString, resolveVariant, } from './resolver.js';
|
|
2
|
+
import { loadUserConfig, hasConfigPath } from './loader.js';
|
|
3
|
+
import { loadSkillDefinitions, createSkillLookup, expandSkillReferences, } from './skills.js';
|
|
4
|
+
import { getConfigType } from './parser.js';
|
|
6
5
|
/**
|
|
7
6
|
* Validate config requirements for execute tasks
|
|
8
|
-
* Returns missing config
|
|
7
|
+
* Returns validation result with missing config and validation errors
|
|
9
8
|
*/
|
|
10
9
|
export function validateExecuteTasks(tasks) {
|
|
11
10
|
const userConfig = loadUserConfig();
|
|
12
11
|
const missing = [];
|
|
13
12
|
const seenPaths = new Set();
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
13
|
+
const validationErrors = [];
|
|
14
|
+
const seenSkills = new Set();
|
|
15
|
+
// Load all skills (including invalid ones for validation)
|
|
16
|
+
const parsedSkills = loadSkillDefinitions();
|
|
17
|
+
const skillLookup = createSkillLookup(parsedSkills);
|
|
18
|
+
// Check for invalid skills being used in tasks
|
|
19
|
+
for (const task of tasks) {
|
|
20
|
+
const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
|
|
21
|
+
if (skillName && !seenSkills.has(skillName)) {
|
|
22
|
+
seenSkills.add(skillName);
|
|
23
|
+
// Check if this skill is invalid
|
|
24
|
+
const skill = skillLookup(skillName);
|
|
25
|
+
if (skill && !skill.isValid) {
|
|
26
|
+
validationErrors.push({
|
|
27
|
+
skill: skill.name,
|
|
28
|
+
issues: [skill.validationError || 'Unknown validation error'],
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// If there are validation errors, return early
|
|
34
|
+
if (validationErrors.length > 0) {
|
|
35
|
+
return {
|
|
36
|
+
missingConfig: [],
|
|
37
|
+
validationErrors,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
20
40
|
for (const task of tasks) {
|
|
21
41
|
// Check if task originates from a skill
|
|
22
42
|
const skillName = typeof task.params?.skill === 'string' ? task.params.skill : null;
|
|
23
43
|
if (skillName) {
|
|
24
44
|
// Task comes from a skill - check skill's Execution section
|
|
25
45
|
const skill = skillLookup(skillName);
|
|
26
|
-
if (!skill
|
|
46
|
+
if (!skill) {
|
|
27
47
|
continue;
|
|
28
48
|
}
|
|
29
49
|
// Get variant from task params (if any)
|
|
@@ -106,5 +126,8 @@ export function validateExecuteTasks(tasks) {
|
|
|
106
126
|
}
|
|
107
127
|
}
|
|
108
128
|
}
|
|
109
|
-
return
|
|
129
|
+
return {
|
|
130
|
+
missingConfig: missing,
|
|
131
|
+
validationErrors: [],
|
|
132
|
+
};
|
|
110
133
|
}
|
|
@@ -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
|
package/dist/ui/Command.js
CHANGED
|
@@ -8,7 +8,7 @@ import { createPlanDefinition } from '../services/components.js';
|
|
|
8
8
|
import { formatErrorMessage } from '../services/messages.js';
|
|
9
9
|
import { useInput } from '../services/keyboard.js';
|
|
10
10
|
import { handleRefinement } from '../services/refinement.js';
|
|
11
|
-
import { routeTasksWithConfirm } from '../services/
|
|
11
|
+
import { routeTasksWithConfirm } from '../services/router.js';
|
|
12
12
|
import { ensureMinimumTime } from '../services/timing.js';
|
|
13
13
|
import { Spinner } from './Spinner.js';
|
|
14
14
|
import { UserQuery } from './UserQuery.js';
|
package/dist/ui/Execute.js
CHANGED
|
@@ -7,8 +7,8 @@ import { useInput } from '../services/keyboard.js';
|
|
|
7
7
|
import { formatErrorMessage } from '../services/messages.js';
|
|
8
8
|
import { formatDuration } from '../services/utils.js';
|
|
9
9
|
import { ExecutionStatus, executeCommands, } from '../services/shell.js';
|
|
10
|
-
import { replacePlaceholders } from '../services/
|
|
11
|
-
import { loadUserConfig } from '../services/
|
|
10
|
+
import { replacePlaceholders } from '../services/resolver.js';
|
|
11
|
+
import { loadUserConfig } from '../services/loader.js';
|
|
12
12
|
import { ensureMinimumTime } from '../services/timing.js';
|
|
13
13
|
import { Spinner } from './Spinner.js';
|
|
14
14
|
const MINIMUM_PROCESSING_TIME = 400;
|
package/dist/ui/Introspect.js
CHANGED
|
@@ -24,23 +24,31 @@ function parseCapabilityFromTask(task) {
|
|
|
24
24
|
const colonIndex = task.action.indexOf(':');
|
|
25
25
|
if (colonIndex === -1) {
|
|
26
26
|
const upperName = task.action.toUpperCase();
|
|
27
|
+
// Check for status markers
|
|
28
|
+
const isIncomplete = task.action.includes('(INCOMPLETE)');
|
|
29
|
+
const cleanName = task.action.replace(/\s*\(INCOMPLETE\)\s*/gi, '').trim();
|
|
27
30
|
return {
|
|
28
|
-
name:
|
|
31
|
+
name: cleanName,
|
|
29
32
|
description: '',
|
|
30
33
|
isBuiltIn: BUILT_IN_CAPABILITIES.has(upperName),
|
|
31
34
|
isIndirect: INDIRECT_CAPABILITIES.has(upperName),
|
|
35
|
+
isIncomplete,
|
|
32
36
|
};
|
|
33
37
|
}
|
|
34
38
|
const name = task.action.substring(0, colonIndex).trim();
|
|
35
39
|
const description = task.action.substring(colonIndex + 1).trim();
|
|
36
|
-
|
|
40
|
+
// Check for status markers
|
|
41
|
+
const isIncomplete = name.includes('(INCOMPLETE)');
|
|
42
|
+
const cleanName = name.replace(/\s*\(INCOMPLETE\)\s*/gi, '').trim();
|
|
43
|
+
const upperName = cleanName.toUpperCase();
|
|
37
44
|
const isBuiltIn = BUILT_IN_CAPABILITIES.has(upperName);
|
|
38
45
|
const isIndirect = INDIRECT_CAPABILITIES.has(upperName);
|
|
39
46
|
return {
|
|
40
|
-
name,
|
|
47
|
+
name: cleanName,
|
|
41
48
|
description,
|
|
42
49
|
isBuiltIn,
|
|
43
50
|
isIndirect,
|
|
51
|
+
isIncomplete,
|
|
44
52
|
};
|
|
45
53
|
}
|
|
46
54
|
export function Introspect({ tasks, state, status, service, children, debug = false, handlers, }) {
|
package/dist/ui/Report.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { Colors } from '../services/colors.js';
|
|
4
|
-
function CapabilityItem({ name, description, isBuiltIn, isIndirect, }) {
|
|
4
|
+
function CapabilityItem({ name, description, isBuiltIn, isIndirect, isIncomplete, }) {
|
|
5
5
|
const color = isIndirect
|
|
6
6
|
? Colors.Origin.Indirect
|
|
7
7
|
: isBuiltIn
|
|
8
8
|
? Colors.Origin.BuiltIn
|
|
9
9
|
: Colors.Origin.UserProvided;
|
|
10
|
-
return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] })] }));
|
|
10
|
+
return (_jsxs(Box, { children: [_jsx(Text, { children: "- " }), _jsx(Text, { color: color, children: name }), _jsxs(Text, { children: [" - ", description] }), isIncomplete && _jsx(Text, { color: Colors.Status.Warning, children: " (incomplete)" })] }));
|
|
11
11
|
}
|
|
12
12
|
export function Report({ message, capabilities }) {
|
|
13
|
-
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect }, index))) })] }));
|
|
13
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Box, { marginLeft: 1, children: _jsx(Text, { children: message }) }), _jsx(Box, { flexDirection: "column", marginLeft: 3, marginTop: 1, children: capabilities.map((capability, index) => (_jsx(CapabilityItem, { name: capability.name, description: capability.description, isBuiltIn: capability.isBuiltIn, isIndirect: capability.isIndirect, isIncomplete: capability.isIncomplete }, index))) })] }));
|
|
14
14
|
}
|
package/dist/ui/Validate.js
CHANGED
|
@@ -128,9 +128,15 @@ export function Validate({ missingConfig, userRequest, state, status, service, c
|
|
|
128
128
|
for (const [section, sectionConfig] of Object.entries(configBySection)) {
|
|
129
129
|
saveConfig(section, sectionConfig);
|
|
130
130
|
}
|
|
131
|
+
// Mark validation component as complete before invoking callback
|
|
132
|
+
// This allows the workflow to proceed to execution
|
|
133
|
+
handlers?.completeActive();
|
|
134
|
+
// Invoke callback which will queue the Execute component
|
|
131
135
|
onComplete?.(configRequirements);
|
|
132
136
|
};
|
|
133
137
|
const handleConfigAborted = (operation) => {
|
|
138
|
+
// Mark validation component as complete when aborted
|
|
139
|
+
handlers?.completeActive();
|
|
134
140
|
onAborted(operation);
|
|
135
141
|
};
|
|
136
142
|
return (_jsxs(Box, { alignSelf: "flex-start", flexDirection: "column", children: [isActive && !completionMessage && !error && (_jsxs(Box, { marginLeft: 1, children: [_jsxs(Text, { color: getTextColor(isActive), children: ["Validating configuration requirements.", ' '] }), _jsx(Spinner, {})] })), completionMessage && (_jsx(Box, { marginLeft: 1, children: _jsx(Text, { color: getTextColor(isActive), children: completionMessage }) })), error && (_jsx(Box, { marginTop: 1, children: _jsxs(Text, { color: Colors.Status.Error, children: ["Error: ", error] }) })), configSteps && !error && (_jsx(Box, { marginTop: 1, children: _jsx(Config, { steps: configSteps, status: status, debug: debug, onFinished: handleConfigFinished, onAborted: handleConfigAborted, handlers: handlers }) })), children] }));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "prompt-language-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.8",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"scripts": {
|
|
14
14
|
"clean": "rm -rf dist",
|
|
15
15
|
"typecheck": "tsc --project tsconfig.eslint.json",
|
|
16
|
-
"build": "npm run typecheck && npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/
|
|
16
|
+
"build": "npm run typecheck && npm run clean && tsc && chmod +x dist/index.js && mkdir -p dist/skills && cp src/skills/*.md dist/skills/",
|
|
17
17
|
"dev": "npm run build && tsc --watch",
|
|
18
18
|
"prepare": "husky",
|
|
19
19
|
"prepublishOnly": "npm run check",
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Parse skill reference from execution line
|
|
3
|
-
* Returns skill name if line is a reference, otherwise null
|
|
4
|
-
*/
|
|
5
|
-
export function parseSkillReference(line) {
|
|
6
|
-
const match = line.match(/^\[(.+)\]$/);
|
|
7
|
-
return match ? match[1].trim() : null;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Check if execution line is a skill reference
|
|
11
|
-
*/
|
|
12
|
-
export function isSkillReference(line) {
|
|
13
|
-
return /^\[.+\]$/.test(line.trim());
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Expand skill references in execution commands
|
|
17
|
-
* Returns expanded execution lines with references replaced
|
|
18
|
-
* Throws error if circular reference detected
|
|
19
|
-
*/
|
|
20
|
-
export function expandSkillReferences(execution, skillLookup, visited = new Set()) {
|
|
21
|
-
const expanded = [];
|
|
22
|
-
for (const line of execution) {
|
|
23
|
-
const skillName = parseSkillReference(line);
|
|
24
|
-
if (!skillName) {
|
|
25
|
-
// Not a reference, keep as-is
|
|
26
|
-
expanded.push(line);
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
// Check for circular reference
|
|
30
|
-
if (visited.has(skillName)) {
|
|
31
|
-
throw new Error(`Circular skill reference detected: ${Array.from(visited).join(' → ')} → ${skillName}`);
|
|
32
|
-
}
|
|
33
|
-
// Look up referenced skill
|
|
34
|
-
const skill = skillLookup(skillName);
|
|
35
|
-
if (!skill) {
|
|
36
|
-
// Referenced skill not found, keep as-is
|
|
37
|
-
expanded.push(line);
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
if (!skill.execution || skill.execution.length === 0) {
|
|
41
|
-
// Referenced skill has no execution, skip
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
// Recursively expand referenced skill's execution
|
|
45
|
-
const newVisited = new Set(visited);
|
|
46
|
-
newVisited.add(skillName);
|
|
47
|
-
const referencedExecution = expandSkillReferences(skill.execution, skillLookup, newVisited);
|
|
48
|
-
expanded.push(...referencedExecution);
|
|
49
|
-
}
|
|
50
|
-
return expanded;
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Get all skill names referenced in execution (including nested)
|
|
54
|
-
* Returns unique set of skill names
|
|
55
|
-
*/
|
|
56
|
-
export function getReferencedSkills(execution, skillLookup, visited = new Set()) {
|
|
57
|
-
const referenced = new Set();
|
|
58
|
-
for (const line of execution) {
|
|
59
|
-
const skillName = parseSkillReference(line);
|
|
60
|
-
if (!skillName || visited.has(skillName)) {
|
|
61
|
-
continue;
|
|
62
|
-
}
|
|
63
|
-
referenced.add(skillName);
|
|
64
|
-
const skill = skillLookup(skillName);
|
|
65
|
-
if (skill && skill.execution) {
|
|
66
|
-
const newVisited = new Set(visited);
|
|
67
|
-
newVisited.add(skillName);
|
|
68
|
-
const nested = getReferencedSkills(skill.execution, skillLookup, newVisited);
|
|
69
|
-
for (const name of nested) {
|
|
70
|
-
referenced.add(name);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
return referenced;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Validate skill references don't form cycles
|
|
78
|
-
* Returns true if valid, false if circular reference detected
|
|
79
|
-
*/
|
|
80
|
-
export function validateNoCycles(execution, skillLookup, visited = new Set()) {
|
|
81
|
-
try {
|
|
82
|
-
expandSkillReferences(execution, skillLookup, visited);
|
|
83
|
-
return true;
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
if (error instanceof Error && error.message.includes('Circular')) {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
throw error;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|