@skyf0xx/hedgehog 5.3.3 → 5.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/cli.mjs +11 -5
- package/package.json +1 -1
- package/src/db/core.mjs +11 -1
- package/src/db/next.mjs +25 -15
- package/src/hosts/gemini/gemini-extension.json +1 -1
package/bin/cli.mjs
CHANGED
|
@@ -1296,11 +1296,15 @@ async function intentCommand(args) {
|
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
1298
|
// On a module-axis core the intent id becomes {module} in every layer's
|
|
1299
|
-
// scope glob and verify command, and
|
|
1300
|
-
//
|
|
1301
|
-
// scoped to a directory the generator will never write, and
|
|
1302
|
-
// downstream catches it: plan compiles it, claim hands out a
|
|
1303
|
-
// own scaffold command contradicts its ALLOWED SCOPE.
|
|
1299
|
+
// scope glob and verify command, and on a core whose generator pluralizes
|
|
1300
|
+
// it (core.yaml's `pluralizes`, default true), a singular id compiles a
|
|
1301
|
+
// whole graph scoped to a directory the generator will never write, and
|
|
1302
|
+
// nothing downstream catches it: plan compiles it, claim hands out a
|
|
1303
|
+
// packet whose own scaffold command contradicts its ALLOWED SCOPE. A core
|
|
1304
|
+
// that declares `pluralizes: false` never has this failure mode, so
|
|
1305
|
+
// warnSingularModuleId and warnSingularModuleIdsAtPlan both skip it —
|
|
1306
|
+
// see core.pluralizes in src/db/core.mjs for why that lives on the core
|
|
1307
|
+
// rather than as a hardcoded exception here.
|
|
1304
1308
|
//
|
|
1305
1309
|
// A warning rather than a refusal: plenty of real modules are singular
|
|
1306
1310
|
// (`billing`, `search`), so the convention cannot be enforced without
|
|
@@ -1332,6 +1336,7 @@ async function warnSingularModuleId(intent) {
|
|
|
1332
1336
|
return;
|
|
1333
1337
|
}
|
|
1334
1338
|
if (!isModuleAxis(core)) return;
|
|
1339
|
+
if (core.pluralizes === false) return;
|
|
1335
1340
|
if (!looksSingular(intent.id)) return;
|
|
1336
1341
|
|
|
1337
1342
|
console.log(
|
|
@@ -1360,6 +1365,7 @@ async function warnSingularModuleId(intent) {
|
|
|
1360
1365
|
// recovery is still per-id, so those lines are listed one per name.
|
|
1361
1366
|
function warnSingularModuleIdsAtPlan(core, db) {
|
|
1362
1367
|
if (!isModuleAxis(core)) return;
|
|
1368
|
+
if (core.pluralizes === false) return;
|
|
1363
1369
|
|
|
1364
1370
|
// Every intent whose tasks have all yet to start, not just the ones
|
|
1365
1371
|
// this run compiled. `db rebuild` replays the committed intent files
|
package/package.json
CHANGED
package/src/db/core.mjs
CHANGED
|
@@ -182,6 +182,7 @@ function indentOf(line) {
|
|
|
182
182
|
|
|
183
183
|
// Parses the narrow subset of YAML a core definition needs:
|
|
184
184
|
// id: <scalar>
|
|
185
|
+
// pluralizes: <bool> # optional, default true
|
|
185
186
|
// layers:
|
|
186
187
|
// - id: <scalar>
|
|
187
188
|
// depends_on: <scalar> # optional
|
|
@@ -201,7 +202,7 @@ export function parseCoreYaml(text) {
|
|
|
201
202
|
lines.push({ indent: indentOf(noComment), text: noComment.trim() });
|
|
202
203
|
}
|
|
203
204
|
|
|
204
|
-
const core = { id: undefined, layers: [] };
|
|
205
|
+
const core = { id: undefined, pluralizes: true, layers: [] };
|
|
205
206
|
let i = 0;
|
|
206
207
|
|
|
207
208
|
while (i < lines.length && lines[i].indent === 0) {
|
|
@@ -214,6 +215,15 @@ export function parseCoreYaml(text) {
|
|
|
214
215
|
if (!match) throw new Error(`unparseable line: ${line.text}`);
|
|
215
216
|
const [, key, value] = match;
|
|
216
217
|
if (key === 'id') core.id = parseScalar(value);
|
|
218
|
+
// Whether this core's own generator takes a module id plural — a
|
|
219
|
+
// fixed, known fact about that generator, not a per-project unknown.
|
|
220
|
+
// Absent means true, so every core written before this field existed
|
|
221
|
+
// keeps warning exactly as it always has; a core whose generator
|
|
222
|
+
// never pluralizes (deepseek-harness's tool generator uses the id
|
|
223
|
+
// verbatim) declares `pluralizes: false` once and the singular-id
|
|
224
|
+
// advisory stops firing on it for good, rather than every user of
|
|
225
|
+
// that core re-discovering the same false positive.
|
|
226
|
+
if (key === 'pluralizes') core.pluralizes = parseScalar(value) === 'true';
|
|
217
227
|
i++;
|
|
218
228
|
}
|
|
219
229
|
|
package/src/db/next.mjs
CHANGED
|
@@ -375,10 +375,19 @@ export function scopePackageRoot(glob, module) {
|
|
|
375
375
|
const segments = glob.replace('{module}', module).split('/');
|
|
376
376
|
const wildcard = segments.findIndex((s) => s.includes('*'));
|
|
377
377
|
// No wildcard means the glob names one literal file, not a directory
|
|
378
|
-
// tree
|
|
379
|
-
//
|
|
380
|
-
//
|
|
381
|
-
|
|
378
|
+
// tree. Most such files are a single prompt/config file (e.g.
|
|
379
|
+
// `.hedgehog/dsh-smoke/{module}.md`) whose parent directory never holds
|
|
380
|
+
// a `package.json` of its own, so it is never a package root a
|
|
381
|
+
// generator could be first into — except when the literal file IS
|
|
382
|
+
// `package.json`: a layer whose own scope names that file directly
|
|
383
|
+
// (deepseek-harness's `bundle`: `plugins/{module}/package.json`) is
|
|
384
|
+
// naming its package root exactly as surely as a wildcard glob under
|
|
385
|
+
// that root would, and its parent directory is that root.
|
|
386
|
+
if (wildcard === -1) {
|
|
387
|
+
if (segments.at(-1) !== 'package.json') return null;
|
|
388
|
+
const literal = segments.slice(0, -1);
|
|
389
|
+
return literal.length >= 2 ? literal.join('/') : null;
|
|
390
|
+
}
|
|
382
391
|
const literal = segments.slice(0, wildcard);
|
|
383
392
|
const src = literal.indexOf('src');
|
|
384
393
|
const root = src === -1 ? literal : literal.slice(0, src);
|
|
@@ -389,12 +398,13 @@ export function scopePackageRoot(glob, module) {
|
|
|
389
398
|
|
|
390
399
|
// A task is the first arrival in its package when the package the scope
|
|
391
400
|
// points into has no package.json on disk yet: the generator that
|
|
392
|
-
// scaffolds this layer will create the package shell (package.json
|
|
393
|
-
//
|
|
394
|
-
// own files, and every one
|
|
395
|
-
//
|
|
396
|
-
//
|
|
397
|
-
// recovery becomes a
|
|
401
|
+
// scaffolds this layer will create the package shell (package.json plus
|
|
402
|
+
// whatever else this core scaffolds alongside it — tsconfig, test
|
|
403
|
+
// config, src/index.ts) alongside the module's own files, and every one
|
|
404
|
+
// of those lands outside a {module}-bearing glob. Detected here rather
|
|
405
|
+
// than left to be discovered from a failed verify, which is where the
|
|
406
|
+
// override stops being available and the recovery becomes a
|
|
407
|
+
// five-command round trip.
|
|
398
408
|
//
|
|
399
409
|
// `exists` is injected so this module keeps no filesystem dependency of
|
|
400
410
|
// its own; the CLI passes a real one and the packet degrades to no
|
|
@@ -429,11 +439,11 @@ function firstArrivalLines(task, roots) {
|
|
|
429
439
|
return [
|
|
430
440
|
'FIRST ARRIVAL',
|
|
431
441
|
` ${roots.join(', ')} ${roots.length > 1 ? 'do' : 'does'} not exist yet, so this task's`,
|
|
432
|
-
|
|
433
|
-
'
|
|
434
|
-
' at the src/ root. All of that
|
|
435
|
-
'
|
|
436
|
-
' block the task:',
|
|
442
|
+
' generator also creates the package shell (package.json plus whatever else',
|
|
443
|
+
' this core scaffolds alongside it — tsconfig, test config, src/index.ts)',
|
|
444
|
+
' plus any package-wide source it writes at the src/ root. All of that',
|
|
445
|
+
' lands OUTSIDE the scope above. Widen this one task before building it,',
|
|
446
|
+
' or verify will reject those paths and block the task:',
|
|
437
447
|
'',
|
|
438
448
|
` hedgehog override add ${task.id} \\`,
|
|
439
449
|
...scopes.map((s) => ` --scope '${s}' \\`),
|