@epicparty/skills 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NOTICE +27 -0
- package/README.md +109 -2
- package/bin/cli.mjs +175 -0
- package/lib/transform.mjs +185 -0
- package/package.json +37 -4
- package/skills/architect/DESIGN-IT-TWICE.md +46 -0
- package/skills/architect/HTML-REPORT.md +126 -0
- package/skills/architect/SKILL.md +101 -0
- package/skills/babysit/SKILL.md +164 -0
- package/skills/code-review/SKILL.md +111 -0
- package/skills/copy/COPY.md +142 -0
- package/skills/copy/SKILL.md +87 -0
- package/skills/deliver/SKILL.md +259 -0
- package/skills/diagnose/SKILL.md +114 -0
- package/skills/diagnose/scripts/hitl-loop.template.sh +44 -0
- package/skills/explore/SKILL.md +78 -0
- package/skills/grill/SKILL.md +75 -0
- package/skills/implement/SKILL.md +132 -0
- package/skills/implement/TDD.md +131 -0
- package/skills/implement/TEST-VALUE.md +114 -0
- package/skills/outdated/SKILL.md +125 -0
- package/skills/research/SKILL.md +115 -0
- package/skills/router/BOARD.md +157 -0
- package/skills/router/DECISIONS.md +71 -0
- package/skills/router/GIT.md +70 -0
- package/skills/router/GLOSSARY-FORMAT.md +48 -0
- package/skills/router/SKILL.md +251 -0
- package/skills/setup/SKILL.md +99 -0
- package/skills/ship/SKILL.md +132 -0
- package/skills/to-pr/SKILL.md +88 -0
- package/skills/to-story/SEAMS.md +51 -0
- package/skills/to-story/SKILL.md +79 -0
- package/skills/to-story/SPLITTING.md +53 -0
- package/skills/to-story/STORIES.md +179 -0
- package/skills/triage/SKILL.md +179 -0
- package/skills/upgrade/SKILL.md +139 -0
- package/skills/writing-for-agents/SKILL-MECHANICS.md +24 -0
- package/skills/writing-for-agents/SKILL.md +88 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 EpicParty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
epicparty/skills
|
|
2
|
+
|
|
3
|
+
Parts of this pack are adapted from mattpocock/skills (https://github.com/mattpocock/skills).
|
|
4
|
+
UPSTREAM.md in the source repository lists which files, and where each one went.
|
|
5
|
+
Those parts are used under the following license:
|
|
6
|
+
|
|
7
|
+
MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 Matt Pocock
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,110 @@
|
|
|
1
|
-
#
|
|
1
|
+
# epicparty/skills
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A delivery workflow for coding agents: from a request to a merged, deployed pull request. Explore the work, write it up as a story, build it test-first, open the PR, watch it, and ship it. A router skill reads a request and walks you through the route.
|
|
4
|
+
|
|
5
|
+
**Credit:** several skills build on [Matt Pocock's skills](https://github.com/mattpocock/skills) — `grill`, `diagnose`, `architect` and `writing-for-agents`, and the test-first, seams, glossary and hand-off parts of others. Thank you, Matt. [`UPSTREAM.md`](./UPSTREAM.md) lists what came from where.
|
|
6
|
+
|
|
7
|
+
The skills install under **your project's prefix**. Generic names like `implement` or `copy` say nothing about where a skill comes from, and they collide with other packs. With the prefix `acme`, you get `/acme` (the router), `/acme-implement`, `/acme-to-pr`, and so on. Each project picks its own.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npx @epicparty/skills add --prefix acme
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Without `--prefix`, the CLI asks for one. It builds the skills under that prefix and installs them with the [`skills`](https://github.com/vercel-labs/skills) CLI, which links them for every agent it supports (Claude Code, Codex, Cursor and others). Pass `--agent <name>` to choose agents.
|
|
16
|
+
|
|
17
|
+
The prefix and the version go into `epicparty-skills.json` at the project root. Commit it.
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx @epicparty/skills@latest update # same prefix and agents, newer skills
|
|
21
|
+
npx @epicparty/skills remove
|
|
22
|
+
npx @epicparty/skills add --dry-run --prefix acme # build and check only
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Do not install this repository with `npx skills add epicparty/skills` directly. That installs the skills under their bare folder names (`implement`, `copy`), while their text refers to each other with a prefix.
|
|
26
|
+
|
|
27
|
+
## The skills
|
|
28
|
+
|
|
29
|
+
| Skill | What it does |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| `<prefix>` | Reads a request, names the route through the skills, and walks it. Start here. `/<prefix> handoff` hands a thread to a fresh agent. |
|
|
32
|
+
| `<prefix>-setup` | Asks every unset setting up front, or changes one. |
|
|
33
|
+
| `<prefix>-explore` | Maps what a piece of work involves before anything is decided. |
|
|
34
|
+
| `<prefix>-grill` | Grills you on a plan in rounds of questions, and keeps the glossary and ADRs as it settles things. |
|
|
35
|
+
| `<prefix>-to-story` | Writes the conversation up as one user story on the tracker. |
|
|
36
|
+
| `<prefix>-triage` | Judges one existing issue and ends in one verdict. |
|
|
37
|
+
| `<prefix>-implement` | Builds one story on one branch: test-first, validated, reviewed, committed. |
|
|
38
|
+
| `<prefix>-diagnose` | Finds and fixes a failure with a loop that goes red on it before any theory. |
|
|
39
|
+
| `<prefix>-architect` | Recommends how to architect a feature, or judges your design. On request, scans a named area for modules worth deepening. |
|
|
40
|
+
| `<prefix>-code-review` | Reviews code on two axes: the project's standards, and the spec. |
|
|
41
|
+
| `<prefix>-to-pr` | Opens a pull request with its suite report and issue link. |
|
|
42
|
+
| `<prefix>-babysit` | Sweeps an open PR toward merged. |
|
|
43
|
+
| `<prefix>-ship` | Merges, confirms the production deploy, and cleans the checkout. |
|
|
44
|
+
| `<prefix>-deliver` | Delivers the stories marked to start, in sequence or across parallel workers. |
|
|
45
|
+
| `<prefix>-copy` | Writes or reviews user-facing copy. |
|
|
46
|
+
| `<prefix>-research` | Investigates a question against primary sources. |
|
|
47
|
+
| `<prefix>-outdated` | Reports which dependencies of a pnpm workspace have newer releases, and what changed. |
|
|
48
|
+
| `<prefix>-upgrade` | Upgrades those dependencies and migrates the code. |
|
|
49
|
+
| `<prefix>-writing-for-agents` | How to write skills, `AGENTS.md` and anything else an agent reads. |
|
|
50
|
+
|
|
51
|
+
GitHub (issues, pull requests, Projects) is built in. Another tracker works when you describe it in the **Tracker** setting, and a project with no tracker answers **None**.
|
|
52
|
+
|
|
53
|
+
## Settings
|
|
54
|
+
|
|
55
|
+
The skills ask for a project fact the first time they need it: the tracker, the labels, how to run the full test suite, how a deploy is confirmed. Each answer is saved where you choose — by default `docs/agents/<topic>.md`, one file per topic — and is never asked again. Run `/<prefix>-setup` to answer them all at once, or to change one.
|
|
56
|
+
|
|
57
|
+
Each skill declares its own settings in its `## Settings` section, so the list grows with the skills you install.
|
|
58
|
+
|
|
59
|
+
## How the prefix is applied
|
|
60
|
+
|
|
61
|
+
The skills in this repository are written with the prefix `ep`, and the router's folder is `skills/router`. The CLI rewrites, in every Markdown file:
|
|
62
|
+
|
|
63
|
+
- `/ep` to `/<prefix>`, and `/ep-<skill>` or `ep-<skill>` to `<prefix>-<skill>`;
|
|
64
|
+
- `ep-*` to `<prefix>-*`;
|
|
65
|
+
- links into a sibling skill's folder, `../<skill>/`, to `../<prefix>-<skill>/`, and `../router/` to `../<prefix>/`;
|
|
66
|
+
- each `SKILL.md`'s `name` to its installed name: `router` becomes `<prefix>`.
|
|
67
|
+
|
|
68
|
+
Then it checks the result: every name matches its folder, every link and heading anchor resolves, and every skill named in the text is installed. Each skill also carries `pack: epicparty/skills` under `metadata`, which is how `/<prefix>-setup` finds the pack's skills whatever the prefix.
|
|
69
|
+
|
|
70
|
+
`npx @epicparty/skills check` runs the same check on an installed project.
|
|
71
|
+
|
|
72
|
+
## Limits
|
|
73
|
+
|
|
74
|
+
- Project installs only. Global (`-g`) installs are not supported yet.
|
|
75
|
+
- `outdated` and `upgrade` work on pnpm workspaces only. The other skills read the package manager from the lockfile.
|
|
76
|
+
- Unattended runs (`/loop`) read the board and the dependency edges with `gh api`, and link branches with `gh issue develop`. Allow both in your agent's permissions, or each beat stops at its first read.
|
|
77
|
+
- A repo with no remote works up to a local commit: the skills that push, open a PR, merge or deploy stop there and say so.
|
|
78
|
+
- The `skills` CLI records each skill it installs in `skills-lock.json`. This CLI removes its own skills from that file, because the `skills` CLI cannot update them; `update` here replaces `skills update` for this pack.
|
|
79
|
+
|
|
80
|
+
## Versions
|
|
81
|
+
|
|
82
|
+
The pack has a version, and so does each skill: `metadata.version` in its `SKILL.md`. A skill's version moves only when that skill changes, so `implement` can stay at 0.3.0 while `triage` reaches 0.5.1. `epicparty-skills.json` records the version of every installed skill, and `update` lists the skills it added, removed or moved to a new version.
|
|
83
|
+
|
|
84
|
+
[`CHANGELOG.md`](./CHANGELOG.md) says what changed in each release, skill by skill.
|
|
85
|
+
|
|
86
|
+
## Develop
|
|
87
|
+
|
|
88
|
+
```sh
|
|
89
|
+
npm install
|
|
90
|
+
npm test
|
|
91
|
+
node bin/cli.mjs build --prefix acme --out /tmp/acme
|
|
92
|
+
node bin/cli.mjs add --from ./skills --prefix acme # in a test project
|
|
93
|
+
npx changeset # describe a change for the next release
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Releasing
|
|
97
|
+
|
|
98
|
+
Releases run on [Changesets](https://github.com/changesets/changesets), the way [mattpocock/skills](https://github.com/mattpocock/skills) runs them:
|
|
99
|
+
|
|
100
|
+
1. **Every pull request that changes the pack adds a changeset** — `npx changeset` — whose entry starts with the skills it changes. [`.changeset/README.md`](./.changeset/README.md) has the format and the bump levels. CI fails a pull request that changes a skill and names it in no changeset.
|
|
101
|
+
2. **On each push to `main`**, the Release workflow opens or updates a `chore: release` pull request. It bumps the pack's version and each named skill's, and writes `CHANGELOG.md`.
|
|
102
|
+
3. **Merging that pull request is the release.** The workflow tags `v<version>` and publishes a GitHub release from the changelog.
|
|
103
|
+
|
|
104
|
+
Until npm publishing is on, a project installs a release from GitHub: `npx github:epicparty/skills#v<version> add --prefix acme`. To turn npm on, set up [trusted publishing](https://docs.npmjs.com/trusted-publishers) for `@epicparty/skills`, add `id-token: write` to the workflow's permissions, and change its `publish-script` to `npx changeset publish`.
|
|
105
|
+
|
|
106
|
+
The GitHub repository must allow it: **Settings → Actions → General → Allow GitHub Actions to create and approve pull requests**.
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT. Parts are adapted from [mattpocock/skills](https://github.com/mattpocock/skills), also MIT: [`NOTICE`](./NOTICE) carries its license, and every installed skill that holds adapted text ships a copy. [`UPSTREAM.md`](./UPSTREAM.md) maps each adapted file and says how to sync with upstream.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Installs the epicparty/skills pack under a project's own prefix, through the `skills` CLI.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { dirname, join, resolve } from 'node:path';
|
|
7
|
+
import { createInterface } from 'node:readline/promises';
|
|
8
|
+
import { parseArgs } from 'node:util';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { build, check, packSkills, skillVersion, validatePrefix } from '../lib/transform.mjs';
|
|
11
|
+
|
|
12
|
+
const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
13
|
+
const PKG = JSON.parse(readFileSync(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
14
|
+
const LOCK = 'epicparty-skills.json';
|
|
15
|
+
const SKILLS_CLI = process.env.EPICPARTY_SKILLS_CLI ?? 'skills@1';
|
|
16
|
+
|
|
17
|
+
const HELP = `${PKG.name} ${PKG.version}
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
npx ${PKG.name} add [--prefix <p>] [--agent <a>]... Install the pack as <p>, <p>-implement, ...
|
|
21
|
+
npx ${PKG.name} update Reinstall with the prefix and agents in ${LOCK}
|
|
22
|
+
npx ${PKG.name} remove Remove the pack's skills
|
|
23
|
+
npx ${PKG.name} build --prefix <p> --out <dir> Write the prefixed skills to a folder, install nothing
|
|
24
|
+
npx ${PKG.name} check --prefix <p> [<dir>] Check installed skills (default .agents/skills)
|
|
25
|
+
|
|
26
|
+
Options:
|
|
27
|
+
--prefix <p> The project's prefix. Asked on first install, then read from ${LOCK}.
|
|
28
|
+
--agent <a> Passed to the skills CLI (repeatable). Default: the agents it detects.
|
|
29
|
+
--from <dir> Use the skills in <dir> instead of the ones bundled with this version.
|
|
30
|
+
--dry-run Build and check in a temporary folder, install nothing.
|
|
31
|
+
`;
|
|
32
|
+
|
|
33
|
+
function fail(message) {
|
|
34
|
+
console.error(`error: ${message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readLock(cwd) {
|
|
39
|
+
const path = join(cwd, LOCK);
|
|
40
|
+
return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The installed skills a lock records, as { name: version }. Older locks list names only. */
|
|
44
|
+
function lockedSkills(lock) {
|
|
45
|
+
const skills = lock?.skills ?? {};
|
|
46
|
+
return Array.isArray(skills) ? Object.fromEntries(skills.map((n) => [n, null])) : skills;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** One line per skill that is new, gone or at another version. */
|
|
50
|
+
function versionChanges(before, after) {
|
|
51
|
+
const lines = [];
|
|
52
|
+
for (const [name, version] of Object.entries(after)) {
|
|
53
|
+
if (!(name in before)) lines.push(` + ${name} ${version}`);
|
|
54
|
+
else if (before[name] !== version) lines.push(` ~ ${name} ${before[name] ?? '?'} -> ${version}`);
|
|
55
|
+
}
|
|
56
|
+
for (const name of Object.keys(before)) if (!(name in after)) lines.push(` - ${name}`);
|
|
57
|
+
return lines;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function writeLock(cwd, lock) {
|
|
61
|
+
writeFileSync(join(cwd, LOCK), `${JSON.stringify(lock, null, 2)}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function skillsCli(args, cwd) {
|
|
65
|
+
console.log(`$ npx ${SKILLS_CLI} ${args.join(' ')}`);
|
|
66
|
+
const r = spawnSync('npx', ['-y', SKILLS_CLI, ...args], { cwd, stdio: 'inherit' });
|
|
67
|
+
if (r.status !== 0) fail(`the skills CLI exited with ${r.status}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The skills CLI records each install from a local folder in skills-lock.json. That folder is
|
|
71
|
+
// gone after install, so those entries could never be restored or updated: this CLI owns them.
|
|
72
|
+
function dropFromSkillsLock(cwd, names) {
|
|
73
|
+
const path = join(cwd, 'skills-lock.json');
|
|
74
|
+
if (!existsSync(path)) return;
|
|
75
|
+
const lock = JSON.parse(readFileSync(path, 'utf8'));
|
|
76
|
+
let changed = false;
|
|
77
|
+
for (const name of names) {
|
|
78
|
+
if (lock.skills?.[name]?.sourceType === 'local') {
|
|
79
|
+
delete lock.skills[name];
|
|
80
|
+
changed = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!changed) return;
|
|
84
|
+
if (Object.keys(lock.skills ?? {}).length === 0) rmSync(path);
|
|
85
|
+
else writeFileSync(path, `${JSON.stringify(lock, null, 2)}\n`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function askPrefix() {
|
|
89
|
+
if (!process.stdin.isTTY) fail(`no prefix: pass --prefix <p>, or run once with a terminal to be asked`);
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
const answer = (await rl.question('Prefix for this project\'s skills (e.g. "ep" gives /ep, /ep-implement): ')).trim();
|
|
92
|
+
rl.close();
|
|
93
|
+
return answer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function sourceDir(from) {
|
|
97
|
+
const dir = from ? resolve(from) : join(PKG_ROOT, 'skills');
|
|
98
|
+
if (!existsSync(dir)) fail(`no skills folder at ${dir}`);
|
|
99
|
+
return dir;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildChecked(source, prefix, out) {
|
|
103
|
+
const names = build(source, prefix, out);
|
|
104
|
+
const problems = check(out, prefix, { canonicalSkills: packSkills(source) });
|
|
105
|
+
if (problems.length) fail(`the built skills are inconsistent:\n ${problems.join('\n ')}`);
|
|
106
|
+
return names;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function install(cwd, opts, lock) {
|
|
110
|
+
const prefix = validatePrefix(opts.prefix ?? lock?.prefix ?? (await askPrefix()));
|
|
111
|
+
const agents = opts.agent?.length ? opts.agent : (lock?.agents ?? []);
|
|
112
|
+
const stage = mkdtempSync(join(tmpdir(), 'epicparty-skills-'));
|
|
113
|
+
try {
|
|
114
|
+
const names = buildChecked(sourceDir(opts.from), prefix, join(stage, 'skills'));
|
|
115
|
+
if (opts['dry-run']) {
|
|
116
|
+
console.log(`Built ${names.length} skills for prefix "${prefix}" in ${stage}: ${names.join(', ')}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const before = lockedSkills(lock);
|
|
120
|
+
const after = Object.fromEntries(names.map((n) => [n, skillVersion(join(stage, 'skills', n))]));
|
|
121
|
+
const stale = Object.keys(before).filter((n) => !names.includes(n));
|
|
122
|
+
if (stale.length) skillsCli(['remove', ...stale, '-y'], cwd);
|
|
123
|
+
skillsCli(['add', stage, '--skill', '*', ...agents.flatMap((a) => ['--agent', a]), '-y'], cwd);
|
|
124
|
+
dropFromSkillsLock(cwd, names);
|
|
125
|
+
writeLock(cwd, {
|
|
126
|
+
prefix,
|
|
127
|
+
version: PKG.version,
|
|
128
|
+
source: opts.from ? resolve(opts.from) : PKG.name,
|
|
129
|
+
agents,
|
|
130
|
+
skills: after,
|
|
131
|
+
});
|
|
132
|
+
console.log(`Installed ${names.length} skills as ${prefix}, ${prefix}-*. Recorded in ${LOCK}.`);
|
|
133
|
+
const changes = versionChanges(before, after);
|
|
134
|
+
if (lock && changes.length) console.log(`Changed:\n${changes.join('\n')}`);
|
|
135
|
+
} finally {
|
|
136
|
+
if (!opts['dry-run']) rmSync(stage, { recursive: true, force: true });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { values: opts, positionals } = parseArgs({
|
|
141
|
+
allowPositionals: true,
|
|
142
|
+
options: {
|
|
143
|
+
prefix: { type: 'string' },
|
|
144
|
+
agent: { type: 'string', multiple: true },
|
|
145
|
+
from: { type: 'string' },
|
|
146
|
+
out: { type: 'string' },
|
|
147
|
+
'dry-run': { type: 'boolean' },
|
|
148
|
+
help: { type: 'boolean', short: 'h' },
|
|
149
|
+
version: { type: 'boolean', short: 'v' },
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
const [command, ...rest] = positionals;
|
|
153
|
+
const cwd = process.cwd();
|
|
154
|
+
|
|
155
|
+
if (opts.version) console.log(PKG.version);
|
|
156
|
+
else if (opts.help || !command) console.log(HELP);
|
|
157
|
+
else if (command === 'add') await install(cwd, opts, readLock(cwd));
|
|
158
|
+
else if (command === 'update') {
|
|
159
|
+
const lock = readLock(cwd) ?? fail(`no ${LOCK} here: run "add" first`);
|
|
160
|
+
await install(cwd, { ...opts, prefix: opts.prefix ?? lock.prefix }, lock);
|
|
161
|
+
} else if (command === 'remove') {
|
|
162
|
+
const lock = readLock(cwd) ?? fail(`no ${LOCK} here: nothing to remove`);
|
|
163
|
+
skillsCli(['remove', ...Object.keys(lockedSkills(lock)), '-y'], cwd);
|
|
164
|
+
rmSync(join(cwd, LOCK));
|
|
165
|
+
} else if (command === 'build') {
|
|
166
|
+
if (!opts.prefix || !opts.out) fail('build needs --prefix and --out');
|
|
167
|
+
const names = buildChecked(sourceDir(opts.from), validatePrefix(opts.prefix), resolve(opts.out));
|
|
168
|
+
console.log(`Built ${names.length} skills in ${resolve(opts.out)}`);
|
|
169
|
+
} else if (command === 'check') {
|
|
170
|
+
const prefix = validatePrefix(opts.prefix ?? readLock(cwd)?.prefix ?? fail('check needs --prefix'));
|
|
171
|
+
const dir = resolve(rest[0] ?? '.agents/skills');
|
|
172
|
+
const problems = check(dir, prefix, { canonicalSkills: packSkills(sourceDir(opts.from)) });
|
|
173
|
+
if (problems.length) fail(`${problems.length} problem(s):\n ${problems.join('\n ')}`);
|
|
174
|
+
console.log(`OK: the ${prefix} skills in ${dir} are consistent.`);
|
|
175
|
+
} else fail(`unknown command "${command}"\n\n${HELP}`);
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// Turns the pack's unprefixed skills into skills named for one project's prefix,
|
|
2
|
+
// and checks that a set of installed skills is consistent with that prefix.
|
|
3
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { dirname, join, relative, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
/** The prefix the pack's own text is written with. */
|
|
7
|
+
export const CANONICAL = 'ep';
|
|
8
|
+
/** The router's folder in the pack. It installs under the bare prefix. */
|
|
9
|
+
export const ROUTER = 'router';
|
|
10
|
+
export const MARKER = 'pack: epicparty/skills';
|
|
11
|
+
|
|
12
|
+
/** Marks text adapted from another project; UPSTREAM.md maps each one. */
|
|
13
|
+
export const UPSTREAM_MARK = '<!-- upstream: ';
|
|
14
|
+
|
|
15
|
+
const PREFIX_RE = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
16
|
+
|
|
17
|
+
export function validatePrefix(prefix) {
|
|
18
|
+
if (!PREFIX_RE.test(prefix ?? '')) {
|
|
19
|
+
throw new Error(`Invalid prefix "${prefix}": use lowercase letters, digits and single hyphens, starting with a letter (e.g. "ep", "acme").`);
|
|
20
|
+
}
|
|
21
|
+
return prefix;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The skill folders of the pack: every directory holding a SKILL.md. */
|
|
25
|
+
export function packSkills(sourceDir) {
|
|
26
|
+
return readdirSync(sourceDir)
|
|
27
|
+
.filter((name) => existsSync(join(sourceDir, name, 'SKILL.md')))
|
|
28
|
+
.sort();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The name a pack skill installs under. The router takes the bare prefix. */
|
|
32
|
+
export function installedName(skill, prefix) {
|
|
33
|
+
return skill === ROUTER ? prefix : `${prefix}-${skill}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const escape = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
37
|
+
|
|
38
|
+
/** One-pass rewrite of every canonical reference in a Markdown text. */
|
|
39
|
+
export function rewriteText(text, skills, prefix) {
|
|
40
|
+
const names = [...skills].filter((s) => s !== ROUTER).sort((a, b) => b.length - a.length).map(escape);
|
|
41
|
+
const alt = names.join('|');
|
|
42
|
+
const re = new RegExp(
|
|
43
|
+
[
|
|
44
|
+
// ../<skill>/ — a link into a sibling skill's folder
|
|
45
|
+
`(?<link>\\.\\./)(?<dir>${escape(ROUTER)}|${alt})(?=/)`,
|
|
46
|
+
// ep-<skill> and ep-* — a skill name, with or without its slash
|
|
47
|
+
`(?<![\\w-])${escape(CANONICAL)}-(?<name>${alt}|\\*)(?![\\w-])`,
|
|
48
|
+
// /ep — the router's slash command
|
|
49
|
+
`(?<![\\w/.-])/${escape(CANONICAL)}(?<router>)(?![\\w-])`,
|
|
50
|
+
].join('|'),
|
|
51
|
+
'g',
|
|
52
|
+
);
|
|
53
|
+
return text.replace(re, (...args) => {
|
|
54
|
+
const g = args.at(-1);
|
|
55
|
+
if (g.link !== undefined) return `../${installedName(g.dir, prefix)}`;
|
|
56
|
+
if (g.name !== undefined) return `${prefix}-${g.name}`;
|
|
57
|
+
return `/${prefix}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function rewriteFrontmatterName(text, skill, prefix) {
|
|
62
|
+
const end = text.indexOf('\n---', 3);
|
|
63
|
+
if (!text.startsWith('---\n') || end < 0) throw new Error(`${skill}/SKILL.md has no frontmatter`);
|
|
64
|
+
const head = text.slice(0, end).replace(/^name: .*$/m, `name: ${installedName(skill, prefix)}`);
|
|
65
|
+
return head + text.slice(end);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function walk(dir) {
|
|
69
|
+
return readdirSync(dir).flatMap((entry) => {
|
|
70
|
+
const path = join(dir, entry);
|
|
71
|
+
return statSync(path).isDirectory() ? walk(path) : [path];
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Writes the prefixed skills into outDir, one folder per installed name. Returns the names. */
|
|
76
|
+
export function build(sourceDir, prefix, outDir) {
|
|
77
|
+
validatePrefix(prefix);
|
|
78
|
+
const skills = packSkills(sourceDir);
|
|
79
|
+
if (!skills.includes(ROUTER)) throw new Error(`The pack at ${sourceDir} has no "${ROUTER}" skill.`);
|
|
80
|
+
mkdirSync(outDir, { recursive: true });
|
|
81
|
+
for (const skill of skills) {
|
|
82
|
+
const from = join(sourceDir, skill);
|
|
83
|
+
const to = join(outDir, installedName(skill, prefix));
|
|
84
|
+
for (const file of walk(from)) {
|
|
85
|
+
const target = join(to, relative(from, file));
|
|
86
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
87
|
+
if (!file.endsWith('.md')) {
|
|
88
|
+
cpSync(file, target);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
let text = rewriteText(readFileSync(file, 'utf8'), skills, prefix);
|
|
92
|
+
if (relative(from, file) === 'SKILL.md') text = rewriteFrontmatterName(text, skill, prefix);
|
|
93
|
+
writeFileSync(target, text);
|
|
94
|
+
}
|
|
95
|
+
// A skill carrying text adapted from another project ships that project's license with it.
|
|
96
|
+
const notice = join(sourceDir, '..', 'NOTICE');
|
|
97
|
+
const adapted = walk(from).some((f) => f.endsWith('.md') && readFileSync(f, 'utf8').includes(UPSTREAM_MARK));
|
|
98
|
+
if (adapted && existsSync(notice)) cpSync(notice, join(to, 'NOTICE'));
|
|
99
|
+
}
|
|
100
|
+
return skills.map((s) => installedName(s, prefix));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A skill's own version: `metadata.version` in its SKILL.md frontmatter, or null. */
|
|
104
|
+
export function skillVersion(skillDir) {
|
|
105
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(readFileSync(join(skillDir, 'SKILL.md'), 'utf8'))?.[1] ?? '';
|
|
106
|
+
return /^ {2}version: (\S+)\s*$/m.exec(fm)?.[1] ?? null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** GitHub's heading anchor: lowercase, punctuation dropped, spaces to hyphens. */
|
|
110
|
+
export function slug(heading) {
|
|
111
|
+
return heading
|
|
112
|
+
.trim()
|
|
113
|
+
.toLowerCase()
|
|
114
|
+
.replace(/[`*_]/g, '')
|
|
115
|
+
.replace(/[^\p{L}\p{N}\s-]/gu, '')
|
|
116
|
+
.replace(/\s/g, '-');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function anchors(text) {
|
|
120
|
+
const seen = new Map();
|
|
121
|
+
const out = new Set();
|
|
122
|
+
for (const line of text.replace(/```[\s\S]*?```/g, '').split('\n')) {
|
|
123
|
+
const m = /^#{1,6}\s+(.*)$/.exec(line);
|
|
124
|
+
if (!m) continue;
|
|
125
|
+
const base = slug(m[1]);
|
|
126
|
+
const n = seen.get(base) ?? 0;
|
|
127
|
+
seen.set(base, n + 1);
|
|
128
|
+
out.add(n === 0 ? base : `${base}-${n}`);
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Checks installed pack skills in dir (folders named <prefix>-<skill> and <prefix>).
|
|
135
|
+
* Returns a list of problems; empty means consistent.
|
|
136
|
+
*/
|
|
137
|
+
export function check(dir, prefix, { canonicalSkills } = {}) {
|
|
138
|
+
validatePrefix(prefix);
|
|
139
|
+
const problems = [];
|
|
140
|
+
const installed = readdirSync(dir).filter((name) => {
|
|
141
|
+
const file = join(dir, name, 'SKILL.md');
|
|
142
|
+
return existsSync(file) && readFileSync(file, 'utf8').includes(`\n ${MARKER}\n`);
|
|
143
|
+
});
|
|
144
|
+
const set = new Set(installed);
|
|
145
|
+
if (!set.has(prefix)) problems.push(`router "${prefix}" is not installed`);
|
|
146
|
+
const nameRe = new RegExp(`(?<![\\w-])/?${escape(prefix)}-([a-z0-9]+(?:-[a-z0-9]+)*|\\*)(?![\\w-])`, 'g');
|
|
147
|
+
const canonical = canonicalSkills?.filter((s) => s !== ROUTER).map(escape).join('|');
|
|
148
|
+
const leftovers = [];
|
|
149
|
+
if (prefix !== CANONICAL && canonical) leftovers.push(`(?<![\\w-])${CANONICAL}-(?:${canonical})(?![\\w-])`, `(?<![\\w/.-])/${CANONICAL}(?![\\w-])`);
|
|
150
|
+
if (prefix !== ROUTER) leftovers.push(`\\.\\./${ROUTER}/`);
|
|
151
|
+
const leftover = leftovers.length ? new RegExp(leftovers.join('|'), 'g') : null;
|
|
152
|
+
|
|
153
|
+
for (const name of installed) {
|
|
154
|
+
const root = join(dir, name);
|
|
155
|
+
const skillText = readFileSync(join(root, 'SKILL.md'), 'utf8');
|
|
156
|
+
const fm = /^---\n([\s\S]*?)\n---/.exec(skillText)?.[1] ?? '';
|
|
157
|
+
const declared = /^name: (.*)$/m.exec(fm)?.[1];
|
|
158
|
+
if (declared !== name) problems.push(`${name}/SKILL.md: name is "${declared}", folder is "${name}"`);
|
|
159
|
+
if (!/^description: .+/m.test(fm)) problems.push(`${name}/SKILL.md: no description`);
|
|
160
|
+
|
|
161
|
+
for (const file of walk(root).filter((f) => f.endsWith('.md'))) {
|
|
162
|
+
const where = relative(dir, file);
|
|
163
|
+
const text = readFileSync(file, 'utf8');
|
|
164
|
+
const prose = text.replace(/```[\s\S]*?```/g, '').replace(/`[^`\n]*`/g, '');
|
|
165
|
+
for (const m of prose.matchAll(/\]\(([^)\s]+)\)/g)) {
|
|
166
|
+
const href = m[1];
|
|
167
|
+
if (/^[a-z]+:/i.test(href)) continue;
|
|
168
|
+
const [path, hash] = href.split('#');
|
|
169
|
+
const target = path ? resolve(dirname(file), path) : file;
|
|
170
|
+
if (!existsSync(target)) {
|
|
171
|
+
problems.push(`${where}: link ${href} points to a missing file`);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (hash && target.endsWith('.md') && !anchors(readFileSync(target, 'utf8')).has(hash)) {
|
|
175
|
+
problems.push(`${where}: link ${href} points to a missing heading`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
for (const m of text.matchAll(nameRe)) {
|
|
179
|
+
if (m[1] !== '*' && !set.has(`${prefix}-${m[1]}`)) problems.push(`${where}: names ${m[0]}, which is not installed`);
|
|
180
|
+
}
|
|
181
|
+
if (leftover) for (const m of text.matchAll(leftover)) problems.push(`${where}: canonical reference "${m[0]}" was not rewritten`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return problems;
|
|
185
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@epicparty/skills",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A delivery workflow for coding agents — explore, story, implement, PR, ship — installed under your project's own prefix.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"epicparty-skills": "bin/cli.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"skills",
|
|
14
|
+
"NOTICE"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test",
|
|
21
|
+
"changeset": "changeset",
|
|
22
|
+
"version": "node scripts/version-skills.mjs && changeset version",
|
|
23
|
+
"check:changesets": "node scripts/version-skills.mjs --check"
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/epicparty/skills.git"
|
|
28
|
+
},
|
|
29
|
+
"keywords": [
|
|
30
|
+
"agent-skills",
|
|
31
|
+
"claude-code",
|
|
32
|
+
"codex",
|
|
33
|
+
"skills"
|
|
34
|
+
],
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@changesets/changelog-github": "1.0.1",
|
|
37
|
+
"@changesets/cli": "3.0.3"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Design it twice
|
|
2
|
+
|
|
3
|
+
<!-- upstream: mattpocock/skills skills/engineering/codebase-design/DESIGN-IT-TWICE.md -->
|
|
4
|
+
|
|
5
|
+
When the shape of an interface is the open question — for a new feature's module, or a chosen deepening candidate — use this parallel sub-agent pattern. Where subagents are unavailable, write the designs yourself, one after another, each under its own constraint, and keep step 3 as it is. Based on "Design It Twice" (Ousterhout): your first idea is unlikely to be the best.
|
|
6
|
+
|
|
7
|
+
Uses the vocabulary in [`SEAMS.md`](../to-story/SEAMS.md): **module**, **interface**, **seam**, **adapter**, **leverage**.
|
|
8
|
+
|
|
9
|
+
## Process
|
|
10
|
+
|
|
11
|
+
### 1. Frame the problem space
|
|
12
|
+
|
|
13
|
+
Before spawning sub-agents, write a user-facing explanation of the problem space for the chosen candidate:
|
|
14
|
+
|
|
15
|
+
- The constraints any new interface would need to satisfy
|
|
16
|
+
- The dependencies it would rely on, and which category they fall into (see [`SEAMS.md`](../to-story/SEAMS.md#dependencies-decide-how-a-seam-is-tested))
|
|
17
|
+
- A rough illustrative code sketch to ground the constraints, not a proposal, just a way to make the constraints concrete
|
|
18
|
+
|
|
19
|
+
Show this to the user, then immediately proceed to Step 2. The user reads and thinks while the sub-agents work in parallel.
|
|
20
|
+
|
|
21
|
+
### 2. Spawn sub-agents
|
|
22
|
+
|
|
23
|
+
Spawn 3+ sub-agents in parallel, each with a fresh context window. Each must produce a **radically different** interface for the deepened module.
|
|
24
|
+
|
|
25
|
+
Prompt each sub-agent with a separate technical brief (file paths, coupling details, dependency category from `SEAMS.md`, what sits behind the seam). The brief is independent of the user-facing problem-space explanation in Step 1. Give each agent a different design constraint:
|
|
26
|
+
|
|
27
|
+
- Agent 1: "Minimize the interface: aim for 1–3 entry points max. Maximise leverage per entry point."
|
|
28
|
+
- Agent 2: "Maximise flexibility: support many use cases and extension."
|
|
29
|
+
- Agent 3: "Optimise for the most common caller: make the default case trivial."
|
|
30
|
+
- Agent 4 (if applicable): "Design around ports & adapters for cross-seam dependencies."
|
|
31
|
+
|
|
32
|
+
Include both the `SEAMS.md` vocabulary and the project glossary's in the brief so each sub-agent names things consistently with the architecture language and the project's domain language.
|
|
33
|
+
|
|
34
|
+
Each sub-agent outputs:
|
|
35
|
+
|
|
36
|
+
1. Interface (types, methods, params, plus invariants, ordering, error modes)
|
|
37
|
+
2. Usage example showing how callers use it
|
|
38
|
+
3. What the implementation hides behind the seam
|
|
39
|
+
4. Dependency strategy and adapters, by the `SEAMS.md` categories
|
|
40
|
+
5. Trade-offs: where leverage is high, where it's thin
|
|
41
|
+
|
|
42
|
+
### 3. Present and compare
|
|
43
|
+
|
|
44
|
+
Present designs sequentially so the user can absorb each one, then compare them in prose. Contrast by **depth** (leverage at the interface), **locality** (where change concentrates), and **seam placement**.
|
|
45
|
+
|
|
46
|
+
After comparing, give your own recommendation: which design you think is strongest and why. If elements from different designs would combine well, propose a hybrid. Be opinionated: the user wants a strong read, not a menu.
|