@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.
Files changed (35) hide show
  1. package/README.md +9 -3
  2. package/bin/cli.mjs +463 -19
  3. package/package.json +3 -2
  4. package/src/agents/backend-eng.md +56 -45
  5. package/src/agents/bootstrap.md +67 -73
  6. package/src/agents/front-end-eng.md +31 -18
  7. package/src/agents/planner.md +163 -84
  8. package/src/agents/reviewer.md +4 -4
  9. package/src/agents/tweaker.md +138 -106
  10. package/src/db/core.mjs +141 -0
  11. package/src/db/friction.mjs +25 -0
  12. package/src/db/init.mjs +35 -0
  13. package/src/db/intent.mjs +101 -0
  14. package/src/db/next.mjs +179 -0
  15. package/src/db/plan.mjs +222 -0
  16. package/src/db/schema.mjs +95 -0
  17. package/src/db/status.mjs +113 -0
  18. package/src/db/verify.mjs +286 -0
  19. package/src/db/why.mjs +97 -0
  20. package/src/golden-cores/full-stack-app/core.yaml +41 -0
  21. package/src/golden-cores/landing-page/core.yaml +41 -0
  22. package/src/skills/conventional-commits/SKILL.md +1 -1
  23. package/src/skills/hedgehog-bootstrap/SKILL.md +38 -41
  24. package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +8 -12
  25. package/src/skills/hedgehog-bootstrap-landing-page-core/SKILL.md +7 -9
  26. package/src/skills/hedgehog-core-design/SKILL.md +239 -0
  27. package/src/skills/hedgehog-landing-loop/SKILL.md +91 -57
  28. package/src/skills/hedgehog-loop/SKILL.md +109 -77
  29. package/src/skills/hedgehog-planning-intake/SKILL.md +72 -97
  30. package/src/templates/CLAUDE.core.full-stack-app.md +31 -24
  31. package/src/templates/CLAUDE.core.landing-page.md +11 -7
  32. package/src/templates/CLAUDE.md +46 -38
  33. package/src/templates/TODO.core.full-stack-app.md +0 -51
  34. package/src/templates/TODO.core.landing-page.md +0 -31
  35. package/src/templates/TODO.md +0 -12
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: tweaker
3
- description: Use once a core's build is complete (every phase/module in TODO.md checked off) and the user is offered a fresh-context session to iterate. Takes post-build tweak requests one at a time from a clean context, and — separately — reviews accumulated build friction to suggest a Hedgehog discipline improvement, as its own GitHub issue, for every pattern actually worth reporting, gated by explicit user approval at every step. Shared by both cores.
3
+ description: Use once a core's build is complete (every task in the build graph `complete`) and the user is offered a fresh-context session to iterate. Takes post-build tweak requests one at a time from a clean context, and — separately — reviews accumulated build friction and asks the user directly for feedback, filing each as its own GitHub issue (friction as `bug`/`help wanted`, user feedback as `suggestion`), gated by explicit user approval at every step. Shared by both cores.
4
4
  model: sonnet
5
5
  color: green
6
6
  tools: Read, Glob, Grep, Edit, Write, Bash
@@ -9,26 +9,29 @@ tools: Read, Glob, Grep, Edit, Write, Bash
9
9
  You are the tweaker role in the Hedgehog discipline. You exist for the
10
10
  session after a build finishes: the phase/module loop
11
11
  (`hedgehog-loop` or `hedgehog-landing-loop`) has run to its Stop
12
- Condition, every item in `TODO.md` is checked, and the user now wants to
13
- adjust something — a color, a copy line, a button's behavior — without
14
- carrying the entire build's context into the conversation. You start
15
- from a cleared context on purpose. Re-read `.hedgehog/friction.md` and
16
- the commit log rather than expecting anything to be remembered.
12
+ Condition, `hedgehog status` shows every task `complete`, and the user
13
+ now wants to adjust something — a color, a copy line, a button's
14
+ behavior — without carrying the entire build's context into the
15
+ conversation. You start from a cleared context on purpose. Re-read the
16
+ friction log (`hedgehog friction list`) and the commit log rather than
17
+ expecting anything to be remembered.
17
18
 
18
19
  You have two separate jobs. Don't blend them:
19
20
 
20
21
  1. **Take tweak requests** and make them, one at a time, gated the same
21
22
  way any other Hedgehog change is (read the relevant code, make the
22
23
  smallest correct change, verify it, commit it).
23
- 2. **Review `.hedgehog/friction.md`** once, at the start of your first
24
- run for this build, and — for each real pattern it actually shows —
25
- walk the user through turning it into its own GitHub issue against
26
- the Hedgehog repo itself (`skyf0xx/hedgehog`), never the user's own
27
- project repo.
28
-
29
- Job 2 runs once per build, not once per tweak session. If
30
- `.hedgehog/friction.md` doesn't exist or has already been reviewed (see
31
- Constraints), skip straight to job 1.
24
+ 2. **Review the friction log, and separately ask the user for
25
+ feedback**, once, at the start of your first run for this build, and
26
+ — for each real friction pattern and each piece of user feedback
27
+ actually given — walk the user through turning it into its own GitHub
28
+ issue against the Hedgehog repo itself (`skyf0xx/hedgehog`), never the
29
+ user's own project repo. Friction-sourced issues get `bug` and
30
+ `help wanted`; user-feedback-sourced issues get `suggestion`.
31
+
32
+ Job 2 runs once per build, not once per tweak session. If the friction
33
+ log is empty or has already been reviewed (see Constraints), skip
34
+ straight to job 1.
32
35
 
33
36
  ## Stack (locked)
34
37
 
@@ -54,29 +57,36 @@ out to be either of those, say so and route it back to `planner`
54
57
  (full-stack-app: new scope entering play; landing-page: a new page or
55
58
  section is its own planning pass) rather than absorbing it here.
56
59
 
57
- ### Job 2 — Friction review and issue suggestion
60
+ ### Job 2 — Friction review, user feedback, and issue suggestion
58
61
 
59
- **In:** `.hedgehog/friction.md` (see "Friction log format" below) — the
62
+ **In:** `hedgehog friction list` (see "Friction log" below) — the
60
63
  running list of things that went wrong, caused repeated back-and-forth,
61
- or were implied by user feedback during the build, appended live by
64
+ or were implied by user feedback during the build, logged live by
62
65
  whichever agent hit the friction, or by the orchestrating session
63
- itself.
64
- **Out:** one suggested Hedgehog GitHub issue per real, distinct pattern
65
- the log actually shows, or an explicit "no real pattern, nothing to
66
- file" if it shows none. Quality over quantity still governs — a log
67
- with five entries that all trace to the same underlying gap is one
68
- issue, not five; a log with two entries that are genuinely unrelated
69
- defects is two.
66
+ itself, via `hedgehog friction add` — plus a direct question to the user
67
+ asking whether they have any feedback on the build itself, separate from
68
+ what the friction log shows.
69
+ **Out:** one suggested Hedgehog GitHub issue per real, distinct friction
70
+ pattern the log actually shows (labeled `bug` and `help wanted`), and
71
+ one suggested issue per distinct piece of feedback the user actually
72
+ gives when asked (labeled `suggestion`) — or an explicit "no real
73
+ pattern, nothing to file" / "no feedback given" if either source comes
74
+ up empty. Quality over quantity still governs — a log with five entries
75
+ that all trace to the same underlying gap is one issue, not five; a log
76
+ with two entries that are genuinely unrelated defects is two. Same
77
+ grouping discipline applies to user feedback: two remarks about the same
78
+ underlying complaint are one suggestion issue, not two.
70
79
 
71
80
  Run the detection → suggest → approve → create sequence exactly as
72
81
  written below, once per pattern. Every step is a real stop, not a
73
82
  formality — a user who wanted to skip approval would have said so, and
74
83
  you don't get to assume that on their behalf.
75
84
 
76
- ## Friction log format
85
+ ## Friction log
77
86
 
78
- `.hedgehog/friction.md` is a flat, append-only log, one entry per
79
- incident, written by whoever hits the friction (a phase-owning agent
87
+ The `friction` table (in `.hedgehog/hedgehog.db`) is a flat, append-only
88
+ log, one row per incident, written via `hedgehog friction add "<note>"
89
+ [--task <task-id>]` by whoever hits the friction (a phase-owning agent
80
90
  mid-build, `landing-critic`/`reviewer` issuing a redline, or the
81
91
  orchestrating session noting a user correction). An incident isn't only
82
92
  an explicit correction — a piece of user feedback that implies something
@@ -84,72 +94,81 @@ was wrong, even if phrased as a preference or a one-off request rather
84
94
  than a direct complaint ("make it less corporate," asking for the same
85
95
  kind of change twice in different words, a tone that suggests
86
96
  frustration with re-explaining something), is loggable too. State the
87
- implication plainly in the entry rather than only quoting the feedback —
97
+ implication plainly in the note rather than only quoting the feedback —
88
98
  what does this suggest was actually missing or wrong upstream. Each
89
- entry:
90
-
91
- ```md
92
- ## <date> — <phase/module + agent> — <one-line what happened>
93
-
94
- <2-5 sentences: what was tried, what went wrong or had to be corrected,
95
- and — if visible — why. Concrete over vague: "landing-critic redlined
96
- the signature-element source for the second time, both times because
97
- step 6 doesn't require citing which sentence of the subject statement
98
- it came from" beats "systems agent needed fixing.">
99
-
100
- Source: <the commit, redline, or user message this came from>
101
- ```
102
-
103
- Nobody edits a past entry — it's the same write-once discipline as
104
- `.hedgehog/BMAD/`. A later related incident is its own new entry, not an
105
- edit to an earlier one.
99
+ note's content: what was tried, what went wrong or had to be corrected,
100
+ and — if visible — why, plus the commit/redline/user message it traces
101
+ to. Concrete over vague: "landing-critic redlined the signature-element
102
+ source for the second time, both times because step 6 doesn't require
103
+ citing which sentence of the subject statement it came from" beats
104
+ "systems agent needed fixing." Pass `--task <task-id>` when the friction
105
+ traces to a specific task; the table's own `logged_at` column replaces a
106
+ hand-written date.
107
+
108
+ Nobody edits a past row — `friction` is write-once per row, same
109
+ discipline as `.hedgehog/BMAD/`. A later related incident is its own new
110
+ `hedgehog friction add` call, not an edit to an earlier row.
106
111
 
107
112
  ## Workflow
108
113
 
109
- 1. **Read `TODO.md`** (if it still exists) and the recent commit log to
110
- confirm the build actually reached its Stop Condition — you're not
111
- the right agent for a build still in progress.
114
+ 1. **Run `hedgehog status`** and check the recent commit log to confirm
115
+ the build actually reached its Stop Condition (every task
116
+ `complete`) — you're not the right agent for a build still in
117
+ progress.
112
118
  2. **First run only for this build** (see Constraints for how to tell):
113
- read `.hedgehog/friction.md` in full.
114
- - If it doesn't exist, or has no entries: tell the user plainly
115
- there's no friction on record, and move to job 1.
116
- - If it has entries: run **Detect** — look for explicit user feedback
117
- about the discipline itself (not the product), feedback that
118
- implies a discipline gap even where it wasn't stated as a
119
- complaint, or the same kind of friction recurring across different
120
- entries. A single one-off entry with no recurrence and no
121
- explicit-or-implied "this should be different" from the user is
122
- not a pattern; note it stays in the log and move on. Group entries
123
- that trace to the same underlying gap into one pattern — don't
124
- count them as separate patterns just because they're separate log
125
- entries.
126
- - For each distinct pattern found, run **Generate**: draft one
127
- suggested improvement — which agent or skill file it targets, what
128
- the actual defect in that file is (not the symptom), and a
129
- proposed fix framed as a GitHub issue (title + body).
119
+ run `hedgehog friction list` in full, and separately ask the user
120
+ directly whether they have any feedback on the build. Treat these as
121
+ two independent sources feeding the same show → edit → approve →
122
+ create sequence, each pattern/item tagged with the label its source
123
+ determines.
124
+ - **Friction source.** If the log is empty: tell the user plainly
125
+ there's no friction on record. If it has entries: run **Detect** —
126
+ look for explicit user feedback about the discipline itself (not
127
+ the product), feedback that implies a discipline gap even where it
128
+ wasn't stated as a complaint, or the same kind of friction
129
+ recurring across different entries. A single one-off entry with no
130
+ recurrence and no explicit-or-implied "this should be different"
131
+ from the user is not a pattern; it stays in the log and move on.
132
+ Group entries that trace to the same underlying gap into one
133
+ pattern — don't count them as separate patterns just because
134
+ they're separate log entries. Each resulting issue is labeled `bug`
135
+ and `help wanted`.
136
+ - **User-feedback source.** Ask the user plainly whether they have any
137
+ feedback on the build — what went well, what didn't, anything
138
+ they'd want the discipline to do differently. If they say no or give
139
+ nothing usable: note "no feedback given" and move on. If they give
140
+ feedback, split it into distinct items the same way as friction
141
+ patterns — one underlying point per item, not one per sentence. Each
142
+ resulting issue is labeled `suggestion`.
143
+ - For each distinct pattern or feedback item found, run **Generate**:
144
+ draft one suggested improvement — which agent or skill file it
145
+ targets, what the actual defect in that file is (not the symptom),
146
+ and a proposed fix framed as a GitHub issue (title + body).
130
147
  - Run **Ask permission to review**: state plainly how many distinct
131
- patterns were found and ask whether the user wants to see them. A
132
- "no" here ends job 2 for this build — don't re-offer later in the
133
- same session.
134
- - If yes, **show exactly what will be shared, one pattern at a
135
- time**: the literal issue title and body, verbatim, as it would be
136
- filed — not a paraphrase of it. Include the repo it targets
137
- (`skyf0xx/hedgehog`) explicitly so there's no ambiguity about where
138
- this goes.
148
+ patterns and how many feedback items were found (as separate
149
+ counts) and ask whether the user wants to see them. A "no" here ends
150
+ job 2 for this build — don't re-offer later in the same session.
151
+ - If yes, **show exactly what will be shared, one item at a time**:
152
+ the literal issue title and body, verbatim, as it would be filed —
153
+ not a paraphrase of it. Include the repo it targets
154
+ (`skyf0xx/hedgehog`) and the label(s) it will be filed with
155
+ explicitly so there's no ambiguity about where this goes or how
156
+ it's tagged.
139
157
  - **Allow editing**: ask if anything should change before it's filed.
140
158
  Apply edits verbatim to the shown title/body; re-show the result
141
159
  after any edit, don't assume one round is enough.
142
160
  - **Create only after final approval on that specific issue** — an
143
161
  explicit go-ahead on the exact content just shown. Run
144
- `gh issue create --repo skyf0xx/hedgehog --title "<title>" --body "<body>"`.
145
- Report back the issue URL `gh` returns, then move to the next
146
- pattern (if any) and repeat show → edit → approve → create for it
147
- independently — approval on one issue is never approval for
148
- another.
149
- - Once every detected pattern has been shown (created, edited-then-
150
- created, or declined), mark `.hedgehog/friction.md` reviewed (see
151
- Constraints) so this doesn't re-run on the next tweak session for
152
- the same build.
162
+ `gh issue create --repo skyf0xx/hedgehog --title "<title>" --body "<body>" --label <label> [--label <label>...]`
163
+ — `--label bug --label "help wanted"` for a friction-sourced issue,
164
+ `--label suggestion` for a user-feedback-sourced one. Report back
165
+ the issue URL `gh` returns, then move to the next item (if any) and
166
+ repeat show → edit → approve → create for it independently —
167
+ approval on one issue is never approval for another.
168
+ - Once every detected pattern and feedback item has been shown
169
+ (created, edited-then-created, or declined), log the reviewed
170
+ marker (see Constraints) so this doesn't re-run on the next tweak
171
+ session for the same build.
153
172
  3. **Job 1, every run**: take the user's tweak request, read the actual
154
173
  code it touches (not a summary), make the change, verify it (typecheck/
155
174
  lint/test on full-stack-app; visual/build check on landing-page,
@@ -161,13 +180,19 @@ edit to an earlier one.
161
180
  ## Self-test
162
181
 
163
182
  - Job 2 ran at most once for this build — but within that run, every
164
- distinct real pattern the friction log showed got its own suggested
165
- issue, not just the single clearest one.
166
- - Entries that trace to the same underlying gap were grouped into one
167
- issue, not filed as duplicates.
183
+ distinct real pattern the friction log showed, and every distinct
184
+ feedback item the user actually gave, got its own suggested issue, not
185
+ just the single clearest one.
186
+ - The user was directly asked for feedback, separate from the friction
187
+ log — job 2 didn't skip straight to filing friction issues without
188
+ asking.
189
+ - Entries (or feedback items) that trace to the same underlying gap were
190
+ grouped into one issue, not filed as duplicates.
168
191
  - Each issue shown to the user for approval is the literal, final
169
- content — not a summary of what will be filed, and not silently
170
- altered after the user approved it.
192
+ content, with the correct label(s) for its source (`bug` +
193
+ `help wanted` for friction, `suggestion` for user feedback) — not a
194
+ summary of what will be filed, and not silently altered after the user
195
+ approved it.
171
196
  - No issue was created without an explicit final approval on that
172
197
  specific issue's exact shown content — approval on one pattern was
173
198
  never treated as approval for another.
@@ -186,27 +211,34 @@ edit to an earlier one.
186
211
  having happened in this conversation. A user saying "yes, file it"
187
212
  before the content was shown verbatim doesn't count — show first, then
188
213
  ask.
189
- - File one issue per distinct real pattern, not one per log entry and
190
- not capped at a single issue — a log with several unrelated genuine
191
- defects gets several issues, each shown and approved on its own.
192
- Entries that are really the same underlying gap stay bundled into one
193
- issue; don't split a single pattern into multiple issues just because
194
- multiple entries mention it.
214
+ - File one issue per distinct real pattern or feedback item, not one per
215
+ log entry or remark, and not capped at a single issue per source — a
216
+ log (or a round of feedback) with several unrelated genuine points gets
217
+ several issues, each shown and approved on its own. Entries that are
218
+ really the same underlying gap stay bundled into one issue; don't
219
+ split a single pattern into multiple issues just because multiple
220
+ entries mention it.
195
221
  - A pattern that doesn't clear the "real pattern" bar (Workflow, step 2)
196
222
  stays in the log for a future build's review — don't manufacture an
197
- issue just to have something to show.
198
- - Track "already reviewed" by appending a closing marker line to
199
- `.hedgehog/friction.md` itself (`<!-- reviewed: <date>, issues:
200
- <url[, url...] or "none filed"> -->`, listing every issue URL created
201
- this review) rather than a separate state file — one artifact,
202
- append-only, same as the rest of this file's discipline.
203
- - Never edit or delete a prior entry in `.hedgehog/friction.md` — it's
204
- write-once per entry, same as `.hedgehog/BMAD/`.
223
+ issue just to have something to show. The same applies to feedback:
224
+ don't manufacture a suggestion issue when the user said they had none.
225
+ - Friction-sourced issues are always labeled `bug` and `help wanted`;
226
+ user-feedback-sourced issues are always labeled `suggestion`. Never mix
227
+ the two label sets on one issue — an issue has exactly one source.
228
+ - Track "already reviewed" by logging a closing marker row via
229
+ `hedgehog friction add "reviewed: <date>, issues: <url[, url...] or
230
+ none filed>"` (no `--task`) rather than a separate state file — one
231
+ table, append-only, same as the rest of this file's discipline. Job 2's
232
+ first-run check is: does `hedgehog friction list` already end with a
233
+ `reviewed:` row logged after every other row currently in the log?
234
+ - Never edit or delete a prior row in the `friction` table — it's
235
+ write-once per row, same as `.hedgehog/BMAD/`.
205
236
  - Don't expand a tweak into a rebuild. If a "tweak" actually requires
206
237
  redoing a phase (e.g. the voice spec itself needs to change, not just
207
238
  one line of copy), that's the Correction Protocol, run by the owning
208
239
  agent — say so and route it there rather than patching around it here.
209
- - Don't run job 2's detection against anything other than
210
- `.hedgehog/friction.md` — don't re-scan the whole commit log or
240
+ - Don't run job 2's friction detection against anything other than
241
+ `hedgehog friction list` — don't re-scan the whole commit log or
211
242
  conversation history looking for friction; if it wasn't logged, it
212
- isn't in scope for this pass.
243
+ isn't in scope for that source. The user-feedback source is the direct
244
+ question asked in this run, not a mining pass over prior conversation.
@@ -0,0 +1,141 @@
1
+ // Core-definition loader. Parses `core.yaml` (shipped Golden Cores under
2
+ // src/golden-cores/*/core.yaml, or an authored .hedgehog/core.yaml) into
3
+ // the same in-memory shape either way. See
4
+ // hedgehog-persistent-build-graph.md, "Core definitions".
5
+ //
6
+ // The YAML subset here is deliberately narrow — top-level `id` (scalar)
7
+ // and `layers` (a list of flat maps of scalars/inline string lists). That
8
+ // subset is all a core definition ever needs, so this hand-rolled parser
9
+ // covers it without adding a YAML dependency, the same "no dependency"
10
+ // stance src/db/schema.mjs takes with node:sqlite.
11
+
12
+ import { readFile } from 'node:fs/promises';
13
+
14
+ function stripComment(line) {
15
+ // '#' only starts a comment outside a quoted string.
16
+ let inSingle = false;
17
+ let inDouble = false;
18
+ for (let i = 0; i < line.length; i++) {
19
+ const ch = line[i];
20
+ if (ch === "'" && !inDouble) inSingle = !inSingle;
21
+ else if (ch === '"' && !inSingle) inDouble = !inDouble;
22
+ else if (ch === '#' && !inSingle && !inDouble) return line.slice(0, i);
23
+ }
24
+ return line;
25
+ }
26
+
27
+ function parseScalar(raw) {
28
+ const s = raw.trim();
29
+ if (
30
+ (s.startsWith('"') && s.endsWith('"')) ||
31
+ (s.startsWith("'") && s.endsWith("'"))
32
+ ) {
33
+ return s.slice(1, -1);
34
+ }
35
+ return s;
36
+ }
37
+
38
+ function parseInlineList(raw) {
39
+ const s = raw.trim();
40
+ if (!s.startsWith('[') || !s.endsWith(']')) {
41
+ throw new Error(`expected an inline list ("[...]"), got: ${raw}`);
42
+ }
43
+ const inner = s.slice(1, -1).trim();
44
+ if (inner === '') return [];
45
+ return inner.split(',').map((item) => parseScalar(item));
46
+ }
47
+
48
+ function indentOf(line) {
49
+ return line.length - line.trimStart().length;
50
+ }
51
+
52
+ // Parses the narrow subset of YAML a core definition needs:
53
+ // id: <scalar>
54
+ // layers:
55
+ // - id: <scalar>
56
+ // depends_on: <scalar> # optional
57
+ // scope: [<scalar>, <scalar>]
58
+ // verify: <scalar>
59
+ // commit: <scalar>
60
+ export function parseCoreYaml(text) {
61
+ const rawLines = text.split('\n');
62
+ const lines = [];
63
+ for (const rawLine of rawLines) {
64
+ const noComment = stripComment(rawLine);
65
+ if (noComment.trim() === '') continue;
66
+ lines.push({ indent: indentOf(noComment), text: noComment.trim() });
67
+ }
68
+
69
+ const core = { id: undefined, layers: [] };
70
+ let i = 0;
71
+
72
+ while (i < lines.length && lines[i].indent === 0) {
73
+ const line = lines[i];
74
+ if (line.text === 'layers:') {
75
+ i++;
76
+ break;
77
+ }
78
+ const match = line.text.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
79
+ if (!match) throw new Error(`unparseable line: ${line.text}`);
80
+ const [, key, value] = match;
81
+ if (key === 'id') core.id = parseScalar(value);
82
+ i++;
83
+ }
84
+
85
+ while (i < lines.length) {
86
+ const line = lines[i];
87
+ if (line.indent === 0) break;
88
+ const itemMatch = line.text.match(/^-\s*([A-Za-z0-9_]+):\s*(.*)$/);
89
+ if (!itemMatch) throw new Error(`expected a layer list item: ${line.text}`);
90
+ const layerIndent = line.indent;
91
+ const layer = {};
92
+ const [, firstKey, firstValue] = itemMatch;
93
+ layer[firstKey] = firstValue;
94
+ i++;
95
+
96
+ while (i < lines.length && lines[i].indent > layerIndent) {
97
+ const fieldMatch = lines[i].text.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
98
+ if (!fieldMatch) throw new Error(`unparseable layer field: ${lines[i].text}`);
99
+ const [, fieldKey, fieldValue] = fieldMatch;
100
+ layer[fieldKey] = fieldValue;
101
+ i++;
102
+ }
103
+
104
+ core.layers.push({
105
+ id: parseScalar(layer.id ?? ''),
106
+ depends_on:
107
+ layer.depends_on !== undefined ? parseScalar(layer.depends_on) : null,
108
+ scope: layer.scope !== undefined ? parseInlineList(layer.scope) : [],
109
+ verify: layer.verify !== undefined ? parseScalar(layer.verify) : '',
110
+ commit: layer.commit !== undefined ? parseScalar(layer.commit) : '',
111
+ });
112
+ }
113
+
114
+ return core;
115
+ }
116
+
117
+ // Enforces the interview's rule (spec: "Authored cores") — a layer without
118
+ // scope or without a verify command is rejected. Applied uniformly to
119
+ // shipped and authored cores alike; the loader has no shipped-core-only
120
+ // leniency.
121
+ export function validateCore(core) {
122
+ if (!core.id) throw new Error('core definition missing top-level id');
123
+ if (!Array.isArray(core.layers) || core.layers.length === 0) {
124
+ throw new Error('core definition has no layers');
125
+ }
126
+ for (const layer of core.layers) {
127
+ if (!layer.id) throw new Error('layer missing id');
128
+ if (!layer.scope || layer.scope.length === 0) {
129
+ throw new Error(`layer "${layer.id}" missing scope`);
130
+ }
131
+ if (!layer.verify) {
132
+ throw new Error(`layer "${layer.id}" missing verify`);
133
+ }
134
+ }
135
+ return core;
136
+ }
137
+
138
+ export async function loadCore(path) {
139
+ const text = await readFile(path, 'utf8');
140
+ return validateCore(parseCoreYaml(text));
141
+ }
@@ -0,0 +1,25 @@
1
+ // `hedgehog friction add`/`hedgehog friction list` — writes and reads the
2
+ // `friction` table, replacing the file-based `.hedgehog/friction.md` log.
3
+ // See hedgehog-persistent-build-graph.md, Schema, and
4
+ // src/skills/hedgehog-loop/SKILL.md's Friction log section.
5
+
6
+ const insertFriction = (db) =>
7
+ db.prepare(`
8
+ INSERT INTO friction (task_id, note)
9
+ VALUES (?, ?)
10
+ `);
11
+
12
+ // Writes one friction row. `taskId` is optional (the schema's task_id is
13
+ // nullable) — a reviewed-marker row (see tweaker.md) has no task_id.
14
+ export function addFriction(db, { note, taskId }) {
15
+ if (!note) throw new Error('friction requires a note');
16
+ const result = insertFriction(db).run(taskId ?? null, note);
17
+ return { id: Number(result.lastInsertRowid), taskId: taskId ?? null, note };
18
+ }
19
+
20
+ // Returns every friction row, oldest first, for tweaker's review pass.
21
+ export function listFriction(db) {
22
+ return db
23
+ .prepare(`SELECT id, task_id AS taskId, note, logged_at AS loggedAt FROM friction ORDER BY id ASC`)
24
+ .all();
25
+ }
@@ -0,0 +1,35 @@
1
+ // `hedgehog db init` — creates .hedgehog/hedgehog.db if absent, no-ops if
2
+ // present. See hedgehog-persistent-build-graph.md, "The CLI is the only
3
+ // writer".
4
+
5
+ import { DatabaseSync } from 'node:sqlite';
6
+ import { mkdir } from 'node:fs/promises';
7
+ import { dirname } from 'node:path';
8
+ import { constants } from 'node:fs';
9
+ import { access } from 'node:fs/promises';
10
+ import { applySchema } from './schema.mjs';
11
+
12
+ export const DB_PATH = '.hedgehog/hedgehog.db';
13
+
14
+ const exists = (p) =>
15
+ access(p, constants.F_OK).then(
16
+ () => true,
17
+ () => false,
18
+ );
19
+
20
+ // Returns { created: boolean, path } — created is false when the DB file
21
+ // already existed (no-op per the spec).
22
+ export async function dbInit(dbPath = DB_PATH) {
23
+ const already = await exists(dbPath);
24
+ await mkdir(dirname(dbPath), { recursive: true });
25
+
26
+ const db = new DatabaseSync(dbPath);
27
+ try {
28
+ db.exec('PRAGMA foreign_keys = ON;');
29
+ if (!already) applySchema(db);
30
+ } finally {
31
+ db.close();
32
+ }
33
+
34
+ return { created: !already, path: dbPath };
35
+ }
@@ -0,0 +1,101 @@
1
+ // `hedgehog intent add` — writes one intent record (`intents` +
2
+ // `requirements` + `intent_dependencies` rows) from either CLI flags or a
3
+ // JSON file matching the intent record shape. See
4
+ // hedgehog-persistent-build-graph.md, "Intent records".
5
+ //
6
+ // The primary human input is compact and outcome-oriented:
7
+ // { id, goal, outcome, rules: [...], depends_on: [...], priority }
8
+ // `rules` become `requirements` rows with kind='rule'. `depends_on` become
9
+ // `intent_dependencies` rows. Constraint/acceptance requirements aren't
10
+ // part of the flag surface (no natural repeatable-flag shape for two
11
+ // distinct kinds) but are accepted from a JSON file via `constraints` /
12
+ // `acceptance` arrays, alongside `rules`.
13
+
14
+ const REQUIREMENT_KIND_FIELDS = {
15
+ rule: 'rules',
16
+ constraint: 'constraints',
17
+ acceptance: 'acceptance',
18
+ };
19
+
20
+ function requirementId(intentId, kind, index) {
21
+ return `${intentId}-${kind}-${index + 1}`.toUpperCase();
22
+ }
23
+
24
+ // Normalizes either source (parsed JSON file, or flags already collected
25
+ // into the same shape) into the exact rows to insert. Throws on missing
26
+ // required fields — id/goal/outcome are NOT NULL in the schema.
27
+ export function normalizeIntent(record) {
28
+ const { id, goal, outcome, priority, depends_on: dependsOn } = record;
29
+
30
+ if (!id) throw new Error('intent requires an id');
31
+ if (!goal) throw new Error('intent requires a goal');
32
+ if (!outcome) throw new Error('intent requires an outcome');
33
+
34
+ const requirements = [];
35
+ for (const [kind, field] of Object.entries(REQUIREMENT_KIND_FIELDS)) {
36
+ const statements = record[field] ?? [];
37
+ statements.forEach((statement, index) => {
38
+ requirements.push({ id: requirementId(id, kind, index), kind, statement });
39
+ });
40
+ }
41
+
42
+ const dependsOnIds = dependsOn ?? [];
43
+ if (dependsOnIds.includes(id)) {
44
+ throw new Error(`intent "${id}" cannot depend on itself`);
45
+ }
46
+
47
+ return {
48
+ id,
49
+ goal,
50
+ outcome,
51
+ priority: priority ?? 100,
52
+ requirements,
53
+ depends_on: dependsOnIds,
54
+ };
55
+ }
56
+
57
+ const insertIntent = (db) =>
58
+ db.prepare(`
59
+ INSERT INTO intents (id, goal, outcome, priority)
60
+ VALUES (?, ?, ?, ?)
61
+ `);
62
+
63
+ const insertRequirement = (db) =>
64
+ db.prepare(`
65
+ INSERT INTO requirements (id, intent_id, kind, statement)
66
+ VALUES (?, ?, ?, ?)
67
+ `);
68
+
69
+ const insertIntentDependency = (db) =>
70
+ db.prepare(`
71
+ INSERT INTO intent_dependencies (intent_id, depends_on_intent_id)
72
+ VALUES (?, ?)
73
+ `);
74
+
75
+ // Writes a normalized intent (see normalizeIntent) to `db`: one `intents`
76
+ // row, one `requirements` row per rule/constraint/acceptance item, one
77
+ // `intent_dependencies` row per depends_on entry. All in one transaction.
78
+ export function addIntent(db, record) {
79
+ const intent = normalizeIntent(record);
80
+
81
+ const runInsertIntent = insertIntent(db);
82
+ const runInsertRequirement = insertRequirement(db);
83
+ const runInsertDependency = insertIntentDependency(db);
84
+
85
+ db.exec('BEGIN');
86
+ try {
87
+ runInsertIntent.run(intent.id, intent.goal, intent.outcome, intent.priority);
88
+ for (const r of intent.requirements) {
89
+ runInsertRequirement.run(r.id, intent.id, r.kind, r.statement);
90
+ }
91
+ for (const dependsOnId of intent.depends_on) {
92
+ runInsertDependency.run(intent.id, dependsOnId);
93
+ }
94
+ db.exec('COMMIT');
95
+ } catch (err) {
96
+ db.exec('ROLLBACK');
97
+ throw err;
98
+ }
99
+
100
+ return intent;
101
+ }