@skyf0xx/hedgehog 5.3.2 → 5.3.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/bin/cli.mjs +48 -5
- package/package.json +1 -1
- package/src/db/next.mjs +6 -1
- package/src/db/verify.mjs +24 -1
- package/src/hosts/gemini/gemini-extension.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -585,9 +585,33 @@ the @latest tag matters, since a bare npx may reuse a cached older CLI.
|
|
|
585
585
|
`);
|
|
586
586
|
}
|
|
587
587
|
|
|
588
|
+
// The entire Hedgehog discipline is commit-per-layer/step — every loop
|
|
589
|
+
// skill's own enforcement mechanism assumes `git commit` works in this
|
|
590
|
+
// directory. Checked here rather than left to surface as a failed commit
|
|
591
|
+
// several planning-intake steps later, after work with nowhere to land
|
|
592
|
+
// as history has already happened. Run unattended, the same as
|
|
593
|
+
// ensureGlobalInstall: this binary has no stdin channel to prompt on in
|
|
594
|
+
// the general case, and `init` already writes every other file without
|
|
595
|
+
// confirmation.
|
|
596
|
+
function ensureGitRepo() {
|
|
597
|
+
try {
|
|
598
|
+
execFileSync('git', ['rev-parse', '--is-inside-work-tree'], {
|
|
599
|
+
stdio: 'ignore',
|
|
600
|
+
timeout: 5_000,
|
|
601
|
+
});
|
|
602
|
+
return;
|
|
603
|
+
} catch {
|
|
604
|
+
// Not inside a work tree — fall through to init one.
|
|
605
|
+
}
|
|
606
|
+
execFileSync('git', ['init'], { stdio: 'ignore', timeout: 5_000 });
|
|
607
|
+
console.log(dim(' (no git repository found here — ran `git init`, since every later step commits)'));
|
|
608
|
+
}
|
|
609
|
+
|
|
588
610
|
// `core` is the fetched core — `{ manifest, root, version }` — or null on
|
|
589
611
|
// a deferred install, where planner picks one later.
|
|
590
612
|
async function init({ force, core, host = DEFAULT_HOST, hostOnly = false }) {
|
|
613
|
+
ensureGitRepo();
|
|
614
|
+
|
|
591
615
|
// Resolve the full list of writes up front so we can detect conflicts
|
|
592
616
|
// before touching anything. A deferred install plans against `null` —
|
|
593
617
|
// the shared agents/skills/build-graph payload only.
|
|
@@ -650,6 +674,16 @@ async function init({ force, core, host = DEFAULT_HOST, hostOnly = false }) {
|
|
|
650
674
|
)}\n`,
|
|
651
675
|
);
|
|
652
676
|
console.log('Next steps:');
|
|
677
|
+
// This install itself just fetched globalInstall.latest to run, so the
|
|
678
|
+
// global binary is already ahead of the version this run scaffolded
|
|
679
|
+
// with. Printed as step 0 here rather than left for `status`'s passive
|
|
680
|
+
// nudge to surface later, mixed in with routine output well into the
|
|
681
|
+
// build.
|
|
682
|
+
if (globalInstall?.updated) {
|
|
683
|
+
console.log(
|
|
684
|
+
` 0. ${bold(`npx @skyf0xx/hedgehog@latest update`)} ${dim(`(picks up ${globalInstall.latest} before intake begins)`)}`,
|
|
685
|
+
);
|
|
686
|
+
}
|
|
653
687
|
// A core that landed a workspace landed its root package.json with it,
|
|
654
688
|
// so there are dependencies to install before the first commit.
|
|
655
689
|
if (core?.manifest.workspace) {
|
|
@@ -1426,8 +1460,15 @@ async function noteAvailableUpdate() {
|
|
|
1426
1460
|
// The install/update itself always runs regardless of `quiet` — silence
|
|
1427
1461
|
// governs only whether this prints, matching `boundary --quiet`'s "exit
|
|
1428
1462
|
// code only, nothing on either stream" contract for shell hooks.
|
|
1463
|
+
// Returns `{ updated, latest }` — `updated` true only when this call
|
|
1464
|
+
// actually installed a newer global version this run is not yet running
|
|
1465
|
+
// on. `init` uses that to put the update command explicitly in its own
|
|
1466
|
+
// "Next steps" rather than leaving it to be discovered later via
|
|
1467
|
+
// `status`'s passive nudge, since by the time `init` prints its own
|
|
1468
|
+
// output the global binary is already ahead of the version this run used
|
|
1469
|
+
// to scaffold the project.
|
|
1429
1470
|
async function ensureGlobalInstall({ quiet = false } = {}) {
|
|
1430
|
-
if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
|
|
1471
|
+
if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return { updated: false };
|
|
1431
1472
|
try {
|
|
1432
1473
|
const globalRoot = execFileSync('npm', ['root', '-g'], {
|
|
1433
1474
|
encoding: 'utf8',
|
|
@@ -1435,8 +1476,8 @@ async function ensureGlobalInstall({ quiet = false } = {}) {
|
|
|
1435
1476
|
}).trim();
|
|
1436
1477
|
const runningFromGlobal = PKG_ROOT === join(globalRoot, '@skyf0xx/hedgehog');
|
|
1437
1478
|
const { latest, stale } = await checkBinaryStaleness(DEST_ROOT, PKG_VERSION);
|
|
1438
|
-
if (runningFromGlobal && !stale) return;
|
|
1439
|
-
if (!latest) return;
|
|
1479
|
+
if (runningFromGlobal && !stale) return { updated: false };
|
|
1480
|
+
if (!latest) return { updated: false };
|
|
1440
1481
|
|
|
1441
1482
|
execFileSync('npm', ['install', '-g', `@skyf0xx/hedgehog@${latest}`], {
|
|
1442
1483
|
stdio: 'ignore',
|
|
@@ -1451,9 +1492,11 @@ async function ensureGlobalInstall({ quiet = false } = {}) {
|
|
|
1451
1492
|
)}`,
|
|
1452
1493
|
);
|
|
1453
1494
|
}
|
|
1495
|
+
return { updated: runningFromGlobal, latest };
|
|
1454
1496
|
} catch {
|
|
1455
1497
|
// Advisory only — a failed check or install is never worth failing a
|
|
1456
1498
|
// command over.
|
|
1499
|
+
return { updated: false };
|
|
1457
1500
|
}
|
|
1458
1501
|
}
|
|
1459
1502
|
|
|
@@ -2877,7 +2920,7 @@ async function main() {
|
|
|
2877
2920
|
console.log(PKG_VERSION);
|
|
2878
2921
|
return;
|
|
2879
2922
|
}
|
|
2880
|
-
await ensureGlobalInstall({ quiet: args.includes('--quiet') });
|
|
2923
|
+
const globalInstall = await ensureGlobalInstall({ quiet: args.includes('--quiet') });
|
|
2881
2924
|
const cmd = args[0];
|
|
2882
2925
|
const force = args.includes('--force') || args.includes('-f');
|
|
2883
2926
|
|
|
@@ -2948,7 +2991,7 @@ async function main() {
|
|
|
2948
2991
|
// the first host; the rest add only what differs per host.
|
|
2949
2992
|
const targets = hosts.length ? hosts : [DEFAULT_HOST];
|
|
2950
2993
|
for (const [i, host] of targets.entries()) {
|
|
2951
|
-
await init({ force, core, host, hostOnly: i > 0 });
|
|
2994
|
+
await init({ force, core, host, hostOnly: i > 0, globalInstall });
|
|
2952
2995
|
}
|
|
2953
2996
|
return;
|
|
2954
2997
|
}
|
package/package.json
CHANGED
package/src/db/next.mjs
CHANGED
|
@@ -374,7 +374,12 @@ const HONESTY = [
|
|
|
374
374
|
export function scopePackageRoot(glob, module) {
|
|
375
375
|
const segments = glob.replace('{module}', module).split('/');
|
|
376
376
|
const wildcard = segments.findIndex((s) => s.includes('*'));
|
|
377
|
-
|
|
377
|
+
// No wildcard means the glob names one literal file, not a directory
|
|
378
|
+
// tree — a single prompt/config file (e.g. `.hedgehog/dsh-smoke/{module}.md`)
|
|
379
|
+
// never holds a `package.json` of its own, so its parent directory is
|
|
380
|
+
// never a package root a generator could be first into.
|
|
381
|
+
if (wildcard === -1) return null;
|
|
382
|
+
const literal = segments.slice(0, wildcard);
|
|
378
383
|
const src = literal.indexOf('src');
|
|
379
384
|
const root = src === -1 ? literal : literal.slice(0, src);
|
|
380
385
|
// A package root is at least `<area>/<name>`; anything shallower is the
|
package/src/db/verify.mjs
CHANGED
|
@@ -61,6 +61,24 @@ import { DB_PATH } from './init.mjs';
|
|
|
61
61
|
import { withCommitLock, LOCK_PATH } from './commitLock.mjs';
|
|
62
62
|
import { reapExpiredLeases, pathFingerprint } from './claim.mjs';
|
|
63
63
|
import { ensureTaskColumns } from './schema.mjs';
|
|
64
|
+
import { FRICTION_DIR } from './friction.mjs';
|
|
65
|
+
import { OVERRIDES_DIR } from './overrides.mjs';
|
|
66
|
+
import { INTENTS_DIR } from './intent.mjs';
|
|
67
|
+
|
|
68
|
+
// Build-graph state directories: written by their own command
|
|
69
|
+
// (`friction add`, `override add`, `intent add`/`db rebuild`), committed
|
|
70
|
+
// by that command's own next step, never by a layer's verify_command. A
|
|
71
|
+
// layer's own work never lands here, so a path under one of these is
|
|
72
|
+
// never this task's doing regardless of when it changed relative to
|
|
73
|
+
// claim time — unlike attributedToTask's fingerprint check, which only
|
|
74
|
+
// excludes a path unchanged since claim and so still attributes a
|
|
75
|
+
// friction note logged mid-layer (exactly what the loop skill instructs)
|
|
76
|
+
// to whichever task happened to be building when it was logged.
|
|
77
|
+
const BUILD_GRAPH_STATE_DIRS = [FRICTION_DIR, OVERRIDES_DIR, INTENTS_DIR];
|
|
78
|
+
|
|
79
|
+
function isBuildGraphStatePath(path) {
|
|
80
|
+
return BUILD_GRAPH_STATE_DIRS.some((dir) => path === dir || path.startsWith(`${dir}/`));
|
|
81
|
+
}
|
|
64
82
|
|
|
65
83
|
// The build graph file and the commit lock are engine state, written
|
|
66
84
|
// only by this CLI, never by an agent — both are excluded from every
|
|
@@ -69,7 +87,12 @@ import { ensureTaskColumns } from './schema.mjs';
|
|
|
69
87
|
// check they're performing. Covers SQLite's journal/WAL/SHM sidecar
|
|
70
88
|
// files too.
|
|
71
89
|
function isEngineStatePath(path) {
|
|
72
|
-
return
|
|
90
|
+
return (
|
|
91
|
+
path === DB_PATH ||
|
|
92
|
+
path.startsWith(`${DB_PATH}-`) ||
|
|
93
|
+
path === LOCK_PATH ||
|
|
94
|
+
isBuildGraphStatePath(path)
|
|
95
|
+
);
|
|
73
96
|
}
|
|
74
97
|
|
|
75
98
|
// Runs git with an argv array and no shell, so every element of `args`
|