@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/agents/tweaker.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: tweaker
|
|
3
|
-
description: Use once a core's build is complete (every
|
|
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,
|
|
13
|
-
adjust something — a color, a copy line, a button's
|
|
14
|
-
carrying the entire build's context into the
|
|
15
|
-
from a cleared context on purpose. Re-read
|
|
16
|
-
the commit log rather than
|
|
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
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
the
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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:**
|
|
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,
|
|
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
|
-
|
|
65
|
-
the log
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
|
85
|
+
## Friction log
|
|
77
86
|
|
|
78
|
-
`.hedgehog/
|
|
79
|
-
incident, written
|
|
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
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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. **
|
|
110
|
-
|
|
111
|
-
the right agent for a build still in
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
same session.
|
|
134
|
-
- If yes, **show exactly what will be shared, one
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
(`skyf0xx/hedgehog`)
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
the
|
|
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
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
|
170
|
-
|
|
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
|
|
190
|
-
not capped at a single issue — a
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
multiple
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
package/src/db/core.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/db/init.mjs
ADDED
|
@@ -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
|
+
}
|