@skyf0xx/hedgehog 3.0.5 → 3.0.7
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 +31 -16
- package/bin/cli.mjs +195 -71
- package/package.json +6 -2
- package/src/db/graph.mjs +32 -2
- package/src/hosts/capabilities.mjs +89 -0
- package/src/hosts/claude/DISPATCH.md +9 -0
- package/src/hosts/cursor/DISPATCH.md +16 -0
- package/src/hosts/cursor/hedgehog.mdc +20 -0
- package/src/hosts/emit.mjs +23 -0
- package/src/hosts/frontmatter.mjs +48 -0
- package/src/hosts/gemini/DISPATCH.md +27 -0
- package/src/hosts/gemini/gemini-extension.json +6 -0
- package/src/hosts/index.mjs +124 -0
- package/src/hosts/installed.mjs +58 -0
- package/src/hosts/routing.mjs +144 -0
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +2 -2
- package/src/skills/hedgehog-bootstrap-landing-page-core/SKILL.md +2 -2
- package/src/skills/hedgehog-core-design/SKILL.md +80 -15
- package/src/skills/hedgehog-core-design/blueprints/bot-agent.md +35 -0
- package/src/skills/hedgehog-core-design/blueprints/browser-extension.md +26 -0
- package/src/skills/hedgehog-core-design/blueprints/cli.md +26 -0
- package/src/skills/hedgehog-core-design/blueprints/compiler-language-tool.md +36 -0
- package/src/skills/hedgehog-core-design/blueprints/data-pipeline.md +36 -0
- package/src/skills/hedgehog-core-design/blueprints/desktop-app.md +34 -0
- package/src/skills/hedgehog-core-design/blueprints/game.md +35 -0
- package/src/skills/hedgehog-core-design/blueprints/infra-deploy-tool.md +34 -0
- package/src/skills/hedgehog-core-design/blueprints/library-sdk.md +31 -0
- package/src/templates/CLAUDE.md +6 -4
- package/src/templates/graph.html +46 -0
package/README.md
CHANGED
|
@@ -88,7 +88,7 @@ Artifact
|
|
|
88
88
|
|
|
89
89
|
### Anything else
|
|
90
90
|
|
|
91
|
-
A CLI, a library, a browser extension, a data pipeline, etc. fitting neither shape gets its own build order, designed at intake rather than chosen from a menu.
|
|
91
|
+
A CLI, a library, a browser extension, a data pipeline, etc. fitting neither shape gets its own build order, designed at intake rather than chosen from a menu — starting from a [battle-tested blueprint](src/skills/hedgehog-core-design/blueprints) for the system's shape where one exists.
|
|
92
92
|
|
|
93
93
|
Run `init` with no core flag: planning intake names the system shape, picks
|
|
94
94
|
the stack, derives the layers, and locks them to `.hedgehog/core.yaml`,
|
|
@@ -101,7 +101,7 @@ scoped file access and a verification command per layer.
|
|
|
101
101
|
|
|
102
102
|
## Install
|
|
103
103
|
|
|
104
|
-
From an empty project folder,
|
|
104
|
+
From an empty project folder, run:
|
|
105
105
|
|
|
106
106
|
``` bash
|
|
107
107
|
# Full-stack app
|
|
@@ -114,7 +114,29 @@ npx @skyf0xx/hedgehog init --landing-page
|
|
|
114
114
|
npx @skyf0xx/hedgehog init
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
Then open
|
|
117
|
+
Then open your coding agent and describe what you want to build.
|
|
118
|
+
|
|
119
|
+
### Coding agents
|
|
120
|
+
|
|
121
|
+
Hedgehog installs for **Claude Code** by default. Add a host flag to
|
|
122
|
+
install for another one, or several at once:
|
|
123
|
+
|
|
124
|
+
``` bash
|
|
125
|
+
npx @skyf0xx/hedgehog init --cursor # Cursor
|
|
126
|
+
npx @skyf0xx/hedgehog init --gemini # Gemini CLI
|
|
127
|
+
npx @skyf0xx/hedgehog init --host=claude,cursor # both
|
|
128
|
+
npx @skyf0xx/hedgehog init --all-hosts # every supported agent
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Each one gets the discipline in its own native shape — agents and skills
|
|
132
|
+
in the directory it reads, and the instructions file it loads at session
|
|
133
|
+
start (`CLAUDE.md`, `HEDGEHOG.md`, or `GEMINI.md`).
|
|
134
|
+
|
|
135
|
+
Every install also writes **`AGENTS.md`** at the repo root: an index of
|
|
136
|
+
every agent and skill, when each applies, and the build loop. Coding
|
|
137
|
+
agents that read `AGENTS.md` — Codex, Copilot CLI, OpenCode, and others —
|
|
138
|
+
work from that index, following the same ordered steps and the same
|
|
139
|
+
`hedgehog verify` gate.
|
|
118
140
|
|
|
119
141
|
Plain `init` (no core flag) installs the agents, skills, and build graph
|
|
120
142
|
that every core shares. Planning intake designs an opinionated build
|
|
@@ -128,9 +150,11 @@ To update:
|
|
|
128
150
|
npx @skyf0xx/hedgehog update
|
|
129
151
|
```
|
|
130
152
|
|
|
131
|
-
This refreshes
|
|
132
|
-
|
|
133
|
-
|
|
153
|
+
This refreshes the installed agents and skills — for every coding agent
|
|
154
|
+
the project was set up for — along with the `AGENTS.md` index derived
|
|
155
|
+
from them. It never touches the instructions file, the build graph, the
|
|
156
|
+
core workspace, or `skills/BMAD`, since those carry project-specific or
|
|
157
|
+
write-once content.
|
|
134
158
|
|
|
135
159
|
To see the build graph:
|
|
136
160
|
|
|
@@ -139,16 +163,7 @@ npx @skyf0xx/hedgehog graph
|
|
|
139
163
|
```
|
|
140
164
|
|
|
141
165
|
Starts a small local server and opens a live, read-only diagram of every
|
|
142
|
-
task and its dependencies
|
|
143
|
-
status, laid out top-to-bottom by dependency order. Click a task to see
|
|
144
|
-
its objective, verify command, and commit message; click empty canvas to
|
|
145
|
-
close it. The page polls for changes, so it keeps updating on its own as
|
|
146
|
-
`hedgehog verify` moves tasks through their lifecycle — no re-running the
|
|
147
|
-
command or reloading the page. Running `graph` again while a server is
|
|
148
|
-
already up reuses it instead of starting a second one. Pass `--no-open`
|
|
149
|
-
to start (or reuse) the server and print its URL instead of launching a
|
|
150
|
-
browser. `hedgehog plan` opens the same live view automatically whenever
|
|
151
|
-
it compiles new tasks.
|
|
166
|
+
task, status and its dependencies.
|
|
152
167
|
|
|
153
168
|
## Why Hedgehog
|
|
154
169
|
|
package/bin/cli.mjs
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
6
6
|
// npx @skyf0xx/hedgehog init install; planner picks the core at intake
|
|
7
7
|
// npx @skyf0xx/hedgehog init --ts-full-stack-app scaffold the full-stack-app core now
|
|
8
8
|
// npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core now
|
|
9
|
+
// npx @skyf0xx/hedgehog init --cursor install for Cursor (default: Claude Code)
|
|
10
|
+
// npx @skyf0xx/hedgehog init --all-hosts install for every supported coding agent
|
|
9
11
|
// npx @skyf0xx/hedgehog init --force overwrite files that already exist
|
|
10
|
-
// npx @skyf0xx/hedgehog update refresh
|
|
12
|
+
// npx @skyf0xx/hedgehog update refresh the installed agents + skills
|
|
11
13
|
// npx @skyf0xx/hedgehog --help
|
|
12
14
|
|
|
13
15
|
import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile } from 'node:fs/promises';
|
|
@@ -25,6 +27,8 @@ import { verifyTask } from '../src/db/verify.mjs';
|
|
|
25
27
|
import { graphStatus, formatStatus } from '../src/db/status.mjs';
|
|
26
28
|
import { whyPath, formatWhy } from '../src/db/why.mjs';
|
|
27
29
|
import { addFriction, listFriction } from '../src/db/friction.mjs';
|
|
30
|
+
import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
|
|
31
|
+
import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
|
|
28
32
|
|
|
29
33
|
const AUTHORED_CORE_PATH = '.hedgehog/core.yaml';
|
|
30
34
|
|
|
@@ -84,26 +88,49 @@ async function availableCores() {
|
|
|
84
88
|
// core `planner` picks — the first time either way. An explicit flag
|
|
85
89
|
// (`--ts-full-stack-app`, `--landing-page`) is a confirmed choice, so it
|
|
86
90
|
// scaffolds that workspace immediately, at install time.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
{ type: 'dir', from: 'skills
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
|
|
91
|
+
// `hostOnly` plans just the parts that differ per host — used when a
|
|
92
|
+
// second host is added to a project whose shared payload (the vendored
|
|
93
|
+
// shelves, the core workspace) is already on disk.
|
|
94
|
+
function plan(core, host = DEFAULT_HOST, { hostOnly = false } = {}) {
|
|
95
|
+
const h = HOSTS[host];
|
|
96
|
+
const perHost = [
|
|
97
|
+
{ type: 'dir', from: 'src/agents', to: h.agentsDir, emit: h.emitAgent },
|
|
98
|
+
{ type: 'dir', from: 'src/skills', to: h.skillsDir },
|
|
99
|
+
// Whatever else this host needs to find the payload — its own rules
|
|
100
|
+
// file, extension manifest, or routing doc. Empty for a host that
|
|
101
|
+
// auto-loads its bootstrap file and registers agents from disk.
|
|
102
|
+
...(h.extraEntries ?? []),
|
|
98
103
|
];
|
|
99
104
|
|
|
105
|
+
// Host-independent: one copy per project however many hosts read it.
|
|
106
|
+
const shared = hostOnly
|
|
107
|
+
? []
|
|
108
|
+
: [
|
|
109
|
+
// The vendored BMAD-METHOD planning shelf that
|
|
110
|
+
// hedgehog-planning-intake runs — referenced by repo-root-relative
|
|
111
|
+
// path (skills/BMAD/...), so it lands there rather than under a
|
|
112
|
+
// host's own directory.
|
|
113
|
+
{ type: 'dir', from: 'skills/BMAD', to: 'skills/BMAD' },
|
|
114
|
+
// The vendored GSAP animation skill shelf that front-end-eng loads
|
|
115
|
+
// for motion work — same repo-root-relative referencing.
|
|
116
|
+
{ type: 'dir', from: 'skills/GSAP', to: 'skills/GSAP' },
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const base = [...perHost, ...shared];
|
|
120
|
+
|
|
100
121
|
if (core === null) {
|
|
101
122
|
return [
|
|
102
123
|
...base,
|
|
103
124
|
// The shell with its {{CORE_SECTION}} placeholder left unfilled —
|
|
104
125
|
// whichever bootstrap-core skill runs first fills it in for the
|
|
105
|
-
// core planner actually picked.
|
|
106
|
-
|
|
126
|
+
// core planner actually picked. {{HOST_DISPATCH}} is filled now:
|
|
127
|
+
// which host this is doesn't depend on the core.
|
|
128
|
+
{
|
|
129
|
+
type: 'merge',
|
|
130
|
+
shell: 'src/templates/CLAUDE.md',
|
|
131
|
+
dispatch: `src/hosts/${host}/DISPATCH.md`,
|
|
132
|
+
to: h.bootstrapFile,
|
|
133
|
+
},
|
|
107
134
|
];
|
|
108
135
|
}
|
|
109
136
|
|
|
@@ -113,30 +140,37 @@ function plan(core) {
|
|
|
113
140
|
type: 'merge',
|
|
114
141
|
shell: 'src/templates/CLAUDE.md',
|
|
115
142
|
include: `src/templates/CLAUDE.core.${core}.md`,
|
|
116
|
-
|
|
143
|
+
dispatch: `src/hosts/${host}/DISPATCH.md`,
|
|
144
|
+
to: h.bootstrapFile,
|
|
117
145
|
},
|
|
118
146
|
// The pre-built, pre-verified workspace for the chosen core —
|
|
119
147
|
// everything a fresh project of that shape needs at repo root
|
|
120
148
|
// (lands the root package.json too, so there's no separate
|
|
121
149
|
// placeholder for it). The relevant bootstrap-core skill verifies
|
|
122
150
|
// this on first run rather than generating it live.
|
|
123
|
-
{ type: 'dir', from: `src/golden-cores/${core}`, to: '.' },
|
|
151
|
+
...(hostOnly ? [] : [{ type: 'dir', from: `src/golden-cores/${core}`, to: '.' }]),
|
|
124
152
|
];
|
|
125
153
|
}
|
|
126
154
|
|
|
127
155
|
// The subset of plan() that's the discipline's payload rather than
|
|
128
156
|
// project-specific or write-once content: `update` re-copies exactly
|
|
129
|
-
// this, always overwriting, since a consuming project's
|
|
130
|
-
//
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
157
|
+
// this, always overwriting, since a consuming project's installed agents
|
|
158
|
+
// and skills are supposed to match upstream verbatim. The bootstrap file
|
|
159
|
+
// carries project-filled content, the build graph and core workspace are
|
|
160
|
+
// verified once by their own init/bootstrap-core steps, and skills/BMAD
|
|
161
|
+
// and skills/GSAP are re-vendored only deliberately (a manual re-vendor,
|
|
162
|
+
// per each shelf's ATTRIBUTION.md) — none of those belong in an update.
|
|
163
|
+
function updatePlan(host = DEFAULT_HOST) {
|
|
164
|
+
const h = HOSTS[host];
|
|
165
|
+
return [
|
|
166
|
+
{ type: 'dir', from: 'src/agents', to: h.agentsDir, emit: h.emitAgent },
|
|
167
|
+
{ type: 'dir', from: 'src/skills', to: h.skillsDir },
|
|
168
|
+
// Derived from the agents and skills above — an agent added, renamed,
|
|
169
|
+
// or redescribed upstream has to be reflected in the index that
|
|
170
|
+
// points at it, so it is regenerated alongside them.
|
|
171
|
+
...(h.extraEntries ?? []).filter((e) => e.type === 'generated'),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
140
174
|
|
|
141
175
|
const exists = (p) =>
|
|
142
176
|
access(p, constants.F_OK).then(
|
|
@@ -150,9 +184,29 @@ const exists = (p) =>
|
|
|
150
184
|
async function writePlannedFile(f) {
|
|
151
185
|
await mkdir(dirname(f.dest), { recursive: true });
|
|
152
186
|
if (f.merge) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
187
|
+
let out = await readFile(join(PKG_ROOT, f.merge.shell), 'utf8');
|
|
188
|
+
// A deferred install has no core yet, so {{CORE_SECTION}} stays put
|
|
189
|
+
// for whichever bootstrap-core skill runs first to fill in. The host
|
|
190
|
+
// is always known at install time, so {{HOST_DISPATCH}} never is.
|
|
191
|
+
if (f.merge.include) {
|
|
192
|
+
const section = await readFile(join(PKG_ROOT, f.merge.include), 'utf8');
|
|
193
|
+
out = out.replaceAll('{{CORE_SECTION}}', section.trimEnd());
|
|
194
|
+
}
|
|
195
|
+
const dispatch = await readFile(join(PKG_ROOT, f.merge.dispatch), 'utf8');
|
|
196
|
+
await writeFile(f.dest, out.replaceAll('{{HOST_DISPATCH}}', dispatch.trimEnd()));
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
// Rendered from the payload rather than copied from it — the routing
|
|
200
|
+
// doc's tables are built from the agents' and skills' own frontmatter.
|
|
201
|
+
if (f.generate) {
|
|
202
|
+
await writeFile(f.dest, await f.generate({ pkgRoot: PKG_ROOT, projectRoot: DEST_ROOT }));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// A host whose format differs from the canonical one rewrites the file
|
|
206
|
+
// on the way in. Hosts that read the canonical format have no emitter,
|
|
207
|
+
// so their payload is copied verbatim.
|
|
208
|
+
if (f.emit) {
|
|
209
|
+
await writeFile(f.dest, f.emit(await readFile(f.src, 'utf8'), { src: f.src }));
|
|
156
210
|
return;
|
|
157
211
|
}
|
|
158
212
|
await cp(f.src, f.dest);
|
|
@@ -163,9 +217,12 @@ async function plannedFiles(entry) {
|
|
|
163
217
|
if (entry.type === 'merge') {
|
|
164
218
|
return [{ dest: join(DEST_ROOT, entry.to), merge: entry }];
|
|
165
219
|
}
|
|
220
|
+
if (entry.type === 'generated') {
|
|
221
|
+
return [{ dest: join(DEST_ROOT, entry.to), generate: entry.generate }];
|
|
222
|
+
}
|
|
166
223
|
const src = join(PKG_ROOT, entry.from);
|
|
167
224
|
if (entry.type === 'file') {
|
|
168
|
-
return [{ src, dest: join(DEST_ROOT, entry.to) }];
|
|
225
|
+
return [{ src, dest: join(DEST_ROOT, entry.to), emit: entry.emit }];
|
|
169
226
|
}
|
|
170
227
|
const out = [];
|
|
171
228
|
async function walk(rel) {
|
|
@@ -175,7 +232,7 @@ async function plannedFiles(entry) {
|
|
|
175
232
|
for (const name of await readdir(abs)) await walk(join(rel, name));
|
|
176
233
|
} else {
|
|
177
234
|
const renamed = DOTFILE_RENAMES[rel] ?? rel;
|
|
178
|
-
out.push({ src: abs, dest: join(DEST_ROOT, entry.to, renamed) });
|
|
235
|
+
out.push({ src: abs, dest: join(DEST_ROOT, entry.to, renamed), emit: entry.emit });
|
|
179
236
|
}
|
|
180
237
|
}
|
|
181
238
|
await walk('.');
|
|
@@ -187,16 +244,20 @@ async function help() {
|
|
|
187
244
|
console.log(`
|
|
188
245
|
${bold('Hedgehog installer')}
|
|
189
246
|
|
|
190
|
-
Copies the Hedgehog agents and skills into
|
|
191
|
-
|
|
192
|
-
into the repo root, so
|
|
247
|
+
Copies the Hedgehog agents and skills into your coding agent's own
|
|
248
|
+
directory, drops that agent's instructions file, an AGENTS.md index, and an
|
|
249
|
+
empty build graph (${bold('.hedgehog/hedgehog.db')}) into the repo root, so
|
|
250
|
+
the discipline is committed alongside your code.
|
|
193
251
|
|
|
194
252
|
${bold('Usage')}
|
|
195
253
|
npx @skyf0xx/hedgehog init install; planner picks the core at intake
|
|
196
254
|
npx @skyf0xx/hedgehog init --ts-full-stack-app scaffold the full-stack-app core now
|
|
197
255
|
npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core now
|
|
256
|
+
npx @skyf0xx/hedgehog init --cursor install for Cursor (default: Claude Code)
|
|
257
|
+
npx @skyf0xx/hedgehog init --host=claude,gemini install for several coding agents at once
|
|
258
|
+
npx @skyf0xx/hedgehog init --all-hosts install for every supported coding agent
|
|
198
259
|
npx @skyf0xx/hedgehog init --force overwrite existing files
|
|
199
|
-
npx @skyf0xx/hedgehog update refresh
|
|
260
|
+
npx @skyf0xx/hedgehog update refresh the installed agents + skills
|
|
200
261
|
npx @skyf0xx/hedgehog db init create .hedgehog/hedgehog.db if absent
|
|
201
262
|
npx @skyf0xx/hedgehog plan compile pending intents into tasks + dependencies,
|
|
202
263
|
then open the build graph if anything compiled
|
|
@@ -213,10 +274,11 @@ ${bold('Usage')}
|
|
|
213
274
|
npx @skyf0xx/hedgehog --help
|
|
214
275
|
|
|
215
276
|
Available cores: ${cores.join(', ')}
|
|
277
|
+
Available hosts: ${availableHosts().join(', ')} (default: ${DEFAULT_HOST})
|
|
216
278
|
|
|
217
|
-
After it runs, commit the payload, open
|
|
218
|
-
you want to build — the planner agent runs planning intake, then
|
|
219
|
-
off to bootstrap.
|
|
279
|
+
After it runs, commit the payload, open your coding agent, and describe
|
|
280
|
+
what you want to build — the planner agent runs planning intake, then
|
|
281
|
+
hands off to bootstrap.
|
|
220
282
|
|
|
221
283
|
Building something else (a CLI, library, browser extension, data
|
|
222
284
|
pipeline, desktop app, etc.)? Run plain 'init' with no core flag rather
|
|
@@ -226,16 +288,17 @@ shares. The planner agent designs a core at planning intake
|
|
|
226
288
|
(hedgehog-core-design) and bootstrap generates that workspace once it's
|
|
227
289
|
confirmed. Describe the actual project and let Phase 0 route it.
|
|
228
290
|
|
|
229
|
-
${bold('update')} re-copies
|
|
230
|
-
installed Hedgehog version, so an
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
291
|
+
${bold('update')} re-copies the agents and skills (and the AGENTS.md index
|
|
292
|
+
derived from them) from the installed Hedgehog version, so an
|
|
293
|
+
already-bootstrapped project can pick up changes from a newer release. It
|
|
294
|
+
refreshes every host the project was installed for, always overwriting
|
|
295
|
+
those directories. The instructions file, the build graph, the core
|
|
296
|
+
workspace, and skills/BMAD and skills/GSAP stay as they are — those are
|
|
297
|
+
project-specific or updated deliberately, not by this command.
|
|
235
298
|
`);
|
|
236
299
|
}
|
|
237
300
|
|
|
238
|
-
async function init({ force, core, explicitCore }) {
|
|
301
|
+
async function init({ force, core, explicitCore, host = DEFAULT_HOST, hostOnly = false }) {
|
|
239
302
|
if (explicitCore) {
|
|
240
303
|
const cores = await availableCores();
|
|
241
304
|
if (!cores.includes(core)) {
|
|
@@ -251,13 +314,18 @@ async function init({ force, core, explicitCore }) {
|
|
|
251
314
|
// before touching anything. A deferred install (no explicit core) plans
|
|
252
315
|
// against `null` — the shared agents/skills/build-graph payload only.
|
|
253
316
|
const groups = [];
|
|
254
|
-
for (const entry of plan(explicitCore ? core : null)) {
|
|
317
|
+
for (const entry of plan(explicitCore ? core : null, host, { hostOnly })) {
|
|
255
318
|
const files = await plannedFiles(entry);
|
|
256
319
|
groups.push({ entry, files });
|
|
257
320
|
}
|
|
258
321
|
|
|
322
|
+
// A generated file is derived from the payload rather than authored in
|
|
323
|
+
// the project, so rewriting it loses nothing and never counts as a
|
|
324
|
+
// conflict — that's what lets a second host be added to a project the
|
|
325
|
+
// first one already set up.
|
|
259
326
|
const conflicts = [];
|
|
260
|
-
for (const { files } of groups) {
|
|
327
|
+
for (const { entry, files } of groups) {
|
|
328
|
+
if (entry.type === 'generated') continue;
|
|
261
329
|
for (const f of files) {
|
|
262
330
|
if (await exists(f.dest)) conflicts.push(f.dest);
|
|
263
331
|
}
|
|
@@ -275,6 +343,10 @@ async function init({ force, core, explicitCore }) {
|
|
|
275
343
|
return;
|
|
276
344
|
}
|
|
277
345
|
|
|
346
|
+
// Recorded before anything is written: the routing doc is generated
|
|
347
|
+
// from this list, so it has to already name the host being installed.
|
|
348
|
+
await recordHosts(DEST_ROOT, [host]);
|
|
349
|
+
|
|
278
350
|
let written = 0;
|
|
279
351
|
let overwritten = 0;
|
|
280
352
|
for (const { files } of groups) {
|
|
@@ -301,10 +373,10 @@ async function init({ force, core, explicitCore }) {
|
|
|
301
373
|
if (explicitCore) {
|
|
302
374
|
console.log(` 1. ${bold('git add -A && git commit -m "chore: install Hedgehog"')}`);
|
|
303
375
|
console.log(` 2. ${bold('pnpm install')}`);
|
|
304
|
-
console.log(` 3. Open
|
|
376
|
+
console.log(` 3. Open ${HOSTS[host].label} and describe what you want to build.`);
|
|
305
377
|
} else {
|
|
306
378
|
console.log(` 1. ${bold('git add -A && git commit -m "chore: install Hedgehog"')}`);
|
|
307
|
-
console.log(` 2. Open
|
|
379
|
+
console.log(` 2. Open ${HOSTS[host].label} and describe what you want to build.`);
|
|
308
380
|
}
|
|
309
381
|
console.log(
|
|
310
382
|
dim(
|
|
@@ -333,36 +405,55 @@ async function init({ force, core, explicitCore }) {
|
|
|
333
405
|
}
|
|
334
406
|
}
|
|
335
407
|
|
|
336
|
-
async function update() {
|
|
337
|
-
|
|
338
|
-
// rename or removal upstream (e.g. an agent renamed between releases)
|
|
339
|
-
// doesn't leave a stale file sitting alongside the new one.
|
|
340
|
-
for (const entry of UPDATE_PLAN) {
|
|
341
|
-
await rm(join(DEST_ROOT, entry.to), { recursive: true, force: true });
|
|
342
|
-
}
|
|
408
|
+
async function update({ hosts }) {
|
|
409
|
+
const targets = hosts?.length ? hosts : await installedHosts(DEST_ROOT);
|
|
343
410
|
|
|
344
411
|
let written = 0;
|
|
345
|
-
for (const
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
412
|
+
for (const host of targets) {
|
|
413
|
+
const entries = updatePlan(host);
|
|
414
|
+
|
|
415
|
+
// Full replace, not a merge: clear each payload directory first so a
|
|
416
|
+
// rename or removal upstream (e.g. an agent renamed between releases)
|
|
417
|
+
// doesn't leave a stale file sitting alongside the new one. Generated
|
|
418
|
+
// files are single files rewritten in place, so they're left alone.
|
|
419
|
+
for (const entry of entries) {
|
|
420
|
+
if (entry.type === 'dir') {
|
|
421
|
+
await rm(join(DEST_ROOT, entry.to), { recursive: true, force: true });
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
for (const f of await plannedFiles(entry)) {
|
|
427
|
+
await writePlannedFile(f);
|
|
428
|
+
written++;
|
|
429
|
+
console.log(` ${green('update')} ${relative(DEST_ROOT, f.dest)}`);
|
|
430
|
+
}
|
|
352
431
|
}
|
|
353
432
|
}
|
|
354
433
|
|
|
434
|
+
const label = targets.map((h) => HOSTS[h].label).join(', ');
|
|
355
435
|
console.log(
|
|
356
|
-
`\n${green(bold('Hedgehog agents/skills updated.'))} ${dim(
|
|
436
|
+
`\n${green(bold('Hedgehog agents/skills updated.'))} ${dim(
|
|
437
|
+
`${written} files written for ${label}`,
|
|
438
|
+
)}\n`,
|
|
357
439
|
);
|
|
358
440
|
console.log('Next steps:');
|
|
359
|
-
|
|
441
|
+
const reviewDirs = [
|
|
442
|
+
...new Set(
|
|
443
|
+
targets.map((h) => {
|
|
444
|
+
const d = dirname(HOSTS[h].agentsDir);
|
|
445
|
+
return d === '.' ? HOSTS[h].agentsDir : `${d}/`;
|
|
446
|
+
}),
|
|
447
|
+
),
|
|
448
|
+
];
|
|
449
|
+
console.log(` 1. ${bold(`git diff ${reviewDirs.join(' ')}`)} to review what changed`);
|
|
360
450
|
console.log(` 2. ${bold('git add -A && git commit -m "chore: update hedgehog"')}\n`);
|
|
451
|
+
const bootstraps = [...new Set(targets.map((h) => HOSTS[h].bootstrapFile))].join(', ');
|
|
361
452
|
console.log(
|
|
362
453
|
dim(
|
|
363
|
-
|
|
364
|
-
'and skills/GSAP are untouched — those carry
|
|
365
|
-
'write-once content.',
|
|
454
|
+
`${bootstraps}, the build graph, the core workspace, and\n` +
|
|
455
|
+
'skills/BMAD and skills/GSAP are untouched — those carry\n' +
|
|
456
|
+
'project-specific or write-once content.',
|
|
366
457
|
),
|
|
367
458
|
);
|
|
368
459
|
}
|
|
@@ -912,13 +1003,46 @@ async function main() {
|
|
|
912
1003
|
}
|
|
913
1004
|
const core = coreFlag ? CORE_FLAGS[coreFlag] : DEFAULT_CORE;
|
|
914
1005
|
|
|
1006
|
+
// Which coding agent this repo is being set up for. `--host=<name>` and
|
|
1007
|
+
// the per-host shorthand flags are equivalent; absent either, the
|
|
1008
|
+
// default host applies.
|
|
1009
|
+
const allHosts = args.includes('--all-hosts');
|
|
1010
|
+
const hostFlags = args.filter((a) => a in HOST_FLAGS).map((a) => HOST_FLAGS[a]);
|
|
1011
|
+
const hostEq = args
|
|
1012
|
+
.filter((a) => a.startsWith('--host='))
|
|
1013
|
+
.flatMap((a) => a.slice('--host='.length).split(','))
|
|
1014
|
+
.map((h) => h.trim())
|
|
1015
|
+
.filter(Boolean);
|
|
1016
|
+
const named = [...new Set([...hostFlags, ...hostEq])];
|
|
1017
|
+
const unknown = named.filter((h) => !(h in HOSTS));
|
|
1018
|
+
if (unknown.length) {
|
|
1019
|
+
console.error(
|
|
1020
|
+
`${red('Unknown host:')} ${unknown.join(', ')}\n\n` +
|
|
1021
|
+
`Available hosts: ${availableHosts().join(', ')}\n`,
|
|
1022
|
+
);
|
|
1023
|
+
process.exitCode = 1;
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
const hosts = allHosts ? availableHosts() : named;
|
|
1027
|
+
|
|
915
1028
|
if (cmd === 'init') {
|
|
916
|
-
|
|
1029
|
+
// The shared payload — vendored shelves, core workspace — lands with
|
|
1030
|
+
// the first host; the rest add only what differs per host.
|
|
1031
|
+
const targets = hosts.length ? hosts : [DEFAULT_HOST];
|
|
1032
|
+
for (const [i, host] of targets.entries()) {
|
|
1033
|
+
await init({
|
|
1034
|
+
force,
|
|
1035
|
+
core,
|
|
1036
|
+
explicitCore: Boolean(coreFlag),
|
|
1037
|
+
host,
|
|
1038
|
+
hostOnly: i > 0,
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
917
1041
|
return;
|
|
918
1042
|
}
|
|
919
1043
|
|
|
920
1044
|
if (cmd === 'update') {
|
|
921
|
-
await update();
|
|
1045
|
+
await update({ hosts });
|
|
922
1046
|
return;
|
|
923
1047
|
}
|
|
924
1048
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyf0xx/hedgehog",
|
|
3
|
-
"version": "3.0.
|
|
4
|
-
"description": "Install the Hedgehog build discipline (agents + skills) into a repo.",
|
|
3
|
+
"version": "3.0.7",
|
|
4
|
+
"description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"bin",
|
|
21
21
|
"src/agents",
|
|
22
22
|
"src/db",
|
|
23
|
+
"src/hosts",
|
|
23
24
|
"src/skills",
|
|
24
25
|
"src/templates",
|
|
25
26
|
"src/golden-cores",
|
|
@@ -32,6 +33,9 @@
|
|
|
32
33
|
"keywords": [
|
|
33
34
|
"claude",
|
|
34
35
|
"claude-code",
|
|
36
|
+
"cursor",
|
|
37
|
+
"gemini-cli",
|
|
38
|
+
"agents-md",
|
|
35
39
|
"agents",
|
|
36
40
|
"skills",
|
|
37
41
|
"scaffold",
|
package/src/db/graph.mjs
CHANGED
|
@@ -18,6 +18,16 @@ const ALL_DEPENDENCIES_SQL = `
|
|
|
18
18
|
SELECT task_id, depends_on_task_id FROM dependencies;
|
|
19
19
|
`;
|
|
20
20
|
|
|
21
|
+
// One query for every task's requirement links, not one query per task —
|
|
22
|
+
// this runs on every poll (see graph-server.mjs), so an N+1 here would
|
|
23
|
+
// mean N+1 queries every 2 seconds rather than once.
|
|
24
|
+
const ALL_TASK_REQUIREMENTS_SQL = `
|
|
25
|
+
SELECT tr.task_id, r.kind, r.statement
|
|
26
|
+
FROM task_requirements tr
|
|
27
|
+
JOIN requirements r ON r.id = tr.requirement_id
|
|
28
|
+
ORDER BY tr.task_id, r.id;
|
|
29
|
+
`;
|
|
30
|
+
|
|
21
31
|
function loadAllTasks(db) {
|
|
22
32
|
return db.prepare(ALL_TASKS_SQL).all();
|
|
23
33
|
}
|
|
@@ -26,13 +36,32 @@ function loadAllDependencies(db) {
|
|
|
26
36
|
return db.prepare(ALL_DEPENDENCIES_SQL).all();
|
|
27
37
|
}
|
|
28
38
|
|
|
39
|
+
function loadAllTaskRequirements(db) {
|
|
40
|
+
return db.prepare(ALL_TASK_REQUIREMENTS_SQL).all();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Groups the flat requirement rows by task_id, same rule/constraint/
|
|
44
|
+
// acceptance mix hedgehog next's RELEVANT RULES section shows — a task
|
|
45
|
+
// is bound by all three kinds equally, so the graph's detail panel
|
|
46
|
+
// doesn't split them out either.
|
|
47
|
+
function groupRequirementsByTask(rows) {
|
|
48
|
+
const byTask = new Map();
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
if (!byTask.has(row.task_id)) byTask.set(row.task_id, []);
|
|
51
|
+
byTask.get(row.task_id).push({ kind: row.kind, statement: row.statement });
|
|
52
|
+
}
|
|
53
|
+
return byTask;
|
|
54
|
+
}
|
|
55
|
+
|
|
29
56
|
// Shapes the full build graph into { nodes, edges } for the viewer.
|
|
30
57
|
// Each node carries exactly the fields the viewer displays on click
|
|
31
|
-
// (objective, verify_command, commit_message) plus the
|
|
32
|
-
// layout and status colour — nothing the viewer
|
|
58
|
+
// (objective, verify_command, commit_message, requirements) plus the
|
|
59
|
+
// fields that drive layout and status colour — nothing the viewer
|
|
60
|
+
// doesn't render.
|
|
33
61
|
export function buildGraph(db) {
|
|
34
62
|
const tasks = loadAllTasks(db);
|
|
35
63
|
const dependencies = loadAllDependencies(db);
|
|
64
|
+
const requirementsByTask = groupRequirementsByTask(loadAllTaskRequirements(db));
|
|
36
65
|
|
|
37
66
|
const nodes = tasks.map((t) => ({
|
|
38
67
|
id: t.id,
|
|
@@ -43,6 +72,7 @@ export function buildGraph(db) {
|
|
|
43
72
|
verifyCommand: t.verify_command,
|
|
44
73
|
commitMessage: t.commit_message,
|
|
45
74
|
intentGoal: t.intent_goal,
|
|
75
|
+
requirements: requirementsByTask.get(t.id) ?? [],
|
|
46
76
|
}));
|
|
47
77
|
|
|
48
78
|
// Edge direction follows the dependency, not the SQL column order:
|