daedalion 0.0.1
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/LICENSE +21 -0
- package/README.md +133 -0
- package/bin/daedalion.js +95 -0
- package/package.json +41 -0
- package/src/commands/build.js +198 -0
- package/src/commands/clean.js +85 -0
- package/src/commands/init.js +88 -0
- package/src/commands/validate.js +141 -0
- package/src/config.js +50 -0
- package/src/generators/agent.js +121 -0
- package/src/generators/instructions.js +65 -0
- package/src/generators/prompt.js +113 -0
- package/src/generators/skill.js +105 -0
- package/src/generators/tools.js +183 -0
- package/src/generators/workflow.js +65 -0
- package/src/index.js +14 -0
- package/src/parsers/proposal.js +52 -0
- package/src/parsers/spec.js +105 -0
- package/src/parsers/tasks.js +46 -0
- package/src/utils.js +51 -0
- package/src/version.js +60 -0
- package/templates/init/daedalion.yaml +18 -0
- package/templates/init/openspec/changes/example-feature/proposal.md +13 -0
- package/templates/init/openspec/changes/example-feature/tasks.md +19 -0
- package/templates/init/openspec/project.md +15 -0
- package/templates/init/openspec/specs/example/spec.md +37 -0
|
@@ -0,0 +1,141 @@
|
|
|
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
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { readFileSync, existsSync } from 'fs';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { ensureDir } from '../utils.js';
|
|
5
|
+
|
|
6
|
+
export function generateInstructions(openspecDir, outputDir, options = {}) {
|
|
7
|
+
const projectPath = join(openspecDir, 'project.md');
|
|
8
|
+
const instructionsPath = join(outputDir, 'copilot-instructions.md');
|
|
9
|
+
|
|
10
|
+
let content;
|
|
11
|
+
|
|
12
|
+
if (existsSync(projectPath)) {
|
|
13
|
+
const projectContent = readFileSync(projectPath, 'utf-8');
|
|
14
|
+
content = transformProjectToInstructions(projectContent);
|
|
15
|
+
} else {
|
|
16
|
+
content = generateDefaultInstructions();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (options.dryRun) {
|
|
20
|
+
return { path: instructionsPath, content };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ensureDir(instructionsPath);
|
|
24
|
+
writeFileSync(instructionsPath, content);
|
|
25
|
+
return { path: instructionsPath, content };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function transformProjectToInstructions(projectContent) {
|
|
29
|
+
// Note: GitHub Copilot natively reads AGENTS.md files, so no reference needed here.
|
|
30
|
+
// See: https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions
|
|
31
|
+
const header = `# Copilot Instructions
|
|
32
|
+
|
|
33
|
+
> Auto-generated by Daedalion from openspec/project.md
|
|
34
|
+
> Do not edit directly - changes will be overwritten.
|
|
35
|
+
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
return header + projectContent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function generateDefaultInstructions() {
|
|
42
|
+
// Note: GitHub Copilot natively reads AGENTS.md files, so no reference needed here.
|
|
43
|
+
// See: https://docs.github.com/en/copilot/how-tos/configure-custom-instructions/add-repository-instructions
|
|
44
|
+
return `# Copilot Instructions
|
|
45
|
+
|
|
46
|
+
> Auto-generated by Daedalion
|
|
47
|
+
> Do not edit directly - changes will be overwritten.
|
|
48
|
+
|
|
49
|
+
## Project Overview
|
|
50
|
+
|
|
51
|
+
This project uses OpenSpec for specification-driven development.
|
|
52
|
+
|
|
53
|
+
## Working with Specifications
|
|
54
|
+
|
|
55
|
+
- Specs are in \`openspec/specs/\`
|
|
56
|
+
- Active changes are in \`openspec/changes/\`
|
|
57
|
+
- Use the available skills and agents for implementation guidance
|
|
58
|
+
|
|
59
|
+
## Available Resources
|
|
60
|
+
|
|
61
|
+
Check the \`.github/skills/\` directory for implementation guides.
|
|
62
|
+
Check the \`.github/agents/\` directory for specialized agents.
|
|
63
|
+
Check the \`.github/prompts/\` directory for slash commands.
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { ensureDir } from '../utils.js';
|
|
2
|
+
import { writeFileSync } from 'fs';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
export function generatePrompt(proposal, tasks, domain, outputDir, options = {}) {
|
|
6
|
+
const promptPath = join(outputDir, 'prompts', `${proposal.changeName}.prompt.md`);
|
|
7
|
+
|
|
8
|
+
const content = `---
|
|
9
|
+
description: ${proposal.title}
|
|
10
|
+
agent: ${domain || 'default'}
|
|
11
|
+
---
|
|
12
|
+
Implement the ${proposal.changeName} change proposal.
|
|
13
|
+
|
|
14
|
+
## Context
|
|
15
|
+
${proposal.why || 'No context provided.'}
|
|
16
|
+
|
|
17
|
+
## Scope
|
|
18
|
+
${proposal.what || 'No scope defined.'}
|
|
19
|
+
|
|
20
|
+
## Reference
|
|
21
|
+
- Proposal: openspec/changes/${proposal.changeName}/proposal.md
|
|
22
|
+
- Tasks: openspec/changes/${proposal.changeName}/tasks.md
|
|
23
|
+
|
|
24
|
+
## Skills
|
|
25
|
+
- #${domain || 'default'}
|
|
26
|
+
${tasks && tasks.items.length > 0 ? `
|
|
27
|
+
## Tasks
|
|
28
|
+
${tasks.items.map(t => `- [ ] ${t}`).join('\n')}
|
|
29
|
+
${tasks.hasMore ? `\n> See full list in tasks.md` : ''}` : ''}
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
if (options.dryRun) {
|
|
33
|
+
return { path: promptPath, content };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
ensureDir(promptPath);
|
|
37
|
+
writeFileSync(promptPath, content);
|
|
38
|
+
return { path: promptPath, content };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function generateCyclePrompt(outputDir, options = {}) {
|
|
42
|
+
const promptPath = join(outputDir, 'prompts', 'daedalion-openspec-cycle.prompt.md');
|
|
43
|
+
const content = `---
|
|
44
|
+
description: OpenSpec cycle coordinator
|
|
45
|
+
agent: default
|
|
46
|
+
---
|
|
47
|
+
You are the OpenSpec cycle coordinator. Route requests to the correct OpenSpec prompt based on the project phase.
|
|
48
|
+
|
|
49
|
+
## Core Principle
|
|
50
|
+
**NEVER start coding until the specs are clear, agreed upon, and explicitly approved by the human.**
|
|
51
|
+
|
|
52
|
+
## Source of Truth
|
|
53
|
+
- \`openspec/specs/\` = approved, canonical specifications (never edit directly)
|
|
54
|
+
- \`openspec/changes/<change-id>/specs/\` = temporary delta specs (additions/modifications/removals) that merge to canonical specs after approval
|
|
55
|
+
- \`openspec/AGENTS.md\` = tool-specific integration instructions
|
|
56
|
+
- \`openspec/project.md\` = project conventions (tech stack, naming rules, patterns)
|
|
57
|
+
|
|
58
|
+
## Helpful CLI Commands
|
|
59
|
+
- Use \`openspec view\` to see the status of proposals, tasks, and specs.
|
|
60
|
+
- Use \`openspec view <change-id>\` to inspect the proposal, tasks, and spec deltas.
|
|
61
|
+
- Use \`openspec list --changes\` or \`openspec list --specs\` for detailed views
|
|
62
|
+
- Use \`openspec --help\` to see all available commands.
|
|
63
|
+
|
|
64
|
+
## Inputs
|
|
65
|
+
- User request
|
|
66
|
+
- Optional change-id
|
|
67
|
+
- Spec approval status
|
|
68
|
+
- Task completion status
|
|
69
|
+
|
|
70
|
+
## Workflow Phases
|
|
71
|
+
|
|
72
|
+
### Phase 1: Draft (Proposal + Spec Deltas)
|
|
73
|
+
- Create \`proposal.md\` (why, goals, scope, non-goals, risks)
|
|
74
|
+
- Create \`tasks.md\` (numbered checklist with \`- [ ]\` checkboxes)
|
|
75
|
+
- Create spec deltas in \`specs/<module>/spec.md\` with sections:
|
|
76
|
+
- \`## ADDED Requirements\`
|
|
77
|
+
- \`## MODIFIED Requirements\`
|
|
78
|
+
- \`## REMOVED Requirements\`
|
|
79
|
+
- Each requirement uses SHALL/MUST language
|
|
80
|
+
- Each requirement includes \`#### Scenario:\` blocks with WHEN → THEN outcomes
|
|
81
|
+
|
|
82
|
+
### Phase 2: Review & Refine
|
|
83
|
+
- Iterate on proposal, tasks, and delta specs based on human feedback
|
|
84
|
+
- **Do NOT proceed to code until human explicitly approves specs**
|
|
85
|
+
|
|
86
|
+
### Phase 3: Implement
|
|
87
|
+
- Follow \`tasks.md\` checklist step-by-step
|
|
88
|
+
- Update tasks: change \`- [ ]\` to \`- [x]\` as you complete items
|
|
89
|
+
- Refine delta specs if needed (keep accurate)
|
|
90
|
+
- Human confirms when all tasks are complete
|
|
91
|
+
|
|
92
|
+
### Phase 4: Archive
|
|
93
|
+
- Merge approved deltas from \`openspec/changes/<id>/specs/\` into \`openspec/specs/\`
|
|
94
|
+
- Move change folder to \`openspec/archive/\`
|
|
95
|
+
- **Requires human confirmation** (CLI: \`openspec archive <change-id> --yes\`)
|
|
96
|
+
|
|
97
|
+
## Rules
|
|
98
|
+
1. If no change-id exists or the request is to start new work → run @openspec-proposal.prompt.md to create proposal, tasks, and spec deltas. Ask for approval before coding.
|
|
99
|
+
2. If specs are approved and implementation is requested → run @openspec-apply.prompt.md.
|
|
100
|
+
3. If all tasks are complete and the user confirms → run @openspec-archive.prompt.md.
|
|
101
|
+
4. If asked for status → summarize proposal + specs + tasks progress. Recommend the next phase clearly.
|
|
102
|
+
|
|
103
|
+
Always state the current phase (Draft/Review/Implement/Ready to Archive/Archived) and the next action.
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
if (options.dryRun) {
|
|
107
|
+
return { path: promptPath, content };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
ensureDir(promptPath);
|
|
111
|
+
writeFileSync(promptPath, content);
|
|
112
|
+
return { path: promptPath, content };
|
|
113
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
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 baseFrontmatter = {
|
|
14
|
+
name: spec.domain,
|
|
15
|
+
description: `${description}. Use when working on ${keywords}.`
|
|
16
|
+
};
|
|
17
|
+
const specFrontmatter = spec.frontmatter && typeof spec.frontmatter === 'object'
|
|
18
|
+
? spec.frontmatter
|
|
19
|
+
: {};
|
|
20
|
+
const frontmatter = { ...baseFrontmatter, ...specFrontmatter };
|
|
21
|
+
const agentInstructions = typeof specFrontmatter.agent_instructions === 'string'
|
|
22
|
+
? specFrontmatter.agent_instructions
|
|
23
|
+
: null;
|
|
24
|
+
|
|
25
|
+
const frontmatterYaml = YAML.stringify(frontmatter).trimEnd();
|
|
26
|
+
|
|
27
|
+
let content = `---
|
|
28
|
+
${frontmatterYaml}
|
|
29
|
+
---
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
if (agentInstructions && agentInstructions.trim()) {
|
|
33
|
+
content += `# Agent Instructions
|
|
34
|
+
|
|
35
|
+
${agentInstructions.trimEnd()}
|
|
36
|
+
|
|
37
|
+
`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
content += `# ${spec.title}
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
${spec.requirements.map(r => `- **${r.name}**: ${r.description}`).join('\n')}
|
|
44
|
+
|
|
45
|
+
## Acceptance Criteria
|
|
46
|
+
${generateAcceptanceCriteria(spec.requirements)}`;
|
|
47
|
+
|
|
48
|
+
if (tasks && tasks.items.length > 0) {
|
|
49
|
+
content += `
|
|
50
|
+
|
|
51
|
+
## Active Tasks
|
|
52
|
+
${tasks.items.map(t => `- ${t}`).join('\n')}
|
|
53
|
+
${tasks.hasMore ? `\n> Full task list: openspec/changes/*/tasks.md` : ''}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
content += '\n';
|
|
57
|
+
|
|
58
|
+
if (options.dryRun) {
|
|
59
|
+
return { path: skillPath, content };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ensureDir(skillPath);
|
|
63
|
+
writeFileSync(skillPath, content);
|
|
64
|
+
return { path: skillPath, content };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function generateDescription(spec) {
|
|
68
|
+
if (spec.requirements.length === 0) {
|
|
69
|
+
return spec.title;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const firstReq = spec.requirements[0];
|
|
73
|
+
const desc = firstReq.description || firstReq.name;
|
|
74
|
+
return desc.slice(0, 100).replace(/\.$/, '');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function extractKeywords(spec) {
|
|
78
|
+
const words = new Set();
|
|
79
|
+
words.add(spec.domain);
|
|
80
|
+
|
|
81
|
+
for (const req of spec.requirements) {
|
|
82
|
+
const nameWords = req.name.toLowerCase().split(/\s+/);
|
|
83
|
+
nameWords.forEach(w => {
|
|
84
|
+
if (w.length > 3) words.add(w);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return Array.from(words).slice(0, 5).join(', ');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function generateAcceptanceCriteria(requirements) {
|
|
92
|
+
const criteria = [];
|
|
93
|
+
|
|
94
|
+
for (const req of requirements) {
|
|
95
|
+
for (const scenario of req.scenarios) {
|
|
96
|
+
criteria.push(`### ${scenario.name}`);
|
|
97
|
+
for (const step of scenario.steps) {
|
|
98
|
+
criteria.push(`- ${step}`);
|
|
99
|
+
}
|
|
100
|
+
criteria.push('');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return criteria.join('\n').trim();
|
|
105
|
+
}
|