@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "5.2.0",
3
+ "version": "5.3.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": {
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "5.2.0",
3
+ "version": "5.3.0",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }