@skyf0xx/hedgehog 4.1.0 → 4.2.2
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 +21 -1
- package/bin/cli.mjs +108 -8
- package/package.json +2 -2
- package/src/agents/backend-eng.md +62 -16
- package/src/agents/front-end-eng.md +48 -14
- package/src/agents/reviewer.md +9 -4
- package/src/agents/ux-planner.md +2 -2
- package/src/db/next.mjs +95 -8
- package/src/db/status.mjs +140 -15
- package/src/golden-cores/full-stack-app/.env.example +1 -0
- package/src/golden-cores/full-stack-app/apps/api/package.json +19 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.spec.ts +15 -0
- package/src/golden-cores/full-stack-app/apps/api/src/app/app.module.ts +6 -1
- package/src/golden-cores/full-stack-app/apps/api/src/app/feature-modules.ts +8 -0
- package/src/golden-cores/full-stack-app/apps/api/src/main.ts +10 -1
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.app.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.json +3 -0
- package/src/golden-cores/full-stack-app/apps/api/tsconfig.spec.json +36 -0
- package/src/golden-cores/full-stack-app/apps/api/vitest.config.mts +18 -0
- package/src/golden-cores/full-stack-app/apps/web/.env.example +12 -0
- package/src/golden-cores/full-stack-app/apps/web/package.json +3 -0
- package/src/golden-cores/full-stack-app/apps/web/src/components/theme-toggle.spec.tsx +20 -0
- package/src/golden-cores/full-stack-app/apps/web/src/test-setup.ts +1 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.json +6 -0
- package/src/golden-cores/full-stack-app/apps/web/tsconfig.spec.json +37 -0
- package/src/golden-cores/full-stack-app/apps/web/vitest.config.mts +27 -0
- package/src/golden-cores/full-stack-app/core.yaml +11 -3
- package/src/golden-cores/full-stack-app/nx.json +4 -1
- package/src/golden-cores/full-stack-app/package.json +8 -0
- package/src/golden-cores/full-stack-app/packages/config/eslint-base.js +108 -13
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.spec.ts +14 -1
- package/src/golden-cores/full-stack-app/packages/config/src/env.schema.ts +1 -0
- package/src/golden-cores/full-stack-app/packages/config/src/index.ts +1 -1
- package/src/golden-cores/full-stack-app/packages/db/src/index.ts +2 -1
- package/src/golden-cores/full-stack-app/packages/db/src/lib/db.spec.ts +3 -3
- package/src/golden-cores/full-stack-app/packages/db/src/schema/index.ts +4 -0
- package/src/golden-cores/full-stack-app/pnpm-lock.yaml +8735 -2907
- package/src/golden-cores/full-stack-app/pnpm-workspace.yaml +4 -0
- package/src/golden-cores/full-stack-app/tools/generate-feature-modules.cjs +104 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/generator.ts +283 -0
- package/src/golden-cores/full-stack-app/tools/generators/contract/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/generator.ts +323 -0
- package/src/golden-cores/full-stack-app/tools/generators/controller/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/fields.ts +126 -0
- package/src/golden-cores/full-stack-app/tools/generators/generators.json +42 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/generator.ts +274 -0
- package/src/golden-cores/full-stack-app/tools/generators/hook/schema.json +19 -0
- package/src/golden-cores/full-stack-app/tools/generators/lib-shell.ts +124 -0
- package/src/golden-cores/full-stack-app/tools/generators/naming.ts +84 -0
- package/src/golden-cores/full-stack-app/tools/generators/package.json +6 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/generator.ts +298 -0
- package/src/golden-cores/full-stack-app/tools/generators/repository/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/generator.ts +169 -0
- package/src/golden-cores/full-stack-app/tools/generators/schema/schema.json +20 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/generator.ts +218 -0
- package/src/golden-cores/full-stack-app/tools/generators/screen/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/generator.ts +194 -0
- package/src/golden-cores/full-stack-app/tools/generators/service/schema.json +15 -0
- package/src/golden-cores/full-stack-app/tsconfig.base.json +2 -2
- package/src/skills/conventional-commits/SKILL.md +2 -3
- package/src/skills/hedgehog-bootstrap/SKILL.md +25 -7
- package/src/skills/hedgehog-bootstrap-full-stack-app-core/SKILL.md +135 -20
- package/src/skills/hedgehog-loop/SKILL.md +161 -8
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Turn AI from a code generator into a reliable software engineer ⭐
|
|
2
2
|
|
|
3
|
-
[](https://www.npmjs.com/package/@skyf0xx/hedgehog)
|
|
4
4
|
|
|
5
5
|
AI can write code in seconds.
|
|
6
6
|
|
|
@@ -68,6 +68,19 @@ Every dependency is explicit, so Hedgehog knows which tasks can run in parallel.
|
|
|
68
68
|
|
|
69
69
|
Agents fan out to give you great outcomes at **faster speeds**.
|
|
70
70
|
|
|
71
|
+
## Deterministic Code Generation
|
|
72
|
+
|
|
73
|
+
Where a piece of code has one correct shape, Hedgehog generates it instead of asking the AI to write it freehand.
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
- **Generators over freehand**: schemas, boilerplate, and scaffolding come from generators that create the code
|
|
78
|
+
- **Faster**: no reasoning required to produce a known shape
|
|
79
|
+
- **More correct**: the same input always produces the same, tested output
|
|
80
|
+
- **Fewer tokens**: nothing spent generating code that a template already covers
|
|
81
|
+
|
|
82
|
+
The AI is reserved for the parts a generator can't cover.
|
|
83
|
+
|
|
71
84
|
## What Hedgehog builds
|
|
72
85
|
|
|
73
86
|
### Full-stack applications
|
|
@@ -138,6 +151,13 @@ npx @skyf0xx/hedgehog init
|
|
|
138
151
|
|
|
139
152
|
Then open your coding agent and describe what you want to build.
|
|
140
153
|
|
|
154
|
+
The golden cores print a `pnpm install` step as part of their next steps.
|
|
155
|
+
On a fresh project with no warm pnpm store, that first install can take
|
|
156
|
+
several minutes — it's pulling a full monorepo toolchain (Nx, webpack,
|
|
157
|
+
sass-embedded, Playwright, etc.) and, on first commit, running the commit
|
|
158
|
+
gate against the whole workspace. A quiet stretch of output during that
|
|
159
|
+
step is expected, not a hang.
|
|
160
|
+
|
|
141
161
|
### Coding agents
|
|
142
162
|
|
|
143
163
|
Hedgehog installs for **Claude Code** by default. Add a host flag to
|
package/bin/cli.mjs
CHANGED
|
@@ -504,6 +504,18 @@ async function init({ force, core, explicitCore, host = DEFAULT_HOST, hostOnly =
|
|
|
504
504
|
// runs the whole workspace (see lefthook.yml), so expect it to take
|
|
505
505
|
// as long as a full typecheck/lint/test — that is the gate working.
|
|
506
506
|
console.log(` 1. ${bold('pnpm install')}`);
|
|
507
|
+
console.log(
|
|
508
|
+
dim(' First run can take several minutes — full dependency install plus'),
|
|
509
|
+
);
|
|
510
|
+
console.log(dim(' the commit gate running against the whole workspace. This is'));
|
|
511
|
+
console.log(dim(' expected; let it finish rather than treating a quiet stretch as stuck.'));
|
|
512
|
+
console.log(
|
|
513
|
+
dim(' If an agent is running this: tell the user up front that first install'),
|
|
514
|
+
);
|
|
515
|
+
console.log(
|
|
516
|
+
dim(' can take several minutes and why, then check on progress every ~30s'),
|
|
517
|
+
);
|
|
518
|
+
console.log(dim(' instead of polling tightly or narrating the wait.'));
|
|
507
519
|
console.log(` 2. ${bold('git add -A && git commit -m "chore: install Hedgehog"')}`);
|
|
508
520
|
console.log(` 3. Open ${HOSTS[host].label} and describe what you want to build.`);
|
|
509
521
|
} else {
|
|
@@ -653,6 +665,20 @@ async function resolveCorePath() {
|
|
|
653
665
|
return null;
|
|
654
666
|
}
|
|
655
667
|
|
|
668
|
+
// The active core's `id` (e.g. `full-stack-app`), or null when no core has
|
|
669
|
+
// landed yet or it fails to parse — packet rendering degrades gracefully
|
|
670
|
+
// either way (see next.mjs's layerShapeLines), so a caller only asking for
|
|
671
|
+
// the id doesn't need its own try/catch.
|
|
672
|
+
async function resolveCoreId() {
|
|
673
|
+
const corePath = await resolveCorePath();
|
|
674
|
+
if (!corePath) return null;
|
|
675
|
+
try {
|
|
676
|
+
return (await loadCore(corePath)).id;
|
|
677
|
+
} catch {
|
|
678
|
+
return null;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
656
682
|
// `hedgehog plan --recompile` — the reconciliation path for a core.yaml
|
|
657
683
|
// edited after `plan` already compiled it.
|
|
658
684
|
//
|
|
@@ -973,7 +999,7 @@ async function nextCommand() {
|
|
|
973
999
|
console.error('');
|
|
974
1000
|
}
|
|
975
1001
|
|
|
976
|
-
console.log(formatNext(packet));
|
|
1002
|
+
console.log(formatNext(packet, await resolveCoreId()));
|
|
977
1003
|
}
|
|
978
1004
|
|
|
979
1005
|
function printStalledTasks(stalled) {
|
|
@@ -1043,6 +1069,49 @@ async function missingBinariesForTask(db, taskId, owner) {
|
|
|
1043
1069
|
return missing.length === 0 ? null : { layer: layer.id, missing };
|
|
1044
1070
|
}
|
|
1045
1071
|
|
|
1072
|
+
// What an out-of-scope path most likely is, and what to do about it.
|
|
1073
|
+
// "Outside allowed scope" is true of every offending path and tells the
|
|
1074
|
+
// reader nothing about which of three quite different situations they're
|
|
1075
|
+
// in: a package shell the layer itself had to create (widen this one
|
|
1076
|
+
// task), shared config a source-level fix touched (commit it separately),
|
|
1077
|
+
// or a genuine stray. Each wants a different next move, and working out
|
|
1078
|
+
// which costs a beat every time.
|
|
1079
|
+
//
|
|
1080
|
+
// Evidence-only, like core.mjs's lint: an unrecognised path gets no
|
|
1081
|
+
// annotation rather than a guessed one.
|
|
1082
|
+
const SCOPE_HINTS = [
|
|
1083
|
+
{
|
|
1084
|
+
// A package root's own scaffolding — package.json, tsconfig*.json,
|
|
1085
|
+
// vitest.config.mts, src/index.ts directly under packages/<pkg>/ or
|
|
1086
|
+
// libs/<a>/<b>/. No {module}-bearing scope glob can cover these, so
|
|
1087
|
+
// the first module through a layer that creates its package always
|
|
1088
|
+
// lands here.
|
|
1089
|
+
test: (p) =>
|
|
1090
|
+
/^(packages\/[^/]+|libs\/[^/]+\/[^/]+)\/(package\.json|tsconfig[^/]*\.json|vitest\.config\.[cm]?ts|project\.json|eslint\.config\.[cm]?js|src\/index\.ts)$/.test(
|
|
1091
|
+
p,
|
|
1092
|
+
),
|
|
1093
|
+
hint: 'package shell — if this layer is the first to create this package, widen just this task with `hedgehog override add` (see hedgehog-loop, "First arrival in a package") and retry',
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
test: (p) =>
|
|
1097
|
+
p === 'pnpm-workspace.yaml' ||
|
|
1098
|
+
p === 'pnpm-lock.yaml' ||
|
|
1099
|
+
p === 'tsconfig.json' ||
|
|
1100
|
+
p === 'nx.json' ||
|
|
1101
|
+
p === 'package.json' ||
|
|
1102
|
+
p.startsWith('packages/config/'),
|
|
1103
|
+
hint: 'shared workspace config — no layer owns it; commit it separately as its own `chore(workspace): …` before retrying',
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
test: (p) => p.startsWith('.hedgehog/'),
|
|
1107
|
+
hint: 'build-graph state — intents, overrides and friction are committed by the command that writes them, not by a layer',
|
|
1108
|
+
},
|
|
1109
|
+
];
|
|
1110
|
+
|
|
1111
|
+
function scopeHintFor(path) {
|
|
1112
|
+
return SCOPE_HINTS.find((h) => h.test(path))?.hint ?? null;
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1046
1115
|
async function verifyCommand(args) {
|
|
1047
1116
|
await ensureDb();
|
|
1048
1117
|
|
|
@@ -1093,7 +1162,11 @@ async function verifyCommand(args) {
|
|
|
1093
1162
|
if (result.outcome === 'scope_violation') {
|
|
1094
1163
|
console.error(`${red(bold('Scope violation.'))} Task ${bold(taskId)} is now ${bold('blocked')}.\n`);
|
|
1095
1164
|
console.error('Touched paths outside allowed scope:');
|
|
1096
|
-
for (const path of result.offending)
|
|
1165
|
+
for (const path of result.offending) {
|
|
1166
|
+
console.error(` ${red('✗')} ${path}`);
|
|
1167
|
+
const hint = scopeHintFor(path);
|
|
1168
|
+
if (hint) console.error(` ${dim(hint)}`);
|
|
1169
|
+
}
|
|
1097
1170
|
console.error();
|
|
1098
1171
|
process.exitCode = 1;
|
|
1099
1172
|
return;
|
|
@@ -1145,14 +1218,15 @@ async function verifyCommand(args) {
|
|
|
1145
1218
|
// doesn't print the packet here, the STATUS/ALLOWED SCOPE/VERIFICATION an
|
|
1146
1219
|
// agent is supposed to be dispatched with is no longer reachable from any
|
|
1147
1220
|
// command. (`hedgehog show <task-id>` reprints it later.)
|
|
1148
|
-
function printPackets(tasks) {
|
|
1221
|
+
async function printPackets(tasks) {
|
|
1222
|
+
const coreId = await resolveCoreId();
|
|
1149
1223
|
const db = openDb();
|
|
1150
1224
|
try {
|
|
1151
1225
|
for (const task of tasks) {
|
|
1152
1226
|
const packet = taskPacket(db, task.id);
|
|
1153
1227
|
if (!packet) continue;
|
|
1154
1228
|
console.log();
|
|
1155
|
-
console.log(formatPacket(packet, taskStatusLine(packet.task)));
|
|
1229
|
+
console.log(formatPacket(packet, taskStatusLine(packet.task), coreId));
|
|
1156
1230
|
}
|
|
1157
1231
|
} finally {
|
|
1158
1232
|
db.close();
|
|
@@ -1239,7 +1313,7 @@ async function claimCommand(args) {
|
|
|
1239
1313
|
if (claimed.length > 1) console.log(` ${bold(task.id)}`);
|
|
1240
1314
|
console.log(` ${dim('expires')} ${task.lease_expires_at}`);
|
|
1241
1315
|
}
|
|
1242
|
-
printPackets(claimed);
|
|
1316
|
+
await printPackets(claimed);
|
|
1243
1317
|
}
|
|
1244
1318
|
|
|
1245
1319
|
// The targeted half of `claim`: one named task, or a non-zero exit
|
|
@@ -1258,7 +1332,7 @@ async function claimOneCommand(taskId, owner) {
|
|
|
1258
1332
|
if (result.claimed) {
|
|
1259
1333
|
console.log(`${green(bold('Claimed.'))} Task ${bold(taskId)} leased to ${bold(owner)}.`);
|
|
1260
1334
|
console.log(` ${dim('expires')} ${result.task.lease_expires_at}`);
|
|
1261
|
-
printPackets([result.task]);
|
|
1335
|
+
await printPackets([result.task]);
|
|
1262
1336
|
return;
|
|
1263
1337
|
}
|
|
1264
1338
|
|
|
@@ -1382,7 +1456,7 @@ async function showCommand(args) {
|
|
|
1382
1456
|
return;
|
|
1383
1457
|
}
|
|
1384
1458
|
|
|
1385
|
-
console.log(formatPacket(packet, taskStatusLine(packet.task)));
|
|
1459
|
+
console.log(formatPacket(packet, taskStatusLine(packet.task), await resolveCoreId()));
|
|
1386
1460
|
}
|
|
1387
1461
|
|
|
1388
1462
|
// `hedgehog release <task-id> --owner <owner>` — hands a claimed task
|
|
@@ -1539,7 +1613,27 @@ async function statusCommand() {
|
|
|
1539
1613
|
}
|
|
1540
1614
|
}
|
|
1541
1615
|
|
|
1542
|
-
|
|
1616
|
+
// Loaded whether or not a core resolved: drift is the only consumer
|
|
1617
|
+
// that needs `core` composed against these, and graphStatus already
|
|
1618
|
+
// gates that on `core` itself. The orphan check reads task ids out of
|
|
1619
|
+
// the database, so it is answerable — and worth answering — on a
|
|
1620
|
+
// project whose core.yaml is missing or unparseable. A missing
|
|
1621
|
+
// overrides directory reads as an empty Map (loadOverrides), so this
|
|
1622
|
+
// costs nothing on a project that has never written one.
|
|
1623
|
+
//
|
|
1624
|
+
// A malformed override file throws, and for the same reason an
|
|
1625
|
+
// unparseable core.yaml doesn't take `status` down, neither may this:
|
|
1626
|
+
// status is the command every session starts with, and the report of
|
|
1627
|
+
// the broken file is more useful than an aborted overview.
|
|
1628
|
+
let overrides = new Map();
|
|
1629
|
+
try {
|
|
1630
|
+
overrides = await loadOverrides();
|
|
1631
|
+
} catch (err) {
|
|
1632
|
+
console.error(
|
|
1633
|
+
`${yellow('Override file unreadable:')} ${err.message}\n${dim('Scope overrides are ignored until it parses.')}\n`,
|
|
1634
|
+
);
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1543
1637
|
const db = openDb();
|
|
1544
1638
|
let result;
|
|
1545
1639
|
try {
|
|
@@ -1711,7 +1805,13 @@ function openInBrowser(url) {
|
|
|
1711
1805
|
// either fails silently or blocks on a text browser. Printing the URL is
|
|
1712
1806
|
// the useful behaviour in that case — a person on the other end of a
|
|
1713
1807
|
// port-forward can still open it.
|
|
1808
|
+
//
|
|
1809
|
+
// HEDGEHOG_FORCE_HEADLESS bypasses the platform check entirely — the
|
|
1810
|
+
// repro suite's only way to exercise the no-display branch on macOS/
|
|
1811
|
+
// Windows, where DISPLAY/WAYLAND_DISPLAY are never consulted for real
|
|
1812
|
+
// users. Not a documented user-facing flag.
|
|
1714
1813
|
function hasDisplay() {
|
|
1814
|
+
if (process.env.HEDGEHOG_FORCE_HEADLESS) return false;
|
|
1715
1815
|
if (process.platform === 'darwin' || process.platform === 'win32') return true;
|
|
1716
1816
|
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
1717
1817
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyf0xx/hedgehog",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.2",
|
|
4
4
|
"description": "Install the Hedgehog build discipline (agents + skills) into a repo, for Claude Code, Cursor, or Gemini CLI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -44,4 +44,4 @@
|
|
|
44
44
|
"hedgehog"
|
|
45
45
|
],
|
|
46
46
|
"license": "MIT"
|
|
47
|
-
}
|
|
47
|
+
}
|
|
@@ -36,29 +36,61 @@ project/target config), `nx-generate` (scaffolding a new library/app), and
|
|
|
36
36
|
`link-workspace-packages` (wiring a new package into a consumer) as
|
|
37
37
|
needed.
|
|
38
38
|
|
|
39
|
+
**Every layer you own starts from its generator in `tools/generators/`** —
|
|
40
|
+
one per layer (`schema`, `contract`, `repository`, `service`,
|
|
41
|
+
`controller`), each landing that layer's package shell, `nx.tags`,
|
|
42
|
+
port-discipline file suffixes, Nest module/controller pair, and barrel
|
|
43
|
+
wiring in one step. The claimed packet's LAYER SHAPE section prints the
|
|
44
|
+
exact command for the layer you're on; `hedgehog-loop`'s "Scaffolding a
|
|
45
|
+
layer" section owns the full flag contract and the workspace wiring a new
|
|
46
|
+
package needs. Generate first, then author this entity's delta — the field
|
|
47
|
+
list and its types, and the business rules below — on top. A hand-copy of
|
|
48
|
+
a sibling module is the drift `hedgehog verify`'s lint step then has to
|
|
49
|
+
catch.
|
|
50
|
+
|
|
39
51
|
## Core Responsibilities
|
|
40
52
|
|
|
41
53
|
- **`schema`**: define the table in `packages/db` (Drizzle). One domain
|
|
42
54
|
module = one table. Cross-module references are FK-by-ID columns
|
|
43
|
-
only — never a foreign schema import.
|
|
55
|
+
only — never a foreign schema import. Add one re-export line for the
|
|
56
|
+
module to `packages/db/src/schema/index.ts` (in scope for this
|
|
57
|
+
layer) so the table is importable outside `packages/db` — the
|
|
58
|
+
package's own `src/index.ts` re-exports that barrel and never
|
|
59
|
+
changes after bootstrap.
|
|
44
60
|
- **`contract`**: derive the Zod schema from Drizzle (`drizzle-zod`) and
|
|
45
|
-
wire the ts-rest contract in `packages/contracts`.
|
|
61
|
+
wire the ts-rest contract in `packages/contracts`. A `date`-mode
|
|
62
|
+
`timestamp` column reflected through `createSelectSchema` is overridden
|
|
63
|
+
to a union of `z.date()` and an ISO datetime string, never left as the
|
|
64
|
+
derived `z.date()` alone and never narrowed to a string-only schema —
|
|
65
|
+
the same field is checked server-side against a real `Date` and
|
|
66
|
+
client-side against JSON's string, and no `.transform()` can satisfy
|
|
67
|
+
both.
|
|
46
68
|
- **`repository`**: a port (interface) plus a Drizzle adapter in
|
|
47
69
|
`libs/<module>/repository`. A `findById`-shaped miss returns
|
|
48
70
|
`undefined` — plain absence, not a thrown error; the service decides
|
|
49
|
-
what absence means.
|
|
50
|
-
|
|
51
|
-
its
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
71
|
+
what absence means. The concrete adapter's file name ends in
|
|
72
|
+
`.adapter.ts` and the lib's entry point exports the port interface and
|
|
73
|
+
its DI token; `packages/config/eslint-base.js` keys its port-discipline
|
|
74
|
+
rule on that suffix, so an adapter named anything else silently opts
|
|
75
|
+
out of the check.
|
|
76
|
+
- **`service`**: domain logic in `libs/<module>/service`, importing its
|
|
77
|
+
own module's port interface from the repository lib's entry point —
|
|
78
|
+
never a `*.adapter` file, never `drizzle-orm` or `packages/db`
|
|
79
|
+
(`no-restricted-imports` in `eslint-base.js` fails lint on either).
|
|
80
|
+
Throws typed, domain-named errors (`OrderNotFoundError`, not a bare
|
|
81
|
+
`Error` or an HTTP exception). No logging, no HTTP, no queue mechanics
|
|
82
|
+
inside a service method. Multi-write operations wrap in one Drizzle
|
|
83
|
+
transaction, passed through the port.
|
|
56
84
|
- **`controller`**: thin HTTP in `apps/api`, wiring the contract to the
|
|
57
85
|
service. The only layer that maps domain errors to status codes.
|
|
58
86
|
Validation happens once, at this boundary, via the Zod contract — past
|
|
59
|
-
it, types are trusted.
|
|
60
|
-
`
|
|
61
|
-
|
|
87
|
+
it, types are trusted. `apps/api` is the composition root: the module's
|
|
88
|
+
`*.module.ts` is the one file that constructs the concrete adapter and
|
|
89
|
+
binds it to the port's DI token, and the only file in `apps/api`
|
|
90
|
+
allowed to import a `*.adapter`. A controller takes the service, or the
|
|
91
|
+
bound token — never the adapter. Bundles queue infra (port + BullMQ
|
|
92
|
+
adapter in `apps/worker`, same shape as the repository) when the Queue
|
|
93
|
+
add-on is on and this operation needs it.
|
|
62
94
|
|
|
63
95
|
## Workflow
|
|
64
96
|
|
|
@@ -75,15 +107,29 @@ needed.
|
|
|
75
107
|
`complete` — no need to re-derive that by hand. Cross-module FK
|
|
76
108
|
targets should already have their own schema landed (the packet's
|
|
77
109
|
dependencies guarantee this); check before writing the FK column.
|
|
78
|
-
2. Build exactly one layer, matching the packet's ALLOWED SCOPE
|
|
79
|
-
|
|
80
|
-
back — necessary, not
|
|
110
|
+
2. Build exactly one layer, matching the packet's ALLOWED SCOPE: run its
|
|
111
|
+
generator, then author this entity's delta. Run typecheck, lint, and
|
|
112
|
+
test yourself as a sanity check before reporting back — necessary, not
|
|
113
|
+
sufficient. If this layer also has to create the package it lands in
|
|
114
|
+
(the first module through `contract` creates `packages/contracts`), the
|
|
115
|
+
shell files its generator lands sit outside the packet's ALLOWED SCOPE
|
|
116
|
+
and `hedgehog verify` will leave them uncommitted — stop and say so
|
|
117
|
+
before building, so the scope can be widened for this one task
|
|
118
|
+
(`hedgehog-loop`, "First arrival in a package"). Don't build against a
|
|
119
|
+
scope you already know won't commit your work. If the layer wires a new
|
|
120
|
+
or newly-linked package into the workspace, run `pnpm install` and
|
|
121
|
+
`pnpm nx sync` yourself (`hedgehog-loop`, "Scaffolding a layer") and
|
|
122
|
+
name the shared files that changed (typically `pnpm-lock.yaml`, root
|
|
123
|
+
`tsconfig.json`) in your report — the orchestrating session commits
|
|
124
|
+
them separately, since you report but never commit (next step).
|
|
81
125
|
3. **Report the work as done; do not commit it yourself.** Per the build
|
|
82
126
|
graph's design, an agent reporting success never moves a task — only
|
|
83
127
|
`hedgehog verify <task-id>`'s passing exit code does. It checks your
|
|
84
128
|
changes against the packet's ALLOWED SCOPE, re-runs the real
|
|
85
129
|
verification command, and on a pass writes the commit (the packet's
|
|
86
|
-
exact Conventional Commit message) itself.
|
|
130
|
+
exact Conventional Commit message) itself. Any shared workspace files
|
|
131
|
+
you flagged in step 2 are a separate commit the orchestrating session
|
|
132
|
+
makes before dispatching `hedgehog verify`, not something you commit.
|
|
87
133
|
4. One layer at a time — never start the next layer before
|
|
88
134
|
`hedgehog verify` reports the current one `complete`.
|
|
89
135
|
5. Once `hedgehog verify` reports the `controller` layer (and any bundled
|
|
@@ -34,6 +34,19 @@ project/target config), `nx-generate` (scaffolding a new library/app), and
|
|
|
34
34
|
`link-workspace-packages` (wiring a new package into a consumer) as
|
|
35
35
|
needed.
|
|
36
36
|
|
|
37
|
+
**Both layers you own start from their generator in `tools/generators/`** —
|
|
38
|
+
`hook` lands `packages/hooks`'s shell, `nx.tags`, the query hook set, and
|
|
39
|
+
its barrel wiring; `screen` lands the route, the screen component wired to
|
|
40
|
+
that hook, and its test file. The claimed packet's LAYER SHAPE section
|
|
41
|
+
prints the exact command for the layer you're on; `hedgehog-loop`'s
|
|
42
|
+
"Scaffolding a layer" section owns the full flag contract and the
|
|
43
|
+
workspace wiring a new package needs. Generate first, then author this
|
|
44
|
+
entity's delta on top. The `screen` generator is skeleton-only by
|
|
45
|
+
design — placeholders for the list, filter shell, empty state, and form,
|
|
46
|
+
with layout, information hierarchy, and interaction pattern left to
|
|
47
|
+
`ux-planner`'s rationale and your build, so no two modules' screens come
|
|
48
|
+
out identical before anyone decided they should.
|
|
49
|
+
|
|
37
50
|
If the screen step calls for animation or motion — entrances, sequencing,
|
|
38
51
|
scroll-driven effects, drag, SVG/morph effects — use GSAP, loading the
|
|
39
52
|
relevant skill from `vendor-skills/GSAP/` (`gsap-core`, `gsap-timeline`,
|
|
@@ -46,17 +59,26 @@ don't reach for a second one.
|
|
|
46
59
|
|
|
47
60
|
- **`hook`**: build the TanStack Query hook in `packages/hooks`, wrapping
|
|
48
61
|
the ts-rest contract client. One hook per contract operation, typed end
|
|
49
|
-
to end from the Zod contract. The client's base URL
|
|
50
|
-
`
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
62
|
+
to end from the Zod contract. The client's base URL is
|
|
63
|
+
`process.env.NEXT_PUBLIC_API_BASE_URL`, scaffolded in
|
|
64
|
+
`apps/web/.env.example` and already carrying `apps/api`'s `/api` global
|
|
65
|
+
prefix. Read it as-is: don't append or strip a path segment (the prefix
|
|
66
|
+
is in the value), don't add it to `packages/config/env.schema.ts` (that
|
|
67
|
+
schema is `apps/api`'s server env, and a `NEXT_PUBLIC_` var is inlined
|
|
68
|
+
into the browser bundle by Next, never parsed at runtime by `loadEnv()`),
|
|
69
|
+
and never fall back to a hardcoded `http://localhost:<port>`. A wrong or
|
|
70
|
+
absent base URL 404s against Next's own dev server — a config bug wearing
|
|
71
|
+
a routing bug's clothes, and one unit tests never see, because they mock
|
|
72
|
+
the client.
|
|
73
|
+
- **`screen`**: build the screen/component in `apps/web` (plus
|
|
74
|
+
`apps/mobile` when the Mobile add-on is on), consuming the hook and
|
|
75
|
+
`ux-planner`'s rationale for that module (screen inventory, interaction
|
|
76
|
+
pattern, information hierarchy). No direct data-fetching in the
|
|
77
|
+
screen — the hook owns that. If this module's screen is the first one
|
|
78
|
+
built, wire `apps/web/src/app/page.tsx`'s primary CTA (ShadCN's
|
|
79
|
+
`asChild` + `next/link`, per `button.tsx`'s existing `asChild` prop)
|
|
80
|
+
to this module's own route — a compiling, lint-clean button with no
|
|
81
|
+
`href` or `onClick` still ships silent and unclickable.
|
|
60
82
|
- Translate design specs into components. If a design tool is wired into
|
|
61
83
|
this project's MCP config, use it for tokens/spacing/typography;
|
|
62
84
|
otherwise match existing ShadCN/Tailwind patterns in the repo.
|
|
@@ -82,12 +104,24 @@ don't reach for a second one.
|
|
|
82
104
|
no such confirmation, stop — you're being asked to build Phase B
|
|
83
105
|
early.
|
|
84
106
|
2. Build the hook against the contract client, matching the packet's
|
|
85
|
-
ALLOWED SCOPE
|
|
86
|
-
|
|
107
|
+
ALLOWED SCOPE: run its generator, then author this entity's delta. Run
|
|
108
|
+
typecheck, lint, and test yourself as a sanity check before reporting
|
|
109
|
+
back — necessary, not sufficient. On the first module through this
|
|
110
|
+
layer, the hook also creates `packages/hooks`, and that package's shell
|
|
111
|
+
sits outside the packet's ALLOWED SCOPE — `hedgehog verify` would leave
|
|
112
|
+
it uncommitted. Stop and say so before building, so the scope can be
|
|
113
|
+
widened for this one task (`hedgehog-loop`, "First arrival in a
|
|
114
|
+
package"); run `pnpm install` and `pnpm nx sync` yourself
|
|
115
|
+
(`hedgehog-loop`, "Scaffolding a layer") to wire the new package into
|
|
116
|
+
the workspace, and name the shared files that changed (typically
|
|
117
|
+
`pnpm-lock.yaml`, root `tsconfig.json`) in your report — the
|
|
118
|
+
orchestrating session commits them separately (next step).
|
|
87
119
|
3. **Report the work as done; do not commit it yourself.** Only
|
|
88
120
|
`hedgehog verify <task-id>`'s passing exit code moves the task to
|
|
89
121
|
`complete` and writes the commit (the packet's exact Conventional
|
|
90
|
-
Commit message).
|
|
122
|
+
Commit message). Any shared workspace files you flagged in step 2 are a
|
|
123
|
+
separate commit the orchestrating session makes before dispatching
|
|
124
|
+
`hedgehog verify`, not something you commit.
|
|
91
125
|
4. Build the screen consuming the hook the same way — packet, build,
|
|
92
126
|
report, `hedgehog verify`.
|
|
93
127
|
5. One layer at a time — `hook` fully `complete` before the `screen`
|
package/src/agents/reviewer.md
CHANGED
|
@@ -32,10 +32,15 @@ Everything lefthook already enforces (typecheck, lint, unit test
|
|
|
32
32
|
pass/fail) is out of scope — don't re-report a green gate. Check what the
|
|
33
33
|
gate structurally cannot:
|
|
34
34
|
|
|
35
|
-
- **Port discipline**:
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
the
|
|
35
|
+
- **Port discipline**: a module's port interface and its Drizzle adapter
|
|
36
|
+
share one lib, so the tag graph has to allow `type:service →
|
|
37
|
+
type:adapter` and the real check is at the import level: does the
|
|
38
|
+
service import the port from the repository lib's entry point, or the
|
|
39
|
+
concrete `*.adapter`? Does anything in `apps/api` outside a
|
|
40
|
+
`*.module.ts` construct an adapter? `eslint-base.js`'s
|
|
41
|
+
`no-restricted-imports` rules catch the named cases — read the actual
|
|
42
|
+
imports anyway, since an adapter file not named `*.adapter.ts` opts
|
|
43
|
+
itself out of the rule. Use `nx show project <name> --json` (per nrwl's
|
|
39
44
|
[nx-workspace](https://github.com/nrwl/nx-ai-agents-config/tree/main/skills/nx-workspace) skill) to check a project's resolved tags and
|
|
40
45
|
dependencies rather than reading `project.json` directly — it only
|
|
41
46
|
holds partial configuration, not tags inferred by plugins.
|
package/src/agents/ux-planner.md
CHANGED
|
@@ -127,8 +127,8 @@ conclusion.
|
|
|
127
127
|
8. Hand off to `front-end-eng` for the screen step. The file isn't a step in
|
|
128
128
|
the Domain Module Pattern and isn't committed on its own — it lands in
|
|
129
129
|
the same commit as the screen step it informs
|
|
130
|
-
(`feat(<module>): screen-web`
|
|
131
|
-
|
|
130
|
+
(`feat(<module>): screen-web`), same as any other file
|
|
131
|
+
`front-end-eng` touches while building that step.
|
|
132
132
|
|
|
133
133
|
## Constraints
|
|
134
134
|
|
package/src/db/next.mjs
CHANGED
|
@@ -234,6 +234,85 @@ export function taskStatusLine(task) {
|
|
|
234
234
|
return task.status.toUpperCase();
|
|
235
235
|
}
|
|
236
236
|
|
|
237
|
+
// full-stack-app's own layer → Nx tag shape, mirroring the tag reference
|
|
238
|
+
// table `src/golden-cores/full-stack-app/packages/config/eslint-base.js`
|
|
239
|
+
// ships as a comment. A layer's `depConstraints` failure is a mechanical
|
|
240
|
+
// consequence of this table crossed with `core.yaml`'s `depends_on` chain
|
|
241
|
+
// — printing it in the packet turns that failure into a pre-flight fact
|
|
242
|
+
// instead of something `nx lint` teaches the agent after the fact. Keyed
|
|
243
|
+
// by layer id, not module, since the shape is the same for every module.
|
|
244
|
+
//
|
|
245
|
+
// `scaffold` is the same kind of fact one step earlier: the generator in
|
|
246
|
+
// `tools/generators/` every layer starts from (hedgehog-loop, "Scaffolding
|
|
247
|
+
// a layer", which owns the flag contract these lines instantiate). It sits
|
|
248
|
+
// here rather than in RELEVANT RULES because that section is per-intent —
|
|
249
|
+
// identical across every layer of an intent, by plan.mjs's requirement
|
|
250
|
+
// linkage — while the command differs per layer, which is exactly what
|
|
251
|
+
// LAYER SHAPE already keys on.
|
|
252
|
+
const FULL_STACK_APP_LAYER_TAGS = {
|
|
253
|
+
schema: {
|
|
254
|
+
tags: ['scope:db', 'type:adapter'],
|
|
255
|
+
dependsOnTags: [],
|
|
256
|
+
scaffold: "nx g ./tools/generators:schema --module={module} --fields='<name:type,...>'",
|
|
257
|
+
},
|
|
258
|
+
contract: {
|
|
259
|
+
tags: ['scope:contracts', 'type:contract'],
|
|
260
|
+
dependsOnTags: ['type:adapter', 'type:util'],
|
|
261
|
+
scaffold: "nx g ./tools/generators:contract --module={module} --fields='<the schema layer's list>'",
|
|
262
|
+
},
|
|
263
|
+
repository: {
|
|
264
|
+
tags: ['scope:{module}', 'type:adapter'],
|
|
265
|
+
dependsOnTags: ['type:adapter', 'type:contract', 'type:util'],
|
|
266
|
+
scaffold: 'nx g ./tools/generators:repository --module={module}',
|
|
267
|
+
},
|
|
268
|
+
service: {
|
|
269
|
+
tags: ['scope:{module}', 'type:service'],
|
|
270
|
+
dependsOnTags: ['type:adapter', 'type:contract', 'type:util'],
|
|
271
|
+
scaffold: 'nx g ./tools/generators:service --module={module}',
|
|
272
|
+
},
|
|
273
|
+
controller: {
|
|
274
|
+
tags: ['scope:api'],
|
|
275
|
+
dependsOnTags: ['type:adapter', 'type:service', 'type:contract', 'type:util'],
|
|
276
|
+
scaffold: "nx g ./tools/generators:controller --module={module} --fields='<the schema layer's list>'",
|
|
277
|
+
},
|
|
278
|
+
hook: {
|
|
279
|
+
tags: ['scope:hooks', 'type:hook'],
|
|
280
|
+
dependsOnTags: ['type:contract', 'type:util'],
|
|
281
|
+
scaffold: 'nx g ./tools/generators:hook --module={module} [--toggleField=<boolField>]',
|
|
282
|
+
},
|
|
283
|
+
screen: {
|
|
284
|
+
tags: ['scope:web'],
|
|
285
|
+
dependsOnTags: ['scope:contracts', 'scope:hooks', 'scope:shared', 'type:util'],
|
|
286
|
+
scaffold: 'nx g ./tools/generators:screen --module={module} (skeleton only — UX is yours)',
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
// A LAYER SHAPE section for full-stack-app tasks only (`coreId ===
|
|
291
|
+
// 'full-stack-app'`) — an authored core has no equivalent tag scheme
|
|
292
|
+
// (layer-eng.md already points that agent at reading .hedgehog/core.yaml
|
|
293
|
+
// directly instead), and `join` has no fixed tag shape of its own to
|
|
294
|
+
// state, so both fall through to null and print nothing.
|
|
295
|
+
function layerShapeLines(task, coreId) {
|
|
296
|
+
if (coreId !== 'full-stack-app') return null;
|
|
297
|
+
const shape = FULL_STACK_APP_LAYER_TAGS[task.layer];
|
|
298
|
+
if (!shape) return null;
|
|
299
|
+
const tags = shape.tags.map((t) => t.replace('{module}', task.module));
|
|
300
|
+
const lines = ['LAYER SHAPE', ` this layer's tags: ${tags.join(', ')}`];
|
|
301
|
+
if (shape.dependsOnTags.length === 0) {
|
|
302
|
+
lines.push(' may depend on tags: (nothing in-workspace — floor layer)');
|
|
303
|
+
} else {
|
|
304
|
+
lines.push(` may depend on tags: ${shape.dependsOnTags.join(', ')}`);
|
|
305
|
+
}
|
|
306
|
+
lines.push(` scaffold from: ${shape.scaffold.replace('{module}', task.module)}`);
|
|
307
|
+
lines.push(
|
|
308
|
+
" Confirm against packages/config/eslint-base.js's depConstraints before",
|
|
309
|
+
" writing an import — a mismatch here is a pre-flight fact, not a lint",
|
|
310
|
+
' failure to discover later. Start from the generator and author only',
|
|
311
|
+
" this entity's delta on top of what it lands.",
|
|
312
|
+
);
|
|
313
|
+
return lines;
|
|
314
|
+
}
|
|
315
|
+
|
|
237
316
|
// The standing honesty requirement, appended to every packet.
|
|
238
317
|
//
|
|
239
318
|
// Every other section is task-specific — this one is constant, which is
|
|
@@ -264,8 +343,8 @@ const HONESTY = [
|
|
|
264
343
|
];
|
|
265
344
|
|
|
266
345
|
// Renders a packet into the STATUS / INTENT / RELEVANT RULES /
|
|
267
|
-
// INHERITED DEBT / WHY NOW / BLOCKED DOWNSTREAM / ALLOWED SCOPE /
|
|
268
|
-
// VERIFICATION / HONESTY format. The spec
|
|
346
|
+
// INHERITED DEBT / WHY NOW / BLOCKED DOWNSTREAM / ALLOWED SCOPE / LAYER
|
|
347
|
+
// SHAPE / VERIFICATION / HONESTY format. The spec
|
|
269
348
|
// splits this across two examples — the `hedgehog next` display and "The
|
|
270
349
|
// task packet" (which carries the intent and its rules) — but an agent
|
|
271
350
|
// receives one thing, so the packet is one thing: everything the worker
|
|
@@ -273,12 +352,15 @@ const HONESTY = [
|
|
|
273
352
|
//
|
|
274
353
|
// `statusLine` is what goes on the STATUS row. `next` passes READY
|
|
275
354
|
// literally, as it always has; `show` passes taskStatusLine(task), which
|
|
276
|
-
// names the task's real state.
|
|
355
|
+
// names the task's real state. `coreId` is the active core's `id` (from
|
|
356
|
+
// `core.yaml`) — optional, since a caller with no core resolved yet (a
|
|
357
|
+
// deferred install) still has to be able to render *something*; LAYER
|
|
358
|
+
// SHAPE only ever appears for a recognised full-stack-app layer.
|
|
277
359
|
//
|
|
278
360
|
// HONESTY is last deliberately: it's the one section that qualifies the
|
|
279
361
|
// gate above it, so it reads as the answer to "and what if I can't clear
|
|
280
362
|
// VERIFICATION honestly" rather than as preamble.
|
|
281
|
-
export function formatPacket(packet, statusLine) {
|
|
363
|
+
export function formatPacket(packet, statusLine, coreId = null) {
|
|
282
364
|
const { task, intent, requirements, dependents, incompleteDeps = [], inheritedDebt = [] } = packet;
|
|
283
365
|
const scopeGlobs = JSON.parse(task.scope_globs);
|
|
284
366
|
|
|
@@ -346,6 +428,11 @@ export function formatPacket(packet, statusLine) {
|
|
|
346
428
|
lines.push('ALLOWED SCOPE');
|
|
347
429
|
for (const glob of scopeGlobs) lines.push(` ${glob}`);
|
|
348
430
|
lines.push('');
|
|
431
|
+
const shapeLines = layerShapeLines(task, coreId);
|
|
432
|
+
if (shapeLines) {
|
|
433
|
+
lines.push(...shapeLines);
|
|
434
|
+
lines.push('');
|
|
435
|
+
}
|
|
349
436
|
lines.push('VERIFICATION');
|
|
350
437
|
lines.push(` ${task.verify_command}`);
|
|
351
438
|
lines.push('');
|
|
@@ -354,8 +441,8 @@ export function formatPacket(packet, statusLine) {
|
|
|
354
441
|
return lines.join('\n');
|
|
355
442
|
}
|
|
356
443
|
|
|
357
|
-
// `hedgehog next`'s rendering
|
|
358
|
-
//
|
|
359
|
-
export function formatNext(packet) {
|
|
360
|
-
return formatPacket(packet, 'READY');
|
|
444
|
+
// `hedgehog next`'s rendering: its task always came out of the readiness
|
|
445
|
+
// SELECT, so STATUS is READY by construction.
|
|
446
|
+
export function formatNext(packet, coreId = null) {
|
|
447
|
+
return formatPacket(packet, 'READY', coreId);
|
|
361
448
|
}
|