@skyf0xx/hedgehog 3.1.0 → 4.0.3

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
@@ -28,7 +28,7 @@ Hedgehog combines:
28
28
 
29
29
  Software that stays structured as it grows.
30
30
 
31
- ![Just describe what you want](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/handoff.jpg)
31
+ ![Just describe what you want](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/handoff.png)
32
32
 
33
33
  ## The Hedgehog Loop
34
34
 
@@ -60,6 +60,14 @@ npx @skyf0xx/hedgehog graph # show graph
60
60
 
61
61
  ![The Hedgehog build graph](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/graph.png)
62
62
 
63
+ ## Parallel by Default
64
+
65
+ Every dependency is explicit, so Hedgehog knows which tasks can run in parallel.
66
+
67
+ ![Comparison](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/comparison.png)
68
+
69
+ Agents fan out to give you great outcomes at **faster speeds**.
70
+
63
71
  ## What Hedgehog builds
64
72
 
65
73
  ### Full-stack applications
@@ -109,7 +117,9 @@ then generates that workspace and builds it one verified layer at a time.
109
117
  The enforcement remains the same: ordered steps,
110
118
  scoped file access and a verification command per layer.
111
119
 
112
- ![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)
120
+ ## Why Hedgehog Works
121
+
122
+ ![Why Hedgehog works](https://raw.githubusercontent.com/skyf0xx/hedgehog/master/docs/images/why.png)
113
123
 
114
124
  ## Install
115
125
 
package/bin/cli.mjs CHANGED
@@ -14,24 +14,32 @@
14
14
 
15
15
  import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile } from 'node:fs/promises';
16
16
  import { constants } from 'node:fs';
17
- import { DatabaseSync } from 'node:sqlite';
18
17
  import { fileURLToPath } from 'node:url';
19
18
  import { dirname, join, relative, resolve } from 'node:path';
20
19
  import { spawn } from 'node:child_process';
21
- import { dbInit, DB_PATH } from '../src/db/init.mjs';
20
+ import { dbInit, DB_PATH, openDb } from '../src/db/init.mjs';
22
21
  import { loadCore } from '../src/db/core.mjs';
23
22
  import { planTasks } from '../src/db/plan.mjs';
24
- import { addIntent } from '../src/db/intent.mjs';
23
+ import { addIntent, INTENTS_DIR } from '../src/db/intent.mjs';
25
24
  import { nextTask, formatNext, stalledTasks } from '../src/db/next.mjs';
26
25
  import { verifyTask } from '../src/db/verify.mjs';
26
+ import { claimTasks, releaseTask, renewLease } from '../src/db/claim.mjs';
27
+ import { readyTasks, formatReady } from '../src/db/ready.mjs';
27
28
  import { graphStatus, formatStatus } from '../src/db/status.mjs';
28
29
  import { whyPath, formatWhy } from '../src/db/why.mjs';
29
30
  import { addFriction, listFriction } from '../src/db/friction.mjs';
31
+ import { rebuildDb } from '../src/db/rebuild.mjs';
30
32
  import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
31
33
  import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
32
34
 
33
35
  const AUTHORED_CORE_PATH = '.hedgehog/core.yaml';
34
36
 
37
+ const BLOCKED_REASON_LABELS = {
38
+ verification_failed: 'verification failed',
39
+ scope_violation: 'scope violation',
40
+ lease_expired: 'lease expired',
41
+ };
42
+
35
43
  const __dirname = dirname(fileURLToPath(import.meta.url));
36
44
  const PKG_ROOT = resolve(__dirname, '..');
37
45
  const DEST_ROOT = process.cwd();
@@ -179,6 +187,42 @@ const exists = (p) =>
179
187
  () => false,
180
188
  );
181
189
 
190
+ // Runs once at the top of every command that needs the build graph. A
191
+ // fresh clone has no `.hedgehog/hedgehog.db` (it's a derived artifact,
192
+ // not committed) but does have `.hedgehog/intents/*.json` — the committed
193
+ // source of truth — so this reconstructs the DB automatically instead of
194
+ // making every such command fail with "no build graph" on a repo that
195
+ // only just lost the file it never should have needed committed. If
196
+ // there's nothing to rebuild from either (a genuinely fresh project, no
197
+ // intents yet), this no-ops and leaves the DB missing — the caller's own
198
+ // existing "No build graph found" guard still fires for that case.
199
+ async function ensureDb() {
200
+ if (await exists(DB_PATH)) return;
201
+
202
+ let intentFiles = [];
203
+ try {
204
+ intentFiles = (await readdir(INTENTS_DIR)).filter((name) => name.endsWith('.json'));
205
+ } catch {
206
+ // .hedgehog/intents/ doesn't exist yet — nothing to rebuild from.
207
+ }
208
+ if (intentFiles.length === 0) return;
209
+
210
+ const corePath = await resolveCorePath();
211
+ if (!corePath) return;
212
+
213
+ await dbInit(DB_PATH);
214
+ const db = openDb();
215
+ let result;
216
+ try {
217
+ result = await rebuildDb(db, { corePath });
218
+ } finally {
219
+ db.close();
220
+ }
221
+ console.log(
222
+ `${dim('DB missing — rebuilt from')} ${bold(INTENTS_DIR)}${dim(':')} ${dim(`${result.intentsReplayed} intent(s) replayed, ${result.tasksMarkedComplete} task(s) marked complete`)}\n`,
223
+ );
224
+ }
225
+
182
226
  // Writes one planned file to disk — a straight copy, or for a `merge`
183
227
  // entry, the shell template with {{CORE_SECTION}} replaced by the
184
228
  // chosen core's include.
@@ -260,13 +304,19 @@ ${bold('Usage')}
260
304
  npx @skyf0xx/hedgehog init --force overwrite existing files
261
305
  npx @skyf0xx/hedgehog update refresh the installed agents + skills
262
306
  npx @skyf0xx/hedgehog db init create .hedgehog/hedgehog.db if absent
307
+ npx @skyf0xx/hedgehog db rebuild re-derive the build graph from committed intents + git history
263
308
  npx @skyf0xx/hedgehog plan compile pending intents into tasks + dependencies,
264
309
  then open the build graph if anything compiled
265
310
  npx @skyf0xx/hedgehog intent add [flags] add an intent (rules/requirements/dependencies)
266
311
  npx @skyf0xx/hedgehog intent add --file <path> add an intent from a JSON file
267
312
  npx @skyf0xx/hedgehog next print the task packet for one ready task
268
- npx @skyf0xx/hedgehog verify <task-id> run scope + verify checks, commit on pass
269
- npx @skyf0xx/hedgehog status graph overview: counts by status, ready list
313
+ npx @skyf0xx/hedgehog claim --owner <owner> [--count <n>] atomically claim up to n ready tasks
314
+ npx @skyf0xx/hedgehog release <task-id> --owner <owner> hand a claimed task back to ready
315
+ npx @skyf0xx/hedgehog renew <task-id> --owner <owner> [--minutes <n>] extend a held lease
316
+ npx @skyf0xx/hedgehog verify <task-id> --owner <owner> run scope + verify checks, commit on pass
317
+ npx @skyf0xx/hedgehog status graph overview: counts by status, ready list, in flight
318
+ npx @skyf0xx/hedgehog ready preview which ready tasks are claimable now vs held back
319
+ npx @skyf0xx/hedgehog quiesce report whether anything is still in flight
270
320
  npx @skyf0xx/hedgehog graph start (or reuse) the live graph server and open it
271
321
  npx @skyf0xx/hedgehog graph --no-open start (or reuse) the server; print the URL instead
272
322
  npx @skyf0xx/hedgehog why <path> provenance chain for a file
@@ -460,10 +510,40 @@ async function update({ hosts }) {
460
510
  );
461
511
  }
462
512
 
513
+ async function dbRebuildCommand() {
514
+ const corePath = await resolveCorePath();
515
+ if (!corePath) {
516
+ console.error(
517
+ `${red('No core definition found.')} Expected ${bold(AUTHORED_CORE_PATH)} or a root ${bold('core.yaml')} (from \`hedgehog init\`).\n`,
518
+ );
519
+ process.exitCode = 1;
520
+ return;
521
+ }
522
+
523
+ await dbInit(DB_PATH);
524
+ const db = openDb();
525
+ let result;
526
+ try {
527
+ result = await rebuildDb(db, { corePath });
528
+ } finally {
529
+ db.close();
530
+ }
531
+
532
+ console.log(
533
+ `${green('rebuilt')} ${dim(`${result.intentsReplayed} intent(s) replayed, ${result.tasksMarkedComplete} task(s) marked complete`)}\n`,
534
+ );
535
+ }
536
+
463
537
  async function dbCommand(args) {
464
538
  const sub = args[0];
539
+ if (sub === 'rebuild') {
540
+ await dbRebuildCommand();
541
+ return;
542
+ }
465
543
  if (sub !== 'init') {
466
- console.error(`${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n`);
544
+ console.error(
545
+ `${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n or: hedgehog db rebuild\n`,
546
+ );
467
547
  process.exitCode = 1;
468
548
  return;
469
549
  }
@@ -491,6 +571,8 @@ async function resolveCorePath() {
491
571
  }
492
572
 
493
573
  async function planCommand() {
574
+ await ensureDb();
575
+
494
576
  const corePath = await resolveCorePath();
495
577
  if (!corePath) {
496
578
  console.error(
@@ -507,10 +589,9 @@ async function planCommand() {
507
589
  }
508
590
 
509
591
  const core = await loadCore(corePath);
510
- const db = new DatabaseSync(DB_PATH);
592
+ const db = openDb();
511
593
  let result;
512
594
  try {
513
- db.exec('PRAGMA foreign_keys = ON;');
514
595
  result = planTasks(db, core);
515
596
  } finally {
516
597
  db.close();
@@ -600,6 +681,8 @@ async function parseIntentArgs(args) {
600
681
  }
601
682
 
602
683
  async function intentCommand(args) {
684
+ await ensureDb();
685
+
603
686
  const sub = args[0];
604
687
  if (sub !== 'add') {
605
688
  console.error(
@@ -624,11 +707,10 @@ async function intentCommand(args) {
624
707
  return;
625
708
  }
626
709
 
627
- const db = new DatabaseSync(DB_PATH);
710
+ const db = openDb();
628
711
  let intent;
629
712
  try {
630
- db.exec('PRAGMA foreign_keys = ON;');
631
- intent = addIntent(db, record);
713
+ intent = await addIntent(db, record);
632
714
  } catch (err) {
633
715
  console.error(`${red('Failed to add intent:')} ${err.message}\n`);
634
716
  process.exitCode = 1;
@@ -642,17 +724,18 @@ async function intentCommand(args) {
642
724
  }
643
725
 
644
726
  async function nextCommand() {
727
+ await ensureDb();
728
+
645
729
  if (!(await exists(DB_PATH))) {
646
730
  console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
647
731
  process.exitCode = 1;
648
732
  return;
649
733
  }
650
734
 
651
- const db = new DatabaseSync(DB_PATH);
735
+ const db = openDb();
652
736
  let packet;
653
737
  let stalled = [];
654
738
  try {
655
- db.exec('PRAGMA foreign_keys = ON;');
656
739
  packet = nextTask(db);
657
740
  if (!packet) stalled = stalledTasks(db);
658
741
  } finally {
@@ -666,8 +749,7 @@ async function nextCommand() {
666
749
  if (stalled.length > 0) {
667
750
  console.error(`${red(bold('No ready task, but the graph is blocked.'))}\n`);
668
751
  for (const task of stalled) {
669
- const reason =
670
- task.status === 'failed' ? 'verification failed' : 'scope violation';
752
+ const reason = BLOCKED_REASON_LABELS[task.blocked_reason] ?? task.blocked_reason;
671
753
  console.error(` ${red('✗')} ${bold(task.id)} ${task.layer} ${dim(reason)}`);
672
754
  }
673
755
  console.error(
@@ -684,9 +766,13 @@ async function nextCommand() {
684
766
  }
685
767
 
686
768
  async function verifyCommand(args) {
769
+ await ensureDb();
770
+
687
771
  const taskId = args[0];
688
- if (!taskId) {
689
- console.error(`${red('Usage:')} hedgehog verify <task-id>\n`);
772
+ const ownerIdx = args.indexOf('--owner');
773
+ const owner = ownerIdx !== -1 ? args[ownerIdx + 1] : undefined;
774
+ if (!taskId || !owner) {
775
+ console.error(`${red('Usage:')} hedgehog verify <task-id> --owner <owner>\n`);
690
776
  process.exitCode = 1;
691
777
  return;
692
778
  }
@@ -697,11 +783,10 @@ async function verifyCommand(args) {
697
783
  return;
698
784
  }
699
785
 
700
- const db = new DatabaseSync(DB_PATH);
786
+ const db = openDb();
701
787
  let result;
702
788
  try {
703
- db.exec('PRAGMA foreign_keys = ON;');
704
- result = verifyTask(db, taskId);
789
+ result = verifyTask(db, taskId, owner);
705
790
  } catch (err) {
706
791
  console.error(`${red('Verify failed:')} ${err.message}\n`);
707
792
  process.exitCode = 1;
@@ -711,7 +796,7 @@ async function verifyCommand(args) {
711
796
  }
712
797
 
713
798
  if (result.outcome === 'scope_violation') {
714
- console.error(`${red(bold('Scope violation.'))} Task ${bold(taskId)} stays ${bold('implemented')}.\n`);
799
+ console.error(`${red(bold('Scope violation.'))} Task ${bold(taskId)} is now ${bold('blocked')}.\n`);
715
800
  console.error('Touched paths outside allowed scope:');
716
801
  for (const path of result.offending) console.error(` ${red('✗')} ${path}`);
717
802
  console.error();
@@ -720,7 +805,7 @@ async function verifyCommand(args) {
720
805
  }
721
806
 
722
807
  if (result.outcome === 'failed') {
723
- console.error(`${red(bold('Verification failed.'))} Task ${bold(taskId)} is now ${bold('failed')} (exit ${result.exitCode}).\n`);
808
+ console.error(`${red(bold('Verification failed.'))} Task ${bold(taskId)} is now ${bold('blocked')} (exit ${result.exitCode}).\n`);
724
809
  if (result.output) console.error(result.output);
725
810
  process.exitCode = 1;
726
811
  return;
@@ -738,17 +823,142 @@ async function verifyCommand(args) {
738
823
  }
739
824
  }
740
825
 
826
+ // `hedgehog claim --owner <owner> [--count <n>]` — atomically claims up to
827
+ // `count` mutually non-conflicting ready tasks (claimTasks's fan-out, item
828
+ // 13) and prints each one's packet-level summary, plus which owner now
829
+ // holds them.
830
+ async function claimCommand(args) {
831
+ await ensureDb();
832
+
833
+ const ownerIdx = args.indexOf('--owner');
834
+ const owner = ownerIdx !== -1 ? args[ownerIdx + 1] : undefined;
835
+ const countIdx = args.indexOf('--count');
836
+ const count = countIdx !== -1 ? Number(args[countIdx + 1]) : 1;
837
+ if (!owner) {
838
+ console.error(`${red('Usage:')} hedgehog claim --owner <owner> [--count <n>]\n`);
839
+ process.exitCode = 1;
840
+ return;
841
+ }
842
+
843
+ if (!(await exists(DB_PATH))) {
844
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
845
+ process.exitCode = 1;
846
+ return;
847
+ }
848
+
849
+ const db = openDb();
850
+ let claimed;
851
+ try {
852
+ claimed = claimTasks(db, { owner, count });
853
+ } finally {
854
+ db.close();
855
+ }
856
+
857
+ if (claimed.length === 0) {
858
+ console.log(`${dim('No claimable task.')} Nothing is ready with no lease held.\n`);
859
+ return;
860
+ }
861
+
862
+ if (claimed.length > 1) {
863
+ console.log(`${green(bold('Claimed.'))} ${claimed.length} task(s) to ${bold(owner)}.`);
864
+ } else {
865
+ console.log(`${green(bold('Claimed.'))} Task ${bold(claimed[0].id)} leased to ${bold(owner)}.`);
866
+ }
867
+ for (const task of claimed) {
868
+ if (claimed.length > 1) console.log(` ${bold(task.id)}`);
869
+ console.log(` ${dim('expires')} ${task.lease_expires_at}`);
870
+ }
871
+ }
872
+
873
+ // `hedgehog release <task-id> --owner <owner>` — hands a claimed task
874
+ // back to `ready` without marking it blocked, for an agent stopping
875
+ // cleanly before finishing.
876
+ async function releaseCommand(args) {
877
+ await ensureDb();
878
+
879
+ const taskId = args[0];
880
+ const ownerIdx = args.indexOf('--owner');
881
+ const owner = ownerIdx !== -1 ? args[ownerIdx + 1] : undefined;
882
+ if (!taskId || !owner) {
883
+ console.error(`${red('Usage:')} hedgehog release <task-id> --owner <owner>\n`);
884
+ process.exitCode = 1;
885
+ return;
886
+ }
887
+
888
+ if (!(await exists(DB_PATH))) {
889
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
890
+ process.exitCode = 1;
891
+ return;
892
+ }
893
+
894
+ const db = openDb();
895
+ let result;
896
+ try {
897
+ result = releaseTask(db, taskId, owner);
898
+ } finally {
899
+ db.close();
900
+ }
901
+
902
+ if (!result.released) {
903
+ console.error(`${red('Not released.')} Task ${bold(taskId)} is not leased to ${bold(owner)} as ${bold('building')}.\n`);
904
+ process.exitCode = 1;
905
+ return;
906
+ }
907
+
908
+ console.log(`${green(bold('Released.'))} Task ${bold(taskId)} is now ${bold('ready')}.`);
909
+ }
910
+
911
+ // `hedgehog renew <task-id> --owner <owner> [--minutes <n>]` — extends a
912
+ // held lease, for an agent still working past the original lease window.
913
+ async function renewCommand(args) {
914
+ await ensureDb();
915
+
916
+ const taskId = args[0];
917
+ const ownerIdx = args.indexOf('--owner');
918
+ const owner = ownerIdx !== -1 ? args[ownerIdx + 1] : undefined;
919
+ const minutesIdx = args.indexOf('--minutes');
920
+ const minutes = minutesIdx !== -1 ? Number(args[minutesIdx + 1]) : 45;
921
+ if (!taskId || !owner) {
922
+ console.error(`${red('Usage:')} hedgehog renew <task-id> --owner <owner> [--minutes <n>]\n`);
923
+ process.exitCode = 1;
924
+ return;
925
+ }
926
+
927
+ if (!(await exists(DB_PATH))) {
928
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
929
+ process.exitCode = 1;
930
+ return;
931
+ }
932
+
933
+ const db = openDb();
934
+ let result;
935
+ try {
936
+ result = renewLease(db, taskId, owner, minutes);
937
+ } finally {
938
+ db.close();
939
+ }
940
+
941
+ if (!result.renewed) {
942
+ console.error(`${red('Not renewed.')} Task ${bold(taskId)} is not leased to ${bold(owner)}.\n`);
943
+ process.exitCode = 1;
944
+ return;
945
+ }
946
+
947
+ console.log(`${green(bold('Renewed.'))} Task ${bold(taskId)}'s lease extended by ${minutes} minute(s).`);
948
+ }
949
+
741
950
  async function statusCommand() {
951
+ await ensureDb();
952
+
742
953
  if (!(await exists(DB_PATH))) {
743
954
  console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
744
955
  process.exitCode = 1;
745
956
  return;
746
957
  }
747
958
 
748
- const db = new DatabaseSync(DB_PATH);
959
+ const db = openDb();
749
960
  let result;
750
961
  try {
751
- db.exec('PRAGMA foreign_keys = ON;');
752
962
  result = graphStatus(db);
753
963
  } finally {
754
964
  db.close();
@@ -757,6 +967,63 @@ async function statusCommand() {
757
967
  console.log(formatStatus(result));
758
968
  }
759
969
 
970
+ // `hedgehog ready` — read-only preview of what a `hedgehog claim` call
971
+ // would claim right now, and why anything ready is held back. Claims
972
+ // nothing.
973
+ async function readyCommand() {
974
+ await ensureDb();
975
+
976
+ if (!(await exists(DB_PATH))) {
977
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
978
+ process.exitCode = 1;
979
+ return;
980
+ }
981
+
982
+ const db = openDb();
983
+ let result;
984
+ try {
985
+ result = readyTasks(db);
986
+ } finally {
987
+ db.close();
988
+ }
989
+
990
+ console.log(formatReady(result));
991
+ }
992
+
993
+ // `hedgehog quiesce` — reports whether anything is still in flight
994
+ // (`building` or `verifying`), for a caller that has stopped dispatching
995
+ // and wants to know it's safe to treat the graph as settled. Claims and
996
+ // changes nothing; exits non-zero when the graph isn't quiesced yet so a
997
+ // caller can poll it in a loop or script.
998
+ async function quiesceCommand() {
999
+ await ensureDb();
1000
+
1001
+ if (!(await exists(DB_PATH))) {
1002
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
1003
+ process.exitCode = 1;
1004
+ return;
1005
+ }
1006
+
1007
+ const db = openDb();
1008
+ let inFlight;
1009
+ try {
1010
+ ({ inFlight } = graphStatus(db));
1011
+ } finally {
1012
+ db.close();
1013
+ }
1014
+
1015
+ if (inFlight.length === 0) {
1016
+ console.log(`${green(bold('Quiesced.'))} Nothing in flight.`);
1017
+ return;
1018
+ }
1019
+
1020
+ console.error(`${yellow(bold('Not quiesced.'))} ${inFlight.length} task(s) still in flight:\n`);
1021
+ for (const task of inFlight) {
1022
+ console.error(` ${task.id} ${task.status} owner: ${task.lease_owner}`);
1023
+ }
1024
+ process.exitCode = 1;
1025
+ }
1026
+
760
1027
  const GRAPH_PIDFILE_PATH = '.hedgehog/graph-server.json';
761
1028
  const GRAPH_SERVER_MODULE = join(PKG_ROOT, 'src/db/graph-server.mjs');
762
1029
  const GRAPH_TEMPLATE_PATH = join(PKG_ROOT, 'src/templates/graph.html');
@@ -885,6 +1152,8 @@ async function graphCommand(args) {
885
1152
  }
886
1153
 
887
1154
  async function whyCommand(args) {
1155
+ await ensureDb();
1156
+
888
1157
  const path = args[0];
889
1158
  if (!path) {
890
1159
  console.error(`${red('Usage:')} hedgehog why <path>\n`);
@@ -898,10 +1167,9 @@ async function whyCommand(args) {
898
1167
  return;
899
1168
  }
900
1169
 
901
- const db = new DatabaseSync(DB_PATH);
1170
+ const db = openDb();
902
1171
  let chain;
903
1172
  try {
904
- db.exec('PRAGMA foreign_keys = ON;');
905
1173
  chain = whyPath(db, path);
906
1174
  } finally {
907
1175
  db.close();
@@ -911,6 +1179,8 @@ async function whyCommand(args) {
911
1179
  }
912
1180
 
913
1181
  async function frictionCommand(args) {
1182
+ await ensureDb();
1183
+
914
1184
  const sub = args[0];
915
1185
 
916
1186
  if (!(await exists(DB_PATH))) {
@@ -941,11 +1211,10 @@ async function frictionCommand(args) {
941
1211
  return;
942
1212
  }
943
1213
 
944
- const db = new DatabaseSync(DB_PATH);
1214
+ const db = openDb();
945
1215
  let entry;
946
1216
  try {
947
- db.exec('PRAGMA foreign_keys = ON;');
948
- entry = addFriction(db, { note, taskId });
1217
+ entry = await addFriction(db, { note, taskId });
949
1218
  } catch (err) {
950
1219
  console.error(`${red('Failed to log friction:')} ${err.message}\n`);
951
1220
  process.exitCode = 1;
@@ -959,10 +1228,9 @@ async function frictionCommand(args) {
959
1228
  }
960
1229
 
961
1230
  if (sub === 'list') {
962
- const db = new DatabaseSync(DB_PATH);
1231
+ const db = openDb();
963
1232
  let entries;
964
1233
  try {
965
- db.exec('PRAGMA foreign_keys = ON;');
966
1234
  entries = listFriction(db);
967
1235
  } finally {
968
1236
  db.close();
@@ -1073,11 +1341,36 @@ async function main() {
1073
1341
  return;
1074
1342
  }
1075
1343
 
1344
+ if (cmd === 'claim') {
1345
+ await claimCommand(args.slice(1));
1346
+ return;
1347
+ }
1348
+
1349
+ if (cmd === 'release') {
1350
+ await releaseCommand(args.slice(1));
1351
+ return;
1352
+ }
1353
+
1354
+ if (cmd === 'renew') {
1355
+ await renewCommand(args.slice(1));
1356
+ return;
1357
+ }
1358
+
1076
1359
  if (cmd === 'status') {
1077
1360
  await statusCommand();
1078
1361
  return;
1079
1362
  }
1080
1363
 
1364
+ if (cmd === 'ready') {
1365
+ await readyCommand();
1366
+ return;
1367
+ }
1368
+
1369
+ if (cmd === 'quiesce') {
1370
+ await quiesceCommand();
1371
+ return;
1372
+ }
1373
+
1081
1374
  if (cmd === 'graph') {
1082
1375
  await graphCommand(args.slice(1));
1083
1376
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "3.1.0",
3
+ "version": "4.0.3",
4
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": {
@@ -11,9 +11,9 @@ You are the backend-eng role in the Hedgehog discipline, building Phase A
11
11
  `apps/worker`) one domain module at a time. The stack and the layer
12
12
  sequence within a module are fixed (`hedgehog-loop`, compiled into
13
13
  `src/golden-cores/full-stack-app/core.yaml`) — not yours to reorder or
14
- reshape. You're invoked with a `hedgehog next` task packet, not a step
15
- name — build exactly what its ALLOWED SCOPE names, one layer at a time,
16
- gated by `hedgehog verify` before the next starts.
14
+ reshape. You're invoked with a claimed task packet, not a step name —
15
+ build exactly what its ALLOWED SCOPE names, one layer at a time, gated by
16
+ `hedgehog verify` before the next starts.
17
17
 
18
18
  ## Stack (locked)
19
19
 
@@ -62,7 +62,7 @@ needed.
62
62
 
63
63
  ## Workflow
64
64
 
65
- 1. Read the `hedgehog next` task packet: its ALLOWED SCOPE is what to
65
+ 1. Read the claimed task packet: its ALLOWED SCOPE is what to
66
66
  build, not a step name you infer independently. Its WHY NOW section
67
67
  already confirms the module is in scope and every dependency is
68
68
  `complete` — no need to re-derive that by hand. Cross-module FK
@@ -86,6 +86,10 @@ needed.
86
86
 
87
87
  ## Constraints
88
88
 
89
+ - Default to no comments. Add one only when the WHY is non-obvious — a
90
+ hidden constraint, a workaround for a specific bug, an invariant the
91
+ code alone can't convey. Never comment WHAT the code does; a
92
+ well-named schema field, function, or variable already says that.
89
93
  - Never self-certify a task as done or run `git commit` for its changes —
90
94
  see Workflow step 3.
91
95
  - Never import another module's repository, service, or schema directly
@@ -108,3 +112,17 @@ needed.
108
112
  - If a downstream step reveals an upstream one (yours or another
109
113
  module's) was wrong, stop and fix it at the source — the Correction
110
114
  Protocol, not a workaround layered on top.
115
+ - You may be one of several agents building concurrently, each holding a
116
+ lease on its own task and scoped to its own ALLOWED SCOPE — a file
117
+ outside your scope changing while you work is another agent's task, not
118
+ a stray edit to fix. Never edit, revert, or "clean up" a file outside
119
+ your own scope, and never run a repo-wide command (a formatter over the
120
+ whole repo, a codemod, `nx migrate`, `nx format:write` with no path
121
+ filter) — it doesn't respect scope boundaries and will collide with
122
+ another agent's in-flight files.
123
+ - If verification fails for a reason plainly not yours — a neighboring
124
+ in-flight task's file shows up as a conflict, or a shared/global check
125
+ fails for reasons outside this task's scope — report it rather than
126
+ fixing it. That's a scheduler or core-design bug, and diagnosing it
127
+ belongs to the orchestrating session's Correction Protocol, not to this
128
+ step reaching outside its task to patch things over.