@skyf0xx/hedgehog 2.0.13 → 3.0.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 +9 -3
- package/bin/cli.mjs +463 -19
- package/package.json +3 -2
- package/src/agents/backend-eng.md +56 -45
- package/src/agents/bootstrap.md +67 -73
- package/src/agents/front-end-eng.md +31 -18
- package/src/agents/planner.md +163 -84
- package/src/agents/reviewer.md +4 -4
- package/src/agents/tweaker.md +138 -106
- package/src/db/core.mjs +141 -0
- package/src/db/friction.mjs +25 -0
- package/src/db/init.mjs +35 -0
- package/src/db/intent.mjs +101 -0
- package/src/db/next.mjs +179 -0
- package/src/db/plan.mjs +222 -0
- package/src/db/schema.mjs +95 -0
- package/src/db/status.mjs +113 -0
- package/src/db/verify.mjs +286 -0
- package/src/db/why.mjs +97 -0
- package/src/golden-cores/full-stack-app/core.yaml +41 -0
- package/src/golden-cores/landing-page/core.yaml +41 -0
- package/src/skills/conventional-commits/SKILL.md +1 -1
- package/src/skills/hedgehog-bootstrap/SKILL.md +38 -41
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +8 -12
- package/src/skills/hedgehog-bootstrap-landing-page-core/SKILL.md +7 -9
- package/src/skills/hedgehog-core-design/SKILL.md +239 -0
- package/src/skills/hedgehog-landing-loop/SKILL.md +91 -57
- package/src/skills/hedgehog-loop/SKILL.md +109 -77
- package/src/skills/hedgehog-planning-intake/SKILL.md +72 -97
- package/src/templates/CLAUDE.core.full-stack-app.md +31 -24
- package/src/templates/CLAUDE.core.landing-page.md +11 -7
- package/src/templates/CLAUDE.md +46 -38
- package/src/templates/TODO.core.full-stack-app.md +0 -51
- package/src/templates/TODO.core.landing-page.md +0 -31
- package/src/templates/TODO.md +0 -12
package/src/db/next.mjs
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// `hedgehog next` — readiness query + task-packet assembly for one task.
|
|
2
|
+
// See hedgehog-persistent-build-graph.md, the readiness `SELECT` under
|
|
3
|
+
// "Schema", and "The task packet" / the `hedgehog next` output example.
|
|
4
|
+
//
|
|
5
|
+
// Readiness: a task with no dependency whose status isn't `complete`,
|
|
6
|
+
// lowest `priority` then `id`. Pickable status is `planned` OR `ready` —
|
|
7
|
+
// `plan.mjs` inserts new tasks as `planned`, but `verify.mjs`'s
|
|
8
|
+
// unlockReadyDependents sets a dependent's status to `ready` directly
|
|
9
|
+
// once its dependencies complete (see verify.mjs), without ever passing
|
|
10
|
+
// back through `planned`. A task already marked `ready` still has to
|
|
11
|
+
// satisfy the same no-incomplete-dependency condition here — the OR
|
|
12
|
+
// widens which statuses are eligible, it doesn't relax the dependency
|
|
13
|
+
// check itself. Once found, the packet is assembled by querying tasks
|
|
14
|
+
// joined through intents/requirements/task_requirements — never
|
|
15
|
+
// hand-written, never the whole plan.
|
|
16
|
+
|
|
17
|
+
const READY_TASK_SQL = `
|
|
18
|
+
SELECT t.* FROM tasks t
|
|
19
|
+
WHERE t.status IN ('planned', 'ready')
|
|
20
|
+
AND NOT EXISTS (
|
|
21
|
+
SELECT 1 FROM dependencies d
|
|
22
|
+
JOIN tasks dep ON dep.id = d.depends_on_task_id
|
|
23
|
+
WHERE d.task_id = t.id AND dep.status <> 'complete'
|
|
24
|
+
)
|
|
25
|
+
ORDER BY t.priority, t.id
|
|
26
|
+
LIMIT 1;
|
|
27
|
+
`;
|
|
28
|
+
|
|
29
|
+
function findReadyTask(db) {
|
|
30
|
+
return db.prepare(READY_TASK_SQL).get();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function loadIntent(db, intentId) {
|
|
34
|
+
return db.prepare('SELECT * FROM intents WHERE id = ?').get(intentId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function loadTaskRequirements(db, taskId) {
|
|
38
|
+
return db
|
|
39
|
+
.prepare(
|
|
40
|
+
`
|
|
41
|
+
SELECT r.* FROM requirements r
|
|
42
|
+
JOIN task_requirements tr ON tr.requirement_id = r.id
|
|
43
|
+
WHERE tr.task_id = ?
|
|
44
|
+
`,
|
|
45
|
+
)
|
|
46
|
+
.all(taskId);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function loadDirectDependents(db, taskId) {
|
|
50
|
+
return db
|
|
51
|
+
.prepare(
|
|
52
|
+
`
|
|
53
|
+
SELECT t.* FROM tasks t
|
|
54
|
+
JOIN dependencies d ON d.task_id = t.id
|
|
55
|
+
WHERE d.depends_on_task_id = ?
|
|
56
|
+
ORDER BY t.priority, t.id
|
|
57
|
+
`,
|
|
58
|
+
)
|
|
59
|
+
.all(taskId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The full transitive closure of tasks blocked on `taskId`, not just its
|
|
63
|
+
// direct dependents (spec example: BLOCKED DOWNSTREAM lists the ready
|
|
64
|
+
// task's entire remaining chain — contract, repository, service, screen —
|
|
65
|
+
// not only the one task directly depending on it). Walked breadth-first,
|
|
66
|
+
// de-duplicated, in dependency order.
|
|
67
|
+
function loadBlockedDownstream(db, taskId) {
|
|
68
|
+
const seen = new Set([taskId]);
|
|
69
|
+
const result = [];
|
|
70
|
+
let frontier = [taskId];
|
|
71
|
+
while (frontier.length > 0) {
|
|
72
|
+
const next = [];
|
|
73
|
+
for (const id of frontier) {
|
|
74
|
+
for (const dep of loadDirectDependents(db, id)) {
|
|
75
|
+
if (seen.has(dep.id)) continue;
|
|
76
|
+
seen.add(dep.id);
|
|
77
|
+
result.push(dep);
|
|
78
|
+
next.push(dep.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
frontier = next;
|
|
82
|
+
}
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Assembles the packet for `task` (already known ready) by querying its
|
|
87
|
+
// intent, requirements, and blocked downstream chain. Returns null fields
|
|
88
|
+
// never — every field here is NOT NULL on tasks, or defaults to an empty
|
|
89
|
+
// list.
|
|
90
|
+
function assemblePacket(db, task) {
|
|
91
|
+
const intent = loadIntent(db, task.intent_id);
|
|
92
|
+
const requirements = loadTaskRequirements(db, task.id);
|
|
93
|
+
const dependents = loadBlockedDownstream(db, task.id);
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
task,
|
|
97
|
+
intent,
|
|
98
|
+
requirements,
|
|
99
|
+
dependents,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Returns the assembled packet for the one ready task (spec: readiness
|
|
104
|
+
// SELECT), or null if no task is ready.
|
|
105
|
+
export function nextTask(db) {
|
|
106
|
+
const task = findReadyTask(db);
|
|
107
|
+
if (!task) return null;
|
|
108
|
+
return assemblePacket(db, task);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Tasks stalled awaiting a re-verify: `failed` (verification returned
|
|
112
|
+
// nonzero) or `implemented` (scope violation refused to run it). Neither
|
|
113
|
+
// is pickable by the readiness query, so when `nextTask` returns null the
|
|
114
|
+
// caller uses this to tell "the build is finished" apart from "the build
|
|
115
|
+
// is stuck on a task that needs fixing" — otherwise a failed task makes
|
|
116
|
+
// a blocked graph look complete.
|
|
117
|
+
export function stalledTasks(db) {
|
|
118
|
+
return db
|
|
119
|
+
.prepare(
|
|
120
|
+
`
|
|
121
|
+
SELECT t.* FROM tasks t
|
|
122
|
+
WHERE t.status IN ('failed', 'implemented')
|
|
123
|
+
ORDER BY t.priority, t.id
|
|
124
|
+
`,
|
|
125
|
+
)
|
|
126
|
+
.all();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Renders a packet into the STATUS / INTENT / RELEVANT RULES / WHY NOW /
|
|
130
|
+
// BLOCKED DOWNSTREAM / ALLOWED SCOPE / VERIFICATION format. The spec
|
|
131
|
+
// splits this across two examples — the `hedgehog next` display and "The
|
|
132
|
+
// task packet" (which carries the intent and its rules) — but an agent
|
|
133
|
+
// receives one thing, so the packet is one thing: everything the worker
|
|
134
|
+
// needs to build the task without reading the plan.
|
|
135
|
+
export function formatNext(packet) {
|
|
136
|
+
const { task, intent, requirements, dependents } = packet;
|
|
137
|
+
const scopeGlobs = JSON.parse(task.scope_globs);
|
|
138
|
+
|
|
139
|
+
const lines = [];
|
|
140
|
+
lines.push(`TASK ${task.id}`);
|
|
141
|
+
lines.push(task.objective);
|
|
142
|
+
lines.push('');
|
|
143
|
+
lines.push('STATUS READY');
|
|
144
|
+
lines.push('');
|
|
145
|
+
lines.push('INTENT');
|
|
146
|
+
lines.push(` ${intent.goal}`);
|
|
147
|
+
lines.push(` ${intent.outcome}`);
|
|
148
|
+
lines.push('');
|
|
149
|
+
lines.push('RELEVANT RULES');
|
|
150
|
+
if (requirements.length === 0) {
|
|
151
|
+
lines.push(' (none recorded)');
|
|
152
|
+
} else {
|
|
153
|
+
for (const req of requirements) {
|
|
154
|
+
lines.push(` - ${req.statement}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
lines.push('');
|
|
158
|
+
lines.push('WHY NOW');
|
|
159
|
+
lines.push(` ✓ Intent "${intent.id}" compiled into the graph`);
|
|
160
|
+
lines.push(` ✓ Domain module "${task.module}" resolved`);
|
|
161
|
+
lines.push(' ✓ No incomplete dependencies');
|
|
162
|
+
lines.push('');
|
|
163
|
+
lines.push('BLOCKED DOWNSTREAM');
|
|
164
|
+
if (dependents.length === 0) {
|
|
165
|
+
lines.push(' (none)');
|
|
166
|
+
} else {
|
|
167
|
+
for (const dep of dependents) {
|
|
168
|
+
lines.push(` ✗ ${dep.id} ${dep.layer}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
lines.push('');
|
|
172
|
+
lines.push('ALLOWED SCOPE');
|
|
173
|
+
for (const glob of scopeGlobs) lines.push(` ${glob}`);
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push('VERIFICATION');
|
|
176
|
+
lines.push(` ${task.verify_command}`);
|
|
177
|
+
|
|
178
|
+
return lines.join('\n');
|
|
179
|
+
}
|
package/src/db/plan.mjs
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// `hedgehog plan` — compiles pending intents against the project's core
|
|
2
|
+
// definition into `tasks` + `dependencies` rows. See
|
|
3
|
+
// hedgehog-persistent-build-graph.md, "The build graph" and "Task
|
|
4
|
+
// lifecycle", plus the readiness SELECT under "Schema".
|
|
5
|
+
//
|
|
6
|
+
// full-stack-app: one task per layer per intent (an intent is a domain
|
|
7
|
+
// module — see the core definition's `{module}` placeholder). landing-page:
|
|
8
|
+
// one task per phase, no module axis. Both are the same operation — walk
|
|
9
|
+
// a core definition's layer chain once per intent — because a linear
|
|
10
|
+
// chain is the degenerate case of the layer graph (spec: MVP scope
|
|
11
|
+
// item 5).
|
|
12
|
+
|
|
13
|
+
function fillModule(template, module) {
|
|
14
|
+
return template.replaceAll('{module}', module);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Deterministic, human-legible task id: <INTENT>-<LAYER>, upper-cased.
|
|
18
|
+
// Stable across repeated `hedgehog plan` runs on the same intent/layer.
|
|
19
|
+
function taskId(intentId, layerId) {
|
|
20
|
+
return `${intentId}-${layerId}`.toUpperCase();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Compiles one intent's tasks + intra-intent dependencies (mirroring the
|
|
24
|
+
// core definition's layer order) without touching the database.
|
|
25
|
+
function compileIntentTasks(intent, core) {
|
|
26
|
+
const module = intent.id;
|
|
27
|
+
const tasks = core.layers.map((layer) => ({
|
|
28
|
+
id: taskId(intent.id, layer.id),
|
|
29
|
+
intent_id: intent.id,
|
|
30
|
+
module,
|
|
31
|
+
layer: layer.id,
|
|
32
|
+
objective: `${layer.id} for ${module}`,
|
|
33
|
+
scope_globs: JSON.stringify(layer.scope.map((g) => fillModule(g, module))),
|
|
34
|
+
verify_command: fillModule(layer.verify, module),
|
|
35
|
+
commit_message: fillModule(layer.commit, module),
|
|
36
|
+
priority: intent.priority,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
const dependencies = [];
|
|
40
|
+
for (const layer of core.layers) {
|
|
41
|
+
if (!layer.depends_on) continue;
|
|
42
|
+
dependencies.push({
|
|
43
|
+
task_id: taskId(intent.id, layer.id),
|
|
44
|
+
depends_on_task_id: taskId(intent.id, layer.depends_on),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return { tasks, dependencies };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Reads pending intents (status 'proposed' or 'planned' — not yet
|
|
52
|
+
// compiled into tasks) in dependency order: an intent is compiled only
|
|
53
|
+
// after every intent it depends_on. Intents with no ordering constraint
|
|
54
|
+
// between them compile in `priority, id` order.
|
|
55
|
+
function orderIntents(intents, intentDependencies) {
|
|
56
|
+
const byId = new Map(intents.map((i) => [i.id, i]));
|
|
57
|
+
const dependsOn = new Map(intents.map((i) => [i.id, []]));
|
|
58
|
+
for (const { intent_id, depends_on_intent_id } of intentDependencies) {
|
|
59
|
+
if (!dependsOn.has(intent_id)) continue;
|
|
60
|
+
if (!byId.has(depends_on_intent_id)) continue;
|
|
61
|
+
dependsOn.get(intent_id).push(depends_on_intent_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ordered = [];
|
|
65
|
+
const visited = new Set();
|
|
66
|
+
const visiting = new Set();
|
|
67
|
+
|
|
68
|
+
function visit(id) {
|
|
69
|
+
if (visited.has(id)) return;
|
|
70
|
+
if (visiting.has(id)) {
|
|
71
|
+
throw new Error(`intent_dependencies cycle detected at "${id}"`);
|
|
72
|
+
}
|
|
73
|
+
visiting.add(id);
|
|
74
|
+
for (const depId of dependsOn.get(id) ?? []) visit(depId);
|
|
75
|
+
visiting.delete(id);
|
|
76
|
+
visited.add(id);
|
|
77
|
+
ordered.push(byId.get(id));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const remaining = [...intents].sort(
|
|
81
|
+
(a, b) => a.priority - b.priority || a.id.localeCompare(b.id),
|
|
82
|
+
);
|
|
83
|
+
for (const intent of remaining) visit(intent.id);
|
|
84
|
+
|
|
85
|
+
return ordered;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const PENDING_INTENT_STATUSES = ['proposed', 'planned'];
|
|
89
|
+
|
|
90
|
+
function loadPendingIntents(db) {
|
|
91
|
+
const placeholders = PENDING_INTENT_STATUSES.map(() => '?').join(',');
|
|
92
|
+
return db
|
|
93
|
+
.prepare(`SELECT * FROM intents WHERE status IN (${placeholders})`)
|
|
94
|
+
.all(...PENDING_INTENT_STATUSES);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function loadIntentDependencies(db) {
|
|
98
|
+
return db.prepare('SELECT * FROM intent_dependencies').all();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function loadIntentRequirements(db, intentId) {
|
|
102
|
+
return db
|
|
103
|
+
.prepare('SELECT id FROM requirements WHERE intent_id = ? ORDER BY id')
|
|
104
|
+
.all(intentId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function taskExists(db, taskId) {
|
|
108
|
+
return db.prepare('SELECT 1 FROM tasks WHERE id = ?').get(taskId) !== undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const insertTask = (db) =>
|
|
112
|
+
db.prepare(`
|
|
113
|
+
INSERT INTO tasks
|
|
114
|
+
(id, intent_id, module, layer, objective, scope_globs, verify_command,
|
|
115
|
+
commit_message, priority, status)
|
|
116
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'planned')
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
const insertDependency = (db) =>
|
|
120
|
+
db.prepare(`
|
|
121
|
+
INSERT OR IGNORE INTO dependencies (task_id, depends_on_task_id)
|
|
122
|
+
VALUES (?, ?)
|
|
123
|
+
`);
|
|
124
|
+
|
|
125
|
+
const insertTaskRequirement = (db) =>
|
|
126
|
+
db.prepare(`
|
|
127
|
+
INSERT OR IGNORE INTO task_requirements (task_id, requirement_id)
|
|
128
|
+
VALUES (?, ?)
|
|
129
|
+
`);
|
|
130
|
+
|
|
131
|
+
// Compiles every pending intent against `core` and writes tasks +
|
|
132
|
+
// dependencies to `db`. Idempotent per intent: an intent whose tasks
|
|
133
|
+
// already exist (by id) is skipped entirely, so re-running `hedgehog
|
|
134
|
+
// plan` after adding a new intent doesn't touch already-compiled ones.
|
|
135
|
+
//
|
|
136
|
+
// Cross-intent ordering: for each `intent_dependencies` edge (A depends
|
|
137
|
+
// on B), A's first task in layer order gets an extra `dependencies` row
|
|
138
|
+
// on B's last task in layer order — so A's chain cannot start until B's
|
|
139
|
+
// chain is entirely `complete`.
|
|
140
|
+
//
|
|
141
|
+
// Requirement linkage: every task an intent compiles to is linked to all
|
|
142
|
+
// of that intent's requirements via `task_requirements`. The compiler has
|
|
143
|
+
// no basis for splitting requirements across layers — a rule like
|
|
144
|
+
// "invitations expire after 7 days" constrains the schema, the service,
|
|
145
|
+
// and the screen alike — so each layer carries the whole set and the task
|
|
146
|
+
// packet shows the agent every rule its intent is bound by. Without this
|
|
147
|
+
// the traceability chain (spec: "Traceability") has no middle link:
|
|
148
|
+
// `hedgehog why` could reach the intent but never name the requirement a
|
|
149
|
+
// file satisfies.
|
|
150
|
+
export function planTasks(db, core) {
|
|
151
|
+
const intents = loadPendingIntents(db);
|
|
152
|
+
const intentDependencies = loadIntentDependencies(db);
|
|
153
|
+
const ordered = orderIntents(intents, intentDependencies);
|
|
154
|
+
|
|
155
|
+
const dependsOnByIntent = new Map();
|
|
156
|
+
for (const { intent_id, depends_on_intent_id } of intentDependencies) {
|
|
157
|
+
if (!dependsOnByIntent.has(intent_id)) dependsOnByIntent.set(intent_id, []);
|
|
158
|
+
dependsOnByIntent.get(intent_id).push(depends_on_intent_id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const firstLayerId = core.layers[0].id;
|
|
162
|
+
const lastLayerId = core.layers[core.layers.length - 1].id;
|
|
163
|
+
|
|
164
|
+
const runInsert = insertTask(db);
|
|
165
|
+
const runInsertDep = insertDependency(db);
|
|
166
|
+
const runInsertTaskReq = insertTaskRequirement(db);
|
|
167
|
+
|
|
168
|
+
const compiledIntentIds = [];
|
|
169
|
+
const skippedIntentIds = [];
|
|
170
|
+
|
|
171
|
+
db.exec('BEGIN');
|
|
172
|
+
try {
|
|
173
|
+
for (const intent of ordered) {
|
|
174
|
+
const firstTaskId = taskId(intent.id, firstLayerId);
|
|
175
|
+
if (taskExists(db, firstTaskId)) {
|
|
176
|
+
skippedIntentIds.push(intent.id);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const { tasks, dependencies } = compileIntentTasks(intent, core);
|
|
181
|
+
const requirementIds = loadIntentRequirements(db, intent.id).map((r) => r.id);
|
|
182
|
+
for (const t of tasks) {
|
|
183
|
+
runInsert.run(
|
|
184
|
+
t.id,
|
|
185
|
+
t.intent_id,
|
|
186
|
+
t.module,
|
|
187
|
+
t.layer,
|
|
188
|
+
t.objective,
|
|
189
|
+
t.scope_globs,
|
|
190
|
+
t.verify_command,
|
|
191
|
+
t.commit_message,
|
|
192
|
+
t.priority,
|
|
193
|
+
);
|
|
194
|
+
for (const requirementId of requirementIds) {
|
|
195
|
+
runInsertTaskReq.run(t.id, requirementId);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
for (const d of dependencies) {
|
|
199
|
+
runInsertDep.run(d.task_id, d.depends_on_task_id);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Cross-intent edge: this intent's first task can't be ready until
|
|
203
|
+
// every intent it depends_on has its last task complete.
|
|
204
|
+
for (const depIntentId of dependsOnByIntent.get(intent.id) ?? []) {
|
|
205
|
+
const depLastTaskId = taskId(depIntentId, lastLayerId);
|
|
206
|
+
runInsertDep.run(firstTaskId, depLastTaskId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// An intent whose tasks now exist is no longer `proposed` — it's
|
|
210
|
+
// been compiled into the graph and is being built.
|
|
211
|
+
db.prepare("UPDATE intents SET status = 'active' WHERE id = ?").run(intent.id);
|
|
212
|
+
|
|
213
|
+
compiledIntentIds.push(intent.id);
|
|
214
|
+
}
|
|
215
|
+
db.exec('COMMIT');
|
|
216
|
+
} catch (err) {
|
|
217
|
+
db.exec('ROLLBACK');
|
|
218
|
+
throw err;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return { compiled: compiledIntentIds, skipped: skippedIntentIds };
|
|
222
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// The Hedgehog build graph schema — see hedgehog-persistent-build-graph.md,
|
|
2
|
+
// "SQLite as build state" → Schema, for the source of truth these table
|
|
3
|
+
// definitions mirror verbatim.
|
|
4
|
+
|
|
5
|
+
export const SCHEMA_SQL = `
|
|
6
|
+
PRAGMA foreign_keys = ON;
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS intents (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
goal TEXT NOT NULL,
|
|
11
|
+
outcome TEXT NOT NULL,
|
|
12
|
+
priority INTEGER NOT NULL DEFAULT 100,
|
|
13
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
14
|
+
CHECK (status IN ('proposed','planned','active','complete')),
|
|
15
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS requirements (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
intent_id TEXT NOT NULL REFERENCES intents(id) ON DELETE CASCADE,
|
|
21
|
+
kind TEXT NOT NULL CHECK (kind IN ('rule','constraint','acceptance')),
|
|
22
|
+
statement TEXT NOT NULL
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
CREATE TABLE IF NOT EXISTS intent_dependencies (
|
|
26
|
+
intent_id TEXT NOT NULL REFERENCES intents(id) ON DELETE CASCADE,
|
|
27
|
+
depends_on_intent_id TEXT NOT NULL REFERENCES intents(id) ON DELETE CASCADE,
|
|
28
|
+
PRIMARY KEY (intent_id, depends_on_intent_id),
|
|
29
|
+
CHECK (intent_id <> depends_on_intent_id)
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
33
|
+
id TEXT PRIMARY KEY,
|
|
34
|
+
intent_id TEXT NOT NULL REFERENCES intents(id) ON DELETE CASCADE,
|
|
35
|
+
module TEXT NOT NULL,
|
|
36
|
+
layer TEXT NOT NULL,
|
|
37
|
+
objective TEXT NOT NULL,
|
|
38
|
+
scope_globs TEXT NOT NULL,
|
|
39
|
+
verify_command TEXT NOT NULL,
|
|
40
|
+
commit_message TEXT NOT NULL,
|
|
41
|
+
priority INTEGER NOT NULL DEFAULT 100,
|
|
42
|
+
-- Every value here is one the engine actually writes. A CHECK listing
|
|
43
|
+
-- states nothing can produce documents a lifecycle that doesn't exist
|
|
44
|
+
-- and invites writing one the engine can't handle.
|
|
45
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
46
|
+
CHECK (status IN ('proposed','planned','ready',
|
|
47
|
+
'implemented','verified',
|
|
48
|
+
'complete','failed')),
|
|
49
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
CREATE TABLE IF NOT EXISTS task_requirements (
|
|
53
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
54
|
+
requirement_id TEXT NOT NULL REFERENCES requirements(id) ON DELETE CASCADE,
|
|
55
|
+
PRIMARY KEY (task_id, requirement_id)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
CREATE TABLE IF NOT EXISTS dependencies (
|
|
59
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
60
|
+
depends_on_task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
61
|
+
PRIMARY KEY (task_id, depends_on_task_id),
|
|
62
|
+
CHECK (task_id <> depends_on_task_id)
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
CREATE TABLE IF NOT EXISTS artifacts (
|
|
66
|
+
id INTEGER PRIMARY KEY,
|
|
67
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
68
|
+
path TEXT NOT NULL,
|
|
69
|
+
kind TEXT NOT NULL CHECK (kind IN ('created','modified')),
|
|
70
|
+
commit_sha TEXT
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
CREATE TABLE IF NOT EXISTS verifications (
|
|
74
|
+
id INTEGER PRIMARY KEY,
|
|
75
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
76
|
+
command TEXT NOT NULL,
|
|
77
|
+
exit_code INTEGER,
|
|
78
|
+
output TEXT,
|
|
79
|
+
status TEXT NOT NULL CHECK (status IN ('passed','failed')),
|
|
80
|
+
ran_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
CREATE TABLE IF NOT EXISTS friction (
|
|
84
|
+
id INTEGER PRIMARY KEY,
|
|
85
|
+
task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
86
|
+
note TEXT NOT NULL,
|
|
87
|
+
logged_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
88
|
+
);
|
|
89
|
+
`;
|
|
90
|
+
|
|
91
|
+
// Applies the schema to an already-open node:sqlite DatabaseSync instance.
|
|
92
|
+
// Idempotent: safe to call against a DB that already has these tables.
|
|
93
|
+
export function applySchema(db) {
|
|
94
|
+
db.exec(SCHEMA_SQL);
|
|
95
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// `hedgehog status` — graph overview: task counts by status, and the
|
|
2
|
+
// current ready list. See hedgehog-persistent-build-graph.md, "Task
|
|
3
|
+
// lifecycle" for the status set and "The CLI is the only writer" for the
|
|
4
|
+
// `hedgehog status` line.
|
|
5
|
+
//
|
|
6
|
+
// The ready list reuses next.mjs's readiness query rather than
|
|
7
|
+
// reimplementing it: a task is pickable by `hedgehog next` when its
|
|
8
|
+
// status is `planned` or `ready` (plan.mjs inserts `planned`;
|
|
9
|
+
// verify.mjs's unlockReadyDependents sets `ready` directly) and it has
|
|
10
|
+
// no dependency whose status isn't `complete`. `hedgehog status` lists
|
|
11
|
+
// every task meeting that condition, not just the one `hedgehog next`
|
|
12
|
+
// would pick.
|
|
13
|
+
|
|
14
|
+
// The task lifecycle in order, matching the tasks CHECK constraint in
|
|
15
|
+
// schema.mjs exactly — every status the engine can write, and no others.
|
|
16
|
+
const TASK_STATUSES = [
|
|
17
|
+
'proposed',
|
|
18
|
+
'planned',
|
|
19
|
+
'ready',
|
|
20
|
+
'implemented',
|
|
21
|
+
'verified',
|
|
22
|
+
'complete',
|
|
23
|
+
'failed',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
const READY_TASKS_SQL = `
|
|
27
|
+
SELECT t.* FROM tasks t
|
|
28
|
+
WHERE t.status IN ('planned', 'ready')
|
|
29
|
+
AND NOT EXISTS (
|
|
30
|
+
SELECT 1 FROM dependencies d
|
|
31
|
+
JOIN tasks dep ON dep.id = d.depends_on_task_id
|
|
32
|
+
WHERE d.task_id = t.id AND dep.status <> 'complete'
|
|
33
|
+
)
|
|
34
|
+
ORDER BY t.priority, t.id;
|
|
35
|
+
`;
|
|
36
|
+
|
|
37
|
+
function countTasksByStatus(db) {
|
|
38
|
+
const rows = db
|
|
39
|
+
.prepare('SELECT status, COUNT(*) AS n FROM tasks GROUP BY status')
|
|
40
|
+
.all();
|
|
41
|
+
const counts = Object.fromEntries(TASK_STATUSES.map((s) => [s, 0]));
|
|
42
|
+
for (const row of rows) counts[row.status] = row.n;
|
|
43
|
+
return counts;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadReadyTasks(db) {
|
|
47
|
+
return db.prepare(READY_TASKS_SQL).all();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Tasks that need a human/agent decision before the graph can move again:
|
|
51
|
+
// `failed` (verification ran and returned nonzero) and `implemented` (a
|
|
52
|
+
// scope violation refused to run verification). Neither is pickable by
|
|
53
|
+
// `hedgehog next`, so without listing them here a build whose only
|
|
54
|
+
// remaining work is a failed task looks identical to a finished one —
|
|
55
|
+
// "READY (none)" with no indication anything is wrong. Listing them is
|
|
56
|
+
// what makes the fix-and-re-verify path in hedgehog-loop's Loop step 4
|
|
57
|
+
// discoverable from a fresh context.
|
|
58
|
+
const ATTENTION_TASKS_SQL = `
|
|
59
|
+
SELECT t.* FROM tasks t
|
|
60
|
+
WHERE t.status IN ('failed', 'implemented')
|
|
61
|
+
ORDER BY t.priority, t.id;
|
|
62
|
+
`;
|
|
63
|
+
|
|
64
|
+
function loadAttentionTasks(db) {
|
|
65
|
+
return db.prepare(ATTENTION_TASKS_SQL).all();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Returns { counts, ready, attention, total } — counts keyed by every
|
|
69
|
+
// status in the tasks CHECK constraint (present even at zero), ready the
|
|
70
|
+
// full list of currently-pickable tasks, attention the stalled tasks
|
|
71
|
+
// needing a re-verify, total the sum across all statuses.
|
|
72
|
+
export function graphStatus(db) {
|
|
73
|
+
const counts = countTasksByStatus(db);
|
|
74
|
+
const ready = loadReadyTasks(db);
|
|
75
|
+
const attention = loadAttentionTasks(db);
|
|
76
|
+
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
77
|
+
return { counts, ready, attention, total };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Renders a graphStatus() result into a plain-text overview: counts by
|
|
81
|
+
// status (only non-zero ones, in lifecycle order), then the ready list.
|
|
82
|
+
export function formatStatus({ counts, ready, attention, total }) {
|
|
83
|
+
const lines = [];
|
|
84
|
+
lines.push(`TASKS ${total}`);
|
|
85
|
+
lines.push('');
|
|
86
|
+
for (const status of TASK_STATUSES) {
|
|
87
|
+
if (counts[status] === 0) continue;
|
|
88
|
+
lines.push(` ${status.padEnd(12)} ${counts[status]}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push('');
|
|
91
|
+
lines.push('READY');
|
|
92
|
+
if (ready.length === 0) {
|
|
93
|
+
lines.push(' (none)');
|
|
94
|
+
} else {
|
|
95
|
+
for (const task of ready) {
|
|
96
|
+
lines.push(` ${task.id} ${task.layer} ${task.objective}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (attention && attention.length > 0) {
|
|
101
|
+
lines.push('');
|
|
102
|
+
lines.push('NEEDS ATTENTION');
|
|
103
|
+
for (const task of attention) {
|
|
104
|
+
const reason =
|
|
105
|
+
task.status === 'failed' ? 'verification failed' : 'scope violation';
|
|
106
|
+
lines.push(` ${task.id} ${task.layer} ${reason}`);
|
|
107
|
+
}
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(' Fix the work, then re-run: hedgehog verify <task-id>');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|