@skyf0xx/hedgehog 2.0.13 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +9 -3
  2. package/bin/cli.mjs +463 -19
  3. package/package.json +3 -2
  4. package/src/agents/backend-eng.md +56 -45
  5. package/src/agents/bootstrap.md +67 -73
  6. package/src/agents/front-end-eng.md +31 -18
  7. package/src/agents/planner.md +163 -84
  8. package/src/agents/reviewer.md +4 -4
  9. package/src/agents/tweaker.md +138 -106
  10. package/src/db/core.mjs +141 -0
  11. package/src/db/friction.mjs +25 -0
  12. package/src/db/init.mjs +35 -0
  13. package/src/db/intent.mjs +101 -0
  14. package/src/db/next.mjs +179 -0
  15. package/src/db/plan.mjs +222 -0
  16. package/src/db/schema.mjs +95 -0
  17. package/src/db/status.mjs +113 -0
  18. package/src/db/verify.mjs +286 -0
  19. package/src/db/why.mjs +97 -0
  20. package/src/golden-cores/full-stack-app/core.yaml +41 -0
  21. package/src/golden-cores/landing-page/core.yaml +41 -0
  22. package/src/skills/conventional-commits/SKILL.md +1 -1
  23. package/src/skills/hedgehog-bootstrap/SKILL.md +38 -41
  24. package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +8 -12
  25. package/src/skills/hedgehog-bootstrap-landing-page-core/SKILL.md +7 -9
  26. package/src/skills/hedgehog-core-design/SKILL.md +239 -0
  27. package/src/skills/hedgehog-landing-loop/SKILL.md +91 -57
  28. package/src/skills/hedgehog-loop/SKILL.md +109 -77
  29. package/src/skills/hedgehog-planning-intake/SKILL.md +72 -97
  30. package/src/templates/CLAUDE.core.full-stack-app.md +31 -24
  31. package/src/templates/CLAUDE.core.landing-page.md +11 -7
  32. package/src/templates/CLAUDE.md +46 -38
  33. package/src/templates/TODO.core.full-stack-app.md +0 -51
  34. package/src/templates/TODO.core.landing-page.md +0 -31
  35. package/src/templates/TODO.md +0 -12
package/README.md CHANGED
@@ -21,7 +21,7 @@ The codebase carries the context, not the model.
21
21
  Hedgehog combines:
22
22
 
23
23
  - **BMAD for planning** — turn an idea into a clear brief, requirements, and architecture
24
- - **An opinionated stack** — remove unnecessary technical decisions
24
+ - **An opinionated stack** — remove unnecessary technical decisions, and settle the necessary ones once
25
25
  - **TDD and progressive layering** — build one tested layer at a time
26
26
  - **Mechanical enforcement** — use tooling and phase gates instead of trusting the AI to follow instructions
27
27
  - **Small context loops** — keep every change focused, verifiable, and easy to review
@@ -86,6 +86,12 @@ Sequence
86
86
  Artifact
87
87
  ```
88
88
 
89
+ ### Anything else
90
+
91
+ A CLI, a library, a data pipeline, a compiler, etc. a project fitting neither
92
+ shape gets its own build order, designed from your planning documents at
93
+ intake rather than chosen from a menu.
94
+
89
95
  ![Why Hedgehog works: a different way to build with AI, comparing traditional AI workflow to Hedgehog](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/why.png)
90
96
 
91
97
  ## Install
@@ -109,7 +115,7 @@ npx @skyf0xx/hedgehog update
109
115
  ```
110
116
 
111
117
  This refreshes `.claude/agents/` and `.claude/skills/` only. It never
112
- touches `CLAUDE.md`, `TODO.md`, the core workspace, or
118
+ touches `CLAUDE.md`, the build graph, the core workspace, or
113
119
  `skills/BMAD`, since those carry project-specific or write-once content.
114
120
 
115
121
  ## Why Hedgehog
@@ -121,7 +127,7 @@ Hedgehog improves the **system AI builds inside**.
121
127
  | | Raw AI | BMAD | Hedgehog |
122
128
  | --- | --- | --- | --- |
123
129
  | **Planning** | Conversation | Multi-agent workflow | BMAD |
124
- | **Architecture** | AI decides | Documented | Opinionated and enforced |
130
+ | **Architecture** | AI decides, drifts | Documented | Decided once, then enforced |
125
131
  | **Build order** | Improvised | Guided by docs | Mechanically enforced |
126
132
  | **Context** | Held in the prompt | Large planning documents | Encoded in the codebase |
127
133
  | **Verification** | Optional | Process-dependent | Tests and phase gates |
package/bin/cli.mjs CHANGED
@@ -11,8 +11,20 @@
11
11
 
12
12
  import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile } from 'node:fs/promises';
13
13
  import { constants } from 'node:fs';
14
+ import { DatabaseSync } from 'node:sqlite';
14
15
  import { fileURLToPath } from 'node:url';
15
16
  import { dirname, join, relative, resolve } from 'node:path';
17
+ import { dbInit, DB_PATH } from '../src/db/init.mjs';
18
+ import { loadCore } from '../src/db/core.mjs';
19
+ import { planTasks } from '../src/db/plan.mjs';
20
+ import { addIntent } from '../src/db/intent.mjs';
21
+ import { nextTask, formatNext, stalledTasks } from '../src/db/next.mjs';
22
+ import { verifyTask } from '../src/db/verify.mjs';
23
+ import { graphStatus, formatStatus } from '../src/db/status.mjs';
24
+ import { whyPath, formatWhy } from '../src/db/why.mjs';
25
+ import { addFriction, listFriction } from '../src/db/friction.mjs';
26
+
27
+ const AUTHORED_CORE_PATH = '.hedgehog/core.yaml';
16
28
 
17
29
  const __dirname = dirname(fileURLToPath(import.meta.url));
18
30
  const PKG_ROOT = resolve(__dirname, '..');
@@ -79,12 +91,6 @@ function plan(core) {
79
91
  include: `src/templates/CLAUDE.core.${core}.md`,
80
92
  to: 'CLAUDE.md',
81
93
  },
82
- {
83
- type: 'merge',
84
- shell: 'src/templates/TODO.md',
85
- include: `src/templates/TODO.core.${core}.md`,
86
- to: 'TODO.md',
87
- },
88
94
  // The pre-built, pre-verified workspace for the chosen core —
89
95
  // everything a fresh project of that shape needs at repo root
90
96
  // (lands the root package.json too, so there's no separate
@@ -98,11 +104,11 @@ function plan(core) {
98
104
  // project-specific or write-once content: `update` re-copies exactly
99
105
  // this, always overwriting, since a consuming project's own
100
106
  // .claude/agents and .claude/skills are supposed to match upstream
101
- // verbatim. CLAUDE.md/TODO.md carry project-filled content, the core
102
- // workspace is verified once by its bootstrap-core skill, and
103
- // skills/BMAD and skills/GSAP are re-vendored only deliberately (a
104
- // manual re-vendor, per each shelf's ATTRIBUTION.md) — none of those
105
- // belong in an update.
107
+ // verbatim. CLAUDE.md carries project-filled content, the build graph
108
+ // and core workspace are verified once by their own init/bootstrap-core
109
+ // steps, and skills/BMAD and skills/GSAP are re-vendored only
110
+ // deliberately (a manual re-vendor, per each shelf's ATTRIBUTION.md) —
111
+ // none of those belong in an update.
106
112
  const UPDATE_PLAN = [
107
113
  { type: 'dir', from: 'src/agents', to: '.claude/agents' },
108
114
  { type: 'dir', from: 'src/skills', to: '.claude/skills' },
@@ -157,9 +163,9 @@ async function help() {
157
163
  console.log(`
158
164
  ${bold('Hedgehog installer')}
159
165
 
160
- Copies the Hedgehog agents and skills into ${bold('.claude/')} and drops the
161
- CLAUDE.md / TODO.md templates into the repo root, so the discipline is
162
- committed alongside your code.
166
+ Copies the Hedgehog agents and skills into ${bold('.claude/')}, drops the
167
+ CLAUDE.md template and an empty build graph (${bold('.hedgehog/hedgehog.db')})
168
+ into the repo root, so the discipline is committed alongside your code.
163
169
 
164
170
  ${bold('Usage')}
165
171
  npx @skyf0xx/hedgehog init scaffold, ${DEFAULT_CORE} core (default)
@@ -167,6 +173,16 @@ ${bold('Usage')}
167
173
  npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core instead
168
174
  npx @skyf0xx/hedgehog init --force overwrite existing files
169
175
  npx @skyf0xx/hedgehog update refresh .claude/agents + .claude/skills
176
+ npx @skyf0xx/hedgehog db init create .hedgehog/hedgehog.db if absent
177
+ npx @skyf0xx/hedgehog plan compile pending intents into tasks + dependencies
178
+ npx @skyf0xx/hedgehog intent add [flags] add an intent (rules/requirements/dependencies)
179
+ npx @skyf0xx/hedgehog intent add --file <path> add an intent from a JSON file
180
+ npx @skyf0xx/hedgehog next print the task packet for one ready task
181
+ npx @skyf0xx/hedgehog verify <task-id> run scope + verify checks, commit on pass
182
+ npx @skyf0xx/hedgehog status graph overview: counts by status, ready list
183
+ npx @skyf0xx/hedgehog why <path> provenance chain for a file
184
+ npx @skyf0xx/hedgehog friction add "<note>" log a friction note [--task <task-id>]
185
+ npx @skyf0xx/hedgehog friction list list logged friction, oldest first
170
186
  npx @skyf0xx/hedgehog --help
171
187
 
172
188
  Available cores: ${cores.join(', ')}
@@ -178,9 +194,9 @@ off to bootstrap.
178
194
  ${bold('update')} re-copies only .claude/agents and .claude/skills from the
179
195
  installed Hedgehog version, so an already-bootstrapped project can pick up
180
196
  agent/skill changes from a newer release. It always overwrites those two
181
- directories and never touches CLAUDE.md, TODO.md, the core workspace, or
182
- skills/BMAD and skills/GSAP — those are project-specific or updated
183
- deliberately, not by this command.
197
+ directories and never touches CLAUDE.md, the build graph, the core
198
+ workspace, or skills/BMAD and skills/GSAP — those are project-specific or
199
+ updated deliberately, not by this command.
184
200
  `);
185
201
  }
186
202
 
@@ -234,6 +250,10 @@ async function init({ force, core }) {
234
250
  }
235
251
  }
236
252
 
253
+ const { created: dbCreated, path: dbPath } = await dbInit(DB_PATH);
254
+ console.log(` ${dbCreated ? green('create') : dim('exists')} ${dbPath}`);
255
+ if (dbCreated) written++;
256
+
237
257
  console.log(
238
258
  `\n${green(bold('Hedgehog installed.'))} ${dim(
239
259
  `${written} created${overwritten ? `, ${overwritten} overwritten` : ''}`,
@@ -287,13 +307,397 @@ async function update() {
287
307
  console.log(` 2. ${bold('git add -A && git commit -m "chore: update hedgehog"')}\n`);
288
308
  console.log(
289
309
  dim(
290
- 'CLAUDE.md, TODO.md, the core workspace, and skills/BMAD and\n' +
291
- 'skills/GSAP are untouched — those carry project-specific or\n' +
310
+ 'CLAUDE.md, the build graph, the core workspace, and skills/BMAD\n' +
311
+ 'and skills/GSAP are untouched — those carry project-specific or\n' +
292
312
  'write-once content.',
293
313
  ),
294
314
  );
295
315
  }
296
316
 
317
+ async function dbCommand(args) {
318
+ const sub = args[0];
319
+ if (sub !== 'init') {
320
+ console.error(`${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n`);
321
+ process.exitCode = 1;
322
+ return;
323
+ }
324
+ const { created, path } = await dbInit(DB_PATH);
325
+ console.log(
326
+ created
327
+ ? ` ${green('create')} ${path}`
328
+ : ` ${dim('exists')} ${path} ${dim('(no-op)')}`,
329
+ );
330
+ }
331
+
332
+ // Resolves the project's core definition: an authored .hedgehog/core.yaml
333
+ // takes precedence (spec: "Authored cores"); otherwise the shipped Golden
334
+ // Core landed at repo root by `init` (its core.yaml copies there along
335
+ // with the rest of src/golden-cores/<core>).
336
+ async function resolveCorePath() {
337
+ if (await exists(join(DEST_ROOT, AUTHORED_CORE_PATH))) {
338
+ return join(DEST_ROOT, AUTHORED_CORE_PATH);
339
+ }
340
+ const rootCore = join(DEST_ROOT, 'core.yaml');
341
+ if (await exists(rootCore)) return rootCore;
342
+ return null;
343
+ }
344
+
345
+ async function planCommand() {
346
+ const corePath = await resolveCorePath();
347
+ if (!corePath) {
348
+ console.error(
349
+ `${red('No core definition found.')} Expected ${bold(AUTHORED_CORE_PATH)} or a root ${bold('core.yaml')} (from \`hedgehog init\`).\n`,
350
+ );
351
+ process.exitCode = 1;
352
+ return;
353
+ }
354
+
355
+ if (!(await exists(DB_PATH))) {
356
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
357
+ process.exitCode = 1;
358
+ return;
359
+ }
360
+
361
+ const core = await loadCore(corePath);
362
+ const db = new DatabaseSync(DB_PATH);
363
+ let result;
364
+ try {
365
+ db.exec('PRAGMA foreign_keys = ON;');
366
+ result = planTasks(db, core);
367
+ } finally {
368
+ db.close();
369
+ }
370
+
371
+ for (const id of result.compiled) console.log(` ${green('compiled')} ${id}`);
372
+ for (const id of result.skipped) console.log(` ${dim('skipped')} ${id} ${dim('(already compiled)')}`);
373
+ console.log(
374
+ `\n${green(bold('Plan complete.'))} ${dim(`${result.compiled.length} intent(s) compiled, ${result.skipped.length} skipped`)}\n`,
375
+ );
376
+ }
377
+
378
+ // Parses `hedgehog intent add` args into the same record shape
379
+ // src/db/intent.mjs#normalizeIntent expects. Two sources: `--file <path>`
380
+ // (a JSON file matching the intent record shape verbatim), or flags —
381
+ // `--id`, `--goal`, `--outcome`, `--priority`, repeatable `--rule`
382
+ // / `--constraint` / `--acceptance` / `--depends-on`. Mixing the two
383
+ // is rejected: one intent, one unambiguous source.
384
+ async function parseIntentArgs(args) {
385
+ const fileIdx = args.indexOf('--file');
386
+ const hasFlags = args.some((a) => a.startsWith('--') && a !== '--file');
387
+
388
+ if (fileIdx !== -1) {
389
+ if (hasFlags) {
390
+ throw new Error('--file cannot be combined with other intent flags');
391
+ }
392
+ const filePath = args[fileIdx + 1];
393
+ if (!filePath) throw new Error('--file requires a path');
394
+ const text = await readFile(resolve(DEST_ROOT, filePath), 'utf8');
395
+ return JSON.parse(text);
396
+ }
397
+
398
+ const record = { rules: [], constraints: [], acceptance: [], depends_on: [] };
399
+ for (let i = 0; i < args.length; i++) {
400
+ const flag = args[i];
401
+ const value = args[i + 1];
402
+ switch (flag) {
403
+ case '--id':
404
+ record.id = value;
405
+ i++;
406
+ break;
407
+ case '--goal':
408
+ record.goal = value;
409
+ i++;
410
+ break;
411
+ case '--outcome':
412
+ record.outcome = value;
413
+ i++;
414
+ break;
415
+ case '--priority':
416
+ record.priority = Number(value);
417
+ i++;
418
+ break;
419
+ case '--rule':
420
+ record.rules.push(value);
421
+ i++;
422
+ break;
423
+ case '--constraint':
424
+ record.constraints.push(value);
425
+ i++;
426
+ break;
427
+ case '--acceptance':
428
+ record.acceptance.push(value);
429
+ i++;
430
+ break;
431
+ case '--depends-on':
432
+ record.depends_on.push(value);
433
+ i++;
434
+ break;
435
+ default:
436
+ throw new Error(`Unknown intent flag: ${flag}`);
437
+ }
438
+ }
439
+ return record;
440
+ }
441
+
442
+ async function intentCommand(args) {
443
+ const sub = args[0];
444
+ if (sub !== 'add') {
445
+ console.error(
446
+ `${red('Unknown intent subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog intent add --id <id> --goal <goal> --outcome <outcome> [--rule <r>]... [--depends-on <id>]...\n or: hedgehog intent add --file <path.json>\n`,
447
+ );
448
+ process.exitCode = 1;
449
+ return;
450
+ }
451
+
452
+ if (!(await exists(DB_PATH))) {
453
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
454
+ process.exitCode = 1;
455
+ return;
456
+ }
457
+
458
+ let record;
459
+ try {
460
+ record = await parseIntentArgs(args.slice(1));
461
+ } catch (err) {
462
+ console.error(`${red('Invalid arguments:')} ${err.message}\n`);
463
+ process.exitCode = 1;
464
+ return;
465
+ }
466
+
467
+ const db = new DatabaseSync(DB_PATH);
468
+ let intent;
469
+ try {
470
+ db.exec('PRAGMA foreign_keys = ON;');
471
+ intent = addIntent(db, record);
472
+ } catch (err) {
473
+ console.error(`${red('Failed to add intent:')} ${err.message}\n`);
474
+ process.exitCode = 1;
475
+ return;
476
+ } finally {
477
+ db.close();
478
+ }
479
+
480
+ console.log(` ${green('added')} ${intent.id}`);
481
+ console.log(` ${dim(`${intent.requirements.length} requirement(s), ${intent.depends_on.length} dependency(ies)`)}`);
482
+ }
483
+
484
+ async function nextCommand() {
485
+ if (!(await exists(DB_PATH))) {
486
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
487
+ process.exitCode = 1;
488
+ return;
489
+ }
490
+
491
+ const db = new DatabaseSync(DB_PATH);
492
+ let packet;
493
+ let stalled = [];
494
+ try {
495
+ db.exec('PRAGMA foreign_keys = ON;');
496
+ packet = nextTask(db);
497
+ if (!packet) stalled = stalledTasks(db);
498
+ } finally {
499
+ db.close();
500
+ }
501
+
502
+ if (!packet) {
503
+ // A stalled task is not pickable, so without naming it here "no ready
504
+ // task" reads identically whether the build is finished or wedged on
505
+ // a failed verification.
506
+ if (stalled.length > 0) {
507
+ console.error(`${red(bold('No ready task, but the graph is blocked.'))}\n`);
508
+ for (const task of stalled) {
509
+ const reason =
510
+ task.status === 'failed' ? 'verification failed' : 'scope violation';
511
+ console.error(` ${red('✗')} ${bold(task.id)} ${task.layer} ${dim(reason)}`);
512
+ }
513
+ console.error(
514
+ `\nFix the work, then re-run ${bold('hedgehog verify <task-id>')}.\n`,
515
+ );
516
+ process.exitCode = 1;
517
+ return;
518
+ }
519
+ console.log(`${dim('No ready task.')} Nothing is planned with all dependencies complete.\n`);
520
+ return;
521
+ }
522
+
523
+ console.log(formatNext(packet));
524
+ }
525
+
526
+ async function verifyCommand(args) {
527
+ const taskId = args[0];
528
+ if (!taskId) {
529
+ console.error(`${red('Usage:')} hedgehog verify <task-id>\n`);
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+
534
+ if (!(await exists(DB_PATH))) {
535
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
536
+ process.exitCode = 1;
537
+ return;
538
+ }
539
+
540
+ const db = new DatabaseSync(DB_PATH);
541
+ let result;
542
+ try {
543
+ db.exec('PRAGMA foreign_keys = ON;');
544
+ result = verifyTask(db, taskId);
545
+ } catch (err) {
546
+ console.error(`${red('Verify failed:')} ${err.message}\n`);
547
+ process.exitCode = 1;
548
+ return;
549
+ } finally {
550
+ db.close();
551
+ }
552
+
553
+ if (result.outcome === 'scope_violation') {
554
+ console.error(`${red(bold('Scope violation.'))} Task ${bold(taskId)} stays ${bold('implemented')}.\n`);
555
+ console.error('Touched paths outside allowed scope:');
556
+ for (const path of result.offending) console.error(` ${red('✗')} ${path}`);
557
+ console.error();
558
+ process.exitCode = 1;
559
+ return;
560
+ }
561
+
562
+ if (result.outcome === 'failed') {
563
+ console.error(`${red(bold('Verification failed.'))} Task ${bold(taskId)} is now ${bold('failed')} (exit ${result.exitCode}).\n`);
564
+ if (result.output) console.error(result.output);
565
+ process.exitCode = 1;
566
+ return;
567
+ }
568
+
569
+ console.log(`${green(bold('Verified.'))} Task ${bold(taskId)} is now ${bold('complete')}.`);
570
+ if (result.commitSha) console.log(` ${dim('commit')} ${result.commitSha}`);
571
+ if (result.unlocked.length === 0) {
572
+ console.log(` ${dim('no dependents unlocked')}`);
573
+ } else {
574
+ for (const id of result.unlocked) console.log(` ${green('ready')} ${id}`);
575
+ }
576
+ if (result.intentComplete) {
577
+ console.log(` ${green('intent complete')} ${dim('every task for this intent is done')}`);
578
+ }
579
+ }
580
+
581
+ async function statusCommand() {
582
+ if (!(await exists(DB_PATH))) {
583
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
584
+ process.exitCode = 1;
585
+ return;
586
+ }
587
+
588
+ const db = new DatabaseSync(DB_PATH);
589
+ let result;
590
+ try {
591
+ db.exec('PRAGMA foreign_keys = ON;');
592
+ result = graphStatus(db);
593
+ } finally {
594
+ db.close();
595
+ }
596
+
597
+ console.log(formatStatus(result));
598
+ }
599
+
600
+ async function whyCommand(args) {
601
+ const path = args[0];
602
+ if (!path) {
603
+ console.error(`${red('Usage:')} hedgehog why <path>\n`);
604
+ process.exitCode = 1;
605
+ return;
606
+ }
607
+
608
+ if (!(await exists(DB_PATH))) {
609
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
610
+ process.exitCode = 1;
611
+ return;
612
+ }
613
+
614
+ const db = new DatabaseSync(DB_PATH);
615
+ let chain;
616
+ try {
617
+ db.exec('PRAGMA foreign_keys = ON;');
618
+ chain = whyPath(db, path);
619
+ } finally {
620
+ db.close();
621
+ }
622
+
623
+ console.log(formatWhy(path, chain));
624
+ }
625
+
626
+ async function frictionCommand(args) {
627
+ const sub = args[0];
628
+
629
+ if (!(await exists(DB_PATH))) {
630
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
631
+ process.exitCode = 1;
632
+ return;
633
+ }
634
+
635
+ if (sub === 'add') {
636
+ // Split `--task <id>` out of the note words by index, not by value —
637
+ // filtering on the *value* dropped the flag but kept its argument in
638
+ // the note (and would mangle a note that legitimately contains the
639
+ // word "--task").
640
+ const rest = args.slice(1);
641
+ const taskIdx = rest.indexOf('--task');
642
+ const taskId = taskIdx !== -1 ? rest[taskIdx + 1] : undefined;
643
+ if (taskIdx !== -1 && !taskId) {
644
+ console.error(`${red('--task requires a task id')}\n`);
645
+ process.exitCode = 1;
646
+ return;
647
+ }
648
+ const note = rest
649
+ .filter((_, i) => taskIdx === -1 || (i !== taskIdx && i !== taskIdx + 1))
650
+ .join(' ');
651
+ if (!note) {
652
+ console.error(`${red('Usage:')} hedgehog friction add "<note>" [--task <task-id>]\n`);
653
+ process.exitCode = 1;
654
+ return;
655
+ }
656
+
657
+ const db = new DatabaseSync(DB_PATH);
658
+ let entry;
659
+ try {
660
+ db.exec('PRAGMA foreign_keys = ON;');
661
+ entry = addFriction(db, { note, taskId });
662
+ } catch (err) {
663
+ console.error(`${red('Failed to log friction:')} ${err.message}\n`);
664
+ process.exitCode = 1;
665
+ return;
666
+ } finally {
667
+ db.close();
668
+ }
669
+
670
+ console.log(` ${green('logged')} #${entry.id}${entry.taskId ? ` (${entry.taskId})` : ''}`);
671
+ return;
672
+ }
673
+
674
+ if (sub === 'list') {
675
+ const db = new DatabaseSync(DB_PATH);
676
+ let entries;
677
+ try {
678
+ db.exec('PRAGMA foreign_keys = ON;');
679
+ entries = listFriction(db);
680
+ } finally {
681
+ db.close();
682
+ }
683
+
684
+ if (entries.length === 0) {
685
+ console.log(`${dim('No friction logged.')}\n`);
686
+ return;
687
+ }
688
+ for (const entry of entries) {
689
+ console.log(`#${entry.id} ${dim(entry.loggedAt)}${entry.taskId ? ` ${bold(entry.taskId)}` : ''}`);
690
+ console.log(` ${entry.note}\n`);
691
+ }
692
+ return;
693
+ }
694
+
695
+ console.error(
696
+ `${red('Unknown friction subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog friction add "<note>" [--task <task-id>]\n or: hedgehog friction list\n`,
697
+ );
698
+ process.exitCode = 1;
699
+ }
700
+
297
701
  async function main() {
298
702
  const args = process.argv.slice(2);
299
703
  if (args.includes('--help') || args.includes('-h') || args.length === 0) {
@@ -324,6 +728,46 @@ async function main() {
324
728
  return;
325
729
  }
326
730
 
731
+ if (cmd === 'db') {
732
+ await dbCommand(args.slice(1));
733
+ return;
734
+ }
735
+
736
+ if (cmd === 'plan') {
737
+ await planCommand();
738
+ return;
739
+ }
740
+
741
+ if (cmd === 'intent') {
742
+ await intentCommand(args.slice(1));
743
+ return;
744
+ }
745
+
746
+ if (cmd === 'next') {
747
+ await nextCommand();
748
+ return;
749
+ }
750
+
751
+ if (cmd === 'verify') {
752
+ await verifyCommand(args.slice(1));
753
+ return;
754
+ }
755
+
756
+ if (cmd === 'status') {
757
+ await statusCommand();
758
+ return;
759
+ }
760
+
761
+ if (cmd === 'why') {
762
+ await whyCommand(args.slice(1));
763
+ return;
764
+ }
765
+
766
+ if (cmd === 'friction') {
767
+ await frictionCommand(args.slice(1));
768
+ return;
769
+ }
770
+
327
771
  console.error(`${red('Unknown command:')} ${cmd}\n`);
328
772
  await help();
329
773
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "2.0.13",
3
+ "version": "3.0.0",
4
4
  "description": "Install the Hedgehog build discipline (agents + skills) into a repo.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -19,6 +19,7 @@
19
19
  "files": [
20
20
  "bin",
21
21
  "src/agents",
22
+ "src/db",
22
23
  "src/skills",
23
24
  "src/templates",
24
25
  "src/golden-cores",
@@ -26,7 +27,7 @@
26
27
  "skills/GSAP"
27
28
  ],
28
29
  "engines": {
29
- "node": ">=18"
30
+ "node": ">=22.5.0"
30
31
  },
31
32
  "keywords": [
32
33
  "claude",