Skip to content

fix(cli): reject unknown arguments on every visible core command - #850

Closed
philcunliffe wants to merge 6 commits into
masterfrom
fix/issue-836
Closed

fix(cli): reject unknown arguments on every visible core command#850
philcunliffe wants to merge 6 commits into
masterfrom
fix/issue-836

Conversation

@philcunliffe

@philcunliffephilcunliffe commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Problem

Core commands did not share one argument-validation contract. Half the set parsed argv through parseCommandArgv() and exited 2 on a token it did not know; the other half hand-read the two flags it cared about and dropped the rest.

Measured on master at a14246d by dispatching every visible core command with --definitely-not-a-real-flag, sixteen accepted it:

behaviourcommands
ignored every argumentversion, query status, daemon stop, daemon restart
read only --jsonstatus, daemon status, backfill list, plugin list, plugin outdated, remote list
took the flag as a positional name (exit 0 or 1)plugin info, plugin remove, query schema, query refresh
filtered flag-shaped tokens outask
dropped unrecognised flags in positionals()/valueFlag()remote add, remote remove, remote login, report render/publish/list/get/delete

So hyp daemon status --jsn printed the human table and exited 0 while hyp sink maintain --jsn exited 2. A script cannot tell those apart, and around daemon stop, daemon restart, and report delete a dropped flag is a different operation, silently.

Fix

  • New src/core/cli/command_args.js: CORE_COMMAND_ARGS, one entry per migrated command holding both its parser schema and its registered usage line, plus parseCoreCommandArgv(), a thin wrapper over the existing parseCommandArgv() codec that writes the refusal in one place (exit 2, the token named on stderr, nothing on stdout; a non-leading --help prints usage on stdout and exits 0).
  • The sixteen commands above now parse through it. daemon start and daemon uninstall moved off their hand-rolled loops onto the same helper.
  • core_commands.js reads each covered command's usage from coreUsage(name) instead of holding a second copy, so the advertised line and the enforced schema are one declaration.
  • Where a command had better wording than the codec for a specific flag it is kept and the strict gate runs behind it: hyp remote login still says "--org expects an org name" (covered by existing tests), and the gate adds the refusal for everything no reader names at all.
  • hyp remote login's usage line becomes [name] rather than <name>, matching what it has done since LLP 0062 #bare-remote.

Regression test

test/core/cli-arg-validation.test.js (91 tests, all new):

  • Parameterized over every visible core command from the live registry: dispatch hyp <cmd> --definitely-not-a-real-flag and assert exit 2, the token named on stderr, and an empty stdout. On master 16 of these fail (hyp remote list ... exited 0, hyp plugin info ... exited 1, and so on) and hyp remote login hangs waiting on stdin; on this branch all 65 pass.
  • Usage/schema agreement, per CORE_COMMAND_ARGS entry: every --flag in the usage line is a non-positional schema property and every non-positional property appears in the usage line, plus every positional binds to a real property.
  • Every specced command registers exactly its spec usage line.

Design

llp/0266-core-command-argument-validation.decision.md records the contract; LLP 0009 carries the Extended-by: forward-ref. The doc is explicit that this is reuse of the argv codec, not the verb registration LLP 0034 #verbs bounded: these stay ctx.commands.register, contribute no MCP tool, and gain no render. It also records what was deliberately not done (generating usage lines from schemas, which cannot express hyp purge <path> | --session <id> | --ignored | --all) and that already-strict commands keep their schemas next to the code that reads their params.

Verification

  • npm test: 4287 pass / 22 fail. The 22 are identical to origin/master in this sandbox (missing optional deps: marked, @aws-sdk/*, parquet/iceberg backends). Baseline was 4196 pass / the same 22 fail, so this adds 91 passing tests and breaks nothing.
  • npx tsc -p tsconfig.json --noEmit: 7 errors, byte-identical to the origin/master baseline, all Cannot find module for deps absent here. None in changed files. (npm run typecheck itself cannot run in this sandbox: @types/node is not installed, on master too.)
  • Manual sweep through bin/hypaware.js confirming happy paths are unchanged (version, status --json, daemon status --json, daemon stop, query status, remote list --json, plugin list/outdated --json, backfill list --json, ask --list, query schema logs, remote add/remove) and refusals are uniform.

Acceptance criteria

  • Inventory every visible core command and its accepted positionals/options (the parameterized test enumerates the live registry; CORE_COMMAND_ARGS records the migrated surfaces, the LLP the rest).
  • Migrate remaining hand parsers to parseCommandArgv().
  • Reject unknown options and unexpected positionals with exit 2.
  • Preserve documented aliases such as -y and -f (untouched; the spec carries an aliases field and the commands that use them, plugin install/update, daemon run, keep their own).
  • Automated usage/schema agreement check.
  • Parameterized test proving every visible core command rejects a representative unknown flag.
  • Stdout purity preserved: the test asserts an empty stdout on every refusal, and hyp mcp is untouched.

One acceptance item is deliberately narrower than written: CORE_COMMAND_ARGS covers the commands migrated here, not the whole core set. Folding in commands that already parse strictly (init, sync, purge, policy *, attach/detach, plugin install) would mean moving their schemas away from the code that consumes their params, which is the drift this is against. They are held to the contract by the parameterized test instead. LLP 0266 #open-questions records this.

Fixes#836

Core commands did not share one argument-validation contract. Half the
set parsed argv through parseCommandArgv() and exited 2 on a token it did
not know; the other half hand-read the two flags it cared about and
dropped the rest, so `hyp daemon status --jsn` printed the human table
and exited 0 while `hyp sink maintain --jsn` exited 2. A script cannot
tell those apart, and around `daemon stop`, `daemon restart`, and
`report delete` the quiet reading is the dangerous one.
Sixteen commands were affected: version, status, ask, backfill list,
daemon status/stop/restart, plugin list/info/outdated/remove,
query schema/status/refresh, remote add/list/remove/login, and the
report subcommands (whose positionals()/valueFlag() helpers skip flags
they do not recognise rather than refusing them).
They now parse through parseCoreCommandArgv(), a thin wrapper over the
existing codec that writes the refusal in one place: exit 2, the token
named on stderr, nothing on stdout. Each command's schema and its
registered usage line are one CORE_COMMAND_ARGS entry, so core_commands.js
reads the usage string from the same declaration the parser enforces
rather than holding a second copy.
LLP 0266 records the contract and LLP 0009 carries the forward-ref.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe added a commit that referenced this pull request Aug 18, 2026
…#853)
`test/core/repo-scratch-hygiene.test.js` has been failing on `master` since it
merged: `x/npm-test.log` and `x/typecheck.log` are tracked, and the test's first
half asserts no `.log` is. Both came in on `adb448ab` (#785) via the `git add -A`
sweep that #786 wrote this test to catch; the files predate the test, so it was
red on arrival. Every branch cut since inherits it, currently blocking #833,
#849, #850 and #851 for a reason none of them caused.
The transcripts are deleted rather than the test relaxed, which is what its
message asks for. `.gitignore` needs nothing: `*.log` is already committed and
the rule test already passes, since an ignore rule cannot reach a path that is
already tracked. That asymmetry is the whole reason the file carries two tests.
Scope is exactly the two `.log` paths. The other eight files under `x/` are
untouched: the hygiene test forbids tracked transcripts and nothing else, and
sweeping up scratch it does not name would be a judgement this fix has no
authority to make.
Fixes#852
Co-authored-by: test <test@test.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
testand others added 2 commits August 19, 2026 00:19
`hyp query schema` is not a verb and has never rendered anything but its
plain-text listing, but three shipped agent-facing files told their reader
to run `hyp query schema <table> --format json` (and one `--format
markdown`). Before this branch the flag was silently dropped and the
listing printed; with the strict gate the same invocation exits 2 with an
empty stdout, so the skills' prescribed first step for an unfamiliar table
now fails.
Drop the flag from both hosts' `hypaware-query` skills and from the
`hypaware-analyst` agent, and record the consequence in LLP 0266. The two
skill trees are edited identically, so the host-divergence record is
unchanged.
Also re-attach the `@ref LLP 0266#usage-agreement` annotation: a blank line
sat between it and `CORE_COMMAND_ARGS`, which breaks attachment.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Review: fix(cli): reject unknown arguments on every visible core command

Verdict: findings (2, both fixed on the branch). The change itself is sound and I could
not fault the parsing work: parseCoreCommandArgv() is a thin, honest wrapper over the
existing codec, the sixteen migrated commands each read the same params they used to
hand-read, the two positionals()/valueFlag() re-reads left behind (remote login, the
report group) are gated rather than replaced and the wording they preserve is real, and
the parameterized test over the live registry is the right shape for this contract. What I
found is the blast radius of the new strictness, not the strictness.

Reviewed with the code-review skill at effort high; the forked run failed with an API
error on the first attempt and never returned on the second, so this is a hand review of
the full diff plus behavioural probes against a temp HYP_HOME on both this head and
origin/master.

Finding 1 (medium, fixed): the strict gate breaks a command line three shipped agent-facing docs prescribe

hyp query schema is not a verb. It has never rendered anything but its plain-text
listing (renderSchema(), src/core/query/schema.js:31), and on master it read
argv[0] and dropped everything else. Three shipped files told their reader to pass
--format to it anyway:

  • hypaware-core/plugins-workspace/claude/skills/hypaware-query/SKILL.md:17, :24, :78
  • hypaware-core/plugins-workspace/codex/skills/hypaware-query/SKILL.md:17, :24, :78
  • hypaware-core/plugins-workspace/claude/agents/hypaware-analyst.md:19, :30

Line 17 of both skills is the prescribed first step for an unfamiliar table, and line 78
calls it "the authoritative column reference".

Measured, same temp HYP_HOME:

origin/master $ hyp query schema logs --format json
dataset: logs
(no dataset registered - install a plugin that contributes it)
exit 0
this head $ hyp query schema logs --format json
hyp query schema: unknown flag --format
usage: hyp query schema <dataset>
exit 2, stdout empty

So an installed agent following its own skill goes from "prints the schema, ignores the
flag" to "prints nothing, exits 2". LLP 0266 #one-contract exempts the kernel render
controls only for the verb family (parseControlFlags), which is correct and does not
cover this command, so the docs are simply wrong and now wrong in a way that fails.

Fixed in 412c5a6c: dropped --format json / --format markdown from the three
files. The two skill trees are edited identically, so test/fixtures/skill-host-divergence.json
and the skill-host-parity guard are untouched. Added a matching bullet to LLP 0266
#consequences so the doc records the fallout of its own D1.

I deliberately did not teach query schema a --format flag. It would mean writing
JSON and Markdown renderers for output nobody has asked for, and advertising a flag the
command implements badly is exactly the drift D2 exists to prevent.

Finding 2 (low, fixed): orphaned @ref on CORE_COMMAND_ARGS

src/core/cli/command_args.js:25 carried
@ref LLP 0266#usage-agreement in a block that was separated from
export const CORE_COMMAND_ARGS by a blank line and a second /** @type ... */ comment.
CLAUDE.md is explicit that a blank line breaks attachment, so the annotation bound to
nothing and /ref-check would report it orphaned.

Fixed in 412c5a6c: folded @type into the same block so the doc comment, the type,
and the ref all attach to the declaration. Typecheck still clean.

Checked and deliberately left alone

  • --remote wording on the report group. Bare hyp report list --remote used to
    reach resolveReportsTarget() and say --remote expects a target name (omit it to use the default target); the gate now answers first with --remote expects a value. Still
    exit 2, still names the flag, and the friendlier branch stays reachable through
    --remote= (empty inline value), which the gate accepts. Not worth a special case.
  • Double parsing in report_commands.js / remote_commands.js.runReportPublish,
    runReportList, runReportGet, runReportDelete and remoteLogin gate with the new
    schema and then re-derive their params with positionals()/valueFlag(), discarding
    gate.params. It is two parsers that must agree, which is a real maintenance hazard,
    but the schemas and the readers do agree today on every form I probed (--flag value,
    --flag=value, --flag= empty, missing value, extra positional), and LLP 0266
    #one-contract records the trade deliberately (keeping --org expects an org name over
    the codec's --org expects a value). Converting them is a bigger change than a review
    round should make.
  • remoteLogin returns { exitCode: 0, reason: 'ok' } for a non-leading --help
    (src/core/cli/remote_commands.js:459). reason: 'ok' conventionally means the login
    succeeded, but the type invariant in src/core/remote/types.d.ts:157 is only
    "exitCode === 0 iff reason === 'ok'", which holds, and the one caller that branches
    on reason (src/core/cli/wizard/join.js:194) passes [], so no --help can reach it.
    Cosmetic, not worth churn.
  • CoreCommandArgSpec.aliases is declared and threaded through
    parseCoreCommandArgv() but no entry sets it. Harmless forward slot; parseCommandArgv
    already defaults it.
  • Every documented invocation of a migrated command, swept across README.md,
    docs/, the smoke flows, and both plugin skill trees. Apart from finding 1, all of them
    (hyp status --json, hyp daemon status --json, hyp plugin list --json,
    hyp remote login --org <name>, hyp report get --output, ...) are inside the new
    schemas.
  • Internal callers.runStatus([], ctx) (src/core/commands/init.js:123) and
    runDaemonStop([], ctx) (src/core/commands/daemon.js:246) pass empty argv, so nothing
    in-process trips the gate.

Checks

Run in a clean worktree off this head, with the repo's real node_modules available:

  • npm test: 4574 pass, 0 fail, 1 skipped (4575 total), before and after my commit.
    The PR body's "22 fail" is a sandbox artefact of missing optional deps, not a real
    baseline failure.
  • npx tsc -p tsconfig.json --noEmit: 0 errors, before and after.
  • Smokes cli_bundled_plugins_activated, status_diagnostics,
    walkthrough_picker_to_first_query, package_bin_boot: all ok.
  • No em dash and no semicolon introduced anywhere in the diff.

Pushed

412c5a6c on fix/issue-836.

…hat the body reads
- An unaliased single-dash token was still a positional value, so
`hyp query refresh -f` looked up the dataset '-f' (exit 1) and
`hyp report get k p id -o out.html` fetched the artifact 'out.html'
instead of writing the file. `parseCoreCommandArgv` now opts into
`strictShortFlags`; the verb family keeps the lenient reading for
greedy positionals that legitimately carry '-1'.
- `report list` and `report delete` gated on the schema and then re-read
argv, so `--json=true` / `--yes=true` validated and were then dropped.
Both read the gate's params now.
- `remote login`'s help path returned reason 'ok', which tells a
LoginOutcome reader the sign-in succeeded. It gets its own 'help'.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe pushed a commit that referenced this pull request Aug 19, 2026
0266 is also claimed by fix/issue-836 (PR #850) and
update/icebird-squirreling-native-batches (PR #866); 0267 is also
claimed by fix/issue-837 (PR #849). Both of those PRs are older, so
this branch yields the numbers. 0276 and 0277 are free across master
and every open branch.
Mechanical renumber only: no content change (LLP 0156).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

philcunliffe commented Aug 19, 2026

Copy link
Copy Markdown
ContributorAuthor

Review round on 412c5a6c

Verdict: sound migration, four gaps, all fixed on this branch in 86071050. The 25 spec entries all have a gate wired in, no migrated command reads an argv flag its schema omits, every internal caller (runStatus([], ctx), runDaemonStop([], ctx), remoteLogin([], ...) from the wizard, ctx.commands.run('claude-desktop install', ...)) passes argv the gate accepts, and the sweep for now-rejected invocations in docs/, README.md, both skill trees, and the smoke flows came back empty (412c5a6c already caught the query schema --format json class). Findings below are gaps in coverage, not breakage.

1. medium - the contract was only proven against ---prefixed tokens

test/core/cli-arg-validation.test.js:23 used a single probe, --definitely-not-a-real-flag. But argvToParams treats only-- tokens as flags (src/core/cli/verb_codec.js:154); a single-dash token is pushed onto the positional list. So on every migrated command that binds a positional, an unknown short flag was silently taken as a value rather than refused with exit 2. Measured on 412c5a6c:

parseCoreCommandArgv('query refresh', ['-f']) -> { ok: true, params: { dataset: '-f' } } // then exit 1, "unknown dataset '-f'"
parseCoreCommandArgv('plugin remove', ['-f']) -> { ok: true, params: { plugin: '-f' } }
parseCoreCommandArgv('remote remove', ['-f']) -> { ok: true, params: { name: '-f' } }
parseCoreCommandArgv('report get', ['weekly','2026-W01','abc','-o','out.html'])
-> { ok: true, params: { path: ['-o','out.html'] } }

Concretely hyp report get weekly 2026-W01 abc -o out.html fetched the artifact named out.html to stdout instead of honoring an output file or refusing: exactly the "a script cannot tell those apart" failure this PR exists to close. hyp status -j exited 2 only because status binds no positional.

Fixed.argvToParams/parseCommandArgv take a strictShortFlags option (src/core/cli/verb_codec.js:138-160,213-224) and parseCoreCommandArgv opts in (src/core/cli/command_args.js:265-272). The verb family keeps the lenient reading deliberately: a greedy SQL positional legitimately carries -1. Test coverage added: the short-flag probe -Z is now parameterized over every CORE_COMMAND_ARGS entry, plus a direct case proving report get ... -o out.html refuses while --output out.html still binds (test/core/cli-arg-validation.test.js:90-115). LLP 0266 #one-contract records the rule and #open-questions records what it still does not cover.

2. low - report list gated on the schema, then re-read argv

src/core/cli/report_commands.js:267 selected its output mode with argv.includes('--json') while the gate declares json: { type: 'boolean' }. The codec accepts the inline form, so hyp report list --json=true validated with params.json === true and then printed the human table and exited 0: an argv the new validator blessed, honored differently by the body.

Fixed. Reads gate.params.json === true, the same shape runStatus/runPluginList/runRemoteList already got in this PR.

3. low - same double-parse in report delete

src/core/cli/report_commands.js:368: gate declares yes and accepts --yes=true, confirmation checked argv.includes('--yes'). hyp report delete <kind> <period> <id> --yes=true in a non-TTY CI job validated cleanly and then died with refusing to delete without confirmation - pass --yes, naming the flag just passed. Fail-safe direction, but confusing.

Fixed. Reads gate.params.yes !== true.

4. low - remote login's help path reported a successful sign-in

src/core/cli/remote_commands.js:456: reason: gate.code === 0 ? 'ok' : 'usage' mapped the help path (code: 0) to 'ok', which per src/core/remote/types.d.ts means the sign-in succeeded. hyp remote login prod --help printed usage and told any LoginOutcome consumer it had logged in. Unreachable today (wizard/join.js calls remoteLogin([], ...)), but it is a new hole in the documented exitCode === 0 iff reason === 'ok' invariant the wizard branches on.

Fixed. The help path returns a distinct reason: 'help'; LoginOutcomeReason gains the member and the interface docblock states the exception in both directions (the other being daemon_incomplete).

Out of scope, recorded not fixed

12 of 64 visible core commands still bind an unknown short flag as a positional rather than refusing it: backfill, backfill plan, ignore, unignore, plugin install, plugin update, policy show, policy unset, query maintain, query sql, sink maintain, sync. Four of them (policy show, policy unset, query maintain, unignore) exit 0. These are the already-strict commands the PR deliberately left out of CORE_COMMAND_ARGS, they behave the same on master, and widening the rule means passing the option at each call site with a look at whether a value there can start with a dash. LLP 0266 #open-questions item 2 now carries it.

Checks

  • npm test: 4600 pass / 0 fail / 1 skipped (the 22 failures the PR body reports are missing optional deps in that sandbox; with node_modules present they pass).
  • npm run typecheck (tsc -p tsconfig.json --noEmit): clean, no output.
  • Fixes verified present in the committed tree via git diff 412c5a6c..HEAD -- <file> for each of the seven files touched.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Triage at head 8607105: the review-round cap was reached, so the residual findings were classified. Both review rounds' findings (rounds on 151ea60 and 412c5a6) are verified fixed at this head; the regression test file passes 117/117 here. The one remaining item, unknown short flags still binding as positionals on the 12 already-strict commands outside CORE_COMMAND_ARGS, is pre-existing on master and deliberately recorded in LLP 0266 #open-questions item 2, so it is non-blocking. Deferred to #903. This PR can merge safely.

@philcunliffephilcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 19, 2026
bgmcmullen added a commit that referenced this pull request Aug 19, 2026
…onfig is not a reconfigure (#874)
* LLP 0266: a hidden picker row stays off the sync gate too
On every enrolled machine the sync gate led with the two hidden
raw-proxy rows (raw-anthropic / raw-openai) wearing the fleet label,
because @hypaware/ai-gateway sits in the central layer so they classify
locked - while the picker had deliberately never offered them. The lane's
locked descriptors now go through the same visiblePickerDescriptors
filter the pick lane uses, at the screen, never at the locked set (which
would re-compose the org gateway into the local layer, LLP 0129).
The no-candidates short-circuit splits accordingly: with no visible org
row to name, it says nothing syncs instead of naming the fleet as owner
of an empty list.
Extends LLP 0202 and supersedes its "sync/opt-out menu is unchanged"
consequence line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* LLP 0267: an answer-less config does not make a reconfigure
hyp remote add before the first hyp init writes a config holding only
query.remotes. The pick phase classified any readable config file as a
reconfigure, so that run seeded from an empty read-back instead of from
detection: every box arrived unchecked, no defaults gate rendered, and
the export default quietly flipped to keep-local.
The classification now keys on whether the config records a pick answer,
discriminated by the plugins key: the composer always writes a plugins
array, the side-channel writers never do. An answer-less config seeds
like no config at all - detection pre-checks, gates render, export takes
the first-run local-parquet default - while its own keys still carry
through the composition fold. plugins: [] stays a reconfigure: an
emptied install must not be re-consented from detection.
Extends LLP 0183.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Review round: hidden rows off the sync gate's candidate list too, and the no-candidates line stops claiming nothing syncs
Two defects in the LLP 0266 half of this PR, both on the sync gate.
The locked list went through `visiblePickerDescriptors`; the candidate
list did not. A carried hidden row (LLP 0202 #carry-through) reaches
`picked.descriptors` whenever that row is not locked - a team join whose
org config has not converged, or a machine whose central layer does not
declare `@hypaware/ai-gateway` - and the gate then rendered it as an
editable checkbox for a row the picker deliberately never offered, where
unchecking it writes a `local-only` entry for a source the user never saw.
Both row lists now take the same filter, which is what makes LLP 0266's
"absent from every wizard screen" true rather than half true.
The new no-candidates line then said "nothing syncs to your server" on
exactly the machine class LLP 0266 targets: an enrolled machine whose
locked set is entirely the hidden `raw-*` pair filtered out of the
display. Those rows are still locked, still composed by the org's central
layer, and under LLP 0188 #locked they always sync and can never be opted
out - so the sentence traded LLP 0202's over-disclosure for an
affirmatively false claim about what leaves the machine. The branch now
splits three ways on `lockedHidden`, a count the lane gets so it can tell
the truth about withheld rows without being able to name them.
LLP 0266 §sync-gate, §no-candidates, and §consequences updated to match.
* Renumber LLP 0266/0267 to 0276/0277 to clear number collisions
0266 is also claimed by fix/issue-836 (PR #850) and
update/icebird-squirreling-native-batches (PR #866); 0267 is also
claimed by fix/issue-837 (PR #849). Both of those PRs are older, so
this branch yields the numbers. 0276 and 0277 are free across master
and every open branch.
Mechanical renumber only: no content change (LLP 0156).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Sync gate must not claim nothing syncs while a hidden picked row stands
The LLP 0276 filter takes hidden rows off both sync-lane lists, but only
the locked list reported how many it removed. A carried hidden row (LLP
0202 #carry-through) that is not locked reaches picked.descriptors, is
composed into the local layer, and syncs unless an opt-out entry says
otherwise - and when it is the only pick, the filter empties `candidates`
with `locked` empty and `lockedHidden` 0, so the lane took the strongest
of its no-question sentences and told the user "nothing syncs to your
server" while capture was in fact leaving the machine. That is the
affirmatively false claim LLP 0276 #no-candidates ruled out for the
locked case and did not carry to the candidate case.
`runInitWizard` now passes `candidatesHidden` alongside `lockedHidden`,
one count per filtered list, and the no-question branch gains a fourth
line: with no locked row but a hidden pick standing, it states that
capture already set up on this machine still syncs, naming neither the
row nor the fleet (which does not own it).
LLP 0276 #no-candidates extended with the case and the sentence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: neutral <neutral@hyperparam.app>
Co-authored-by: test <test@example.com>
@philcunliffephilcunliffe removed the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 19, 2026
test added 2 commits August 19, 2026 06:59
# Conflicts:
#	src/core/commands/query.js
# Conflicts:
#	src/core/commands/status.js
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

⚠️Merge note: this PR is fully contained in #914

Both PRs are mergeable and green right now, and merging both will conflict. Re-derived from git, not inferred:

  • git log fix/issue-836..fix/issue-903 --no-merges1 commit (0f3c8d3c, "the short-flag rule reaches the commands that parse the codec directly").
  • git log fix/issue-903..fix/issue-836 --no-merges0 commits. This PR has nothing Widen the short-flag refusal to the core commands that parse the codec directly #914 lacks.
  • git diff fix/issue-903 fix/issue-836 is exactly the reverse of 0f3c8d3c (+38 / -107 across 11 files).
  • git merge-tree fix/issue-836 fix/issue-903conflicts.

So #914 is this PR's tree plus one more commit. That is expected: #903 is the deferred-findings follow-up to this PR (#850), and its branch carried this work forward rather than branching from a merged base.

What this means for the merge button: merging #914 alone delivers everything here. Merging this one too, in either order, hands you a conflict on the second. neutral is not choosing between them, and has changed nothing on either branch beyond the base merges already reported.

Filed by neutral during reconcile. Reply here if you want it to act on this rather than just report it.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Triage at head 82ca71a: the review-round cap was reached, so the residual findings were classified against the current tree.

All six findings from the two review rounds (on 151ea60 and 412c5a6) are verified still fixed at this head, which differs from the last-triaged 8607105 only by two merges of origin/master:

  • query schema docs no longer advertise --format (no matches in either skill tree or the analyst agent doc)
  • the @ref LLP 0266#usage-agreement annotation attaches to CORE_COMMAND_ARGS (src/core/cli/command_args.js:26)
  • strictShortFlags refusal wired through the codec and opted into by parseCoreCommandArgv (src/core/cli/verb_codec.js:159, src/core/cli/command_args.js:272)
  • report list and report delete read gate.params (src/core/cli/report_commands.js:269, :372)
  • remote login --help returns reason: 'help' (src/core/cli/remote_commands.js:460, member present in src/core/remote/types.d.ts)

Verification at this head: test/core/cli-arg-validation.test.js passes 117/117, and the full npm test passes 4645/4645 (1 skipped, 0 fail), so the base merges regressed nothing.

The one remaining item, unknown short flags still binding as positionals on the 12 already-strict commands outside CORE_COMMAND_ARGS, is pre-existing on master, behaviourally unchanged by this PR, and recorded in LLP 0266 #open-questions item 2. It is a scope deferral, not a production risk introduced here. Deferred to #903.

This PR can merge safely. Note the standing merge-coordination report above: #914 fully contains this tree plus one commit, so a human should merge one of the two, not both.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Closing as contained by #914. #914 carries this argument-validation work plus the remaining codec-parser coverage; only the surviving PR should be rebased onto the new CLI.

@philcunliffe
philcunliffe deleted the fix/issue-836 branch August 25, 2026 02:11
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approvedneutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standardize core command argument validation and usage generation

1 participant

@philcunliffe