@skyf0xx/hedgehog 6.1.6 → 6.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +2 -2
- package/package.json +1 -1
- package/src/agents/bootstrap.md +4 -1
- package/src/db/debt.mjs +24 -14
- package/src/db/decision.mjs +20 -11
- package/src/db/notes.mjs +121 -0
- package/src/db/rebuild.mjs +37 -56
- package/src/db/verify.mjs +4 -3
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/skills/hedgehog-contributing/SKILL.md +11 -18
package/bin/cli.mjs
CHANGED
|
@@ -3293,7 +3293,7 @@ async function debtCommand(args) {
|
|
|
3293
3293
|
const db = openDb();
|
|
3294
3294
|
let entry;
|
|
3295
3295
|
try {
|
|
3296
|
-
entry = addDebt(db, { taskId, note });
|
|
3296
|
+
entry = await addDebt(db, { taskId, note });
|
|
3297
3297
|
} catch (err) {
|
|
3298
3298
|
console.error(
|
|
3299
3299
|
`${red('Failed to declare debt:')} ${err.message}\n\nRun ${bold('hedgehog status')} to see valid task ids.\n`,
|
|
@@ -3364,7 +3364,7 @@ async function decisionCommand(args) {
|
|
|
3364
3364
|
const db = openDb();
|
|
3365
3365
|
let entry;
|
|
3366
3366
|
try {
|
|
3367
|
-
entry = addDecision(db, { taskId, note });
|
|
3367
|
+
entry = await addDecision(db, { taskId, note });
|
|
3368
3368
|
} catch (err) {
|
|
3369
3369
|
console.error(
|
|
3370
3370
|
`${red('Failed to declare decision:')} ${err.message}\n\nRun ${bold('hedgehog status')} to see valid task ids.\n`,
|
package/package.json
CHANGED
package/src/agents/bootstrap.md
CHANGED
|
@@ -16,7 +16,10 @@ read it.
|
|
|
16
16
|
|
|
17
17
|
You touch no build content on any core. That's the first build step,
|
|
18
18
|
started after Bootstrap closes, run by the core's own loop skill and its
|
|
19
|
-
agents.
|
|
19
|
+
agents. This is a scope discipline, not a tool restriction — your tool
|
|
20
|
+
grant includes `Edit`/`Write`, so nothing stops you mechanically from
|
|
21
|
+
writing domain code. Follow the chosen core's bootstrap skill exactly and
|
|
22
|
+
stop where it stops.
|
|
20
23
|
|
|
21
24
|
## Finding your step
|
|
22
25
|
|
package/src/db/debt.mjs
CHANGED
|
@@ -10,30 +10,37 @@
|
|
|
10
10
|
// arrives. `debt add` records the note against the declaring task, and
|
|
11
11
|
// next.mjs renders it into the packet of every task that depends on it.
|
|
12
12
|
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// `.hedgehog
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
13
|
+
// A note recorded here has a committed source behind it —
|
|
14
|
+
// `.hedgehog/notes/<task-id>.json` (notes.mjs) — the same way
|
|
15
|
+
// `.hedgehog/reconciled/*.json` backs a reconciliation. Debt is in-build
|
|
16
|
+
// traffic between two tasks, and a file under `.hedgehog/` written
|
|
17
|
+
// mid-task sits outside every task's scope globs — it would trip verify's
|
|
18
|
+
// scope gate on the very task that declared it — so `hedgehog debt add`
|
|
19
|
+
// writes it directly rather than through the declaring task's own commit.
|
|
20
|
+
// `hedgehog db rebuild` replays it from there (rebuild.mjs), the same way
|
|
21
|
+
// it replays overrides and reconciliations.
|
|
20
22
|
|
|
21
23
|
import { applySchema } from './schema.mjs';
|
|
24
|
+
import { appendNote } from './notes.mjs';
|
|
22
25
|
|
|
23
26
|
const insertDebt = (db) =>
|
|
24
27
|
db.prepare(`
|
|
25
|
-
INSERT INTO debt (task_id, note)
|
|
26
|
-
VALUES (?, ?)
|
|
28
|
+
INSERT INTO debt (task_id, note, logged_at)
|
|
29
|
+
VALUES (?, ?, ?)
|
|
27
30
|
`);
|
|
28
31
|
|
|
29
32
|
function taskExists(db, taskId) {
|
|
30
33
|
return db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(taskId) !== undefined;
|
|
31
34
|
}
|
|
32
35
|
|
|
33
|
-
// Writes one debt row against `taskId
|
|
34
|
-
// addressed to nobody reaches nobody, and the
|
|
35
|
-
// reject it anyway, less legibly.
|
|
36
|
-
|
|
36
|
+
// Writes one debt row against `taskId`, and its committed record. The
|
|
37
|
+
// task must exist — debt addressed to nobody reaches nobody, and the
|
|
38
|
+
// schema's foreign key would reject it anyway, less legibly.
|
|
39
|
+
//
|
|
40
|
+
// Committed file before DB row, deliberately — reconcile.mjs's same
|
|
41
|
+
// ordering, for the same reason: if the write fails, nothing has been
|
|
42
|
+
// recorded on a fact that would not survive the next rebuild.
|
|
43
|
+
export async function addDebt(db, { taskId, note }, notesDir = undefined) {
|
|
37
44
|
// Idempotent, and the migration path for a build graph created before
|
|
38
45
|
// the `debt` table existed: dbInit only applies the schema to a DB it
|
|
39
46
|
// just created, so an in-flight project's DB would otherwise have no
|
|
@@ -44,7 +51,10 @@ export function addDebt(db, { taskId, note }) {
|
|
|
44
51
|
if (!note) throw new Error('debt requires a note');
|
|
45
52
|
if (!taskExists(db, taskId)) throw new Error(`no such task: ${taskId}`);
|
|
46
53
|
|
|
47
|
-
const
|
|
54
|
+
const loggedAt = new Date().toISOString();
|
|
55
|
+
await appendNote(taskId, { kind: 'debt', note, loggedAt }, notesDir);
|
|
56
|
+
|
|
57
|
+
const result = insertDebt(db).run(taskId, note, loggedAt);
|
|
48
58
|
return { id: Number(result.lastInsertRowid), taskId, note };
|
|
49
59
|
}
|
|
50
60
|
|
package/src/db/decision.mjs
CHANGED
|
@@ -12,27 +12,33 @@
|
|
|
12
12
|
// records the note against the declaring task, and next.mjs renders it
|
|
13
13
|
// into the packet of every task that depends on it.
|
|
14
14
|
//
|
|
15
|
-
// Same as debt:
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// Same as debt: a note recorded here has a committed source behind it —
|
|
16
|
+
// `.hedgehog/notes/<task-id>.json` (notes.mjs), the same way
|
|
17
|
+
// `.hedgehog/reconciled/*.json` backs a reconciliation. `hedgehog db
|
|
18
|
+
// rebuild` replays it from there, alongside overrides and
|
|
19
|
+
// reconciliations.
|
|
19
20
|
|
|
20
21
|
import { applySchema } from './schema.mjs';
|
|
22
|
+
import { appendNote } from './notes.mjs';
|
|
21
23
|
|
|
22
24
|
const insertDecision = (db) =>
|
|
23
25
|
db.prepare(`
|
|
24
|
-
INSERT INTO decisions (task_id, note)
|
|
25
|
-
VALUES (?, ?)
|
|
26
|
+
INSERT INTO decisions (task_id, note, logged_at)
|
|
27
|
+
VALUES (?, ?, ?)
|
|
26
28
|
`);
|
|
27
29
|
|
|
28
30
|
function taskExists(db, taskId) {
|
|
29
31
|
return db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(taskId) !== undefined;
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
// Writes one decision row against `taskId
|
|
33
|
-
// decision addressed to nobody reaches nobody, and
|
|
34
|
-
// key would reject it anyway, less legibly.
|
|
35
|
-
|
|
34
|
+
// Writes one decision row against `taskId`, and its committed record. The
|
|
35
|
+
// task must exist — a decision addressed to nobody reaches nobody, and
|
|
36
|
+
// the schema's foreign key would reject it anyway, less legibly.
|
|
37
|
+
//
|
|
38
|
+
// Committed file before DB row, deliberately — reconcile.mjs's same
|
|
39
|
+
// ordering, for the same reason: if the write fails, nothing has been
|
|
40
|
+
// recorded on a fact that would not survive the next rebuild.
|
|
41
|
+
export async function addDecision(db, { taskId, note }, notesDir = undefined) {
|
|
36
42
|
// Idempotent, and the migration path for a build graph created before
|
|
37
43
|
// the `decisions` table existed: dbInit only applies the schema to a DB
|
|
38
44
|
// it just created, so an in-flight project's DB would otherwise have no
|
|
@@ -43,7 +49,10 @@ export function addDecision(db, { taskId, note }) {
|
|
|
43
49
|
if (!note) throw new Error('decision requires a note');
|
|
44
50
|
if (!taskExists(db, taskId)) throw new Error(`no such task: ${taskId}`);
|
|
45
51
|
|
|
46
|
-
const
|
|
52
|
+
const loggedAt = new Date().toISOString();
|
|
53
|
+
await appendNote(taskId, { kind: 'decision', note, loggedAt }, notesDir);
|
|
54
|
+
|
|
55
|
+
const result = insertDecision(db).run(taskId, note, loggedAt);
|
|
47
56
|
return { id: Number(result.lastInsertRowid), taskId, note };
|
|
48
57
|
}
|
|
49
58
|
|
package/src/db/notes.mjs
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// `.hedgehog/notes/<task-id>.json` — the committed record behind `debt`
|
|
2
|
+
// and `decisions` rows.
|
|
3
|
+
//
|
|
4
|
+
// Both tables are operator- or agent-recorded notes with no committed
|
|
5
|
+
// source, in the same position `.hedgehog/reconciled/*.json` was in
|
|
6
|
+
// before reconcile.mjs existed: the only place they live is the DB, and
|
|
7
|
+
// `hedgehog db rebuild` clears and re-derives the DB from committed files
|
|
8
|
+
// on every run (rebuild.mjs#clearDerivedGraph). Before this file existed,
|
|
9
|
+
// rebuild carried debt/decisions across by reading them out of the *same*
|
|
10
|
+
// DB it was about to wipe — which only works because today there is
|
|
11
|
+
// always exactly one DB. A worktree gets its own DB with no memory of
|
|
12
|
+
// what a sibling worktree's DB held, so a note logged there and merged
|
|
13
|
+
// back has nothing for a post-merge rebuild to carry across, and is
|
|
14
|
+
// silently dropped. This file gives both tables the committed source
|
|
15
|
+
// `friction` already has (`.hedgehog/friction/log.md`) and `reconciled`
|
|
16
|
+
// records have (`.hedgehog/reconciled/*.json`), so a rebuild anywhere —
|
|
17
|
+
// worktree or trunk — replays the same notes from the same files.
|
|
18
|
+
//
|
|
19
|
+
// One file per task, holding every debt and decision note logged against
|
|
20
|
+
// it — unlike a reconciliation, which is one-shot per task, `debt add`
|
|
21
|
+
// and `decision add` are routinely called more than once against the same
|
|
22
|
+
// task, so the file is a list, not a single record, and a write appends
|
|
23
|
+
// rather than refusing to overwrite.
|
|
24
|
+
|
|
25
|
+
import { readdir, readFile, mkdir, writeFile, rename, rm } from 'node:fs/promises';
|
|
26
|
+
|
|
27
|
+
export const NOTES_DIR = '.hedgehog/notes';
|
|
28
|
+
|
|
29
|
+
function notesFilePath(taskId, notesDir = NOTES_DIR) {
|
|
30
|
+
return `${notesDir}/${taskId.toLowerCase()}.json`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateNotesFile(record, path) {
|
|
34
|
+
if (record === null || typeof record !== 'object' || Array.isArray(record)) {
|
|
35
|
+
throw new Error(`${path}: notes file must be a JSON object`);
|
|
36
|
+
}
|
|
37
|
+
const { task, notes } = record;
|
|
38
|
+
if (!task || typeof task !== 'string') {
|
|
39
|
+
throw new Error(`${path}: notes file requires a "task" id (string)`);
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(notes)) {
|
|
42
|
+
throw new Error(`${path}: notes file requires a "notes" array`);
|
|
43
|
+
}
|
|
44
|
+
for (const entry of notes) {
|
|
45
|
+
if (entry === null || typeof entry !== 'object') {
|
|
46
|
+
throw new Error(`${path}: notes file "${task}" has a non-object entry in notes`);
|
|
47
|
+
}
|
|
48
|
+
if (entry.kind !== 'debt' && entry.kind !== 'decision') {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${path}: notes file "${task}" has an entry with kind "${entry.kind}" — expected "debt" or "decision"`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
if (!entry.note || typeof entry.note !== 'string') {
|
|
54
|
+
throw new Error(`${path}: notes file "${task}" has an entry with no "note" (string)`);
|
|
55
|
+
}
|
|
56
|
+
if (!entry.logged_at || typeof entry.logged_at !== 'string') {
|
|
57
|
+
throw new Error(`${path}: notes file "${task}" has an entry with no "logged_at" timestamp`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { task: task.toUpperCase(), notes };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Every notes file in `notesDir`, validated, as a Map from task id to its
|
|
64
|
+
// full { debt: [], decisions: [] } note list — the shape rebuild.mjs
|
|
65
|
+
// replays directly. Absent directory reads as "no notes recorded", the
|
|
66
|
+
// same way overrides.mjs#loadOverrides treats a missing directory.
|
|
67
|
+
export async function loadNotes(notesDir = NOTES_DIR) {
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await readdir(notesDir);
|
|
71
|
+
} catch {
|
|
72
|
+
return new Map();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const byTask = new Map();
|
|
76
|
+
for (const name of entries.filter((n) => n.endsWith('.json')).sort()) {
|
|
77
|
+
const path = `${notesDir}/${name}`;
|
|
78
|
+
let parsed;
|
|
79
|
+
try {
|
|
80
|
+
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
81
|
+
} catch (err) {
|
|
82
|
+
throw new Error(`could not read notes file ${path}: ${err.message}`);
|
|
83
|
+
}
|
|
84
|
+
const { task, notes } = validateNotesFile(parsed, path);
|
|
85
|
+
byTask.set(task, notes);
|
|
86
|
+
}
|
|
87
|
+
return byTask;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Appends one note to `taskId`'s file, creating it if this is the task's
|
|
91
|
+
// first. Read-modify-write via temp file + rename, so a crash mid-write
|
|
92
|
+
// never leaves a half-written file for loadNotes to trip on —
|
|
93
|
+
// reconcile.mjs#writeReconciledFile's pattern, applied to a file that
|
|
94
|
+
// grows instead of one written once.
|
|
95
|
+
export async function appendNote(taskId, { kind, note, loggedAt }, notesDir = NOTES_DIR) {
|
|
96
|
+
const path = notesFilePath(taskId, notesDir);
|
|
97
|
+
|
|
98
|
+
let existing = [];
|
|
99
|
+
try {
|
|
100
|
+
const parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
101
|
+
existing = validateNotesFile(parsed, path).notes;
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (!err || err.code !== 'ENOENT') throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const record = {
|
|
107
|
+
task: taskId.toUpperCase(),
|
|
108
|
+
notes: [...existing, { kind, note, logged_at: loggedAt }],
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
await mkdir(notesDir, { recursive: true });
|
|
112
|
+
const tempPath = `${path}.tmp-${process.pid}`;
|
|
113
|
+
try {
|
|
114
|
+
await writeFile(tempPath, `${JSON.stringify(record, null, 2)}\n`);
|
|
115
|
+
await rename(tempPath, path);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
await rm(tempPath, { force: true }).catch(() => {});
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
return record;
|
|
121
|
+
}
|
package/src/db/rebuild.mjs
CHANGED
|
@@ -30,8 +30,8 @@ import {
|
|
|
30
30
|
orphanedReconciliations,
|
|
31
31
|
reconciledNote,
|
|
32
32
|
RECONCILED_DIR,
|
|
33
|
-
RECONCILED_NOTE_PREFIX,
|
|
34
33
|
} from './reconcile.mjs';
|
|
34
|
+
import { loadNotes, NOTES_DIR } from './notes.mjs';
|
|
35
35
|
|
|
36
36
|
// Alphabetical by filename, purely to make the *tie-break* deterministic
|
|
37
37
|
// across machines and runs. It is NOT the replay order — see
|
|
@@ -61,38 +61,32 @@ function intentExists(db, id) {
|
|
|
61
61
|
// back, producing a graph no set of committed intents describes.
|
|
62
62
|
//
|
|
63
63
|
// Deleting `intents` cascades through requirements, tasks,
|
|
64
|
-
// task_requirements, dependencies, artifacts
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
64
|
+
// task_requirements, dependencies, artifacts, verifications, `debt` and
|
|
65
|
+
// `decisions` (all `ON DELETE CASCADE` from `tasks`) — every one of which
|
|
66
|
+
// this run re-derives. `debt` and `decisions` are then replayed from
|
|
67
|
+
// `.hedgehog/notes/*.json` (replayNotes, below) rather than carried
|
|
68
|
+
// across from the DB's own prior rows — a worktree's own DB never held a
|
|
69
|
+
// sibling worktree's notes, so carrying across only the current DB's rows
|
|
70
|
+
// would silently drop everything logged elsewhere. `friction` keeps its
|
|
71
|
+
// own committed source (`.hedgehog/friction/log.md`, friction.mjs) and is
|
|
72
|
+
// left untouched here — its `task_id` is `ON DELETE SET NULL`, not
|
|
73
|
+
// CASCADE, so its rows outlive this delete unattached rather than being
|
|
74
|
+
// cleared.
|
|
71
75
|
//
|
|
72
|
-
// One class of decision row
|
|
73
|
-
// provenance note a reconciliation writes (reconcile.mjs). That one
|
|
74
|
-
//
|
|
75
|
-
// replayReconciliations below re-writes it from that file.
|
|
76
|
-
// across as well would give a reconciled task two identical notes after
|
|
77
|
-
// the first rebuild, and one more on every rebuild after that.
|
|
76
|
+
// One class of decision row needs no replay from notes.mjs: the
|
|
77
|
+
// provenance note a reconciliation writes (reconcile.mjs). That one has a
|
|
78
|
+
// different committed source — `.hedgehog/reconciled/*.json` — and
|
|
79
|
+
// replayReconciliations below re-writes it from that file instead.
|
|
78
80
|
function clearDerivedGraph(db) {
|
|
79
|
-
const debt = db.prepare('SELECT task_id, note, logged_at FROM debt').all();
|
|
80
|
-
const decisions = db
|
|
81
|
-
.prepare('SELECT task_id, note, logged_at FROM decisions')
|
|
82
|
-
.all()
|
|
83
|
-
.filter((row) => !row.note.startsWith(RECONCILED_NOTE_PREFIX));
|
|
84
|
-
const friction = db.prepare('SELECT task_id, note, logged_at FROM friction').all();
|
|
85
|
-
|
|
86
81
|
db.prepare('DELETE FROM intents').run();
|
|
87
|
-
// `friction.task_id` is ON DELETE SET NULL rather than CASCADE, so its
|
|
88
|
-
// rows outlive the delete above. Clear them too and let restoreNotes be
|
|
89
|
-
// the single writer, so a note is not duplicated against its own copy.
|
|
90
|
-
db.prepare('DELETE FROM friction').run();
|
|
91
|
-
|
|
92
|
-
return { debt, decisions, friction };
|
|
93
82
|
}
|
|
94
83
|
|
|
95
|
-
|
|
84
|
+
// Replays `.hedgehog/notes/*.json` (notes.mjs) — the committed record
|
|
85
|
+
// behind every `debt add` / `decision add` call — the same way
|
|
86
|
+
// replayReconciliations below replays `.hedgehog/reconciled/*.json`. A
|
|
87
|
+
// note whose task no longer exists in the new graph has nowhere to live
|
|
88
|
+
// and is reported rather than silently dropped.
|
|
89
|
+
function replayNotes(db, notesByTask) {
|
|
96
90
|
const taskExists = db.prepare('SELECT 1 FROM tasks WHERE id = ?');
|
|
97
91
|
const insertDebt = db.prepare(
|
|
98
92
|
'INSERT INTO debt (task_id, note, logged_at) VALUES (?, ?, ?)',
|
|
@@ -100,38 +94,23 @@ function restoreNotes(db, { debt, decisions, friction }) {
|
|
|
100
94
|
const insertDecision = db.prepare(
|
|
101
95
|
'INSERT INTO decisions (task_id, note, logged_at) VALUES (?, ?, ?)',
|
|
102
96
|
);
|
|
103
|
-
const insertFriction = db.prepare(
|
|
104
|
-
'INSERT INTO friction (task_id, note, logged_at) VALUES (?, ?, ?)',
|
|
105
|
-
);
|
|
106
97
|
|
|
107
98
|
const orphaned = [];
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
insertDebt.run(row.task_id, row.note, row.logged_at);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
for (const row of decisions) {
|
|
118
|
-
if (taskExists.get(row.task_id) === undefined) {
|
|
119
|
-
orphaned.push({ kind: 'decision', taskId: row.task_id, note: row.note });
|
|
99
|
+
for (const [taskId, notes] of notesByTask) {
|
|
100
|
+
if (taskExists.get(taskId) === undefined) {
|
|
101
|
+
for (const entry of notes) {
|
|
102
|
+
orphaned.push({ kind: entry.kind, taskId, note: entry.note });
|
|
103
|
+
}
|
|
120
104
|
continue;
|
|
121
105
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
insertFriction.run(null, row.note, row.logged_at);
|
|
129
|
-
orphaned.push({ kind: 'friction', taskId: row.task_id, note: row.note });
|
|
130
|
-
continue;
|
|
106
|
+
for (const entry of notes) {
|
|
107
|
+
if (entry.kind === 'debt') {
|
|
108
|
+
insertDebt.run(taskId, entry.note, entry.logged_at);
|
|
109
|
+
} else {
|
|
110
|
+
insertDecision.run(taskId, entry.note, entry.logged_at);
|
|
111
|
+
}
|
|
131
112
|
}
|
|
132
|
-
insertFriction.run(row.task_id, row.note, row.logged_at);
|
|
133
113
|
}
|
|
134
|
-
|
|
135
114
|
return orphaned;
|
|
136
115
|
}
|
|
137
116
|
|
|
@@ -392,17 +371,19 @@ export async function rebuildDb(
|
|
|
392
371
|
intentsDir = INTENTS_DIR,
|
|
393
372
|
overridesDir = OVERRIDES_DIR,
|
|
394
373
|
reconciledDir = RECONCILED_DIR,
|
|
374
|
+
notesDir = NOTES_DIR,
|
|
395
375
|
} = {},
|
|
396
376
|
) {
|
|
397
377
|
applySchema(db);
|
|
398
378
|
|
|
399
|
-
|
|
379
|
+
clearDerivedGraph(db);
|
|
400
380
|
|
|
401
381
|
const intentsReplayed = await replayIntents(db, intentsDir);
|
|
402
382
|
|
|
403
383
|
const core = await loadCore(corePath);
|
|
404
384
|
const overrides = await loadOverrides(overridesDir);
|
|
405
385
|
const reconciliations = await loadReconciliations(reconciledDir);
|
|
386
|
+
const notesByTask = await loadNotes(notesDir);
|
|
406
387
|
|
|
407
388
|
planTasks(db, core, overrides);
|
|
408
389
|
|
|
@@ -412,7 +393,7 @@ export async function rebuildDb(
|
|
|
412
393
|
const tasksReconciled = replayReconciliations(db, reconciliations);
|
|
413
394
|
const orphanedReconciled = orphanedReconciliations(db, reconciliations);
|
|
414
395
|
|
|
415
|
-
const orphanedNotes =
|
|
396
|
+
const orphanedNotes = replayNotes(db, notesByTask);
|
|
416
397
|
|
|
417
398
|
const drift = detectDrift(db, core, { overrides });
|
|
418
399
|
|
package/src/db/verify.mjs
CHANGED
|
@@ -66,18 +66,19 @@ import { OVERRIDES_DIR } from './overrides.mjs';
|
|
|
66
66
|
import { RECONCILED_DIR } from './reconcile.mjs';
|
|
67
67
|
import { INTENTS_DIR } from './intent.mjs';
|
|
68
68
|
import { COMMUNITY_PATH } from './community.mjs';
|
|
69
|
+
import { NOTES_DIR } from './notes.mjs';
|
|
69
70
|
|
|
70
71
|
// Build-graph state directories: written by their own command
|
|
71
72
|
// (`friction add`, `override add`, `intent add`/`db rebuild`, `reconcile
|
|
72
|
-
// confirm`), committed by that command's own
|
|
73
|
-
// layer's verify_command. A
|
|
73
|
+
// confirm`, `debt add`/`decision add`), committed by that command's own
|
|
74
|
+
// next step, never by a layer's verify_command. A
|
|
74
75
|
// layer's own work never lands here, so a path under one of these is
|
|
75
76
|
// never this task's doing regardless of when it changed relative to
|
|
76
77
|
// claim time — unlike attributedToTask's fingerprint check, which only
|
|
77
78
|
// excludes a path unchanged since claim and so still attributes a
|
|
78
79
|
// friction note logged mid-layer (exactly what the loop skill instructs)
|
|
79
80
|
// to whichever task happened to be building when it was logged.
|
|
80
|
-
const BUILD_GRAPH_STATE_DIRS = [FRICTION_DIR, OVERRIDES_DIR, INTENTS_DIR, RECONCILED_DIR];
|
|
81
|
+
const BUILD_GRAPH_STATE_DIRS = [FRICTION_DIR, OVERRIDES_DIR, INTENTS_DIR, RECONCILED_DIR, NOTES_DIR];
|
|
81
82
|
|
|
82
83
|
function isBuildGraphStatePath(path) {
|
|
83
84
|
return BUILD_GRAPH_STATE_DIRS.some((dir) => path === dir || path.startsWith(`${dir}/`));
|
|
@@ -76,29 +76,22 @@ paragraph.
|
|
|
76
76
|
land correctly, and that the specific thing you changed shows up as
|
|
77
77
|
expected (a new skill directory copied over, a new host's files in the
|
|
78
78
|
right place, a new blueprint reachable from `hedgehog-core-design`).
|
|
79
|
-
5. **Commit with Conventional Commits
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
atomic commits, use the `conventional-commits` skill rather than
|
|
85
|
-
hand-rolling the split.
|
|
79
|
+
5. **Commit with Conventional Commits**, in the format the
|
|
80
|
+
`conventional-commits` skill states. If the change is already one
|
|
81
|
+
logical unit, commit it directly. If the working tree has accumulated
|
|
82
|
+
several unrelated changes that need splitting into atomic commits, use
|
|
83
|
+
the `conventional-commits` skill rather than hand-rolling the split.
|
|
86
84
|
6. **Push and open the PR**, following `pr-writing`'s checklist and shape
|
|
87
85
|
(CI passing, one change, only verified claims):
|
|
88
86
|
```bash
|
|
89
87
|
git push -u origin <branch-name>
|
|
90
|
-
gh pr create --repo skyf0xx/hedgehog --title "<type>(<scope>): <summary>" --body "
|
|
91
|
-
## Summary
|
|
92
|
-
<1-3 bullets: what changed and why>
|
|
93
|
-
|
|
94
|
-
## Test plan
|
|
95
|
-
- [ ] Ran `node bin/cli.mjs init` in a scratch dir and confirmed the change lands correctly
|
|
96
|
-
EOF
|
|
97
|
-
)"
|
|
88
|
+
gh pr create --repo skyf0xx/hedgehog --title "<type>(<scope>): <summary>" --body "..."
|
|
98
89
|
```
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
90
|
+
The body follows `pr-writing`'s shape (a short Summary, a Test plan
|
|
91
|
+
listing what was actually run — for this repo, `node bin/cli.mjs init`
|
|
92
|
+
in a scratch dir, confirming the change lands correctly). If the PR
|
|
93
|
+
closes or addresses a `ROADMAP.md` item or a filed issue, reference it
|
|
94
|
+
(`Addresses the "<item name>" item in ROADMAP.md`, or `Fixes #<n>`).
|
|
102
95
|
7. **Check CI** with `gh pr checks <number> --repo skyf0xx/hedgehog` after
|
|
103
96
|
opening. Fix a red check before asking for review.
|
|
104
97
|
8. **Report the PR URL** `gh` returns and stop — don't merge, don't push
|