Repository navigation
fix(cli): report missing projects with one closed code and refuse empty native run selections - #394
Merged
Merged
Conversation
…ty native run selections - `ps`, `logs`, `run`, `exec` and `open` now report a path without a project as `E_PROJECT_NOT_FOUND`, the code `up`, `down` and `restart` already used, instead of `E_UNEXPECTED` (and a stack trace without `--json`). - Native `hack run --env ""` and `--profile ""` (or whitespace/comma-only values) refuse with a closed `E_USAGE` line before any runtime request, instead of silently meaning base env or no profiles. - Remove the redundant legacy-env clause in `requireNativeRunSelection`. - Cover the scan's lone `-` positional and subcommand-after-positional branches; update closed-error tests to current refusal text. - Document the per-command option parsing edge and the #370 parse differences in docs/cli.md, and the empty-value rule in the native guide. Refs HACK-1339 Refs HACK-1338 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
Found 1 test failure on Blacksmith runners: Failure
|
This was referenced Oct 22, 2026
roodboi
marked this pull request as ready for review
October 22, 2026 19:39
roodboi
added a commit
that referenced
this pull request
Oct 23, 2026
…t commands (#427) Follow-ups to #394 (merged into `next` as d0c8184) for HACK-1371. ## Rework (4d30eff): hosted `test` failure on e3d6917 **Root cause.** Fix 4 below moved the empty `--env`/`--profile` check ahead of authored dispatch, so `hack run --profile ""` on an authored native project now refuses with `E_USAGE`, not `E_NATIVE_PROJECT_UNSUPPORTED`. The source CLI run table in `tests/native-authored-project- oneoff.test.ts` still expected `E_NATIVE_PROJECT_UNSUPPORTED` for the `["--profile", ""]` row. That failure also set the file's `uncertain` latch, so 4 later tests in the file failed as well (`--profile dev`, `--env base`, "public cancellation after run completion returns130…", and "unconfirmed run completion maps to the closed run refusal"). Hosted run 39846392499 reported 5 failures. My earlier focused run (33 files) did not include this file. **Fix (test only).** Each row of that table now carries its expected refusal: - The `--profile ""` row expects `ERROR: E_USAGE: ${NATIVE_RUN_EMPTY_SELECTION_REFUSAL}` and no runtime attempt. - The `--profile dev` and `--env base` rows keep `E_NATIVE_PROJECT_UNSUPPORTED` and no runtime attempt. No source file changed. **Proof.** - `bun test tests/native-authored-project-oneoff.test.ts`: 50 pass, 0 fail. Before the fix: 45 pass, 5 fail. - Mutation: I swapped in `src/commands/project.ts` from `origin/next` (old dispatch order), and the `--profile ""` row failed. The two later rows then failed through the existing latch, as designed. I restored the file afterwards. - I searched the other tests for an empty `--env`/`--profile` expecting `E_NATIVE_PROJECT_UNSUPPORTED`. Only the `logs` and `exec` tables do that, and the early refusal applies only to `run`. `native-authored-project-logs` and `native-authored-project- exec` pass 91/91. ## Root cause (original commit e3d6917) 1. **Commands still reported a missing project with different codes.** #394 gave `project.ts` a closed `MissingProjectContextError`, but other command files kept their own copy of `requireProjectContext`. For `hack <cmd> -p <empty dir>`: - `branch add/list/remove/open`, `config get/set` and `project owner show` threw a plain `Error`. They printed `ERROR: Error: ...`, a stack trace, and a stray `└` line on stdout. `project owner show --json` reported `E_UNEXPECTED`. - `env ...` and `host exec/shell` threw `CliUsageError("No .hack/ found. Run: hack init")`. That reported `E_USAGE` and added a "See --help" hint. - `tui` (with a TTY) threw a plain `Error`. `docs/cli.md` said every project command reported `E_PROJECT_NOT_FOUND`, which was not true. 2. **The docs gave the wrong refusal for a short spelling.** `docs/cli.md` said an option that only another command declares is refused as `Option(s) not valid for "<command>"`. That is true only for long spellings. The per-command parser (#370) reads another command's option only by its long name, so a short spelling the command does not declare is unknown to it. For example, `hack doctor -f --json` gives `Unknown option '-f'. ...`. The old union parser gave `Option(s) not valid for "doctor": --follow` instead, and the #370 differential notes did not record that difference. 3. **A mutant survived.** Changing `.every(...)` to `.some(...)` in `requireNonEmptyNativeRunOptions` survived, because no test passed a profile list that mixes empty and real entries. 4. **Authored projects got the wrong refusal.** On the native backend, `hack run` ran authored dispatch (`tryNativeComposeCommand`) before the empty check. So an authored `hack.project.json` project refused `--profile ""` with `E_NATIVE_PROJECT_UNSUPPORTED` instead of `E_USAGE`. ## Fix 1. I changed the commands to match the doc instead of narrowing the doc. A new `src/lib/missing- project-error.ts` exports `MissingProjectContextError` (`E_PROJECT_NOT_FOUND`, message `No .hack/ (or legacy .dev/) found. Run: hack init`). `project.ts`, `branch.ts`, `config.ts`, `env.ts` (which covers `host`), `project-admin.ts` and `tui.ts` now throw it. `docs/cli.md` lists the commands and the documented exceptions: - `tui` without a TTY refuses for that reason first. - `env get` prints one fixed stderr line, because its output is raw values. - `config validate/plan/import/adopt` are native configuration commands and keep their own refusals. - `project target/task` use the same code with their own message. - `init`, `doctor`, `crash-capture`, `setup`, `mcp` and `agent` do not need a project. 2. I documented the short-spelling refusal in "Option parsing", and added it as a second difference in the #370 old- versus-new notes. I checked the old behaviour by running the pre-#370 tree (a25160d^) with the same arguments. Making the two match would mean reading other commands' short flags again, which brings back the `-p`/`-d` conflict that #370 removed. So I documented the difference instead. 3. No source change; new tests pin the behaviour (see Tests). 4. I moved the empty check earlier, into `refuseEmptyNativeRunSelection`, which `handleRun` calls first when `HACK_RUNTIME_BACKEND=native`. Authored, adopted and legacy projects now all refuse an empty value with the same `E_USAGE` line, before any project is selected. The Compose backend is unchanged: there an empty `--env` still means the base layers. `docs/guides/native-candidate.md` states which check runs first and the `web,` rule. `docs/reference/cli.md` is unchanged, because `bun run docs:cli-reference` produced no diff (the help output did not change). ## Tests (each proven by mutation) - `tests/missing-project-error-code.test.ts` was rewritten: - A table classifies every command that declares `--path`, and one test asserts that the table equals the set of commands walked from `CLI_SPEC`. A new `--path` command therefore fails until someone classifies it. Mutation: dropping the `crash-capture` row fails the enumeration test. - Each project command is spawned against an empty dir (`privateCliEnv`, temp HOME) and must print the exact envelope or line, with no stack trace. The documented exceptions are pinned to their exact output. - Mutations: restoring the `origin/next` version of `branch.ts` fails 4 tests, `config.ts` fails 2, `env.ts` fails 12 (env and host), and `project-admin.ts` fails 1. - `tests/cli-renderer-loading.test.ts`: the `tui-missing-project` control (forced TTY) now expects `ERROR: E_PROJECT_NOT_FOUND: No .hack/`. Mutation: restoring the `origin/next` version of `tui.ts` fails it. - `tests/cli-per-command-options.test.ts`: every short spelling that some other command declares but `doctor` does not (`-f`, `-d`, `-c`, `-H`, `-u`, `-t`) throws `Unknown option '<short>'` on `doctor`. `doctor --follow` still parses, so the CLI can say it is not valid. Mutation: letting `optionTableFor` also read other commands' short spellings fails it. - `tests/native-run-selection-cli.test.ts`: - `--profile "toolchain,"`, `",toolchain"` and `" , toolchain , "` run against a project started with `toolchain`. - `--profile "web,"` against `toolchain` gets the mismatch refusal, not the empty one. - Mutation `.every` to `.some`: all 4 fail. - `tests/native-authored-command-dispatch.test.ts`: on an authored project with the native backend, `run` with `--profile ""`, `--profile " , "`, `--env ""` or `--env= ` prints exactly `ERROR: E_USAGE: <NATIVE_RUN_EMPTY_SELECTION_REFUSAL>`, and neither the compiler nor the runtime runs. Mutation: disabling the early check fails all 4 (they report `E_NATIVE_PROJECT_UNSUPPORTED`). - `tests/native-authored-project-oneoff.test.ts`: the source CLI run table, as described in Rework. ## Evidence - Bun 1.4.2 (`mise x bun@1.4.2`). Base `origin/next` d0c8184, which includes a25160d. - At 4d30eff: `bun run --cwd packages/cli typecheck` has 0 errors. Root `bun run check` has 0 errors and prints `privacy-check: ok`. `biome check` is clean on the changed test file. - Focused suites at 4d30eff, run on this Mac: - The 6 changed test files: 155/155 pass. - `native-authored-project-logs` and `native-authored-project-exec`: 91/91 pass. - An earlier run at e3d6917 passed 436/436 across 33 files: the branch, config, env, host, project owner/target/task, MCP, lifecycle-json, CLI parsing, help and reference-docs, renderer loading, closed-error output, native run selection, native one-off and authored dispatch suites. That run did not include `native-authored-project-oneoff`, which is how the failure got through. - `bun run docs:cli-reference`: no change. No consumer agent guidance or `package.json` scripts changed. - gitleaks 8.30.1 (`gitleaks dir` over a `git log -p origin/next..HEAD` export of both commits): no leaks. - Hosted CI at 4d30eff (run 39854071300): the `Tests (Turbo)` step of the `test` job passed. That step runs the full suite on a macOS runner. Every other job that had finished when I wrote this also passed; see the checks tab for the final state. - Full suite on the M3: **not run.** At 17:01 I queued it at exactly 4d30eff behind the shared `m3-ci/.lock`. M3 had about 67 GiB free then. The lock was held first by G41, then from 17:39 by G52, whose 10-round loaded loop was still running at 20:28. About a dozen other units were also waiting, some for over 9 hours. I did not break a lock I don't own. At 20:30 I stopped my own waiter, which never took the lock and never created a worktree or ref. The hosted `Tests (Turbo)` full-suite step passed at this exact head. ## Gaps - The M3 full suite has not run at 4d30eff, because the shared lock queue never reached it (see Evidence). The hosted macOS `Tests (Turbo)` step ran the full suite at this head and passed. - On the Compose backend, an authored native-Compose `run` still resolves `--env ""` as the base env. This is unchanged and documented. - `remote` and `gateway` (unsupported) and `internal` keep their own missing-project errors. - `tui` with a real TTY is covered only by the renderer-loading fixture, which forces `isTTY`. Refs HACK-1371 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Small CLI follow-ups to #368 and #370 (HACK-1339, and the code parts of HACK-1338; not the M3 host gate).
Root cause
requireProjectContextthrew a plainMissingProjectContextError. Onlyup,downandrestartcaught it and mapped it toE_PROJECT_NOT_FOUND. Every other project command let it reach the top-level handler, which reported it asE_UNEXPECTEDunder--jsonand, without--json, printed a stack trace. Before this change:hack ps -p <empty dir> --jsonandhack logs -p <empty dir> --jsonreportedE_UNEXPECTED.hack up -p <empty dir> --jsonanddownreportedE_PROJECT_NOT_FOUND.hack run --env ""resolved to the base env (null) and--profile ""resolved to no profiles ([]). Each was then compared with the running selection. Against a project started with the base env and no profiles, both silently matched and the command ran. Against any other selection, they were refused with the "does not match" message. So an empty value meant different things depending on what was running.Fix
MissingProjectContextErrornow extendsHackCliErrorwith codeE_PROJECT_NOT_FOUND. Most commands already used that code, and the MCP and target-inspect contracts use it too.ps,logs,run,execandopennow report the same envelope asup,downandrestart. Without--jsonthey print one closedE_PROJECT_NOT_FOUNDline and no stack. The existingup/down/restartcatch sites are unchanged.Native
run: an empty--envor--profileis a closedE_USAGEusage error. Values that contain only whitespace (--env " ") or only commas (--profile " , ") count as empty. The check runs inrunBackendSelection, before the value is resolved, the project is loaded, or any runtime request is made. The error message is exported asNATIVE_RUN_EMPTY_SELECTION_REFUSAL:Omitting both options still inherits the running selection. The Compose
runpath is unchanged: there, an empty--envstill means the base layers.requireNativeRunSelection: removed the redundantrun.effectiveEnvName === undefined ||clause (item 7). A legacy mapping with no recorded env still refuses:undefinednever equals a given selection, which is always a string ornull. The existing legacy-mapping unit test still covers this.Docs:
docs/cli.mdhas a new "Option parsing" section. It describes per-command parsing and the residual edge (item 3): an option placed before the command name that is a flag in one command and takes a value in another is read as a flag. It also records the parse differences that fix(cli): parse options per resolved command so -p means --path again #370's old-versus-new comparison found (item 4). The only difference was the intended one:-p <dir>on a project command now sets--path. The section lists the edge cases that parse the same as before.docs/cli.mdnow states the missing-project code.docs/guides/native-candidate.mdstates the empty-value rule.Tests (each checked by mutation)
New
tests/missing-project-error-code.test.ts. It spawns the source CLI withprivateCliEnvand checks two things:ps,logs,up,downandrestartwith-p <empty dir> --jsonall return{ok:false, error:{code:"E_PROJECT_NOT_FOUND", ...}}.pswithout--jsonprints exactly oneERROR: E_PROJECT_NOT_FOUND: ...line and no stack.Mutation: reverting the class to a plain
Errorfails theps,logsand non-JSONpstests.tests/native-run-selection-cli.test.ts:--env "",--env=,--env " ",--profile ""and--profile " , ". They run against a project started with the base env and no profiles, where the old code would have run the command. Each must print one closedE_USAGEline and make no runtime call.Mutations:
""profile fails the comma case.tests/cli-per-command-options.test.ts(item 2): I mutated each branch ofscanInvocation,scanPositional,scanOption,consumesNextToken,omitsOptionValueandrepresentativeDeclarations, then ran the parser suites against each mutant. Two non-equivalent mutants survived, and I added a test for each:-is a positional, so it can start the child command. Mutation: treating-as an option.subcommandsafter a positional.Both new tests fail under their mutation. Two mutants still survive, and both are equivalent:
includes("=")early return inscanOption: no long or short key can contain=."done": no subcommands remain, and no passthrough index is set.tests/cli-closed-error-output.test.ts(item 6):runhandler's currentCliUsageError("Missing required argument: service").NATIVE_RUN_SELECTION_REFUSAL, raised by the realrequireNativeRunSelection, as one codedE_USAGEline with no hint.--jsonusage test's stale "Native exec does not support --env." text is updated to the current exec refusal.Evidence
mise x bun@1.4.2), baseorigin/next16f4547 (which includes a25160d).bun run --cwd packages/cli typecheck: 0 errors. Rootbun run check(includesprivacy:check: ok): 0 errors. The changed files passbiome checkcleanly.cli-per-command-options,cli-closed-error-output,native-run-selection-cli,missing-project-error-code,native-project-one-off,cli-passthrough-argv,cli-command,project-up-command,lifecycle-json,project-run-command,native-project-exec,cli-reference-docsandoutput-hygiene. Also 44/44 acrossenv-key-argument-redaction-cli,cli-renderer-loading,integration-ownershipandmcp.bun run docs:cli-reference: no change todocs/reference/cli.md, because the help surface is unchanged. No consumer agent guidance changed, so I did not regenerate the plugins. Nopackage.jsonscripts changed, so nothing needed re-pinning.gitleaks dirover agit log -p origin/next..HEADexport): no leaks.bun run build:config-compiler): 7691 pass, 126 skip, 4 fail, across 549 files in 2750.78s. None of the 4 failures touch this change, and the same 4 also fail on unrelated branches run on the same M3:repository catalog > the checked-in .hack/tasks.json recipes are all reviewed. On the M3 it fails withE_USAGEdetached_checkout("Detached linked worktree cannot select an instance implicitly"), because the M3 harness checks out a detached worktree. It passes locally on a branch checkout at this head. It also failed this way in the F20r3, G35, G36 and G41 runs.native-authored-process-incarnationandnative-authored-project-recovery("original capture uses its live session…", "new no-host death uses real read-only quiescence facts…" and "new no-host recovery retains original bindings on last-await"). These are timing-dependent. The identical set failed in the F20r3 run, and they also show up in the G35 and G41 runs.--timeout 60000at this head. The shared M3 CI lock stayed busy for over 100 minutes, so I cancelled the reruns before they got the lock. No lock or worktree was left behind.next(8e53df3), which merges cleanly: 68/68 acrossmissing-project-error-code,native-run-selection-cli,cli-per-command-options,cli-closed-error-output,project-run-commandandlifecycle-json.pattern-p01-dependency-bootstrapwithdocker inspect <id>, thenError: No such object: the fixture container disappeared between listing and inspect. That scenario does not use the code this PR changes. I did not re-run the job.Gaps
HACK_RUNTIME_BACKEND=native,nativeProjectOneOff). Two paths are unchanged:runpath (tryNativeComposeCommand), which prepares a new generation, still resolves--env ""as the base env.run, where an empty--envstill means the base env.Refs HACK-1339
Refs HACK-1338
🤖 Generated with Claude Code