@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 +30 -15
- 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/templates/CLAUDE.md +6 -4
- package/src/templates/graph.html +46 -0
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,
|
|
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.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
|
|
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:
|
|
@@ -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,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
|
-
|
|
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
|
-
|
|
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
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -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.
|
|
109
|
-
`hedgehog status`/`hedgehog next` and continue. Nothing
|
|
110
|
-
because the build graph, commits, and code hold all the state.
|
|
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}}
|
package/src/templates/graph.html
CHANGED
|
@@ -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'),
|