@skyf0xx/hedgehog 6.3.3 → 6.4.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
@@ -14,6 +14,7 @@
14
14
  // npx @skyf0xx/hedgehog --help
15
15
 
16
16
  import { cp, mkdir, access, readdir, stat, rm, readFile, writeFile, mkdtemp } from 'node:fs/promises';
17
+ import { Buffer } from 'node:buffer';
17
18
  import { constants, existsSync, realpathSync } from 'node:fs';
18
19
  import { fileURLToPath } from 'node:url';
19
20
  import { dirname, join, relative, resolve } from 'node:path';
@@ -58,7 +59,7 @@ import {
58
59
  formatMissingRequirements,
59
60
  } from '../src/db/requires.mjs';
60
61
  import { whyPath, formatWhy } from '../src/db/why.mjs';
61
- import { addFriction, listFriction } from '../src/db/friction.mjs';
62
+ import { addFriction, listFriction, resolveFriction } from '../src/db/friction.mjs';
62
63
  import { addDebt, listDebt, resolveDebt } from '../src/db/debt.mjs';
63
64
  import { addDecision, listDecisions } from '../src/db/decision.mjs';
64
65
  import {
@@ -104,7 +105,7 @@ import { NOOP_DIR } from '../src/db/noop.mjs';
104
105
  import { runFastpath, loadFastpaths, orphanedFastpathTasks, FASTPATH_DIR } from '../src/db/fastpath.mjs';
105
106
  import { HOSTS, HOST_FLAGS, DEFAULT_HOST, availableHosts } from '../src/hosts/index.mjs';
106
107
  import { recordHosts, installedHosts } from '../src/hosts/installed.mjs';
107
- import { wrapSection } from '../src/hosts/claude-md-merge.mjs';
108
+ import { wrapSection, stripPhaseBlocks } from '../src/hosts/claude-md-merge.mjs';
108
109
  import {
109
110
  recordVersion,
110
111
  checkForUpdate,
@@ -372,6 +373,11 @@ function corePayload(core, h, { hostOnly = false } = {}) {
372
373
  // re-vendor, per each shelf's ATTRIBUTION.md) — none of those belong in
373
374
  // an update, so a core's `workspace` and `vendor_skills` are left alone
374
375
  // here while its agents and skills are refreshed.
376
+ // The bootstrap file is excluded here on purpose: `hedgehog shed` (and
377
+ // writePlannedFile's own self-heal) is the only thing allowed to remove
378
+ // content from it, and an update pass that also touched it would race
379
+ // that removal against whatever bootstrap-only content a fresh shell
380
+ // still carries.
375
381
  function updatePlan(host = DEFAULT_HOST, core = null) {
376
382
  const h = HOSTS[host];
377
383
  return [
@@ -478,6 +484,98 @@ function warnOrphanedNotes({ orphanedNotes }) {
478
484
  console.log('');
479
485
  }
480
486
 
487
+ const PHASE_NAME = 'bootstrap-only';
488
+
489
+ // Engine-generic conditions for "this project's build graph is past
490
+ // bootstrap" — never core-specific facts like nx.json or
491
+ // astro.config.mjs, which the engine has no business knowing about. Both
492
+ // must hold: a build graph exists, and at least one task has been
493
+ // compiled into it. The bootstrap file's own {{PROJECT_SUMMARY}}
494
+ // placeholder is the third condition (see shedCommand and
495
+ // writePlannedFile's self-heal) but is checked per-file, since a
496
+ // multi-host project can have several bootstrap files to test.
497
+ async function graphPastBootstrap() {
498
+ if (!(await exists(DB_PATH))) return false;
499
+ const db = openDb();
500
+ try {
501
+ return db.prepare('SELECT 1 FROM tasks LIMIT 1').get() !== undefined;
502
+ } finally {
503
+ db.close();
504
+ }
505
+ }
506
+
507
+ async function canShed(bootstrapContent) {
508
+ if (!(await graphPastBootstrap())) return false;
509
+ return !bootstrapContent.includes('{{PROJECT_SUMMARY}}');
510
+ }
511
+
512
+ // `hedgehog shed` — strips bootstrap-only content from every host
513
+ // bootstrap file actually on disk, once the project has provably moved
514
+ // past bootstrap. Guard conditions are engine-generic only (see
515
+ // graphPastBootstrap/canShed above); this command names which one failed
516
+ // rather than a single generic refusal, in the style of `hedgehog
517
+ // boundary`.
518
+ async function shedCommand() {
519
+ if (!(await exists(DB_PATH))) {
520
+ console.error(
521
+ `${red('No build graph found.')} ${bold(dbAbsPath())} does not exist — run ${bold('hedgehog init')} or ${bold('hedgehog db init')} first.\n`,
522
+ );
523
+ process.exitCode = 1;
524
+ return;
525
+ }
526
+ if (!(await graphPastBootstrap())) {
527
+ console.error(
528
+ `${red('No task has been compiled into the build graph yet.')} Run ${bold('hedgehog plan')} first.\n`,
529
+ );
530
+ process.exitCode = 1;
531
+ return;
532
+ }
533
+
534
+ const hosts = await installedHosts(DEST_ROOT);
535
+ const bootstrapFiles = [
536
+ ...new Set(hosts.map((name) => HOSTS[name].bootstrapFile).filter(Boolean)),
537
+ ];
538
+
539
+ const present = [];
540
+ for (const rel of bootstrapFiles) {
541
+ const abs = join(DEST_ROOT, rel);
542
+ if (await exists(abs)) present.push({ rel, abs });
543
+ }
544
+ if (present.length === 0) {
545
+ console.error(`${red('No bootstrap file found.')} Expected one of: ${bootstrapFiles.join(', ')}\n`);
546
+ process.exitCode = 1;
547
+ return;
548
+ }
549
+
550
+ const unfilled = [];
551
+ for (const { rel, abs } of present) {
552
+ const content = await readFile(abs, 'utf8');
553
+ if (content.includes('{{PROJECT_SUMMARY}}')) unfilled.push(rel);
554
+ }
555
+ if (unfilled.length > 0) {
556
+ console.error(
557
+ `${red('{{PROJECT_SUMMARY}} is still unfilled in:')} ${unfilled.join(', ')} — nothing has been built yet.\n`,
558
+ );
559
+ process.exitCode = 1;
560
+ return;
561
+ }
562
+
563
+ let anyStripped = false;
564
+ for (const { rel, abs } of present) {
565
+ const before = await readFile(abs, 'utf8');
566
+ const after = stripPhaseBlocks(before, PHASE_NAME);
567
+ if (after === before) continue;
568
+ anyStripped = true;
569
+ await writeFile(abs, after);
570
+ const beforeBytes = Buffer.byteLength(before, 'utf8').toLocaleString('en-US');
571
+ const afterBytes = Buffer.byteLength(after, 'utf8').toLocaleString('en-US');
572
+ console.log(`${rel} ${beforeBytes} → ${afterBytes} B`);
573
+ }
574
+ if (!anyStripped) {
575
+ console.log('nothing to shed');
576
+ }
577
+ }
578
+
481
579
  // Writes one planned file to disk — a straight copy, or for a `merge`
482
580
  // entry, the shell template with {{CORE_SECTION}} replaced by the
483
581
  // chosen core's include.
@@ -528,7 +626,12 @@ async function writePlannedFile(f) {
528
626
  out = out.replaceAll('{{CORE_SECTION}}', wrapSection(section));
529
627
  }
530
628
  const dispatch = await readFile(join(PKG_ROOT, f.merge.dispatch), 'utf8');
531
- await writeFile(f.dest, out.replaceAll('{{HOST_DISPATCH}}', dispatch.trimEnd()));
629
+ out = out.replaceAll('{{HOST_DISPATCH}}', dispatch.trimEnd());
630
+ // Self-heal: a project past bootstrap that re-runs `init --force`
631
+ // must not silently regain bootstrap-only content just because this
632
+ // write started from the pristine shell again.
633
+ if (await canShed(out)) out = stripPhaseBlocks(out, PHASE_NAME);
634
+ await writeFile(f.dest, out);
532
635
  return;
533
636
  }
534
637
  // Rendered from the payload rather than copied from it — the routing
@@ -668,12 +771,15 @@ ${bold('Usage')}
668
771
  npx @skyf0xx/hedgehog graph --no-open start (or reuse) the server; print the URL instead
669
772
  npx @skyf0xx/hedgehog why <path> provenance chain for a file
670
773
  npx @skyf0xx/hedgehog friction add "<note>" log a friction note [--task <task-id>]
671
- npx @skyf0xx/hedgehog friction list list logged friction, oldest first
774
+ npx @skyf0xx/hedgehog friction list [--all] list open friction, oldest first (--all includes resolved)
775
+ npx @skyf0xx/hedgehog friction resolve <friction-id> --reason "<why>" mark a friction note resolved
672
776
  npx @skyf0xx/hedgehog debt add <task-id> "<note>" declare debt that lands in dependent tasks' packets
673
777
  npx @skyf0xx/hedgehog debt list [<task-id>] [--all] list open debt, oldest first (--all includes resolved)
674
778
  npx @skyf0xx/hedgehog debt resolve <debt-id> --reason "<why>" mark a debt note resolved
675
779
  npx @skyf0xx/hedgehog decision add <task-id> "<note>" declare a decision that lands in dependent tasks' packets
676
780
  npx @skyf0xx/hedgehog decision list [<task-id>] list declared decisions, oldest first
781
+ npx @skyf0xx/hedgehog shed strip bootstrap-only content from the installed
782
+ bootstrap file(s), once the project is past bootstrap
677
783
  npx @skyf0xx/hedgehog db migrate bring the graph's schema up to the latest version
678
784
  npx @skyf0xx/hedgehog community star --answer <a> record the star prompt's answer
679
785
  npx @skyf0xx/hedgehog community showcase --repo <url> [--description <text>]
@@ -3299,27 +3405,60 @@ async function frictionCommand(args) {
3299
3405
  }
3300
3406
 
3301
3407
  if (sub === 'list') {
3408
+ const includeResolved = args.includes('--all') || args.includes('--resolved');
3302
3409
  const db = openDb();
3303
3410
  let entries;
3304
3411
  try {
3305
- entries = listFriction(db);
3412
+ entries = listFriction(db, { includeResolved });
3306
3413
  } finally {
3307
3414
  db.close();
3308
3415
  }
3309
3416
 
3310
3417
  if (entries.length === 0) {
3311
- console.log(`${dim('No friction logged.')}\n`);
3418
+ console.log(`${dim(includeResolved ? 'No friction logged.' : 'No open friction.')}\n`);
3312
3419
  return;
3313
3420
  }
3314
3421
  for (const entry of entries) {
3315
3422
  console.log(`#${entry.id} ${dim(entry.loggedAt)}${entry.taskId ? ` ${bold(entry.taskId)}` : ''}`);
3316
- console.log(` ${entry.note}\n`);
3423
+ console.log(` ${entry.note}`);
3424
+ if (entry.resolvedAt) {
3425
+ console.log(` ${green('resolved')} ${dim(entry.resolvedAt)} — ${entry.resolvedReason}`);
3426
+ }
3427
+ console.log('');
3317
3428
  }
3318
3429
  return;
3319
3430
  }
3320
3431
 
3432
+ if (sub === 'resolve') {
3433
+ const frictionId = args[1];
3434
+ const reasonIdx = args.indexOf('--reason');
3435
+ const reason = reasonIdx !== -1 ? args[reasonIdx + 1] : undefined;
3436
+
3437
+ if (!frictionId || frictionId.startsWith('--') || !reason) {
3438
+ console.error(`${red('Usage:')} hedgehog friction resolve <friction-id> --reason "<why>"\n`);
3439
+ process.exitCode = 1;
3440
+ return;
3441
+ }
3442
+
3443
+ const db = openDb();
3444
+ let result;
3445
+ try {
3446
+ result = await resolveFriction(db, { frictionId: Number(frictionId), reason });
3447
+ } catch (err) {
3448
+ console.error(`${red('Failed to resolve friction:')} ${err.message}\n`);
3449
+ process.exitCode = 1;
3450
+ return;
3451
+ } finally {
3452
+ db.close();
3453
+ }
3454
+
3455
+ console.log(` ${green('resolved')} #${result.id}${result.taskId ? ` (${result.taskId})` : ''}`);
3456
+ console.log(` ${dim(result.note)}`);
3457
+ return;
3458
+ }
3459
+
3321
3460
  console.error(
3322
- `${red('Unknown friction subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog friction add "<note>" [--task <task-id>]\n or: hedgehog friction list\n`,
3461
+ `${red('Unknown friction subcommand:')} ${sub ?? '(none)'}\n\nUsage: hedgehog friction add "<note>" [--task <task-id>]\n or: hedgehog friction list [--all]\n or: hedgehog friction resolve <friction-id> --reason "<why>"\n`,
3323
3462
  );
3324
3463
  process.exitCode = 1;
3325
3464
  }
@@ -4513,6 +4652,11 @@ async function main() {
4513
4652
  return;
4514
4653
  }
4515
4654
 
4655
+ if (cmd === 'shed') {
4656
+ await shedCommand();
4657
+ return;
4658
+ }
4659
+
4516
4660
  console.error(`${red('Unknown command:')} ${cmd}\n`);
4517
4661
  await help();
4518
4662
  process.exitCode = 1;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "6.3.3",
3
+ "version": "6.4.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": {
@@ -61,9 +61,11 @@ workspace is scaffolded (see `planner.md`'s Workflow step 7 and step 9).
61
61
  This compiles those intents into tasks so the core's loop skill has
62
62
  something to pick up from `hedgehog next`. Then run `hedgehog graph` to
63
63
  start (or reuse) the live graph server and open it, so the build graph is
64
- on screen before the first build step starts. Then state plainly that
65
- Bootstrap is closed and name the loop skill that owns everything from
66
- here. Don't hand off to another instance of yourself.
64
+ on screen before the first build step starts. Run `hedgehog shed` to
65
+ strip bootstrap-only content from the bootstrap file now that it no
66
+ longer applies. Then state plainly that Bootstrap is closed and name the
67
+ loop skill that owns everything from here. Don't hand off to another
68
+ instance of yourself.
67
69
 
68
70
 
69
71
  ## Constraints
@@ -33,8 +33,44 @@ export async function addFriction(db, { note, taskId }) {
33
33
  }
34
34
 
35
35
  // Returns every friction row, oldest first, for tweaker's review pass.
36
- export function listFriction(db) {
36
+ // Resolved rows are excluded by default — same convention as
37
+ // listDebt — and included when `includeResolved` is set (`--all`).
38
+ export function listFriction(db, { includeResolved = false } = {}) {
39
+ const where = includeResolved ? '' : 'WHERE resolved_at IS NULL';
37
40
  return db
38
- .prepare(`SELECT id, task_id AS taskId, note, logged_at AS loggedAt FROM friction ORDER BY id ASC`)
41
+ .prepare(
42
+ `SELECT id, task_id AS taskId, note, logged_at AS loggedAt,
43
+ resolved_at AS resolvedAt, resolved_reason AS resolvedReason
44
+ FROM friction ${where} ORDER BY id ASC`,
45
+ )
39
46
  .all();
40
47
  }
48
+
49
+ // Resolves one friction row by id: marks it resolved in the DB and
50
+ // appends the marker to the same committed log (FRICTION_LOG_PATH) the
51
+ // original entry was written to — friction has no `.hedgehog/notes/`
52
+ // record the way debt does, so this is the row's only committed source
53
+ // and `db rebuild` leaves friction rows untouched (see rebuild.mjs).
54
+ export async function resolveFriction(db, { frictionId, reason }) {
55
+ if (!frictionId) throw new Error('friction resolve requires a friction id');
56
+ if (!reason) throw new Error('friction resolve requires a --reason');
57
+
58
+ const row = db
59
+ .prepare('SELECT id, task_id AS taskId, note, resolved_at AS resolvedAt FROM friction WHERE id = ?')
60
+ .get(frictionId);
61
+ if (!row) throw new Error(`no such friction entry: #${frictionId}`);
62
+ if (row.resolvedAt) throw new Error(`friction #${frictionId} is already resolved`);
63
+
64
+ const resolvedAt = new Date().toISOString();
65
+ db.prepare('UPDATE friction SET resolved_at = ?, resolved_reason = ? WHERE id = ?').run(
66
+ resolvedAt,
67
+ reason,
68
+ frictionId,
69
+ );
70
+
71
+ await mkdir(FRICTION_DIR, { recursive: true });
72
+ const header = `## ${resolvedAt} resolved #${frictionId}${row.taskId ? ` ${row.taskId}` : ''}`;
73
+ await appendFile(FRICTION_LOG_PATH, `${header}\n\n${reason}\n\n`);
74
+
75
+ return { id: row.id, taskId: row.taskId, note: row.note, resolvedAt, resolvedReason: reason };
76
+ }
package/src/db/ready.mjs CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { findClaimableTasks, findInFlightTasks } from './claim.mjs';
9
9
  import { conflicts, verifyRadius } from './conflict.mjs';
10
+ import { ORCHESTRATING_FOOTER } from './status.mjs';
10
11
 
11
12
  // Walks the same candidates claimTasks would, in the same priority/id
12
13
  // order, greedily sorting each into CLAIMABLE (doesn't conflict with
@@ -92,5 +93,8 @@ export function formatReady({ claimable, heldBack }) {
92
93
  }
93
94
  }
94
95
 
96
+ lines.push('');
97
+ lines.push(ORCHESTRATING_FOOTER);
98
+
95
99
  return lines.join('\n');
96
100
  }
@@ -0,0 +1,116 @@
1
+ // Possibly-satisfied detection: a `planned` task whose scope_globs are
2
+ // already fully covered by git-tracked, committed files with nothing
3
+ // pending inside that scope — the heuristic nudge toward `hedgehog
4
+ // reconcile` for work that landed under a different task's or a
5
+ // different intent's commit, so `db rebuild`'s exact-commit-subject
6
+ // attribution (rebuild.mjs#markCompletedTasks) never had a subject to
7
+ // credit it against and the task sits `planned` forever with no signal.
8
+ //
9
+ // This is evidence, not proof, exactly like reconcile.mjs's
10
+ // gatherEvidence: a scope glob resolving to tracked, clean paths says
11
+ // the files a task would have written already exist and nothing is
12
+ // mid-edit there. It says nothing about whether the task's objective was
13
+ // actually met by that content — a `planned` task can legitimately share
14
+ // a glob with pre-existing, unrelated code that still needs real work.
15
+ // False positives are expected and acceptable; this only flags "look at
16
+ // this", it never completes anything itself.
17
+ //
18
+ // Uses the same git pathspec mechanism verify.mjs's own scope gate uses
19
+ // (`:(glob)<glob>` handed to git literally, no in-process glob dialect of
20
+ // its own to keep in sync with core.mjs's), rather than matchesGlob's
21
+ // witness-path walk — a witness path proves non-containment against a
22
+ // *set* of globs (core.mjs's own use, and coverage's), not "which real
23
+ // files in this repo currently match one glob", which only git's own
24
+ // pathspec matcher can answer without re-walking the whole tree.
25
+
26
+ import { execFileSync } from 'node:child_process';
27
+ import { isEngineStatePath } from './engineState.mjs';
28
+
29
+ function git(args) {
30
+ return execFileSync('git', args, { encoding: 'utf8' });
31
+ }
32
+
33
+ function pathspecsFor(scopeGlobs) {
34
+ return scopeGlobs.map((glob) => `:(glob)${glob}`);
35
+ }
36
+
37
+ // Tracked files under a task's scope, minus engine-state paths — the
38
+ // same exclusion verify.mjs's gate applies, so a task whose scope happens
39
+ // to reach into `.hedgehog/` never gets flagged on the build graph's own
40
+ // bookkeeping.
41
+ function trackedPathsInScope(scopeGlobs) {
42
+ let output;
43
+ try {
44
+ output = git(['ls-files', '--', ...pathspecsFor(scopeGlobs)]);
45
+ } catch {
46
+ return [];
47
+ }
48
+ return output
49
+ .split('\n')
50
+ .map((p) => p.trim())
51
+ .filter(Boolean)
52
+ .filter((p) => !isEngineStatePath(p));
53
+ }
54
+
55
+ // Modified, staged, or untracked paths under a task's scope — anything
56
+ // `git status --porcelain` reports there means the scope is still
57
+ // mid-edit, the opposite of "already satisfied".
58
+ function dirtyPathsInScope(scopeGlobs) {
59
+ let output;
60
+ try {
61
+ output = git(['status', '--porcelain', '--', ...pathspecsFor(scopeGlobs)]);
62
+ } catch {
63
+ return [];
64
+ }
65
+ return output
66
+ .split('\n')
67
+ .map((line) => line.slice(3).trim())
68
+ .filter(Boolean)
69
+ .filter((p) => !isEngineStatePath(p));
70
+ }
71
+
72
+ // Every `planned` task whose scope_globs resolve to at least one tracked
73
+ // file and zero dirty ones — a task with an empty scope match (nothing in
74
+ // the repo yet touches its globs at all) is not flagged, since that is
75
+ // simply unstarted work, not possibly-satisfied work.
76
+ //
77
+ // Only `planned` is read, not `ready`: a `ready` task already cleared
78
+ // every dependency and is next.mjs/claim.mjs's own queue to hand out —
79
+ // flagging it here would tell an operator to reconcile work a claim is
80
+ // about to pick up anyway. `building`/`verifying` are leased and mid-flight
81
+ // by definition; `blocked` has its own NEEDS ATTENTION path; `complete` is
82
+ // done. `planned` is the one status this signal exists for: work nothing
83
+ // has looked at yet, sitting behind whatever unlocked it.
84
+ const PLANNED_TASKS_SQL = `
85
+ SELECT id, layer, objective, scope_globs FROM tasks
86
+ WHERE status = 'planned'
87
+ ORDER BY priority, id;
88
+ `;
89
+
90
+ export function possiblySatisfiedTasks(db) {
91
+ const tasks = db.prepare(PLANNED_TASKS_SQL).all();
92
+ const flagged = [];
93
+ for (const task of tasks) {
94
+ const scopeGlobs = JSON.parse(task.scope_globs);
95
+ if (scopeGlobs.length === 0) continue;
96
+ const tracked = trackedPathsInScope(scopeGlobs);
97
+ if (tracked.length === 0) continue;
98
+ const dirty = dirtyPathsInScope(scopeGlobs);
99
+ if (dirty.length > 0) continue;
100
+ flagged.push({ id: task.id, layer: task.layer, objective: task.objective, paths: tracked });
101
+ }
102
+ return flagged;
103
+ }
104
+
105
+ export function formatPossiblySatisfied(flagged) {
106
+ const lines = [`POSSIBLY SATISFIED ${flagged.length}`];
107
+ for (const { id, layer, paths } of flagged) {
108
+ lines.push(` ${id} ${layer} ${paths.length} tracked path${paths.length === 1 ? '' : 's'} in scope, none pending`);
109
+ }
110
+ lines.push('');
111
+ lines.push(
112
+ ' Scope glob already covered by clean, tracked files — not proof the objective was met.' +
113
+ ' See: hedgehog reconcile',
114
+ );
115
+ return lines.join('\n');
116
+ }
package/src/db/schema.mjs CHANGED
@@ -127,10 +127,12 @@ CREATE TABLE IF NOT EXISTS decisions (
127
127
  );
128
128
 
129
129
  CREATE TABLE IF NOT EXISTS friction (
130
- id INTEGER PRIMARY KEY,
131
- task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
132
- note TEXT NOT NULL,
133
- logged_at TEXT NOT NULL DEFAULT (datetime('now'))
130
+ id INTEGER PRIMARY KEY,
131
+ task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
132
+ note TEXT NOT NULL,
133
+ logged_at TEXT NOT NULL DEFAULT (datetime('now')),
134
+ resolved_at TEXT,
135
+ resolved_reason TEXT
134
136
  );
135
137
  `;
136
138
 
@@ -178,7 +180,7 @@ export function ensureTaskColumns(db) {
178
180
  // hand-set past what MIGRATIONS actually covers, since runMigrations
179
181
  // trusts this number to mean "every migration through this version has
180
182
  // run."
181
- export const CURRENT_SCHEMA_VERSION = 3;
183
+ export const CURRENT_SCHEMA_VERSION = 4;
182
184
 
183
185
  // Forward migrations, applied in order to bring a graph's user_version up
184
186
  // to CURRENT_SCHEMA_VERSION. Unlike the CREATE TABLE IF NOT EXISTS /
@@ -223,6 +225,18 @@ const MIGRATIONS = [
223
225
  }
224
226
  },
225
227
  },
228
+ {
229
+ version: 4,
230
+ // Same shape as version 3's debt columns, for friction — an existing
231
+ // graph's rows default to open the moment these columns exist.
232
+ migrate: (db) => {
233
+ const existing = new Set(db.prepare('PRAGMA table_info(friction)').all().map((row) => row.name));
234
+ if (!existing.has('resolved_at')) db.exec('ALTER TABLE friction ADD COLUMN resolved_at TEXT');
235
+ if (!existing.has('resolved_reason')) {
236
+ db.exec('ALTER TABLE friction ADD COLUMN resolved_reason TEXT');
237
+ }
238
+ },
239
+ },
226
240
  ];
227
241
 
228
242
  // Brings a graph's `PRAGMA user_version` up to CURRENT_SCHEMA_VERSION,
package/src/db/status.mjs CHANGED
@@ -17,6 +17,7 @@ import { listFriction } from './friction.mjs';
17
17
  import { orphanedOverrides } from './overrides.mjs';
18
18
  import { RECONCILED_DIR, RECONCILED_NOTE_PREFIX } from './reconcile.mjs';
19
19
  import { formatMissingRequirements } from './requires.mjs';
20
+ import { possiblySatisfiedTasks, formatPossiblySatisfied } from './satisfied.mjs';
20
21
  import { readyTasks, heldBackReason } from './ready.mjs';
21
22
  import { worktreeStatus } from './worktree.mjs';
22
23
  import { findClaimableTasks } from './claim.mjs';
@@ -219,6 +220,15 @@ function countFriction(db) {
219
220
  // in every count and list here. Reported unconditionally, not as a
220
221
  // warning: reconciling is a supported act, and the point is that the
221
222
  // distinction stays visible after the session that made it is gone.
223
+ //
224
+ // `possiblySatisfied` (satisfied.mjs) is a `planned` task whose scope
225
+ // globs already resolve to tracked, clean files — a heuristic nudge, not
226
+ // a completion: `db rebuild`'s attribution only credits a task whose own
227
+ // commit_message appears as some commit's subject (rebuild.mjs), so work
228
+ // that landed inside a commit made for a different task never has a
229
+ // subject to match, and the task stays `planned` with no other signal
230
+ // that its scope may already be covered. Read-only, like drift and
231
+ // orphaned overrides — nothing here is ever auto-completed.
222
232
  // Synchronous, as it always was: `boundary.mjs#boundaryState` (a hot path
223
233
  // that only ever reads `graph.inFlight`) depends on that, and every other
224
234
  // section here is a plain SQL read with no I/O to await. The worktree
@@ -236,6 +246,7 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
236
246
  const debt = loadDebtByTask(db);
237
247
  const frictionCount = countFriction(db);
238
248
  const reconciled = loadReconciledTasks(db);
249
+ const possiblySatisfied = possiblySatisfiedTasks(db);
239
250
  const total = Object.values(counts).reduce((a, b) => a + b, 0);
240
251
  return {
241
252
  counts,
@@ -248,6 +259,7 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
248
259
  debt,
249
260
  frictionCount,
250
261
  reconciled,
262
+ possiblySatisfied,
251
263
  total,
252
264
  };
253
265
  }
@@ -262,6 +274,12 @@ export async function graphWorktreeStatus(db, opts) {
262
274
  return worktreeStatus(db, opts);
263
275
  }
264
276
 
277
+ // Printed at the bottom of both `hedgehog status` and `hedgehog ready` —
278
+ // the two commands every session provably runs — since no static check
279
+ // can verify an orchestrating session actually reads the skill this
280
+ // names.
281
+ export const ORCHESTRATING_FOOTER = 'See the hedgehog-orchestrating skill for the claim → dispatch → verify cycle.';
282
+
265
283
  const BLOCKED_REASON_LABELS = {
266
284
  verification_failed: 'verification failed',
267
285
  scope_violation: 'scope violation',
@@ -293,6 +311,7 @@ export function formatStatus({
293
311
  debt = [],
294
312
  frictionCount = 0,
295
313
  reconciled = [],
314
+ possiblySatisfied = [],
296
315
  worktrees = { active: [], orphaned: [] },
297
316
  total,
298
317
  missingRequirements,
@@ -381,6 +400,15 @@ export function formatStatus({
381
400
  lines.push(formatDrift(drift));
382
401
  }
383
402
 
403
+ // Below drift, same family of "condition of the graph as a whole" —
404
+ // and above orphaned overrides, since a possibly-satisfied task is work
405
+ // an operator can act on right now (`hedgehog reconcile`), where an
406
+ // orphaned override is only ever inert.
407
+ if (possiblySatisfied.length > 0) {
408
+ lines.push('');
409
+ lines.push(formatPossiblySatisfied(possiblySatisfied));
410
+ }
411
+
384
412
  // Below drift because it's the rarer of the two and never blocks the
385
413
  // build: an orphaned override is work the operator asked for that isn't
386
414
  // happening, not work the graph is getting wrong. `hedgehog override
@@ -448,5 +476,8 @@ export function formatStatus({
448
476
  lines.push(' See: hedgehog reconcile list');
449
477
  }
450
478
 
479
+ lines.push('');
480
+ lines.push(ORCHESTRATING_FOOTER);
481
+
451
482
  return lines.join('\n');
452
483
  }
@@ -6,14 +6,5 @@ context with its own tool grant. The skills in `.claude/skills/` are
6
6
  available the same way; invoke one by name rather than reimplementing what
7
7
  it describes.
8
8
 
9
- Claude Code reads that registration once, at session start — an agent or
10
- skill file written mid-session (by `hedgehog init` or `hedgehog update`
11
- just now) is not yet dispatchable by name in this session. If a name-based
12
- dispatch reports it as not found, read the file directly from
13
- `.claude/agents/` or `.claude/skills/` and follow it inline instead of
14
- retrying the dispatch; it becomes dispatchable by name after a session
15
- restart or a fresh context.
16
-
17
- Clear context with `/clear` at the unit boundaries described above —
18
- `hedgehog boundary` tells you whether you're at one (exit 0), and
19
- `hedgehog boundary --handoff` is what the next session starts from.
9
+ Clear context with `/clear` at the unit boundaries the
10
+ `hedgehog-orchestrating` skill states.
@@ -64,6 +64,74 @@ export function appendCoreSection(existingContent, section) {
64
64
  return `${existingContent.trimEnd()}\n\n${block}\n`;
65
65
  }
66
66
 
67
+ // One phase name only, `bootstrap-only` — there is exactly one
68
+ // transition in a project's life (still bootstrapping vs. not), so a
69
+ // predicate grammar for a one-bit state would be a maintenance burden
70
+ // with no second case to justify it.
71
+ const PHASE_MARKER_START = (phase) => `<!-- hedgehog:${phase} start -->`;
72
+ const PHASE_MARKER_END = (phase) => `<!-- hedgehog:${phase} end -->`;
73
+
74
+ // Removes every `<!-- hedgehog:<phase> start -->...end -->` block from
75
+ // `content`, along with the blank lines immediately surrounding each
76
+ // removed block, so stripping never leaves a double blank line behind.
77
+ // Byte-for-byte identity when no markers are present is the contract
78
+ // every core package not yet using markers relies on.
79
+ //
80
+ // A block is self-contained by convention (no section outside it may
81
+ // reference into it), so removal never leaves a dangling reference for
82
+ // this function to worry about — that's enforced by review of what gets
83
+ // marked, not by this code.
84
+ export function stripPhaseBlocks(content, phase) {
85
+ const start = PHASE_MARKER_START(phase);
86
+ const end = PHASE_MARKER_END(phase);
87
+ const startCount = countOccurrences(content, start);
88
+ const endCount = countOccurrences(content, end);
89
+ if (startCount === 0 && endCount === 0) return content;
90
+ if (startCount !== endCount) {
91
+ throw new Error(
92
+ `stripPhaseBlocks: ${startCount} "${start}" marker(s) but ${endCount} "${end}" marker(s) — unterminated or orphaned`,
93
+ );
94
+ }
95
+
96
+ // Flat, non-nesting blocks only: a start found before the matching end
97
+ // of the previous block indicates nesting, which this vocabulary
98
+ // deliberately does not support.
99
+ const blockRe = new RegExp(`${escapeRe(start)}[\\s\\S]*?${escapeRe(end)}`, 'g');
100
+ // Not a valid marker (the phase name can't contain whitespace), and not
101
+ // realistic page content either, so it can't collide with anything
102
+ // already in `content` — used as a removal placeholder instead of a
103
+ // control character, which linting disallows anywhere a regex pattern
104
+ // could embed it.
105
+ const PLACEHOLDER = '⁣hedgehog-stripped-block⁣';
106
+ let matchCount = 0;
107
+ let stripped = content.replace(blockRe, () => {
108
+ matchCount++;
109
+ return PLACEHOLDER;
110
+ });
111
+ if (matchCount !== startCount) {
112
+ throw new Error(
113
+ `stripPhaseBlocks: found ${startCount} "${start}" marker(s) but only ${matchCount} well-formed block(s) — check for nesting or an orphaned marker`,
114
+ );
115
+ }
116
+
117
+ // Each removed block leaves a placeholder; collapse it and the blank
118
+ // lines around it so no double blank line remains.
119
+ const placeholderRe = new RegExp(`[ \\t]*\\n?[ \\t]*${escapeRe(PLACEHOLDER.trim())}[ \\t]*\\n?[ \\t]*\\n?`, 'g');
120
+ stripped = stripped.replace(placeholderRe, '\n');
121
+ stripped = stripped.replace(/\n{3,}/g, '\n\n');
122
+ return stripped;
123
+ }
124
+
125
+ function countOccurrences(haystack, needle) {
126
+ let count = 0;
127
+ let i = 0;
128
+ while ((i = haystack.indexOf(needle, i)) !== -1) {
129
+ count++;
130
+ i += needle.length;
131
+ }
132
+ return count;
133
+ }
134
+
67
135
  function escapeRe(s) {
68
136
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
69
137
  }
@@ -16,6 +16,5 @@ gates the commit either way.
16
16
  The skills in `.cursor/skills/` are procedures to follow — read the one
17
17
  whose situation applies rather than improvising the steps.
18
18
 
19
- Clear the conversation at the unit boundaries described above —
20
- `hedgehog boundary` tells you whether you're at one (exit 0), and
21
- `hedgehog boundary --handoff` is what the next session starts from.
19
+ Clear the conversation at the unit boundaries the `hedgehog-orchestrating`
20
+ skill states.
@@ -22,8 +22,7 @@ commit lands.
22
22
  The skills in `.gemini/skills/` are procedures to follow — read the one
23
23
  whose situation applies rather than improvising the steps.
24
24
 
25
- Clear the conversation at the unit boundaries described above —
26
- `hedgehog boundary` tells you whether you're at one (exit 0), and
27
- `hedgehog boundary --handoff` is what the next session starts from.
25
+ Clear the conversation at the unit boundaries the `hedgehog-orchestrating`
26
+ skill states.
28
27
 
29
28
  @./AGENTS.md
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "6.3.3",
3
+ "version": "6.4.0",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }
@@ -47,7 +47,7 @@
47
47
  {
48
48
  "name": "adopted",
49
49
  "package": "@skyf0xx/hedgehog-core-adopted",
50
- "version": "^1.1.0",
50
+ "version": ">=1.0.0 <2.0.0",
51
51
  "language": "typescript",
52
52
  "repository": "https://github.com/skyf0xx/hedgehog-core-adopted",
53
53
  "selects_when": "The description is about bringing Hedgehog's discipline to a codebase that already exists, rather than building something new — the repo already has real source files, or the user says so explicitly: \"adopt this repo\", \"add Hedgehog to my existing project\", \"I want scope/verify enforcement on my changes here\". Not chosen by matching a `when` paragraph the way a shipped core is: hedgehog-adopt reads the repo read-only, proposes a linear-chain .hedgehog/core.yaml whose verify commands are the repo's own, and writes only .hedgehog/ — never a workspace, never a stack migration. This is the core most often confused with authored: adopted brings discipline to an existing repo, while authored designs and scaffolds a workspace from scratch for something being built new — route here only when the work is landing on a codebase that already exists."
@@ -0,0 +1,154 @@
1
+ ---
2
+ name: hedgehog-orchestrating
3
+ description: Use at the start of any session on a project with a build graph (`.hedgehog/hedgehog.db`), and again at every claim/dispatch/verify cycle within it. Owns consuming the build graph — claiming task packets, delegating each to the layer's agent, running `hedgehog verify` to gate the commit, recording debt and decisions, the intent check at an intent's last layer, and when to clear context. Not for a build agent handed an already-claimed packet — that agent never claims, dispatches, or verifies; this skill is the orchestrating session's own.
4
+ ---
5
+
6
+ # Orchestrating the build
7
+
8
+ `.hedgehog/hedgehog.db` is the source of truth for what's next — never
9
+ re-derive build state from prose. To work from it:
10
+
11
+ 1. Run `hedgehog claim --count N --owner <owner>`. It's atomic and
12
+ lease-based, and returns up to N tasks (each with its own full
13
+ STATUS/INTENT/RELEVANT RULES/INHERITED DEBT/INHERITED DECISIONS/WHY
14
+ NOW/BLOCKED DOWNSTREAM/ALLOWED SCOPE/VERIFICATION packet) that the
15
+ scheduler has already verified are safe to run together right now —
16
+ scope and verify-radius disjoint. `--count` is a maximum, not a
17
+ promise: a call may return fewer than N, or zero. `hedgehog ready` is
18
+ a read-only preview of the claimable/held-back split (and why a task
19
+ is held back — conflict with another claimable task, or exclusivity)
20
+ before you claim.
21
+ 2. Delegate each claimed packet to this core's loop skill (named in the
22
+ project's core section), one dispatch per packet, running concurrently.
23
+ 3. As each agent reports its packet done, run `hedgehog verify
24
+ <task-id> --owner <owner>` **serially** — one at a time, even though
25
+ the building happened concurrently, because verify writes git commits
26
+ and those must land one at a time. It checks the touched files
27
+ against the packet's ALLOWED SCOPE, runs the verification command,
28
+ and on a pass writes the commit and unlocks whatever the task was
29
+ blocking. An agent reporting success never moves the task — only a
30
+ passing `hedgehog verify` exit code does.
31
+
32
+ `hedgehog claim` hands out only tasks safe to run together. Never run
33
+ two tasks it didn't hand you together.
34
+
35
+ The packet's **INTENT** block names the goal and outcome of the *whole*
36
+ intent, not just this layer. A layer's own verify command runs the tests
37
+ that layer wrote, so it measures internal consistency and never coverage
38
+ of what was asked — a layer that builds half the intent and tests that
39
+ half exhaustively is green. Build the layer's share of the goal, and say
40
+ so when the packet doesn't account for something the goal asks for.
41
+ When `hedgehog verify` closes the **last** layer of an intent it prints
42
+ that goal and outcome back as an **INTENT CHECK**: read the built work
43
+ against it there, because nothing else in the build does.
44
+
45
+ A layer that discovers a limitation the next layer has to compensate for
46
+ records it with `hedgehog debt add <task-id> "<note>"` — it lands in the
47
+ **INHERITED DEBT** section of every packet that depends on that task. A
48
+ comment in a source file is not a mechanism; nothing reads it.
49
+
50
+ A layer that makes a choice a dependent layer needs to know about — a
51
+ pattern, a library, a trade-off, anything the next task should follow
52
+ rather than reinvent or contradict — records it with `hedgehog decision
53
+ add <task-id> "<note>"`, landing in the **INHERITED DECISIONS** section
54
+ the same way. Debt is what's still wrong with a task; a decision is why
55
+ it was built the way it was.
56
+
57
+ `planner` owns writing intents (`hedgehog intent add`) at planning
58
+ intake; `hedgehog plan` compiles them into the task graph the loop
59
+ consumes. Nothing checks a box — there is no checklist, only queryable
60
+ state.
61
+
62
+ **When the build is done:** once `hedgehog status` shows every task
63
+ `complete` and `hedgehog boundary` exits 0 (see **Managing context**
64
+ below — it checks nothing-in-flight, a clean tree, and a closed intent
65
+ together), the build session is complete. The permanent record is the
66
+ committed intents (`.hedgehog/intents/*.json`), the friction log
67
+ (`.hedgehog/friction/*.md`), the core definition (root `core.yaml` for a
68
+ shipped core, `.hedgehog/core.yaml` for an authored one), and the git
69
+ commit history itself — not the database. `.hedgehog/hedgehog.db` is
70
+ gitignored: a derived index, rebuildable at any time via `hedgehog db
71
+ rebuild`, which replays those committed sources against git history.
72
+ That rebuild also runs automatically on a fresh clone when the DB is
73
+ missing but `.hedgehog/intents/` exists. That's what makes every later
74
+ session cheap.
75
+
76
+ A completed build is **extendable, not sealed**. Offer the user a
77
+ fresh-context handoff, and name both ways forward:
78
+
79
+ - **Adjustments to what's built** → the `tweaker` agent, from a *new*
80
+ chat window, not a subagent call inside this one — this session's
81
+ context has been building the whole project and is exactly what
82
+ "clearing context now costs nothing" (below) means to discard. Tell
83
+ the user plainly: close this chat window and open a new one, then
84
+ paste this to start it:
85
+
86
+ > The build for {{PROJECT_NAME}} is complete. Use the tweaker agent:
87
+ > first review the friction log and ask me for feedback on the build,
88
+ > then take my tweak requests one at a time.
89
+
90
+ In the new window, `tweaker` starts clean, once reviews the friction
91
+ log (`hedgehog friction list`) for possible discipline-improvement
92
+ issues and separately asks the user directly for feedback on the
93
+ build, filing each real pattern or piece of feedback as its own GitHub
94
+ issue against the Hedgehog repo itself, never this project's repo
95
+ (friction as `bug`/`help wanted`, feedback as `suggestion`, each only
96
+ after showing the exact content and getting explicit approval), then
97
+ takes any tweak requests one at a time.
98
+ - **New scope** — a new module or feature, anything beyond adjusting what
99
+ exists → on a core with a module axis, the `planner` agent, which runs
100
+ `hedgehog-planning-intake`'s **Re-entry pass**. It reads the existing
101
+ planning archive as context and elicits only what's new, then adds
102
+ intents and runs `hedgehog plan`. This is append-only: `plan` skips
103
+ intents already compiled, so every `complete` task keeps its status and
104
+ its commits, and `hedgehog claim` resumes at the first tasks of the new
105
+ work. Planning is not re-run from scratch, and the workspace is not
106
+ re-scaffolded. (This core's own section states where new scope goes if
107
+ this core has no module axis to add an intent to.)
108
+
109
+ If a request turns out to be structural rather than either of those —
110
+ something already built is wrong at its source — that's the Correction
111
+ Protocol's post-build entry, in this core's own loop skill.
112
+
113
+ ## Managing context
114
+
115
+ Hedgehog is designed so the conversation is disposable. Keep the working
116
+ context small:
117
+
118
+ - **Clear context at natural boundaries** — a module's Phase A, a
119
+ landing page section, whatever this core's own unit boundary is — once
120
+ that unit is done and committed. Ask `hedgehog boundary` rather than
121
+ judging it: it exits 0 only when all three of nothing-in-flight, a
122
+ clean working tree, and a last closed task that completed its intent
123
+ hold, and names which one failed otherwise. Clear the conversation and
124
+ start fresh, then run `hedgehog status`/`hedgehog claim` and continue.
125
+ Nothing is lost, because the build graph, commits, and code hold all
126
+ the state. Prefer this over letting one session accumulate the entire
127
+ project.
128
+ - **`hedgehog quiesce` and `hedgehog boundary` answer different
129
+ questions.** `quiesce` reports whether anything is still in flight —
130
+ necessary before clearing (clearing while a lease is outstanding
131
+ orphans that lease until it expires), but not sufficient: a graph can
132
+ be perfectly settled halfway through an intent, with a dirty working
133
+ tree. `boundary` is the whole question — is this a moment to throw the
134
+ conversation away — and it includes the `quiesce` check as its first
135
+ condition. Use `quiesce` when you're waiting for dispatched work to
136
+ land (the Correction Protocol), `boundary` when you're deciding whether
137
+ to clear.
138
+ - **A cleared or new session recovers by running `hedgehog status` and
139
+ reading the commit log**, never by needing the prior conversation.
140
+ `hedgehog boundary --handoff` prints that recovery block directly —
141
+ where the build is, what's next and why, what's in flight, what's
142
+ blocked — derived from the graph, so no session hands a summary to the
143
+ next one.
144
+ - **Delegate heavy work to agents.** Scaffolding and every build step
145
+ run in their own isolated context, so work doesn't pile up in the
146
+ main thread. Planning intake's BMAD Phase 0 is the exception — the
147
+ project's own instructions file states it — and stays in that session
148
+ through Confirm & Lock; the mining, `bootstrap`, and per-module steps
149
+ after it delegate as usual.
150
+ - **Don't paste large context back in.** If you find yourself
151
+ re-explaining the architecture, stop — it's fixed and stated in the
152
+ project's core section, not something to reconstruct. If you need a
153
+ project specific, read it from the code. That's the self-documenting
154
+ design working as intended.
@@ -1,3 +1,4 @@
1
+ <!-- hedgehog:bootstrap-only start -->
1
2
  <!--
2
3
  Hedgehog project CLAUDE.md template.
3
4
 
@@ -11,6 +12,7 @@
11
12
 
12
13
  Delete this comment block after the placeholders are filled in.
13
14
  -->
15
+ <!-- hedgehog:bootstrap-only end -->
14
16
 
15
17
  # {{PROJECT_NAME}}
16
18
 
@@ -24,6 +26,7 @@ This project is built with **Hedgehog**: a one-step-at-a-time build
24
26
  discipline. The rules below aren't project preferences — they're how the
25
27
  build stays mechanically correct. Follow them exactly.
26
28
 
29
+ <!-- hedgehog:bootstrap-only start -->
27
30
  ## First message in a fresh install
28
31
 
29
32
  If `{{PROJECT_SUMMARY}}` above is still an unfilled placeholder, this is a
@@ -51,6 +54,7 @@ read the file and carry on.
51
54
  Don't re-explain the discipline or summarize this file; the greeting is
52
55
  one line, not a tour. Skip this entirely once the placeholder is filled
53
56
  in — every later session starts with `hedgehog status`, not a greeting.
57
+ <!-- hedgehog:bootstrap-only end -->
54
58
 
55
59
  ## How to work here
56
60
 
@@ -74,8 +78,8 @@ whole plan in context — the plan lives in the structure:
74
78
 
75
79
  Because state lives in those places and not in the conversation, a fresh
76
80
  context loses nothing: the architecture is known a priori, and the
77
- project's specifics are re-read on demand. Use that (see **Managing
78
- context** below).
81
+ project's specifics are re-read on demand. The `hedgehog-orchestrating`
82
+ skill (see **Running the build** below) is what uses that.
79
83
 
80
84
  **Use only the skills and agents this repo provides**, including its
81
85
  vendored BMAD shelf — never a general-purpose build-tool skill pack
@@ -97,154 +101,12 @@ taking the faster path.
97
101
 
98
102
  {{CORE_SECTION}}
99
103
 
100
- ## Consuming the graph
101
-
102
- `.hedgehog/hedgehog.db` is the source of truth for what's next — never
103
- re-derive build state from prose. To work from it:
104
-
105
- 1. Run `hedgehog claim --count N --owner <owner>`. It's atomic and
106
- lease-based, and returns up to N tasks (each with its own full
107
- STATUS/INTENT/RELEVANT RULES/INHERITED DEBT/INHERITED DECISIONS/WHY
108
- NOW/BLOCKED DOWNSTREAM/ALLOWED SCOPE/VERIFICATION packet) that the
109
- scheduler has already verified are safe to run together right now —
110
- scope and verify-radius disjoint. `--count` is a maximum, not a
111
- promise: a call may return fewer than N, or zero. `hedgehog ready` is
112
- a read-only preview of the claimable/held-back split (and why a task
113
- is held back — conflict with another claimable task, or exclusivity)
114
- before you claim.
115
- 2. Delegate each claimed packet to this core's loop skill (named in the
116
- section above), one dispatch per packet, running concurrently.
117
- 3. As each agent reports its packet done, run `hedgehog verify
118
- <task-id> --owner <owner>` **serially** — one at a time, even though
119
- the building happened concurrently, because verify writes git commits
120
- and those must land one at a time. It checks the touched files
121
- against the packet's ALLOWED SCOPE, runs the verification command,
122
- and on a pass writes the commit and unlocks whatever the task was
123
- blocking. An agent reporting success never moves the task — only a
124
- passing `hedgehog verify` exit code does.
125
-
126
- `hedgehog claim` hands out only tasks safe to run together. Never run
127
- two tasks it didn't hand you together.
128
-
129
- The packet's **INTENT** block names the goal and outcome of the *whole*
130
- intent, not just this layer. A layer's own verify command runs the tests
131
- that layer wrote, so it measures internal consistency and never coverage
132
- of what was asked — a layer that builds half the intent and tests that
133
- half exhaustively is green. Build the layer's share of the goal, and say
134
- so when the packet doesn't account for something the goal asks for.
135
- When `hedgehog verify` closes the **last** layer of an intent it prints
136
- that goal and outcome back as an **INTENT CHECK**: read the built work
137
- against it there, because nothing else in the build does.
138
-
139
- A layer that discovers a limitation the next layer has to compensate for
140
- records it with `hedgehog debt add <task-id> "<note>"` — it lands in the
141
- **INHERITED DEBT** section of every packet that depends on that task. A
142
- comment in a source file is not a mechanism; nothing reads it.
143
-
144
- A layer that makes a choice a dependent layer needs to know about — a
145
- pattern, a library, a trade-off, anything the next task should follow
146
- rather than reinvent or contradict — records it with `hedgehog decision
147
- add <task-id> "<note>"`, landing in the **INHERITED DECISIONS** section
148
- the same way. Debt is what's still wrong with a task; a decision is why
149
- it was built the way it was.
150
-
151
- `planner` owns writing intents (`hedgehog intent add`) at planning
152
- intake; `hedgehog plan` compiles them into the task graph the loop
153
- consumes. Nothing checks a box — there is no checklist, only queryable
154
- state.
155
-
156
- **When the build is done:** once `hedgehog status` shows every task
157
- `complete` and `hedgehog boundary` exits 0 (see **Managing context**
158
- below — it checks nothing-in-flight, a clean tree, and a closed intent
159
- together), the build session is complete. The permanent record is the committed
160
- intents (`.hedgehog/intents/*.json`), the friction log
161
- (`.hedgehog/friction/*.md`), the core definition (root `core.yaml` for a
162
- shipped core, `.hedgehog/core.yaml` for an authored one), and the git
163
- commit history itself — not the database. `.hedgehog/hedgehog.db` is gitignored: a
164
- derived index, rebuildable at any time via `hedgehog db rebuild`, which
165
- replays those committed sources against git history. That rebuild also
166
- runs automatically on a fresh clone when the DB is missing but
167
- `.hedgehog/intents/` exists. That's what makes every later session
168
- cheap.
169
-
170
- A completed build is **extendable, not sealed**. Offer the user a
171
- fresh-context handoff, and name both ways forward:
172
-
173
- - **Adjustments to what's built** → the `tweaker` agent, from a *new*
174
- chat window, not a subagent call inside this one — this session's
175
- context has been building the whole project and is exactly what
176
- "clearing context now costs nothing" (above) means to discard. Tell
177
- the user plainly: close this chat window and open a new one, then
178
- paste this to start it:
179
-
180
- > The build for {{PROJECT_NAME}} is complete. Use the tweaker agent:
181
- > first review the friction log and ask me for feedback on the build,
182
- > then take my tweak requests one at a time.
183
-
184
- In the new window, `tweaker` starts clean, once reviews the friction
185
- log (`hedgehog friction list`) for possible discipline-improvement
186
- issues and separately asks the user directly for feedback on the
187
- build, filing each real pattern or piece of feedback as its own GitHub
188
- issue against the Hedgehog repo itself, never this project's repo
189
- (friction as `bug`/`help wanted`, feedback as `suggestion`, each only
190
- after showing the exact content and getting explicit approval), then
191
- takes any tweak requests one at a time.
192
- - **New scope** — a new module or feature, anything beyond adjusting what
193
- exists → on a core with a module axis, the `planner` agent, which runs
194
- `hedgehog-planning-intake`'s **Re-entry pass**. It reads the existing
195
- planning archive as context and elicits only what's new, then adds
196
- intents and runs `hedgehog plan`. This is append-only: `plan` skips
197
- intents already compiled, so every `complete` task keeps its status and
198
- its commits, and `hedgehog claim` resumes at the first tasks of the new
199
- work. Planning is not re-run from scratch, and the workspace is not
200
- re-scaffolded. (This core's own section above states where new scope
201
- goes if this core has no module axis to add an intent to.)
202
-
203
- If a request turns out to be structural rather than either of those —
204
- something already built is wrong at its source — that's the Correction
205
- Protocol's post-build entry, in this core's own loop skill.
206
-
207
- ## Managing context
208
-
209
- Hedgehog is designed so the conversation is disposable. Keep the working
210
- context small:
211
-
212
- - **Clear context at natural boundaries** — a module's Phase A, a
213
- landing page section, whatever this core's own unit boundary is — once
214
- that unit is done and committed. Ask `hedgehog boundary` rather than
215
- judging it: it exits 0 only when all three of nothing-in-flight, a
216
- clean working tree, and a last closed task that completed its intent
217
- hold, and names which one failed otherwise. Clear the conversation and
218
- start fresh, then run `hedgehog status`/`hedgehog claim` and continue.
219
- Nothing is lost, because the build graph, commits, and code hold all
220
- the state. Prefer this over letting one session accumulate the entire
221
- project.
222
- - **`hedgehog quiesce` and `hedgehog boundary` answer different
223
- questions.** `quiesce` reports whether anything is still in flight —
224
- necessary before clearing (clearing while a lease is outstanding
225
- orphans that lease until it expires), but not sufficient: a graph can
226
- be perfectly settled halfway through an intent, with a dirty working
227
- tree. `boundary` is the whole question — is this a moment to throw the
228
- conversation away — and it includes the `quiesce` check as its first
229
- condition. Use `quiesce` when you're waiting for dispatched work to
230
- land (the Correction Protocol), `boundary` when you're deciding whether
231
- to clear.
232
- - **A cleared or new session recovers by running `hedgehog status` and
233
- reading the commit log**, never by needing the prior conversation.
234
- `hedgehog boundary --handoff` prints that recovery block directly —
235
- where the build is, what's next and why, what's in flight, what's
236
- blocked — derived from the graph, so no session hands a summary to the
237
- next one.
238
- - **Delegate heavy work to agents.** Scaffolding and every build step
239
- run in their own isolated context, so work doesn't pile up in the
240
- main thread. Planning intake's BMAD Phase 0 is the exception — see
241
- **First message in a fresh install** above — and stays here through
242
- Confirm & Lock; the mining, `bootstrap`, and per-module steps after it
243
- delegate as usual.
244
- - **Don't paste large context back in.** If you find yourself
245
- re-explaining the architecture, stop — it's fixed and stated in this
246
- file's core section, not something to reconstruct. If you need a
247
- project specific, read it from the code. That's the self-documenting
248
- design working as intended.
104
+ ## Running the build
105
+
106
+ The build graph is the source of truth for what's next — never re-derive
107
+ build state from prose. The `hedgehog-orchestrating` skill owns the claim →
108
+ dispatch → verify cycle, the intent check, debt and decision recording, the
109
+ context boundaries to clear at, and the post-build handoff. Read it at the
110
+ start of every session and follow it.
249
111
 
250
112
  {{HOST_DISPATCH}}