@skyf0xx/hedgehog 4.1.0 → 4.2.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 +1 -1
- package/bin/cli.mjs +96 -8
- package/package.json +1 -1
- package/src/agents/backend-eng.md +46 -14
- package/src/agents/front-end-eng.md +30 -13
- package/src/agents/reviewer.md +9 -4
- package/src/agents/ux-planner.md +2 -2
- package/src/db/next.mjs +57 -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/src/main.ts +10 -1
- 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/core.yaml +11 -3
- 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-workspace.yaml +4 -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 +72 -18
- package/src/skills/hedgehog-loop/SKILL.md +91 -4
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
|
|
package/bin/cli.mjs
CHANGED
|
@@ -653,6 +653,20 @@ async function resolveCorePath() {
|
|
|
653
653
|
return null;
|
|
654
654
|
}
|
|
655
655
|
|
|
656
|
+
// The active core's `id` (e.g. `full-stack-app`), or null when no core has
|
|
657
|
+
// landed yet or it fails to parse — packet rendering degrades gracefully
|
|
658
|
+
// either way (see next.mjs's layerShapeLines), so a caller only asking for
|
|
659
|
+
// the id doesn't need its own try/catch.
|
|
660
|
+
async function resolveCoreId() {
|
|
661
|
+
const corePath = await resolveCorePath();
|
|
662
|
+
if (!corePath) return null;
|
|
663
|
+
try {
|
|
664
|
+
return (await loadCore(corePath)).id;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
656
670
|
// `hedgehog plan --recompile` — the reconciliation path for a core.yaml
|
|
657
671
|
// edited after `plan` already compiled it.
|
|
658
672
|
//
|
|
@@ -973,7 +987,7 @@ async function nextCommand() {
|
|
|
973
987
|
console.error('');
|
|
974
988
|
}
|
|
975
989
|
|
|
976
|
-
console.log(formatNext(packet));
|
|
990
|
+
console.log(formatNext(packet, await resolveCoreId()));
|
|
977
991
|
}
|
|
978
992
|
|
|
979
993
|
function printStalledTasks(stalled) {
|
|
@@ -1043,6 +1057,49 @@ async function missingBinariesForTask(db, taskId, owner) {
|
|
|
1043
1057
|
return missing.length === 0 ? null : { layer: layer.id, missing };
|
|
1044
1058
|
}
|
|
1045
1059
|
|
|
1060
|
+
// What an out-of-scope path most likely is, and what to do about it.
|
|
1061
|
+
// "Outside allowed scope" is true of every offending path and tells the
|
|
1062
|
+
// reader nothing about which of three quite different situations they're
|
|
1063
|
+
// in: a package shell the layer itself had to create (widen this one
|
|
1064
|
+
// task), shared config a source-level fix touched (commit it separately),
|
|
1065
|
+
// or a genuine stray. Each wants a different next move, and working out
|
|
1066
|
+
// which costs a beat every time.
|
|
1067
|
+
//
|
|
1068
|
+
// Evidence-only, like core.mjs's lint: an unrecognised path gets no
|
|
1069
|
+
// annotation rather than a guessed one.
|
|
1070
|
+
const SCOPE_HINTS = [
|
|
1071
|
+
{
|
|
1072
|
+
// A package root's own scaffolding — package.json, tsconfig*.json,
|
|
1073
|
+
// vitest.config.mts, src/index.ts directly under packages/<pkg>/ or
|
|
1074
|
+
// libs/<a>/<b>/. No {module}-bearing scope glob can cover these, so
|
|
1075
|
+
// the first module through a layer that creates its package always
|
|
1076
|
+
// lands here.
|
|
1077
|
+
test: (p) =>
|
|
1078
|
+
/^(packages\/[^/]+|libs\/[^/]+\/[^/]+)\/(package\.json|tsconfig[^/]*\.json|vitest\.config\.[cm]?ts|project\.json|eslint\.config\.[cm]?js|src\/index\.ts)$/.test(
|
|
1079
|
+
p,
|
|
1080
|
+
),
|
|
1081
|
+
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',
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
test: (p) =>
|
|
1085
|
+
p === 'pnpm-workspace.yaml' ||
|
|
1086
|
+
p === 'pnpm-lock.yaml' ||
|
|
1087
|
+
p === 'tsconfig.json' ||
|
|
1088
|
+
p === 'nx.json' ||
|
|
1089
|
+
p === 'package.json' ||
|
|
1090
|
+
p.startsWith('packages/config/'),
|
|
1091
|
+
hint: 'shared workspace config — no layer owns it; commit it separately as its own `chore(workspace): …` before retrying',
|
|
1092
|
+
},
|
|
1093
|
+
{
|
|
1094
|
+
test: (p) => p.startsWith('.hedgehog/'),
|
|
1095
|
+
hint: 'build-graph state — intents, overrides and friction are committed by the command that writes them, not by a layer',
|
|
1096
|
+
},
|
|
1097
|
+
];
|
|
1098
|
+
|
|
1099
|
+
function scopeHintFor(path) {
|
|
1100
|
+
return SCOPE_HINTS.find((h) => h.test(path))?.hint ?? null;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1046
1103
|
async function verifyCommand(args) {
|
|
1047
1104
|
await ensureDb();
|
|
1048
1105
|
|
|
@@ -1093,7 +1150,11 @@ async function verifyCommand(args) {
|
|
|
1093
1150
|
if (result.outcome === 'scope_violation') {
|
|
1094
1151
|
console.error(`${red(bold('Scope violation.'))} Task ${bold(taskId)} is now ${bold('blocked')}.\n`);
|
|
1095
1152
|
console.error('Touched paths outside allowed scope:');
|
|
1096
|
-
for (const path of result.offending)
|
|
1153
|
+
for (const path of result.offending) {
|
|
1154
|
+
console.error(` ${red('✗')} ${path}`);
|
|
1155
|
+
const hint = scopeHintFor(path);
|
|
1156
|
+
if (hint) console.error(` ${dim(hint)}`);
|
|
1157
|
+
}
|
|
1097
1158
|
console.error();
|
|
1098
1159
|
process.exitCode = 1;
|
|
1099
1160
|
return;
|
|
@@ -1145,14 +1206,15 @@ async function verifyCommand(args) {
|
|
|
1145
1206
|
// doesn't print the packet here, the STATUS/ALLOWED SCOPE/VERIFICATION an
|
|
1146
1207
|
// agent is supposed to be dispatched with is no longer reachable from any
|
|
1147
1208
|
// command. (`hedgehog show <task-id>` reprints it later.)
|
|
1148
|
-
function printPackets(tasks) {
|
|
1209
|
+
async function printPackets(tasks) {
|
|
1210
|
+
const coreId = await resolveCoreId();
|
|
1149
1211
|
const db = openDb();
|
|
1150
1212
|
try {
|
|
1151
1213
|
for (const task of tasks) {
|
|
1152
1214
|
const packet = taskPacket(db, task.id);
|
|
1153
1215
|
if (!packet) continue;
|
|
1154
1216
|
console.log();
|
|
1155
|
-
console.log(formatPacket(packet, taskStatusLine(packet.task)));
|
|
1217
|
+
console.log(formatPacket(packet, taskStatusLine(packet.task), coreId));
|
|
1156
1218
|
}
|
|
1157
1219
|
} finally {
|
|
1158
1220
|
db.close();
|
|
@@ -1239,7 +1301,7 @@ async function claimCommand(args) {
|
|
|
1239
1301
|
if (claimed.length > 1) console.log(` ${bold(task.id)}`);
|
|
1240
1302
|
console.log(` ${dim('expires')} ${task.lease_expires_at}`);
|
|
1241
1303
|
}
|
|
1242
|
-
printPackets(claimed);
|
|
1304
|
+
await printPackets(claimed);
|
|
1243
1305
|
}
|
|
1244
1306
|
|
|
1245
1307
|
// The targeted half of `claim`: one named task, or a non-zero exit
|
|
@@ -1258,7 +1320,7 @@ async function claimOneCommand(taskId, owner) {
|
|
|
1258
1320
|
if (result.claimed) {
|
|
1259
1321
|
console.log(`${green(bold('Claimed.'))} Task ${bold(taskId)} leased to ${bold(owner)}.`);
|
|
1260
1322
|
console.log(` ${dim('expires')} ${result.task.lease_expires_at}`);
|
|
1261
|
-
printPackets([result.task]);
|
|
1323
|
+
await printPackets([result.task]);
|
|
1262
1324
|
return;
|
|
1263
1325
|
}
|
|
1264
1326
|
|
|
@@ -1382,7 +1444,7 @@ async function showCommand(args) {
|
|
|
1382
1444
|
return;
|
|
1383
1445
|
}
|
|
1384
1446
|
|
|
1385
|
-
console.log(formatPacket(packet, taskStatusLine(packet.task)));
|
|
1447
|
+
console.log(formatPacket(packet, taskStatusLine(packet.task), await resolveCoreId()));
|
|
1386
1448
|
}
|
|
1387
1449
|
|
|
1388
1450
|
// `hedgehog release <task-id> --owner <owner>` — hands a claimed task
|
|
@@ -1539,7 +1601,27 @@ async function statusCommand() {
|
|
|
1539
1601
|
}
|
|
1540
1602
|
}
|
|
1541
1603
|
|
|
1542
|
-
|
|
1604
|
+
// Loaded whether or not a core resolved: drift is the only consumer
|
|
1605
|
+
// that needs `core` composed against these, and graphStatus already
|
|
1606
|
+
// gates that on `core` itself. The orphan check reads task ids out of
|
|
1607
|
+
// the database, so it is answerable — and worth answering — on a
|
|
1608
|
+
// project whose core.yaml is missing or unparseable. A missing
|
|
1609
|
+
// overrides directory reads as an empty Map (loadOverrides), so this
|
|
1610
|
+
// costs nothing on a project that has never written one.
|
|
1611
|
+
//
|
|
1612
|
+
// A malformed override file throws, and for the same reason an
|
|
1613
|
+
// unparseable core.yaml doesn't take `status` down, neither may this:
|
|
1614
|
+
// status is the command every session starts with, and the report of
|
|
1615
|
+
// the broken file is more useful than an aborted overview.
|
|
1616
|
+
let overrides = new Map();
|
|
1617
|
+
try {
|
|
1618
|
+
overrides = await loadOverrides();
|
|
1619
|
+
} catch (err) {
|
|
1620
|
+
console.error(
|
|
1621
|
+
`${yellow('Override file unreadable:')} ${err.message}\n${dim('Scope overrides are ignored until it parses.')}\n`,
|
|
1622
|
+
);
|
|
1623
|
+
}
|
|
1624
|
+
|
|
1543
1625
|
const db = openDb();
|
|
1544
1626
|
let result;
|
|
1545
1627
|
try {
|
|
@@ -1711,7 +1793,13 @@ function openInBrowser(url) {
|
|
|
1711
1793
|
// either fails silently or blocks on a text browser. Printing the URL is
|
|
1712
1794
|
// the useful behaviour in that case — a person on the other end of a
|
|
1713
1795
|
// port-forward can still open it.
|
|
1796
|
+
//
|
|
1797
|
+
// HEDGEHOG_FORCE_HEADLESS bypasses the platform check entirely — the
|
|
1798
|
+
// repro suite's only way to exercise the no-display branch on macOS/
|
|
1799
|
+
// Windows, where DISPLAY/WAYLAND_DISPLAY are never consulted for real
|
|
1800
|
+
// users. Not a documented user-facing flag.
|
|
1714
1801
|
function hasDisplay() {
|
|
1802
|
+
if (process.env.HEDGEHOG_FORCE_HEADLESS) return false;
|
|
1715
1803
|
if (process.platform === 'darwin' || process.platform === 'win32') return true;
|
|
1716
1804
|
return Boolean(process.env.DISPLAY || process.env.WAYLAND_DISPLAY);
|
|
1717
1805
|
}
|
package/package.json
CHANGED
|
@@ -34,31 +34,54 @@ build exactly what its ALLOWED SCOPE names, one layer at a time, gated by
|
|
|
34
34
|
Use `nx-run-tasks` (build/lint/test/typecheck), `nx-workspace` (inspecting
|
|
35
35
|
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
|
-
needed.
|
|
37
|
+
needed. A layer's first-arrival package shell (`contract`, `repository`,
|
|
38
|
+
`service`) is a generator call, not a hand-copy of a sibling package —
|
|
39
|
+
`hedgehog-loop`'s "First arrival in a package" section names the exact
|
|
40
|
+
command and tags for each.
|
|
38
41
|
|
|
39
42
|
## Core Responsibilities
|
|
40
43
|
|
|
41
44
|
- **`schema`**: define the table in `packages/db` (Drizzle). One domain
|
|
42
45
|
module = one table. Cross-module references are FK-by-ID columns
|
|
43
|
-
only — never a foreign schema import.
|
|
46
|
+
only — never a foreign schema import. Add one re-export line for the
|
|
47
|
+
module to `packages/db/src/schema/index.ts` (in scope for this
|
|
48
|
+
layer) so the table is importable outside `packages/db` — the
|
|
49
|
+
package's own `src/index.ts` re-exports that barrel and never
|
|
50
|
+
changes after bootstrap.
|
|
44
51
|
- **`contract`**: derive the Zod schema from Drizzle (`drizzle-zod`) and
|
|
45
|
-
wire the ts-rest contract in `packages/contracts`.
|
|
52
|
+
wire the ts-rest contract in `packages/contracts`. A `date`-mode
|
|
53
|
+
`timestamp` column reflected through `createSelectSchema` is overridden
|
|
54
|
+
to a union of `z.date()` and an ISO datetime string, never left as the
|
|
55
|
+
derived `z.date()` alone and never narrowed to a string-only schema —
|
|
56
|
+
the same field is checked server-side against a real `Date` and
|
|
57
|
+
client-side against JSON's string, and no `.transform()` can satisfy
|
|
58
|
+
both.
|
|
46
59
|
- **`repository`**: a port (interface) plus a Drizzle adapter in
|
|
47
60
|
`libs/<module>/repository`. A `findById`-shaped miss returns
|
|
48
61
|
`undefined` — plain absence, not a thrown error; the service decides
|
|
49
|
-
what absence means.
|
|
50
|
-
|
|
51
|
-
its
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
what absence means. The concrete adapter's file name ends in
|
|
63
|
+
`.adapter.ts` and the lib's entry point exports the port interface and
|
|
64
|
+
its DI token; `packages/config/eslint-base.js` keys its port-discipline
|
|
65
|
+
rule on that suffix, so an adapter named anything else silently opts
|
|
66
|
+
out of the check.
|
|
67
|
+
- **`service`**: domain logic in `libs/<module>/service`, importing its
|
|
68
|
+
own module's port interface from the repository lib's entry point —
|
|
69
|
+
never a `*.adapter` file, never `drizzle-orm` or `packages/db`
|
|
70
|
+
(`no-restricted-imports` in `eslint-base.js` fails lint on either).
|
|
71
|
+
Throws typed, domain-named errors (`OrderNotFoundError`, not a bare
|
|
72
|
+
`Error` or an HTTP exception). No logging, no HTTP, no queue mechanics
|
|
73
|
+
inside a service method. Multi-write operations wrap in one Drizzle
|
|
74
|
+
transaction, passed through the port.
|
|
56
75
|
- **`controller`**: thin HTTP in `apps/api`, wiring the contract to the
|
|
57
76
|
service. The only layer that maps domain errors to status codes.
|
|
58
77
|
Validation happens once, at this boundary, via the Zod contract — past
|
|
59
|
-
it, types are trusted.
|
|
60
|
-
`
|
|
61
|
-
|
|
78
|
+
it, types are trusted. `apps/api` is the composition root: the module's
|
|
79
|
+
`*.module.ts` is the one file that constructs the concrete adapter and
|
|
80
|
+
binds it to the port's DI token, and the only file in `apps/api`
|
|
81
|
+
allowed to import a `*.adapter`. A controller takes the service, or the
|
|
82
|
+
bound token — never the adapter. Bundles queue infra (port + BullMQ
|
|
83
|
+
adapter in `apps/worker`, same shape as the repository) when the Queue
|
|
84
|
+
add-on is on and this operation needs it.
|
|
62
85
|
|
|
63
86
|
## Workflow
|
|
64
87
|
|
|
@@ -77,7 +100,16 @@ needed.
|
|
|
77
100
|
dependencies guarantee this); check before writing the FK column.
|
|
78
101
|
2. Build exactly one layer, matching the packet's ALLOWED SCOPE. Run
|
|
79
102
|
typecheck, lint, and test yourself as a sanity check before reporting
|
|
80
|
-
back — necessary, not sufficient.
|
|
103
|
+
back — necessary, not sufficient. If this layer also has to create the
|
|
104
|
+
package it lands in (the first module through `contract` creates
|
|
105
|
+
`packages/contracts`), its shell files sit outside the packet's ALLOWED
|
|
106
|
+
SCOPE and `hedgehog verify` will leave them uncommitted — stop and say
|
|
107
|
+
so before building, so the scope can be widened for this one task
|
|
108
|
+
(`hedgehog-loop`, "First arrival in a package"). Don't build against a
|
|
109
|
+
scope you already know won't commit your work. That section also owns
|
|
110
|
+
the workspace wiring a new package needs (`pnpm install`, `pnpm nx
|
|
111
|
+
sync`, and its own `chore(workspace):` commit) — follow it rather than
|
|
112
|
+
leaving the package unlinked for a later layer to trip over.
|
|
81
113
|
3. **Report the work as done; do not commit it yourself.** Per the build
|
|
82
114
|
graph's design, an agent reporting success never moves a task — only
|
|
83
115
|
`hedgehog verify <task-id>`'s passing exit code does. It checks your
|
|
@@ -32,7 +32,9 @@ before the next starts.
|
|
|
32
32
|
Use `nx-run-tasks` (build/lint/test/typecheck), `nx-workspace` (inspecting
|
|
33
33
|
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
|
-
needed.
|
|
35
|
+
needed. The `hook` layer's first-arrival package shell is a generator
|
|
36
|
+
call, not a hand-copy of a sibling package — `hedgehog-loop`'s "First
|
|
37
|
+
arrival in a package" section names the exact command and tags.
|
|
36
38
|
|
|
37
39
|
If the screen step calls for animation or motion — entrances, sequencing,
|
|
38
40
|
scroll-driven effects, drag, SVG/morph effects — use GSAP, loading the
|
|
@@ -46,17 +48,26 @@ don't reach for a second one.
|
|
|
46
48
|
|
|
47
49
|
- **`hook`**: build the TanStack Query hook in `packages/hooks`, wrapping
|
|
48
50
|
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
|
-
|
|
51
|
+
to end from the Zod contract. The client's base URL is
|
|
52
|
+
`process.env.NEXT_PUBLIC_API_BASE_URL`, scaffolded in
|
|
53
|
+
`apps/web/.env.example` and already carrying `apps/api`'s `/api` global
|
|
54
|
+
prefix. Read it as-is: don't append or strip a path segment (the prefix
|
|
55
|
+
is in the value), don't add it to `packages/config/env.schema.ts` (that
|
|
56
|
+
schema is `apps/api`'s server env, and a `NEXT_PUBLIC_` var is inlined
|
|
57
|
+
into the browser bundle by Next, never parsed at runtime by `loadEnv()`),
|
|
58
|
+
and never fall back to a hardcoded `http://localhost:<port>`. A wrong or
|
|
59
|
+
absent base URL 404s against Next's own dev server — a config bug wearing
|
|
60
|
+
a routing bug's clothes, and one unit tests never see, because they mock
|
|
61
|
+
the client.
|
|
62
|
+
- **`screen`**: build the screen/component in `apps/web` (plus
|
|
63
|
+
`apps/mobile` when the Mobile add-on is on), consuming the hook and
|
|
64
|
+
`ux-planner`'s rationale for that module (screen inventory, interaction
|
|
65
|
+
pattern, information hierarchy). No direct data-fetching in the
|
|
66
|
+
screen — the hook owns that. If this module's screen is the first one
|
|
67
|
+
built, wire `apps/web/src/app/page.tsx`'s primary CTA (ShadCN's
|
|
68
|
+
`asChild` + `next/link`, per `button.tsx`'s existing `asChild` prop)
|
|
69
|
+
to this module's own route — a compiling, lint-clean button with no
|
|
70
|
+
`href` or `onClick` still ships silent and unclickable.
|
|
60
71
|
- Translate design specs into components. If a design tool is wired into
|
|
61
72
|
this project's MCP config, use it for tokens/spacing/typography;
|
|
62
73
|
otherwise match existing ShadCN/Tailwind patterns in the repo.
|
|
@@ -83,7 +94,13 @@ don't reach for a second one.
|
|
|
83
94
|
early.
|
|
84
95
|
2. Build the hook against the contract client, matching the packet's
|
|
85
96
|
ALLOWED SCOPE. Run typecheck, lint, and test yourself as a sanity
|
|
86
|
-
check before reporting back — necessary, not sufficient.
|
|
97
|
+
check before reporting back — necessary, not sufficient. On the first
|
|
98
|
+
module through this layer, the hook also creates `packages/hooks`, and
|
|
99
|
+
that package's shell sits outside the packet's ALLOWED SCOPE —
|
|
100
|
+
`hedgehog verify` would leave it uncommitted. Stop and say so before
|
|
101
|
+
building, so the scope can be widened for this one task
|
|
102
|
+
(`hedgehog-loop`, "First arrival in a package") — that section also
|
|
103
|
+
owns the workspace wiring the new package needs.
|
|
87
104
|
3. **Report the work as done; do not commit it yourself.** Only
|
|
88
105
|
`hedgehog verify <task-id>`'s passing exit code moves the task to
|
|
89
106
|
`complete` and writes the commit (the packet's exact Conventional
|
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,47 @@ 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
|
+
const FULL_STACK_APP_LAYER_TAGS = {
|
|
245
|
+
schema: { tags: ['scope:db', 'type:adapter'], dependsOnTags: [] },
|
|
246
|
+
contract: { tags: ['scope:contracts', 'type:contract'], dependsOnTags: ['type:adapter', 'type:util'] },
|
|
247
|
+
repository: { tags: ['scope:{module}', 'type:adapter'], dependsOnTags: ['type:adapter', 'type:contract', 'type:util'] },
|
|
248
|
+
service: { tags: ['scope:{module}', 'type:service'], dependsOnTags: ['type:adapter', 'type:contract', 'type:util'] },
|
|
249
|
+
controller: { tags: ['scope:api'], dependsOnTags: ['type:adapter', 'type:service', 'type:contract', 'type:util'] },
|
|
250
|
+
hook: { tags: ['scope:hooks', 'type:hook'], dependsOnTags: ['type:contract', 'type:util'] },
|
|
251
|
+
screen: { tags: ['scope:web'], dependsOnTags: ['scope:contracts', 'scope:hooks', 'scope:shared', 'type:util'] },
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
// A LAYER SHAPE section for full-stack-app tasks only (`coreId ===
|
|
255
|
+
// 'full-stack-app'`) — an authored core has no equivalent tag scheme
|
|
256
|
+
// (layer-eng.md already points that agent at reading .hedgehog/core.yaml
|
|
257
|
+
// directly instead), and `join` has no fixed tag shape of its own to
|
|
258
|
+
// state, so both fall through to null and print nothing.
|
|
259
|
+
function layerShapeLines(task, coreId) {
|
|
260
|
+
if (coreId !== 'full-stack-app') return null;
|
|
261
|
+
const shape = FULL_STACK_APP_LAYER_TAGS[task.layer];
|
|
262
|
+
if (!shape) return null;
|
|
263
|
+
const tags = shape.tags.map((t) => t.replace('{module}', task.module));
|
|
264
|
+
const lines = ['LAYER SHAPE', ` this layer's tags: ${tags.join(', ')}`];
|
|
265
|
+
if (shape.dependsOnTags.length === 0) {
|
|
266
|
+
lines.push(' may depend on tags: (nothing in-workspace — floor layer)');
|
|
267
|
+
} else {
|
|
268
|
+
lines.push(` may depend on tags: ${shape.dependsOnTags.join(', ')}`);
|
|
269
|
+
}
|
|
270
|
+
lines.push(
|
|
271
|
+
" Confirm against packages/config/eslint-base.js's depConstraints before",
|
|
272
|
+
" writing an import — a mismatch here is a pre-flight fact, not a lint",
|
|
273
|
+
' failure to discover later.',
|
|
274
|
+
);
|
|
275
|
+
return lines;
|
|
276
|
+
}
|
|
277
|
+
|
|
237
278
|
// The standing honesty requirement, appended to every packet.
|
|
238
279
|
//
|
|
239
280
|
// Every other section is task-specific — this one is constant, which is
|
|
@@ -264,8 +305,8 @@ const HONESTY = [
|
|
|
264
305
|
];
|
|
265
306
|
|
|
266
307
|
// Renders a packet into the STATUS / INTENT / RELEVANT RULES /
|
|
267
|
-
// INHERITED DEBT / WHY NOW / BLOCKED DOWNSTREAM / ALLOWED SCOPE /
|
|
268
|
-
// VERIFICATION / HONESTY format. The spec
|
|
308
|
+
// INHERITED DEBT / WHY NOW / BLOCKED DOWNSTREAM / ALLOWED SCOPE / LAYER
|
|
309
|
+
// SHAPE / VERIFICATION / HONESTY format. The spec
|
|
269
310
|
// splits this across two examples — the `hedgehog next` display and "The
|
|
270
311
|
// task packet" (which carries the intent and its rules) — but an agent
|
|
271
312
|
// receives one thing, so the packet is one thing: everything the worker
|
|
@@ -273,12 +314,15 @@ const HONESTY = [
|
|
|
273
314
|
//
|
|
274
315
|
// `statusLine` is what goes on the STATUS row. `next` passes READY
|
|
275
316
|
// literally, as it always has; `show` passes taskStatusLine(task), which
|
|
276
|
-
// names the task's real state.
|
|
317
|
+
// names the task's real state. `coreId` is the active core's `id` (from
|
|
318
|
+
// `core.yaml`) — optional, since a caller with no core resolved yet (a
|
|
319
|
+
// deferred install) still has to be able to render *something*; LAYER
|
|
320
|
+
// SHAPE only ever appears for a recognised full-stack-app layer.
|
|
277
321
|
//
|
|
278
322
|
// HONESTY is last deliberately: it's the one section that qualifies the
|
|
279
323
|
// gate above it, so it reads as the answer to "and what if I can't clear
|
|
280
324
|
// VERIFICATION honestly" rather than as preamble.
|
|
281
|
-
export function formatPacket(packet, statusLine) {
|
|
325
|
+
export function formatPacket(packet, statusLine, coreId = null) {
|
|
282
326
|
const { task, intent, requirements, dependents, incompleteDeps = [], inheritedDebt = [] } = packet;
|
|
283
327
|
const scopeGlobs = JSON.parse(task.scope_globs);
|
|
284
328
|
|
|
@@ -346,6 +390,11 @@ export function formatPacket(packet, statusLine) {
|
|
|
346
390
|
lines.push('ALLOWED SCOPE');
|
|
347
391
|
for (const glob of scopeGlobs) lines.push(` ${glob}`);
|
|
348
392
|
lines.push('');
|
|
393
|
+
const shapeLines = layerShapeLines(task, coreId);
|
|
394
|
+
if (shapeLines) {
|
|
395
|
+
lines.push(...shapeLines);
|
|
396
|
+
lines.push('');
|
|
397
|
+
}
|
|
349
398
|
lines.push('VERIFICATION');
|
|
350
399
|
lines.push(` ${task.verify_command}`);
|
|
351
400
|
lines.push('');
|
|
@@ -354,8 +403,8 @@ export function formatPacket(packet, statusLine) {
|
|
|
354
403
|
return lines.join('\n');
|
|
355
404
|
}
|
|
356
405
|
|
|
357
|
-
// `hedgehog next`'s rendering
|
|
358
|
-
//
|
|
359
|
-
export function formatNext(packet) {
|
|
360
|
-
return formatPacket(packet, 'READY');
|
|
406
|
+
// `hedgehog next`'s rendering: its task always came out of the readiness
|
|
407
|
+
// SELECT, so STATUS is READY by construction.
|
|
408
|
+
export function formatNext(packet, coreId = null) {
|
|
409
|
+
return formatPacket(packet, 'READY', coreId);
|
|
361
410
|
}
|