@skyf0xx/hedgehog 5.2.0 → 5.3.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 +89 -1
- package/package.json +1 -1
- package/src/agents/planner.md +10 -1
- package/src/db/claim.mjs +88 -0
- package/src/hosts/gemini/gemini-extension.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
releaseTask,
|
|
38
38
|
renewLease,
|
|
39
39
|
retryTask,
|
|
40
|
+
reopenTask,
|
|
40
41
|
reapExpiredLeases,
|
|
41
42
|
} from '../src/db/claim.mjs';
|
|
42
43
|
import { readyTasks, formatReady } from '../src/db/ready.mjs';
|
|
@@ -499,7 +500,9 @@ ${bold('Usage')}
|
|
|
499
500
|
npx @skyf0xx/hedgehog update refresh the installed agents + skills
|
|
500
501
|
npx @skyf0xx/hedgehog update --check report whether a newer release is published
|
|
501
502
|
npx @skyf0xx/hedgehog db init create .hedgehog/hedgehog.db if absent
|
|
502
|
-
npx @skyf0xx/hedgehog db rebuild re-derive the build graph from committed intents + git history
|
|
503
|
+
npx @skyf0xx/hedgehog db rebuild re-derive the build graph from committed intents + git history;
|
|
504
|
+
also reconciles graph state after a hand-committed fix landed
|
|
505
|
+
outside 'hedgehog verify' — run it to mark that work complete
|
|
503
506
|
npx @skyf0xx/hedgehog plan compile pending intents into tasks + dependencies
|
|
504
507
|
(starts no graph server; --no-open says so explicitly)
|
|
505
508
|
npx @skyf0xx/hedgehog plan --open also start the graph server and open it, if anything compiled
|
|
@@ -515,6 +518,8 @@ ${bold('Usage')}
|
|
|
515
518
|
npx @skyf0xx/hedgehog claim --owner <owner> [--count <n>] atomically claim up to n ready tasks
|
|
516
519
|
npx @skyf0xx/hedgehog claim <task-id> --owner <owner> claim one specific task (breaks a starvation tie)
|
|
517
520
|
npx @skyf0xx/hedgehog retry <task-id> return a blocked task to planned, so it can be rebuilt
|
|
521
|
+
npx @skyf0xx/hedgehog reopen <task-id> --confirm return a complete task (and its complete dependents) to
|
|
522
|
+
planned, for a Correction Protocol fix to an already-shipped layer
|
|
518
523
|
npx @skyf0xx/hedgehog release <task-id> --owner <owner> hand a claimed task back to ready
|
|
519
524
|
npx @skyf0xx/hedgehog renew <task-id> --owner <owner> [--minutes <n>] extend a held lease
|
|
520
525
|
npx @skyf0xx/hedgehog verify <task-id> --owner <owner> run scope + verify checks, commit on pass
|
|
@@ -1927,6 +1932,84 @@ async function retryCommand(args) {
|
|
|
1927
1932
|
console.log(` ${dim('claim it with')} hedgehog claim ${taskId} --owner <owner>`);
|
|
1928
1933
|
}
|
|
1929
1934
|
|
|
1935
|
+
// `hedgehog reopen <task-id> --confirm` — the transition out of
|
|
1936
|
+
// `complete`, for a Correction Protocol fix: a downstream layer reveals
|
|
1937
|
+
// an upstream one (already verified, committed, and possibly built upon)
|
|
1938
|
+
// was wrong. `retry` only returns a `blocked` task to `planned`; nothing
|
|
1939
|
+
// else in the CLI moves a `complete` task backward, which left a
|
|
1940
|
+
// Correction Protocol fix with no path but a hand-authored commit
|
|
1941
|
+
// outside `hedgehog verify` entirely (see the issue this command closes).
|
|
1942
|
+
//
|
|
1943
|
+
// `--confirm` is required and is the whole difference from `retry`'s
|
|
1944
|
+
// UX: retrying a blocked task undoes nothing that shipped, but reopening
|
|
1945
|
+
// a complete task does, transitively, for everything built on top of it
|
|
1946
|
+
// — that's consequential enough to need the caller to say so explicitly
|
|
1947
|
+
// rather than default to it.
|
|
1948
|
+
async function reopenCommand(args) {
|
|
1949
|
+
await ensureDb();
|
|
1950
|
+
|
|
1951
|
+
const taskId = args[0] && !args[0].startsWith('--') ? args[0] : undefined;
|
|
1952
|
+
const confirmed = args.includes('--confirm');
|
|
1953
|
+
if (!taskId) {
|
|
1954
|
+
console.error(`${red('Usage:')} hedgehog reopen <task-id> --confirm\n`);
|
|
1955
|
+
process.exitCode = 1;
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
if (!confirmed) {
|
|
1959
|
+
console.error(
|
|
1960
|
+
`${red('Refused.')} Reopening ${bold(taskId)} undoes a completed, committed layer and\n` +
|
|
1961
|
+
`every completed layer built on top of it. Re-run with ${bold('--confirm')} to proceed:\n\n` +
|
|
1962
|
+
` hedgehog reopen ${taskId} --confirm\n`,
|
|
1963
|
+
);
|
|
1964
|
+
process.exitCode = 1;
|
|
1965
|
+
return;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
if (!(await exists(DB_PATH))) {
|
|
1969
|
+
console.error(`${red('No build graph found.')} Run ${bold('hedgehog db init')} first.\n`);
|
|
1970
|
+
process.exitCode = 1;
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
printDbTarget();
|
|
1975
|
+
|
|
1976
|
+
const db = openDb();
|
|
1977
|
+
let result;
|
|
1978
|
+
try {
|
|
1979
|
+
result = reopenTask(db, taskId);
|
|
1980
|
+
} finally {
|
|
1981
|
+
db.close();
|
|
1982
|
+
}
|
|
1983
|
+
|
|
1984
|
+
if (!result.reopened) {
|
|
1985
|
+
process.exitCode = 1;
|
|
1986
|
+
if (result.reason === 'no_such_task') {
|
|
1987
|
+
console.error(`${red('No such task:')} ${bold(taskId)}${dim(` (in ${dbAbsPath()})`)}\n`);
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
console.error(
|
|
1991
|
+
`${red('Not reopened.')} Task ${bold(taskId)} is ${bold(result.task.status)}, not ${bold('complete')}.\n`,
|
|
1992
|
+
);
|
|
1993
|
+
return;
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
console.log(
|
|
1997
|
+
`${green(bold('Reopened.'))} ${bold(result.reopenedIds.length)} task(s) back to ${bold('planned')}: ${result.reopenedIds.join(', ')}`,
|
|
1998
|
+
);
|
|
1999
|
+
console.log(` ${dim('claim the fix with')} hedgehog claim ${taskId} --owner <owner>`);
|
|
2000
|
+
if (result.stillInFlight.length > 0) {
|
|
2001
|
+
console.log(
|
|
2002
|
+
`\n${yellow(bold('Downstream, not reopened'))} (not complete — nothing shipped yet to invalidate):`,
|
|
2003
|
+
);
|
|
2004
|
+
for (const t of result.stillInFlight) {
|
|
2005
|
+
console.log(` ${bold(t.id)} ${t.status}`);
|
|
2006
|
+
}
|
|
2007
|
+
}
|
|
2008
|
+
console.log(
|
|
2009
|
+
`\n${dim('Hand-committing the fix instead of running it through')} ${bold('hedgehog verify')}${dim('? Reconcile graph state afterward with')} ${bold('hedgehog db rebuild')}${dim('.')}`,
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
|
|
1930
2013
|
// `hedgehog show <task-id>` — the same packet `next` prints, for a task
|
|
1931
2014
|
// named by id whatever its status. Read-only: claims nothing, changes
|
|
1932
2015
|
// nothing.
|
|
@@ -2922,6 +3005,11 @@ async function main() {
|
|
|
2922
3005
|
return;
|
|
2923
3006
|
}
|
|
2924
3007
|
|
|
3008
|
+
if (cmd === 'reopen') {
|
|
3009
|
+
await reopenCommand(args.slice(1));
|
|
3010
|
+
return;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
2925
3013
|
if (cmd === 'release') {
|
|
2926
3014
|
await releaseCommand(args.slice(1));
|
|
2927
3015
|
return;
|
package/package.json
CHANGED
package/src/agents/planner.md
CHANGED
|
@@ -256,11 +256,20 @@ the first-run shape; on re-entry, run `hedgehog-planning-intake`'s
|
|
|
256
256
|
owns `.hedgehog/chain/00-brief.md` and this core's own Confirm & Lock
|
|
257
257
|
stage; `.hedgehog/BMAD/` is written by the shared Phase 0 in
|
|
258
258
|
`hedgehog-planning-intake`.
|
|
259
|
+
- **`deepseek-harness`** → no BMAD shelf runs on this core, and none of
|
|
260
|
+
`hedgehog-planning-intake` applies. Intake is mechanical, owned
|
|
261
|
+
entirely by `hedgehog-dsh-loop`'s own Planning intake section: confirm
|
|
262
|
+
the plugin name and goal with the user, one intent per plugin named
|
|
263
|
+
directly, `hedgehog intent add`, `hedgehog plan`, commit, hand off to
|
|
264
|
+
`bootstrap`. There is no subject/audience/job to mine and no brief to
|
|
265
|
+
lock — open that skill's section rather than looking for the shape
|
|
266
|
+
above here.
|
|
259
267
|
|
|
260
268
|
Either way, this is the mechanical procedure; the judgment — what's
|
|
261
269
|
actually in scope, where a table becomes a module (full-stack-app,
|
|
262
270
|
pwa-app) or what the page's single job actually is (landing-page) —
|
|
263
|
-
stays yours throughout
|
|
271
|
+
stays yours throughout, except on deepseek-harness, where the procedure
|
|
272
|
+
itself is the judgment call: which plugin, named directly with the user.
|
|
264
273
|
|
|
265
274
|
## The Add-ons decision (full-stack-app only)
|
|
266
275
|
|
package/src/db/claim.mjs
CHANGED
|
@@ -401,6 +401,94 @@ export function retryTask(db, taskId) {
|
|
|
401
401
|
});
|
|
402
402
|
}
|
|
403
403
|
|
|
404
|
+
// The full transitive closure of tasks that depend on `taskId`, directly
|
|
405
|
+
// or through another dependent — the same walk verify.mjs's
|
|
406
|
+
// loadDirectDependents feeds one layer at a time, extended to every
|
|
407
|
+
// layer downstream. A Correction Protocol reopen has to see this whole
|
|
408
|
+
// chain: an upstream task built on wrong output can have several
|
|
409
|
+
// already-complete layers stacked on it, and every one of them was
|
|
410
|
+
// built against the thing that's about to change.
|
|
411
|
+
function loadTransitiveDependents(db, taskId) {
|
|
412
|
+
const directDependents = db.prepare(
|
|
413
|
+
'SELECT task_id AS id FROM dependencies WHERE depends_on_task_id = ?',
|
|
414
|
+
);
|
|
415
|
+
const seen = new Set([taskId]);
|
|
416
|
+
const downstream = [];
|
|
417
|
+
let frontier = [taskId];
|
|
418
|
+
while (frontier.length > 0) {
|
|
419
|
+
const next = [];
|
|
420
|
+
for (const id of frontier) {
|
|
421
|
+
for (const row of directDependents.all(id)) {
|
|
422
|
+
if (seen.has(row.id)) continue;
|
|
423
|
+
seen.add(row.id);
|
|
424
|
+
downstream.push(row.id);
|
|
425
|
+
next.push(row.id);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
frontier = next;
|
|
429
|
+
}
|
|
430
|
+
return downstream;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Reopens `taskId` for a Correction Protocol fix: moves a `complete` task
|
|
434
|
+
// (and every `complete` task downstream of it, transitively) back to
|
|
435
|
+
// `planned`, so the fix and every layer built on top of it are rebuilt
|
|
436
|
+
// and re-verified in dependency order, the same as any other `planned`
|
|
437
|
+
// task.
|
|
438
|
+
//
|
|
439
|
+
// This is deliberately not folded into `retryTask` as an automatic extra
|
|
440
|
+
// case: `retry` returns a task the loop itself put into `blocked` —
|
|
441
|
+
// expected, low-stakes, no confirmation needed. Reopening a `complete`
|
|
442
|
+
// task undoes a task the loop already verified and committed, and can
|
|
443
|
+
// invalidate everything built against it since, which is why the CLI
|
|
444
|
+
// requires an explicit `--confirm` before calling this — see
|
|
445
|
+
// `reopenCommand`.
|
|
446
|
+
//
|
|
447
|
+
// A downstream task not yet `complete` (still `planned`, `ready`,
|
|
448
|
+
// `building`, `blocked`) is left exactly where it is: it hasn't shipped
|
|
449
|
+
// anything for the fix to invalidate, and its own lease (if any) is not
|
|
450
|
+
// this command's to touch. Only `complete` tasks — upstream's own status
|
|
451
|
+
// plus every `complete` descendant — move; anything else downstream is
|
|
452
|
+
// reported back so the caller can decide what to do about in-flight
|
|
453
|
+
// work sitting on top of a reopened dependency.
|
|
454
|
+
export function reopenTask(db, taskId) {
|
|
455
|
+
return inTransaction(db, () => {
|
|
456
|
+
reapExpiredLeases(db);
|
|
457
|
+
const task = loadTask(db, taskId);
|
|
458
|
+
if (task === undefined) return { reopened: false, reason: 'no_such_task' };
|
|
459
|
+
if (task.status !== 'complete') {
|
|
460
|
+
return { reopened: false, reason: 'not_complete', task };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const downstreamIds = loadTransitiveDependents(db, taskId);
|
|
464
|
+
const downstreamTasks = downstreamIds.map((id) => loadTask(db, id));
|
|
465
|
+
const toReopen = [task, ...downstreamTasks.filter((t) => t.status === 'complete')];
|
|
466
|
+
const stillInFlight = downstreamTasks.filter((t) => t.status !== 'complete');
|
|
467
|
+
|
|
468
|
+
const reopenOne = db.prepare(
|
|
469
|
+
`
|
|
470
|
+
UPDATE tasks SET status = 'planned', blocked_reason = NULL,
|
|
471
|
+
lease_owner = NULL, lease_expires_at = NULL, leased_at = NULL,
|
|
472
|
+
claim_snapshot = NULL
|
|
473
|
+
WHERE id = ? AND status = 'complete'
|
|
474
|
+
RETURNING id
|
|
475
|
+
`,
|
|
476
|
+
);
|
|
477
|
+
const reopenedIds = [];
|
|
478
|
+
for (const t of toReopen) {
|
|
479
|
+
const result = reopenOne.get(t.id);
|
|
480
|
+
if (result !== undefined) reopenedIds.push(result.id);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return {
|
|
484
|
+
reopened: true,
|
|
485
|
+
reopenedIds,
|
|
486
|
+
stillInFlight,
|
|
487
|
+
task: loadTask(db, taskId),
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
|
|
404
492
|
// Releases `taskId` back to `ready` if `owner` currently holds its lease.
|
|
405
493
|
// Scoped to `status = 'building'` — `verifying` is the engine's own
|
|
406
494
|
// transient lease during verifyTask, not something an external release
|