@skyf0xx/hedgehog 6.0.4 → 6.1.0

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 CHANGED
@@ -93,7 +93,7 @@ Every dependency is explicit, so Hedgehog knows which tasks can run in parallel.
93
93
 
94
94
  Agents fan out to give you great outcomes at **faster speeds**.
95
95
 
96
- ## Your Code is a Graph
96
+ ## Live Dependency Awareness
97
97
 
98
98
  Hedgehog reaches for your editor's own Language Server Protocol integration to see what depends on what.
99
99
 
@@ -104,10 +104,9 @@ flowchart TD
104
104
  C --> D[Hedgehog]
105
105
  ```
106
106
 
107
- - **No index to go stale**: the language server's view of the code is always current
108
- - **No time spent hunting through the codebase for context**: find references and callers directly
109
- - **No more surprise breakage**: every task can check what depends on it before it edits
110
- - **Tests cover what changed**: verification that misses affected code gets caught
107
+ - **Lower token cost**: a targeted symbol lookup replaces reading or grepping whole files for context
108
+ - **Impact known before editing**: every task checks what depends on it before it changes anything
109
+ - **Verification matches the real surface**: tests target what actually changed
111
110
 
112
111
  ## Deterministic Code Generation
113
112
 
package/README.zh-CN.md CHANGED
@@ -93,7 +93,7 @@ npx @skyf0xx/hedgehog graph # 显示构建图
93
93
 
94
94
  多个 agent 并行展开工作,以**更快的速度**交付出色的结果。
95
95
 
96
- ## 你的代码是一张图
96
+ ## 实时感知依赖关系
97
97
 
98
98
  Hedgehog 借助你编辑器自带的 Language Server Protocol 集成来查看代码间的依赖关系。
99
99
 
@@ -104,10 +104,9 @@ flowchart TD
104
104
  C --> D[Hedgehog]
105
105
  ```
106
106
 
107
- - **索引永不过期**:语言服务器看到的代码状态始终是最新的
108
- - **不必在代码库中翻找上下文**:直接查找引用和调用方
109
- - **不再意外改坏**:每个任务在动手前都能查一查有哪些代码依赖它
110
- - **测试覆盖改动**:遗漏受影响代码的验证会被发现
107
+ - **更低的 token 成本**:精准的符号查找取代了为获取上下文而阅读或搜索整个文件
108
+ - **改动前先知影响**:每个任务在动手前都能查清有哪些代码依赖它
109
+ - **验证匹配真实改动范围**:测试针对实际发生变化的部分运行
111
110
 
112
111
  ## 确定性代码生成
113
112
 
package/bin/cli.mjs CHANGED
@@ -53,6 +53,7 @@ import {
53
53
  import { whyPath, formatWhy } from '../src/db/why.mjs';
54
54
  import { addFriction, listFriction } from '../src/db/friction.mjs';
55
55
  import { addDebt, listDebt } from '../src/db/debt.mjs';
56
+ import { addDecision, listDecisions } from '../src/db/decision.mjs';
56
57
  import {
57
58
  shouldPromptForStar,
58
59
  recordStarAnswer,
@@ -280,19 +281,6 @@ function corePayload(core, h, { hostOnly = false } = {}) {
280
281
  // than generating it live. A core that scaffolds nothing (its
281
282
  // workspace is designed during planning) declares no `workspace`.
282
283
  ...(manifest.workspace ? [{ type: 'dir', root, from: manifest.workspace, to: '.' }] : []),
283
- // A second CLAUDE.md section for cores whose path fills
284
- // {{CORE_SECTION}} after install rather than at it — adoption reads
285
- // it from the project instead of the package it ships in.
286
- ...(manifest.template_adopted
287
- ? [
288
- {
289
- type: 'file',
290
- root,
291
- from: manifest.template_adopted,
292
- to: `.hedgehog/${manifest.template_adopted}`,
293
- },
294
- ]
295
- : []),
296
284
  ];
297
285
  }
298
286
 
@@ -397,19 +385,19 @@ function warnRebuildDrift({ drift }, corePath) {
397
385
  );
398
386
  }
399
387
 
400
- // Debt and friction notes are operator-recorded and have no committed
401
- // source, so a rebuild carries them across by task id. A note whose task
402
- // is no longer in the recompiled graph — its intent file was renamed,
403
- // deleted, or its layer sequence changed — has nowhere to re-attach.
404
- // Friction notes survive unattached (their task_id is nullable); debt
405
- // notes are lost, so both are printed with their text rather than
406
- // disappearing into a count.
388
+ // Debt, decision, and friction notes are operator- or agent-recorded and
389
+ // have no committed source, so a rebuild carries them across by task id.
390
+ // A note whose task is no longer in the recompiled graph — its intent
391
+ // file was renamed, deleted, or its layer sequence changed — has nowhere
392
+ // to re-attach. Friction notes survive unattached (their task_id is
393
+ // nullable); debt and decision notes are lost, so all three are printed
394
+ // with their text rather than disappearing into a count.
407
395
  function warnOrphanedNotes({ orphanedNotes }) {
408
396
  if (!orphanedNotes || orphanedNotes.length === 0) return;
409
397
  console.log(
410
398
  `${yellow(bold('Notes without a task after rebuild.'))} ${orphanedNotes.length} note(s) referenced a\n` +
411
399
  'task the recompiled graph no longer holds. Friction notes were kept unattached;\n' +
412
- 'debt notes could not be, so they are reproduced here:\n',
400
+ 'debt and decision notes could not be, so they are reproduced here:\n',
413
401
  );
414
402
  for (const note of orphanedNotes) {
415
403
  console.log(` ${dim(note.kind)} ${bold(note.taskId)} ${note.note}`);
@@ -525,7 +513,7 @@ ${bold('Usage')}
525
513
  npx @skyf0xx/hedgehog init --pwa-app scaffold the pwa-app core now
526
514
  npx @skyf0xx/hedgehog init --landing-page scaffold the landing-page core now
527
515
  npx @skyf0xx/hedgehog cores list every core this release can install
528
- npx @skyf0xx/hedgehog core record-adopted land the authored core's agents/skills and record
516
+ npx @skyf0xx/hedgehog core record-adopted land the adopted core's agents/skills and record
529
517
  this project as adopted (hedgehog-adopt calls this
530
518
  after writing .hedgehog/core.yaml; not for other cores)
531
519
  npx @skyf0xx/hedgehog init --cursor install for Cursor (default: Claude Code)
@@ -573,6 +561,9 @@ ${bold('Usage')}
573
561
  npx @skyf0xx/hedgehog friction list list logged friction, oldest first
574
562
  npx @skyf0xx/hedgehog debt add <task-id> "<note>" declare debt that lands in dependent tasks' packets
575
563
  npx @skyf0xx/hedgehog debt list [<task-id>] list declared debt, oldest first
564
+ npx @skyf0xx/hedgehog decision add <task-id> "<note>" declare a decision that lands in dependent tasks' packets
565
+ npx @skyf0xx/hedgehog decision list [<task-id>] list declared decisions, oldest first
566
+ npx @skyf0xx/hedgehog db migrate bring the graph's schema up to the latest version
576
567
  npx @skyf0xx/hedgehog community star --answer <a> record the star prompt's answer
577
568
  npx @skyf0xx/hedgehog --help
578
569
 
@@ -1042,10 +1033,10 @@ async function resolveInstalledCore() {
1042
1033
  }
1043
1034
 
1044
1035
  // `hedgehog core record-adopted` — the record path for `hedgehog-adopt`
1045
- // (shipped in @skyf0xx/hedgehog-core-authored), which brings the
1036
+ // (shipped in @skyf0xx/hedgehog-core-adopted), which brings the
1046
1037
  // discipline to an existing repo by writing `.hedgehog/core.yaml` and
1047
1038
  // `.hedgehog/adoption.md` directly. That path has no `init` step and so
1048
- // never fetches the `authored` package or calls `recordCore` — a no-flag
1039
+ // never fetches the `adopted` package or calls `recordCore` — a no-flag
1049
1040
  // `init` (what the offer skill actually runs before adoption) installs
1050
1041
  // only the shared engine payload, never a core's own agents/skills, and
1051
1042
  // `bootstrap` (the only other place a core package gets fetched) is
@@ -1057,24 +1048,27 @@ async function resolveInstalledCore() {
1057
1048
  // `core.yaml` (the shipped-core workspace marker) and finds nothing for
1058
1049
  // an adopted repo's `.hedgehog/core.yaml`, so it would read as "no core
1059
1050
  // yet" and update would silently rewrite `.claude/agents`/`.claude/skills`
1060
- // down to just the shared payload, deleting the authored package's files
1051
+ // down to just the shared payload, deleting the adopted package's files
1061
1052
  // with nothing put back.
1062
1053
  //
1063
- // This command is both fixes at once: it fetches the `authored` package
1054
+ // This command is both fixes at once: it fetches the `adopted` package
1064
1055
  // and lands its agents/skills for every host this project already has
1065
- // installed (never its workspace, template, or vendor_skills — adoption
1066
- // already wrote its own CLAUDE.md section and root workspace is the one
1067
- // thing adoption must never touch), then records the core with
1068
- // `adopted: true` so `update` refreshes it correctly from here on.
1069
- // Idempotent and safe to re-run e.g. from a later `hedgehog-adopt` pass
1070
- // adding new change-work since it always overwrites from the current
1071
- // package rather than merging.
1056
+ // installed (never its workspace or vendor_skills — root workspace is
1057
+ // the one thing adoption must never touch), plus a local copy of its
1058
+ // `CLAUDE.core.md` at `.hedgehog/CLAUDE.core.md` `hedgehog-adopt`'s own
1059
+ // Confirm & Lock step reads that file from there to merge into root
1060
+ // `CLAUDE.md`, since a no-flag `init` never ran the normal
1061
+ // `{{CORE_SECTION}}` merge this project's core would otherwise get then
1062
+ // records the core the same as any other, naming `adopted`, so `update`
1063
+ // refreshes it correctly from here on. Idempotent and safe to re-run —
1064
+ // e.g. from a later `hedgehog-adopt` pass adding new change-work — since
1065
+ // it always overwrites from the current package rather than merging.
1072
1066
  async function recordAdoptedCommand() {
1073
1067
  const entry = await resolveCore(ADOPTED_CORE_NAME);
1074
1068
  if (!entry) {
1075
1069
  console.error(
1076
- `${red('authored core not in this release.')} This Hedgehog release's registry has no\n` +
1077
- `entry named "authored" — nothing to install. Update Hedgehog and retry.\n`,
1070
+ `${red('adopted core not in this release.')} This Hedgehog release's registry has no\n` +
1071
+ `entry named "adopted" — nothing to install. Update Hedgehog and retry.\n`,
1078
1072
  );
1079
1073
  process.exitCode = 1;
1080
1074
  return;
@@ -1102,7 +1096,19 @@ async function recordAdoptedCommand() {
1102
1096
  }
1103
1097
  }
1104
1098
 
1105
- await recordCore(DEST_ROOT, { name: core.manifest.name, version: core.version, adopted: true });
1099
+ const templateFile = (
1100
+ await plannedFiles({
1101
+ type: 'file',
1102
+ root: core.root,
1103
+ from: core.manifest.template,
1104
+ to: '.hedgehog/CLAUDE.core.md',
1105
+ })
1106
+ )[0];
1107
+ await writePlannedFile(templateFile);
1108
+ written++;
1109
+ console.log(` ${green('install')} ${relative(DEST_ROOT, templateFile.dest)}`);
1110
+
1111
+ await recordCore(DEST_ROOT, { name: core.manifest.name, version: core.version });
1106
1112
 
1107
1113
  console.log(
1108
1114
  `\n${green(bold('Adopted core recorded.'))} ${dim(
@@ -1155,9 +1161,13 @@ async function dbCommand(args) {
1155
1161
  await dbRebuildCommand();
1156
1162
  return;
1157
1163
  }
1164
+ if (sub === 'migrate') {
1165
+ await dbMigrateCommand();
1166
+ return;
1167
+ }
1158
1168
  if (sub !== 'init') {
1159
1169
  console.error(
1160
- `${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n or: hedgehog db rebuild\n`,
1170
+ `${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n or: hedgehog db rebuild\n or: hedgehog db migrate\n`,
1161
1171
  );
1162
1172
  process.exitCode = 1;
1163
1173
  return;
@@ -1179,6 +1189,44 @@ async function dbCommand(args) {
1179
1189
  }
1180
1190
  }
1181
1191
 
1192
+ // `hedgehog db migrate` — brings an existing build graph's schema up to
1193
+ // CURRENT_SCHEMA_VERSION on demand (see schema.mjs's runMigrations),
1194
+ // rather than only as a side effect of the next command that happens to
1195
+ // open the graph writably. Reports what moved, since an upgrade
1196
+ // shouldn't be a silent side effect the first time some other command
1197
+ // happens to trigger it.
1198
+ async function dbMigrateCommand() {
1199
+ if (!(await exists(DB_PATH))) {
1200
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
1201
+ process.exitCode = 1;
1202
+ return;
1203
+ }
1204
+
1205
+ const before = openDb({ readOnly: true });
1206
+ const { user_version: fromVersion } = before.prepare('PRAGMA user_version').get();
1207
+ before.close();
1208
+
1209
+ let toVersion;
1210
+ try {
1211
+ const db = openDb();
1212
+ try {
1213
+ ({ user_version: toVersion } = db.prepare('PRAGMA user_version').get());
1214
+ } finally {
1215
+ db.close();
1216
+ }
1217
+ } catch (err) {
1218
+ console.error(`${red(err.message)}\n`);
1219
+ process.exitCode = 1;
1220
+ return;
1221
+ }
1222
+
1223
+ if (fromVersion === toVersion) {
1224
+ console.log(`${dim(`Build graph is already at the latest schema (v${toVersion}). Nothing to do.`)}\n`);
1225
+ return;
1226
+ }
1227
+ console.log(`${green('Migrated')} build graph from schema v${fromVersion} to v${toVersion}.\n`);
1228
+ }
1229
+
1182
1230
  // Resolves the project's core definition: an authored .hedgehog/core.yaml
1183
1231
  // takes precedence (spec: "Authored cores"); otherwise the core's
1184
1232
  // own core.yaml, which lands at repo root along with the rest of that
@@ -3095,6 +3143,77 @@ async function debtCommand(args) {
3095
3143
  process.exitCode = 1;
3096
3144
  }
3097
3145
 
3146
+ // `hedgehog decision add <task-id> "<note>"` / `hedgehog decision list
3147
+ // [<task-id>]` — declared decisions between tasks. A note recorded
3148
+ // against a task is rendered into the INHERITED DECISIONS section of the
3149
+ // packet of every task that depends on it (see src/db/decision.mjs and
3150
+ // src/db/next.mjs).
3151
+ async function decisionCommand(args) {
3152
+ await ensureDb();
3153
+
3154
+ const sub = args[0];
3155
+
3156
+ if (!(await exists(DB_PATH))) {
3157
+ console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
3158
+ process.exitCode = 1;
3159
+ return;
3160
+ }
3161
+
3162
+ if (sub === 'add') {
3163
+ const taskId = args[1];
3164
+ const note = args.slice(2).join(' ');
3165
+ if (!taskId || !note) {
3166
+ console.error(`${red('Usage:')} hedgehog decision add <task-id> "<note>"\n`);
3167
+ process.exitCode = 1;
3168
+ return;
3169
+ }
3170
+
3171
+ const db = openDb();
3172
+ let entry;
3173
+ try {
3174
+ entry = addDecision(db, { taskId, note });
3175
+ } catch (err) {
3176
+ console.error(
3177
+ `${red('Failed to declare decision:')} ${err.message}\n\nRun ${bold('hedgehog status')} to see valid task ids.\n`,
3178
+ );
3179
+ process.exitCode = 1;
3180
+ return;
3181
+ } finally {
3182
+ db.close();
3183
+ }
3184
+
3185
+ console.log(` ${green('declared')} #${entry.id} ${bold(entry.taskId)}`);
3186
+ console.log(` ${dim('reaches the packet of every task depending on it')}`);
3187
+ return;
3188
+ }
3189
+
3190
+ if (sub === 'list') {
3191
+ const taskId = args[1];
3192
+ const db = openDb();
3193
+ let entries;
3194
+ try {
3195
+ entries = listDecisions(db, taskId);
3196
+ } finally {
3197
+ db.close();
3198
+ }
3199
+
3200
+ if (entries.length === 0) {
3201
+ console.log(`${dim('No decisions declared.')}\n`);
3202
+ return;
3203
+ }
3204
+ for (const entry of entries) {
3205
+ console.log(`#${entry.id} ${dim(entry.loggedAt)} ${bold(entry.taskId)}`);
3206
+ console.log(` ${entry.note}\n`);
3207
+ }
3208
+ return;
3209
+ }
3210
+
3211
+ console.error(
3212
+ `${red('Unknown decision subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog decision add <task-id> "<note>"\n or: hedgehog decision list [<task-id>]\n`,
3213
+ );
3214
+ process.exitCode = 1;
3215
+ }
3216
+
3098
3217
  // `hedgehog community star --answer starred|later|dismissed` — records
3099
3218
  // the star prompt's answer. No build graph or core needed: this is
3100
3219
  // project state about a question asked, not about the build.
@@ -3400,6 +3519,11 @@ async function main() {
3400
3519
  return;
3401
3520
  }
3402
3521
 
3522
+ if (cmd === 'decision') {
3523
+ await decisionCommand(args.slice(1));
3524
+ return;
3525
+ }
3526
+
3403
3527
  if (cmd === 'community') {
3404
3528
  await communityCommand(args.slice(1));
3405
3529
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "6.0.4",
3
+ "version": "6.1.0",
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": {
@@ -31,7 +31,8 @@ You run on two paths, and Workflow step 2 decides which:
31
31
  scope into additional intents without re-running the BMAD shelf.
32
32
  Landing-page has no module axis, so this path doesn't apply to it — see
33
33
  the landing-page constraint below for where its new scope actually
34
- goes.
34
+ goes. New change-work on an **adopted** core is a separate case again —
35
+ see "An existing repo, ongoing adoption" below.
35
36
 
36
37
  Either path is entered when the user says "plan", "scope", "break down",
37
38
  asks for something that's new scope rather than a tweak (routed here by
@@ -200,14 +201,14 @@ none of them is a description matching a `when` paragraph:
200
201
  repo", "add Hedgehog to my existing project", "I want scope/verify
201
202
  enforcement on my changes here"). This is a distinct question from
202
203
  everything above: it's not about which core fits new work, because no
203
- new workspace gets built at all. Route straight to `hedgehog-adopt`
204
- bootstrap and every other Phase 0 outcome are skipped entirely, since
205
- there is no workspace to scaffold and no shipped stack to adopt toward.
206
- `hedgehog-adopt` runs its own read-only intake and writes its own
207
- `.hedgehog/core.yaml`; don't run `hedgehog-planning-intake`'s BMAD shelf
208
- first the drivers that skill elicits (persistence, stack, deployment
209
- target) are already settled facts of the existing repo, not open
210
- decisions.
204
+ new workspace gets built at all. This project gets the **adopted
205
+ core**. Route straight to `hedgehog-adopt` bootstrap and every other
206
+ Phase 0 outcome are skipped entirely, since there is no workspace to
207
+ scaffold and no shipped stack to adopt toward. `hedgehog-adopt` runs
208
+ its own read-only intake and writes its own `.hedgehog/core.yaml`;
209
+ don't run `hedgehog-planning-intake`'s BMAD shelf first the drivers
210
+ that skill elicits (persistence, stack, deployment target) are already
211
+ settled facts of the existing repo, not open decisions.
211
212
 
212
213
  State the decision plainly before Phase 1 begins, with the one-line
213
214
  reason it landed there — this is cheap to correct now and expensive once
@@ -375,7 +376,7 @@ as full-stack-app's Auth/Queue/Mobile trio.
375
376
  the build graph.
376
377
  - **landing-page**: owns `.hedgehog/BMAD/` and
377
378
  `.hedgehog/chain/00-brief.md` as artifacts.
378
- - **brownfield adoption**: owns nothing here — `hedgehog-adopt` owns
379
+ - **adopted**: owns nothing here — `hedgehog-adopt` owns
379
380
  `.hedgehog/core.yaml` and `.hedgehog/adoption.md`, the same way an
380
381
  authored core's design is `hedgehog-core-design`'s.
381
382
 
@@ -30,7 +30,8 @@ Everything the commit gate already enforces — the layer's own `verify`
30
30
  command, and whatever typecheck/lint/test it runs — is out of scope;
31
31
  don't re-report a green gate. Read the core's own design first: its loop
32
32
  skill for a shipped core, `.hedgehog/core.yaml` and
33
- `.hedgehog/core-design.md` for an authored one. That is where the layer
33
+ `.hedgehog/core-design.md` for an authored one, `.hedgehog/core.yaml` and
34
+ `.hedgehog/adoption.md` for an adopted one. That is where the layer
34
35
  boundaries, the interface between them, and this core's own conventions
35
36
  are stated. Your checklist is derived from it, not from a stack you
36
37
  recognize.
@@ -96,7 +97,7 @@ Check what the gate structurally cannot:
96
97
  - Don't nitpick style. Focus on structural correctness relative to the
97
98
  stack and build order the core's own design fixed — its loop and
98
99
  bootstrap skills on a shipped core, `.hedgehog/core-design.md` on an
99
- authored one.
100
+ authored one, `.hedgehog/adoption.md` on an adopted one.
100
101
  - 3 real findings beats 20 suggestions. This review sits at a phase or
101
102
  layer boundary, not mid-Loop — don't slow the Loop down for anything
102
103
  that isn't load-bearing for the work that comes next.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tweaker
3
- description: Use once a core's build is complete (every task in the build graph `complete`) and the user is offered a fresh-context session to iterate. Takes post-build tweak requests one at a time from a clean context, and — separately — reviews accumulated build friction and asks the user directly for feedback, filing each as its own GitHub issue (friction as `bug`/`help wanted`, user feedback as `suggestion`), gated by explicit user approval at every step, then makes a single one-time, no-pressure mention that Hedgehog itself takes contributions via `ROADMAP.md`. Shared by every core with a Stop Condition — not an adopted repo (`hedgehog-adopt`), which has none; there, new change-work goes straight through `hedgehog-adopt` and `hedgehog-authored-loop` instead.
3
+ description: Use once a core's build is complete (every task in the build graph `complete`) and the user is offered a fresh-context session to iterate. Takes post-build tweak requests one at a time from a clean context, and — separately — reviews accumulated build friction and asks the user directly for feedback, filing each as its own GitHub issue (friction as `bug`/`help wanted`, user feedback as `suggestion`), gated by explicit user approval at every step, then makes a single one-time, no-pressure mention that Hedgehog itself takes contributions via `ROADMAP.md`. Shared by every core with a Stop Condition — not the `adopted` core, which has none; there, new change-work goes straight through `hedgehog-adopt` and `hedgehog-authored-loop` instead.
4
4
  model: sonnet
5
5
  color: green
6
6
  tools: Read, Glob, Grep, Edit, Write, Bash
@@ -15,7 +15,7 @@ conversation. You start from a cleared context on purpose. Re-read the
15
15
  friction log (`hedgehog friction list`) and the commit log rather than
16
16
  expecting anything to be remembered.
17
17
 
18
- **Not for an adopted repo (`.hedgehog/core.yaml` written by
18
+ **Not for the `adopted` core (`.hedgehog/core.yaml` written by
19
19
  `hedgehog-adopt`).** That core has no Stop Condition and no "build
20
20
  finished" moment for you to follow — adoption is the permanent way
21
21
  change lands, not a project with an end. A request there is just the
@@ -44,8 +44,8 @@ straight to job 1.
44
44
 
45
45
  None of its own — you work inside whichever core's stack is already
46
46
  installed (a shipped core's, or the stack an authored core's
47
- `.hedgehog/core-design.md` names — an adopted repo never reaches you, per
48
- the note above), editing the same files the core's own build agents
47
+ `.hedgehog/core-design.md` names — the `adopted` core never reaches you,
48
+ per the note above), editing the same files the core's own build agents
49
49
  would. `gh` (GitHub CLI) for issue creation only, and only against
50
50
  `skyf0xx/hedgehog`, never the project's own remote.
51
51
 
package/src/db/debt.mjs CHANGED
@@ -14,9 +14,9 @@
14
14
  // Debt is in-build traffic between two tasks, and a file under
15
15
  // `.hedgehog/` written mid-task sits outside every task's scope globs —
16
16
  // it would trip verify's scope gate on the very task that declared it.
17
- // The consequence is that debt does not survive `hedgehog db rebuild`
18
- // (which replays only `.hedgehog/intents/*.json`); by then the
19
- // inheriting task has usually already consumed it.
17
+ // Debt has no committed source `hedgehog db rebuild` could replay it
18
+ // from, so rebuild.mjs carries it across a rebuild by task id instead
19
+ // (a note whose task no longer exists there is reported, not dropped).
20
20
 
21
21
  import { applySchema } from './schema.mjs';
22
22
 
@@ -0,0 +1,64 @@
1
+ // `hedgehog decision add` / `hedgehog decision list` — declared decisions
2
+ // between tasks. See schema.mjs's `decisions` table and next.mjs's
3
+ // INHERITED DECISIONS packet section.
4
+ //
5
+ // A layer that chose a pattern, a library, or a trade-off while building
6
+ // has no way to tell the layer that inherits from it *why* — only debt.mjs
7
+ // exists for that, and debt is specifically a known limitation, not a
8
+ // decision that was made correctly and simply needs to be known. A
9
+ // "we chose X because Y" comment in a source file is not a mechanism: the
10
+ // inheriting task's packet is assembled from the graph, not from reading
11
+ // its dependencies' comments, so the note never arrives. `decision add`
12
+ // records the note against the declaring task, and next.mjs renders it
13
+ // into the packet of every task that depends on it.
14
+ //
15
+ // Same as debt: nothing is written to a committed markdown log, so a
16
+ // decision has no source `hedgehog db rebuild` could replay it from —
17
+ // rebuild.mjs carries debt, decisions, and friction across a rebuild by
18
+ // task id instead, for exactly this reason.
19
+
20
+ import { applySchema } from './schema.mjs';
21
+
22
+ const insertDecision = (db) =>
23
+ db.prepare(`
24
+ INSERT INTO decisions (task_id, note)
25
+ VALUES (?, ?)
26
+ `);
27
+
28
+ function taskExists(db, taskId) {
29
+ return db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(taskId) !== undefined;
30
+ }
31
+
32
+ // Writes one decision row against `taskId`. The task must exist — a
33
+ // decision addressed to nobody reaches nobody, and the schema's foreign
34
+ // key would reject it anyway, less legibly.
35
+ export function addDecision(db, { taskId, note }) {
36
+ // Idempotent, and the migration path for a build graph created before
37
+ // the `decisions` table existed: dbInit only applies the schema to a DB
38
+ // it just created, so an in-flight project's DB would otherwise have no
39
+ // table to insert into.
40
+ applySchema(db);
41
+
42
+ if (!taskId) throw new Error('decision requires a task id');
43
+ if (!note) throw new Error('decision requires a note');
44
+ if (!taskExists(db, taskId)) throw new Error(`no such task: ${taskId}`);
45
+
46
+ const result = insertDecision(db).run(taskId, note);
47
+ return { id: Number(result.lastInsertRowid), taskId, note };
48
+ }
49
+
50
+ // Every decision row, oldest first, optionally narrowed to one task.
51
+ export function listDecisions(db, taskId) {
52
+ const where = taskId ? 'WHERE task_id = ?' : '';
53
+ const params = taskId ? [taskId] : [];
54
+ try {
55
+ return db
56
+ .prepare(
57
+ `SELECT id, task_id AS taskId, note, logged_at AS loggedAt FROM decisions ${where} ORDER BY id ASC`,
58
+ )
59
+ .all(...params);
60
+ } catch {
61
+ // No `decisions` table yet (a build graph from before this table existed).
62
+ return [];
63
+ }
64
+ }
package/src/db/init.mjs CHANGED
@@ -24,12 +24,24 @@ export const dbAbsPath = (root = process.cwd()) => resolve(root, DB_PATH);
24
24
  // a call site that forgot them. journal_mode is skipped for read-only
25
25
  // handles — it requires write access and a readOnly connection has no
26
26
  // business changing the file's journal mode anyway.
27
+ //
28
+ // applySchema also runs here, not only from `dbInit`: a graph created by
29
+ // an older CLI version is missing whatever tables/columns shipped since,
30
+ // and every command besides `init`/`update` opens the graph straight
31
+ // from here rather than going through dbInit first. Without this, those
32
+ // commands only see the fix after someone thinks to rerun `init` on an
33
+ // already-initialized project — which nothing prompts them to do — and
34
+ // until then every query naming a newer column fails with "no such
35
+ // column". Skipped for read-only handles for the same reason
36
+ // journal_mode is: it requires write access, and a readOnly caller is
37
+ // only ever reached after a writable open earlier in the same command.
27
38
  export function openDb({ readOnly = false } = {}) {
28
39
  const db = new DatabaseSync(dbAbsPath(), { readOnly });
29
40
  db.exec('PRAGMA foreign_keys = ON');
30
41
  db.exec('PRAGMA busy_timeout = 10000');
31
42
  if (!readOnly) db.exec('PRAGMA journal_mode = WAL');
32
43
  db.exec('PRAGMA synchronous = NORMAL');
44
+ if (!readOnly) applySchema(db);
33
45
  return db;
34
46
  }
35
47
 
package/src/db/next.mjs CHANGED
@@ -113,6 +113,24 @@ function loadInheritedDebt(db, taskId) {
113
113
  }
114
114
  }
115
115
 
116
+ // Decisions declared by anything this task inherits from (see
117
+ // decision.mjs) — same upstream walk as loadInheritedDebt, same tolerance
118
+ // for a `decisions` table that doesn't exist yet on an older graph.
119
+ function loadInheritedDecisions(db, taskId) {
120
+ const upstream = loadUpstreamTaskIds(db, taskId);
121
+ if (upstream.length === 0) return [];
122
+ const placeholders = upstream.map(() => '?').join(',');
123
+ try {
124
+ return db
125
+ .prepare(
126
+ `SELECT task_id AS taskId, note FROM decisions WHERE task_id IN (${placeholders}) ORDER BY id ASC`,
127
+ )
128
+ .all(...upstream);
129
+ } catch {
130
+ return [];
131
+ }
132
+ }
133
+
116
134
  function loadDirectDependents(db, taskId) {
117
135
  return db
118
136
  .prepare(
@@ -162,6 +180,7 @@ function assemblePacket(db, task) {
162
180
  const dependents = loadBlockedDownstream(db, task.id);
163
181
  const incompleteDeps = incompleteDependencies(db, task.id);
164
182
  const inheritedDebt = loadInheritedDebt(db, task.id);
183
+ const inheritedDecisions = loadInheritedDecisions(db, task.id);
165
184
 
166
185
  return {
167
186
  task,
@@ -170,6 +189,7 @@ function assemblePacket(db, task) {
170
189
  dependents,
171
190
  incompleteDeps,
172
191
  inheritedDebt,
192
+ inheritedDecisions,
173
193
  };
174
194
  }
175
195
 
@@ -344,8 +364,8 @@ const HONESTY = [
344
364
  ];
345
365
 
346
366
  // Renders a packet into the STATUS / INTENT / RELEVANT RULES /
347
- // INHERITED DEBT / WHY NOW / BLOCKED DOWNSTREAM / ALLOWED SCOPE /
348
- // PRE-READ / LAYER SHAPE / VERIFICATION / HONESTY format. The spec
367
+ // INHERITED DEBT / INHERITED DECISIONS / WHY NOW / BLOCKED DOWNSTREAM /
368
+ // ALLOWED SCOPE / PRE-READ / LAYER SHAPE / VERIFICATION / HONESTY format. The spec
349
369
  // splits this across two examples — the `hedgehog next` display and "The
350
370
  // task packet" (which carries the intent and its rules) — but an agent
351
371
  // receives one thing, so the packet is one thing: everything the worker
@@ -456,7 +476,15 @@ function firstArrivalLines(task, roots) {
456
476
  }
457
477
 
458
478
  export function formatPacket(packet, statusLine, coreId = null, exists = null) {
459
- const { task, intent, requirements, dependents, incompleteDeps = [], inheritedDebt = [] } = packet;
479
+ const {
480
+ task,
481
+ intent,
482
+ requirements,
483
+ dependents,
484
+ incompleteDeps = [],
485
+ inheritedDebt = [],
486
+ inheritedDecisions = [],
487
+ } = packet;
460
488
  const scopeGlobs = JSON.parse(task.scope_globs);
461
489
  const firstArrival = firstArrivalPackages(task, exists);
462
490
 
@@ -494,6 +522,15 @@ export function formatPacket(packet, statusLine, coreId = null, exists = null) {
494
522
  }
495
523
  }
496
524
  lines.push('');
525
+ lines.push('INHERITED DECISIONS');
526
+ if (inheritedDecisions.length === 0) {
527
+ lines.push(' (none declared)');
528
+ } else {
529
+ for (const entry of inheritedDecisions) {
530
+ lines.push(` * ${entry.taskId} ${entry.note}`);
531
+ }
532
+ }
533
+ lines.push('');
497
534
  lines.push('WHY NOW');
498
535
  lines.push(` ✓ Intent "${intent.id}" compiled into the graph`);
499
536
  // A once: true layer has no module — it compiled one task for the whole
@@ -53,14 +53,15 @@ function intentExists(db, id) {
53
53
  //
54
54
  // Deleting `intents` cascades through requirements, tasks,
55
55
  // task_requirements, dependencies, artifacts and verifications — every
56
- // one of which this run re-derives. `debt` and `friction` are the
57
- // exception: they are operator-recorded notes with no committed source,
58
- // so they are carried across by task id (deterministic, so a note
59
- // re-attaches to the same task the replay recompiles). A note whose task
60
- // no longer exists in the new graph has nowhere to live and is reported
61
- // rather than silently dropped.
56
+ // one of which this run re-derives. `debt`, `decisions`, and `friction`
57
+ // are the exception: they are operator- or agent-recorded notes with no
58
+ // committed source, so they are carried across by task id (deterministic,
59
+ // so a note re-attaches to the same task the replay recompiles). A note
60
+ // whose task no longer exists in the new graph has nowhere to live and is
61
+ // reported rather than silently dropped.
62
62
  function clearDerivedGraph(db) {
63
63
  const debt = db.prepare('SELECT task_id, note, logged_at FROM debt').all();
64
+ const decisions = db.prepare('SELECT task_id, note, logged_at FROM decisions').all();
64
65
  const friction = db.prepare('SELECT task_id, note, logged_at FROM friction').all();
65
66
 
66
67
  db.prepare('DELETE FROM intents').run();
@@ -69,14 +70,17 @@ function clearDerivedGraph(db) {
69
70
  // the single writer, so a note is not duplicated against its own copy.
70
71
  db.prepare('DELETE FROM friction').run();
71
72
 
72
- return { debt, friction };
73
+ return { debt, decisions, friction };
73
74
  }
74
75
 
75
- function restoreNotes(db, { debt, friction }) {
76
+ function restoreNotes(db, { debt, decisions, friction }) {
76
77
  const taskExists = db.prepare('SELECT 1 FROM tasks WHERE id = ?');
77
78
  const insertDebt = db.prepare(
78
79
  'INSERT INTO debt (task_id, note, logged_at) VALUES (?, ?, ?)',
79
80
  );
81
+ const insertDecision = db.prepare(
82
+ 'INSERT INTO decisions (task_id, note, logged_at) VALUES (?, ?, ?)',
83
+ );
80
84
  const insertFriction = db.prepare(
81
85
  'INSERT INTO friction (task_id, note, logged_at) VALUES (?, ?, ?)',
82
86
  );
@@ -91,6 +95,14 @@ function restoreNotes(db, { debt, friction }) {
91
95
  insertDebt.run(row.task_id, row.note, row.logged_at);
92
96
  }
93
97
 
98
+ for (const row of decisions) {
99
+ if (taskExists.get(row.task_id) === undefined) {
100
+ orphaned.push({ kind: 'decision', taskId: row.task_id, note: row.note });
101
+ continue;
102
+ }
103
+ insertDecision.run(row.task_id, row.note, row.logged_at);
104
+ }
105
+
94
106
  for (const row of friction) {
95
107
  // friction.task_id is nullable — an unattached note always survives.
96
108
  if (row.task_id !== null && taskExists.get(row.task_id) === undefined) {
package/src/db/schema.mjs CHANGED
@@ -109,6 +109,21 @@ CREATE TABLE IF NOT EXISTS debt (
109
109
  logged_at TEXT NOT NULL DEFAULT (datetime('now'))
110
110
  );
111
111
 
112
+ -- Declared decision: a note one task leaves for the tasks that inherit
113
+ -- from it, delivered the same way debt is (rendered into every dependent
114
+ -- task's packet) but for a different purpose. Debt records what's still
115
+ -- wrong with a task; a decision records why it was built the way it was
116
+ -- — the pattern, library, or trade-off chosen. Without it, that
117
+ -- reasoning lives only in the agent session that made the choice, and a
118
+ -- dependent task built by a different, isolated session has no way to
119
+ -- learn it beyond reading the diff and guessing.
120
+ CREATE TABLE IF NOT EXISTS decisions (
121
+ id INTEGER PRIMARY KEY,
122
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
123
+ note TEXT NOT NULL,
124
+ logged_at TEXT NOT NULL DEFAULT (datetime('now'))
125
+ );
126
+
112
127
  CREATE TABLE IF NOT EXISTS friction (
113
128
  id INTEGER PRIMARY KEY,
114
129
  task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
@@ -121,7 +136,25 @@ CREATE TABLE IF NOT EXISTS friction (
121
136
  // IF NOT EXISTS` above is a no-op against a DB created by an earlier
122
137
  // version, so a new column has to be ALTERed in explicitly or every
123
138
  // statement naming it fails on that DB. Each entry is `[name, ddl]`.
124
- const TASK_COLUMN_MIGRATIONS = [['claim_snapshot', 'claim_snapshot TEXT']];
139
+ //
140
+ // The lease columns (lease_owner, lease_expires_at, leased_at) and their
141
+ // siblings (exclusive, verify_radius, blocked_reason) shipped together in
142
+ // the same commit that introduced them to SCHEMA_SQL above, but were
143
+ // never added here — so a graph created before that commit still lacks
144
+ // them today, with every query naming lease_owner failing "no such
145
+ // column" instead of self-healing the way claim_snapshot already does.
146
+ const TASK_COLUMN_MIGRATIONS = [
147
+ ['exclusive', 'exclusive INTEGER NOT NULL DEFAULT 0'],
148
+ ['verify_radius', 'verify_radius TEXT'],
149
+ [
150
+ 'blocked_reason',
151
+ "blocked_reason TEXT CHECK (blocked_reason IS NULL OR blocked_reason IN ('scope_violation','verification_failed','lease_expired'))",
152
+ ],
153
+ ['lease_owner', 'lease_owner TEXT'],
154
+ ['lease_expires_at', 'lease_expires_at TEXT'],
155
+ ['leased_at', 'leased_at TEXT'],
156
+ ['claim_snapshot', 'claim_snapshot TEXT'],
157
+ ];
125
158
 
126
159
  // Brings an already-created `tasks` table up to the current column set.
127
160
  // Idempotent and cheap (one PRAGMA), so callers that must not fail on a
@@ -136,9 +169,76 @@ export function ensureTaskColumns(db) {
136
169
  }
137
170
  }
138
171
 
139
- // Applies the schema to an already-open node:sqlite DatabaseSync instance.
140
- // Idempotent: safe to call against a DB that already has these tables.
172
+ // Schema version this installed CLI knows about, tracked in the graph's
173
+ // own `PRAGMA user_version` (an integer SQLite stores in the file header
174
+ // — no table required, so it reads back even on a brand-new file).
175
+ // Bumped by one for every entry added to MIGRATIONS below; never
176
+ // hand-set past what MIGRATIONS actually covers, since runMigrations
177
+ // trusts this number to mean "every migration through this version has
178
+ // run."
179
+ export const CURRENT_SCHEMA_VERSION = 2;
180
+
181
+ // Forward migrations, applied in order to bring a graph's user_version up
182
+ // to CURRENT_SCHEMA_VERSION. Unlike the CREATE TABLE IF NOT EXISTS /
183
+ // ensureTaskColumns pair above — which only ever reconciles present-day
184
+ // shape against however old a graph is, and can't touch anything already
185
+ // baked into an existing column (a CHECK constraint, a rename, a data
186
+ // transform) — this is versioned, so a future change too structural for
187
+ // a bare ADD COLUMN has somewhere to go, and upgrading is one fail-loud
188
+ // step instead of every command's queries silently assuming a shape
189
+ // that might not be there yet.
190
+ const MIGRATIONS = [
191
+ {
192
+ version: 1,
193
+ // The lease/task columns TASK_COLUMN_MIGRATIONS covers already had to
194
+ // be idempotent against a fresh CREATE TABLE (which includes them
195
+ // from the start) — ensureTaskColumns' own presence check already
196
+ // does exactly what a migration step needs.
197
+ migrate: (db) => ensureTaskColumns(db),
198
+ },
199
+ {
200
+ version: 2,
201
+ migrate: (db) => {
202
+ db.exec(`
203
+ CREATE TABLE IF NOT EXISTS decisions (
204
+ id INTEGER PRIMARY KEY,
205
+ task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
206
+ note TEXT NOT NULL,
207
+ logged_at TEXT NOT NULL DEFAULT (datetime('now'))
208
+ );
209
+ `);
210
+ },
211
+ },
212
+ ];
213
+
214
+ // Brings a graph's `PRAGMA user_version` up to CURRENT_SCHEMA_VERSION,
215
+ // running only the migrations it hasn't seen yet. A graph newer than
216
+ // this installed CLI knows about (its user_version already ahead of
217
+ // CURRENT_SCHEMA_VERSION — created by, or migrated with, a later
218
+ // Hedgehog) fails loudly here with a plain-English fix, instead of every
219
+ // later query failing confusingly on a column or table shape this code
220
+ // has never heard of.
221
+ export function runMigrations(db) {
222
+ const { user_version: current } = db.prepare('PRAGMA user_version').get();
223
+
224
+ if (current > CURRENT_SCHEMA_VERSION) {
225
+ throw new Error(
226
+ `This build graph was created by a newer version of Hedgehog (schema v${current}) than the one installed here (schema v${CURRENT_SCHEMA_VERSION}). Upgrade Hedgehog (\`npx @skyf0xx/hedgehog@latest update\`) before running commands against this graph.`,
227
+ );
228
+ }
229
+
230
+ for (const { version, migrate } of MIGRATIONS) {
231
+ if (version > current) {
232
+ migrate(db);
233
+ db.exec(`PRAGMA user_version = ${version}`);
234
+ }
235
+ }
236
+ }
237
+
238
+ // Applies the schema to an already-open node:sqlite DatabaseSync instance,
239
+ // then brings it up to CURRENT_SCHEMA_VERSION. Idempotent: safe to call
240
+ // against a DB that already has these tables and has already migrated.
141
241
  export function applySchema(db) {
142
242
  db.exec(SCHEMA_SQL);
143
- ensureTaskColumns(db);
243
+ runMigrations(db);
144
244
  }
@@ -13,7 +13,7 @@
13
13
  // instead of substituting into one.
14
14
  //
15
15
  // Referenced by name (not substance) from hedgehog-adopt's own SKILL.md
16
- // in the @skyf0xx/hedgehog-core-authored package — that skill invokes
16
+ // in the @skyf0xx/hedgehog-core-adopted package — that skill invokes
17
17
  // `appendCoreSection` the same way it already invokes `loadCore` from
18
18
  // src/db/core.mjs, via `node -e "import('<path-to-hedgehog-install>/
19
19
  // src/hosts/claude-md-merge.mjs')..."`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "6.0.4",
3
+ "version": "6.1.0",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -39,10 +39,18 @@
39
39
  {
40
40
  "name": "authored",
41
41
  "package": "@skyf0xx/hedgehog-core-authored",
42
- "version": "^1.0.2",
42
+ "version": "^1.1.0",
43
43
  "language": "typescript",
44
44
  "repository": "https://github.com/skyf0xx/hedgehog-core-authored",
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."
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. This is the core most often confused with adopted: authored designs a workspace from scratch for a project being built new, while adopted brings Hedgehog's discipline to a repo that already exists — route here only when there is no existing codebase this work is being added to."
46
+ },
47
+ {
48
+ "name": "adopted",
49
+ "package": "@skyf0xx/hedgehog-core-adopted",
50
+ "version": "^1.0.0",
51
+ "language": "typescript",
52
+ "repository": "https://github.com/skyf0xx/hedgehog-core-adopted",
53
+ "selects_when": "The description is about bringing Hedgehog's discipline to a codebase that already exists, rather than building something new — the repo already has real source files, or the user says so explicitly: \"adopt this repo\", \"add Hedgehog to my existing project\", \"I want scope/verify enforcement on my changes here\". Not chosen by matching a `when` paragraph the way a shipped core is: hedgehog-adopt reads the repo read-only, proposes a linear-chain .hedgehog/core.yaml whose verify commands are the repo's own, and writes only .hedgehog/ — never a workspace, never a stack migration. This is the core most often confused with authored: adopted brings discipline to an existing repo, while authored designs and scaffolds a workspace from scratch for something being built new — route here only when the work is landing on a codebase that already exists."
46
54
  }
47
55
  ]
48
56
  }
@@ -7,21 +7,19 @@
7
7
  // the core — `installedCore` returns null until then, and `update` limits
8
8
  // itself to the shared payload.
9
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
10
+ // `adopted` has no `init` step at all: `hedgehog-adopt` (shipped in
11
+ // @skyf0xx/hedgehog-core-adopted) brings the discipline to a repo that
12
+ // already exists by writing `.hedgehog/core.yaml` itself, directly, and
13
+ // never calls `recordCore`. `hedgehog core record-adopted` (bin/cli.mjs)
14
+ // is the record path for that case instead: it fetches the `adopted`
15
+ // package and lands its agents/skills otherwise never installed, since
16
+ // adoption's `init` runs with no `--core` flag and `bootstrap` is skipped
17
+ // entirely for adoption and calls `recordCore` the same as any other
18
+ // core, naming `adopted`. That name is the one thing distinguishing an
19
+ // adopted record from a normal one: `update`'s resolveInstalledCore
20
+ // refreshes an adopted project's payload the same as any other (same
21
+ // package, same agents/skills, kept current) but must never treat
22
+ // `record.name === 'adopted'` changing upstream, or the record going
25
23
  // missing, as license to fetch and install a *different* core the way it
26
24
  // would for a project that chose one at `init` — adoption's core is a
27
25
  // fixed fact of the repo (its `.hedgehog/core.yaml`), not a choice
@@ -30,38 +28,34 @@
30
28
  import { readFile, writeFile, mkdir } from 'node:fs/promises';
31
29
  import { dirname, join } from 'node:path';
32
30
 
33
- export const ADOPTED_CORE_NAME = 'authored';
31
+ export const ADOPTED_CORE_NAME = 'adopted';
34
32
 
35
33
  const CORE_PATH = '.hedgehog/core.json';
36
34
 
37
35
  /**
38
36
  * Record the core a project installed and the exact version resolved for
39
37
  * it. Written after the core's files land, so the record describes what
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.
38
+ * is on disk.
42
39
  */
43
- export async function recordCore(root, { name, version, adopted = false }) {
40
+ export async function recordCore(root, { name, version }) {
44
41
  const path = join(root, CORE_PATH);
45
42
  await mkdir(dirname(path), { recursive: true });
46
43
  await writeFile(
47
44
  path,
48
- `${JSON.stringify(
49
- { core: name, version, installedAt: new Date().toISOString(), ...(adopted ? { adopted: true } : {}) },
50
- null,
51
- 2,
52
- )}\n`,
45
+ `${JSON.stringify({ core: name, version, installedAt: new Date().toISOString() }, null, 2)}\n`,
53
46
  );
54
47
  }
55
48
 
56
49
  /**
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.
50
+ * The core `update` should refresh — `{ name, version }` — or null when
51
+ * this project has no core installed yet. `name === 'adopted'` marks a
52
+ * record written by `hedgehog core record-adopted` rather than by `init`
53
+ * — see the module comment above.
60
54
  */
61
55
  export async function installedCore(root) {
62
56
  try {
63
- const { core, version, adopted } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
64
- return core ? { name: core, version, adopted: adopted === true } : null;
57
+ const { core, version } = JSON.parse(await readFile(join(root, CORE_PATH), 'utf8'));
58
+ return core ? { name: core, version } : null;
65
59
  } catch {
66
60
  return null;
67
61
  }
@@ -11,7 +11,6 @@
11
11
  // engine: "^<major>.<minor>.<patch>" which CLI versions can install it
12
12
  // workspace: workspace/ omitted by a core that scaffolds nothing
13
13
  // template: CLAUDE.core.md fills the CLAUDE.md shell's core section
14
- // template_adopted: <path> optional second section, for adoption
15
14
  // agents: [<name>, ...] agents/<name>.md in the package
16
15
  // skills: [<name>, ...] skills/<name>/ in the package
17
16
  // vendor_skills: [<name>, ...] vendor-skills/<name>/ in the package
@@ -40,25 +40,16 @@ it.
40
40
 
41
41
  ## Writing the issue or PR
42
42
 
43
- Most of Hedgehog's inbound queue is read first by `inbound-triage`, an
44
- agent, before a human ever sees it the same register `inbound-triage`
45
- uses when it comments back (see that skill's "Comment style" section).
46
- Write plainly for both readers at once:
43
+ Use the `pr-writing` skill for style: brief, info-dense, Simplified
44
+ Technical English, only verified claims. Most of Hedgehog's inbound queue
45
+ is read first by `inbound-triage`, an agent, before a human ever sees it —
46
+ the same register `inbound-triage` uses when it comments back (see that
47
+ skill's "Comment style" section) — so write plainly for both readers at
48
+ once.
47
49
 
48
- - Plain technical English, one claim per sentence, no hedging or
49
- marketing language.
50
- - Lead with the concrete fact: "`hedgehog init` writes `.claude/agents/`
51
- twice on Windows" beats "I noticed there might be an issue with how
52
- the installer handles paths."
53
- - Fill every field the bug-report template asks for as its own labeled
54
- fact (symptom, expected behavior, exact repro steps) rather than one
55
- collapsed paragraph.
56
- - Cite `file:line` for anything about existing behavior — an
57
- uncited claim is a hypothesis both readers have to re-derive.
58
- - One issue, one problem; one PR, one change.
59
- - Say what you verified, not what you assume: "Ran `node bin/cli.mjs
60
- init` in a scratch dir, `.claude/skills/` is missing the new
61
- directory" beats "this probably breaks the install."
50
+ Fill every field the bug-report template asks for as its own labeled fact
51
+ (symptom, expected behavior, exact repro steps) rather than one collapsed
52
+ paragraph.
62
53
 
63
54
  ## Workflow
64
55
 
@@ -92,7 +83,8 @@ Write plainly for both readers at once:
92
83
  tree has accumulated several unrelated changes that need splitting into
93
84
  atomic commits, use the `conventional-commits` skill rather than
94
85
  hand-rolling the split.
95
- 6. **Push and open the PR.**
86
+ 6. **Push and open the PR**, following `pr-writing`'s checklist and shape
87
+ (CI passing, one change, only verified claims):
96
88
  ```bash
97
89
  git push -u origin <branch-name>
98
90
  gh pr create --repo skyf0xx/hedgehog --title "<type>(<scope>): <summary>" --body "$(cat <<'EOF'
@@ -104,9 +96,10 @@ Write plainly for both readers at once:
104
96
  EOF
105
97
  )"
106
98
  ```
107
- Describe the *why* in the PR body, not in the file being changed — same
108
- rule `CONTRIBUTING.md` states for the content itself. If the PR closes
109
- or addresses a `ROADMAP.md` item or a filed issue, reference it
110
- (`Addresses the "<item name>" item in ROADMAP.md`, or `Fixes #<n>`).
111
- 7. **Report the PR URL** `gh` returns and stop — don't merge, don't push
99
+ If the PR closes or addresses a `ROADMAP.md` item or a filed issue,
100
+ reference it (`Addresses the "<item name>" item in ROADMAP.md`, or
101
+ `Fixes #<n>`).
102
+ 7. **Check CI** with `gh pr checks <number> --repo skyf0xx/hedgehog` after
103
+ opening. Fix a red check before asking for review.
104
+ 8. **Report the PR URL** `gh` returns and stop — don't merge, don't push
112
105
  further commits without being asked.
@@ -0,0 +1,63 @@
1
+ ---
2
+ name: pr-writing
3
+ description: Use whenever writing a PR title/description, a commit message body, a code review comment, or an issue — in Hedgehog's own repo or any consuming project. Triggers on "open a PR", "write the PR description", "comment on this PR", "file an issue". Covers writing style (terse, info-dense, Simplified Technical English) and the pre-open checklist (CI status, scope, verified claims only).
4
+ ---
5
+
6
+ # PR Writing
7
+
8
+ A PR description, commit message, or review comment is read by a human
9
+ deciding whether to trust and merge the change. Write for that reader, not
10
+ as a record of the work session.
11
+
12
+ ## Style rules
13
+
14
+ - **Brief.** State the change and the reason. Skip the narrative of how you
15
+ got there.
16
+ - **Info-dense, not verbose.** Every sentence carries a fact. Cut sentences
17
+ that restate the diff, the title, or each other.
18
+ - **Don't write what's inferable.** A reviewer can read the diff — don't
19
+ describe what a line change does if the code already says so. State only
20
+ what the diff can't show: intent, a non-obvious constraint, a fact you
21
+ verified.
22
+ - **Simplified Technical English.** One claim per sentence. Concrete
23
+ subjects, active voice, present tense for current behavior. No hedging
24
+ ("might", "could potentially", "it seems"), no filler ("simply",
25
+ "basically", "just"), no marketing language. Say "X fails when Y" — not
26
+ "there might be an issue where X could fail if Y happens."
27
+ - **Write for a human, not an AI reviewer.** No emoji, no "Generated by",
28
+ no restating the obvious for machine parsing. Plain prose a teammate
29
+ would send in Slack.
30
+
31
+ ## Pre-open checklist
32
+
33
+ - **CI must pass before you ask for review.** Run the project's checks
34
+ locally first — lint, tests, build. If a check is red after pushing, fix
35
+ it or say plainly in the PR why it's expected (a known, unrelated
36
+ flake), never leave it unexplained.
37
+ - **One PR, one change.** A second unrelated fix noticed along the way is a
38
+ separate PR, not scope creep on this one.
39
+ - **State only what you verified.** "Ran `X`, confirmed `Y`" — never "this
40
+ should work" or "this probably fixes it." An unverified claim in a test
41
+ plan is itself a defect; a reviewer trusts it and later finds it was
42
+ false.
43
+ - **Cite `file:line` for claims about existing behavior.** An uncited claim
44
+ is a hypothesis the reviewer has to re-derive themselves.
45
+ - **Reference the issue it closes**, if any (`Fixes #123`), instead of
46
+ restating the issue's content.
47
+
48
+ ## Shape
49
+
50
+ - **Title**: `<type>(<scope>): <summary>`, imperative mood, under ~70
51
+ chars.
52
+ - **Description**: 1-3 bullets — what changed, why. A test plan section
53
+ listing what you actually ran, not what should theoretically pass.
54
+ - **Comments**: lead with the concrete finding, then (if needed) the fix
55
+ requested. No preamble.
56
+
57
+ ## When NOT to apply
58
+
59
+ - Internal scratch notes, planning docs, or anything not read by another
60
+ person — write those however is fastest for you.
61
+ - The user asks for a different register explicitly (e.g. a detailed
62
+ design-doc-style PR description for an architectural change that needs
63
+ the extra context).
@@ -104,8 +104,8 @@ re-derive build state from prose. To work from it:
104
104
 
105
105
  1. Run `hedgehog claim --count N --owner <owner>`. It's atomic and
106
106
  lease-based, and returns up to N tasks (each with its own full
107
- STATUS/INTENT/RELEVANT RULES/INHERITED DEBT/WHY NOW/BLOCKED
108
- DOWNSTREAM/ALLOWED SCOPE/VERIFICATION packet) that the
107
+ STATUS/INTENT/RELEVANT RULES/INHERITED DEBT/INHERITED DECISIONS/WHY
108
+ NOW/BLOCKED DOWNSTREAM/ALLOWED SCOPE/VERIFICATION packet) that the
109
109
  scheduler has already verified are safe to run together right now —
110
110
  scope and verify-radius disjoint. `--count` is a maximum, not a
111
111
  promise: a call may return fewer than N, or zero. `hedgehog ready` is
@@ -141,6 +141,13 @@ records it with `hedgehog debt add <task-id> "<note>"` — it lands in the
141
141
  **INHERITED DEBT** section of every packet that depends on that task. A
142
142
  comment in a source file is not a mechanism; nothing reads it.
143
143
 
144
+ A layer that makes a choice a dependent layer needs to know about — a
145
+ pattern, a library, a trade-off, anything the next task should follow
146
+ rather than reinvent or contradict — records it with `hedgehog decision
147
+ add <task-id> "<note>"`, landing in the **INHERITED DECISIONS** section
148
+ the same way. Debt is what's still wrong with a task; a decision is why
149
+ it was built the way it was.
150
+
144
151
  `planner` owns writing intents (`hedgehog intent add`) at planning
145
152
  intake; `hedgehog plan` compiles them into the task graph the loop
146
153
  consumes. Nothing checks a box — there is no checklist, only queryable