@skyf0xx/hedgehog 6.0.2 → 6.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 +2 -4
- package/bin/cli.mjs +110 -2
- package/package.json +1 -1
- package/src/db/plan.mjs +5 -4
- package/src/hosts/claude-md-merge.mjs +69 -0
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/registry/cores.json +5 -5
- package/src/registry/index.mjs +10 -8
- package/src/registry/installed.mjs +35 -7
package/README.md
CHANGED
|
@@ -202,9 +202,7 @@ Where possible, Hedgehog uses a battle-tested blueprint in [`hedgehog-core-autho
|
|
|
202
202
|
|
|
203
203
|
### Existing codebases
|
|
204
204
|
|
|
205
|
-
Hedgehog also adopts onto
|
|
206
|
-
|
|
207
|
-
It scans the repo's shape and is able to create new changes with the same scoped, verified, committed loop.
|
|
205
|
+
Hedgehog also adopts onto a repo it didn't build. It reads the repo and proposes a layer chain that verifies with the repo's own test/lint/build commands.
|
|
208
206
|
|
|
209
207
|
## Why Hedgehog Works
|
|
210
208
|
|
|
@@ -223,7 +221,7 @@ Mention `Hedgehog` whenever you want to build something with it.
|
|
|
223
221
|
2. Check Python >=3.10 is installed — it runs [CodeGraphContext](https://github.com/CodeGraphContext/CodeGraphContext), the code index every project uses.
|
|
224
222
|
3. Check CodeGraphContext is installed and configured.
|
|
225
223
|
4. If anything is missing, offer to set it up with the `hedgehog-code-intelligence-setup` skill — `init` stops without it.
|
|
226
|
-
5. Run the install commands
|
|
224
|
+
5. Run the install commands below.
|
|
227
225
|
|
|
228
226
|
</details>
|
|
229
227
|
|
package/bin/cli.mjs
CHANGED
|
@@ -80,6 +80,7 @@ import { rebuildDb } from '../src/db/rebuild.mjs';
|
|
|
80
80
|
import { loadOverrides, addOverride, orphanedOverrides, OVERRIDES_DIR } from '../src/db/overrides.mjs';
|
|
81
81
|
import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
|
|
82
82
|
import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
|
|
83
|
+
import { wrapSection } from '../src/hosts/claude-md-merge.mjs';
|
|
83
84
|
import {
|
|
84
85
|
recordVersion,
|
|
85
86
|
checkForUpdate,
|
|
@@ -88,7 +89,7 @@ import {
|
|
|
88
89
|
} from '../src/hosts/version.mjs';
|
|
89
90
|
import { loadRegistry, resolveCore } from '../src/registry/index.mjs';
|
|
90
91
|
import { fetchCore, cachedCore, cachedVersions, cachedEngine } from '../src/registry/fetch.mjs';
|
|
91
|
-
import { recordCore, installedCore } from '../src/registry/installed.mjs';
|
|
92
|
+
import { recordCore, installedCore, ADOPTED_CORE_NAME } from '../src/registry/installed.mjs';
|
|
92
93
|
|
|
93
94
|
const AUTHORED_CORE_PATH = '.hedgehog/core.yaml';
|
|
94
95
|
const CODE_INTELLIGENCE_CONFIG_PATH = '.hedgehog/code-intelligence.json';
|
|
@@ -472,7 +473,13 @@ async function writePlannedFile(f) {
|
|
|
472
473
|
join(f.merge.includeRoot ?? PKG_ROOT, f.merge.include),
|
|
473
474
|
'utf8',
|
|
474
475
|
);
|
|
475
|
-
|
|
476
|
+
// Wrapped in the same markers appendCoreSection uses on a
|
|
477
|
+
// brownfield CLAUDE.md with no {{CORE_SECTION}} placeholder to
|
|
478
|
+
// substitute into — see src/hosts/claude-md-merge.mjs. Keeping
|
|
479
|
+
// both paths' output mutually recognizable is what lets
|
|
480
|
+
// hasCoreSection/appendCoreSection treat a template-filled file
|
|
481
|
+
// and an appended one the same way on a later re-run.
|
|
482
|
+
out = out.replaceAll('{{CORE_SECTION}}', wrapSection(section));
|
|
476
483
|
}
|
|
477
484
|
const dispatch = await readFile(join(PKG_ROOT, f.merge.dispatch), 'utf8');
|
|
478
485
|
await writeFile(f.dest, out.replaceAll('{{HOST_DISPATCH}}', dispatch.trimEnd()));
|
|
@@ -544,6 +551,9 @@ ${bold('Usage')}
|
|
|
544
551
|
npx @skyf0xx/hedgehog init --pwa-app scaffold the pwa-app core now
|
|
545
552
|
npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core now
|
|
546
553
|
npx @skyf0xx/hedgehog cores list every core this release can install
|
|
554
|
+
npx @skyf0xx/hedgehog core record-adopted land the authored core's agents/skills and record
|
|
555
|
+
this project as adopted (hedgehog-adopt calls this
|
|
556
|
+
after writing .hedgehog/core.yaml; not for other cores)
|
|
547
557
|
npx @skyf0xx/hedgehog init --cursor install for Cursor (default: Claude Code)
|
|
548
558
|
npx @skyf0xx/hedgehog init --host=claude,gemini install for several coding agents at once
|
|
549
559
|
npx @skyf0xx/hedgehog init --all-hosts install for every supported coding agent
|
|
@@ -1173,6 +1183,84 @@ async function resolveInstalledCore() {
|
|
|
1173
1183
|
}
|
|
1174
1184
|
}
|
|
1175
1185
|
|
|
1186
|
+
// `hedgehog core record-adopted` — the record path for `hedgehog-adopt`
|
|
1187
|
+
// (shipped in @skyf0xx/hedgehog-core-authored), which brings the
|
|
1188
|
+
// discipline to an existing repo by writing `.hedgehog/core.yaml` and
|
|
1189
|
+
// `.hedgehog/adoption.md` directly. That path has no `init` step and so
|
|
1190
|
+
// never fetches the `authored` package or calls `recordCore` — a no-flag
|
|
1191
|
+
// `init` (what the offer skill actually runs before adoption) installs
|
|
1192
|
+
// only the shared engine payload, never a core's own agents/skills, and
|
|
1193
|
+
// `bootstrap` (the only other place a core package gets fetched) is
|
|
1194
|
+
// explicitly skipped for adoption. Left alone, `hedgehog-authored-loop`
|
|
1195
|
+
// and `layer-eng` — the skill and agent adoption hands off to — would
|
|
1196
|
+
// never land on disk, and even if they did by some other means, `update`
|
|
1197
|
+
// would have nothing telling it they're there: `resolveInstalledCore`
|
|
1198
|
+
// falls through to `detectUnrecordedCore`, which only looks for a root
|
|
1199
|
+
// `core.yaml` (the shipped-core workspace marker) and finds nothing for
|
|
1200
|
+
// an adopted repo's `.hedgehog/core.yaml`, so it would read as "no core
|
|
1201
|
+
// yet" and update would silently rewrite `.claude/agents`/`.claude/skills`
|
|
1202
|
+
// down to just the shared payload, deleting the authored package's files
|
|
1203
|
+
// with nothing put back.
|
|
1204
|
+
//
|
|
1205
|
+
// This command is both fixes at once: it fetches the `authored` package
|
|
1206
|
+
// and lands its agents/skills for every host this project already has
|
|
1207
|
+
// installed (never its workspace, template, or vendor_skills — adoption
|
|
1208
|
+
// already wrote its own CLAUDE.md section and root workspace is the one
|
|
1209
|
+
// thing adoption must never touch), then records the core with
|
|
1210
|
+
// `adopted: true` so `update` refreshes it correctly from here on.
|
|
1211
|
+
// Idempotent and safe to re-run — e.g. from a later `hedgehog-adopt` pass
|
|
1212
|
+
// adding new change-work — since it always overwrites from the current
|
|
1213
|
+
// package rather than merging.
|
|
1214
|
+
async function recordAdoptedCommand() {
|
|
1215
|
+
const entry = await resolveCore(ADOPTED_CORE_NAME);
|
|
1216
|
+
if (!entry) {
|
|
1217
|
+
console.error(
|
|
1218
|
+
`${red('authored core not in this release.')} This Hedgehog release's registry has no\n` +
|
|
1219
|
+
`entry named "authored" — nothing to install. Update Hedgehog and retry.\n`,
|
|
1220
|
+
);
|
|
1221
|
+
process.exitCode = 1;
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
let core;
|
|
1226
|
+
try {
|
|
1227
|
+
core = await fetchCore(entry);
|
|
1228
|
+
} catch (err) {
|
|
1229
|
+
console.error(`\n${red(bold('Core unavailable.'))} ${err.message}\n`);
|
|
1230
|
+
process.exitCode = 1;
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const targets = await installedHosts(DEST_ROOT);
|
|
1235
|
+
let written = 0;
|
|
1236
|
+
for (const host of targets) {
|
|
1237
|
+
const h = HOSTS[host];
|
|
1238
|
+
for (const payloadEntry of corePayload(core, h, { hostOnly: true })) {
|
|
1239
|
+
for (const f of await plannedFiles(payloadEntry)) {
|
|
1240
|
+
await writePlannedFile(f);
|
|
1241
|
+
written++;
|
|
1242
|
+
console.log(` ${green('install')} ${relative(DEST_ROOT, f.dest)}`);
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
await recordCore(DEST_ROOT, { name: core.manifest.name, version: core.version, adopted: true });
|
|
1248
|
+
|
|
1249
|
+
console.log(
|
|
1250
|
+
`\n${green(bold('Adopted core recorded.'))} ${dim(
|
|
1251
|
+
`${written} file(s) written for ${targets.map((h) => HOSTS[h].label).join(', ')}`,
|
|
1252
|
+
)}\n`,
|
|
1253
|
+
);
|
|
1254
|
+
console.log(
|
|
1255
|
+
dim(
|
|
1256
|
+
'hedgehog-authored-loop and layer-eng are now on disk and this project is\n' +
|
|
1257
|
+
'recorded as adopted — `hedgehog update` will keep them current alongside\n' +
|
|
1258
|
+
"the shared engine payload, and will never treat this project's core as a\n" +
|
|
1259
|
+
'name it gets to change.\n',
|
|
1260
|
+
),
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1176
1264
|
async function dbRebuildCommand() {
|
|
1177
1265
|
const corePath = await resolveCorePath();
|
|
1178
1266
|
if (!corePath) {
|
|
@@ -3569,6 +3657,21 @@ function engineNote(engine) {
|
|
|
3569
3657
|
return `${engine} ${dim(`— CLI is ${PKG_VERSION}`)}`;
|
|
3570
3658
|
}
|
|
3571
3659
|
|
|
3660
|
+
// `hedgehog core record-adopted` — see recordAdoptedCommand for why this
|
|
3661
|
+
// exists. `cores` (plural) lists the registry; `core` (singular) acts on
|
|
3662
|
+
// this project's own core, so the two don't collide despite the name.
|
|
3663
|
+
async function coreCommand(args) {
|
|
3664
|
+
const sub = args[0];
|
|
3665
|
+
if (sub !== 'record-adopted') {
|
|
3666
|
+
console.error(
|
|
3667
|
+
`${red('Unknown core subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog core record-adopted\n`,
|
|
3668
|
+
);
|
|
3669
|
+
process.exitCode = 1;
|
|
3670
|
+
return;
|
|
3671
|
+
}
|
|
3672
|
+
await recordAdoptedCommand();
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3572
3675
|
// Wraps prose to `width`, indenting every line after the first so it sits
|
|
3573
3676
|
// under the column its label opened.
|
|
3574
3677
|
function wrapProse(text, width, indent) {
|
|
@@ -3675,6 +3778,11 @@ async function main() {
|
|
|
3675
3778
|
return;
|
|
3676
3779
|
}
|
|
3677
3780
|
|
|
3781
|
+
if (cmd === 'core') {
|
|
3782
|
+
await coreCommand(args.slice(1));
|
|
3783
|
+
return;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3678
3786
|
if (cmd === 'update') {
|
|
3679
3787
|
if (args.includes('--check')) {
|
|
3680
3788
|
await updateCheck();
|
package/package.json
CHANGED
package/src/db/plan.mjs
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
//
|
|
6
6
|
// full-stack-app and pwa-app: one task per layer per intent (an intent is
|
|
7
7
|
// a domain module — see the core definition's `{module}` placeholder).
|
|
8
|
-
// landing-page
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// landing-page, deepseek-harness, and authored (whether designed from
|
|
9
|
+
// scratch or adopted onto an existing repo): one task per phase/layer, no
|
|
10
|
+
// module axis. All are the same operation — walk a core definition's layer
|
|
11
|
+
// chain once per intent — because a linear chain is the degenerate case of
|
|
12
|
+
// the layer graph (spec: MVP scope item 5).
|
|
12
13
|
//
|
|
13
14
|
// Layer cardinality: a layer marked `once: true` in the core definition
|
|
14
15
|
// opts out of that per-intent multiplication and compiles a single task
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// Merges a core's CLAUDE.md section into a project's root instructions
|
|
2
|
+
// file, for the one path where that file is not a Hedgehog shell: a
|
|
3
|
+
// repo adopted onto existing, hand-written content that has no
|
|
4
|
+
// {{CORE_SECTION}} placeholder at all.
|
|
5
|
+
//
|
|
6
|
+
// `writePlannedFile` in bin/cli.mjs (the `merge` entry kind) fills
|
|
7
|
+
// {{CORE_SECTION}} by substring replacement, which requires the shell's
|
|
8
|
+
// placeholder to already be present — true for every project `init`
|
|
9
|
+
// ever wrote, since it always starts from src/templates/CLAUDE.md. An
|
|
10
|
+
// adopted repo's CLAUDE.md predates Hedgehog entirely and carries no
|
|
11
|
+
// such marker, so that replacement has nothing to replace. This module
|
|
12
|
+
// is the one place that gap is closed: appending a delimited section
|
|
13
|
+
// instead of substituting into one.
|
|
14
|
+
//
|
|
15
|
+
// Referenced by name (not substance) from hedgehog-adopt's own SKILL.md
|
|
16
|
+
// in the @skyf0xx/hedgehog-core-authored package — that skill invokes
|
|
17
|
+
// `appendCoreSection` the same way it already invokes `loadCore` from
|
|
18
|
+
// src/db/core.mjs, via `node -e "import('<path-to-hedgehog-install>/
|
|
19
|
+
// src/hosts/claude-md-merge.mjs')..."`.
|
|
20
|
+
|
|
21
|
+
const MARKER_START = '<!-- hedgehog:core-section start -->';
|
|
22
|
+
const MARKER_END = '<!-- hedgehog:core-section end -->';
|
|
23
|
+
|
|
24
|
+
// True when `content` already carries a Hedgehog-managed section — from
|
|
25
|
+
// either mechanism: the shell's own {{CORE_SECTION}} placeholder (still
|
|
26
|
+
// unfilled, or already filled by writePlannedFile's substring
|
|
27
|
+
// replacement — filled content stays wrapped in the same markers, so
|
|
28
|
+
// this check still finds it) or this module's own appended, delimited
|
|
29
|
+
// block.
|
|
30
|
+
export function hasCoreSection(content) {
|
|
31
|
+
return content.includes('{{CORE_SECTION}}') || content.includes(MARKER_START);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Wraps `section` (a core's CLAUDE.core.*.md content, verbatim) in the
|
|
35
|
+
// stable markers that make both the substring-replace path
|
|
36
|
+
// (writePlannedFile, bin/cli.mjs) and this append path idempotent and
|
|
37
|
+
// mutually recognizable. Exported so writePlannedFile's {{CORE_SECTION}}
|
|
38
|
+
// substitution wraps its own output the same way, rather than each path
|
|
39
|
+
// hand-rolling the marker text.
|
|
40
|
+
export function wrapSection(section) {
|
|
41
|
+
return `${MARKER_START}\n\n${section.trimEnd()}\n\n${MARKER_END}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Appends `section` to `existingContent` as a clearly delimited block,
|
|
45
|
+
// never touching a byte of what's already there. Idempotent: if a
|
|
46
|
+
// marked section is already present, its content is replaced in place
|
|
47
|
+
// (so a later `hedgehog-adopt` re-run, or a core switch, updates the
|
|
48
|
+
// section without duplicating it) rather than appending a second copy.
|
|
49
|
+
//
|
|
50
|
+
// This is the merge path for a CLAUDE.md hedgehog-adopt's Step 5 finds
|
|
51
|
+
// with no {{CORE_SECTION}} placeholder — the file predates Hedgehog and
|
|
52
|
+
// was never written from src/templates/CLAUDE.md. Every file that shell
|
|
53
|
+
// ever produced already has the placeholder, so that case always goes
|
|
54
|
+
// through writePlannedFile's ordinary substitution instead; this
|
|
55
|
+
// function is never called on that path.
|
|
56
|
+
export function appendCoreSection(existingContent, section) {
|
|
57
|
+
const block = wrapSection(section);
|
|
58
|
+
const markerRe = new RegExp(
|
|
59
|
+
`${escapeRe(MARKER_START)}[\\s\\S]*?${escapeRe(MARKER_END)}`,
|
|
60
|
+
);
|
|
61
|
+
if (markerRe.test(existingContent)) {
|
|
62
|
+
return existingContent.replace(markerRe, block);
|
|
63
|
+
}
|
|
64
|
+
return `${existingContent.trimEnd()}\n\n${block}\n`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function escapeRe(s) {
|
|
68
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
69
|
+
}
|
package/src/registry/cores.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"name": "full-stack-app",
|
|
5
5
|
"flag": "--ts-full-stack-app",
|
|
6
6
|
"package": "@skyf0xx/hedgehog-core-full-stack-app",
|
|
7
|
-
"version": "^1.0.
|
|
7
|
+
"version": "^1.0.6",
|
|
8
8
|
"language": "typescript",
|
|
9
9
|
"repository": "https://github.com/skyf0xx/hedgehog-core-full-stack-app",
|
|
10
10
|
"selects_when": "The description names server-side logic across most of the app: authorization more expressive than per-object row-level security, background jobs, scheduled work or webhook receivers as the app's primary function, server-rendered or SEO-critical pages beyond an app shell, or a data model whose working set doesn't sensibly fit on a device. Persistent domain data alone is not the signal — a tracker, journal, notebook, or planner whose data fits on the user's device is pwa-app, even with sharing, accounts, or multi-device sync in scope, and even with one or two entities that must be server-authoritative. If the project has both a marketing page and a real app behind it, this is still full-stack-app: the page becomes routes inside apps/web, not a separate project."
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "pwa-app",
|
|
14
14
|
"flag": "--pwa-app",
|
|
15
15
|
"package": "@skyf0xx/hedgehog-core-pwa-app",
|
|
16
|
-
"version": "^1.0.
|
|
16
|
+
"version": "^1.0.4",
|
|
17
17
|
"language": "typescript",
|
|
18
18
|
"repository": "https://github.com/skyf0xx/hedgehog-core-pwa-app",
|
|
19
19
|
"selects_when": "The description names an app whose data model fits on the user's device and whose reads and writes are the user's own — a tracker, journal, notebook, planner, offline reference, or utility. Offline capability or installability named explicitly is a strong signal. Sharing, collaboration, accounts, and multi-device sync do NOT disqualify a project: Dexie Cloud provides sync, authentication, and server-enforced per-object access control, so a shared list, a family calendar, or a small team's board is still this core. A small number of entities that must be server-authoritative — a points balance, a reward ledger, anything a client must not write to directly — do NOT disqualify a project either: those entities are declared --remote and backed by Supabase (Postgres + RLS + Edge Functions) behind the same repository interface, while the rest of the app stays local-first. What routes a project to full-stack-app is server-side logic across most of the app, not the presence of one or two such entities: authorization beyond row-level policies, background jobs or webhooks as the app's primary function, server-rendered pages, or a working set too large for a device."
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"name": "landing-page",
|
|
23
23
|
"flag": "--landing-page",
|
|
24
24
|
"package": "@skyf0xx/hedgehog-core-landing-page",
|
|
25
|
-
"version": "^1.0.
|
|
25
|
+
"version": "^1.0.2",
|
|
26
26
|
"language": "typescript",
|
|
27
27
|
"repository": "https://github.com/skyf0xx/hedgehog-core-landing-page",
|
|
28
28
|
"selects_when": "The description is a marketing/announcement/waitlist/portfolio page (or a small handful of such pages) with no persistent domain data of its own. A page that only collects an email into a third-party form service, or has no state at all, qualifies. The bar is \"no domain module\" — a landing page with a dozen sections is still landing-page, not promoted to full-stack-app for being long."
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"name": "deepseek-harness",
|
|
32
32
|
"flag": "--deepseek-harness",
|
|
33
33
|
"package": "@skyf0xx/hedgehog-core-deepseek-harness",
|
|
34
|
-
"version": ">=0.
|
|
34
|
+
"version": ">=0.2.3 <1.0.0",
|
|
35
35
|
"language": "typescript",
|
|
36
36
|
"repository": "https://github.com/skyf0xx/hedgehog-core-deepseek-harness",
|
|
37
37
|
"selects_when": "The description names building a plugin, tool, hook, or extension for DeepSeek Harness (DSH) — a Cordis-based agent framework — or otherwise extending an existing DSH installation with new capabilities via its plugin/bundle system. Concrete signals: DSH, DeepSeek Harness, Cordis, `defineTool`, `ctx.tools.register`, a `cordis.patch.yml` manifest, or a request to add a tool that a DSH agent can call. This is the core most often confused with authored: authored fits when no shipped core matches and the planner must design a system shape from scratch, but a DSH plugin already has a fixed, battle-tested shape (the six-layer scaffold → logic → wiring → smoke → bundle → join sequence, one plugin per intent) that authored's from-scratch design would only reinvent worse. Route here whenever the target of the work is a DSH plugin, not a general \"no other core fits\" fallback."
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
{
|
|
40
40
|
"name": "authored",
|
|
41
41
|
"package": "@skyf0xx/hedgehog-core-authored",
|
|
42
|
-
"version": "^1.0.
|
|
42
|
+
"version": "^1.0.2",
|
|
43
43
|
"language": "typescript",
|
|
44
44
|
"repository": "https://github.com/skyf0xx/hedgehog-core-authored",
|
|
45
45
|
"selects_when": "Neither shipped core fits, but the description names a real artifact a Builder step would produce — just not in either shipped core's shape. This core is designed by the planner rather than chosen from a fixed set: hedgehog-planning-intake's Phase 0 elicits the drivers first, then hedgehog-core-design names the system shape, picks the stack, derives the layers, and writes .hedgehog/core.yaml. It carries the same enforcement as a shipped core — ordered layers, scoped file access, verification before completion — but the sequence is designed for this project rather than battle-tested across many."
|
package/src/registry/index.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// Core package registry. One entry per Hedgehog core — full-stack-app,
|
|
2
|
-
// pwa-app, landing-page, authored — naming the npm
|
|
3
|
-
// agents, skills, and (for the
|
|
4
|
-
// flag `hedgehog init` accepts for it and the prose `planner`
|
|
5
|
-
// in Phase 0 to choose one. A fixed table, one entry per core,
|
|
6
|
-
// by name or flag rather than convention.
|
|
2
|
+
// pwa-app, landing-page, deepseek-harness, and authored — naming the npm
|
|
3
|
+
// package that ships its agents, skills, and (for the first four) scaffold,
|
|
4
|
+
// plus the CLI flag `hedgehog init` accepts for it and the prose `planner`
|
|
5
|
+
// reads aloud in Phase 0 to choose one. A fixed table, one entry per core,
|
|
6
|
+
// discovered by name or flag rather than convention.
|
|
7
7
|
//
|
|
8
8
|
// `authored` carries no flag — it is never selected off a fixed list at
|
|
9
|
-
// install time. hedgehog-core-design chooses it during planning
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// install time. hedgehog-core-design chooses it during planning (from-scratch
|
|
10
|
+
// design path), or hedgehog-adopt brings it to an existing repo (brownfield
|
|
11
|
+
// adoption path), from the drivers hedgehog-planning-intake's Phase 0 elicits
|
|
12
|
+
// or the repo characteristics adoption discovers, not from a value a user
|
|
13
|
+
// passes to `init`.
|
|
12
14
|
|
|
13
15
|
import { readFile } from 'node:fs/promises';
|
|
14
16
|
import { dirname, join } from 'node:path';
|
|
@@ -6,34 +6,62 @@
|
|
|
6
6
|
// install time has nothing recorded until its bootstrap-core skill lands
|
|
7
7
|
// the core — `installedCore` returns null until then, and `update` limits
|
|
8
8
|
// itself to the shared payload.
|
|
9
|
+
//
|
|
10
|
+
// `authored` has a second entry point with no `init` step at all:
|
|
11
|
+
// `hedgehog-adopt` brings the discipline to a repo that already exists by
|
|
12
|
+
// writing `.hedgehog/core.yaml` itself, directly, and never calls
|
|
13
|
+
// `recordCore` (see that skill, shipped in
|
|
14
|
+
// @skyf0xx/hedgehog-core-authored — adoption has no npm package of its
|
|
15
|
+
// own to fetch, just a core.yaml and adoption.md derived from the
|
|
16
|
+
// existing repo). `hedgehog core record-adopted` (bin/cli.mjs) is the
|
|
17
|
+
// record path for that case instead: it lands the authored package's
|
|
18
|
+
// agents/skills — otherwise never installed, since adoption's `init` runs
|
|
19
|
+
// with no `--core` flag and `bootstrap` is skipped entirely for adoption
|
|
20
|
+
// — and calls `recordCore` with `adopted: true`. That flag is the one
|
|
21
|
+
// thing distinguishing an adopted record from a normal one: `update`'s
|
|
22
|
+
// resolveInstalledCore refreshes an adopted project's payload the same as
|
|
23
|
+
// any other (same package, same agents/skills, kept current) but must
|
|
24
|
+
// never treat `record.name` changing upstream, or the record going
|
|
25
|
+
// missing, as license to fetch and install a *different* core the way it
|
|
26
|
+
// would for a project that chose one at `init` — adoption's core is a
|
|
27
|
+
// fixed fact of the repo (its `.hedgehog/core.yaml`), not a choice
|
|
28
|
+
// `update` gets to revisit.
|
|
9
29
|
|
|
10
30
|
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
11
31
|
import { dirname, join } from 'node:path';
|
|
12
32
|
|
|
33
|
+
export const ADOPTED_CORE_NAME = 'authored';
|
|
34
|
+
|
|
13
35
|
const CORE_PATH = '.hedgehog/core.json';
|
|
14
36
|
|
|
15
37
|
/**
|
|
16
38
|
* Record the core a project installed and the exact version resolved for
|
|
17
39
|
* it. Written after the core's files land, so the record describes what
|
|
18
|
-
* is on disk.
|
|
40
|
+
* is on disk. `adopted: true` marks a record written by `hedgehog core
|
|
41
|
+
* record-adopted` rather than by `init` — see the module comment above.
|
|
19
42
|
*/
|
|
20
|
-
export async function recordCore(root, { name, version }) {
|
|
43
|
+
export async function recordCore(root, { name, version, adopted = false }) {
|
|
21
44
|
const path = join(root, CORE_PATH);
|
|
22
45
|
await mkdir(dirname(path), { recursive: true });
|
|
23
46
|
await writeFile(
|
|
24
47
|
path,
|
|
25
|
-
`${JSON.stringify(
|
|
48
|
+
`${JSON.stringify(
|
|
49
|
+
{ core: name, version, installedAt: new Date().toISOString(), ...(adopted ? { adopted: true } : {}) },
|
|
50
|
+
null,
|
|
51
|
+
2,
|
|
52
|
+
)}\n`,
|
|
26
53
|
);
|
|
27
54
|
}
|
|
28
55
|
|
|
29
56
|
/**
|
|
30
|
-
* The core `update` should refresh — `{ name, version }` — or
|
|
31
|
-
* this project has no core installed yet.
|
|
57
|
+
* The core `update` should refresh — `{ name, version, adopted }` — or
|
|
58
|
+
* null when this project has no core installed yet. `adopted` is always a
|
|
59
|
+
* boolean, `false` for every record `init` writes.
|
|
32
60
|
*/
|
|
33
61
|
export async function installedCore(root) {
|
|
34
62
|
try {
|
|
35
|
-
const { core, version } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
|
|
36
|
-
return core ? { name: core, version } : null;
|
|
63
|
+
const { core, version, adopted } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
|
|
64
|
+
return core ? { name: core, version, adopted: adopted === true } : null;
|
|
37
65
|
} catch {
|
|
38
66
|
return null;
|
|
39
67
|
}
|