@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 +12 -2
- package/bin/cli.mjs +324 -31
- package/package.json +1 -1
- package/src/agents/backend-eng.md +22 -4
- package/src/agents/front-end-eng.md +23 -5
- package/src/agents/layer-eng.md +21 -3
- package/src/agents/planner.md +4 -2
- package/src/db/claim.mjs +147 -0
- package/src/db/commitLock.mjs +77 -0
- package/src/db/conflict.mjs +52 -0
- package/src/db/core.mjs +34 -0
- package/src/db/friction.mjs +19 -4
- package/src/db/graph-server.mjs +3 -0
- package/src/db/graph.mjs +1 -0
- package/src/db/init.mjs +46 -1
- package/src/db/intent.mjs +22 -3
- package/src/db/next.mjs +8 -7
- package/src/db/plan.mjs +25 -12
- package/src/db/ready.mjs +94 -0
- package/src/db/rebuild.mjs +100 -0
- package/src/db/schema.mjs +11 -4
- package/src/db/status.mjs +47 -21
- package/src/db/verify.mjs +251 -119
- package/src/golden-cores/full-stack-app/core.yaml +34 -10
- package/src/golden-cores/full-stack-app/gitignore.template +3 -0
- package/src/golden-cores/landing-page/gitignore.template +3 -0
- package/src/skills/hedgehog-authored-loop/SKILL.md +73 -45
- package/src/skills/hedgehog-core-design/SKILL.md +47 -3
- package/src/skills/hedgehog-landing-loop/SKILL.md +44 -31
- package/src/skills/hedgehog-loop/SKILL.md +83 -43
- package/src/skills/no-history-in-output/SKILL.md +38 -0
- package/src/templates/CLAUDE.md +41 -22
- package/src/templates/graph.html +34 -50
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Hedgehog combines:
|
|
|
28
28
|
|
|
29
29
|
Software that stays structured as it grows.
|
|
30
30
|
|
|
31
|
-

|
|
32
32
|
|
|
33
33
|
## The Hedgehog Loop
|
|
34
34
|
|
|
@@ -60,6 +60,14 @@ npx @skyf0xx/hedgehog graph # show graph
|
|
|
60
60
|
|
|
61
61
|

|
|
62
62
|
|
|
63
|
+
## Parallel by Default
|
|
64
|
+
|
|
65
|
+
Every dependency is explicit, so Hedgehog knows which tasks can run in parallel.
|
|
66
|
+
|
|
67
|
+

|
|
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
|
-
|
|
120
|
+
## Why Hedgehog Works
|
|
121
|
+
|
|
122
|
+

|
|
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
|
|
269
|
-
npx @skyf0xx/hedgehog
|
|
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(
|
|
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 =
|
|
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 =
|
|
710
|
+
const db = openDb();
|
|
628
711
|
let intent;
|
|
629
712
|
try {
|
|
630
|
-
|
|
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 =
|
|
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
|
-
|
|
689
|
-
|
|
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 =
|
|
786
|
+
const db = openDb();
|
|
701
787
|
let result;
|
|
702
788
|
try {
|
|
703
|
-
db
|
|
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)}
|
|
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('
|
|
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 =
|
|
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 =
|
|
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 =
|
|
1214
|
+
const db = openDb();
|
|
945
1215
|
let entry;
|
|
946
1216
|
try {
|
|
947
|
-
db
|
|
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 =
|
|
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
|
@@ -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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
|
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.
|