@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "5.3.2",
3
+ "version": "5.3.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": {
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
- const literal = wildcard === -1 ? segments.slice(0, -1) : segments.slice(0, wildcard);
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 path === DB_PATH || path.startsWith(`${DB_PATH}-`) || path === LOCK_PATH;
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`
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "5.3.2",
3
+ "version": "5.3.3",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }