@skyf0xx/hedgehog 6.0.4 → 6.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -5
- package/README.zh-CN.md +4 -5
- package/bin/cli.mjs +131 -9
- package/package.json +1 -1
- 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/gemini/gemini-extension.json +1 -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,
|
|
@@ -397,19 +398,19 @@ function warnRebuildDrift({ drift }, corePath) {
|
|
|
397
398
|
);
|
|
398
399
|
}
|
|
399
400
|
|
|
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.
|
|
401
|
+
// Debt, decision, and friction notes are operator- or agent-recorded and
|
|
402
|
+
// have no committed source, so a rebuild carries them across by task id.
|
|
403
|
+
// A note whose task is no longer in the recompiled graph — its intent
|
|
404
|
+
// file was renamed, deleted, or its layer sequence changed — has nowhere
|
|
405
|
+
// to re-attach. Friction notes survive unattached (their task_id is
|
|
406
|
+
// nullable); debt and decision notes are lost, so all three are printed
|
|
407
|
+
// with their text rather than disappearing into a count.
|
|
407
408
|
function warnOrphanedNotes({ orphanedNotes }) {
|
|
408
409
|
if (!orphanedNotes || orphanedNotes.length === 0) return;
|
|
409
410
|
console.log(
|
|
410
411
|
`${yellow(bold('Notes without a task after rebuild.'))} ${orphanedNotes.length} note(s) referenced a\n` +
|
|
411
412
|
'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',
|
|
413
|
+
'debt and decision notes could not be, so they are reproduced here:\n',
|
|
413
414
|
);
|
|
414
415
|
for (const note of orphanedNotes) {
|
|
415
416
|
console.log(` ${dim(note.kind)} ${bold(note.taskId)} ${note.note}`);
|
|
@@ -573,6 +574,9 @@ ${bold('Usage')}
|
|
|
573
574
|
npx @skyf0xx/hedgehog friction list list logged friction, oldest first
|
|
574
575
|
npx @skyf0xx/hedgehog debt add <task-id> "<note>" declare debt that lands in dependent tasks' packets
|
|
575
576
|
npx @skyf0xx/hedgehog debt list [<task-id>] list declared debt, oldest first
|
|
577
|
+
npx @skyf0xx/hedgehog decision add <task-id> "<note>" declare a decision that lands in dependent tasks' packets
|
|
578
|
+
npx @skyf0xx/hedgehog decision list [<task-id>] list declared decisions, oldest first
|
|
579
|
+
npx @skyf0xx/hedgehog db migrate bring the graph's schema up to the latest version
|
|
576
580
|
npx @skyf0xx/hedgehog community star --answer <a> record the star prompt's answer
|
|
577
581
|
npx @skyf0xx/hedgehog --help
|
|
578
582
|
|
|
@@ -1155,9 +1159,13 @@ async function dbCommand(args) {
|
|
|
1155
1159
|
await dbRebuildCommand();
|
|
1156
1160
|
return;
|
|
1157
1161
|
}
|
|
1162
|
+
if (sub === 'migrate') {
|
|
1163
|
+
await dbMigrateCommand();
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1158
1166
|
if (sub !== 'init') {
|
|
1159
1167
|
console.error(
|
|
1160
|
-
`${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n or: hedgehog db rebuild\n`,
|
|
1168
|
+
`${red('Unknown db subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog db init\n or: hedgehog db rebuild\n or: hedgehog db migrate\n`,
|
|
1161
1169
|
);
|
|
1162
1170
|
process.exitCode = 1;
|
|
1163
1171
|
return;
|
|
@@ -1179,6 +1187,44 @@ async function dbCommand(args) {
|
|
|
1179
1187
|
}
|
|
1180
1188
|
}
|
|
1181
1189
|
|
|
1190
|
+
// `hedgehog db migrate` — brings an existing build graph's schema up to
|
|
1191
|
+
// CURRENT_SCHEMA_VERSION on demand (see schema.mjs's runMigrations),
|
|
1192
|
+
// rather than only as a side effect of the next command that happens to
|
|
1193
|
+
// open the graph writably. Reports what moved, since an upgrade
|
|
1194
|
+
// shouldn't be a silent side effect the first time some other command
|
|
1195
|
+
// happens to trigger it.
|
|
1196
|
+
async function dbMigrateCommand() {
|
|
1197
|
+
if (!(await exists(DB_PATH))) {
|
|
1198
|
+
console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
|
|
1199
|
+
process.exitCode = 1;
|
|
1200
|
+
return;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
const before = openDb({ readOnly: true });
|
|
1204
|
+
const { user_version: fromVersion } = before.prepare('PRAGMA user_version').get();
|
|
1205
|
+
before.close();
|
|
1206
|
+
|
|
1207
|
+
let toVersion;
|
|
1208
|
+
try {
|
|
1209
|
+
const db = openDb();
|
|
1210
|
+
try {
|
|
1211
|
+
({ user_version: toVersion } = db.prepare('PRAGMA user_version').get());
|
|
1212
|
+
} finally {
|
|
1213
|
+
db.close();
|
|
1214
|
+
}
|
|
1215
|
+
} catch (err) {
|
|
1216
|
+
console.error(`${red(err.message)}\n`);
|
|
1217
|
+
process.exitCode = 1;
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
if (fromVersion === toVersion) {
|
|
1222
|
+
console.log(`${dim(`Build graph is already at the latest schema (v${toVersion}). Nothing to do.`)}\n`);
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
console.log(`${green('Migrated')} build graph from schema v${fromVersion} to v${toVersion}.\n`);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1182
1228
|
// Resolves the project's core definition: an authored .hedgehog/core.yaml
|
|
1183
1229
|
// takes precedence (spec: "Authored cores"); otherwise the core's
|
|
1184
1230
|
// own core.yaml, which lands at repo root along with the rest of that
|
|
@@ -3095,6 +3141,77 @@ async function debtCommand(args) {
|
|
|
3095
3141
|
process.exitCode = 1;
|
|
3096
3142
|
}
|
|
3097
3143
|
|
|
3144
|
+
// `hedgehog decision add <task-id> "<note>"` / `hedgehog decision list
|
|
3145
|
+
// [<task-id>]` — declared decisions between tasks. A note recorded
|
|
3146
|
+
// against a task is rendered into the INHERITED DECISIONS section of the
|
|
3147
|
+
// packet of every task that depends on it (see src/db/decision.mjs and
|
|
3148
|
+
// src/db/next.mjs).
|
|
3149
|
+
async function decisionCommand(args) {
|
|
3150
|
+
await ensureDb();
|
|
3151
|
+
|
|
3152
|
+
const sub = args[0];
|
|
3153
|
+
|
|
3154
|
+
if (!(await exists(DB_PATH))) {
|
|
3155
|
+
console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
|
|
3156
|
+
process.exitCode = 1;
|
|
3157
|
+
return;
|
|
3158
|
+
}
|
|
3159
|
+
|
|
3160
|
+
if (sub === 'add') {
|
|
3161
|
+
const taskId = args[1];
|
|
3162
|
+
const note = args.slice(2).join(' ');
|
|
3163
|
+
if (!taskId || !note) {
|
|
3164
|
+
console.error(`${red('Usage:')} hedgehog decision add <task-id> "<note>"\n`);
|
|
3165
|
+
process.exitCode = 1;
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
|
|
3169
|
+
const db = openDb();
|
|
3170
|
+
let entry;
|
|
3171
|
+
try {
|
|
3172
|
+
entry = addDecision(db, { taskId, note });
|
|
3173
|
+
} catch (err) {
|
|
3174
|
+
console.error(
|
|
3175
|
+
`${red('Failed to declare decision:')} ${err.message}\n\nRun ${bold('hedgehog status')} to see valid task ids.\n`,
|
|
3176
|
+
);
|
|
3177
|
+
process.exitCode = 1;
|
|
3178
|
+
return;
|
|
3179
|
+
} finally {
|
|
3180
|
+
db.close();
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
console.log(` ${green('declared')} #${entry.id} ${bold(entry.taskId)}`);
|
|
3184
|
+
console.log(` ${dim('reaches the packet of every task depending on it')}`);
|
|
3185
|
+
return;
|
|
3186
|
+
}
|
|
3187
|
+
|
|
3188
|
+
if (sub === 'list') {
|
|
3189
|
+
const taskId = args[1];
|
|
3190
|
+
const db = openDb();
|
|
3191
|
+
let entries;
|
|
3192
|
+
try {
|
|
3193
|
+
entries = listDecisions(db, taskId);
|
|
3194
|
+
} finally {
|
|
3195
|
+
db.close();
|
|
3196
|
+
}
|
|
3197
|
+
|
|
3198
|
+
if (entries.length === 0) {
|
|
3199
|
+
console.log(`${dim('No decisions declared.')}\n`);
|
|
3200
|
+
return;
|
|
3201
|
+
}
|
|
3202
|
+
for (const entry of entries) {
|
|
3203
|
+
console.log(`#${entry.id} ${dim(entry.loggedAt)} ${bold(entry.taskId)}`);
|
|
3204
|
+
console.log(` ${entry.note}\n`);
|
|
3205
|
+
}
|
|
3206
|
+
return;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
console.error(
|
|
3210
|
+
`${red('Unknown decision subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog decision add <task-id> "<note>"\n or: hedgehog decision list [<task-id>]\n`,
|
|
3211
|
+
);
|
|
3212
|
+
process.exitCode = 1;
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3098
3215
|
// `hedgehog community star --answer starred|later|dismissed` — records
|
|
3099
3216
|
// the star prompt's answer. No build graph or core needed: this is
|
|
3100
3217
|
// project state about a question asked, not about the build.
|
|
@@ -3400,6 +3517,11 @@ async function main() {
|
|
|
3400
3517
|
return;
|
|
3401
3518
|
}
|
|
3402
3519
|
|
|
3520
|
+
if (cmd === 'decision') {
|
|
3521
|
+
await decisionCommand(args.slice(1));
|
|
3522
|
+
return;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3403
3525
|
if (cmd === 'community') {
|
|
3404
3526
|
await communityCommand(args.slice(1));
|
|
3405
3527
|
return;
|
package/package.json
CHANGED
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
|
}
|
|
@@ -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
|