@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 +4 -5
- package/README.zh-CN.md +4 -5
- package/bin/cli.mjs +161 -37
- package/package.json +1 -1
- package/src/agents/planner.md +11 -10
- package/src/agents/reviewer.md +3 -2
- package/src/agents/tweaker.md +4 -4
- package/src/db/debt.mjs +3 -3
- package/src/db/decision.mjs +64 -0
- package/src/db/init.mjs +12 -0
- package/src/db/next.mjs +40 -3
- package/src/db/rebuild.mjs +20 -8
- package/src/db/schema.mjs +104 -4
- package/src/hosts/claude-md-merge.mjs +1 -1
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/registry/cores.json +10 -2
- package/src/registry/installed.mjs +23 -29
- package/src/registry/manifest.mjs +0 -1
- package/src/skills/hedgehog-contributing/SKILL.md +17 -24
- package/src/skills/pr-writing/SKILL.md +63 -0
- package/src/templates/CLAUDE.md +9 -2
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
|
-
##
|
|
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
|
-
- **
|
|
108
|
-
- **
|
|
109
|
-
- **
|
|
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
|
|
401
|
-
// source, so a rebuild carries them across by task id.
|
|
402
|
-
// is no longer in the recompiled graph — its intent
|
|
403
|
-
// deleted, or its layer sequence changed — has nowhere
|
|
404
|
-
// Friction notes survive unattached (their task_id is
|
|
405
|
-
// notes are lost, so
|
|
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
|
|
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-
|
|
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 `
|
|
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
|
|
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 `
|
|
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
|
|
1066
|
-
//
|
|
1067
|
-
//
|
|
1068
|
-
//
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
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('
|
|
1077
|
-
`entry named "
|
|
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
|
-
|
|
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
package/src/agents/planner.md
CHANGED
|
@@ -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.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
`.hedgehog/core.yaml`;
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
- **
|
|
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
|
|
package/src/agents/reviewer.md
CHANGED
|
@@ -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.
|
|
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.
|
package/src/agents/tweaker.md
CHANGED
|
@@ -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
|
|
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
|
|
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 —
|
|
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
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
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 /
|
|
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 {
|
|
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
|
package/src/db/rebuild.mjs
CHANGED
|
@@ -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`
|
|
57
|
-
// exception: they are operator-recorded notes with no
|
|
58
|
-
// so they are carried across by task id (deterministic,
|
|
59
|
-
// re-attaches to the same task the replay recompiles). A note
|
|
60
|
-
// no longer exists in the new graph has nowhere to live and is
|
|
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
|
-
|
|
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
|
-
//
|
|
140
|
-
//
|
|
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
|
-
|
|
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-
|
|
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')..."`.
|
package/src/registry/cores.json
CHANGED
|
@@ -39,10 +39,18 @@
|
|
|
39
39
|
{
|
|
40
40
|
"name": "authored",
|
|
41
41
|
"package": "@skyf0xx/hedgehog-core-authored",
|
|
42
|
-
"version": "^1.0
|
|
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
|
-
// `
|
|
11
|
-
//
|
|
12
|
-
// writing `.hedgehog/core.yaml` itself, directly, and
|
|
13
|
-
// `recordCore`
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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 = '
|
|
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.
|
|
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
|
|
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
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
|
64
|
-
return core ? { name: core, version
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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).
|
package/src/templates/CLAUDE.md
CHANGED
|
@@ -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/
|
|
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
|