@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.
Files changed (29) hide show
  1. package/README.md +31 -16
  2. package/bin/cli.mjs +195 -71
  3. package/package.json +6 -2
  4. package/src/db/graph.mjs +32 -2
  5. package/src/hosts/capabilities.mjs +89 -0
  6. package/src/hosts/claude/DISPATCH.md +9 -0
  7. package/src/hosts/cursor/DISPATCH.md +16 -0
  8. package/src/hosts/cursor/hedgehog.mdc +20 -0
  9. package/src/hosts/emit.mjs +23 -0
  10. package/src/hosts/frontmatter.mjs +48 -0
  11. package/src/hosts/gemini/DISPATCH.md +27 -0
  12. package/src/hosts/gemini/gemini-extension.json +6 -0
  13. package/src/hosts/index.mjs +124 -0
  14. package/src/hosts/installed.mjs +58 -0
  15. package/src/hosts/routing.mjs +144 -0
  16. package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +2 -2
  17. package/src/skills/hedgehog-bootstrap-landing-page-core/SKILL.md +2 -2
  18. package/src/skills/hedgehog-core-design/SKILL.md +80 -15
  19. package/src/skills/hedgehog-core-design/blueprints/bot-agent.md +35 -0
  20. package/src/skills/hedgehog-core-design/blueprints/browser-extension.md +26 -0
  21. package/src/skills/hedgehog-core-design/blueprints/cli.md +26 -0
  22. package/src/skills/hedgehog-core-design/blueprints/compiler-language-tool.md +36 -0
  23. package/src/skills/hedgehog-core-design/blueprints/data-pipeline.md +36 -0
  24. package/src/skills/hedgehog-core-design/blueprints/desktop-app.md +34 -0
  25. package/src/skills/hedgehog-core-design/blueprints/game.md +35 -0
  26. package/src/skills/hedgehog-core-design/blueprints/infra-deploy-tool.md +34 -0
  27. package/src/skills/hedgehog-core-design/blueprints/library-sdk.md +31 -0
  28. package/src/templates/CLAUDE.md +6 -4
  29. 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, ask Claude to run:
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 Claude Code and describe what you want to build.
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 `.claude/agents/` and `.claude/skills/` only. It never
132
- touches `CLAUDE.md`, the build graph, the core workspace, or
133
- `skills/BMAD`, since those carry project-specific or write-once content.
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 — one node per task, coloured by lifecycle
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 .claude/agents + .claude/skills
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
- function plan(core) {
88
- const base = [
89
- { type: 'dir', from: 'src/agents', to: '.claude/agents' },
90
- { type: 'dir', from: 'src/skills', to: '.claude/skills' },
91
- // The vendored BMAD-METHOD planning shelf that hedgehog-planning-intake
92
- // runs referenced by repo-root-relative path (skills/BMAD/...), so it
93
- // lands there rather than under .claude/.
94
- { type: 'dir', from: 'skills/BMAD', to: 'skills/BMAD' },
95
- // The vendored GSAP animation skill shelf that front-end-eng loads for
96
- // motion work same repo-root-relative referencing as skills/BMAD.
97
- { type: 'dir', from: 'skills/GSAP', to: 'skills/GSAP' },
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
- { type: 'file', from: 'src/templates/CLAUDE.md', to: 'CLAUDE.md' },
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
- to: 'CLAUDE.md',
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 own
130
- // .claude/agents and .claude/skills are supposed to match upstream
131
- // verbatim. CLAUDE.md carries project-filled content, the build graph
132
- // and core workspace are verified once by their own init/bootstrap-core
133
- // steps, and skills/BMAD and skills/GSAP are re-vendored only
134
- // deliberately (a manual re-vendor, per each shelf's ATTRIBUTION.md) —
135
- // none of those belong in an update.
136
- const UPDATE_PLAN = [
137
- { type: 'dir', from: 'src/agents', to: '.claude/agents' },
138
- { type: 'dir', from: 'src/skills', to: '.claude/skills' },
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
- const shell = await readFile(join(PKG_ROOT, f.merge.shell), 'utf8');
154
- const section = await readFile(join(PKG_ROOT, f.merge.include), 'utf8');
155
- await writeFile(f.dest, shell.replaceAll('{{CORE_SECTION}}', section.trimEnd()));
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 ${bold('.claude/')}, drops the
191
- CLAUDE.md template and an empty build graph (${bold('.hedgehog/hedgehog.db')})
192
- into the repo root, so the discipline is committed alongside your code.
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 .claude/agents + .claude/skills
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 Claude Code, and describe what
218
- you want to build — the planner agent runs planning intake, then hands
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 only .claude/agents and .claude/skills from the
230
- installed Hedgehog version, so an already-bootstrapped project can pick up
231
- agent/skill changes from a newer release. It always overwrites those two
232
- directories and never touches CLAUDE.md, the build graph, the core
233
- workspace, or skills/BMAD and skills/GSAP those are project-specific or
234
- updated deliberately, not by this command.
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 Claude Code and describe what you want to build.`);
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 Claude Code and describe what you want to build.`);
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
- // Full replace, not a merge: clear each destination dir first so a
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 entry of UPDATE_PLAN) {
346
- const files = await plannedFiles(entry);
347
- for (const f of files) {
348
- await mkdir(dirname(f.dest), { recursive: true });
349
- await cp(f.src, f.dest);
350
- written++;
351
- console.log(` ${green('update')} ${relative(DEST_ROOT, f.dest)}`);
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(`${written} files written`)}\n`,
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
- console.log(` 1. ${bold('git diff .claude/')} to review what changed`);
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
- 'CLAUDE.md, the build graph, the core workspace, and skills/BMAD\n' +
364
- 'and skills/GSAP are untouched — those carry project-specific or\n' +
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
- await init({ force, core, explicitCore: Boolean(coreFlag) });
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.5",
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 fields that drive
32
- // layout and status colour — nothing the viewer doesn't render.
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: