@skyf0xx/hedgehog 4.0.14 → 4.1.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/bin/cli.mjs +41 -8
- package/package.json +2 -2
- package/src/db/claim.mjs +51 -7
- package/src/db/ready.mjs +5 -3
- package/src/db/status.mjs +21 -7
- package/src/skills/hedgehog-authored-loop/SKILL.md +15 -8
- package/src/skills/hedgehog-core-design/SKILL.md +68 -0
- package/src/skills/hedgehog-landing-loop/SKILL.md +13 -4
- package/src/skills/hedgehog-loop/SKILL.md +15 -8
package/bin/cli.mjs
CHANGED
|
@@ -933,10 +933,10 @@ async function nextCommand() {
|
|
|
933
933
|
|
|
934
934
|
const db = openDb();
|
|
935
935
|
let packet;
|
|
936
|
-
let stalled
|
|
936
|
+
let stalled;
|
|
937
937
|
try {
|
|
938
938
|
packet = nextTask(db);
|
|
939
|
-
|
|
939
|
+
stalled = stalledTasks(db);
|
|
940
940
|
} finally {
|
|
941
941
|
db.close();
|
|
942
942
|
}
|
|
@@ -947,10 +947,7 @@ async function nextCommand() {
|
|
|
947
947
|
// a failed verification.
|
|
948
948
|
if (stalled.length > 0) {
|
|
949
949
|
console.error(`${red(bold('No ready task, but the graph is blocked.'))}\n`);
|
|
950
|
-
|
|
951
|
-
const reason = BLOCKED_REASON_LABELS[task.blocked_reason] ?? task.blocked_reason;
|
|
952
|
-
console.error(` ${red('✗')} ${bold(task.id)} ${task.layer} ${dim(reason)}`);
|
|
953
|
-
}
|
|
950
|
+
printStalledTasks(stalled);
|
|
954
951
|
// `verify` refuses anything that isn't `building` and leased to
|
|
955
952
|
// the caller, so a blocked task has to go back through the queue
|
|
956
953
|
// — retry, claim, then verify — rather than straight to verify.
|
|
@@ -964,9 +961,28 @@ async function nextCommand() {
|
|
|
964
961
|
return;
|
|
965
962
|
}
|
|
966
963
|
|
|
964
|
+
// A blocked task elsewhere in the graph doesn't stop this ready task
|
|
965
|
+
// from being handed out — leases are scoped to disjoint work, so an
|
|
966
|
+
// unrelated block is no reason to halt everything else. But it's easy
|
|
967
|
+
// to miss otherwise: the queue keeps producing ready tasks right up
|
|
968
|
+
// until it doesn't, and a block can sit unnoticed the whole time. This
|
|
969
|
+
// warns without withholding the packet.
|
|
970
|
+
if (stalled.length > 0) {
|
|
971
|
+
console.error(`${yellow(bold(`${stalled.length} task(s) blocked elsewhere in the graph:`))}`);
|
|
972
|
+
printStalledTasks(stalled);
|
|
973
|
+
console.error('');
|
|
974
|
+
}
|
|
975
|
+
|
|
967
976
|
console.log(formatNext(packet));
|
|
968
977
|
}
|
|
969
978
|
|
|
979
|
+
function printStalledTasks(stalled) {
|
|
980
|
+
for (const task of stalled) {
|
|
981
|
+
const reason = BLOCKED_REASON_LABELS[task.blocked_reason] ?? task.blocked_reason;
|
|
982
|
+
console.error(` ${red('✗')} ${bold(task.id)} ${task.layer} ${dim(reason)}`);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
970
986
|
// Mirrors, read-only, the condition verifyTask's own Phase 0
|
|
971
987
|
// (claimForVerify) enforces: after expired leases are reaped, the task
|
|
972
988
|
// must be `building` and leased to `owner`. The expiry predicate is
|
|
@@ -1185,13 +1201,30 @@ async function claimCommand(args) {
|
|
|
1185
1201
|
}
|
|
1186
1202
|
|
|
1187
1203
|
const db = openDb();
|
|
1188
|
-
let claimed;
|
|
1204
|
+
let claimed, blocked;
|
|
1189
1205
|
try {
|
|
1190
|
-
claimed = claimTasks(db, { owner, count });
|
|
1206
|
+
({ claimed, blocked } = claimTasks(db, { owner, count }));
|
|
1191
1207
|
} finally {
|
|
1192
1208
|
db.close();
|
|
1193
1209
|
}
|
|
1194
1210
|
|
|
1211
|
+
// Stop-the-line: claimTasks refuses the whole batch, in any module,
|
|
1212
|
+
// while any task anywhere is blocked — see its own comment in
|
|
1213
|
+
// claim.mjs. A targeted `hedgehog claim <task-id>` is unaffected, which
|
|
1214
|
+
// is how the blocked task itself gets reclaimed after `hedgehog retry`.
|
|
1215
|
+
if (blocked.length > 0) {
|
|
1216
|
+
console.error(`${red(bold('Claim refused.'))} ${blocked.length} task(s) blocked:\n`);
|
|
1217
|
+
for (const task of blocked) {
|
|
1218
|
+
const reason = BLOCKED_REASON_LABELS[task.blocked_reason] ?? task.blocked_reason;
|
|
1219
|
+
console.error(` ${red('✗')} ${bold(task.id)} ${task.layer} ${dim(reason)}`);
|
|
1220
|
+
}
|
|
1221
|
+
console.error(
|
|
1222
|
+
`\nFix the work, then ${bold('hedgehog retry <task-id>')} before claiming more.\n`,
|
|
1223
|
+
);
|
|
1224
|
+
process.exitCode = 1;
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1195
1228
|
if (claimed.length === 0) {
|
|
1196
1229
|
console.log(`${dim('No claimable task.')} Nothing is ready with no lease held.\n`);
|
|
1197
1230
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyf0xx/hedgehog",
|
|
3
|
-
"version": "4.0
|
|
3
|
+
"version": "4.1.0",
|
|
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
|
+
}
|
package/src/db/claim.mjs
CHANGED
|
@@ -180,16 +180,25 @@ function loadTask(db, taskId) {
|
|
|
180
180
|
// concurrent `claim` call in the interim would otherwise reach `verify`
|
|
181
181
|
// with a technically-expired lease that still matches status/lease_owner
|
|
182
182
|
// and sail through unreaped.
|
|
183
|
+
//
|
|
184
|
+
// Returns the ids it just flipped, as a Set — claimTasks's stop-the-line
|
|
185
|
+
// check uses this to tell a block this very call produced from one that
|
|
186
|
+
// was already sitting there, so a dead agent's lease lapsing doesn't
|
|
187
|
+
// itself become the reason every other module's fan-out refuses.
|
|
183
188
|
export function reapExpiredLeases(db) {
|
|
184
|
-
db
|
|
185
|
-
|
|
189
|
+
const rows = db
|
|
190
|
+
.prepare(
|
|
191
|
+
`
|
|
186
192
|
UPDATE tasks
|
|
187
193
|
SET status = 'blocked', blocked_reason = 'lease_expired',
|
|
188
194
|
lease_owner = NULL, lease_expires_at = NULL, leased_at = NULL,
|
|
189
195
|
claim_snapshot = NULL
|
|
190
196
|
WHERE status IN ('building', 'verifying') AND lease_expires_at < datetime('now')
|
|
197
|
+
RETURNING id
|
|
191
198
|
`,
|
|
192
|
-
|
|
199
|
+
)
|
|
200
|
+
.all();
|
|
201
|
+
return new Set(rows.map((r) => r.id));
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
// The atomic claim primitive: the WHERE clause re-checks status and
|
|
@@ -204,8 +213,38 @@ const claimOne = (db) =>
|
|
|
204
213
|
RETURNING id
|
|
205
214
|
`);
|
|
206
215
|
|
|
207
|
-
//
|
|
208
|
-
//
|
|
216
|
+
// Every `blocked` task in the graph, regardless of module or reason —
|
|
217
|
+
// the fan-out claim's stop-the-line check. Ordered the same as the
|
|
218
|
+
// NEEDS ATTENTION list in status.mjs, so the two never disagree about
|
|
219
|
+
// which tasks are outstanding.
|
|
220
|
+
function findBlockedTasks(db) {
|
|
221
|
+
return db.prepare(`SELECT * FROM tasks WHERE status = 'blocked' ORDER BY priority, id`).all();
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Claims up to `count` ready tasks for `owner`, returning
|
|
225
|
+
// `{ claimed, blocked }`. `count` is a maximum, not a promise — `claimed`
|
|
226
|
+
// may hold fewer tasks than `count`, or none. `blocked` is non-empty only
|
|
227
|
+
// on the stop-the-line refusal below, in which case `claimed` is always
|
|
228
|
+
// empty.
|
|
229
|
+
//
|
|
230
|
+
// Stop-the-line: if any task anywhere in the graph was already `blocked`
|
|
231
|
+
// before this call, the fan-out refuses to claim anything at all, in any
|
|
232
|
+
// module — a blocked task needs a human/agent decision (retry after a
|
|
233
|
+
// fix, or leave it), and handing out fresh work around it makes that easy
|
|
234
|
+
// to ignore indefinitely. Deliberately stricter than dependency-based
|
|
235
|
+
// blocking: an unrelated module isn't literally stuck on the blocked
|
|
236
|
+
// task, but the fan-out stops anyway until it's retried. A targeted
|
|
237
|
+
// `hedgehog claim <task-id>` (claimTask, below) is exempt — that's how
|
|
238
|
+
// the blocked task itself gets reclaimed after `hedgehog retry`.
|
|
239
|
+
//
|
|
240
|
+
// "Already blocked" excludes a lease this same call's reapExpiredLeases
|
|
241
|
+
// just flipped: a dead agent's lease can lapse on an unrelated module,
|
|
242
|
+
// and the first `claim` call after that lapse is whichever one happens to
|
|
243
|
+
// discover it. That call still claims normally; the reaped task still
|
|
244
|
+
// lands in `blocked`/`lease_expired` and still needs `hedgehog retry`
|
|
245
|
+
// before it's claimable — only this one call's refusal is skipped. Every
|
|
246
|
+
// `claim` call after it sees the same task still blocked and stops as
|
|
247
|
+
// usual.
|
|
209
248
|
//
|
|
210
249
|
// Fan-out keeps the batch mutually non-conflicting, and non-conflicting
|
|
211
250
|
// with every task already in flight, per conflict.mjs's
|
|
@@ -229,7 +268,12 @@ export function claimTasks(db, { owner, count = 1, leaseMinutes = 45 }) {
|
|
|
229
268
|
const claimSnapshot = snapshotWorkingTree();
|
|
230
269
|
|
|
231
270
|
return inTransaction(db, () => {
|
|
232
|
-
reapExpiredLeases(db);
|
|
271
|
+
const justReaped = reapExpiredLeases(db);
|
|
272
|
+
|
|
273
|
+
const blocked = findBlockedTasks(db).filter((task) => !justReaped.has(task.id));
|
|
274
|
+
if (blocked.length > 0) {
|
|
275
|
+
return { claimed: [], blocked };
|
|
276
|
+
}
|
|
233
277
|
|
|
234
278
|
const candidates = findClaimableTasks(db);
|
|
235
279
|
const inFlight = findInFlightTasks(db);
|
|
@@ -245,7 +289,7 @@ export function claimTasks(db, { owner, count = 1, leaseMinutes = 45 }) {
|
|
|
245
289
|
claimed.push(loadTask(db, candidate.id));
|
|
246
290
|
}
|
|
247
291
|
|
|
248
|
-
return claimed;
|
|
292
|
+
return { claimed, blocked: [] };
|
|
249
293
|
});
|
|
250
294
|
}
|
|
251
295
|
|
package/src/db/ready.mjs
CHANGED
|
@@ -46,8 +46,10 @@ export function readyTasks(db) {
|
|
|
46
46
|
// Renders one HELD BACK reason. `exclusive` splits on which side of the
|
|
47
47
|
// pair is exclusive: the candidate itself (runs alone, full stop) vs. the
|
|
48
48
|
// already-CLAIMABLE task it lost the slot to (named, so the reason points
|
|
49
|
-
// at what's actually occupying the batch).
|
|
50
|
-
|
|
49
|
+
// at what's actually occupying the batch). Exported for status.mjs, which
|
|
50
|
+
// annotates the same held-back tasks inline in its own READY section
|
|
51
|
+
// rather than duplicating this formatting.
|
|
52
|
+
export function heldBackReason(candidate, conflict) {
|
|
51
53
|
const { with: other, kind } = conflict;
|
|
52
54
|
|
|
53
55
|
if (kind === 'exclusive') {
|
|
@@ -86,7 +88,7 @@ export function formatReady({ claimable, heldBack }) {
|
|
|
86
88
|
lines.push('');
|
|
87
89
|
lines.push('HELD BACK');
|
|
88
90
|
for (const { task, conflict } of heldBack) {
|
|
89
|
-
lines.push(` ${task.id.padEnd(20)}${task.layer.padEnd(13)}${
|
|
91
|
+
lines.push(` ${task.id.padEnd(20)}${task.layer.padEnd(13)}${heldBackReason(task, conflict)}`);
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
|
package/src/db/status.mjs
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { detectDrift, formatDrift } from './drift.mjs';
|
|
15
15
|
import { formatMissingRequirements } from './requires.mjs';
|
|
16
|
+
import { readyTasks, heldBackReason } from './ready.mjs';
|
|
16
17
|
|
|
17
18
|
// The task lifecycle in order, matching the tasks CHECK constraint in
|
|
18
19
|
// schema.mjs exactly — every status the engine can write, and no others.
|
|
@@ -81,11 +82,16 @@ function loadAttentionTasks(db) {
|
|
|
81
82
|
return db.prepare(ATTENTION_TASKS_SQL).all();
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
// Returns { counts, ready, inFlight, attention, drift, total } —
|
|
85
|
-
// keyed by every status in the tasks CHECK constraint (present even
|
|
86
|
-
// zero), ready the full list of currently-pickable tasks,
|
|
87
|
-
//
|
|
88
|
-
//
|
|
85
|
+
// Returns { counts, ready, heldBack, inFlight, attention, drift, total } —
|
|
86
|
+
// counts keyed by every status in the tasks CHECK constraint (present even
|
|
87
|
+
// at zero), ready the full list of currently-pickable tasks, heldBack the
|
|
88
|
+
// subset of those that `hedgehog claim` would skip over right now because
|
|
89
|
+
// they conflict with in-flight work or another ready task ahead of them
|
|
90
|
+
// (ready.mjs's own simulation, reused rather than reimplemented — without
|
|
91
|
+
// this a task can sit in READY indefinitely with no visible reason, the
|
|
92
|
+
// same invisible-stall shape blocked tasks had before `attention` existed),
|
|
93
|
+
// inFlight the tasks currently leased (building or verifying), attention
|
|
94
|
+
// the stalled tasks needing a fix, total the sum across all statuses.
|
|
89
95
|
//
|
|
90
96
|
// `core` is optional and, when given, adds `drift`: the tasks whose
|
|
91
97
|
// layer-derived fields no longer match core.yaml (drift.mjs). It's a
|
|
@@ -100,11 +106,12 @@ function loadAttentionTasks(db) {
|
|
|
100
106
|
export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
101
107
|
const counts = countTasksByStatus(db);
|
|
102
108
|
const ready = loadReadyTasks(db);
|
|
109
|
+
const { heldBack } = readyTasks(db);
|
|
103
110
|
const inFlight = loadInFlightTasks(db);
|
|
104
111
|
const attention = loadAttentionTasks(db);
|
|
105
112
|
const drift = core ? detectDrift(db, core, { overrides }) : [];
|
|
106
113
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
107
|
-
return { counts, ready, inFlight, attention, drift, total };
|
|
114
|
+
return { counts, ready, heldBack, inFlight, attention, drift, total };
|
|
108
115
|
}
|
|
109
116
|
|
|
110
117
|
const BLOCKED_REASON_LABELS = {
|
|
@@ -127,6 +134,7 @@ const BLOCKED_REASON_LABELS = {
|
|
|
127
134
|
export function formatStatus({
|
|
128
135
|
counts,
|
|
129
136
|
ready,
|
|
137
|
+
heldBack = [],
|
|
130
138
|
inFlight,
|
|
131
139
|
attention,
|
|
132
140
|
drift,
|
|
@@ -146,12 +154,18 @@ export function formatStatus({
|
|
|
146
154
|
lines.push(...missingLines);
|
|
147
155
|
lines.push('');
|
|
148
156
|
}
|
|
157
|
+
// Each ready task is annotated inline with why `hedgehog claim` would
|
|
158
|
+
// skip it right now, rather than listed flatly — a task can otherwise
|
|
159
|
+
// sit in READY indefinitely with no visible reason (see graphStatus).
|
|
160
|
+
const heldBackById = new Map(heldBack.map(({ task, conflict }) => [task.id, conflict]));
|
|
149
161
|
lines.push('READY');
|
|
150
162
|
if (ready.length === 0) {
|
|
151
163
|
lines.push(' (none)');
|
|
152
164
|
} else {
|
|
153
165
|
for (const task of ready) {
|
|
154
|
-
|
|
166
|
+
const conflict = heldBackById.get(task.id);
|
|
167
|
+
const note = conflict ? ` (held back — ${heldBackReason(task, conflict)})` : '';
|
|
168
|
+
lines.push(` ${task.id} ${task.layer} ${task.objective}${note}`);
|
|
155
169
|
}
|
|
156
170
|
}
|
|
157
171
|
|
|
@@ -117,14 +117,21 @@ runtime detail.
|
|
|
117
117
|
moves to `blocked` with a `blocked_reason` of `scope_violation` or
|
|
118
118
|
`verification_failed`, and nothing downstream unlocks. Fix the work,
|
|
119
119
|
then run `hedgehog retry <task-id>` to return the task to `planned`,
|
|
120
|
-
claim it again
|
|
121
|
-
|
|
122
|
-
back through `retry` and
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
claim it again (by task id — see below), and verify again —
|
|
121
|
+
`hedgehog verify` only accepts a task you currently hold in
|
|
122
|
+
`building`, so a blocked task has to go back through `retry` and
|
|
123
|
+
`claim` first. Don't hand-commit around it.
|
|
124
|
+
|
|
125
|
+
A `blocked` task anywhere in the graph — in this layer or any other —
|
|
126
|
+
makes `hedgehog claim --count N` refuse to hand out anything at all,
|
|
127
|
+
with a non-zero exit naming the blocked task(s). `hedgehog status`
|
|
128
|
+
lists them too, under NEEDS ATTENTION. Fix and `retry` the named
|
|
129
|
+
task(s) before claiming more. A **targeted** `hedgehog claim <task-id>
|
|
130
|
+
--owner <owner>` is exempt — that's how the just-retried task gets
|
|
131
|
+
reclaimed in the step above. A lease the same `claim` call reaps for
|
|
132
|
+
having just expired is exempt too: that call still claims whatever
|
|
133
|
+
else is ready, and the reaped task lands in NEEDS ATTENTION for the
|
|
134
|
+
next `claim` call to stop on.
|
|
128
135
|
5. **Repeat** — `hedgehog claim --count N --owner <owner>` again for the
|
|
129
136
|
next batch.
|
|
130
137
|
|
|
@@ -358,6 +358,61 @@ expressible: it requires a commit before the verify that gates the
|
|
|
358
358
|
commit. Choose a model that applies directly rather than designing a
|
|
359
359
|
layer around it.
|
|
360
360
|
|
|
361
|
+
## Step 4d — make the product's own surface reachable
|
|
362
|
+
|
|
363
|
+
Skip this only if nothing the project publishes is reached through an
|
|
364
|
+
**arbitrator**: an ingress, a router, a gateway, a reverse proxy, a
|
|
365
|
+
process supervisor — anything that decides which backend answers a
|
|
366
|
+
request, and that no layer publishing a surface owns. For every core
|
|
367
|
+
with one, add a `once: true` **reachability layer** at the tail,
|
|
368
|
+
`depends_on` the last per-module layer, whose `verify` makes a real
|
|
369
|
+
request to each intent's primary surface and asserts something specific
|
|
370
|
+
about the answer.
|
|
371
|
+
|
|
372
|
+
Every other check in this skill judges one layer against its own claim.
|
|
373
|
+
This one exists because the defect it catches belongs to no layer. On a
|
|
374
|
+
module axis each layer's `scope` is a disjoint per-module subtree, so an
|
|
375
|
+
API layer choosing a route and a web layer choosing a screen path make
|
|
376
|
+
independent choices that only ever meet in the arbitrator's config —
|
|
377
|
+
owned by a third layer that knows neither. Each layer is individually
|
|
378
|
+
correct, each verify passes in isolation, and the request 404s. That is
|
|
379
|
+
a whole-graph property, and only a task that runs after the whole graph
|
|
380
|
+
can hold it.
|
|
381
|
+
|
|
382
|
+
What the layer asserts is the intent's `outcome` read back from outside:
|
|
383
|
+
the route answers, with a status and a body that could not come from the
|
|
384
|
+
wrong backend. Assert something the *other* side of the arbitrator
|
|
385
|
+
cannot produce — a JSON shape, a header, a field the API returns and the
|
|
386
|
+
web app does not. A bare 200 is satisfied by the fall-through app's own
|
|
387
|
+
200, which is the exact confusion this layer exists to detect.
|
|
388
|
+
|
|
389
|
+
The tail position is doing real work here, and the same three properties
|
|
390
|
+
that make `once: true` right for a composition seam (Step 4) make it
|
|
391
|
+
right here:
|
|
392
|
+
|
|
393
|
+
- **One task, after everything.** `depends_on` the last per-module layer
|
|
394
|
+
makes it wait on *every* module's copy, so it runs once, with the
|
|
395
|
+
whole product standing up.
|
|
396
|
+
- **Re-entrant by construction.** When `planner`'s Re-entry pass adds a
|
|
397
|
+
module to a finished build, that module's task becomes a prerequisite
|
|
398
|
+
of this already-complete layer, so `hedgehog plan` reopens it — a new
|
|
399
|
+
module's surface gets checked for reachability without anyone
|
|
400
|
+
remembering to ask.
|
|
401
|
+
- **It closes the graph's own claim.** Without it, "every task complete"
|
|
402
|
+
is a statement about tasks. With it, the last task completing means
|
|
403
|
+
the product answers.
|
|
404
|
+
|
|
405
|
+
Two failure modes to design out. The layer must carry no `{module}`
|
|
406
|
+
anywhere — `validateCore` rejects a `once` layer that does — so it
|
|
407
|
+
enumerates the surfaces it checks in one command rather than
|
|
408
|
+
substituting a module in. And it needs the arbitrator's routing to
|
|
409
|
+
already be applied, which makes it strictly later than the deploy layer,
|
|
410
|
+
never merged into it: a deploy layer verifies its own manifests landed,
|
|
411
|
+
which is what six green deploy layers over a 404 already looked like.
|
|
412
|
+
|
|
413
|
+
Record in `core-design.md` which layer is the reachability gate and what
|
|
414
|
+
each surface it checks is asserting.
|
|
415
|
+
|
|
361
416
|
## Step 5 — write `.hedgehog/core.yaml`
|
|
362
417
|
|
|
363
418
|
The loader parses `id` plus a `layers` list of flat maps. Every layer
|
|
@@ -487,6 +542,16 @@ if missed:
|
|
|
487
542
|
deployment/app` read the same thing regardless of which module's task
|
|
488
543
|
is running, so six modules' deploy tasks assert the identical claim six
|
|
489
544
|
times rather than each module's own deployment.
|
|
545
|
+
- **A reachability layer's `verify` asserts the product answers, not
|
|
546
|
+
that a layer deployed.** The tail `once: true` layer Step 4d calls for
|
|
547
|
+
is the one place a `verify` command is about the whole build rather
|
|
548
|
+
than its own scope, so the usual scope/filter cross-checks above have
|
|
549
|
+
nothing to say about it. Its command makes a real request through the
|
|
550
|
+
arbitrator to each intent's primary surface and asserts a response
|
|
551
|
+
only the intended backend could produce. Its `scope` is whatever
|
|
552
|
+
fixture or script the check itself lives in — a layer that writes
|
|
553
|
+
nothing still needs a scope glob it may write, since `hedgehog verify`
|
|
554
|
+
rejects writes outside it.
|
|
490
555
|
- **Declare the binaries `verify` needs, in `requires`.** Optional, an
|
|
491
556
|
inline list alongside `scope`/`verify`/`commit`
|
|
492
557
|
(`requires: ["terraform", "kubectl"]`), and only for tools that come
|
|
@@ -549,6 +614,9 @@ to change only until the file lands. Hard stop.
|
|
|
549
614
|
(intents × layers tasks, or one task per layer).
|
|
550
615
|
- For any layer that deploys or publishes: where the push happens
|
|
551
616
|
relative to that layer's commit (Step 4c) — or that no layer deploys.
|
|
617
|
+
- Which layer is the reachability gate and what each surface it checks
|
|
618
|
+
asserts (Step 4d) — or that nothing published is reached through an
|
|
619
|
+
arbitrator.
|
|
552
620
|
- That this is an authored core: the sequence was designed for this
|
|
553
621
|
project, not battle-tested across many, and it carries the same
|
|
554
622
|
enforcement as a Golden Core but a weaker guarantee.
|
|
@@ -187,10 +187,19 @@ paragraph algorithm, and their self-tests.
|
|
|
187
187
|
check, the task moves to `blocked` with a `blocked_reason` of
|
|
188
188
|
`scope_violation` or `verification_failed`, and nothing downstream
|
|
189
189
|
unlocks. Fix the work, then run `hedgehog retry <task-id>` to return
|
|
190
|
-
the task to `planned`, claim it again, and verify again —
|
|
191
|
-
verify` only accepts a task you currently hold in
|
|
192
|
-
blocked task has to go back through `retry` and
|
|
193
|
-
hand-commit around it.
|
|
190
|
+
the task to `planned`, claim it again by task id, and verify again —
|
|
191
|
+
`hedgehog verify` only accepts a task you currently hold in
|
|
192
|
+
`building`, so a blocked task has to go back through `retry` and
|
|
193
|
+
`claim` first. Don't hand-commit around it.
|
|
194
|
+
|
|
195
|
+
A `blocked` task anywhere in the graph makes `hedgehog claim --count
|
|
196
|
+
<n>` refuse to hand out anything at all, with a non-zero exit naming
|
|
197
|
+
the blocked task(s). Fix and `retry` it before claiming more. A
|
|
198
|
+
**targeted** `hedgehog claim <task-id> --owner <owner>` is exempt —
|
|
199
|
+
that's how the just-retried task gets reclaimed above. A lease the
|
|
200
|
+
same `claim` call reaps for having just expired is exempt too: that
|
|
201
|
+
call still claims whatever else is ready, and the reaped task lands in
|
|
202
|
+
NEEDS ATTENTION for the next `claim` call to stop on.
|
|
194
203
|
5. **Repeat** — `hedgehog claim --owner <owner> --count <n>` again for
|
|
195
204
|
the following layer.
|
|
196
205
|
|
|
@@ -169,14 +169,21 @@ writes `docs/design/<module>.md`, not its own compiled layer — the
|
|
|
169
169
|
`blocked` with a `blocked_reason` of `scope_violation` or
|
|
170
170
|
`verification_failed`, and nothing downstream unlocks. Fix the work,
|
|
171
171
|
then run `hedgehog retry <task-id>` to return the task to `planned`,
|
|
172
|
-
claim it again
|
|
173
|
-
|
|
174
|
-
back through `retry` and
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
172
|
+
claim it again (by task id — see below), and verify again —
|
|
173
|
+
`hedgehog verify` only accepts a task you currently hold in
|
|
174
|
+
`building`, so a blocked task has to go back through `retry` and
|
|
175
|
+
`claim` first. Don't hand-commit around it.
|
|
176
|
+
|
|
177
|
+
A `blocked` task anywhere in the graph — in this module or any other —
|
|
178
|
+
makes `hedgehog claim --count N` refuse to hand out anything at all,
|
|
179
|
+
with a non-zero exit naming the blocked task(s). `hedgehog status`
|
|
180
|
+
lists them too, under NEEDS ATTENTION. Fix and `retry` the named
|
|
181
|
+
task(s) before claiming more. A **targeted** `hedgehog claim <task-id>
|
|
182
|
+
--owner <owner>` is exempt — that's how the just-retried task gets
|
|
183
|
+
reclaimed in the step above. A lease the same `claim` call reaps for
|
|
184
|
+
having just expired is exempt too: that call still claims whatever
|
|
185
|
+
else is ready, and the reaped task lands in NEEDS ATTENTION for the
|
|
186
|
+
next `claim` call to stop on.
|
|
180
187
|
5. **Repeat** — `hedgehog claim --count N --owner <owner>` again for the
|
|
181
188
|
next batch.
|
|
182
189
|
|