@skyf0xx/hedgehog 5.4.2 → 6.0.2
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 +37 -10
- package/README.zh-CN.md +35 -7
- package/bin/cli.mjs +604 -10
- package/package.json +3 -2
- package/src/agents/bootstrap.md +10 -5
- package/src/agents/planner.md +19 -9
- package/src/agents/tweaker.md +10 -2
- package/src/db/code-intelligence-requires.mjs +296 -0
- package/src/db/code-intelligence.mjs +291 -0
- package/src/db/community.mjs +26 -1
- package/src/db/core.mjs +3 -2
- package/src/db/drift.mjs +15 -0
- package/src/db/friction.mjs +75 -0
- package/src/db/gate.mjs +1 -1
- package/src/db/next.mjs +39 -2
- package/src/db/plan.mjs +60 -4
- package/src/db/rebuild.mjs +7 -0
- package/src/db/schema.mjs +6 -1
- package/src/db/status.mjs +108 -9
- package/src/hosts/gemini/gemini-extension.json +1 -1
- package/src/hosts/version.mjs +6 -3
- package/src/registry/fetch.mjs +43 -10
- package/src/registry/manifest.mjs +23 -8
- package/src/skills/hedgehog-code-intelligence-setup/SKILL.md +586 -0
package/src/db/status.mjs
CHANGED
|
@@ -11,9 +11,10 @@
|
|
|
11
11
|
// every task meeting that condition, not just the one `hedgehog next`
|
|
12
12
|
// would pick.
|
|
13
13
|
|
|
14
|
+
import { radiusGaps } from './code-intelligence.mjs';
|
|
14
15
|
import { listDebt } from './debt.mjs';
|
|
15
16
|
import { detectDrift, formatDrift } from './drift.mjs';
|
|
16
|
-
import { listFriction } from './friction.mjs';
|
|
17
|
+
import { frictionByModule, listFriction } from './friction.mjs';
|
|
17
18
|
import { orphanedOverrides } from './overrides.mjs';
|
|
18
19
|
import { formatMissingRequirements } from './requires.mjs';
|
|
19
20
|
import { readyTasks, heldBackReason } from './ready.mjs';
|
|
@@ -102,6 +103,33 @@ function loadDebtByTask(db) {
|
|
|
102
103
|
.sort((a, b) => a.taskId.localeCompare(b.taskId));
|
|
103
104
|
}
|
|
104
105
|
|
|
106
|
+
// Advisory radius suggestions: for every task whose context was resolved
|
|
107
|
+
// against a code-intelligence index (context_files non-NULL), the files
|
|
108
|
+
// its computed blast radius reaches that no verify_radius/scope_globs
|
|
109
|
+
// glob covers (code-intelligence.mjs#radiusGaps). Skips every task with
|
|
110
|
+
// no resolved context outright, which is what a task planned without a
|
|
111
|
+
// working index leaves behind — the same NULL a broken or removed CGC
|
|
112
|
+
// install produces, not a state `status` distinguishes from an
|
|
113
|
+
// intentionally unindexed run.
|
|
114
|
+
//
|
|
115
|
+
// Wrapped the way countFriction is: `status` reads through a read-only
|
|
116
|
+
// handle, which never migrates, so on a graph predating the context
|
|
117
|
+
// columns this SELECT throws on a column that isn't there yet. An
|
|
118
|
+
// unmigrated graph reads as no suggestions, not as a failed `status`.
|
|
119
|
+
function loadRadiusSuggestions(db) {
|
|
120
|
+
try {
|
|
121
|
+
const tasks = db.prepare('SELECT * FROM tasks WHERE context_files IS NOT NULL').all();
|
|
122
|
+
const suggestions = [];
|
|
123
|
+
for (const task of tasks) {
|
|
124
|
+
const gaps = radiusGaps(task);
|
|
125
|
+
if (gaps.length > 0) suggestions.push({ taskId: task.id, files: gaps });
|
|
126
|
+
}
|
|
127
|
+
return suggestions;
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
105
133
|
// The friction row count, or 0. `listFriction` reads the table
|
|
106
134
|
// unguarded, so a build graph predating it throws here where `listDebt`
|
|
107
135
|
// would return [] — caught rather than propagated for the same reason
|
|
@@ -117,11 +145,13 @@ function countFriction(db) {
|
|
|
117
145
|
}
|
|
118
146
|
|
|
119
147
|
// Returns { counts, ready, heldBack, inFlight, attention, drift,
|
|
120
|
-
// orphanedOverrides, debt, frictionCount,
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
148
|
+
// orphanedOverrides, debt, frictionCount, frictionHotspots,
|
|
149
|
+
// radiusSuggestions, total } —
|
|
150
|
+
// counts keyed by every status in the tasks CHECK constraint (present
|
|
151
|
+
// even at zero), ready the full list of currently-pickable tasks,
|
|
152
|
+
// heldBack the subset of those that `hedgehog claim` would skip over
|
|
153
|
+
// right now because they conflict with in-flight work or another ready
|
|
154
|
+
// task ahead of them
|
|
125
155
|
// (ready.mjs's own simulation, reused rather than reimplemented —
|
|
126
156
|
// without this a task can sit in READY indefinitely with no visible
|
|
127
157
|
// reason, the same invisible-stall shape blocked tasks had before
|
|
@@ -159,6 +189,20 @@ function countFriction(db) {
|
|
|
159
189
|
// only the existence signal: `debt list` needs a task id the operator
|
|
160
190
|
// has no way to guess, and `friction list` needs the operator to
|
|
161
191
|
// already suspect there is something to read.
|
|
192
|
+
//
|
|
193
|
+
// `frictionHotspots` (friction.mjs#frictionByModule) is the one reading
|
|
194
|
+
// of friction that isn't a count: the files the friction-carrying tasks
|
|
195
|
+
// reach, ranked. It rides along here because it is a read of `friction`
|
|
196
|
+
// joined to `tasks` and needs nothing else passed in. On a project whose
|
|
197
|
+
// tasks carry no resolved context it is an empty hotspot list and a full
|
|
198
|
+
// `uncorrelated` count, which is the same signal `frictionCount` alone
|
|
199
|
+
// already gave.
|
|
200
|
+
//
|
|
201
|
+
// `radiusSuggestions` (code-intelligence.mjs#radiusGaps) is advisory,
|
|
202
|
+
// never enforcement: files a task's computed blast radius reaches that
|
|
203
|
+
// its declared verify_radius/scope_globs doesn't cover. Computed only
|
|
204
|
+
// for tasks with a resolved context_files, same gate as the hotspots
|
|
205
|
+
// read — a project with no index attached contributes nothing here.
|
|
162
206
|
export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
163
207
|
const counts = countTasksByStatus(db);
|
|
164
208
|
const ready = loadReadyTasks(db);
|
|
@@ -169,6 +213,8 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
|
169
213
|
const orphaned = orphanedOverrides(db, overrides);
|
|
170
214
|
const debt = loadDebtByTask(db);
|
|
171
215
|
const frictionCount = countFriction(db);
|
|
216
|
+
const frictionHotspots = frictionByModule(db);
|
|
217
|
+
const radiusSuggestions = loadRadiusSuggestions(db);
|
|
172
218
|
const total = Object.values(counts).reduce((a, b) => a + b, 0);
|
|
173
219
|
return {
|
|
174
220
|
counts,
|
|
@@ -180,6 +226,8 @@ export function graphStatus(db, { core = null, overrides = new Map() } = {}) {
|
|
|
180
226
|
orphanedOverrides: orphaned,
|
|
181
227
|
debt,
|
|
182
228
|
frictionCount,
|
|
229
|
+
frictionHotspots,
|
|
230
|
+
radiusSuggestions,
|
|
183
231
|
total,
|
|
184
232
|
};
|
|
185
233
|
}
|
|
@@ -213,8 +261,15 @@ export function formatStatus({
|
|
|
213
261
|
orphanedOverrides = [],
|
|
214
262
|
debt = [],
|
|
215
263
|
frictionCount = 0,
|
|
264
|
+
frictionHotspots = { hotspots: [], uncorrelated: 0 },
|
|
265
|
+
radiusSuggestions = [],
|
|
216
266
|
total,
|
|
217
267
|
missingRequirements,
|
|
268
|
+
// Rendered lines from formatIndexStaleness, or [] when the index is
|
|
269
|
+
// fresh or absent. Passed in rather than computed: this module reads
|
|
270
|
+
// the graph, and index provenance lives on disk and in git, which is
|
|
271
|
+
// the CLI's side of the line.
|
|
272
|
+
indexStaleness = [],
|
|
218
273
|
}) {
|
|
219
274
|
const lines = [];
|
|
220
275
|
lines.push(`TASKS ${total}`);
|
|
@@ -266,9 +321,9 @@ export function formatStatus({
|
|
|
266
321
|
lines.push(' Fix the work, then: hedgehog retry <task-id> && hedgehog claim <task-id> --owner <owner>');
|
|
267
322
|
}
|
|
268
323
|
|
|
269
|
-
//
|
|
270
|
-
// than of any one task, and because an operator reading top-down
|
|
271
|
-
// reach them after knowing what's ready and what's stuck.
|
|
324
|
+
// These three, because each is a condition of the graph as a whole
|
|
325
|
+
// rather than of any one task, and because an operator reading top-down
|
|
326
|
+
// should reach them after knowing what's ready and what's stuck.
|
|
272
327
|
if (drift && drift.length > 0) {
|
|
273
328
|
lines.push('');
|
|
274
329
|
lines.push(formatDrift(drift));
|
|
@@ -290,6 +345,35 @@ export function formatStatus({
|
|
|
290
345
|
lines.push(' Each widens nothing until the id matches a real task. See: hedgehog override list');
|
|
291
346
|
}
|
|
292
347
|
|
|
348
|
+
// Advisory, never a defect count: verify_radius/scope_globs is what
|
|
349
|
+
// actually gates `hedgehog verify`, and stays the sole authority on
|
|
350
|
+
// that whether or not this prints. This only names what a
|
|
351
|
+
// code-intelligence index thinks a task's blast radius reaches beyond
|
|
352
|
+
// its declared radius — a widening the operator may or may not want.
|
|
353
|
+
if (radiusSuggestions.length > 0) {
|
|
354
|
+
lines.push('');
|
|
355
|
+
lines.push('RADIUS SUGGESTIONS');
|
|
356
|
+
for (const { taskId, files } of radiusSuggestions) {
|
|
357
|
+
lines.push(` ${taskId} verify_radius may be missing:`);
|
|
358
|
+
for (const file of files) lines.push(` ${file}`);
|
|
359
|
+
}
|
|
360
|
+
lines.push('');
|
|
361
|
+
lines.push(
|
|
362
|
+
indexStaleness.length > 0
|
|
363
|
+
? ' Sourced from a code-intelligence index that is out of date (below). Widen with: hedgehog plan --recompile'
|
|
364
|
+
: ' Sourced from a code-intelligence index. Widen with: hedgehog plan --recompile',
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// Below the suggestions it qualifies, and printed whether or not any
|
|
369
|
+
// were raised: an index that has drifted from HEAD also under-reports,
|
|
370
|
+
// so an empty RADIUS SUGGESTIONS section is exactly when a reader most
|
|
371
|
+
// needs to know the index is stale rather than the code clean.
|
|
372
|
+
if (indexStaleness.length > 0) {
|
|
373
|
+
lines.push('');
|
|
374
|
+
lines.push(...indexStaleness);
|
|
375
|
+
}
|
|
376
|
+
|
|
293
377
|
// Bottom, below every condition above, and two sections rather than
|
|
294
378
|
// one. Everything above is something wrong — work stalled, a graph
|
|
295
379
|
// that disagrees with its core, an override doing nothing. These two
|
|
@@ -320,6 +404,21 @@ export function formatStatus({
|
|
|
320
404
|
if (frictionCount > 0) {
|
|
321
405
|
lines.push('');
|
|
322
406
|
lines.push(`FRICTION LOGGED ${frictionCount}`);
|
|
407
|
+
// The top three only: this is a lead for tweaker's grouping pass, not
|
|
408
|
+
// a listing, and a ranked tail nobody acts on costs the whole block
|
|
409
|
+
// its readability. The uncorrelated count prints whenever it's
|
|
410
|
+
// non-zero — including with no hotspots at all, which is how a
|
|
411
|
+
// project whose tasks carry no resolved context reads.
|
|
412
|
+
const { hotspots = [], uncorrelated = 0 } = frictionHotspots ?? {};
|
|
413
|
+
if (hotspots.length > 0 || uncorrelated > 0) {
|
|
414
|
+
lines.push('');
|
|
415
|
+
for (const { path, frictionCount: n } of hotspots.slice(0, 3)) {
|
|
416
|
+
lines.push(` ${path} ${n} note${n === 1 ? '' : 's'}`);
|
|
417
|
+
}
|
|
418
|
+
if (uncorrelated > 0) {
|
|
419
|
+
lines.push(` ${uncorrelated} note${uncorrelated === 1 ? '' : 's'} not correlated to any file`);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
323
422
|
lines.push('');
|
|
324
423
|
lines.push(' Reviewed as a batch at the end of a build. See: hedgehog friction list');
|
|
325
424
|
}
|
package/src/hosts/version.mjs
CHANGED
|
@@ -101,16 +101,19 @@ export async function latestVersion(root, { force = false } = {}) {
|
|
|
101
101
|
if (!latest) return null;
|
|
102
102
|
|
|
103
103
|
// Cache alongside the install stamp rather than in a separate file, so
|
|
104
|
-
// one read answers both halves of the question.
|
|
104
|
+
// one read answers both halves of the question. Written only into a
|
|
105
|
+
// `.hedgehog/` that already exists — a version lookup is a read, and
|
|
106
|
+
// creating the directory here would leave one behind in a project that
|
|
107
|
+
// never completed `init`, which `init`'s own preconditions rely on
|
|
108
|
+
// being absent when they abort.
|
|
105
109
|
const path = join(root, VERSION_PATH);
|
|
106
110
|
try {
|
|
107
|
-
await mkdir(dirname(path), { recursive: true });
|
|
108
111
|
await writeFile(
|
|
109
112
|
path,
|
|
110
113
|
`${JSON.stringify({ ...cached, latest, checkedAt: new Date().toISOString() }, null, 2)}\n`,
|
|
111
114
|
);
|
|
112
115
|
} catch {
|
|
113
|
-
// A read-only or
|
|
116
|
+
// A read-only or absent .hedgehog just means no caching; the
|
|
114
117
|
// answer we fetched is still good for this call.
|
|
115
118
|
}
|
|
116
119
|
return latest;
|
package/src/registry/fetch.mjs
CHANGED
|
@@ -61,9 +61,10 @@ function packSpec(entry) {
|
|
|
61
61
|
* Resolve, extract, and read one core package.
|
|
62
62
|
*
|
|
63
63
|
* @param {object} entry - a cores.json entry (`name`, `package`, `version`)
|
|
64
|
-
* @returns {Promise<{manifest: object, root: string, version: string
|
|
65
|
-
* the parsed hedgehog-core.yaml, the
|
|
66
|
-
*
|
|
64
|
+
* @returns {Promise<{manifest: object, root: string, version: string,
|
|
65
|
+
* engineAdvisory: string|null}>} the parsed hedgehog-core.yaml, the
|
|
66
|
+
* directory the package extracted to, the exact version resolved, and a
|
|
67
|
+
* line to print when the CLI is ahead of the core's engine line.
|
|
67
68
|
*/
|
|
68
69
|
export async function fetchCore(entry) {
|
|
69
70
|
const staged = await packAndExtract(entry);
|
|
@@ -74,9 +75,9 @@ export async function fetchCore(entry) {
|
|
|
74
75
|
// `cores list` reports as reusable and what an offline `update` reads
|
|
75
76
|
// back, so only a core this engine can actually install belongs in it —
|
|
76
77
|
// a manifest that fails here throws before anything is cached.
|
|
77
|
-
let
|
|
78
|
+
let read;
|
|
78
79
|
try {
|
|
79
|
-
|
|
80
|
+
read = await readManifest(staged.root, entry);
|
|
80
81
|
} catch (err) {
|
|
81
82
|
await rm(staged.tmp, { recursive: true, force: true });
|
|
82
83
|
throw err;
|
|
@@ -86,13 +87,13 @@ export async function fetchCore(entry) {
|
|
|
86
87
|
// and now; its copy is as good as this one, so keep it and drop ours.
|
|
87
88
|
if (await exists(cached)) {
|
|
88
89
|
await rm(staged.tmp, { recursive: true, force: true });
|
|
89
|
-
return {
|
|
90
|
+
return { ...read, root: cached, version };
|
|
90
91
|
}
|
|
91
92
|
|
|
92
93
|
await mkdir(join(CORE_CACHE_ROOT, entry.name), { recursive: true });
|
|
93
94
|
await rename(staged.root, cached);
|
|
94
95
|
await rm(staged.tmp, { recursive: true, force: true });
|
|
95
|
-
return {
|
|
96
|
+
return { ...read, root: cached, version };
|
|
96
97
|
}
|
|
97
98
|
|
|
98
99
|
/**
|
|
@@ -104,7 +105,7 @@ export async function fetchCore(entry) {
|
|
|
104
105
|
export async function cachedCore(entry, version) {
|
|
105
106
|
const root = join(CORE_CACHE_ROOT, entry.name, version);
|
|
106
107
|
if (!(await exists(join(root, MANIFEST_NAME)))) return null;
|
|
107
|
-
return {
|
|
108
|
+
return { ...(await readManifest(root, entry)), root, version };
|
|
108
109
|
}
|
|
109
110
|
|
|
110
111
|
async function packAndExtract(entry) {
|
|
@@ -159,8 +160,12 @@ async function readManifest(root, entry) {
|
|
|
159
160
|
`The package and the registry entry have to agree on the core's name.`,
|
|
160
161
|
);
|
|
161
162
|
}
|
|
162
|
-
assertEngineSatisfies(
|
|
163
|
-
|
|
163
|
+
const engineAdvisory = assertEngineSatisfies(
|
|
164
|
+
manifest,
|
|
165
|
+
ENGINE_VERSION,
|
|
166
|
+
`${entry.package}/${MANIFEST_NAME}`,
|
|
167
|
+
);
|
|
168
|
+
return { manifest, engineAdvisory };
|
|
164
169
|
}
|
|
165
170
|
|
|
166
171
|
// npm's own failure text is a wall of registry detail; what a user can act
|
|
@@ -178,6 +183,34 @@ function unresolvableMessage(entry, spec, err) {
|
|
|
178
183
|
);
|
|
179
184
|
}
|
|
180
185
|
|
|
186
|
+
/**
|
|
187
|
+
* The engine line the newest cached extraction of a core declares, or
|
|
188
|
+
* null when that core has never been fetched. `cores list` reports it so
|
|
189
|
+
* the engine line each core is written against is visible at the point a
|
|
190
|
+
* core is chosen, rather than only once an install has run.
|
|
191
|
+
*/
|
|
192
|
+
export async function cachedEngine(name, versions) {
|
|
193
|
+
const newest = [...versions].sort(compareVersions).pop();
|
|
194
|
+
if (!newest) return null;
|
|
195
|
+
try {
|
|
196
|
+
const text = await readFile(join(CORE_CACHE_ROOT, name, newest, MANIFEST_NAME), 'utf8');
|
|
197
|
+
return parseCoreManifest(text, `${name}/${MANIFEST_NAME}`).engine ?? null;
|
|
198
|
+
} catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Orders `1.0.5` after `1.0.0` rather than before it, which a plain
|
|
204
|
+
// string sort would not.
|
|
205
|
+
function compareVersions(a, b) {
|
|
206
|
+
const parts = (v) => String(v).split('.').map(Number);
|
|
207
|
+
const [x, y] = [parts(a), parts(b)];
|
|
208
|
+
for (let i = 0; i < 3; i += 1) {
|
|
209
|
+
if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) - (y[i] ?? 0);
|
|
210
|
+
}
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
181
214
|
/**
|
|
182
215
|
* List which versions of a core are already extracted in the cache.
|
|
183
216
|
* Reported by `hedgehog cores list` so a user can see what an install
|
|
@@ -137,17 +137,22 @@ export function parseCoreManifest(text, source = 'hedgehog-core.yaml') {
|
|
|
137
137
|
/**
|
|
138
138
|
* Check a core's declared `engine` range against the CLI installing it.
|
|
139
139
|
*
|
|
140
|
-
* A core states which engine line it installs against
|
|
140
|
+
* A core states which engine line it installs against. An engine *older*
|
|
141
141
|
* than that line writes a payload the core's own skills expect features
|
|
142
|
-
* from, so the install stops here rather than part-way through.
|
|
142
|
+
* from, so the install stops here rather than part-way through. An engine
|
|
143
|
+
* *newer* than that line is a different situation: the CLI carries every
|
|
144
|
+
* feature the core asks for, plus more, so the install proceeds and the
|
|
145
|
+
* skew is reported back to the caller as an advisory instead.
|
|
143
146
|
*
|
|
144
147
|
* @param {object} manifest - a parsed manifest, carrying `engine`
|
|
145
148
|
* @param {string} engineVersion - the CLI's own package.json `version`
|
|
146
149
|
* @param {string} source - what to name in the error
|
|
150
|
+
* @returns {string|null} an advisory line when the CLI is ahead of the
|
|
151
|
+
* core's line, `null` when the two agree
|
|
147
152
|
*/
|
|
148
153
|
export function assertEngineSatisfies(manifest, engineVersion, source = 'hedgehog-core.yaml') {
|
|
149
154
|
const required = manifest.engine;
|
|
150
|
-
if (required == null) return;
|
|
155
|
+
if (required == null) return null;
|
|
151
156
|
|
|
152
157
|
const range = ENGINE_RANGE.exec(String(required).trim());
|
|
153
158
|
if (!range) {
|
|
@@ -166,12 +171,12 @@ export function assertEngineSatisfies(manifest, engineVersion, source = 'hedgeho
|
|
|
166
171
|
const [, wantMajor, wantMinor, wantPatch] = range.map(Number);
|
|
167
172
|
const [, haveMajor, haveMinor, havePatch] = installed.map(Number);
|
|
168
173
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
haveMajor === wantMajor &&
|
|
172
|
-
|
|
174
|
+
const behind =
|
|
175
|
+
haveMajor < wantMajor ||
|
|
176
|
+
(haveMajor === wantMajor &&
|
|
177
|
+
(haveMinor < wantMinor || (haveMinor === wantMinor && havePatch < wantPatch)));
|
|
173
178
|
|
|
174
|
-
if (
|
|
179
|
+
if (behind) {
|
|
175
180
|
throw new Error(
|
|
176
181
|
`This core needs Hedgehog ${required}, and the CLI running is ${engineVersion}.\n\n` +
|
|
177
182
|
`The core's agents and skills are written against that engine line, so installing\n` +
|
|
@@ -180,4 +185,14 @@ export function assertEngineSatisfies(manifest, engineVersion, source = 'hedgeho
|
|
|
180
185
|
` npx @skyf0xx/hedgehog@latest init\n`,
|
|
181
186
|
);
|
|
182
187
|
}
|
|
188
|
+
|
|
189
|
+
if (haveMajor > wantMajor) {
|
|
190
|
+
return (
|
|
191
|
+
`This core is written against Hedgehog ${required}, and the CLI running is ${engineVersion}.\n` +
|
|
192
|
+
`Installing it anyway: a newer engine carries everything the core asks for. If the\n` +
|
|
193
|
+
`core's own skills behave oddly, that skew is the first thing to suspect.`
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return null;
|
|
183
198
|
}
|