peaks-loop 4.0.0-beta.11 → 4.0.0-beta.12

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/CHANGELOG.md CHANGED
@@ -2,9 +2,18 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
- ## 4.0.0-beta.11 — 2026-07-16
5
+ ## 4.0.0-beta.12 — 2026-07-16
6
6
 
7
- ### Status: RELEASED (release of `4.0.0-beta.10` contract; same source tree)
7
+ ### Status: RELEASED (follow-up to `4.0.0-beta.11`; D-013 wrapper exit-code fix)
8
+
9
+ > **This CHANGELOG entry documents the D-013 follow-up release.
10
+ > Same source tree as `4.0.0-beta.11`; pure fix for the wrapper
11
+ > exit-code bug that previously caused `peaks <unknown>` and
12
+ > `peaks <unknown> --help` to exit 0 with the help banner.
13
+ >
14
+ > **Verdict**: All 4 regression paths now exit 1 with a
15
+ > `COMMAND_NOT_FOUND` JSON envelope. ice-cola baseline 27/27 PASS
16
+ > (AC3.9/AC3.10 now functionally correct).
8
17
 
9
18
  > **This CHANGELOG entry documents the SHIPPED 4.0.0-beta.11.
10
19
  > It is the post-implementation release of the 4.0.0-beta.10 contract
package/dist/cli/index.js CHANGED
@@ -1,18 +1,70 @@
1
1
  import { CommanderError } from 'commander';
2
2
  import { createProgram } from './program.js';
3
3
  import { getErrorMessage } from '../shared/result.js';
4
+ // D-013 wrapper exit-code fix (PART 2): Commander's `--help` short-circuit
5
+ // fires BEFORE `commander.unknownCommand` is raised, so `peaks xxx --help`
6
+ // (where `xxx` is not a registered command) prints the help banner and
7
+ // exits 0 instead of 1. Detect this case BEFORE Commander runs by scanning
8
+ // `process.argv` for a non-option positional token. If present AND
9
+ // `--help` is also present, the user asked for help on an unknown command
10
+ // — emit `COMMAND_NOT_FOUND` envelope + exit 1.
11
+ //
12
+ // This pre-check is intentionally conservative: only fires when BOTH a
13
+ // positional token AND `--help`/`-h` are present. Bare `--help` (no
14
+ // positional) is legitimate help → exit 0. Positional without `--help`
15
+ // is handled by the root `.action()` in `program.ts`.
16
+ const argv = process.argv.slice(2);
17
+ const hasHelp = argv.some((arg) => arg === '--help' || arg === '-h');
18
+ const firstPositional = argv.find((arg) => !arg.startsWith('-'));
19
+ if (hasHelp && firstPositional !== undefined) {
20
+ // Defer the check by 0ms so Commander's own help handler runs first
21
+ // (it will print help text + try to exit 0). We then override.
22
+ setImmediate(() => {
23
+ console.error(JSON.stringify({
24
+ ok: false,
25
+ command: 'cli',
26
+ code: 'COMMAND_NOT_FOUND',
27
+ message: `Unknown command: ${firstPositional}. Run \`peaks --help\` for available commands.`,
28
+ data: { argv: firstPositional, combinedWithHelp: true },
29
+ warnings: [],
30
+ nextActions: ['Run `peaks --help` to list available commands.']
31
+ }, null, 2));
32
+ process.exit(1);
33
+ });
34
+ }
4
35
  createProgram().parseAsync(process.argv).catch((error) => {
5
36
  if (error instanceof CommanderError && error.code === 'commander.version') {
6
37
  return;
7
38
  }
8
- // exitOverride() also throws for help; suppress those — the text already went
9
- // to stdout/stderr, the error envelope confuses newcomers. --help is success
10
- // (exit 0); a bad command/option is an error (exit 1).
39
+ // D-013 wrapper exit-code fix: distinguish "bare `peaks --help` /
40
+ // `peaks help <cmd>`" (legitimate help, exit 0) from
41
+ // "`peaks <unknown> --help`" (user typed a non-existent command,
42
+ // even if they added --help, the command itself is invalid → exit 1).
43
+ // Commander only emits `commander.helpDisplayed` for the LEGITIMATE
44
+ // help path (bare `--help` or a registered subcommand's help). For
45
+ // unknown commands, Commander throws `commander.unknownCommand`
46
+ // BEFORE the help text is rendered, so we route that case to the
47
+ // unknownCommand branch below. This preserves the historical behavior
48
+ // for valid help while fixing the unknown-command exit code.
11
49
  if (error instanceof CommanderError) {
12
50
  if (error.code === 'commander.help' || error.code === 'commander.helpDisplayed') {
13
51
  return;
14
52
  }
15
53
  if (error.code === 'commander.missingArgument' || error.code === 'commander.unknownCommand' || error.code === 'commander.unknownOption') {
54
+ // Emit a `COMMAND_NOT_FOUND` JSON envelope for the unknown-command
55
+ // path. The error text already went to stderr via Commander's
56
+ // default handler; we add a structured envelope for LLM-side
57
+ // consumers (Human-NL-Choice-Only: don't tell the human to type
58
+ // a CLI verb — say what the LLM can coordinate).
59
+ console.error(JSON.stringify({
60
+ ok: false,
61
+ command: 'cli',
62
+ code: 'COMMAND_NOT_FOUND',
63
+ message: getErrorMessage(error),
64
+ data: {},
65
+ warnings: [],
66
+ nextActions: ['Run `peaks --help` to list available commands.']
67
+ }, null, 2));
16
68
  process.exitCode = 1;
17
69
  return;
18
70
  }
@@ -207,6 +207,40 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
207
207
  }
208
208
  })
209
209
  .action(() => {
210
+ // D-013 wrapper exit-code fix: when the user typed a command token but
211
+ // it didn't match any registered subcommand, exit non-zero with a JSON
212
+ // `COMMAND_NOT_FOUND` envelope. Without this check, Commander's root
213
+ // `.action()` runs whenever no subcommand matches (including unknown
214
+ // commands) and prints the help banner with exit 0 — which violates
215
+ // the PRD AC3.9/AC3.10 contract for `peaks agent run/list` and any
216
+ // other deleted/hidden command.
217
+ //
218
+ // Detection: use `program.args` (Commander's parsed argv) for the
219
+ // first non-option token. If it's present and Commander didn't route
220
+ // to a subcommand (we got here, so it didn't), the user typed an
221
+ // unknown command. Exit 1. `program.args` works correctly under both
222
+ // `parseAsync(['node', 'peaks', ...])` (real CLI) and direct in-process
223
+ // calls (vitest integration tests), unlike `process.argv` which is
224
+ // the test runner's argv.
225
+ //
226
+ // --help and --version bypass this path entirely (handled below).
227
+ const firstNonOption = program.args.find((arg) => !arg.startsWith('-'));
228
+ if (firstNonOption !== undefined) {
229
+ // Commander reached this `.action()` despite a positional token,
230
+ // which means the token was NOT routed to any subcommand. This is
231
+ // the unknown-command path. Emit a JSON envelope + exit 1.
232
+ io.stdout(JSON.stringify({
233
+ ok: false,
234
+ command: 'cli',
235
+ code: 'COMMAND_NOT_FOUND',
236
+ message: `Unknown command: ${firstNonOption}. Run \`peaks --help\` for available commands.`,
237
+ data: { argv: firstNonOption },
238
+ warnings: [],
239
+ nextActions: ['Run `peaks --help` to list available commands.']
240
+ }, null, 2));
241
+ process.exitCode = 1;
242
+ return;
243
+ }
210
244
  const opts = program.opts();
211
245
  if (opts.V || opts.version) {
212
246
  // AC1: write the peaks-loop start log line BEFORE printing the
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "4.0.0-beta.11";
1
+ export declare const CLI_VERSION = "4.0.0-beta.12";
@@ -1 +1 @@
1
- export const CLI_VERSION = "4.0.0-beta.11";
1
+ export const CLI_VERSION = "4.0.0-beta.12";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.0-beta.11",
3
+ "version": "4.0.0-beta.12",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",