@skyf0xx/hedgehog 6.2.2 → 6.2.4
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 -0
- package/README.zh-CN.md +1 -0
- package/bin/cli.mjs +1 -6
- package/package.json +8 -3
- package/src/db/drift.mjs +0 -1
- package/src/db/gate.mjs +1 -1
- package/src/db/notes.mjs +1 -1
- package/src/db/overrides.mjs +1 -1
- package/src/db/plan.mjs +2 -2
- package/src/db/rebuild.mjs +125 -53
- package/src/db/reconcile.mjs +1 -1
- package/src/db/verify.mjs +1 -0
- package/src/db/worktree.mjs +1 -1
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/registry/fetch.mjs +2 -2
- package/src/skills/hedgehog-daily/SKILL.md +20 -11
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ English | [简体中文](README.zh-CN.md)
|
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://github.com/skyf0xx/hedgehog/actions/workflows/codeql.yml)
|
|
14
14
|
[](https://github.com/skyf0xx/hedgehog/actions/workflows/check.yml)
|
|
15
|
+
[](https://www.bestpractices.dev/projects/14399)
|
|
15
16
|
|
|
16
17
|
AI can write code in seconds.
|
|
17
18
|
|
package/README.zh-CN.md
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
[](LICENSE)
|
|
13
13
|
[](https://github.com/skyf0xx/hedgehog/actions/workflows/codeql.yml)
|
|
14
14
|
[](https://github.com/skyf0xx/hedgehog/actions/workflows/check.yml)
|
|
15
|
+
[](https://www.bestpractices.dev/projects/14399)
|
|
15
16
|
|
|
16
17
|
AI 能在几秒钟内写出代码。
|
|
17
18
|
|
package/bin/cli.mjs
CHANGED
|
@@ -78,17 +78,13 @@ import {
|
|
|
78
78
|
createWorktree,
|
|
79
79
|
hedgehogWorktrees,
|
|
80
80
|
branchName,
|
|
81
|
-
worktreePath,
|
|
82
81
|
intentReadyToMerge,
|
|
83
|
-
intentTaskStatuses,
|
|
84
82
|
mergeBranch,
|
|
85
83
|
removeWorktree,
|
|
86
84
|
intentFileCommitted,
|
|
87
85
|
onHedgehogBranch,
|
|
88
|
-
loadAbandoned,
|
|
89
86
|
writeAbandonedFile,
|
|
90
87
|
applyAbandonment,
|
|
91
|
-
replayAbandonments,
|
|
92
88
|
ABANDONED_DIR,
|
|
93
89
|
} from '../src/db/worktree.mjs';
|
|
94
90
|
import { loadOverrides, addOverride, orphanedOverrides, OVERRIDES_DIR } from '../src/db/overrides.mjs';
|
|
@@ -126,7 +122,6 @@ const BLOCKED_REASON_LABELS = {
|
|
|
126
122
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
127
123
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
128
124
|
const DEST_ROOT = process.cwd();
|
|
129
|
-
const DEFAULT_CORE = 'full-stack-app';
|
|
130
125
|
|
|
131
126
|
// The version of the payload this CLI carries — what `init` and
|
|
132
127
|
// `update` stamp into the project they write to.
|
|
@@ -3342,7 +3337,7 @@ async function reconcileCommand(args) {
|
|
|
3342
3337
|
if (sub === 'list') {
|
|
3343
3338
|
const reconciliations = await loadReconciliations();
|
|
3344
3339
|
const db = openDb({ readOnly: true });
|
|
3345
|
-
let orphaned
|
|
3340
|
+
let orphaned;
|
|
3346
3341
|
try {
|
|
3347
3342
|
orphaned = orphanedReconciliations(db, reconciliations);
|
|
3348
3343
|
} finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skyf0xx/hedgehog",
|
|
3
|
-
"version": "6.2.
|
|
3
|
+
"version": "6.2.4",
|
|
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": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"hedgehog": "bin/cli.mjs"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
|
-
"
|
|
17
|
+
"lint": "eslint .",
|
|
18
|
+
"check": "npm run lint && node scripts/check.mjs",
|
|
18
19
|
"repro": "node --experimental-sqlite repro/run-all.mjs",
|
|
19
20
|
"release": "node scripts/bump-package-version.mjs",
|
|
20
21
|
"release:plugin": "node scripts/bump-plugin-version.mjs"
|
|
@@ -43,5 +44,9 @@
|
|
|
43
44
|
"scaffold",
|
|
44
45
|
"hedgehog"
|
|
45
46
|
],
|
|
46
|
-
"license": "MIT"
|
|
47
|
+
"license": "MIT",
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^10.0.1",
|
|
50
|
+
"eslint": "^10.9.1"
|
|
51
|
+
}
|
|
47
52
|
}
|
package/src/db/drift.mjs
CHANGED
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
CORE_MODULE,
|
|
33
33
|
loadIntentDependencies,
|
|
34
34
|
} from './plan.mjs';
|
|
35
|
-
import { loadOverrides } from './overrides.mjs';
|
|
36
35
|
|
|
37
36
|
// Statuses a field rewrite is safe on: nothing has been built, leased,
|
|
38
37
|
// committed, or failed against these rows yet, so moving their ALLOWED
|
package/src/db/gate.mjs
CHANGED
package/src/db/notes.mjs
CHANGED
|
@@ -79,7 +79,7 @@ export async function loadNotes(notesDir = NOTES_DIR) {
|
|
|
79
79
|
try {
|
|
80
80
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
81
81
|
} catch (err) {
|
|
82
|
-
throw new Error(`could not read notes file ${path}: ${err.message}
|
|
82
|
+
throw new Error(`could not read notes file ${path}: ${err.message}`, { cause: err });
|
|
83
83
|
}
|
|
84
84
|
const { task, notes } = validateNotesFile(parsed, path);
|
|
85
85
|
byTask.set(task, notes);
|
package/src/db/overrides.mjs
CHANGED
|
@@ -122,7 +122,7 @@ export async function loadOverrides(overridesDir = OVERRIDES_DIR) {
|
|
|
122
122
|
try {
|
|
123
123
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
124
124
|
} catch (err) {
|
|
125
|
-
throw new Error(`could not read override file ${path}: ${err.message}
|
|
125
|
+
throw new Error(`could not read override file ${path}: ${err.message}`, { cause: err });
|
|
126
126
|
}
|
|
127
127
|
const record = validateOverride(parsed, path);
|
|
128
128
|
if (!byTask.has(record.task)) byTask.set(record.task, []);
|
package/src/db/plan.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
// graph holds. Without it such a layer compiles once per intent and
|
|
20
20
|
// every copy but the first is a replay of the same command.
|
|
21
21
|
|
|
22
|
-
import { composeScope
|
|
22
|
+
import { composeScope } from './overrides.mjs';
|
|
23
23
|
|
|
24
24
|
export function fillModule(template, module) {
|
|
25
25
|
return template.replaceAll('{module}', module);
|
|
@@ -535,7 +535,7 @@ export function planTasks(db, core, overrides = new Map(), { excludeIntentIds =
|
|
|
535
535
|
const compiledOnceTaskIds = [];
|
|
536
536
|
// Once-tasks that a newly compiled intent hangs a prerequisite on.
|
|
537
537
|
const onceTasksGainingPrereqs = [];
|
|
538
|
-
let reopenedOnceTaskIds
|
|
538
|
+
let reopenedOnceTaskIds;
|
|
539
539
|
|
|
540
540
|
db.exec('BEGIN IMMEDIATE');
|
|
541
541
|
try {
|
package/src/db/rebuild.mjs
CHANGED
|
@@ -198,12 +198,12 @@ async function replayIntents(db, intentsDir) {
|
|
|
198
198
|
try {
|
|
199
199
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
200
200
|
} catch (err) {
|
|
201
|
-
throw new Error(`could not read intent file ${path}: ${err.message}
|
|
201
|
+
throw new Error(`could not read intent file ${path}: ${err.message}`, { cause: err });
|
|
202
202
|
}
|
|
203
203
|
try {
|
|
204
204
|
records.push({ file, intent: normalizeIntent(parsed) });
|
|
205
205
|
} catch (err) {
|
|
206
|
-
throw new Error(`invalid intent file ${path}: ${err.message}
|
|
206
|
+
throw new Error(`invalid intent file ${path}: ${err.message}`, { cause: err });
|
|
207
207
|
}
|
|
208
208
|
}
|
|
209
209
|
|
|
@@ -218,26 +218,32 @@ async function replayIntents(db, intentsDir) {
|
|
|
218
218
|
return count;
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
// Every commit subject in history mapped to
|
|
222
|
-
// one `git log` call rather than one per task. Membership
|
|
223
|
-
// "did this task ever run"; position also answers "did it
|
|
224
|
-
// the thing it depends on", which is what a `once: true`
|
|
225
|
-
// since its commit subject is a constant that one historical
|
|
226
|
-
// would otherwise satisfy forever.
|
|
221
|
+
// Every commit subject in history mapped to EVERY position it occurs at,
|
|
222
|
+
// newest first — one `git log` call rather than one per task. Membership
|
|
223
|
+
// alone answers "did this task ever run"; position also answers "did it
|
|
224
|
+
// run *after* the thing it depends on", which is what a `once: true`
|
|
225
|
+
// task needs, since its commit subject is a constant that one historical
|
|
226
|
+
// occurrence would otherwise satisfy forever. Keeping every occurrence
|
|
227
|
+
// (not just the newest) is what lets markCompletedTasks tell two tasks
|
|
228
|
+
// that share a constant commit_message apart on a linear-chain core: one
|
|
229
|
+
// real commit per intent that actually ran the layer, each position
|
|
230
|
+
// claimable by at most one task. `--topo-order` so position is a
|
|
227
231
|
// property of the history's shape rather than of commit timestamps.
|
|
228
232
|
function loadCommitSubjects() {
|
|
229
233
|
const output = execSync('git log --topo-order --format=%H%x00%s', { encoding: 'utf8' });
|
|
230
|
-
const
|
|
234
|
+
const positions = new Map();
|
|
231
235
|
let position = 0;
|
|
232
236
|
for (const line of output.split('\n')) {
|
|
233
237
|
if (!line) continue;
|
|
234
238
|
const [, subject] = line.split('\0');
|
|
235
239
|
if (subject === undefined) continue;
|
|
236
|
-
// Newest first, so
|
|
237
|
-
|
|
240
|
+
// Newest first, so pushing in read order keeps each subject's array
|
|
241
|
+
// newest-to-oldest.
|
|
242
|
+
if (!positions.has(subject)) positions.set(subject, []);
|
|
243
|
+
positions.get(subject).push(position);
|
|
238
244
|
position++;
|
|
239
245
|
}
|
|
240
|
-
return
|
|
246
|
+
return positions;
|
|
241
247
|
}
|
|
242
248
|
|
|
243
249
|
// A task is complete iff some commit's subject exactly matches its
|
|
@@ -246,25 +252,65 @@ function loadCommitSubjects() {
|
|
|
246
252
|
// there's no verify_command to re-run and no working tree diff to check,
|
|
247
253
|
// only the historical fact that the commit already happened.
|
|
248
254
|
//
|
|
249
|
-
// A
|
|
250
|
-
//
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
//
|
|
255
|
+
// A task's `commit_message` is unique to it exactly when the layer's
|
|
256
|
+
// `commit` template interpolates `{module}` — the module-axis case,
|
|
257
|
+
// where each intent's copy of the layer produces a distinct string. A
|
|
258
|
+
// `once: true` layer has no module to substitute (core.mjs's
|
|
259
|
+
// validateCore rejects one that names {module}), so its single task's
|
|
260
|
+
// commit subject is a constant by construction — and, since a core
|
|
261
|
+
// compiles at most one once-task per once-layer, that task is the only
|
|
262
|
+
// one carrying its subject. A linear-chain core (authored, adopted) has
|
|
263
|
+
// no module axis at all: every layer's `commit` is a fixed string in
|
|
264
|
+
// core.yaml with no `{module}` token, so every intent that walks the
|
|
265
|
+
// chain compiles a per-layer task carrying that same constant — one task
|
|
266
|
+
// per intent, all sharing one subject.
|
|
261
267
|
//
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
+
// A commit_message shared by more than one task (grouped below into
|
|
269
|
+
// `ambiguousTasks`) means membership in commitSubjects can't tell those
|
|
270
|
+
// tasks apart: every commit ever made with that subject is a candidate
|
|
271
|
+
// match for every one of them. Two conditions resolve it:
|
|
272
|
+
//
|
|
273
|
+
// 1. Ordering — a task's own matching commit must sit *above* (be
|
|
274
|
+
// newer than) every one of its own prerequisites' matching
|
|
275
|
+
// commits, the same way a once-task has always required its commit
|
|
276
|
+
// to postdate the module it deploys. Without this, the first
|
|
277
|
+
// intent's commit for a layer would satisfy every later intent's
|
|
278
|
+
// task of that same layer forever, re-closing the layer on a fresh
|
|
279
|
+
// clone regardless of what that later intent's own chain has
|
|
280
|
+
// actually done.
|
|
281
|
+
// 2. Consumption — a group of N tasks sharing a subject can credit at
|
|
282
|
+
// most as many of them complete as there are actual commits with
|
|
283
|
+
// that subject in history, each commit backing at most one task.
|
|
284
|
+
// Ordering alone doesn't catch a head-of-chain task: with no
|
|
285
|
+
// prerequisite of its own, "ran after its prerequisites" is
|
|
286
|
+
// vacuously true regardless of which commit it points at, which is
|
|
287
|
+
// exactly the shape of the bug — a fresh intent's first layer,
|
|
288
|
+
// sharing a constant commit_message with an already-built intent's
|
|
289
|
+
// completed first layer, has nothing to check position against.
|
|
290
|
+
// Consumption is what a head-of-chain task actually needs: once
|
|
291
|
+
// every real commit for that subject is claimed by other tasks,
|
|
292
|
+
// none is left for it to point at.
|
|
293
|
+
//
|
|
294
|
+
// Both are walked together, per group, in a single deterministic pass
|
|
295
|
+
// ordered by task id: earlier-sorted tasks get first claim on the
|
|
296
|
+
// oldest still-unclaimed matching commit that satisfies the ordering
|
|
297
|
+
// condition against whatever their own prerequisites already claimed.
|
|
298
|
+
// A task that finds no claimable commit is left incomplete — the safe
|
|
299
|
+
// direction, matching every other cross-cutting-layer default in this
|
|
300
|
+
// engine (re-running an idempotent step beats silently skipping one).
|
|
301
|
+
// The whole thing is walked to a fixpoint, since one ambiguous task's
|
|
302
|
+
// claim can be the prerequisite another ambiguous task needs before it
|
|
303
|
+
// can claim its own (a once-layer behind another once-layer, or one
|
|
304
|
+
// linear-chain layer behind the one before it in the same intent).
|
|
305
|
+
//
|
|
306
|
+
// A task with a commit_message unique to it (the ordinary module-axis
|
|
307
|
+
// case) skips all of this: it is marked complete directly from
|
|
308
|
+
// commitSubjects membership, with no ordering or consumption check.
|
|
309
|
+
// That's necessary, not just cheaper — a task that completed without
|
|
310
|
+
// touching any file leaves no commit at all (verifyTask writes none),
|
|
311
|
+
// and applying either check there would cascade that gap through the
|
|
312
|
+
// whole chain and reset already-built modules that have no ambiguity to
|
|
313
|
+
// resolve in the first place.
|
|
268
314
|
function markCompletedTasks(db, commitSubjects) {
|
|
269
315
|
const tasks = db.prepare('SELECT id, module, commit_message FROM tasks').all();
|
|
270
316
|
const prerequisites = new Map(tasks.map((t) => [t.id, []]));
|
|
@@ -272,39 +318,64 @@ function markCompletedTasks(db, commitSubjects) {
|
|
|
272
318
|
prerequisites.get(d.task_id)?.push(d.depends_on_task_id);
|
|
273
319
|
}
|
|
274
320
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
);
|
|
321
|
+
const messageCounts = new Map();
|
|
322
|
+
for (const task of tasks) {
|
|
323
|
+
messageCounts.set(task.commit_message, (messageCounts.get(task.commit_message) ?? 0) + 1);
|
|
324
|
+
}
|
|
325
|
+
const isAmbiguous = (task) => messageCounts.get(task.commit_message) > 1;
|
|
280
326
|
|
|
327
|
+
// Position of each unambiguous task's one matching commit; undefined
|
|
328
|
+
// means the task never committed. An ambiguous task's position is
|
|
329
|
+
// resolved separately below, since "the" matching commit for its
|
|
330
|
+
// subject isn't decided until a claim succeeds.
|
|
331
|
+
const positionOf = new Map();
|
|
281
332
|
const complete = new Set();
|
|
282
|
-
const
|
|
333
|
+
const ambiguousTasks = [];
|
|
283
334
|
for (const task of tasks) {
|
|
284
|
-
if (task.module === CORE_MODULE) {
|
|
285
|
-
|
|
335
|
+
if (task.module === CORE_MODULE || isAmbiguous(task)) {
|
|
336
|
+
ambiguousTasks.push(task);
|
|
286
337
|
continue;
|
|
287
338
|
}
|
|
288
|
-
|
|
339
|
+
const position = (commitSubjects.get(task.commit_message) ?? [])[0];
|
|
340
|
+
positionOf.set(task.id, position);
|
|
341
|
+
if (position !== undefined) complete.add(task.id);
|
|
289
342
|
}
|
|
343
|
+
// Deterministic claim order within a shared subject: sorted by task id.
|
|
344
|
+
ambiguousTasks.sort((a, b) => a.id.localeCompare(b.id));
|
|
345
|
+
|
|
346
|
+
// Every commit position still unclaimed, per subject — shrinks as
|
|
347
|
+
// ambiguous tasks below claim one each.
|
|
348
|
+
const available = new Map(
|
|
349
|
+
[...commitSubjects].map(([subject, positions]) => [subject, [...positions]]),
|
|
350
|
+
);
|
|
290
351
|
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
const ranAfterPrerequisites = (task) =>
|
|
294
|
-
|
|
295
|
-
return prerequisites.get(task.id).every((id) => {
|
|
352
|
+
// A task ran after a prerequisite when its own claimed position is
|
|
353
|
+
// strictly smaller (newer) than the prerequisite's.
|
|
354
|
+
const ranAfterPrerequisites = (task, position) =>
|
|
355
|
+
prerequisites.get(task.id).every((id) => {
|
|
296
356
|
const prereq = positionOf.get(id);
|
|
297
|
-
return prereq === undefined ||
|
|
357
|
+
return prereq === undefined || position < prereq;
|
|
298
358
|
});
|
|
299
|
-
};
|
|
300
359
|
|
|
301
360
|
let changed = true;
|
|
302
361
|
while (changed) {
|
|
303
362
|
changed = false;
|
|
304
|
-
for (const task of
|
|
305
|
-
if (complete.has(task.id) || positionOf.
|
|
363
|
+
for (const task of ambiguousTasks) {
|
|
364
|
+
if (complete.has(task.id) || positionOf.has(task.id)) continue;
|
|
306
365
|
if (!prerequisites.get(task.id).every((id) => complete.has(id))) continue;
|
|
307
|
-
|
|
366
|
+
|
|
367
|
+
const slots = available.get(task.commit_message) ?? [];
|
|
368
|
+
// Oldest-first, so a task claims the least-recent commit that
|
|
369
|
+
// still satisfies its ordering condition — leaving newer slots
|
|
370
|
+
// free for whichever task in the group depends on this one.
|
|
371
|
+
const slotIndex = [...slots]
|
|
372
|
+
.map((position, i) => [position, i])
|
|
373
|
+
.sort((a, b) => b[0] - a[0])
|
|
374
|
+
.find(([position]) => ranAfterPrerequisites(task, position))?.[1];
|
|
375
|
+
if (slotIndex === undefined) continue;
|
|
376
|
+
|
|
377
|
+
const [position] = slots.splice(slotIndex, 1);
|
|
378
|
+
positionOf.set(task.id, position);
|
|
308
379
|
complete.add(task.id);
|
|
309
380
|
changed = true;
|
|
310
381
|
}
|
|
@@ -313,13 +384,14 @@ function markCompletedTasks(db, commitSubjects) {
|
|
|
313
384
|
const setComplete = db.prepare("UPDATE tasks SET status = 'complete' WHERE id = ?");
|
|
314
385
|
for (const id of complete) setComplete.run(id);
|
|
315
386
|
|
|
316
|
-
// A
|
|
317
|
-
//
|
|
318
|
-
// Reconcile it back rather than leaving the stale status
|
|
387
|
+
// A task on the ambiguous path can be marked complete by the loop
|
|
388
|
+
// above and then fail to claim a slot on a later pass of the fixpoint
|
|
389
|
+
// walk. Reconcile it back rather than leaving the stale status
|
|
390
|
+
// untouched.
|
|
319
391
|
const reopen = db.prepare(
|
|
320
392
|
"UPDATE tasks SET status = 'planned' WHERE id = ? AND status = 'complete'",
|
|
321
393
|
);
|
|
322
|
-
for (const task of
|
|
394
|
+
for (const task of ambiguousTasks) {
|
|
323
395
|
if (!complete.has(task.id)) reopen.run(task.id);
|
|
324
396
|
}
|
|
325
397
|
|
package/src/db/reconcile.mjs
CHANGED
|
@@ -148,7 +148,7 @@ export async function loadReconciliations(reconciledDir = RECONCILED_DIR) {
|
|
|
148
148
|
try {
|
|
149
149
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
150
150
|
} catch (err) {
|
|
151
|
-
throw new Error(`could not read reconciliation file ${path}: ${err.message}
|
|
151
|
+
throw new Error(`could not read reconciliation file ${path}: ${err.message}`, { cause: err });
|
|
152
152
|
}
|
|
153
153
|
const record = validateReconciled(parsed, path);
|
|
154
154
|
byTask.set(record.task, record);
|
package/src/db/verify.mjs
CHANGED
|
@@ -562,6 +562,7 @@ export function verifyTask(db, taskId, owner) {
|
|
|
562
562
|
`the cause (a rejecting git hook, commit signing, or git identity), then ` +
|
|
563
563
|
`re-run \`hedgehog verify ${task.id} --owner ${owner}\`, or hand it back ` +
|
|
564
564
|
`with \`hedgehog release ${task.id} --owner ${owner}\`.\n\n${err.message}`,
|
|
565
|
+
{ cause: err },
|
|
565
566
|
);
|
|
566
567
|
}
|
|
567
568
|
|
package/src/db/worktree.mjs
CHANGED
|
@@ -376,7 +376,7 @@ export async function loadAbandoned(abandonedDir = ABANDONED_DIR) {
|
|
|
376
376
|
try {
|
|
377
377
|
parsed = JSON.parse(await readFile(path, 'utf8'));
|
|
378
378
|
} catch (err) {
|
|
379
|
-
throw new Error(`could not read abandonment file ${path}: ${err.message}
|
|
379
|
+
throw new Error(`could not read abandonment file ${path}: ${err.message}`, { cause: err });
|
|
380
380
|
}
|
|
381
381
|
const record = validateAbandoned(parsed, path);
|
|
382
382
|
byIntent.set(record.intent, record);
|
package/src/registry/fetch.mjs
CHANGED
|
@@ -122,14 +122,14 @@ async function packAndExtract(entry) {
|
|
|
122
122
|
tarball = join(tmp, JSON.parse(stdout)[0].filename);
|
|
123
123
|
} catch (err) {
|
|
124
124
|
await rm(tmp, { recursive: true, force: true });
|
|
125
|
-
throw new Error(unresolvableMessage(entry, spec, err));
|
|
125
|
+
throw new Error(unresolvableMessage(entry, spec, err), { cause: err });
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
try {
|
|
129
129
|
await execFileAsync('tar', ['-xzf', tarball, '-C', tmp]);
|
|
130
130
|
} catch (err) {
|
|
131
131
|
await rm(tmp, { recursive: true, force: true });
|
|
132
|
-
throw new Error(`Could not extract ${entry.package} from ${tarball}: ${err.message}
|
|
132
|
+
throw new Error(`Could not extract ${entry.package} from ${tarball}: ${err.message}`, { cause: err });
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
const root = join(tmp, TARBALL_ROOT);
|
|
@@ -29,22 +29,31 @@ with a real cost, taken on stated conditions — not the safe default.
|
|
|
29
29
|
## The three exits
|
|
30
30
|
|
|
31
31
|
Decide by the conditions, in order. The first one that holds is the exit.
|
|
32
|
+
Re-plan is checked first, and its check is a read to make, not a
|
|
33
|
+
suspicion to already hold — do not reach Change-work or Tweak without
|
|
34
|
+
having made it.
|
|
32
35
|
|
|
33
36
|
### Re-plan
|
|
34
37
|
|
|
35
|
-
The
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
for an adopted one.
|
|
38
|
+
The core's own loop skill names which artifact governs this project —
|
|
39
|
+
the brief and layer sequence for a shipped core, `.hedgehog/core-design.md`
|
|
40
|
+
for an authored one, `.hedgehog/adoption.md` for an adopted one. Read
|
|
41
|
+
that artifact, then check:
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
already built.
|
|
43
|
+
- Does anything the request needs contradict a statement locked there —
|
|
44
|
+
an interface, a stack choice, a scope boundary?
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
If it holds, this is Re-plan — regardless of how many layers the request
|
|
47
|
+
reaches or whether every file it touches already exists. Route to
|
|
48
|
+
`planner`'s re-entry pass, which adds intents for new work without
|
|
49
|
+
re-running planning from scratch and without disturbing anything already
|
|
50
|
+
built.
|
|
51
|
+
|
|
52
|
+
Where the contradiction means the request is a different project rather
|
|
53
|
+
than an extension of this one, say so plainly instead of routing. That
|
|
54
|
+
artifact is never rewritten to accommodate new scope.
|
|
55
|
+
|
|
56
|
+
If it does not hold, move on to Change-work.
|
|
48
57
|
|
|
49
58
|
### Change-work
|
|
50
59
|
|