@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 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 the generators the packet points at
1300
- // take the module name plural. A singular id compiles a whole graph
1301
- // scoped to a directory the generator will never write, and nothing
1302
- // downstream catches it: plan compiles it, claim hands out a packet whose
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skyf0xx/hedgehog",
3
- "version": "5.3.3",
3
+ "version": "5.3.4",
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": {
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 a single prompt/config file (e.g. `.hedgehog/dsh-smoke/{module}.md`)
379
- // never holds a `package.json` of its own, so its parent directory is
380
- // never a package root a generator could be first into.
381
- if (wildcard === -1) return null;
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
- // tsconfig*.json, vitest.config.mts, src/index.ts) alongside the module's
394
- // own files, and every one of those lands outside a {module}-bearing
395
- // glob. Detected here rather than left to be discovered from a failed
396
- // verify, which is where the override stops being available and the
397
- // recovery becomes a five-command round trip.
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
- " generator also creates the package shell (package.json, tsconfig*.json,",
433
- ' vitest.config.mts, src/index.ts) plus any package-wide source it writes',
434
- ' at the src/ root. All of that lands OUTSIDE the scope above. Widen this',
435
- ' one task before building it, or verify will reject those paths and',
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}' \\`),
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hedgehog",
3
- "version": "5.3.3",
3
+ "version": "5.3.4",
4
4
  "description": "Hedgehog build discipline: ordered, tested, verified build steps.",
5
5
  "contextFileName": "GEMINI.md"
6
6
  }