@llman-sdd/cli 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llman-sdd/cli",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Spec-driven development workflow CLI (the `llman-sdd` command)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,7 +27,7 @@
27
27
  "build": "bun run ./scripts/build-binary.ts"
28
28
  },
29
29
  "dependencies": {
30
- "@llman-sdd/core": "0.6.0",
30
+ "@llman-sdd/core": "0.7.0",
31
31
  "commander": "^15.0.0"
32
32
  },
33
33
  "engines": {
package/src/cli-shared.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Command modules depend one-way on this file; it must not import any of them.
5
5
  */
6
6
  import { existsSync, readFileSync } from 'node:fs';
7
+ import { join } from 'node:path';
7
8
 
8
9
  import {
9
10
  discoverSpecs,
@@ -12,6 +13,7 @@ import {
12
13
  loadConfigDetail,
13
14
  makeEmbeddedTemplateIo,
14
15
  resolveChangeId,
16
+ resolveInstanceRoot,
15
17
  type TemplateIo,
16
18
  } from '@llman-sdd/core';
17
19
  import type { Command } from 'commander';
@@ -75,6 +77,26 @@ export function loadSpecEntries(): ReturnType<typeof discoverSpecs> {
75
77
  return discoverSpecs('llmanspec/specs', newIo());
76
78
  }
77
79
 
80
+ /**
81
+ * Per-root specs dir (r94): the nearest llmanspec/ instance at or above the
82
+ * start dir (--directory override). Identity 'llmanspec/specs' when the cwd
83
+ * is itself the instance root — legacy relative output paths stay
84
+ * byte-identical there.
85
+ */
86
+ export function resolveRootSpecsDir(directory?: string): string {
87
+ const start = directory ?? process.cwd();
88
+ const root = resolveInstanceRoot(start, newIo());
89
+ if (root === null) {
90
+ throw new CliError(`no llman spec root at or above: ${start}`);
91
+ }
92
+ return root === process.cwd() ? 'llmanspec/specs' : join(root, 'llmanspec', 'specs');
93
+ }
94
+
95
+ /** Spec entries of the resolved instance root (per-root registry, r94). */
96
+ export function loadRootSpecEntries(directory?: string): ReturnType<typeof discoverSpecs> {
97
+ return discoverSpecs(resolveRootSpecsDir(directory), newIo());
98
+ }
99
+
78
100
  /** One-time legacy `bdd:` migration warning — printed to stderr by the first
79
101
  * config-reading command of a process (specs-check-config-and-unbound-feed). */
80
102
  let legacyBddWarningShown = false;
@@ -1,11 +1,21 @@
1
1
  import { mkdirSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
 
4
- import { runInit } from '@llman-sdd/core';
4
+ import { discoverRoots, refreshSubRootBlocks, runInit } from '@llman-sdd/core';
5
5
  import type { Command } from 'commander';
6
6
 
7
7
  import { CliError, templateIo, version } from '../cli-shared.ts';
8
- import { makeIo } from '../io.ts';
8
+ import { makeCliGit, makeIo } from '../io.ts';
9
+
10
+ /**
11
+ * r93: the repo-root instance (git toplevel, or outside any repo) carries the
12
+ * agent skill surface; a sub-root init defaults to blocks-only. `--skills`
13
+ * opts a sub-root in.
14
+ */
15
+ function isRepoRootInstance(root: string): boolean {
16
+ const toplevel = makeCliGit(root).runOpt(['rev-parse', '--show-toplevel']);
17
+ return toplevel === null || resolve(toplevel) === root;
18
+ }
9
19
 
10
20
  export function registerInit(program: Command): void {
11
21
  program
@@ -13,10 +23,17 @@ export function registerInit(program: Command): void {
13
23
  .description('Initialize llmanspec in your project (--update to refresh existing)')
14
24
  .argument('[path]', 'target directory (created when missing; defaults to cwd)')
15
25
  .option('--update', 'refresh an existing installation')
26
+ .option(
27
+ '--skills',
28
+ 'inject .agents/skills at a sub-root instance (repo-root instances always inject)',
29
+ )
16
30
  .option('--locale <locale>', 'locale for generated templates (defaults to config or en)')
17
31
  .option('--lang <locale>', 'alias of --locale')
18
32
  .action(
19
- (path: string | undefined, options: { update?: boolean; locale?: string; lang?: string }) => {
33
+ (
34
+ path: string | undefined,
35
+ options: { update?: boolean; skills?: boolean; locale?: string; lang?: string },
36
+ ) => {
20
37
  if (options.locale !== undefined && options.lang !== undefined) {
21
38
  throw new CliError('--locale and --lang are mutually exclusive (they are aliases)');
22
39
  }
@@ -25,13 +42,28 @@ export function registerInit(program: Command): void {
25
42
  root = resolve(path);
26
43
  mkdirSync(root, { recursive: true });
27
44
  }
45
+ const repoRootInstance = isRepoRootInstance(root);
28
46
  const result = runInit(makeIo(root), templateIo, {
29
47
  update: options.update ?? false,
30
48
  locale: options.locale ?? options.lang,
31
49
  version,
50
+ skills: options.skills === true || repoRootInstance,
32
51
  });
52
+ let swept = '';
53
+ if ((options.update ?? false) && repoRootInstance) {
54
+ // r93 --update sweep: refresh managed blocks of every discovered
55
+ // sub-root (blocks only; skills stay a repo-root surface).
56
+ const subRoots = discoverRoots(root, makeIo(root)).filter((r) => r.rootDir !== root);
57
+ const refreshed: string[] = [];
58
+ for (const sub of subRoots) {
59
+ if (refreshSubRootBlocks(makeIo(sub.rootDir), templateIo, version)) {
60
+ refreshed.push(sub.rootDir);
61
+ }
62
+ }
63
+ if (refreshed.length > 0) swept = `, blocks refreshed: ${refreshed.join(', ')}`;
64
+ }
33
65
  const removed = result.removed.length > 0 ? `, removed: ${result.removed.join(', ')}` : '';
34
- console.log(`initialized llmanspec (${result.skills.length} skills${removed})`);
66
+ console.log(`initialized llmanspec (${result.skills.length} skills${removed}${swept})`);
35
67
  },
36
68
  );
37
69
  }
@@ -20,9 +20,10 @@ import {
20
20
  addReportOutputOptions,
21
21
  CliError,
22
22
  loadCliConfig,
23
- loadSpecEntries,
23
+ loadRootSpecEntries,
24
24
  newIo,
25
25
  resolveOutMode,
26
+ resolveRootSpecsDir,
26
27
  } from '../cli-shared.ts';
27
28
 
28
29
  /** Walk paths collecting .feature files (directories recurse). */
@@ -61,13 +62,18 @@ export function registerSpec(program: Command): void {
61
62
  .description('Generate a single-track spec skeleton for a capability')
62
63
  .argument('<capability>')
63
64
  .option('--force', 'overwrite an existing spec file')
64
- .action((capability: string, options: { force?: boolean }) => {
65
+ .option(
66
+ '--directory <path>',
67
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
68
+ )
69
+ .action((capability: string, options: { force?: boolean; directory?: string }) => {
65
70
  const locale = loadCliConfig()?.locale ?? 'en';
66
- const path = join('llmanspec', 'specs', `${capability}.feature`);
71
+ const specsDir = resolveRootSpecsDir(options.directory);
72
+ const path = join(specsDir, `${capability}.feature`);
67
73
  if (!options.force && existsSync(path)) {
68
74
  throw new CliError(`spec already exists: ${path} (use --force to overwrite)`);
69
75
  }
70
- const written = scaffoldSpec(newIo(), 'llmanspec/specs', capability, locale, {
76
+ const written = scaffoldSpec(newIo(), specsDir, capability, locale, {
71
77
  force: options.force,
72
78
  });
73
79
  console.log(`wrote ${written}`);
@@ -75,10 +81,14 @@ export function registerSpec(program: Command): void {
75
81
 
76
82
  spec
77
83
  .command('next-req-id')
78
- .description('Allocate the next free global req id (rN)')
84
+ .description('Allocate the next global req id (max in use + 1, rN)')
79
85
  .option('--json', 'emit {reqId}')
80
- .action((options: { json?: boolean }) => {
81
- const reqId = nextReqId(newIo(), 'llmanspec/specs');
86
+ .option(
87
+ '--directory <path>',
88
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
89
+ )
90
+ .action((options: { json?: boolean; directory?: string }) => {
91
+ const reqId = nextReqId(newIo(), resolveRootSpecsDir(options.directory));
82
92
  if (options.json) console.log(JSON.stringify({ reqId }, null, 2));
83
93
  else console.log(reqId);
84
94
  });
@@ -91,16 +101,31 @@ export function registerSpec(program: Command): void {
91
101
  .argument('<req_id>')
92
102
  .requiredOption('--title <title>', 'rule title')
93
103
  .requiredOption('--statement <statement>', 'rule statement (free text; multiple lines via \\n)')
94
- .action((capability: string, reqId: string, options: { title: string; statement: string }) => {
95
- const io = newIo();
96
- const path = addReq(io, 'llmanspec/specs', loadSpecEntries(), {
97
- capability,
98
- reqId,
99
- title: options.title,
100
- statement: options.statement,
101
- });
102
- console.log(path);
103
- });
104
+ .option(
105
+ '--directory <path>',
106
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
107
+ )
108
+ .action(
109
+ (
110
+ capability: string,
111
+ reqId: string,
112
+ options: { title: string; statement: string; directory?: string },
113
+ ) => {
114
+ const io = newIo();
115
+ const path = addReq(
116
+ io,
117
+ resolveRootSpecsDir(options.directory),
118
+ loadRootSpecEntries(options.directory),
119
+ {
120
+ capability,
121
+ reqId,
122
+ title: options.title,
123
+ statement: options.statement,
124
+ },
125
+ );
126
+ console.log(path);
127
+ },
128
+ );
104
129
 
105
130
  spec
106
131
  .command('add-scenario')
@@ -111,22 +136,31 @@ export function registerSpec(program: Command): void {
111
136
  .option('--given <given>', 'Given step (optional)')
112
137
  .requiredOption('--when <when>', 'When step')
113
138
  .requiredOption('--then <then>', 'Then step')
139
+ .option(
140
+ '--directory <path>',
141
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
142
+ )
114
143
  .action(
115
144
  (
116
145
  capability: string,
117
146
  reqId: string,
118
147
  scenarioId: string,
119
- options: { given?: string; when: string; then: string },
148
+ options: { given?: string; when: string; then: string; directory?: string },
120
149
  ) => {
121
150
  const io = newIo();
122
- const path = addScenario(io, 'llmanspec/specs', loadSpecEntries(), {
123
- capability,
124
- reqId,
125
- scenarioId,
126
- given: options.given,
127
- when: options.when,
128
- thenText: options.then,
129
- });
151
+ const path = addScenario(
152
+ io,
153
+ resolveRootSpecsDir(options.directory),
154
+ loadRootSpecEntries(options.directory),
155
+ {
156
+ capability,
157
+ reqId,
158
+ scenarioId,
159
+ given: options.given,
160
+ when: options.when,
161
+ thenText: options.then,
162
+ },
163
+ );
130
164
  console.log(path);
131
165
  },
132
166
  );
@@ -176,8 +210,12 @@ export function registerSpec(program: Command): void {
176
210
  .command('resolve-req')
177
211
  .description('Resolve an rN to its capability and statement')
178
212
  .argument('<req_id>')
179
- .action((reqId: string) => {
180
- const resolved = resolveReq(loadSpecEntries(), reqId);
213
+ .option(
214
+ '--directory <path>',
215
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
216
+ )
217
+ .action((reqId: string, options: { directory?: string }) => {
218
+ const resolved = resolveReq(loadRootSpecEntries(options.directory), reqId);
181
219
  if (resolved === null) {
182
220
  throw new CliError(`req id not found: ${reqId}`);
183
221
  }
@@ -201,7 +239,7 @@ export function registerSpec(program: Command): void {
201
239
  throw new CliError(`invalid --limit: ${raw} (non-negative integer)`, 2);
202
240
  }
203
241
  const limit = Number(raw);
204
- const feed: UnboundFeed = buildUnboundFeed(loadSpecEntries(), limit);
242
+ const feed: UnboundFeed = buildUnboundFeed(loadRootSpecEntries(), limit);
205
243
  const mode = resolveOutMode(options.output, options.json, options.compactJson);
206
244
  if (mode !== 'human') {
207
245
  console.log(renderMachine(feed, mode));
@@ -1,3 +1,5 @@
1
+ import { relative } from 'node:path';
2
+
1
3
  import {
2
4
  applyStrict,
3
5
  buildDuplicatesFor,
@@ -5,11 +7,14 @@ import {
5
7
  collectChanges,
6
8
  currentBranch,
7
9
  defaultBranch,
10
+ discoverRoots,
8
11
  evaluateStaleness,
9
12
  formatTotals,
10
13
  notApplicableStaleness,
11
14
  renderMachine,
15
+ resolveInstanceRoot,
12
16
  runHarnessForSpecs,
17
+ scopeCrossings,
13
18
  specRelFor,
14
19
  specIdOf,
15
20
  STAGE_ORDER,
@@ -17,6 +22,7 @@ import {
17
22
  validateChange,
18
23
  type ChangeIssue,
19
24
  type HarnessGate,
25
+ type RootEntry,
20
26
  type StalenessInfo,
21
27
  } from '@llman-sdd/core';
22
28
  import type { Command } from 'commander';
@@ -51,13 +57,34 @@ function compareItems(a: VItem, b: VItem): number {
51
57
  return a.id < b.id ? -1 : a.id > b.id ? 1 : a.type.localeCompare(b.type);
52
58
  }
53
59
 
60
+ /**
61
+ * Multi-root context of the specs being validated (r91-r92): `repoRelPrefix`
62
+ * lifts instance-root-relative scopes into repo-relative paths for the
63
+ * staleness diff match ('' when the instance root is the git toplevel —
64
+ * byte-identical legacy behavior); `otherRoots` feeds the single-ownership
65
+ * crossings that surface as ERROR issues.
66
+ */
67
+ export interface InstanceContext {
68
+ repoRelPrefix: string;
69
+ otherRoots: RootEntry[];
70
+ }
71
+
54
72
  function specV1Items(
55
- opts: { strict?: boolean; harness?: HarnessGate; harnessOnlyIds?: string[] },
73
+ opts: {
74
+ strict?: boolean;
75
+ harness?: HarnessGate;
76
+ harnessOnlyIds?: string[];
77
+ instance?: InstanceContext;
78
+ },
56
79
  entries: ReturnType<typeof loadSpecEntries> = loadSpecEntries(),
57
80
  ): VItem[] {
58
81
  const io = newIo();
59
82
  const git = makeCliGit(process.cwd());
60
83
  const duplicatesFor = buildDuplicatesFor(entries);
84
+ const liftScope = (scope: string): string =>
85
+ opts.instance !== undefined && opts.instance.repoRelPrefix !== ''
86
+ ? `${opts.instance.repoRelPrefix}/${scope}`
87
+ : scope;
61
88
 
62
89
  const items: VItem[] = [];
63
90
  for (const entry of entries) {
@@ -65,12 +92,13 @@ function specV1Items(
65
92
  const verdict = validateCapability(entry as never, duplicatesFor, io, {
66
93
  strict: opts.strict === true,
67
94
  });
95
+ const scopes = entry.doc.header.scope?.split(',').map((s) => s.trim()) ?? [];
68
96
  const specRel = specRelFor(entry.fileName);
69
97
  const staleness = evaluateStaleness({
70
98
  git,
71
99
  root: process.cwd(),
72
100
  specRel,
73
- scope: entry.doc.header.scope?.split(',').map((s) => s.trim()) ?? [],
101
+ scope: scopes.map(liftScope),
74
102
  baseRefEnv: process.env.LLMANSPEC_BASE_REF,
75
103
  });
76
104
  let issues: ChangeIssue[] = verdict.items.map((i) => ({
@@ -78,6 +106,18 @@ function specV1Items(
78
106
  path: i.id,
79
107
  message: i.message,
80
108
  }));
109
+ if (opts.instance !== undefined) {
110
+ for (const crossing of scopeCrossings(scopes, process.cwd(), opts.instance.otherRoots)) {
111
+ issues = [
112
+ ...issues,
113
+ {
114
+ level: 'ERROR',
115
+ path: 'single-ownership',
116
+ message: `scope '${crossing.scope}' crosses sub-root '${crossing.rootDir}' — migrate these specs into that root or narrow the scope (one path, one root)`,
117
+ },
118
+ ];
119
+ }
120
+ }
81
121
  if (opts.strict === true) issues = [...issues, ...applyStrict(staleness.issues)];
82
122
  else issues = [...issues, ...staleness.issues];
83
123
  items.push({
@@ -201,9 +241,33 @@ function renderValidateText(items: VItem[]): void {
201
241
  console.log(formatTotals(passed, failed, items.length));
202
242
  }
203
243
 
204
- function renderValidateReport(items: VItem[], mode: 'json' | 'compact-json' | 'toon'): void {
244
+ function gitToplevelOrCwd(): string {
245
+ const toplevel = makeCliGit(process.cwd()).runOpt(['rev-parse', '--show-toplevel']);
246
+ return toplevel ?? process.cwd();
247
+ }
248
+
249
+ /**
250
+ * Multi-root context of the cwd instance (r91-r92): empty otherRoots + ''
251
+ * prefix when run at a lone root — legacy byte-identical behavior.
252
+ */
253
+ function instanceContextFor(maxDepth: number): InstanceContext {
254
+ const toplevel = gitToplevelOrCwd();
255
+ const otherRoots = discoverRoots(toplevel, newIo(), { maxDepth }).filter(
256
+ (r) => r.rootDir !== process.cwd(),
257
+ );
258
+ const repoRelPrefix = toplevel === process.cwd() ? '' : relative(toplevel, process.cwd());
259
+ return { repoRelPrefix, otherRoots };
260
+ }
261
+
262
+ interface RootReport {
263
+ root: string;
264
+ items: VItem[];
265
+ summary: ReturnType<typeof summarizeItems>;
266
+ }
267
+
268
+ function summarizeItems(items: VItem[]) {
205
269
  const types = [...new Set(items.map((i) => i.type))] as string[];
206
- const summary = {
270
+ return {
207
271
  totals: {
208
272
  items: items.length,
209
273
  passed: items.filter((i) => i.valid).length,
@@ -222,7 +286,10 @@ function renderValidateReport(items: VItem[], mode: 'json' | 'compact-json' | 't
222
286
  {},
223
287
  ),
224
288
  };
225
- console.log(renderMachine({ items, summary, version: '1.0' }, mode));
289
+ }
290
+
291
+ function renderValidateReport(items: VItem[], mode: 'json' | 'compact-json' | 'toon'): void {
292
+ console.log(renderMachine({ items, summary: summarizeItems(items), version: '1.0' }, mode));
226
293
  }
227
294
 
228
295
  const SPEC_NEXT_STEPS = [
@@ -285,6 +352,14 @@ export function registerValidate(program: Command): void {
285
352
  .option('--type <type>', 'force disambiguation: change | spec')
286
353
  .option('--stage <stage>', 'change stage gate: draft | designed | planned | full')
287
354
  .option('--strict', 'warnings also make the exit code non-zero')
355
+ .option(
356
+ '--directory <path>',
357
+ 'instance root resolution start (default: nearest llmanspec/ at or above cwd)',
358
+ )
359
+ .option(
360
+ '--all-roots',
361
+ 'aggregate: validate every discovered llmanspec root (specs scope, any root red → non-zero exit)',
362
+ )
288
363
  .option('--include-info', 'keep INFO-level issues (default: WARNING and above)')
289
364
  .option('--no-check', 'skip the spec check')
290
365
  .option('--check', 'run the spec check (default when specs.check_command is configured)');
@@ -304,6 +379,8 @@ export function registerValidate(program: Command): void {
304
379
  includeInfo?: boolean;
305
380
  output?: string;
306
381
  check?: boolean;
382
+ directory?: string;
383
+ allRoots?: boolean;
307
384
  },
308
385
  ) => {
309
386
  if (!assertCompactJsonPairing(options)) return;
@@ -323,6 +400,77 @@ export function registerValidate(program: Command): void {
323
400
  ) {
324
401
  throw new CliError(`invalid --stage: ${options.stage}`);
325
402
  }
403
+ const maxDepth = cliMaxScanDepth(program);
404
+
405
+ // ---- aggregate over every discovered root (r91) ----
406
+ if (options.allRoots === true) {
407
+ if (item !== undefined) {
408
+ throw new CliError('--all-roots validates every root; drop the <item> argument');
409
+ }
410
+ const start = options.directory ?? gitToplevelOrCwd();
411
+ const roots = discoverRoots(start, newIo(), { maxDepth });
412
+ if (roots.length === 0) {
413
+ throw new CliError(`no llmanspec roots discovered under: ${start}`);
414
+ }
415
+ const originalCwd = process.cwd();
416
+ const reports: RootReport[] = [];
417
+ let anyFail = false;
418
+ try {
419
+ for (const root of roots) {
420
+ process.chdir(root.rootDir);
421
+ const items = specV1Items({
422
+ strict: options.strict,
423
+ harness: makeHarnessGate(options),
424
+ instance: instanceContextFor(maxDepth),
425
+ });
426
+ const visible = keepInfo ? items : items.map(stripInfo);
427
+ if (visible.some((i) => !i.valid)) anyFail = true;
428
+ reports.push({
429
+ root: relative(start, root.rootDir) || '.',
430
+ items: visible,
431
+ summary: summarizeItems(visible),
432
+ });
433
+ }
434
+ } finally {
435
+ process.chdir(originalCwd);
436
+ }
437
+ if (outMode === 'human') {
438
+ for (const report of reports) {
439
+ console.log(`root ${report.root}`);
440
+ renderValidateText(report.items);
441
+ }
442
+ } else {
443
+ const totals = reports.reduce(
444
+ (acc, r) => ({
445
+ items: acc.items + r.summary.totals.items,
446
+ passed: acc.passed + r.summary.totals.passed,
447
+ failed: acc.failed + r.summary.totals.failed,
448
+ }),
449
+ { items: 0, passed: 0, failed: 0 },
450
+ );
451
+ console.log(
452
+ renderMachine({ roots: reports, summary: { totals }, version: '1.0' }, outMode),
453
+ );
454
+ }
455
+ if (anyFail) console.error('Error: validation failed');
456
+ exitWith(anyFail ? 1 : 0);
457
+ return;
458
+ }
459
+
460
+ // ---- instance root resolution: --directory start, else nearest
461
+ // ancestor llmanspec/ ("cd into the subpackage and run", r94). A no-op
462
+ // when cwd already sits at a root (byte-identical legacy path). ----
463
+ if (options.directory !== undefined) {
464
+ const root = resolveInstanceRoot(options.directory, newIo());
465
+ if (root === null) {
466
+ throw new CliError(`no llmanspec root at or above: ${options.directory}`);
467
+ }
468
+ process.chdir(root);
469
+ } else {
470
+ const root = resolveInstanceRoot(process.cwd(), newIo());
471
+ if (root !== null && root !== process.cwd()) process.chdir(root);
472
+ }
473
+
326
474
  const harness = makeHarnessGate(options);
327
475
 
328
476
  // ---- single item (auto-disambiguate: spec first, then change) ----
@@ -332,7 +480,12 @@ export function registerValidate(program: Command): void {
332
480
  options.type === 'change' ? undefined : entries.find((e) => specIdOf(e) === item);
333
481
  if (specEntry !== undefined) {
334
482
  const items = specV1Items(
335
- { strict: options.strict, harness, harnessOnlyIds: [item] },
483
+ {
484
+ strict: options.strict,
485
+ harness,
486
+ harnessOnlyIds: [item],
487
+ instance: instanceContextFor(maxDepth),
488
+ },
336
489
  entries,
337
490
  );
338
491
  const mine = items.find((i) => i.id === item);
@@ -424,10 +577,18 @@ export function registerValidate(program: Command): void {
424
577
  const effectiveChanges = defaultSpecsOnly ? false : changeScope;
425
578
 
426
579
  let items: VItem[] = [];
427
- if (effectiveSpecs) items = items.concat(specV1Items({ strict: options.strict, harness }));
580
+ if (effectiveSpecs) {
581
+ items = items.concat(
582
+ specV1Items({
583
+ strict: options.strict,
584
+ harness,
585
+ instance: instanceContextFor(maxDepth),
586
+ }),
587
+ );
588
+ }
428
589
  if (effectiveChanges) {
429
590
  const names = collectChanges(newIo(), process.cwd(), new Date(), {
430
- maxScanDepth: cliMaxScanDepth(program),
591
+ maxScanDepth: maxDepth,
431
592
  }).map((c) => c.name);
432
593
  items = items.concat(
433
594
  changeV1Items(names, { stage: options.stage, strict: options.strict }),