@llman-sdd/cli 0.5.1 → 0.6.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.5.1",
3
+ "version": "0.6.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.5.1",
30
+ "@llman-sdd/core": "0.6.0",
31
31
  "commander": "^15.0.0"
32
32
  },
33
33
  "engines": {
package/src/cli-shared.ts CHANGED
@@ -9,6 +9,7 @@ import {
9
9
  discoverSpecs,
10
10
  embeddedTemplates,
11
11
  loadConfig,
12
+ loadConfigDetail,
12
13
  makeEmbeddedTemplateIo,
13
14
  resolveChangeId,
14
15
  type TemplateIo,
@@ -74,10 +75,29 @@ export function loadSpecEntries(): ReturnType<typeof discoverSpecs> {
74
75
  return discoverSpecs('llmanspec/specs', newIo());
75
76
  }
76
77
 
78
+ /** One-time legacy `bdd:` migration warning — printed to stderr by the first
79
+ * config-reading command of a process (specs-check-config-and-unbound-feed). */
80
+ let legacyBddWarningShown = false;
81
+
82
+ function warnLegacyBdd(elevated: boolean): void {
83
+ if (!elevated || legacyBddWarningShown) return;
84
+ legacyBddWarningShown = true;
85
+ console.error(
86
+ '[WARNING] llmanspec/config.yaml uses the legacy `bdd:` section — migrate to `specs:` (`bdd.run_command` → `specs.check_command`). See migrations/v0.5-v0.6/README.md.',
87
+ );
88
+ }
89
+
90
+ /** Surface the legacy-section warning from a raw config source (config command). */
91
+ export function warnLegacyBddOnce(source: string): void {
92
+ if (legacyBddWarningShown) return;
93
+ warnLegacyBdd(loadConfigDetail(source).legacyBddElevated);
94
+ }
95
+
77
96
  export function loadCliConfig(): ReturnType<typeof loadConfig> | null {
78
- return existsSync('llmanspec/config.yaml')
79
- ? loadConfig(readFileSync('llmanspec/config.yaml', 'utf8'))
80
- : null;
97
+ if (!existsSync('llmanspec/config.yaml')) return null;
98
+ const detail = loadConfigDetail(readFileSync('llmanspec/config.yaml', 'utf8'));
99
+ warnLegacyBdd(detail.legacyBddElevated);
100
+ return detail.config;
81
101
  }
82
102
 
83
103
  export function cliMaxScanDepth(program: Command): number {
@@ -40,21 +40,15 @@ function runValidateSweep(): boolean {
40
40
  return report.verdicts.some((v) => !v.ok);
41
41
  }
42
42
 
43
- /** r81: run or refuse the acceptance command before any merge or rename. */
43
+ /** r81: run or refuse the spec verification command before any merge or rename. */
44
44
  function enforceCloseOutHarness(id: string, noCheck: boolean): void {
45
45
  const proposalPath = `llmanspec/changes/${id}/proposal.md`;
46
46
  const proposal = existsSync(proposalPath) ? readFileSync(proposalPath, 'utf8') : '';
47
47
  const needsSpecsChange = readNeedsSpecsChange(extractFrontmatter(proposal));
48
- const hasExecutable = loadSpecEntries().some(
49
- (entry) =>
50
- entry.doc.rules.some((r) => r.scenarios.some((s) => s.runnable && s.stepCount > 0)) ||
51
- entry.doc.orphans.some((s) => s.runnable && s.stepCount > 0),
52
- );
53
- const runCommand = loadCliConfig()?.bdd?.run_command ?? null;
48
+ const runCommand = loadCliConfig()?.specs?.check_command ?? null;
54
49
  const decision = decideCloseOutHarness({
55
50
  noCheck,
56
51
  needsSpecsChange,
57
- hasExecutable,
58
52
  runCommand,
59
53
  nested: process.env.LLMAN_SDD_HARNESS_ACTIVE === '1',
60
54
  });
@@ -62,13 +56,14 @@ function enforceCloseOutHarness(id: string, noCheck: boolean): void {
62
56
  throw new CliError(`close-out aborted: ${decision.message}`);
63
57
  }
64
58
  if (decision.kind === 'skip') {
65
- if (decision.announce) console.error('bdd harness skipped: --no-check');
59
+ if (decision.announce) console.error('spec check skipped: --no-check');
60
+ if (decision.warning !== undefined) console.error(`[WARNING] ${decision.warning}`);
66
61
  return;
67
62
  }
68
63
  const result = makeCliHarnessRunner().run(decision.command, process.cwd());
69
64
  if (result.spawnError !== undefined || result.exitCode !== 0) {
70
65
  const why = result.spawnError ?? `exit ${result.exitCode}`;
71
- throw new CliError(`close-out aborted: bdd harness failed (${why})`);
66
+ throw new CliError(`close-out aborted: spec check failed (${why})`);
72
67
  }
73
68
  }
74
69
 
@@ -3,7 +3,12 @@ import { readFileSync } from 'node:fs';
3
3
  import { renderConfigOverview, renderMachine, skillsJson } from '@llman-sdd/core';
4
4
  import type { Command } from 'commander';
5
5
 
6
- import { addReportOutputOptions, resolveOutMode, skillDesc } from '../cli-shared.ts';
6
+ import {
7
+ addReportOutputOptions,
8
+ resolveOutMode,
9
+ skillDesc,
10
+ warnLegacyBddOnce,
11
+ } from '../cli-shared.ts';
7
12
 
8
13
  export function registerConfig(program: Command): void {
9
14
  const configCmd = program
@@ -12,6 +17,7 @@ export function registerConfig(program: Command): void {
12
17
 
13
18
  configCmd.description('Print a read-only llmanspec/config.yaml overview').action(() => {
14
19
  const source = readFileSync('llmanspec/config.yaml', 'utf8');
20
+ warnLegacyBddOnce(source);
15
21
  console.log(renderConfigOverview(source).join('\n'));
16
22
  });
17
23
 
@@ -19,7 +25,9 @@ export function registerConfig(program: Command): void {
19
25
  addReportOutputOptions(skills);
20
26
  skills.action((options: { json?: boolean; output?: string; compactJson?: boolean }) => {
21
27
  const path = 'llmanspec/config.yaml';
22
- const info = skillsJson(readFileSync(path, 'utf8'));
28
+ const source = readFileSync(path, 'utf8');
29
+ warnLegacyBddOnce(source);
30
+ const info = skillsJson(source);
23
31
  const mode = resolveOutMode(options.output, options.json, options.compactJson);
24
32
  if (mode !== 'human') {
25
33
  console.log(renderMachine(info, mode));
@@ -26,7 +26,7 @@ import { makeCliGit } from '../io.ts';
26
26
  export function registerReview(program: Command): void {
27
27
  const review = program
28
28
  .command('review')
29
- .description('Aggregate review: pending/unbound/stale signals plus a validate sweep');
29
+ .description('Aggregate review: unbound/stale signals plus a validate sweep');
30
30
 
31
31
  review.option('--capability <capability>', 'restrict the sweep to one capability/spec id');
32
32
  addReportOutputOptions(review);
@@ -209,7 +209,7 @@ export function registerShow(program: Command): void {
209
209
  const raw = readFileSync(specPath, 'utf8').trimEnd();
210
210
  const summary = collectSpecs(entries).find((x) => x.id === item);
211
211
  const morphology = summary
212
- ? `\n\n## Morphology\nruleCount=${summary.morphology.ruleCount} enforced=${summary.morphology.ruleEnforcedCount} pending=${summary.morphology.rulePendingCount} acceptanceCount=${summary.morphology.acceptanceCount}`
212
+ ? `\n\n## Morphology\nrequirements=${summary.requirementCount} bound=${summary.morphology.requirementBoundCount} unbound=${summary.morphology.requirementUnboundCount} acceptanceCount=${summary.morphology.acceptanceCount}`
213
213
  : '';
214
214
  console.log(`## Spec\n${raw}${morphology}`);
215
215
  return;
@@ -5,15 +5,25 @@ import { createInterface } from 'node:readline';
5
5
  import {
6
6
  addReq,
7
7
  addScenario,
8
+ buildUnboundFeed,
8
9
  hasNativeRules,
9
10
  migrateNativeSource,
10
11
  nextReqId,
12
+ renderMachine,
11
13
  resolveReq,
12
14
  scaffoldSpec,
15
+ type UnboundFeed,
13
16
  } from '@llman-sdd/core';
14
17
  import type { Command } from 'commander';
15
18
 
16
- import { CliError, loadCliConfig, loadSpecEntries, newIo } from '../cli-shared.ts';
19
+ import {
20
+ addReportOutputOptions,
21
+ CliError,
22
+ loadCliConfig,
23
+ loadSpecEntries,
24
+ newIo,
25
+ resolveOutMode,
26
+ } from '../cli-shared.ts';
17
27
 
18
28
  /** Walk paths collecting .feature files (directories recurse). */
19
29
  function collectFeatureFiles(paths: string[]): string[] {
@@ -178,4 +188,31 @@ export function registerSpec(program: Command): void {
178
188
  console.log('harness:');
179
189
  for (const h of resolved.harness) console.log(` - ${h}`);
180
190
  });
191
+
192
+ const unbound = spec
193
+ .command('unbound')
194
+ .description('List unbound requirements (no runnable nested scenario) for implementation')
195
+ .option('--limit <N>', 'max entries to return (default 1; 0 = all)');
196
+ addReportOutputOptions(unbound);
197
+ unbound.action(
198
+ (options: { limit?: string; output?: string; json?: boolean; compactJson?: boolean }) => {
199
+ const raw = options.limit ?? '1';
200
+ if (!/^\d+$/u.test(raw)) {
201
+ throw new CliError(`invalid --limit: ${raw} (non-negative integer)`, 2);
202
+ }
203
+ const limit = Number(raw);
204
+ const feed: UnboundFeed = buildUnboundFeed(loadSpecEntries(), limit);
205
+ const mode = resolveOutMode(options.output, options.json, options.compactJson);
206
+ if (mode !== 'human') {
207
+ console.log(renderMachine(feed, mode));
208
+ return;
209
+ }
210
+ console.log(`spec unbound: ${feed.returned} shown of ${feed.total} total`);
211
+ for (const r of feed.requirements) {
212
+ console.log(` - [${r.reqId}] ${r.featurePath} :: ${r.title}`);
213
+ for (const line of r.statement.split('\n')) console.log(` ${line}`);
214
+ }
215
+ if (feed.hint !== '') console.log(feed.hint);
216
+ },
217
+ );
181
218
  }
@@ -258,7 +258,7 @@ let harnessBannerShown = false;
258
258
  * --check → check=true, --no-check → check=false, absent → undefined.
259
259
  */
260
260
  function makeHarnessGate(options: { check?: boolean }): HarnessGate {
261
- const runCommand = loadCliConfig()?.bdd?.run_command ?? null;
261
+ const runCommand = loadCliConfig()?.specs?.check_command ?? null;
262
262
  const configured = runCommand !== null && runCommand !== '';
263
263
  return {
264
264
  nested: process.env.LLMAN_SDD_HARNESS_ACTIVE === '1',
@@ -269,7 +269,7 @@ function makeHarnessGate(options: { check?: boolean }): HarnessGate {
269
269
  onBeforeFirstRun: (expanded: string): void => {
270
270
  if (harnessBannerShown) return;
271
271
  harnessBannerShown = true;
272
- console.error(`running bdd harness: ${expanded} (use --no-check to skip)`);
272
+ console.error(`running spec check: ${expanded} (use --no-check to skip)`);
273
273
  },
274
274
  };
275
275
  }
@@ -286,8 +286,8 @@ export function registerValidate(program: Command): void {
286
286
  .option('--stage <stage>', 'change stage gate: draft | designed | planned | full')
287
287
  .option('--strict', 'warnings also make the exit code non-zero')
288
288
  .option('--include-info', 'keep INFO-level issues (default: WARNING and above)')
289
- .option('--no-check', 'skip the bdd harness')
290
- .option('--check', 'run the bdd harness (default when bdd.run_command is configured)');
289
+ .option('--no-check', 'skip the spec check')
290
+ .option('--check', 'run the spec check (default when specs.check_command is configured)');
291
291
  addReportOutputOptions(validate);
292
292
  validate.action(
293
293
  (
package/src/harness.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * CLI HarnessRunner adapter (validation r13/r48): executes the expanded
3
- * bdd.run_command through `sh -c` in the project root, exporting
3
+ * specs.check_command through `sh -c` in the project root, exporting
4
4
  * LLMAN_SDD_HARNESS_ACTIVE=1 so harness-spawned validate invocations skip
5
5
  * execution (nested guard). Windows (no sh) is out of scope for this change —
6
6
  * the spawnError branch surfaces it as an ERROR. stdout and stderr are merged;