@ericrisco/rsc 0.1.8 → 0.1.10
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 +59 -16
- package/manifest.json +2 -2
- package/package.json +2 -1
- package/scripts/doctor.js +2 -2
- package/scripts/install-apply.js +24 -12
- package/scripts/install-plan.js +2 -2
- package/scripts/rsc.js +45 -20
- package/skills/specify/SKILL.md +25 -14
- package/targets/{codex.js → _md-block.js} +8 -4
- package/targets/claude.js +3 -4
- package/targets/index.js +108 -43
- package/targets/gemini.js +0 -29
package/README.md
CHANGED
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
# `rsc` — 231 agent skills, one CLI, zero bloat
|
|
13
13
|
|
|
14
|
-
**A self-recommending skill catalog for
|
|
14
|
+
**A self-recommending skill catalog for 17 coding assistants** — Claude Code,
|
|
15
|
+
Codex, GitHub Copilot, Cursor, Gemini, Windsurf, Cline, Antigravity, Zed and more.
|
|
15
16
|
Describe what you want in plain language. It reads your repo, installs *only* the
|
|
16
|
-
skills that fit — one at a time —
|
|
17
|
+
skills that fit — one at a time — into every assistant you pick, and keeps them
|
|
18
|
+
equipped as you work.
|
|
17
19
|
|
|
18
20
|
From *"document my company"* to *"ship a FastAPI service"* to *"grow my YouTube
|
|
19
21
|
channel"* — **231 skills across 21 domains**, every one researched against live
|
|
@@ -70,9 +72,17 @@ git clone https://github.com/ericrisco/skills.git ~/rsc-skills
|
|
|
70
72
|
cd ~/rsc-skills && npm install && npm link
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
The first run
|
|
74
|
-
|
|
75
|
-
|
|
75
|
+
The first run asks **which assistants** you want — Claude Code, Codex, Copilot,
|
|
76
|
+
Cursor, Gemini, Windsurf, Cline and 11 more (pick any combination) — and installs
|
|
77
|
+
the **floor**:
|
|
78
|
+
`orient` + `rsc-suggest` (always-on) + `harness` + `init`. In Claude Code it
|
|
79
|
+
also wires a `SessionStart` hook so your assistant proposes new skills on its
|
|
80
|
+
own from then on.
|
|
81
|
+
|
|
82
|
+
Everything stays **in the project**, and the real skill files are written
|
|
83
|
+
**once** to `.rsc/skills/<id>/`. Each assistant you pick gets a lightweight
|
|
84
|
+
symlink back to that shared base — no copy is duplicated across IDEs. (If the
|
|
85
|
+
filesystem can't symlink, it falls back to a real copy automatically.)
|
|
76
86
|
|
|
77
87
|
---
|
|
78
88
|
|
|
@@ -102,18 +112,33 @@ Languages: ↑↓ move · space toggle · a all · enter c
|
|
|
102
112
|
◯ rust
|
|
103
113
|
```
|
|
104
114
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
115
|
+
Then it asks **which assistants** to install for — tick as many as you like:
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
Which assistants do you want to install for? space toggle · a all · enter confirm
|
|
119
|
+
❯ ◉ Claude Code (.claude/skills/) ⟵ detected here
|
|
120
|
+
◉ Codex CLI (AGENTS.md)
|
|
121
|
+
◯ GitHub Copilot (.github/copilot-instructions.md)
|
|
122
|
+
◯ Cursor (.cursor/rules/)
|
|
123
|
+
◉ Windsurf (.windsurf/rules/)
|
|
124
|
+
◯ Cline (.clinerules/)
|
|
125
|
+
…17 in total — Gemini, Antigravity, Zed, Continue, Roo, Amp, opencode, Jules, Junie, Kiro, Aider
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
It detects your stack, asks which assistants to install for (the one it found in
|
|
129
|
+
your folder is pre-marked), installs only what you chose, then prints the exact
|
|
130
|
+
next steps for **Claude Code / Codex / Cursor / Gemini / Antigravity** — and from
|
|
131
|
+
there keeps proposing the skills a task needs.
|
|
108
132
|
|
|
109
133
|
---
|
|
110
134
|
|
|
111
135
|
## The CLI
|
|
112
136
|
|
|
113
137
|
```bash
|
|
114
|
-
rsc # plain-language wizard (recommended)
|
|
138
|
+
rsc # plain-language wizard (recommended) — pick skills AND assistants
|
|
115
139
|
rsc add fastapi postgresdb # install specific skills, by name
|
|
116
140
|
rsc add youtube-api remotion-video # …grow a channel, edit with Remotion
|
|
141
|
+
rsc add fastapi --target claude,codex # install into several assistants at once
|
|
117
142
|
rsc install --profile minimal # the floor: orient + suggest + harness + init
|
|
118
143
|
rsc install --profile core # floor + the full SDD workflow
|
|
119
144
|
rsc install --profile full # everything (all 231)
|
|
@@ -238,15 +263,33 @@ Each with a `02-DOCS` feedback loop that learns from your own results. `remotion
|
|
|
238
263
|
|
|
239
264
|
## Multi-target
|
|
240
265
|
|
|
241
|
-
`skills/<name>/` is the source
|
|
242
|
-
|
|
266
|
+
`skills/<name>/` is the catalog source. On install the real files land **once**
|
|
267
|
+
in the project at `.rsc/skills/<id>/`; each assistant you pick gets a symlink
|
|
268
|
+
(or a converted file) back to that shared base — pick several and nothing is
|
|
269
|
+
duplicated. The wizard asks which ones; `--target a,b` does it non-interactively.
|
|
243
270
|
|
|
244
|
-
| Target |
|
|
271
|
+
| Target | Skill destination (→ `.rsc/skills/<id>/`) | Always-on detector |
|
|
245
272
|
| --- | --- | --- |
|
|
246
|
-
| `claude` |
|
|
247
|
-
| `
|
|
248
|
-
| `
|
|
249
|
-
| `
|
|
273
|
+
| `claude` | `.claude/skills/rsc/<id>/` → symlink | SessionStart hook in `.claude/settings.json` |
|
|
274
|
+
| `codex` | `.codex/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
275
|
+
| `copilot` | `.github/rsc/<id>/` → symlink | block in `.github/copilot-instructions.md` |
|
|
276
|
+
| `cursor` | `.cursor/rules/<id>.mdc` (converted) | always-apply rule |
|
|
277
|
+
| `gemini` | `.gemini/rsc/<id>/` → symlink | block in `GEMINI.md` |
|
|
278
|
+
| `windsurf` | `.windsurf/rsc/<id>/` → symlink | rule in `.windsurf/rules/rsc-suggest.md` |
|
|
279
|
+
| `cline` | `.clinerules/rsc/<id>/` → symlink | rule in `.clinerules/rsc-suggest.md` |
|
|
280
|
+
| `antigravity` | `.antigravity/rsc/<id>/` → symlink | block in `.antigravity/AGENTS.md` |
|
|
281
|
+
| `zed` | `.zed/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
282
|
+
| `continue` | `.continue/rsc/<id>/` → symlink | rule in `.continue/rules/rsc-suggest.md` |
|
|
283
|
+
| `roo` | `.roo/rsc/<id>/` → symlink | rule in `.roo/rules/rsc-suggest.md` |
|
|
284
|
+
| `amp` | `.amp/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
285
|
+
| `opencode` | `.opencode/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
286
|
+
| `jules` | `.jules/rsc/<id>/` → symlink | block in `AGENTS.md` |
|
|
287
|
+
| `junie` | `.junie/rsc/<id>/` → symlink | block in `.junie/guidelines.md` |
|
|
288
|
+
| `kiro` | `.kiro/rsc/<id>/` → symlink | doc in `.kiro/steering/rsc-suggest.md` |
|
|
289
|
+
| `aider` | `.aider/rsc/<id>/` → symlink | block in `CONVENTIONS.md` |
|
|
290
|
+
|
|
291
|
+
> `codex`, `zed`, `amp`, `opencode` and `jules` all share the one root
|
|
292
|
+
> `AGENTS.md`; the block is idempotent, so picking several writes it once.
|
|
250
293
|
|
|
251
294
|
---
|
|
252
295
|
|
package/manifest.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.10",
|
|
3
3
|
"counts": {
|
|
4
4
|
"skills": 231
|
|
5
5
|
},
|
|
@@ -3971,7 +3971,7 @@
|
|
|
3971
3971
|
},
|
|
3972
3972
|
{
|
|
3973
3973
|
"id": "specify",
|
|
3974
|
-
"description": "Use when a feature, change, or product idea is still fuzzy and needs
|
|
3974
|
+
"description": "Use when a feature, change, or product idea is still fuzzy and needs brainstorming into an APPROVED spec BEFORE any planning or code — the brainstorming front door of SDD. Turns a one-line intent into a WHAT/WHY spec (problem, goals, users, scope, behaviour, acceptance) with zero implementation detail, via one-question-at-a-time dialogue, 2-3 proposed approaches with a recommendation, and a design the user approves before anything gets built. Triggers: 'write a spec for…', 'spec this out', 'brainstorm this feature', 'especifica esta feature', 'I want to add X', 'tengo una idea', 'se me ha ocurrido', '¿y si…?', 'wouldn't it be nice if…', 'let's think this through', 'what should this feature do', 'draft a PRD', or any moment someone jumps to HOW before WHAT is agreed. NOT the technical plan (that's `plan`), NOT the de-risking ambiguity sweep (that's `clarify`), NOT project-wide principles (that's `constitution`).",
|
|
3975
3975
|
"tags": [
|
|
3976
3976
|
"sdd",
|
|
3977
3977
|
"spec",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ericrisco/rsc",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
4
4
|
"description": "Eric Risco's agent-skills catalog as a granular, self-recommending CLI installer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"cursor",
|
|
41
41
|
"codex",
|
|
42
42
|
"gemini",
|
|
43
|
+
"antigravity",
|
|
43
44
|
"cli",
|
|
44
45
|
"skills",
|
|
45
46
|
"ai"
|
package/scripts/doctor.js
CHANGED
|
@@ -3,8 +3,8 @@ import { targetPaths } from '../targets/index.js';
|
|
|
3
3
|
import { readState } from './lib/state.js';
|
|
4
4
|
import { loadManifest } from './lib/manifest.js';
|
|
5
5
|
|
|
6
|
-
export function doctor({ target, home }) {
|
|
7
|
-
const paths = targetPaths(target, home);
|
|
6
|
+
export function doctor({ target, home, cwd }) {
|
|
7
|
+
const paths = targetPaths(target, home, cwd);
|
|
8
8
|
const state = readState(paths.stateFile);
|
|
9
9
|
const manifest = loadManifest();
|
|
10
10
|
const report = {
|
package/scripts/install-apply.js
CHANGED
|
@@ -1,36 +1,48 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { rmSync, existsSync } from 'node:fs';
|
|
2
|
+
import { rmSync, existsSync, cpSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { planInstall } from './install-plan.js';
|
|
6
|
-
import { targetPaths, writeSkill, wireHook } from '../targets/index.js';
|
|
6
|
+
import { targetPaths, writeSkill, wireHook, baseDir } from '../targets/index.js';
|
|
7
7
|
import { readState, writeState } from './lib/state.js';
|
|
8
8
|
|
|
9
9
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
// Materialize the real skill files into the project-local base exactly once.
|
|
12
|
+
// Re-installs and other assistants reuse it instead of copying again.
|
|
13
|
+
function ensureBase(id, cwd) {
|
|
14
|
+
const dest = baseDir(id, cwd);
|
|
15
|
+
if (!existsSync(dest)) {
|
|
16
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
17
|
+
cpSync(join(ROOT, 'skills', id), dest, { recursive: true });
|
|
18
|
+
}
|
|
19
|
+
return dest;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function applyInstall({ skillIds, target, home, cwd = process.cwd() }) {
|
|
23
|
+
const paths = targetPaths(target, home, cwd);
|
|
24
|
+
const plan = planInstall({ skillIds, target, home, cwd });
|
|
14
25
|
const state = readState(paths.stateFile);
|
|
15
26
|
for (const step of plan) {
|
|
16
27
|
if (step.kind === 'skill') {
|
|
17
|
-
const
|
|
18
|
-
|
|
28
|
+
const base = ensureBase(step.id, cwd);
|
|
29
|
+
const files = await writeSkill(target, step.id, base, step.to);
|
|
30
|
+
state.skills[step.id] = { files, base };
|
|
19
31
|
} else if (step.kind === 'hook') {
|
|
20
|
-
await wireHook(target, paths, join(
|
|
32
|
+
await wireHook(target, paths, join(ensureBase('suggest', cwd), 'SKILL.md'));
|
|
21
33
|
}
|
|
22
34
|
}
|
|
23
35
|
writeState(paths.stateFile, state);
|
|
24
36
|
return state;
|
|
25
37
|
}
|
|
26
38
|
|
|
27
|
-
export function listInstalled({ target, home }) {
|
|
28
|
-
const paths = targetPaths(target, home);
|
|
39
|
+
export function listInstalled({ target, home, cwd = process.cwd() }) {
|
|
40
|
+
const paths = targetPaths(target, home, cwd);
|
|
29
41
|
return Object.keys(readState(paths.stateFile).skills);
|
|
30
42
|
}
|
|
31
43
|
|
|
32
|
-
export async function uninstall({ skillIds, target, home, dryRun }) {
|
|
33
|
-
const paths = targetPaths(target, home);
|
|
44
|
+
export async function uninstall({ skillIds, target, home, cwd = process.cwd(), dryRun }) {
|
|
45
|
+
const paths = targetPaths(target, home, cwd);
|
|
34
46
|
const state = readState(paths.stateFile);
|
|
35
47
|
const removed = [];
|
|
36
48
|
for (const id of skillIds) {
|
package/scripts/install-plan.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { targetPaths } from '../targets/index.js';
|
|
2
2
|
|
|
3
|
-
export function planInstall({ skillIds, target, home }) {
|
|
4
|
-
const t = targetPaths(target, home);
|
|
3
|
+
export function planInstall({ skillIds, target, home, cwd }) {
|
|
4
|
+
const t = targetPaths(target, home, cwd);
|
|
5
5
|
const plan = [];
|
|
6
6
|
for (const id of skillIds) {
|
|
7
7
|
plan.push({ kind: 'skill', id, from: `skills/${id}`, to: t.skillDir(id) });
|
package/scripts/rsc.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { loadManifest, skillsForProfile } from './lib/manifest.js';
|
|
3
|
-
import { detectTarget } from '../targets/index.js';
|
|
3
|
+
import { detectTarget, TARGETS } from '../targets/index.js';
|
|
4
4
|
import { detectRepo } from './detect-repo.js';
|
|
5
5
|
import { rank } from './consult.js';
|
|
6
6
|
import { expandRecommends, toOutcomes, hasOutcome } from './lib/recommend.js';
|
|
@@ -45,18 +45,26 @@ async function manualSelect() {
|
|
|
45
45
|
return [...chosen];
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
// Ask which assistants to install into. The detected one is pre-labelled but
|
|
49
|
+
// nothing is auto-applied — the user always confirms the set (one or many).
|
|
50
|
+
async function selectAgents() {
|
|
51
|
+
const detected = detectTarget();
|
|
52
|
+
const items = TARGETS.map((t) => ({
|
|
53
|
+
id: t.id,
|
|
54
|
+
label: `${t.label} (${t.hint})${t.id === detected ? ' ⟵ detected here' : ''}`,
|
|
55
|
+
}));
|
|
56
|
+
const chosen = await pickFrom('Which assistants do you want to install for? (space to toggle, a = all)', items);
|
|
57
|
+
return chosen.length ? chosen : [detected];
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
// After installing, remind the user how to actually start — per IDE — and that
|
|
49
61
|
// rsc keeps recommending skills as they work. The harness/SDD *init* runs INSIDE
|
|
50
62
|
// the assistant (with the user present), never blindly from this CLI.
|
|
51
|
-
function printNextSteps(
|
|
63
|
+
function printNextSteps(targets, ids) {
|
|
52
64
|
const hasHarness = ids.includes('harness');
|
|
53
65
|
const hasSdd = ids.includes('sdd') || ids.includes('sdd-init');
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
cursor: 'Open **Cursor** in this project folder.',
|
|
57
|
-
codex: 'Open **Codex CLI** (it reads AGENTS.md) in this project folder.',
|
|
58
|
-
gemini: 'Open **Gemini CLI** in this project folder.',
|
|
59
|
-
}[target] || 'Open your assistant in this project folder.';
|
|
66
|
+
const label = (id) => TARGETS.find((t) => t.id === id)?.label || id;
|
|
67
|
+
const openLine = `Open this project in: ${targets.map((t) => `**${label(t)}**`).join(' · ')}`;
|
|
60
68
|
|
|
61
69
|
say('\n────────────────────────────────────────────────────────');
|
|
62
70
|
say('👉 When you start working (these steps happen in your assistant, not here):');
|
|
@@ -82,7 +90,7 @@ function printNextSteps(target, ids) {
|
|
|
82
90
|
async function wizard() {
|
|
83
91
|
const m = loadManifest();
|
|
84
92
|
await banner();
|
|
85
|
-
say(' the skill catalog for your assistant (Claude Code · Cursor ·
|
|
93
|
+
say(' the skill catalog for your assistant (Claude Code · Codex · Cursor · Gemini · Antigravity)\n');
|
|
86
94
|
const choice = await select('What do you want to do?', [
|
|
87
95
|
{ key: 'base', label: 'Base install — the essentials (orient + suggest + harness + init)' },
|
|
88
96
|
{ key: 'sdd', label: 'Base + Spec-Driven Development — the specify → plan → implement → ship flow' },
|
|
@@ -102,33 +110,50 @@ async function wizard() {
|
|
|
102
110
|
return;
|
|
103
111
|
}
|
|
104
112
|
|
|
105
|
-
const
|
|
106
|
-
say(`\nI'll install ${ids.length} skills
|
|
113
|
+
const targets = await selectAgents();
|
|
114
|
+
say(`\nI'll install ${ids.length} skills for: ${targets.join(', ')}`);
|
|
107
115
|
say(' ' + ids.join(', '));
|
|
116
|
+
say(' (real files live once in .rsc/skills/ — each assistant just links to them)');
|
|
108
117
|
if (!(await confirm('Install it?'))) {
|
|
109
118
|
say('No problem. Anytime: npx @ericrisco/rsc');
|
|
110
119
|
return;
|
|
111
120
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
121
|
+
for (const target of targets) {
|
|
122
|
+
await applyInstall({ skillIds: ids, target });
|
|
123
|
+
say(` ✅ ${target}`);
|
|
124
|
+
}
|
|
125
|
+
say(`\n✅ Installed ${ids.length} skills for ${targets.length} assistant(s).`);
|
|
126
|
+
printNextSteps(targets, ids);
|
|
115
127
|
}
|
|
116
128
|
|
|
117
129
|
async function main() {
|
|
118
|
-
|
|
130
|
+
// --target accepts one id or a comma list (e.g. --target claude,codex). No flag → detect.
|
|
131
|
+
const f = flag('target');
|
|
132
|
+
const targets = typeof f === 'string'
|
|
133
|
+
? f.split(',').map((s) => s.trim()).filter(Boolean)
|
|
134
|
+
: [detectTarget()];
|
|
135
|
+
const target = targets[0];
|
|
119
136
|
switch (cmd) {
|
|
120
137
|
case undefined:
|
|
121
138
|
return wizard();
|
|
122
|
-
case 'add':
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
case 'add': {
|
|
140
|
+
// Positional args = skill ids; skip flags and any flag value (the token after a --flag).
|
|
141
|
+
const requested = [];
|
|
142
|
+
for (let i = 1; i < argv.length; i++) {
|
|
143
|
+
if (argv[i].startsWith('--')) { i++; continue; }
|
|
144
|
+
requested.push(argv[i]);
|
|
145
|
+
}
|
|
146
|
+
const ids = [...new Set(['orient', 'suggest', ...requested])];
|
|
147
|
+
for (const t of targets) await applyInstall({ skillIds: ids, target: t });
|
|
148
|
+
return void say(`✅ Installed for ${targets.join(', ')}: ${requested.join(', ')}`);
|
|
149
|
+
}
|
|
125
150
|
case 'install': {
|
|
126
151
|
const profile = flag('profile') || 'minimal';
|
|
127
152
|
const without = argv.filter((a, i) => argv[i - 1] === '--without');
|
|
128
153
|
let ids = skillsForProfile(loadManifest(), profile);
|
|
129
154
|
ids = [...new Set(['orient', 'suggest', ...ids])].filter((id) => !without.includes(id));
|
|
130
|
-
await applyInstall({ skillIds: ids, target });
|
|
131
|
-
return void say(`✅ Profile '${profile}' installed
|
|
155
|
+
for (const t of targets) await applyInstall({ skillIds: ids, target: t });
|
|
156
|
+
return void say(`✅ Profile '${profile}' installed for ${targets.join(', ')} (${ids.length} skills)`);
|
|
132
157
|
}
|
|
133
158
|
case 'consult': {
|
|
134
159
|
const ids = await recommendIds(argv.slice(1).join(' '));
|
package/skills/specify/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: specify
|
|
3
|
-
description: "Use when a feature, change, or product idea is still fuzzy and needs
|
|
3
|
+
description: "Use when a feature, change, or product idea is still fuzzy and needs brainstorming into an APPROVED spec BEFORE any planning or code — the brainstorming front door of SDD. Turns a one-line intent into a WHAT/WHY spec (problem, goals, users, scope, behaviour, acceptance) with zero implementation detail, via one-question-at-a-time dialogue, 2-3 proposed approaches with a recommendation, and a design the user approves before anything gets built. Triggers: 'write a spec for…', 'spec this out', 'brainstorm this feature', 'especifica esta feature', 'I want to add X', 'tengo una idea', 'se me ha ocurrido', '¿y si…?', 'wouldn't it be nice if…', 'let's think this through', 'what should this feature do', 'draft a PRD', or any moment someone jumps to HOW before WHAT is agreed. NOT the technical plan (that's `plan`), NOT the de-risking ambiguity sweep (that's `clarify`), NOT project-wide principles (that's `constitution`)."
|
|
4
4
|
tags: [sdd, spec, requirements]
|
|
5
5
|
recommends: [clarify, plan]
|
|
6
6
|
profiles: [core, full]
|
|
@@ -15,12 +15,12 @@ A spec is a contract about behaviour and outcomes, readable by a non-technical s
|
|
|
15
15
|
|
|
16
16
|
## Detect the moment — and hold the gate
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
Fire on the **faintest** sign the user is thinking about a new feature or change — not just "spec this", but any musing: "I want to add…", "can we build…", "it should also…", "wouldn't it be nice if…", "what if we…", "I've been thinking about…", "let's brainstorm…", "tengo una idea", "se me ha ocurrido", "¿y si…?", "estaría guapo que…", "quiero añadir…", "necesito que haga…". This phase owns that moment, *even if a stack skill (nextjs/fastapi/flutter…) also fired and is itching to build it*. Catch it here first — being too eager to brainstorm is cheap; skipping it is expensive.
|
|
19
19
|
|
|
20
|
-
**The hard gate
|
|
21
|
-
> No
|
|
20
|
+
**The hard gate — every feature, including the "obvious" ones:**
|
|
21
|
+
> No implementation starts — not a stack skill, not `plan`-to-code, not "just a quick version" — until the user has **approved a design** (the spec at step 9 below) and `plan` has produced the technical plan + task list. If the user says "just build it", do not; name the gate in one friendly line and run the loop. "Too simple to need a design" is the rationalization that wastes the most work — every feature gets the loop. The only thing that skips it is a literal one-line, zero-risk change (typo/copy/config) — say so out loud and do it.
|
|
22
22
|
|
|
23
|
-
You are not slowing them down; you
|
|
23
|
+
You are not slowing them down; you make the intent reviewable *before* code exists, which is far cheaper than discovering the misunderstanding in a PR. End every spec by handing to `clarify`/`plan` — never to `implement`.
|
|
24
24
|
|
|
25
25
|
## The one rule that defines this skill
|
|
26
26
|
|
|
@@ -78,16 +78,24 @@ A criterion that needs a human to "decide if it's good enough" is not done yet
|
|
|
78
78
|
|
|
79
79
|
## The pass, end to end
|
|
80
80
|
|
|
81
|
+
Run these in order. It is a collaborative dialogue, not a form you fill in silence — and **you do not skip to a design dump.** Track the steps with a todo list so none is dropped.
|
|
82
|
+
|
|
81
83
|
```text
|
|
82
|
-
1.
|
|
83
|
-
2.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
84
|
+
1. EXPLORE context → profile + sdd config + constitution + existing specs + wiki + recent git
|
|
85
|
+
2. SCOPE check → if the request is several independent subsystems, DECOMPOSE into sub-specs first,
|
|
86
|
+
then brainstorm the FIRST one; each gets its own spec → plan → build cycle
|
|
87
|
+
3. RESTATE in one sentence→ confirm you understood the intent before anything else
|
|
88
|
+
4. ASK, one at a time → only the gaps that change scope; multiple-choice when you can; wait, record, repeat
|
|
89
|
+
5. PROPOSE 2-3 approaches → distinct directions with honest trade-offs; lead with your recommendation and why
|
|
90
|
+
6. PRESENT the design → section by section (problem, users, behaviour, acceptance), scaled to complexity;
|
|
91
|
+
after EACH section ask "does this look right?" and adjust before moving on
|
|
92
|
+
7. WRITE the spec → 02-DOCS/wiki/sdd/specs/<slug>.md (WHAT/WHY), index it in CLAUDE.md, commit if a repo
|
|
93
|
+
8. SELF-REVIEW → scan for TODO/placeholder, contradictions, ambiguity, scope creep; fix inline
|
|
94
|
+
9. USER APPROVES → ask them to read the written spec and confirm; loop on changes until they approve
|
|
95
|
+
10. HAND OFF → only now, result envelope → clarify/plan. NEVER to implement.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
**The gate is steps 5-9, and it is the point of this skill.** Implementation does not begin — not `plan`-to-code, not a stack skill, not "just a quick version" — until the user has approved the design at step 9. This holds for **every** feature, including ones that feel too small to design; "too simple to spec" is exactly where unexamined assumptions cost the most. The only thing that skips the loop is a literal one-line, zero-risk change (a typo, a copy tweak) — name it as such and do it.
|
|
91
99
|
```
|
|
92
100
|
|
|
93
101
|
`<slug>` is a short kebab-case name derived from the feature (e.g. `bulk-csv-import`, `magic-link-login`). If a spec with that slug exists, read it and update rather than overwrite.
|
|
@@ -165,6 +173,9 @@ The proposal is allowed to mention options and tradeoffs; the spec that follows
|
|
|
165
173
|
| Dump 12 questions in one message | One focused question per turn. Infer the rest from the constitution and wiki. |
|
|
166
174
|
| Ask a question you could answer from the constitution | Read it first. Only ask what genuinely changes the spec. |
|
|
167
175
|
| Write "it should work well / be fast / be intuitive" | Not testable. Make it a binary Given/When/Then or move it to Points to clarify. |
|
|
176
|
+
| Skip the whole loop because "this is too simple to design" | That's the rationalization that wastes the most work. Every feature gets the loop; only a literal one-line, zero-risk change skips. |
|
|
177
|
+
| Present one approach as the answer | Offer 2-3 distinct directions with trade-offs and a recommendation; let the user choose before you write the spec. |
|
|
178
|
+
| Hand to `plan`/`implement` before the user approved the written spec | The approval at step 9 is the gate. No design approved → nothing gets built. |
|
|
168
179
|
| Skip non-goals because "it's obvious" | Unsaid scope becomes assumed scope. State what you are *not* doing. |
|
|
169
180
|
| Resolve every ambiguity yourself to look finished | Inventing answers is worse than naming gaps. List them in Points to clarify. |
|
|
170
181
|
| Start designing the solution because it's clearer | Stay on WHAT/WHY. The plan is a later, separate phase. |
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { linkOrCopy } from './index.js';
|
|
3
4
|
|
|
5
|
+
// Shared adapter for every assistant whose "always-on" surface is a plain
|
|
6
|
+
// markdown instructions/rules file (AGENTS.md, copilot-instructions.md, a
|
|
7
|
+
// .windsurf/.roo/.continue rule, …). Skills are symlinked to the shared base;
|
|
8
|
+
// the suggest block is injected between idempotent markers so re-installs and
|
|
9
|
+
// multiple assistants sharing one file never duplicate it.
|
|
4
10
|
const MARK_START = '<!-- rsc-suggest:start -->';
|
|
5
11
|
const MARK_END = '<!-- rsc-suggest:end -->';
|
|
6
12
|
|
|
7
13
|
export function writeSkill(id, fromDir, toPath) {
|
|
8
|
-
|
|
9
|
-
cpSync(fromDir, toPath, { recursive: true });
|
|
10
|
-
return [toPath];
|
|
14
|
+
return linkOrCopy(fromDir, toPath);
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
export function wireHook(paths, sourceMd) {
|
package/targets/claude.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
|
+
import { linkOrCopy } from './index.js';
|
|
3
4
|
|
|
4
5
|
export function writeSkill(id, fromDir, toPath) {
|
|
5
|
-
|
|
6
|
-
cpSync(fromDir, toPath, { recursive: true });
|
|
7
|
-
return [toPath];
|
|
6
|
+
return linkOrCopy(fromDir, toPath);
|
|
8
7
|
}
|
|
9
8
|
|
|
10
9
|
export function wireHook(paths) {
|
package/targets/index.js
CHANGED
|
@@ -1,55 +1,120 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import { join } from 'node:path';
|
|
1
|
+
import { existsSync, lstatSync, mkdirSync, rmSync, symlinkSync, cpSync } from 'node:fs';
|
|
2
|
+
import { join, dirname, relative } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import * as claudeAdapter from './claude.js';
|
|
5
|
+
import * as cursorAdapter from './cursor.js';
|
|
6
|
+
import * as mdAdapter from './_md-block.js';
|
|
4
7
|
|
|
8
|
+
// Project-local single source of truth. Real skill files live here exactly once;
|
|
9
|
+
// every assistant gets a lightweight pointer (symlink) back to it — no duplication.
|
|
10
|
+
export function baseDir(id, cwd = process.cwd()) {
|
|
11
|
+
return join(cwd, '.rsc', 'skills', id);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// Point an assistant's skill folder at the shared base via a relative symlink.
|
|
15
|
+
// Falls back to a real copy when the filesystem rejects symlinks (e.g. Windows
|
|
16
|
+
// without privileges). Idempotent: replaces any existing link/dir at toPath.
|
|
17
|
+
export function linkOrCopy(fromDir, toPath) {
|
|
18
|
+
mkdirSync(dirname(toPath), { recursive: true });
|
|
19
|
+
try { lstatSync(toPath); rmSync(toPath, { recursive: true, force: true }); } catch { /* nothing there */ }
|
|
20
|
+
try {
|
|
21
|
+
symlinkSync(relative(dirname(toPath), fromDir), toPath, 'dir');
|
|
22
|
+
} catch {
|
|
23
|
+
cpSync(fromDir, toPath, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
return [toPath];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// One row per assistant. `root` is where its skill folder lives (relative to the
|
|
29
|
+
// project), `hook` is the file that gets the always-on suggest block, `adapter`
|
|
30
|
+
// picks how skills + hook are written. `skillExt` (cursor only) means each skill
|
|
31
|
+
// is a single converted file, not a linked directory.
|
|
32
|
+
const SPEC = {
|
|
33
|
+
// JSON-hook + linked skill dirs
|
|
34
|
+
claude: { root: '.claude/skills/rsc', hook: '.claude/settings.json', adapter: 'claude' },
|
|
35
|
+
// Converted .mdc rules
|
|
36
|
+
cursor: { root: '.cursor/rules', hook: '.cursor/rules/rsc-suggest.mdc', adapter: 'cursor', skillExt: '.mdc' },
|
|
37
|
+
// AGENTS.md family — all read the same root AGENTS.md
|
|
38
|
+
codex: { root: '.codex/rsc', hook: 'AGENTS.md', adapter: 'md' },
|
|
39
|
+
opencode: { root: '.opencode/rsc', hook: 'AGENTS.md', adapter: 'md' },
|
|
40
|
+
amp: { root: '.amp/rsc', hook: 'AGENTS.md', adapter: 'md' },
|
|
41
|
+
jules: { root: '.jules/rsc', hook: 'AGENTS.md', adapter: 'md' },
|
|
42
|
+
zed: { root: '.zed/rsc', hook: 'AGENTS.md', adapter: 'md' },
|
|
43
|
+
// Own markdown instructions/rules file
|
|
44
|
+
gemini: { root: '.gemini/rsc', hook: 'GEMINI.md', adapter: 'md' },
|
|
45
|
+
antigravity: { root: '.antigravity/rsc', hook: '.antigravity/AGENTS.md', adapter: 'md' },
|
|
46
|
+
copilot: { root: '.github/rsc', hook: '.github/copilot-instructions.md', adapter: 'md' },
|
|
47
|
+
windsurf: { root: '.windsurf/rsc', hook: '.windsurf/rules/rsc-suggest.md', adapter: 'md' },
|
|
48
|
+
cline: { root: '.clinerules/rsc', hook: '.clinerules/rsc-suggest.md', adapter: 'md' },
|
|
49
|
+
roo: { root: '.roo/rsc', hook: '.roo/rules/rsc-suggest.md', adapter: 'md' },
|
|
50
|
+
continue: { root: '.continue/rsc', hook: '.continue/rules/rsc-suggest.md', adapter: 'md' },
|
|
51
|
+
junie: { root: '.junie/rsc', hook: '.junie/guidelines.md', adapter: 'md' },
|
|
52
|
+
kiro: { root: '.kiro/rsc', hook: '.kiro/steering/rsc-suggest.md', adapter: 'md' },
|
|
53
|
+
aider: { root: '.aider/rsc', hook: 'CONVENTIONS.md', adapter: 'md' },
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const ADAPTER = { claude: claudeAdapter, cursor: cursorAdapter, md: mdAdapter };
|
|
57
|
+
|
|
58
|
+
// Wizard multi-select list, in "most famous first" order. label/hint are display
|
|
59
|
+
// only; detectTarget just pre-marks the one found in the folder.
|
|
60
|
+
export const TARGETS = [
|
|
61
|
+
{ id: 'claude', label: 'Claude Code', hint: '.claude/skills/' },
|
|
62
|
+
{ id: 'codex', label: 'Codex CLI', hint: 'AGENTS.md' },
|
|
63
|
+
{ id: 'copilot', label: 'GitHub Copilot', hint: '.github/copilot-instructions.md' },
|
|
64
|
+
{ id: 'cursor', label: 'Cursor', hint: '.cursor/rules/' },
|
|
65
|
+
{ id: 'gemini', label: 'Gemini CLI', hint: 'GEMINI.md' },
|
|
66
|
+
{ id: 'windsurf', label: 'Windsurf', hint: '.windsurf/rules/' },
|
|
67
|
+
{ id: 'cline', label: 'Cline', hint: '.clinerules/' },
|
|
68
|
+
{ id: 'antigravity', label: 'Antigravity', hint: '.antigravity/' },
|
|
69
|
+
{ id: 'zed', label: 'Zed', hint: 'AGENTS.md' },
|
|
70
|
+
{ id: 'continue', label: 'Continue', hint: '.continue/rules/' },
|
|
71
|
+
{ id: 'roo', label: 'Roo Code', hint: '.roo/rules/' },
|
|
72
|
+
{ id: 'amp', label: 'Amp', hint: 'AGENTS.md' },
|
|
73
|
+
{ id: 'opencode', label: 'opencode', hint: 'AGENTS.md' },
|
|
74
|
+
{ id: 'jules', label: 'Jules', hint: 'AGENTS.md' },
|
|
75
|
+
{ id: 'junie', label: 'JetBrains Junie', hint: '.junie/guidelines.md' },
|
|
76
|
+
{ id: 'kiro', label: 'Kiro', hint: '.kiro/steering/' },
|
|
77
|
+
{ id: 'aider', label: 'Aider', hint: 'CONVENTIONS.md' },
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Best-effort default for the wizard's pre-mark. Unique config dirs win; the
|
|
81
|
+
// shared AGENTS.md / GEMINI.md fall through to codex / gemini.
|
|
5
82
|
export function detectTarget(cwd = process.cwd()) {
|
|
6
|
-
|
|
7
|
-
if (
|
|
8
|
-
if (
|
|
83
|
+
const dir = (d) => existsSync(join(cwd, d));
|
|
84
|
+
if (dir('.cursor')) return 'cursor';
|
|
85
|
+
if (dir('.windsurf')) return 'windsurf';
|
|
86
|
+
if (dir('.clinerules')) return 'cline';
|
|
87
|
+
if (dir('.roo')) return 'roo';
|
|
88
|
+
if (dir('.continue')) return 'continue';
|
|
89
|
+
if (dir('.junie')) return 'junie';
|
|
90
|
+
if (dir('.kiro')) return 'kiro';
|
|
91
|
+
if (dir('.zed')) return 'zed';
|
|
92
|
+
if (dir('.opencode')) return 'opencode';
|
|
93
|
+
if (dir('.amp')) return 'amp';
|
|
94
|
+
if (dir('.jules')) return 'jules';
|
|
95
|
+
if (dir('.antigravity')) return 'antigravity';
|
|
96
|
+
if (existsSync(join(cwd, '.github', 'copilot-instructions.md'))) return 'copilot';
|
|
97
|
+
if (dir('.codex') || dir('AGENTS.md')) return 'codex';
|
|
98
|
+
if (dir('.gemini') || dir('GEMINI.md')) return 'gemini';
|
|
9
99
|
return 'claude';
|
|
10
100
|
}
|
|
11
101
|
|
|
12
102
|
export function targetPaths(target, home = homedir(), cwd = process.cwd()) {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
root: join(cwd, '.cursor', 'rules'),
|
|
24
|
-
skillDir: (id) => join(cwd, '.cursor', 'rules', `${id}.mdc`),
|
|
25
|
-
stateFile: join(cwd, '.cursor', 'rules', '.rsc-state.json'),
|
|
26
|
-
hookTarget: join(cwd, '.cursor', 'rules', 'rsc-suggest.mdc'),
|
|
27
|
-
};
|
|
28
|
-
case 'codex':
|
|
29
|
-
return {
|
|
30
|
-
root: join(cwd, '.codex', 'rsc'),
|
|
31
|
-
skillDir: (id) => join(cwd, '.codex', 'rsc', id),
|
|
32
|
-
stateFile: join(cwd, '.codex', 'rsc', '.rsc-state.json'),
|
|
33
|
-
hookTarget: join(cwd, 'AGENTS.md'),
|
|
34
|
-
};
|
|
35
|
-
case 'gemini':
|
|
36
|
-
return {
|
|
37
|
-
root: join(cwd, '.gemini', 'rsc'),
|
|
38
|
-
skillDir: (id) => join(cwd, '.gemini', 'rsc', id),
|
|
39
|
-
stateFile: join(cwd, '.gemini', 'rsc', '.rsc-state.json'),
|
|
40
|
-
hookTarget: join(cwd, 'GEMINI.md'),
|
|
41
|
-
};
|
|
42
|
-
default:
|
|
43
|
-
throw new Error(`unknown target ${target}`);
|
|
44
|
-
}
|
|
103
|
+
const s = SPEC[target];
|
|
104
|
+
if (!s) throw new Error(`unknown target ${target}`);
|
|
105
|
+
const rootAbs = join(cwd, ...s.root.split('/'));
|
|
106
|
+
return {
|
|
107
|
+
root: rootAbs,
|
|
108
|
+
skillDir: (id) => (s.skillExt ? join(rootAbs, `${id}${s.skillExt}`) : join(rootAbs, id)),
|
|
109
|
+
stateFile: join(rootAbs, '.rsc-state.json'),
|
|
110
|
+
hookTarget: join(cwd, ...s.hook.split('/')),
|
|
111
|
+
};
|
|
45
112
|
}
|
|
46
113
|
|
|
47
|
-
export
|
|
48
|
-
|
|
49
|
-
return w(id, fromDir, toPath);
|
|
114
|
+
export function writeSkill(target, id, fromDir, toPath) {
|
|
115
|
+
return ADAPTER[SPEC[target].adapter].writeSkill(id, fromDir, toPath);
|
|
50
116
|
}
|
|
51
117
|
|
|
52
|
-
export
|
|
53
|
-
|
|
54
|
-
return w(paths, sourceMd);
|
|
118
|
+
export function wireHook(target, paths, sourceMd) {
|
|
119
|
+
return ADAPTER[SPEC[target].adapter].wireHook(paths, sourceMd);
|
|
55
120
|
}
|
package/targets/gemini.js
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { cpSync, readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { dirname } from 'node:path';
|
|
3
|
-
|
|
4
|
-
const MARK_START = '<!-- rsc-suggest:start -->';
|
|
5
|
-
const MARK_END = '<!-- rsc-suggest:end -->';
|
|
6
|
-
|
|
7
|
-
export function writeSkill(id, fromDir, toPath) {
|
|
8
|
-
mkdirSync(dirname(toPath), { recursive: true });
|
|
9
|
-
cpSync(fromDir, toPath, { recursive: true });
|
|
10
|
-
return [toPath];
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function wireHook(paths, sourceMd) {
|
|
14
|
-
const body = stripFrontmatter(readFileSync(sourceMd, 'utf8'));
|
|
15
|
-
const block = `${MARK_START}\n${body}\n${MARK_END}`;
|
|
16
|
-
let doc = existsSync(paths.hookTarget) ? readFileSync(paths.hookTarget, 'utf8') : '';
|
|
17
|
-
if (doc.includes(MARK_START)) {
|
|
18
|
-
doc = doc.replace(new RegExp(`${MARK_START}[\\s\\S]*?${MARK_END}`), block);
|
|
19
|
-
} else {
|
|
20
|
-
doc += `\n\n${block}\n`;
|
|
21
|
-
}
|
|
22
|
-
mkdirSync(dirname(paths.hookTarget), { recursive: true });
|
|
23
|
-
writeFileSync(paths.hookTarget, doc);
|
|
24
|
-
return [paths.hookTarget];
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function stripFrontmatter(md) {
|
|
28
|
-
return md.replace(/^---\n[\s\S]*?\n---\n?/, '');
|
|
29
|
-
}
|