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.
@@ -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
+ }