@skyf0xx/hedgehog 3.0.5 → 3.0.6

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 CHANGED
@@ -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.6",
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:
@@ -0,0 +1,89 @@
1
+ // Which tools each agent role needs, named as a host-neutral capability.
2
+ //
3
+ // Each host spells its tool grant in its own vocabulary, so the grant is
4
+ // stated once here and mapped per host below. This file owns the
5
+ // agent -> capability fact; `src/agents/*.md` carries the Claude Code
6
+ // spelling of it in its `tools:` frontmatter, which Claude Code reads
7
+ // directly.
8
+
9
+ /** @typedef {'full'|'no-bash'|'readonly-bash'|'readonly'|'write-only'} Capability */
10
+
11
+ /** @type {Record<string, Capability>} */
12
+ export const AGENT_CAPABILITY = {
13
+ // Build and scaffold: read, write, and run commands.
14
+ 'backend-eng': 'full',
15
+ bootstrap: 'full',
16
+ 'front-end-eng': 'full',
17
+ 'landing-builder': 'full',
18
+ 'landing-copywriter': 'full',
19
+ 'layer-eng': 'full',
20
+ planner: 'full',
21
+ tweaker: 'full',
22
+
23
+ // Author artifacts, but never run commands.
24
+ 'landing-headline-writer': 'no-bash',
25
+ 'landing-sequencer': 'no-bash',
26
+ 'landing-strategist': 'no-bash',
27
+ 'landing-systems': 'no-bash',
28
+
29
+ // Inspect and report; the verification command is theirs to run.
30
+ reviewer: 'readonly-bash',
31
+
32
+ // Inspect and report only.
33
+ 'landing-critic': 'readonly',
34
+
35
+ // Write its rationale artifact, nothing else.
36
+ 'ux-planner': 'write-only',
37
+ };
38
+
39
+ // What each capability permits, in host-neutral terms. Hosts that can't
40
+ // enforce a per-agent grant state these as constraints in the agent's
41
+ // prompt instead — `hedgehog verify` is what actually gates the commit,
42
+ // by checking touched files against the packet's ALLOWED SCOPE.
43
+ /** @type {Record<Capability, { read: boolean, write: boolean, run: boolean, summary: string }>} */
44
+ export const CAPABILITY_GRANT = {
45
+ full: {
46
+ read: true,
47
+ write: true,
48
+ run: true,
49
+ summary: 'Read, write, and edit files, and run shell commands.',
50
+ },
51
+ 'no-bash': {
52
+ read: true,
53
+ write: true,
54
+ run: false,
55
+ summary: 'Read, write, and edit files. Do not run shell commands.',
56
+ },
57
+ 'readonly-bash': {
58
+ read: true,
59
+ write: false,
60
+ run: true,
61
+ summary:
62
+ 'Read files and run shell commands. Do not write or edit any file — report findings as text.',
63
+ },
64
+ readonly: {
65
+ read: true,
66
+ write: false,
67
+ run: false,
68
+ summary:
69
+ 'Read files only. Do not write or edit any file, and do not run shell commands — report findings as text.',
70
+ },
71
+ 'write-only': {
72
+ read: true,
73
+ write: true,
74
+ run: false,
75
+ summary:
76
+ 'Read files, and write only this role\'s own artifact. Do not run shell commands.',
77
+ },
78
+ };
79
+
80
+ // The capability for one agent, by `name` frontmatter. Unknown agents get
81
+ // the most restrictive grant rather than the most permissive one, so a new
82
+ // agent added without a table entry fails closed.
83
+ export function capabilityFor(agentName) {
84
+ return AGENT_CAPABILITY[agentName] ?? 'readonly';
85
+ }
86
+
87
+ export function grantFor(agentName) {
88
+ return CAPABILITY_GRANT[capabilityFor(agentName)];
89
+ }
@@ -0,0 +1,9 @@
1
+ ## Delegating on this host
2
+
3
+ The agents in `.claude/agents/` are registered automatically — delegate to
4
+ one by name (`backend-eng`, `reviewer`, and so on) and it runs in its own
5
+ context with its own tool grant. The skills in `.claude/skills/` are
6
+ available the same way; invoke one by name rather than reimplementing what
7
+ it describes.
8
+
9
+ Clear context with `/clear` at the unit boundaries described above.
@@ -0,0 +1,16 @@
1
+ ## Delegating on this host
2
+
3
+ The agent files in `.cursor/agents/` are the roles this build uses. To run
4
+ one, read its file and follow it for that task, passing the `hedgehog
5
+ next` packet in full. Where a subagent is available, pass the file's
6
+ entire body as that subagent's prompt — the file is the role.
7
+
8
+ Each agent file opens with the tools that role may use. Honor it: Cursor
9
+ grants tools per session, so the constraint is yours to keep. `hedgehog
10
+ verify` checks the touched files against the packet's ALLOWED SCOPE and
11
+ gates the commit either way.
12
+
13
+ The skills in `.cursor/skills/` are procedures to follow — read the one
14
+ whose situation applies rather than improvising the steps.
15
+
16
+ Clear the conversation at the unit boundaries described above.
@@ -0,0 +1,20 @@
1
+ ---
2
+ description: Hedgehog build discipline — how to work in this repo
3
+ alwaysApply: true
4
+ ---
5
+
6
+ This project is built with Hedgehog: one small, tested, committed step at
7
+ a time. State lives in the build graph (`.hedgehog/hedgehog.db`), the
8
+ commit log, and the code.
9
+
10
+ Start every session with `hedgehog status`, then `hedgehog next` for the
11
+ task packet of one ready step. When the work is done, `hedgehog verify
12
+ <task-id>` checks the touched files against the packet's ALLOWED SCOPE,
13
+ runs the verification command, and on a pass writes the commit. A task
14
+ moves only on a passing `hedgehog verify`.
15
+
16
+ - `HEDGEHOG.md` — this project's own context and its core's build order.
17
+ - `AGENTS.md` — the index of every agent and skill, and when each applies.
18
+ - `.cursor/agents/` — the roles. Read the file and follow it for that
19
+ task, passing the packet in full.
20
+ - `.cursor/skills/` — procedures to follow when their situation applies.
@@ -0,0 +1,23 @@
1
+ // Rewrites a canonical agent file for a host that carries the role in a
2
+ // prompt rather than registering it with its own tool grant.
3
+ //
4
+ // `model:`, `color:`, and `tools:` are Claude Code's subagent schema.
5
+ // Elsewhere the role travels as text, so those keys come off and the tool
6
+ // grant is restated as a line the agent reads — the same fact in the form
7
+ // that host can act on. `hedgehog verify` gates the commit on ALLOWED
8
+ // SCOPE regardless.
9
+
10
+ import { parse, stringify } from './frontmatter.mjs';
11
+ import { grantFor } from './capabilities.mjs';
12
+
13
+ const CLAUDE_ONLY = ['model', 'color', 'tools'];
14
+
15
+ export function emitPromptAgent(text) {
16
+ const { data, body } = parse(text);
17
+ const kept = { ...data };
18
+ for (const key of CLAUDE_ONLY) delete kept[key];
19
+
20
+ const grant = grantFor(data.name);
21
+ const constraint = `**Tools for this role:** ${grant.summary}\n\n`;
22
+ return stringify(kept, `\n${constraint}${body.replace(/^\n+/, '')}`);
23
+ }
@@ -0,0 +1,48 @@
1
+ // Minimal YAML frontmatter reader/writer for the agent and skill files.
2
+ //
3
+ // The payload's frontmatter is a flat block of `key: value` scalars — no
4
+ // nesting, no lists, no anchors — so a full YAML parser would be a
5
+ // dependency bought for nothing. This handles exactly that shape and
6
+ // leaves the body untouched.
7
+
8
+ const FENCE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
9
+
10
+ /** Split a file into its frontmatter object and its body. */
11
+ export function parse(text) {
12
+ const m = text.match(FENCE);
13
+ if (!m) return { data: {}, body: text };
14
+
15
+ const data = {};
16
+ for (const line of m[1].split(/\r?\n/)) {
17
+ // `description:` values routinely contain colons, so split on the
18
+ // first one only.
19
+ const at = line.indexOf(':');
20
+ if (at === -1 || line.startsWith('#')) continue;
21
+ const key = line.slice(0, at).trim();
22
+ let value = line.slice(at + 1).trim();
23
+ if (
24
+ (value.startsWith('"') && value.endsWith('"')) ||
25
+ (value.startsWith("'") && value.endsWith("'"))
26
+ ) {
27
+ value = value.slice(1, -1);
28
+ }
29
+ if (key) data[key] = value;
30
+ }
31
+ return { data, body: text.slice(m[0].length) };
32
+ }
33
+
34
+ // Quote only when a value would otherwise change meaning as bare YAML.
35
+ // Descriptions commonly contain `: ` and `#`, both of which need it.
36
+ function scalar(value) {
37
+ const s = String(value);
38
+ const needsQuote = /:\s|^\s|\s$|^[[{>|*&!%@`'"]|#\s|\r|\n/.test(s) || s === '';
39
+ return needsQuote ? `"${s.replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"` : s;
40
+ }
41
+
42
+ /** Rebuild a file from a frontmatter object and a body, key order preserved. */
43
+ export function stringify(data, body) {
44
+ const lines = Object.entries(data)
45
+ .filter(([, v]) => v !== undefined && v !== null)
46
+ .map(([k, v]) => `${k}: ${scalar(v)}`);
47
+ return `---\n${lines.join('\n')}\n---\n${body}`;
48
+ }
@@ -0,0 +1,27 @@
1
+ ## Delegating on this host
2
+
3
+ `invoke_agent` dispatches to a generalist — the role travels in the
4
+ prompt. To delegate to an agent named in this file or in `AGENTS.md`:
5
+
6
+ 1. Read `.gemini/agents/<name>.md` in full.
7
+ 2. Call `invoke_agent` with `agent_name: "generalist"`, passing that
8
+ file's **entire body** as the prompt, followed by the `hedgehog next`
9
+ packet in full.
10
+
11
+ The file is the role — pass it whole rather than summarizing it, and pass
12
+ the packet rather than a step name.
13
+
14
+ Independent steps can go out as several `invoke_agent` calls in one
15
+ response. Steps that depend on each other stay sequential.
16
+
17
+ Each agent file opens with the tools that role may use. A dispatched
18
+ generalist keeps that constraint by reading it, and `hedgehog verify`
19
+ checks the touched files against the packet's ALLOWED SCOPE before any
20
+ commit lands.
21
+
22
+ The skills in `.gemini/skills/` are procedures to follow — read the one
23
+ whose situation applies rather than improvising the steps.
24
+
25
+ Clear the conversation at the unit boundaries described above.
26
+
27
+ @./AGENTS.md
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "hedgehog",
3
+ "version": "1.0.0",
4
+ "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
+ "contextFileName": "GEMINI.md"
6
+ }
@@ -0,0 +1,124 @@
1
+ // The host registry: one entry per coding agent Hedgehog installs into.
2
+ //
3
+ // A host describes where the discipline's payload lands in a consuming
4
+ // repo and, where the host's format differs from the canonical one, how
5
+ // to emit it. `src/agents/` and `src/skills/` are the single source of
6
+ // truth for content — a host adapts packaging, never substance.
7
+ //
8
+ // Adding a host means adding one entry here (and a matching directory
9
+ // under src/hosts/<name>/ for whatever templates it needs).
10
+
11
+ import { emitPromptAgent } from './emit.mjs';
12
+
13
+ // Hosts that dispatch real subagents get the full discipline: each build
14
+ // step runs in its own context with its own role. Hosts that don't are
15
+ // still supported through the AGENTS.md routing doc, which indexes the
16
+ // same agent and skill files for an agent to read inline. Either way
17
+ // `hedgehog verify` is what gates a commit, so correctness does not
18
+ // depend on the host's tool grants.
19
+
20
+ /**
21
+ * @typedef {object} Host
22
+ * @property {string} agentsDir where agent definitions land
23
+ * @property {string} skillsDir where skill directories land
24
+ * @property {string} bootstrapFile the project file this host auto-loads
25
+ * @property {(body: string, meta: object) => string} [emitAgent]
26
+ * rewrites one agent file for this host. Omitted when the canonical
27
+ * file is already in the host's format — the installer then copies it
28
+ * verbatim, which is what keeps Claude Code output byte-identical.
29
+ * @property {Array<object>} [extraEntries] extra payload entries for this host
30
+ */
31
+
32
+ // AGENTS.md is the convention a growing number of coding agents read
33
+ // from a repo root. Every host ships it: it costs one generated file and
34
+ // it means a second agent opening the project finds the discipline
35
+ // without a second install.
36
+ const routingDoc = (host) => ({
37
+ type: 'generated',
38
+ to: 'AGENTS.md',
39
+ // Indexes every host the project is set up for, not just the one being
40
+ // installed, so adding a second host doesn't strand the first one's
41
+ // paths.
42
+ generate: async ({ pkgRoot, projectRoot }) => {
43
+ const [{ renderAgentsMd }, { installedHosts }] = await Promise.all([
44
+ import('./routing.mjs'),
45
+ import('./installed.mjs'),
46
+ ]);
47
+ const recorded = projectRoot ? await installedHosts(projectRoot) : [];
48
+ const names = [...new Set([host, ...recorded])];
49
+ return renderAgentsMd({ pkgRoot, hosts: names.map((n) => HOSTS[n]) });
50
+ },
51
+ });
52
+
53
+ /** @type {Record<string, Host>} */
54
+ export const HOSTS = {
55
+ // Claude Code reads src/agents/*.md and src/skills/ * /SKILL.md as
56
+ // authored — frontmatter and all — so it has no emitter. This is the
57
+ // canonical format every other host is adapted from.
58
+ claude: {
59
+ agentsDir: '.claude/agents',
60
+ skillsDir: '.claude/skills',
61
+ bootstrapFile: 'CLAUDE.md',
62
+ label: 'Claude Code',
63
+ get extraEntries() {
64
+ return [routingDoc('claude')];
65
+ },
66
+ },
67
+
68
+ // Cursor loads `.cursor/rules/*.mdc` on every request and reads
69
+ // AGENTS.md at the repo root. The rule points at both, the project's
70
+ // own context lives in HEDGEHOG.md, and the agent files carry their
71
+ // tool grant as a line of the prompt.
72
+ cursor: {
73
+ agentsDir: '.cursor/agents',
74
+ skillsDir: '.cursor/skills',
75
+ bootstrapFile: 'HEDGEHOG.md',
76
+ label: 'Cursor',
77
+ emitAgent: emitPromptAgent,
78
+ get extraEntries() {
79
+ return [
80
+ routingDoc('cursor'),
81
+ {
82
+ type: 'file',
83
+ from: 'src/hosts/cursor/hedgehog.mdc',
84
+ to: '.cursor/rules/hedgehog.mdc',
85
+ },
86
+ ];
87
+ },
88
+ },
89
+
90
+ // Gemini CLI loads GEMINI.md at session start and resolves its `@./`
91
+ // includes, so the routing doc comes in with it. `invoke_agent`
92
+ // dispatches an untyped generalist, so each agent file carries its
93
+ // tool grant as a line of the prompt it will be passed as.
94
+ gemini: {
95
+ agentsDir: '.gemini/agents',
96
+ skillsDir: '.gemini/skills',
97
+ bootstrapFile: 'GEMINI.md',
98
+ label: 'Gemini CLI',
99
+ emitAgent: emitPromptAgent,
100
+ get extraEntries() {
101
+ return [
102
+ routingDoc('gemini'),
103
+ {
104
+ type: 'file',
105
+ from: 'src/hosts/gemini/gemini-extension.json',
106
+ to: 'gemini-extension.json',
107
+ },
108
+ ];
109
+ },
110
+ },
111
+ };
112
+
113
+ // One install flag per host, named for the tool a user is asking to use.
114
+ // Mirrors CORE_FLAGS in bin/cli.mjs: the public flag and the internal
115
+ // name are allowed to diverge so the CLI surface can stay stable.
116
+ export const HOST_FLAGS = {
117
+ '--claude': 'claude',
118
+ '--cursor': 'cursor',
119
+ '--gemini': 'gemini',
120
+ };
121
+
122
+ export const DEFAULT_HOST = 'claude';
123
+
124
+ export const availableHosts = () => Object.keys(HOSTS).sort();
@@ -0,0 +1,58 @@
1
+ // Which hosts a project has Hedgehog installed for.
2
+ //
3
+ // `init` records them so `update` refreshes every host the project
4
+ // actually uses without being told again. A project installed before
5
+ // this file existed is read off the filesystem instead, by looking for
6
+ // the directories each host's payload lands in.
7
+
8
+ import { readFile, writeFile, mkdir, access } from 'node:fs/promises';
9
+ import { constants } from 'node:fs';
10
+ import { dirname, join } from 'node:path';
11
+ import { HOSTS, DEFAULT_HOST } from './index.mjs';
12
+
13
+ const HOSTS_PATH = '.hedgehog/hosts.json';
14
+
15
+ const exists = (p) =>
16
+ access(p, constants.F_OK).then(
17
+ () => true,
18
+ () => false,
19
+ );
20
+
21
+ export async function recordHosts(root, hosts) {
22
+ const path = join(root, HOSTS_PATH);
23
+ const known = new Set(await readRecorded(root));
24
+ for (const h of hosts) known.add(h);
25
+ const merged = [...known].sort();
26
+ await mkdir(dirname(path), { recursive: true });
27
+ await writeFile(path, `${JSON.stringify({ hosts: merged }, null, 2)}\n`);
28
+ return merged;
29
+ }
30
+
31
+ async function readRecorded(root) {
32
+ try {
33
+ const { hosts } = JSON.parse(await readFile(join(root, HOSTS_PATH), 'utf8'));
34
+ return Array.isArray(hosts) ? hosts.filter((h) => h in HOSTS) : [];
35
+ } catch {
36
+ return [];
37
+ }
38
+ }
39
+
40
+ // A host counts as installed when its agents directory is on disk.
41
+ async function detectHosts(root) {
42
+ const found = [];
43
+ for (const [name, h] of Object.entries(HOSTS)) {
44
+ if (await exists(join(root, h.agentsDir))) found.push(name);
45
+ }
46
+ return found;
47
+ }
48
+
49
+ /**
50
+ * The hosts `update` should refresh: what `init` recorded, else what's on
51
+ * disk, else the default.
52
+ */
53
+ export async function installedHosts(root) {
54
+ const recorded = await readRecorded(root);
55
+ if (recorded.length) return recorded;
56
+ const detected = await detectHosts(root);
57
+ return detected.length ? detected : [DEFAULT_HOST];
58
+ }
@@ -0,0 +1,144 @@
1
+ // Generates AGENTS.md — the routing doc for coding agents that read a
2
+ // root instructions file but don't register Hedgehog's agents themselves.
3
+ //
4
+ // It is an *index*: every row points at the file that owns the
5
+ // substance. The tables are built from each agent's and skill's own
6
+ // `description` frontmatter, so a new agent or a reworded description
7
+ // shows up here without this file being edited.
8
+
9
+ import { readdir, readFile } from 'node:fs/promises';
10
+ import { join } from 'node:path';
11
+ import { parse } from './frontmatter.mjs';
12
+ import { capabilityFor, CAPABILITY_GRANT } from './capabilities.mjs';
13
+
14
+ // A description is a full paragraph aimed at a dispatcher deciding
15
+ // whether to invoke this role. The table wants the trigger, so take the
16
+ // first sentence and let the file itself carry the rest.
17
+ function firstSentence(text = '') {
18
+ const flat = text.replace(/\s+/g, ' ').trim();
19
+ const end = flat.search(/\.\s|\.$/);
20
+ const out = end === -1 ? flat : flat.slice(0, end + 1);
21
+ return out.replaceAll('|', '\\|');
22
+ }
23
+
24
+ async function readEntries(dir, filename) {
25
+ const names = (await readdir(dir, { withFileTypes: true }))
26
+ .filter((e) => (filename ? e.isDirectory() : e.isFile() && e.name.endsWith('.md')))
27
+ .map((e) => e.name)
28
+ .sort();
29
+
30
+ const out = [];
31
+ for (const name of names) {
32
+ const path = filename ? join(dir, name, filename) : join(dir, name);
33
+ const { data } = parse(await readFile(path, 'utf8'));
34
+ if (!data.name) continue;
35
+ out.push({ ...data, file: filename ? `${name}/${filename}` : name });
36
+ }
37
+ return out;
38
+ }
39
+
40
+ export async function renderAgentsMd({ pkgRoot, hosts }) {
41
+ const agents = await readEntries(join(pkgRoot, 'src/agents'));
42
+ const skills = await readEntries(join(pkgRoot, 'src/skills'), 'SKILL.md');
43
+
44
+ // One project can be set up for several coding agents at once. Paths
45
+ // are quoted per host so whichever one is reading finds its own copy.
46
+ const dirs = (pick) =>
47
+ [...new Set(hosts.map(pick))].map((d) => `\`${d}\``).join(' or ');
48
+
49
+ const agentRows = agents
50
+ .map((a) => {
51
+ const grant = CAPABILITY_GRANT[capabilityFor(a.name)];
52
+ return `| \`${a.name}\` | ${firstSentence(a.description)} | ${grant.summary} | \`${a.file}\` |`;
53
+ })
54
+ .join('\n');
55
+
56
+ const skillRows = skills
57
+ .map((s) => `| \`${s.name}\` | ${firstSentence(s.description)} | \`${s.file}\` |`)
58
+ .join('\n');
59
+
60
+ return `# Working in this repo
61
+
62
+ This project is built with **Hedgehog**, a one-step-at-a-time build
63
+ discipline. The rules below aren't preferences — they're how the build
64
+ stays mechanically correct.
65
+
66
+ **State lives in the build graph (\`.hedgehog/hedgehog.db\`), the commit
67
+ log, and the code.** A fresh session loses nothing: run \`hedgehog status\`
68
+ and read the commit log to recover.
69
+
70
+ ## Start here
71
+
72
+ \`\`\`bash
73
+ hedgehog status # what's built, what's ready, what's blocked
74
+ hedgehog next # the task packet for one ready step
75
+ \`\`\`
76
+
77
+ Agent files live in ${dirs((h) => h.agentsDir)}, and skills in
78
+ ${dirs((h) => h.skillsDir)}. The tables below name each file relative to
79
+ those directories.
80
+
81
+ If this project's instructions file still has unfilled \`{{PLACEHOLDER}}\`
82
+ text, nothing has been built yet — read \`planner.md\` and run planning
83
+ intake first.
84
+
85
+ ## The loop
86
+
87
+ 1. \`hedgehog next\` emits one task packet — STATUS, WHY NOW, BLOCKED
88
+ DOWNSTREAM, ALLOWED SCOPE, VERIFICATION. Trust it: a task is never
89
+ emitted unless every dependency is \`complete\`.
90
+ 2. Delegate the **full packet** to the agent that owns that layer (see
91
+ the table below). Don't summarize it, and don't pass just a step name.
92
+ 3. When the work is done, run \`hedgehog verify <task-id>\`. It checks the
93
+ touched files against ALLOWED SCOPE, runs the verification command,
94
+ and on a pass writes the commit and unlocks what the task blocked.
95
+
96
+ **An agent reporting success never moves a task — only a passing
97
+ \`hedgehog verify\` does.** This is the enforcement, and it holds no
98
+ matter which coding agent you are or what tools you were granted.
99
+
100
+ ## Delegating
101
+
102
+ If your harness dispatches subagents, read the agent's file and pass its
103
+ **entire body** as that subagent's prompt, followed by the task packet.
104
+ The file is the role — don't summarize it.
105
+
106
+ If your harness has no subagent mechanism, read the agent's file and
107
+ follow it yourself in this thread, then clear the conversation at the
108
+ next unit boundary (a module's Phase A, a landing page section) and
109
+ recover with \`hedgehog status\`.
110
+
111
+ Either way, honor the "May" column below. Where a harness can't enforce a
112
+ tool grant, the constraint is yours to keep — and \`hedgehog verify\` still
113
+ gates the commit on ALLOWED SCOPE.
114
+
115
+ ## Agents
116
+
117
+ | Agent | Use when | May | File |
118
+ | --- | --- | --- | --- |
119
+ ${agentRows}
120
+
121
+ ## Skills
122
+
123
+ Procedures to follow, not to improvise around. Read the file when its
124
+ situation applies.
125
+
126
+ | Skill | Use when | File |
127
+ | --- | --- | --- |
128
+ ${skillRows}
129
+
130
+ ## CLI reference
131
+
132
+ | Command | Does |
133
+ | --- | --- |
134
+ | \`hedgehog status\` | Every task, its state, and what it blocks |
135
+ | \`hedgehog next\` | The task packet for one ready task |
136
+ | \`hedgehog verify <task-id>\` | Gate a task: scope check, verification command, commit |
137
+ | \`hedgehog why <path>\` | Which task and layer a file belongs to |
138
+ | \`hedgehog plan\` | Compile intents into the task graph |
139
+ | \`hedgehog intent add\` | Record an intent at planning intake |
140
+ | \`hedgehog friction add "<note>"\` | Log build friction for later review |
141
+ | \`hedgehog friction list\` | Read the friction log |
142
+ | \`hedgehog graph\` | Live read-only diagram of the build graph |
143
+ `;
144
+ }
@@ -88,8 +88,8 @@ natively-installed Postgres. See **Local infra: Docker, always** below.
88
88
 
89
89
  `hedgehog init --ts-full-stack-app` copies `src/golden-cores/full-stack-app/`
90
90
  to the repo root at install time, the same way it copies `src/agents` to
91
- `.claude/agents` — check whether the core files are already present
92
- (same check as step 1) before copying again. On a project that ran
91
+ this host's own agents directory — check whether the core files are
92
+ already present (same check as step 1) before copying again. On a project that ran
93
93
  plain `init` (no core flag) and only reaches `full-stack-app` because
94
94
  `planner` picked it at Phase 0, this hasn't happened yet: copy
95
95
  `src/golden-cores/full-stack-app/`'s contents to the repo root now. Also
@@ -61,8 +61,8 @@ re-copy: patch the specific file at its source.
61
61
 
62
62
  `hedgehog init --landing-page` copies `src/golden-cores/landing-page/` to
63
63
  the repo root at install time, the same way it copies `src/agents` to
64
- `.claude/agents` — check whether the core files are already present
65
- (same check as step 1) before copying again. On a project that ran plain
64
+ this host's own agents directory — check whether the core files are
65
+ already present (same check as step 1) before copying again. On a project that ran plain
66
66
  `init` (no core flag) and only reaches `landing-page` because `planner`
67
67
  picked it at Phase 0, this hasn't happened yet: copy
68
68
  `src/golden-cores/landing-page/`'s contents to the repo root now. Also
@@ -105,10 +105,10 @@ context small:
105
105
 
106
106
  - **Clear context at natural boundaries** — a module's Phase A, a
107
107
  landing page section, whatever this core's own unit boundary is — once
108
- that unit is done and committed. `/clear` and start fresh, then run
109
- `hedgehog status`/`hedgehog next` and continue. Nothing is lost,
110
- because the build graph, commits, and code hold all the state. Prefer
111
- this over letting one session accumulate the entire project.
108
+ that unit is done and committed. Clear the conversation and start
109
+ fresh, then run `hedgehog status`/`hedgehog next` and continue. Nothing
110
+ is lost, because the build graph, commits, and code hold all the state.
111
+ Prefer this over letting one session accumulate the entire project.
112
112
  - **A cleared or new session recovers by running `hedgehog status` and
113
113
  reading the commit log**, never by needing the prior conversation.
114
114
  - **Delegate heavy work to agents.** Planning intake, scaffolding, and
@@ -119,3 +119,5 @@ context small:
119
119
  file's core section, not something to reconstruct. If you need a
120
120
  project specific, read it from the code. That's the self-documenting
121
121
  design working as intended.
122
+
123
+ {{HOST_DISPATCH}}
@@ -75,6 +75,31 @@
75
75
  border-radius: 6px;
76
76
  font-size: 12px;
77
77
  }
78
+ #detail-panel .muted { color: #8b949e; font-size: 13px; }
79
+ #detail-panel ul.requirements {
80
+ margin: 4px 0 0;
81
+ padding-left: 0;
82
+ list-style: none;
83
+ }
84
+ #detail-panel ul.requirements li {
85
+ font-size: 13px;
86
+ padding: 6px 0;
87
+ border-bottom: 1px solid #21262d;
88
+ }
89
+ #detail-panel ul.requirements li:last-child { border-bottom: none; }
90
+ .req-kind {
91
+ display: inline-block;
92
+ font-size: 10px;
93
+ font-weight: 600;
94
+ text-transform: uppercase;
95
+ letter-spacing: 0.03em;
96
+ padding: 1px 6px;
97
+ border-radius: 999px;
98
+ margin-right: 4px;
99
+ }
100
+ .req-kind-rule { background: #58a6ff33; color: #79c0ff; }
101
+ .req-kind-constraint { background: #d2992233; color: #e3b341; }
102
+ .req-kind-acceptance { background: #3fb95033; color: #56d364; }
78
103
  #legend {
79
104
  position: fixed;
80
105
  bottom: 16px;
@@ -173,6 +198,7 @@
173
198
 
174
199
  function DetailPanel({ task }) {
175
200
  if (!task) return h('div', { id: 'detail-panel' });
201
+ const requirements = task.requirements || [];
176
202
  return h(
177
203
  'div',
178
204
  { id: 'detail-panel', className: 'open' },
@@ -181,6 +207,26 @@
181
207
  h('div', null, task.intentGoal),
182
208
  h('div', { className: 'field-label' }, 'Objective'),
183
209
  h('div', null, task.objective),
210
+ // Every requirement kind together, same as hedgehog next's
211
+ // RELEVANT RULES — a task is bound by its rules, constraints, and
212
+ // acceptance criteria equally, so they read as one list rather
213
+ // than three separate sections.
214
+ h('div', { className: 'field-label' }, 'Requirements'),
215
+ requirements.length === 0
216
+ ? h('div', { className: 'muted' }, 'None recorded')
217
+ : h(
218
+ 'ul',
219
+ { className: 'requirements' },
220
+ requirements.map((r, i) =>
221
+ h(
222
+ 'li',
223
+ { key: i },
224
+ h('span', { className: `req-kind req-kind-${r.kind}` }, r.kind),
225
+ ' ',
226
+ r.statement,
227
+ ),
228
+ ),
229
+ ),
184
230
  h('div', { className: 'field-label' }, 'Verify command'),
185
231
  h('pre', null, task.verifyCommand),
186
232
  h('div', { className: 'field-label' }, 'Commit message'),