daedalion 0.0.2 → 0.1.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.
- package/README.md +5 -0
- package/bin/daedalion.js +11 -5
- package/dist/commands/build.d.ts +3 -0
- package/dist/commands/build.d.ts.map +1 -0
- package/dist/commands/build.js +167 -0
- package/dist/commands/build.js.map +1 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.d.ts.map +1 -0
- package/dist/commands/clean.js +106 -0
- package/dist/commands/clean.js.map +1 -0
- package/dist/commands/init.d.ts +3 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +83 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +119 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +101 -0
- package/dist/config.js.map +1 -0
- package/dist/generators/agent.d.ts +3 -0
- package/dist/generators/agent.d.ts.map +1 -0
- package/dist/generators/agent.js +105 -0
- package/dist/generators/agent.js.map +1 -0
- package/dist/generators/instructions.d.ts +3 -0
- package/dist/generators/instructions.d.ts.map +1 -0
- package/{src → dist}/generators/instructions.js +42 -52
- package/dist/generators/instructions.js.map +1 -0
- package/dist/generators/prompt.d.ts +4 -0
- package/dist/generators/prompt.d.ts.map +1 -0
- package/{src → dist}/generators/prompt.js +96 -102
- package/dist/generators/prompt.js.map +1 -0
- package/dist/generators/skill.d.ts +3 -0
- package/dist/generators/skill.d.ts.map +1 -0
- package/dist/generators/skill.js +89 -0
- package/dist/generators/skill.js.map +1 -0
- package/dist/generators/tools.d.ts +3 -0
- package/dist/generators/tools.d.ts.map +1 -0
- package/dist/generators/tools.js +192 -0
- package/dist/generators/tools.js.map +1 -0
- package/dist/generators/workflow.d.ts +3 -0
- package/dist/generators/workflow.d.ts.map +1 -0
- package/{src → dist}/generators/workflow.js +47 -53
- package/dist/generators/workflow.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/{src → dist}/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/proposal.d.ts +3 -0
- package/dist/parsers/proposal.d.ts.map +1 -0
- package/dist/parsers/proposal.js +45 -0
- package/dist/parsers/proposal.js.map +1 -0
- package/dist/parsers/spec.d.ts +3 -0
- package/dist/parsers/spec.d.ts.map +1 -0
- package/dist/parsers/spec.js +87 -0
- package/dist/parsers/spec.js.map +1 -0
- package/dist/parsers/tasks.d.ts +4 -0
- package/dist/parsers/tasks.d.ts.map +1 -0
- package/dist/parsers/tasks.js +39 -0
- package/dist/parsers/tasks.js.map +1 -0
- package/dist/types.d.ts +107 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +5 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +46 -0
- package/dist/utils.js.map +1 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +47 -0
- package/dist/version.js.map +1 -0
- package/package.json +19 -4
- package/src/commands/build.js +0 -198
- package/src/commands/clean.js +0 -85
- package/src/commands/init.js +0 -101
- package/src/commands/validate.js +0 -141
- package/src/config.js +0 -50
- package/src/generators/agent.js +0 -121
- package/src/generators/skill.js +0 -108
- package/src/generators/tools.js +0 -183
- package/src/parsers/proposal.js +0 -52
- package/src/parsers/spec.js +0 -105
- package/src/parsers/tasks.js +0 -46
- package/src/utils.js +0 -51
- package/src/version.js +0 -60
package/src/commands/init.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, copyFileSync, readdirSync, statSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
-
import { join, dirname, relative } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
import chalk from 'chalk';
|
|
5
|
-
import { VERSION } from '../version.js';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = dirname(__filename);
|
|
9
|
-
|
|
10
|
-
export async function init(cwd, options = {}) {
|
|
11
|
-
console.log();
|
|
12
|
-
console.log(chalk.bold(` Daedalion v${VERSION}`));
|
|
13
|
-
console.log();
|
|
14
|
-
|
|
15
|
-
const templatesDir = join(__dirname, '../../templates/init');
|
|
16
|
-
const files = getAllFiles(templatesDir);
|
|
17
|
-
const { agentTarget, withExample } = options;
|
|
18
|
-
|
|
19
|
-
let created = 0;
|
|
20
|
-
let skipped = 0;
|
|
21
|
-
|
|
22
|
-
for (const file of files) {
|
|
23
|
-
const relativePath = relative(templatesDir, file);
|
|
24
|
-
const targetPath = join(cwd, relativePath);
|
|
25
|
-
|
|
26
|
-
// Skip example files if --with-example not provided
|
|
27
|
-
if (!withExample && isExampleFile(relativePath)) {
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (existsSync(targetPath)) {
|
|
32
|
-
console.log(chalk.yellow(` ⚠ ${relativePath} already exists, skipping`));
|
|
33
|
-
skipped++;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const dir = dirname(targetPath);
|
|
38
|
-
if (!existsSync(dir)) {
|
|
39
|
-
mkdirSync(dir, { recursive: true });
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
copyFileSync(file, targetPath);
|
|
43
|
-
console.log(chalk.green(` ✓ ${relativePath}`));
|
|
44
|
-
created++;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (agentTarget === 'sdk') {
|
|
48
|
-
const configPath = join(cwd, 'daedalion.yaml');
|
|
49
|
-
if (existsSync(configPath)) {
|
|
50
|
-
let config = readFileSync(configPath, 'utf-8');
|
|
51
|
-
config = config.replace(/agents:\s*\n\s*target:\s*ide/, 'agents:\n target: sdk');
|
|
52
|
-
writeFileSync(configPath, config);
|
|
53
|
-
console.log(chalk.cyan(` ✓ Updated daedalion.yaml with SDK target`));
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
console.log();
|
|
58
|
-
if (created > 0) {
|
|
59
|
-
console.log(chalk.green(` Done. ${created} files created.`));
|
|
60
|
-
}
|
|
61
|
-
if (skipped > 0) {
|
|
62
|
-
console.log(chalk.yellow(` ${skipped} files skipped (already exist).`));
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (agentTarget === 'sdk') {
|
|
66
|
-
console.log();
|
|
67
|
-
console.log(chalk.cyan(` Agent target set to SDK (for CI pipelines)`));
|
|
68
|
-
console.log(chalk.gray(` Edit daedalion.yaml to add custom tool names under agents.tools`));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
console.log();
|
|
72
|
-
console.log(' Next steps:');
|
|
73
|
-
console.log(' 1. Edit openspec/specs/example/spec.md with your specifications');
|
|
74
|
-
console.log(' 2. Run `daedalion build` to generate GitHub Copilot artifacts');
|
|
75
|
-
console.log();
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function getAllFiles(dir, files = []) {
|
|
79
|
-
const entries = readdirSync(dir);
|
|
80
|
-
|
|
81
|
-
for (const entry of entries) {
|
|
82
|
-
const fullPath = join(dir, entry);
|
|
83
|
-
const stat = statSync(fullPath);
|
|
84
|
-
|
|
85
|
-
if (stat.isDirectory()) {
|
|
86
|
-
getAllFiles(fullPath, files);
|
|
87
|
-
} else {
|
|
88
|
-
files.push(fullPath);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return files;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function isExampleFile(relativePath) {
|
|
96
|
-
// Example files are in specs/example/ or changes/example-feature/
|
|
97
|
-
return relativePath.includes('specs/example/') ||
|
|
98
|
-
relativePath.includes('specs\\example\\') ||
|
|
99
|
-
relativePath.includes('changes/example-feature/') ||
|
|
100
|
-
relativePath.includes('changes\\example-feature\\');
|
|
101
|
-
}
|
package/src/commands/validate.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
import { existsSync, readdirSync, statSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import chalk from 'chalk';
|
|
4
|
-
import { VERSION } from '../version.js';
|
|
5
|
-
import { glob } from 'glob';
|
|
6
|
-
import { loadConfig, resolveOpenspecPath, resolveOutputPath } from '../config.js';
|
|
7
|
-
import { parseSpec } from '../parsers/spec.js';
|
|
8
|
-
import { parseProposal } from '../parsers/proposal.js';
|
|
9
|
-
|
|
10
|
-
export async function validate(cwd) {
|
|
11
|
-
console.log();
|
|
12
|
-
console.log(chalk.bold(` Daedalion v${VERSION} - Validate`));
|
|
13
|
-
console.log();
|
|
14
|
-
|
|
15
|
-
const config = loadConfig(cwd);
|
|
16
|
-
const openspecDir = resolveOpenspecPath(cwd, config);
|
|
17
|
-
const outputDir = resolveOutputPath(cwd, config);
|
|
18
|
-
|
|
19
|
-
const errors = [];
|
|
20
|
-
|
|
21
|
-
// Parse all specs
|
|
22
|
-
const specs = await findAndParseSpecs(openspecDir);
|
|
23
|
-
const changes = await findAndParseChanges(openspecDir);
|
|
24
|
-
|
|
25
|
-
// Rule 1: Every spec has ≥1 requirement
|
|
26
|
-
for (const spec of specs) {
|
|
27
|
-
if (spec.requirements.length === 0) {
|
|
28
|
-
errors.push({
|
|
29
|
-
rule: 'spec-has-requirements',
|
|
30
|
-
message: `Spec "${spec.path}" has no requirements.\n Add at least one "### Requirement:" section.`
|
|
31
|
-
});
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Rule 2: Every requirement has ≥1 scenario
|
|
36
|
-
for (const spec of specs) {
|
|
37
|
-
for (const req of spec.requirements) {
|
|
38
|
-
if (req.scenarios.length === 0) {
|
|
39
|
-
errors.push({
|
|
40
|
-
rule: 'requirement-has-scenarios',
|
|
41
|
-
message: `Requirement "${req.name}" in ${spec.domain} lacks scenarios.\n Add at least one "#### Scenario:" section.`
|
|
42
|
-
});
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Rule 3: Generated skill exists for each spec
|
|
48
|
-
for (const spec of specs) {
|
|
49
|
-
const skillPath = join(outputDir, 'skills', spec.domain, 'SKILL.md');
|
|
50
|
-
if (!existsSync(skillPath)) {
|
|
51
|
-
errors.push({
|
|
52
|
-
rule: 'skill-exists-for-spec',
|
|
53
|
-
message: `Missing skill for spec ${spec.domain}.\n Run 'daedalion build' to generate.`
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Rule 4: Generated prompt exists for each active change
|
|
59
|
-
for (const change of changes) {
|
|
60
|
-
const promptPath = join(outputDir, 'prompts', `${change.changeName}.prompt.md`);
|
|
61
|
-
if (!existsSync(promptPath)) {
|
|
62
|
-
errors.push({
|
|
63
|
-
rule: 'prompt-exists-for-change',
|
|
64
|
-
message: `Missing prompt for change ${change.changeName}.\n Run 'daedalion build' to generate.`
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Rule 5: No orphaned skills
|
|
70
|
-
const skillsDir = join(outputDir, 'skills');
|
|
71
|
-
if (existsSync(skillsDir)) {
|
|
72
|
-
const skillDirs = readdirSync(skillsDir).filter(name => {
|
|
73
|
-
const fullPath = join(skillsDir, name);
|
|
74
|
-
return statSync(fullPath).isDirectory();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
for (const skillName of skillDirs) {
|
|
78
|
-
const hasSpec = specs.some(s => s.domain === skillName);
|
|
79
|
-
if (!hasSpec) {
|
|
80
|
-
errors.push({
|
|
81
|
-
rule: 'no-orphan-skills',
|
|
82
|
-
message: `Orphan skill: ${skillName} has no source spec.\n Remove .github/skills/${skillName}/ or create openspec/specs/${skillName}/spec.md`
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Output results
|
|
89
|
-
if (errors.length === 0) {
|
|
90
|
-
console.log(chalk.green(' ✓ All validations passed'));
|
|
91
|
-
console.log();
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
console.log(chalk.red(` ✗ ${errors.length} validation error(s) found:`));
|
|
96
|
-
console.log();
|
|
97
|
-
|
|
98
|
-
for (const error of errors) {
|
|
99
|
-
console.log(chalk.red(` Error: ${error.message}`));
|
|
100
|
-
console.log();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
async function findAndParseSpecs(openspecDir) {
|
|
107
|
-
const specsDir = join(openspecDir, 'specs');
|
|
108
|
-
if (!existsSync(specsDir)) {
|
|
109
|
-
return [];
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const specFiles = await glob('*/spec.md', { cwd: specsDir });
|
|
113
|
-
return specFiles.map(file => parseSpec(join(specsDir, file)));
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
async function findAndParseChanges(openspecDir) {
|
|
117
|
-
const changesDir = join(openspecDir, 'changes');
|
|
118
|
-
if (!existsSync(changesDir)) {
|
|
119
|
-
return [];
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const changes = [];
|
|
123
|
-
const changeDirs = readdirSync(changesDir).filter(name => {
|
|
124
|
-
const fullPath = join(changesDir, name);
|
|
125
|
-
return statSync(fullPath).isDirectory();
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
for (const changeDir of changeDirs) {
|
|
129
|
-
const proposalPath = join(changesDir, changeDir, 'proposal.md');
|
|
130
|
-
|
|
131
|
-
if (existsSync(proposalPath)) {
|
|
132
|
-
const proposal = parseProposal(proposalPath);
|
|
133
|
-
changes.push({
|
|
134
|
-
changeName: proposal.changeName,
|
|
135
|
-
path: proposalPath
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
return changes;
|
|
141
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import yaml from 'yaml';
|
|
4
|
-
|
|
5
|
-
const DEFAULT_CONFIG = {
|
|
6
|
-
version: 1,
|
|
7
|
-
target: 'github',
|
|
8
|
-
openspec: './openspec',
|
|
9
|
-
output: './.github',
|
|
10
|
-
ci: {
|
|
11
|
-
auto_commit: false,
|
|
12
|
-
commit_message: 'chore: regenerate agents from specs'
|
|
13
|
-
},
|
|
14
|
-
agents: {
|
|
15
|
-
target: 'ide', // 'ide' or 'sdk'
|
|
16
|
-
tools: null // null to use IDE defaults, or array of custom tool names
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export function loadConfig(cwd) {
|
|
21
|
-
const configPath = join(cwd, 'daedalion.yaml');
|
|
22
|
-
|
|
23
|
-
if (!existsSync(configPath)) {
|
|
24
|
-
return { ...DEFAULT_CONFIG };
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const content = readFileSync(configPath, 'utf-8');
|
|
28
|
-
const userConfig = yaml.parse(content) || {};
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
...DEFAULT_CONFIG,
|
|
32
|
-
...userConfig,
|
|
33
|
-
ci: {
|
|
34
|
-
...DEFAULT_CONFIG.ci,
|
|
35
|
-
...(userConfig.ci || {})
|
|
36
|
-
},
|
|
37
|
-
agents: {
|
|
38
|
-
...DEFAULT_CONFIG.agents,
|
|
39
|
-
...(userConfig.agents || {})
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export function resolveOpenspecPath(cwd, config) {
|
|
45
|
-
return join(cwd, config.openspec);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function resolveOutputPath(cwd, config) {
|
|
49
|
-
return join(cwd, config.output);
|
|
50
|
-
}
|
package/src/generators/agent.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { ensureDir } from '../utils.js';
|
|
2
|
-
import { writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
|
|
5
|
-
export function generateAgent(spec, outputDir, options = {}, config = {}) {
|
|
6
|
-
const agentPath = join(outputDir, 'agents', `${spec.domain}.agent.md`);
|
|
7
|
-
|
|
8
|
-
const skillDescription = generateSkillDescription(spec);
|
|
9
|
-
const agentConfig = config.agents || {};
|
|
10
|
-
const target = agentConfig.target || 'ide';
|
|
11
|
-
|
|
12
|
-
let tools;
|
|
13
|
-
let workflow;
|
|
14
|
-
|
|
15
|
-
if (target === 'sdk') {
|
|
16
|
-
const specTools = extractTools(spec);
|
|
17
|
-
tools = specTools.length > 0 ? specTools.map(t => t.name) : (agentConfig.tools || []);
|
|
18
|
-
workflow = generateSDKWorkflow(spec, tools);
|
|
19
|
-
} else {
|
|
20
|
-
tools = ['edit', 'search', 'terminal'];
|
|
21
|
-
workflow = generateIDEWorkflow(spec);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const toolsYaml = tools.length > 0
|
|
25
|
-
? `tools: [${tools.map(t => `'${t}'`).join(', ')}]`
|
|
26
|
-
: '';
|
|
27
|
-
|
|
28
|
-
const content = `---
|
|
29
|
-
name: ${spec.domain}
|
|
30
|
-
description: Implements ${spec.domain} features following specifications
|
|
31
|
-
${toolsYaml}
|
|
32
|
-
---
|
|
33
|
-
# ${spec.domain} Agent
|
|
34
|
-
|
|
35
|
-
${workflow}
|
|
36
|
-
`;
|
|
37
|
-
|
|
38
|
-
if (options.dryRun) {
|
|
39
|
-
return { path: agentPath, content };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
ensureDir(agentPath);
|
|
43
|
-
writeFileSync(agentPath, content);
|
|
44
|
-
return { path: agentPath, content };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function extractTools(spec) {
|
|
48
|
-
const tools = [];
|
|
49
|
-
const frontmatter = spec.frontmatter || {};
|
|
50
|
-
|
|
51
|
-
if (frontmatter.tools && Array.isArray(frontmatter.tools)) {
|
|
52
|
-
for (const tool of frontmatter.tools) {
|
|
53
|
-
if (typeof tool === 'string') {
|
|
54
|
-
tools.push({ name: tool });
|
|
55
|
-
} else if (tool.name) {
|
|
56
|
-
tools.push(tool);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return tools;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function generateIDEWorkflow(spec) {
|
|
65
|
-
const skillDescription = generateSkillDescription(spec);
|
|
66
|
-
|
|
67
|
-
return `You implement ${spec.domain} features following the specification.
|
|
68
|
-
|
|
69
|
-
## Available Skills
|
|
70
|
-
- **#${spec.domain}** — ${skillDescription}
|
|
71
|
-
|
|
72
|
-
## Workflow
|
|
73
|
-
1. Read the #${spec.domain} skill for requirements
|
|
74
|
-
2. Implement following acceptance criteria
|
|
75
|
-
3. Verify all scenarios pass
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function generateSDKWorkflow(spec, tools) {
|
|
80
|
-
const skillDescription = generateSkillDescription(spec);
|
|
81
|
-
|
|
82
|
-
if (tools.length === 0) {
|
|
83
|
-
return `You implement ${spec.domain} features following the specification.
|
|
84
|
-
|
|
85
|
-
## Available Skills
|
|
86
|
-
- **#${spec.domain}** — ${skillDescription}
|
|
87
|
-
|
|
88
|
-
## Workflow
|
|
89
|
-
1. Read the #${spec.domain} skill for requirements
|
|
90
|
-
2. Implement following acceptance criteria
|
|
91
|
-
3. Verify all scenarios pass
|
|
92
|
-
`;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return `You implement ${spec.domain} features following the specification.
|
|
96
|
-
|
|
97
|
-
## Available Skills
|
|
98
|
-
- **#${spec.domain}** — ${skillDescription}
|
|
99
|
-
|
|
100
|
-
## Available Tools
|
|
101
|
-
${tools.map(t => `- **${t}**` ).join('\n')}
|
|
102
|
-
|
|
103
|
-
## Workflow
|
|
104
|
-
1. Read the #${spec.domain} skill for requirements
|
|
105
|
-
2. Use available tools to implement following acceptance criteria
|
|
106
|
-
3. Verify all scenarios pass
|
|
107
|
-
`;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function generateSkillDescription(spec) {
|
|
111
|
-
if (spec.requirements.length === 0) {
|
|
112
|
-
return `${spec.title} requirements and acceptance criteria`;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const reqNames = spec.requirements
|
|
116
|
-
.slice(0, 3)
|
|
117
|
-
.map(r => r.name.toLowerCase())
|
|
118
|
-
.join(', ');
|
|
119
|
-
|
|
120
|
-
return `${spec.title} - ${reqNames}`;
|
|
121
|
-
}
|
package/src/generators/skill.js
DELETED
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
import { ensureDir } from '../utils.js';
|
|
2
|
-
import { writeFileSync } from 'fs';
|
|
3
|
-
import { join } from 'path';
|
|
4
|
-
import YAML from 'yaml';
|
|
5
|
-
|
|
6
|
-
export function generateSkill(spec, tasks, outputDir, options = {}) {
|
|
7
|
-
const skillDir = join(outputDir, 'skills', spec.domain);
|
|
8
|
-
const skillPath = join(skillDir, 'SKILL.md');
|
|
9
|
-
|
|
10
|
-
const description = generateDescription(spec);
|
|
11
|
-
const keywords = extractKeywords(spec);
|
|
12
|
-
|
|
13
|
-
const specFrontmatter = spec.frontmatter && typeof spec.frontmatter === 'object'
|
|
14
|
-
? spec.frontmatter
|
|
15
|
-
: {};
|
|
16
|
-
|
|
17
|
-
// Build frontmatter, preserving spec frontmatter (including tools) while providing defaults
|
|
18
|
-
const frontmatter = {
|
|
19
|
-
name: specFrontmatter.name || spec.domain,
|
|
20
|
-
description: specFrontmatter.description || `${description}. Use when working on ${keywords}.`,
|
|
21
|
-
...specFrontmatter // Preserve all other frontmatter properties (tools, etc.)
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const agentInstructions = typeof specFrontmatter.agent_instructions === 'string'
|
|
25
|
-
? specFrontmatter.agent_instructions
|
|
26
|
-
: null;
|
|
27
|
-
|
|
28
|
-
const frontmatterYaml = YAML.stringify(frontmatter).trimEnd();
|
|
29
|
-
|
|
30
|
-
let content = `---
|
|
31
|
-
${frontmatterYaml}
|
|
32
|
-
---
|
|
33
|
-
`;
|
|
34
|
-
|
|
35
|
-
if (agentInstructions && agentInstructions.trim()) {
|
|
36
|
-
content += `# Agent Instructions
|
|
37
|
-
|
|
38
|
-
${agentInstructions.trimEnd()}
|
|
39
|
-
|
|
40
|
-
`;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
content += `# ${spec.title}
|
|
44
|
-
|
|
45
|
-
## Requirements
|
|
46
|
-
${spec.requirements.map(r => `- **${r.name}**: ${r.description}`).join('\n')}
|
|
47
|
-
|
|
48
|
-
## Acceptance Criteria
|
|
49
|
-
${generateAcceptanceCriteria(spec.requirements)}`;
|
|
50
|
-
|
|
51
|
-
if (tasks && tasks.items.length > 0) {
|
|
52
|
-
content += `
|
|
53
|
-
|
|
54
|
-
## Active Tasks
|
|
55
|
-
${tasks.items.map(t => `- ${t}`).join('\n')}
|
|
56
|
-
${tasks.hasMore ? `\n> Full task list: openspec/changes/*/tasks.md` : ''}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
content += '\n';
|
|
60
|
-
|
|
61
|
-
if (options.dryRun) {
|
|
62
|
-
return { path: skillPath, content };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
ensureDir(skillPath);
|
|
66
|
-
writeFileSync(skillPath, content);
|
|
67
|
-
return { path: skillPath, content };
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function generateDescription(spec) {
|
|
71
|
-
if (spec.requirements.length === 0) {
|
|
72
|
-
return spec.title;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const firstReq = spec.requirements[0];
|
|
76
|
-
const desc = firstReq.description || firstReq.name;
|
|
77
|
-
return desc.slice(0, 100).replace(/\.$/, '');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function extractKeywords(spec) {
|
|
81
|
-
const words = new Set();
|
|
82
|
-
words.add(spec.domain);
|
|
83
|
-
|
|
84
|
-
for (const req of spec.requirements) {
|
|
85
|
-
const nameWords = req.name.toLowerCase().split(/\s+/);
|
|
86
|
-
nameWords.forEach(w => {
|
|
87
|
-
if (w.length > 3) words.add(w);
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return Array.from(words).slice(0, 5).join(', ');
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
function generateAcceptanceCriteria(requirements) {
|
|
95
|
-
const criteria = [];
|
|
96
|
-
|
|
97
|
-
for (const req of requirements) {
|
|
98
|
-
for (const scenario of req.scenarios) {
|
|
99
|
-
criteria.push(`### ${scenario.name}`);
|
|
100
|
-
for (const step of scenario.steps) {
|
|
101
|
-
criteria.push(`- ${step}`);
|
|
102
|
-
}
|
|
103
|
-
criteria.push('');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return criteria.join('\n').trim();
|
|
108
|
-
}
|
package/src/generators/tools.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { writeFileSync } from 'fs';
|
|
2
|
-
import { dirname, join } from 'path';
|
|
3
|
-
import { ensureDir } from '../utils.js';
|
|
4
|
-
|
|
5
|
-
export function generateTools(spec, outputDir, config = {}, options = {}) {
|
|
6
|
-
const tools = extractTools(spec);
|
|
7
|
-
const language = config.tools?.language || 'python';
|
|
8
|
-
const generated = [];
|
|
9
|
-
|
|
10
|
-
for (const tool of tools) {
|
|
11
|
-
const stubResult = generateToolStub(tool, spec, outputDir, language, options);
|
|
12
|
-
generated.push(stubResult);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return generated;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function extractTools(spec) {
|
|
19
|
-
const tools = [];
|
|
20
|
-
const frontmatter = spec.frontmatter || {};
|
|
21
|
-
|
|
22
|
-
if (frontmatter.tools && Array.isArray(frontmatter.tools)) {
|
|
23
|
-
for (const tool of frontmatter.tools) {
|
|
24
|
-
if (typeof tool === 'string') {
|
|
25
|
-
tools.push({ name: tool });
|
|
26
|
-
} else if (tool.name) {
|
|
27
|
-
tools.push(tool);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return tools;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function mapPythonType(type) {
|
|
36
|
-
const typeMap = {
|
|
37
|
-
'string': 'str',
|
|
38
|
-
'String': 'str',
|
|
39
|
-
'number': 'int | float',
|
|
40
|
-
'Number': 'int | float',
|
|
41
|
-
'object': 'dict',
|
|
42
|
-
'Object': 'dict',
|
|
43
|
-
'array': 'Array',
|
|
44
|
-
'Array': 'Array',
|
|
45
|
-
'boolean': 'bool',
|
|
46
|
-
'Boolean': 'bool'
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
if (typeof type !== 'string') {
|
|
50
|
-
return 'Any';
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
return typeMap[type] || type || 'Any';
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function getAllRequirementIds(spec) {
|
|
57
|
-
return spec.requirements.map((req, i) => ({
|
|
58
|
-
id: `REQ-${String(i + 1).padStart(3, '0')}`,
|
|
59
|
-
name: req.name
|
|
60
|
-
}));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function filterRelevantRequirements(spec, tool) {
|
|
64
|
-
if (!tool.requirements || !Array.isArray(tool.requirements)) {
|
|
65
|
-
return getAllRequirementIds(spec).slice(0, 2);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const allReqs = getAllRequirementIds(spec);
|
|
69
|
-
const toolReqIds = tool.requirements.map(r => {
|
|
70
|
-
if (typeof r === 'string' && r.startsWith('REQ-')) {
|
|
71
|
-
return r;
|
|
72
|
-
}
|
|
73
|
-
const idx = parseInt(r) || 1;
|
|
74
|
-
return `REQ-${String(idx).padStart(3, '0')}`;
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
return allReqs.filter(req => toolReqIds.includes(req.id));
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function generatePythonStub(tool, spec, filePath, options) {
|
|
81
|
-
const toolReqs = filterRelevantRequirements(spec, tool);
|
|
82
|
-
const toolReqIds = toolReqs.map(r => r.id);
|
|
83
|
-
|
|
84
|
-
let content = `"""
|
|
85
|
-
${tool.description || tool.name}
|
|
86
|
-
Requirements: ${toolReqIds.join(', ') || 'TODO'}
|
|
87
|
-
"""
|
|
88
|
-
|
|
89
|
-
`;
|
|
90
|
-
|
|
91
|
-
content += `def ${tool.name}(${generatePythonSignature(tool)}) -> ${mapPythonType(tool.outputs?.[0] || 'dict')}:\n`;
|
|
92
|
-
content += ` """
|
|
93
|
-
${tool.description || 'TODO: Add description'}
|
|
94
|
-
\n`;
|
|
95
|
-
|
|
96
|
-
if (tool.inputs && tool.inputs.length > 0) {
|
|
97
|
-
content += ` Args:\n`;
|
|
98
|
-
for (const input of tool.inputs) {
|
|
99
|
-
content += ` ${input.name}: ${mapPythonType(input.type || 'Any')} - ${input.description || 'TODO'}\n`;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (tool.outputs && tool.outputs.length > 0) {
|
|
104
|
-
content += ` \n Returns:\n`;
|
|
105
|
-
content += ` ${mapPythonType(tool.outputs[0] || 'dict')}\n`;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (toolReqIds.length > 0) {
|
|
109
|
-
content += ` \n Spec References:\n`;
|
|
110
|
-
for (const ref of toolReqIds) {
|
|
111
|
-
content += ` - ${ref}\n`;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
content += ` """\n`;
|
|
116
|
-
content += ` raise NotImplementedError("Implement ${tool.name} logic")\n`;
|
|
117
|
-
|
|
118
|
-
if (options.dryRun) {
|
|
119
|
-
return { path: filePath, content };
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
ensureDir(filePath);
|
|
123
|
-
writeFileSync(filePath, content, 'utf-8');
|
|
124
|
-
return { path: filePath, content };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function generateToolStub(tool, spec, outputDir, language, options) {
|
|
128
|
-
const toolsDir = join(outputDir, 'tools');
|
|
129
|
-
const ext = getLanguageExt(language);
|
|
130
|
-
const toolPath = join(toolsDir, `${tool.name.toLowerCase()}${ext}`);
|
|
131
|
-
|
|
132
|
-
if (language === 'python') {
|
|
133
|
-
return generatePythonStub(tool, spec, toolPath, options);
|
|
134
|
-
} else if (language === 'javascript') {
|
|
135
|
-
return generateJavaScriptStub(tool, spec, toolPath, options);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function generatePythonSignature(tool) {
|
|
140
|
-
if (!tool.inputs || tool.inputs.length === 0) {
|
|
141
|
-
return '';
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return tool.inputs.map(input => `${input.name}: ${mapPythonType(input.type)}`).join(', ');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function generateJSSignature(tool) {
|
|
148
|
-
if (!tool.inputs || tool.inputs.length === 0) {
|
|
149
|
-
return '{}';
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return `{ ${tool.inputs.map(input => `${input.name}`).join(', ')} }`;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function getLanguageExt(language) {
|
|
156
|
-
const exts = { python: '.py', javascript: '.js' };
|
|
157
|
-
return exts[language] || '.py';
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
function mapJSType(type) {
|
|
161
|
-
const typeMap = {
|
|
162
|
-
'string': 'string',
|
|
163
|
-
'String': 'string',
|
|
164
|
-
'number': 'number',
|
|
165
|
-
'Number': 'number',
|
|
166
|
-
'object': 'Object',
|
|
167
|
-
'Object': 'Object',
|
|
168
|
-
'array': 'Array',
|
|
169
|
-
'Array': 'Array',
|
|
170
|
-
'boolean': 'boolean',
|
|
171
|
-
'Boolean': 'boolean'
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
if (typeof type !== 'string') {
|
|
175
|
-
return 'any';
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return typeMap[type] || type || 'any';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function camelCase(str) {
|
|
182
|
-
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase());
|
|
183
|
-
}
|