Skip to content

Commit 46a4d78

Browse files
clay-goodclaude
andauthored
feat(skills): publish workflow skills to skills.sh (#1357)
* feat(skills): publish workflow skills to skills.sh Commit the 12 OpenSpec workflow skills as static skills/<name>/SKILL.md so `npx skills add Fission-AI/OpenSpec` can install them (skills.sh reads static files from the repo; OpenSpec otherwise only generates skills at init time). Files are generated from the existing templates via `pnpm generate:skills`, not hand-copied, and skillssh-parity.test.ts fails CI if a template changes without regenerating. The volatile generatedBy frontmatter line is stripped so the committed copies stay byte-stable across releases. Closes #1258 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): force LF on committed skills/ so Windows CI parity holds The skills.sh distribution files are generated LF-only and compared byte-for-byte by skillssh-parity.test.ts. Windows autocrlf checked them out as CRLF, failing the parity assertion. A scoped .gitattributes pins them to LF on checkout. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(skills): reject symlinks and assert the exact committed skill set Review feedback (alfred): the parity test only visited expected templates, so an extra or renamed skills/ directory shipped with green CI, and the generator would write through a pre-existing symlinked skill directory to anywhere on disk. - generator: refuse to run if skills/ contains any symlink (checked before any deletion, so a bad tree is left intact), validate dirNames against a path-segment allowlist, and lstat the target before writing. - parity test: assert skills/ holds exactly README.md plus one real directory per template, each containing a single real SKILL.md. - focused tests cover symlink refusal (no partial deletion), traversal names, and stale-directory cleanup. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(templates): abort archive on Cancel, honest summary, fence languages CodeRabbit review on #1357, fixed at the template source and regenerated: - archive-change: choosing "Cancel" at the sync prompt now stops the flow instead of archiving anyway (skill + command templates). - archive-change skill: the success output no longer hardcodes "All artifacts complete. All tasks complete." when archiving incomplete work. - archive/bulk-archive/sync-specs/verify-change: language identifiers on previously plain code fences (MD040), skill and command twins alike. Golden hashes in skill-templates-parity.test.ts recomputed from dist/; skills/ regenerated via pnpm generate:skills. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ac656c9 commit 46a4d78

24 files changed

Lines changed: 2512 additions & 46 deletions

File tree

.gitattributes

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# The skills.sh distribution files are generated LF-only and compared
2+
# byte-for-byte by test/core/templates/skillssh-parity.test.ts. Force LF on
3+
# checkout so Windows autocrlf doesn't turn them into CRLF and fail parity.
4+
skills/** text eol=lf

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"scripts": {
4343
"lint": "eslint src/",
4444
"build": "node build.js",
45+
"generate:skills": "node scripts/generate-skillssh.mjs",
4546
"dev": "tsc --watch",
4647
"dev:cli": "pnpm build && node bin/openspec.js",
4748
"test": "vitest run",

scripts/generate-skillssh.mjs

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/usr/bin/env node
2+
3+
/**
4+
* Generate the static skills.sh distribution of the OpenSpec workflow skills.
5+
*
6+
* skills.sh installs skills by reading committed `SKILL.md` files straight from
7+
* a GitHub repo (`npx skills add Fission-AI/OpenSpec`). OpenSpec normally
8+
* *generates* these skills into a user's project via `openspec init`, so this
9+
* script mirrors that same output into a committed `skills/<name>/SKILL.md`
10+
* tree that skills.sh can discover.
11+
*
12+
* The committed copies are kept honest by `test/core/templates/skillssh-parity.test.ts`,
13+
* which regenerates and diffs against disk. Run this after any skill-template
14+
* change: `pnpm build && pnpm generate:skills`.
15+
*/
16+
17+
import { writeFileSync } from 'node:fs';
18+
import { dirname, join } from 'node:path';
19+
import { fileURLToPath } from 'node:url';
20+
21+
import { getSkillTemplates, generateSkillContent } from '../dist/core/shared/skill-generation.js';
22+
import {
23+
cleanSkillSubdirectories,
24+
prepareSkillDirectory,
25+
stripVolatileFrontmatter,
26+
SKILLS_DIR,
27+
} from './skillssh-shared.mjs';
28+
29+
const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..');
30+
const outDir = join(repoRoot, SKILLS_DIR);
31+
32+
cleanSkillSubdirectories(outDir);
33+
34+
let count = 0;
35+
for (const { template, dirName } of getSkillTemplates()) {
36+
const content = stripVolatileFrontmatter(generateSkillContent(template, 'skills.sh'));
37+
const skillDir = prepareSkillDirectory(outDir, dirName);
38+
writeFileSync(join(skillDir, 'SKILL.md'), content, 'utf8');
39+
count++;
40+
}
41+
42+
console.log(`Generated ${count} skills into ${SKILLS_DIR}/`);

scripts/skillssh-shared.mjs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/**
2+
* Shared helpers for the skills.sh distribution generator and its parity test.
3+
*/
4+
5+
import { lstatSync, mkdirSync, readdirSync, rmSync } from 'node:fs';
6+
import { join } from 'node:path';
7+
8+
/** Directory (repo-relative) that skills.sh scans for `SKILL.md` files. */
9+
export const SKILLS_DIR = 'skills';
10+
11+
/**
12+
* Drop the per-release `generatedBy` frontmatter line so the committed
13+
* skills.sh copies stay byte-stable across OpenSpec version bumps. The line is
14+
* meaningful only for skills that `openspec init` writes into a project; in the
15+
* standalone distribution it would just churn the files on every release.
16+
*/
17+
export function stripVolatileFrontmatter(content) {
18+
return content.replace(/^ {2}generatedBy: .*\n/m, '');
19+
}
20+
21+
/**
22+
* Remove existing skill subdirectories (clears any renamed/removed skills)
23+
* while preserving top-level files like README.md. Refuses to run if the tree
24+
* contains a symlink: deleting one would only unlink it, and a symlinked skill
25+
* directory would otherwise let later writes land outside the repo.
26+
*/
27+
export function cleanSkillSubdirectories(outDir) {
28+
mkdirSync(outDir, { recursive: true });
29+
const entries = readdirSync(outDir, { withFileTypes: true });
30+
// Reject before deleting anything so a bad tree is left fully intact.
31+
for (const entry of entries) {
32+
if (entry.isSymbolicLink()) {
33+
throw new Error(
34+
`Refusing to generate: ${join(outDir, entry.name)} is a symlink. Remove it and re-run.`
35+
);
36+
}
37+
}
38+
for (const entry of entries) {
39+
if (entry.isDirectory()) {
40+
rmSync(join(outDir, entry.name), { recursive: true, force: true });
41+
}
42+
}
43+
}
44+
45+
/**
46+
* Create `<outDir>/<dirName>` and return its path, guaranteeing the write
47+
* target is a real directory contained in outDir — never a path-traversing
48+
* name and never a symlink that would redirect the write elsewhere.
49+
*/
50+
export function prepareSkillDirectory(outDir, dirName) {
51+
if (!/^[a-z0-9][a-z0-9-]*$/.test(dirName)) {
52+
throw new Error(`Refusing to generate: unsafe skill directory name ${JSON.stringify(dirName)}`);
53+
}
54+
const skillDir = join(outDir, dirName);
55+
mkdirSync(skillDir, { recursive: true });
56+
if (!lstatSync(skillDir).isDirectory()) {
57+
throw new Error(`Refusing to write through ${skillDir}: not a real directory.`);
58+
}
59+
return skillDir;
60+
}

skills/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# OpenSpec skills for skills.sh
2+
3+
Install the OpenSpec workflow skills into any [skills.sh](https://skills.sh)-compatible agent:
4+
5+
```bash
6+
npx skills add Fission-AI/OpenSpec
7+
```
8+
9+
Each `openspec-*/SKILL.md` here is the same skill `openspec init` writes into a
10+
project. The skills drive the `openspec` CLI, so for the full setup (CLI +
11+
`openspec/` project scaffolding + slash commands) run:
12+
13+
```bash
14+
npx openspec@latest init
15+
```
16+
17+
> These files are generated from the skill templates — do not edit by hand. Run
18+
> `pnpm build && pnpm generate:skills` after changing a template;
19+
> `skillssh-parity.test.ts` fails if they drift.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
---
2+
name: openspec-apply-change
3+
description: Implement tasks from an OpenSpec change. Use when the user wants to start implementing, continue implementation, or work through tasks.
4+
allowed-tools: Bash(openspec:*)
5+
license: MIT
6+
compatibility: Requires openspec CLI.
7+
metadata:
8+
author: openspec
9+
version: "1.0"
10+
---
11+
12+
Implement tasks from an OpenSpec change.
13+
14+
**Store selection:** If the user names a store (a store is a standalone OpenSpec repo registered on this machine) or the work lives in one, run `openspec store list --json` to discover registered store ids, then pass `--store <id>` on the commands that read or write specs and changes (`new change`, `status`, `instructions`, `list`, `show`, `validate`, `archive`, `doctor`, `context`). Other commands do not take the flag. Hints printed by commands already carry the flag; keep it on follow-ups. Without a store, commands act on the nearest local `openspec/` root.
15+
16+
**Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
17+
18+
**Steps**
19+
20+
1. **Select the change**
21+
22+
If a name is provided, use it. Otherwise:
23+
- Infer from conversation context if the user mentioned a change
24+
- Auto-select if only one active change exists
25+
- If ambiguous, run `openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
26+
27+
Always announce: "Using change: <name>" and how to override (e.g., `/opsx:apply <other>`).
28+
29+
2. **Check status to understand the schema**
30+
```bash
31+
openspec status --change "<name>" --json
32+
```
33+
Parse the JSON to understand:
34+
- `schemaName`: The workflow being used (e.g., "spec-driven")
35+
- `planningHome`, `changeRoot`, and `actionContext`: planning scope and edit constraints
36+
- Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
37+
38+
3. **Get apply instructions**
39+
40+
```bash
41+
openspec instructions apply --change "<name>" --json
42+
```
43+
44+
This returns:
45+
- `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
46+
- Progress (total, complete, remaining)
47+
- Task list with status
48+
- Dynamic instruction based on current state
49+
50+
**Handle states:**
51+
- If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
52+
- If `state: "all_done"`: congratulate, suggest archive
53+
- Otherwise: proceed to implementation
54+
55+
4. **Read context files**
56+
57+
Read every file path listed under `contextFiles` from the apply instructions output.
58+
The files depend on the schema being used:
59+
- **spec-driven**: proposal, specs, design, tasks
60+
- Other schemas: follow the contextFiles from CLI output
61+
62+
5. **Show current progress**
63+
64+
Display:
65+
- Schema being used
66+
- Progress: "N/M tasks complete"
67+
- Remaining tasks overview
68+
- Dynamic instruction from CLI
69+
70+
6. **Implement tasks (loop until done or blocked)**
71+
72+
For each pending task:
73+
- Show which task is being worked on
74+
- Make the code changes required
75+
- Keep changes minimal and focused
76+
- Mark task complete in the tasks file: `- [ ]``- [x]`
77+
- Continue to next task
78+
79+
**Pause if:**
80+
- Task is unclear → ask for clarification
81+
- Implementation reveals a design issue → suggest updating artifacts
82+
- Error or blocker encountered → report and wait for guidance
83+
- User interrupts
84+
85+
7. **On completion or pause, show status**
86+
87+
Display:
88+
- Tasks completed this session
89+
- Overall progress: "N/M tasks complete"
90+
- If all done: suggest archive
91+
- If paused: explain why and wait for guidance
92+
93+
**Output During Implementation**
94+
95+
```
96+
## Implementing: <change-name> (schema: <schema-name>)
97+
98+
Working on task 3/7: <task description>
99+
[...implementation happening...]
100+
✓ Task complete
101+
102+
Working on task 4/7: <task description>
103+
[...implementation happening...]
104+
✓ Task complete
105+
```
106+
107+
**Output On Completion**
108+
109+
```
110+
## Implementation Complete
111+
112+
**Change:** <change-name>
113+
**Schema:** <schema-name>
114+
**Progress:** 7/7 tasks complete ✓
115+
116+
### Completed This Session
117+
- [x] Task 1
118+
- [x] Task 2
119+
...
120+
121+
All tasks complete! Ready to archive this change.
122+
```
123+
124+
**Output On Pause (Issue Encountered)**
125+
126+
```
127+
## Implementation Paused
128+
129+
**Change:** <change-name>
130+
**Schema:** <schema-name>
131+
**Progress:** 4/7 tasks complete
132+
133+
### Issue Encountered
134+
<description of the issue>
135+
136+
**Options:**
137+
1. <option 1>
138+
2. <option 2>
139+
3. Other approach
140+
141+
What would you like to do?
142+
```
143+
144+
**Guardrails**
145+
- Keep going through tasks until done or blocked
146+
- Always read context files before starting (from the apply instructions output)
147+
- If task is ambiguous, pause and ask before implementing
148+
- If implementation reveals issues, pause and suggest artifact updates
149+
- Keep code changes minimal and scoped to each task
150+
- Update task checkbox immediately after completing each task
151+
- Pause on errors, blockers, or unclear requirements - don't guess
152+
- Use contextFiles from CLI output, don't assume specific file names
153+
154+
**Fluid Workflow Integration**
155+
156+
This skill supports the "actions on a change" model:
157+
158+
- **Can be invoked anytime**: Before all artifacts are done (if tasks exist), after partial implementation, interleaved with other actions
159+
- **Allows artifact updates**: If implementation reveals design issues, suggest updating artifacts - not phase-locked, work fluidly

0 commit comments

Comments
 (0)