@skyf0xx/hedgehog 5.1.3 → 5.1.5

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
@@ -17,7 +17,7 @@ import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile } from 'node:
17
17
  import { constants, existsSync } from 'node:fs';
18
18
  import { fileURLToPath } from 'node:url';
19
19
  import { dirname, join, relative, resolve } from 'node:path';
20
- import { spawn } from 'node:child_process';
20
+ import { spawn, execFileSync } from 'node:child_process';
21
21
  import { dbInit, DB_PATH, dbAbsPath, openDb } from '../src/db/init.mjs';
22
22
  import { loadCore, lintCore, isModuleAxis } from '../src/db/core.mjs';
23
23
  import { planTasks, CORE_INTENT_ID } from '../src/db/plan.mjs';
@@ -52,7 +52,12 @@ import { rebuildDb } from '../src/db/rebuild.mjs';
52
52
  import { loadOverrides, addOverride, orphanedOverrides, OVERRIDES_DIR } from '../src/db/overrides.mjs';
53
53
  import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
54
54
  import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
55
- import { recordVersion, checkForUpdate, installedVersion } from '../src/hosts/version.mjs';
55
+ import {
56
+ recordVersion,
57
+ checkForUpdate,
58
+ checkBinaryStaleness,
59
+ installedVersion,
60
+ } from '../src/hosts/version.mjs';
56
61
  import { loadRegistry, resolveCore } from '../src/registry/index.mjs';
57
62
  import { fetchCore, cachedCore, cachedVersions } from '../src/registry/fetch.mjs';
58
63
  import { recordCore, installedCore } from '../src/registry/installed.mjs';
@@ -681,7 +686,11 @@ async function init({ force, core, host = DEFAULT_HOST, hostOnly = false }) {
681
686
  dim(
682
687
  ` Read ${bold(`${agents}/planner.md`)} and follow it — it runs planning\n` +
683
688
  ` intake (${skills}/hedgehog-planning-intake/SKILL.md), then hands\n` +
684
- ` off to ${agents}/bootstrap.md.`,
689
+ ` off to ${agents}/bootstrap.md.\n` +
690
+ ` Some hosts register agents/skills once, at session start: if a\n` +
691
+ ` later dispatch by name reports one of these as not found, read\n` +
692
+ ` its file directly instead — it becomes dispatchable by name after\n` +
693
+ ` a session restart or a fresh context.`,
685
694
  ),
686
695
  );
687
696
  console.log();
@@ -806,6 +815,14 @@ async function update({ hosts }) {
806
815
  'write-once content.',
807
816
  ),
808
817
  );
818
+ console.log(
819
+ dim(
820
+ 'Some hosts register agents/skills once, at session start: a renamed or\n' +
821
+ 'newly-added one may not be dispatchable by name until this session\n' +
822
+ 'restarts or a fresh context starts — read its file directly if a\n' +
823
+ 'name-based dispatch reports it as not found.',
824
+ ),
825
+ );
809
826
  }
810
827
 
811
828
  // Returned by resolveInstalledCore when the project has a core but its
@@ -1363,17 +1380,76 @@ async function noteAvailableUpdate() {
1363
1380
  if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
1364
1381
  try {
1365
1382
  const { installed, latest, stale } = await checkForUpdate(DEST_ROOT);
1366
- if (!stale) return;
1367
- console.error(
1368
- `\n${yellow(bold('Hedgehog update available.'))} ${dim(
1369
- `${installed} → ${latest}. Run \`npx @skyf0xx/hedgehog@latest update\` to refresh this project's agents and skills.`,
1370
- )}`,
1371
- );
1383
+ if (stale) {
1384
+ console.error(
1385
+ `\n${yellow(bold('Hedgehog update available.'))} ${dim(
1386
+ `${installed} → ${latest}. Run \`npx @skyf0xx/hedgehog@latest update\` to refresh this project's agents and skills.`,
1387
+ )}`,
1388
+ );
1389
+ }
1372
1390
  } catch {
1373
1391
  // Advisory only — a failed check is never worth failing a command over.
1374
1392
  }
1375
1393
  }
1376
1394
 
1395
+ // Hedgehog runs as a global install: every host (Claude Code, Cursor,
1396
+ // Gemini CLI) drives it through the `hedgehog` binary on PATH, and `npx
1397
+ // @skyf0xx/hedgehog` is expected to arrive at that same global install
1398
+ // rather than an ephemeral, unmanaged copy. This makes both halves of
1399
+ // that true on every invocation, before any command's real work starts:
1400
+ //
1401
+ // - **No global install yet** (a fresh `npx` fetch, or a dev checkout
1402
+ // run directly): install the latest release globally now.
1403
+ // - **A global install exists but is behind latest** (the running code's
1404
+ // own version, not any project's stamped `.hedgehog/version.json` —
1405
+ // that stamp answers a different question, whether a project's payload
1406
+ // is current): update it to latest now.
1407
+ //
1408
+ // Either way this reuses `latestVersion`'s day-long cache, so it costs a
1409
+ // network round trip at most once a day, not on every invocation. Runs
1410
+ // unattended — `npm install -g`, no confirmation prompt. Every caller of
1411
+ // this binary is either an agent driving it non-interactively or a human
1412
+ // who ran a command expecting it to just work, and there is no stdin
1413
+ // channel to prompt on in the general case. A failure here (offline,
1414
+ // permission-restricted global dir, npm itself missing) is swallowed the
1415
+ // same as a failed staleness check — the current process keeps running on
1416
+ // whatever code it already loaded rather than failing the command over
1417
+ // an advisory step.
1418
+ //
1419
+ // The install/update itself always runs regardless of `quiet` — silence
1420
+ // governs only whether this prints, matching `boundary --quiet`'s "exit
1421
+ // code only, nothing on either stream" contract for shell hooks.
1422
+ async function ensureGlobalInstall({ quiet = false } = {}) {
1423
+ if (process.env.HEDGEHOG_NO_UPDATE_CHECK) return;
1424
+ try {
1425
+ const globalRoot = execFileSync('npm', ['root', '-g'], {
1426
+ encoding: 'utf8',
1427
+ timeout: 5_000,
1428
+ }).trim();
1429
+ const runningFromGlobal = PKG_ROOT === join(globalRoot, '@skyf0xx/hedgehog');
1430
+ const { latest, stale } = await checkBinaryStaleness(DEST_ROOT, PKG_VERSION);
1431
+ if (runningFromGlobal && !stale) return;
1432
+ if (!latest) return;
1433
+
1434
+ execFileSync('npm', ['install', '-g', `@skyf0xx/hedgehog@${latest}`], {
1435
+ stdio: 'ignore',
1436
+ timeout: 60_000,
1437
+ });
1438
+ if (!quiet) {
1439
+ console.error(
1440
+ `\n${dim(
1441
+ runningFromGlobal
1442
+ ? `Updated hedgehog ${PKG_VERSION} → ${latest}. This run continues on ${PKG_VERSION}; the next command picks up ${latest}.`
1443
+ : `Installed hedgehog ${latest} globally. Future commands resolve to it directly.`,
1444
+ )}`,
1445
+ );
1446
+ }
1447
+ } catch {
1448
+ // Advisory only — a failed check or install is never worth failing a
1449
+ // command over.
1450
+ }
1451
+ }
1452
+
1377
1453
  async function nextCommand() {
1378
1454
  await ensureDb();
1379
1455
 
@@ -2716,6 +2792,7 @@ async function main() {
2716
2792
  console.log(PKG_VERSION);
2717
2793
  return;
2718
2794
  }
2795
+ await ensureGlobalInstall({ quiet: args.includes('--quiet') });
2719
2796
  const cmd = args[0];
2720
2797
  const force = args.includes('--force') || args.includes('-f');
2721
2798
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "5.1.3",
3
+ "version": "5.1.5",
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": {
@@ -6,6 +6,14 @@ context with its own tool grant. The skills in `.claude/skills/` are
6
6
  available the same way; invoke one by name rather than reimplementing what
7
7
  it describes.
8
8
 
9
+ Claude Code reads that registration once, at session start — an agent or
10
+ skill file written mid-session (by `hedgehog init` or `hedgehog update`
11
+ just now) is not yet dispatchable by name in this session. If a name-based
12
+ dispatch reports it as not found, read the file directly from
13
+ `.claude/agents/` or `.claude/skills/` and follow it inline instead of
14
+ retrying the dispatch; it becomes dispatchable by name after a session
15
+ restart or a fresh context.
16
+
9
17
  Clear context with `/clear` at the unit boundaries described above —
10
18
  `hedgehog boundary` tells you whether you're at one (exit 0), and
11
19
  `hedgehog boundary --handoff` is what the next session starts from.
@@ -152,3 +152,26 @@ export async function checkForUpdate(root, { force = false } = {}) {
152
152
  stale: Boolean(installed && latest && isNewer(latest, installed)),
153
153
  };
154
154
  }
155
+
156
+ /**
157
+ * Whether the CLI binary actually executing right now — `binaryVersion`,
158
+ * read from this package's own package.json, not from any project's
159
+ * `.hedgehog/version.json` — is behind the newest published release.
160
+ *
161
+ * This is a distinct question from `checkForUpdate`: that one compares a
162
+ * *project's* stamped version against latest, so a shadowed, stale global
163
+ * install (a `hedgehog` binary resolved from PATH ahead of the scoped
164
+ * `npx @skyf0xx/hedgehog` package) reads its own old code, sees the
165
+ * project's stamp already at-or-past whatever it thinks "latest" is, and
166
+ * says nothing — the exact case this function exists to catch instead, by
167
+ * comparing the running code's own version rather than a file the running
168
+ * code could itself be stale relative to.
169
+ */
170
+ export async function checkBinaryStaleness(root, binaryVersion, { force = false } = {}) {
171
+ const latest = await latestVersion(root, { force });
172
+ return {
173
+ binaryVersion,
174
+ latest,
175
+ stale: Boolean(binaryVersion && latest && isNewer(latest, binaryVersion)),
176
+ };
177
+ }