Repository navigation
fix(cli): pass child flags through and print closed native refusals - #359
Merged
Merged
Conversation
Flags after a child command now belong to the child: `hack run web bun --version` no longer prints Hack's version. Positionals marked `passthrough` (run, exec, host exec, env exec, session exec, x) end Hack's option parsing at the child's first word; a leading `--` stays optional and a later `--` reaches the child unchanged. Known native refusals print one closed line instead of a stack: the configuration refusal is E_NATIVE_PROJECT_UNSUPPORTED, the shared-source grant is E_PERMISSION, and the remaining value-free native start, run, exec, down and restart refusals are ClosedCliError. Usage refusals thrown by a handler print the message and a one-line help pointer instead of the full help. Native up --detach/--json/--target/services refuse by name with the supported foreground form. Authored native up checks runtime status before reserving its startup intent, so a stopped runtime refuses with E_DOCKER_UNAVAILABLE and leaves ps at not_started instead of pending. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A child `--json` no longer selects Hack's JSON error envelope: only argv before the passthrough child command and before the first `--` counts. A cancel or deadline during the authored start's runtime status check now refuses before the startup intent is reserved, so ps stays not_started. A start that fails because authored foreground control reports a stopped runtime wrapped in native_graph_foreground (#331) now reports E_DOCKER_UNAVAILABLE like the direct runtime_not_running code; the serve exit diagnostic carries the bounded cause code to the start error. Native project input and route refusals are E_NATIVE_PROJECT_UNSUPPORTED; branch, adaptation, AWS and fresh-exec environment refusals are E_CONFIG_INVALID; source admission and restart preflight refusals are closed. Each prints one line without a stack. `session exec` keeps its single quoted command: an unquoted command now refuses with a hint to quote it, and its help and docs say so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
force-pushed
the
wf/g1-cli-refusals-b17
branch
from
October 19, 2026 18:12
75e4fe5 to
c5b4202
Compare
Complete the native refusal audit on the image-project up, restart, run, exec and ps paths. Configuration refusals carry a stable code (E_CONFIG_INVALID, E_NATIVE_PROJECT_UNSUPPORTED or E_PERMISSION). Integrity, ownership and change refusals are closed, so the CLI prints one line without a stack. Start-time receipt, ownership, dependency-plan and HTTPS verification failures no longer reuse the configuration refusal. Mark the dispatch run command as passthrough, so a child --version reaches the child. Give authored up/restart --detach the image project's one-line E_USAGE refusal. Add a CLI spawn test with a stub runtime for representative refusal sites, class and code assertions for the round-1 conversions, a non-running phase preflight case, and a failedAdmission wrapped-cause test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On macOS, authored up --detach now refuses with the image project's one-line E_USAGE message. The native plan CLI acceptance scripts expected E_NATIVE_PROJECT_UNSUPPORTED on every platform; expect E_USAGE on macOS and keep E_NATIVE_PROJECT_UNSUPPORTED elsewhere. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… refusals Three image-project `up` refusals still printed `ERROR: Error: ...` with a stack. They now print one closed line: - An image reference the runtime refuses (`registry_image`) or an authored service without an image is `E_NATIVE_PROJECT_UNSUPPORTED`. A malformed `runtime admit-image` reply is a closed integrity error, and a canceled admission no longer reports a reference refusal. - An invalid HACK_NATIVE_PREPARED_BASE / _STORE selection is `E_CONFIG_INVALID`. - An invalid workspace cache layout in HACK_NATIVE_ADAPTATION is `E_CONFIG_INVALID`, like its sibling adaptation refusals. Spawn tests drive each case through the real CLI with a stub runtime. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings #355 (G4, HACK-1272: retire authored starts refused before journal reservation), #352, #341, #342 and #346 into the G1 closed-refusal branch. Both sides' behavior is kept. Conflict resolutions: - docs/reference/native-authored-commands.md: keep G1's explanation of `pending` (in-progress start or failed start with retained evidence; only the owner's cleanup proof may retire it) and append G4's pointer that `down --recover` retires an attempt that never reserved runtime state when no startup is running. - src/lib/native-authored-command.ts: the `pending` status note keeps G1's wording and adds G4's `hack down --recover` hint. Auto-merged code keeps G4's canceled `unreserved` exit 130 ahead of G1's stopped-runtime classification. Semantic fix: G1's stoppedRuntimeStartup message map had no entry for G4's new `unreserved` outcome, so a runtime_not_running start that was retired as unreserved would have produced an undefined message. It now reports E_DOCKER_UNAVAILABLE with an `unreserved` message (intent retired, `hack ps` reports not_started). G4's `down --recover` pending-start path already refuses through commandFailure as a closed coded HackCliError (E_LIFECYCLE_FAILED, values omitted), and unreserved startup failures map to E_STARTUP_INCOMPLETE or E_DOCKER_UNAVAILABLE, so no stack or help is printed. - tests/native-authored-project-start.test.ts: the fixture keeps G1's `stoppedDuringServe` and G4's `failBeforeReceiptCode` and `unreservedInspection` options. The stub serve emits G4's configurable failBeforeReceipt code and G1's wrapped stopped-runtime refusal. The stub inspect answers both failure modes, with `native_graph_unreserved` only when requested. The duplicate native-runtime-client import is merged into one. Semantic fix: G4's HACK-1272 call-log expectation now includes G1's read-only `runtime status --json` preflight between plan and serve. Both sides' tests are kept. - tests/native-authored-command.test.ts (auto-merged): add an `unreserved` case to the stopped-runtime startup table. Verification (Bun 1.4.2, macOS): - packages/cli typecheck and check: 0 errors. - native-authored-command, native-refusal-cli, native-authored-command-dispatch and cli-spawn-env-isolation: 124 pass. - Other native-authored-* and native-runtime-* suites: 409 pass, 5 fail. 2 are load-timing failures that pass when rerun alone. 3 are host live-session census failures in the recovery and incarnation suites, which fail identically on origin/next and ead8383 on this host. - native-authored-project-start (--timeout 60000): 48 pass. The 3 host-metadata cases are intermittent (HACK-1299). They also fail intermittently on origin/next and ead8383 and pass when run alone. - Six scripts/check-native-*-plan-cli.ts: all pass after build and build:config-compiler. - docs:cli-reference and generate:agent-plugins: no drift. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 21, 2026 17:54
roodboi
added a commit
that referenced
this pull request
Oct 21, 2026
#340 is now squash-merged into next (9a80dbd), so next and the F7 head (e99f68c) hold the same F7 text. Conflicts were resolved against that head as the effective base, keeping F8's changes and next's other work: - docs/env.md, src/agents/pattern-catalogue.ts, src/lib/project-env-config.ts: F8's text. Next's side was F7's text, which F8 had already extended (hydrate pointer, P08 card, CAS expectedExisting option). - src/cli/command.ts: keep #359's single-valued passthrough refusal exactly; other extra positionals are still reported by count only. - plugins/.../patterns.md: next's version, then regenerated (output equals F8's render). - src/lib/fs.ts merged without a conflict marker but duplicated writeTextFileAtomicIfChanged, AtomicWriteRefusedError and inspectAtomicWriteTarget; restored F8's single CAS-aware copy, which next's F7 version is a subset of. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…losed refusals (#365) ## Summary User decision (2026-10-19): in v5, unchanged real Compose projects such as Omega stay on the supported Compose backend. Native support for these features is post-v5 work (HACK-1270). This PR makes native `hack up` and `hack restart` refuse each blocking Compose feature exactly, before any mutation. It also documents the features and adds a `hack doctor` check that reports them. The explicit `HACK_NATIVE_ADAPTATION` path stays supported. A network that a valid adaptation lists in `isolatedNetworks` is not treated as an external network: the check classifies the Compose file after that adaptation, as native start does. ### Root cause Native image-project `up` (`handleNativeUp` → `startNativeProject`) had no early classification of Compose-only features: - **External networks** and **non-arm64 `platform`** were not checked on the TypeScript side. They failed only after `runtime check-project-share`, run-storage preparation, lifecycle `before` hooks and `runtime up` (the runtime-core review rejects `external_network`, and registry admission selects arm64 only). - **Builds** and **labels** were refused, but only as the general `NATIVE_CONFIGURATION_REFUSAL`. That came after the source preflight and storage preparation, and it did not say the project belongs on the Compose backend. ### Rework after verification (adaptation regression) The first version of this PR (9ce0f3b) read only the original `.hack/docker-compose.yml`. It ignored the documented `HACK_NATIVE_ADAPTATION` `isolatedNetworks` selection, which native start applies to the normalized copy (`replaceNetworks` sets `x-hack-isolated`). So `"isolatedNetworks": ["hack-dev"]` with an external `hack-dev` network was refused with the Compose-backend line before the adaptation was read. That blocked `up`, `restart` and `restart --recover`, and native doctor warned about it. On `next` the same project passes native review and reaches `runtime check-project-share` and `runtime up`. Fix (76003b5): - `findComposeBackendFeatures(compose, {isolatedNetworks})` does not count a network named in the set as external. Other features are unaffected. - `readComposeBackendFeatures(composeFile, {adaptationFile})` reads the selection only when the unadapted file has an external network. A project without one keeps its existing refusal order. - `readAdaptedIsolatedNetworks` reads the file with native start's own `readNativeSelection` (absolute path, bounded, unchanged regular file). If the file cannot be read, the selection is not version 1, or `isolatedNetworks` is not a bounded list of unique strings, it refuses with the same one-line `E_CONFIG_INVALID` message native start gives, before any runtime request. Native start still validates the whole selection against the normalized Compose file, including that each listed name is an external network. - `handleNativeUp` (covers `up`, `restart` and `restart --recover`) passes `process.env.HACK_NATIVE_ADAPTATION`, the same value it gives `startNativeProject`. Native doctor passes the same value. Compose doctor does not, because the adaptation is native-only. - Docs: the "External network" row, the refusal-contract paragraph and the doctor paragraph in `docs/guides/native-candidate.md` now describe the adaptation. The adaptation section links back to the Compose-backend section, and `docs/reference/native-config-import.md` qualifies its summary. The two sections no longer contradict each other, and `isolatedNetworks` is not retired. ### Fix (unchanged from the first version) - `src/lib/native-compose-backend-features.ts` classifies the project's Compose file into a closed, ordered set: `external_network`, `reserved_label`, `unsupported_build`, `foreign_platform`. - **External network**: top-level `external: true` or `external: {name}`, unless it is `x-hack-isolated: true` or isolated by the adaptation. - **Reserved ownership labels**: keys starting with `com.docker.compose.`, `io.hack-local.` or `io.hack.native-config.`, on service, network and volume labels. Routing (`caddy*`), cache (`hack.dependencies.*`), one-shot and other ordinary labels keep their native behavior. - **Build**: reuses the import preview's `mapLegacyComposeBuild`. The result matches what `hack config import --dry-run` reports as `invalid_or_unsupported_build` or `image_build_exclusive`. - **Platform**: anything other than `linux/arm64` or `linux/arm64/v8`. - `handleNativeUp` calls `refuseNativeComposeBackendFeatures` after option refusal and project selection, before branch, config, env-fingerprint, lifecycle, shared-source, storage and runtime work. `--detach` still gets its one-line `E_USAGE` first (#359). `down` is not affected. Authored and adopted native-config projects go through `tryNativeComposeCommand` first and never reach this check. - Each refusal is a `HackCliError` with the stable code `E_NATIVE_PROJECT_UNSUPPORTED` and one value-free line that names every feature found and the `HACK_RUNTIME_BACKEND=compose` selection. `detail` is `{backend: "compose", features: [...]}`. - Unreadable, oversized (over 256 KiB), symlinked or unparseable Compose files produce no features. Native input review still refuses them as before. - `hack doctor` has a `compose backend features` check, in both native and Compose doctor. On native, a feature found is a `warn`. On Compose it is informational (`ok`). With `--json`, the check carries `composeBackend.features` (closed names only). The CLI surface (options and help) is unchanged, so `docs/reference/cli.md` was not regenerated. ## Tests - `tests/native-compose-backend-features.test.ts` (unit, 13 tests). The original cases are kept. New in the rework: - An adaptation-isolated network is not external. A parity check runs the real `adaptNativeProject` on the same selection and classifies its normalized output the same way. A network that is not listed is still external, and isolation never hides another feature. - A file classified with a valid isolating selection has no features, while a selection that lists another network or has no `isolatedNetworks` leaves the external network. - Five invalid selections refuse with `E_CONFIG_INVALID` and the exact message. For each one, the real `adaptNativeProject` throws the same message. Without an external network the selection is not read. Missing and relative paths refuse. - `tests/native-compose-backend-features-cli.test.ts` (real CLI through `privateCliEnv`, stub runtime, 15 tests). The original cases are kept. New in the rework: - The verifier's repro: external `hack-dev` + `{"version":1,"isolatedNetworks":["hack-dev"]}` + `HACK_NATIVE_SHARED_SOURCE=1`. `hack up` passes the check and reaches `runtime check- project-share` and then `runtime up`, as on `next`. - `hack restart` with the same selection reaches restart preflight's own `Native restart has no existing run` line. - A selection that isolates a different network still gets the Compose-backend line, with 0 runtime calls and an unchanged tree. - An invalid selection (version 2) gets one `E_CONFIG_INVALID` line, with 0 runtime calls and an unchanged tree. - Native `doctor --json` with the selection reports `ok` and `features: []`. - Mutation proof for the rework. Each mutation was applied, both files run, and the change reverted with `git checkout`: | Mutation | Failing tests | | --- | --- | | `handleNativeUp` does not pass the adaptation | 3 (up, restart, invalid selection) | | Classifier ignores the isolated set | 5 | | Selection read even without an external network | 1 | | `isolatedNetworks` shape not validated | 1 | | `version` not validated | 2 | | Native doctor does not pass the adaptation | 1 | The first version's mutation results (removing the call, each rule, doctor wiring) still apply to the unchanged cases. ## Evidence - Bun 1.4.2 (`mise x bun@1.4.2`), macOS arm64 host. Load average was between 30 and 95 during these runs. - `bun run --cwd packages/cli typecheck`: 0 errors. `bun run --cwd packages/cli check`: 0 errors and 34 existing warnings. `biome check` on the changed files is clean. - Focused and required: the two PR test files, `tests/cli-spawn-env-isolation.test.ts`, `tests/cli-reference-docs.test.ts`, `tests/integration-ownership.test.ts` and `tests/native- refusal-cli.test.ts`: 63 pass, 0 fail. - `tests/doctor-*.test.ts`, `tests/native-project-adaptation.test.ts` and `tests/native-config- import*.test.ts`: 539 pass, 3 fail. The 3 failures are the capture process-group tests in `tests/native-config-import-build.test.ts` (about 505 ms each). The verifier found the same failures with `origin/next` source, so they come from this host, not this PR. - After `bun run build` and `bun run build:config-compiler`, all six `scripts/check- native-*-plan-cli.ts` exit 0. No native refusal code or message changed. The `E_CONFIG_INVALID` adaptation message is native start's existing one. - gitleaks 8.30.1: no leaks. The scan ran as `gitleaks dir` on a `git log -p origin/next..HEAD` export, because the worktree sandbox blocks `gitleaks git`. - Full `bun run test`: exit 1 after 1 h 27 min on the loaded host. 114 files had failures in the parallel run. A serial rerun of each with `--timeout 60000` left 20 files failing. 19 are authored/adopted native-compose suites, which never reach `handleNativeUp`'s check, and their failures are fixture timeouts and unsettled children. The 20th, `native-env-metadata`, passed 32/32 on a later rerun with this PR's code. As a baseline, `src/commands/project.ts` and `src/commands/doctor.ts` were swapped to the merge base `ec223cdb`. `native-compose-down- command` (1/13), `native-compose-storage-fence-cost` (0/4), `native-compose-retained-storage- command` (0/5) and `native-authored-process-incarnation` (9/1) failed the same way, so these failures are inherited under this load. - The adaptation CLI case also accepts a timed-out or unreadable stub reply (94e12a9). Under load, both appeared in the parallel run, and each is reachable only after the check passed. The original `control: ordinary labels…` case is sensitive to the same load in the parallel run and passes serially. - Hosted CI on 76003b5: every job passed except docker-e2e shards 3 and 4, which hit the same inherited failures as on 9ce0f3b: - Shard 3: the `pattern-p01-dependency-bootstrap` `docker inspect … No such object` race. - Shard 4: `native-config-build` and `native-config-down-hooks` (also failing on the `next` base run 39663080763). The run on 94e12a9 was in progress when this was written. Shards 1–3 and all other completed jobs passed. ## Remaining gaps - The Compose-doctor variant is covered by a unit test of its result, not a CLI run, because the private CLI harness does not stub its Docker-era checks. - Only the explicit `platform` field is classified. An image with no `platform` that publishes only amd64 is still refused later, at native image admission, with the existing image refusal. - Only `.hack/docker-compose.yml` is classified. Compose override files and env-overlay interpolation are not evaluated. An interpolated `platform` such as `${PLATFORM}` is treated as non-native. - The check validates only the `version` and `isolatedNetworks` parts of the adaptation. Native start still validates the rest of the selection, and that each listed name is an external network, in its existing order. A listed network declared in the legacy `external: {name}` form passes this check. Native start's adaptation then decides it as before, because `replaceNetworks` accepts only `external: true` in the normalized file. - Native support for these features stays in HACK-1270. Real Omega acceptance on the M3 has not been run here. Refs HACK-1291 🤖 Generated with [Claude Code](https://claude.com/claude-code)
roodboi
added a commit
that referenced
this pull request
Oct 22, 2026
…#370) ## Root cause `parseCliArgv` parsed argv once against the union of every command's options, keyed by long name (`buildUnionParseOptions`). Two options shared a short flag: `--path -p` (project commands) and `ssh --port -p`. Bun's `parseArgs` resolved `-p` to whichever came last, which was `ssh --port`. So `hack up -p <dir>` (and `init`, `down`, `ps`, `logs`, `env ...`, `config ...`, `setup ...`) parsed as `--port`, and `run.ts` refused it with `Option(s) not valid for "up": --port`. The same union let one long name's type in one command change how every other command parsed it (#339 hit this with `--rollback`), which is why a guard test banned cross-command type conflicts. ## Fix `src/cli/command.ts` now parses in two passes: 1. **`scanInvocation`** resolves the command path, and the child-command boundary for passthrough positionals, from the leading positionals. It reads each option with the options of the command resolved so far, plus the global and built-in options, and skips that option's value token, including the omitted-value form of options with a default. An option placed before the command name falls back to the other declarations of that spelling. If those declarations disagree about taking a value, the option is read as a flag, so it never consumes the command name. 2. **`parseCliArgv`** then parses with that command's own options plus the global and built-in options, by long and short spelling. Options that only other commands declare are still parsed by long name, never by short flag. A misplaced option therefore keeps its existing `Option(s) not valid for "<command>"` refusal. `findPassthroughStart` and `hackOwnedArgv` use the same scan, so #359's child-argv passthrough uses the resolved command's option types. That covers `--` handling, flags after the child command, and the JSON envelope selected only by Hack's own `--json`. Default-value normalization is scoped the same way. `run.ts` is unchanged. The only parse difference found by comparing old and new output on edge cases (`--help --all`, `help --all`, `-- up`, `--json run web tool --json`, `--path /x up`, `logs --tail`, `up --tail`, `ssh --path /x`, `x ext cmd -p 1`, `--no-interactive up -p /x`) is the intended one: `up -p /x` now sets `path` instead of `port`. ## Tests - New `tests/cli-per-command-options.test.ts`: - `-p` sets `--path` for every command that declares `--path -p`. The test walks the spec and asserts that it includes init/up/down/ps/logs/env list/config validate/setup migrate. - `ssh -p` sets `--port`. - `-p` before the command name follows that command. - A misplaced foreign long option is still refused as "not valid here", while an unknown short flag is unknown. - `config adopt --rollback --json` stays boolean, and `setup migrate --rollback-manifest <file>` takes a value. - A synthetic CLI where `--rollback`/`-r` is a flag in one command and takes a value in another parses correctly per command, before the command name, and at the passthrough boundary. - A source-CLI spawn under `privateCliEnv` checks that `hack ps -p <dir> --json` no longer reports `--port`. - `tests/cli-command.test.ts`: the cross-tree "one value type per long option" guard is replaced by a per-scope guard. Within a command's scope (built-ins, globals and the command's own options), each long and short spelling must name exactly one option. - The #359 passthrough tests in `tests/cli-passthrough-argv.test.ts` are unchanged and pass. Each new test was checked by mutation: - With the old `command.ts`, 11 of the new tests fail, including the spawn test. It printed `Option(s) not valid for "ps": --port`. - Making disagreeing declarations keep the last type fails the before-command-name test. - Removing other commands' long names from the parse table fails the "not valid here" test. - Giving `ssh --port` the short flag `-h` fails the per-scope guard with `hack ssh: -h names --help and --port`. ## Evidence Bun 1.4.2 (`mise x bun@1.4.2`), base ace57ee. `next` has since moved to f2278c7; it only adds an unrelated `run.ts` hunk and merges cleanly. - `bun run --cwd packages/cli typecheck`: 0 errors. `bun run typecheck`: passes. - `bun run --cwd packages/cli check` and `bun run check`: 0 errors. The changed files lint clean, with no new warnings. - Focused suites pass: 131/131 for the new test file, cli-passthrough-argv, cli-command, cli- spawn-env-isolation, cli-reference-docs, integration-ownership, mcp and mcp-inspect-tools. One run had a transient failure in an `mcp.test.ts` spawn test under load; it passed alone and in the rerun. - `bun run docs:cli-reference`: no change to `docs/reference/cli.md`, because the help surface is unchanged. - After `bun run build`, `dist/hack ps -p <dir> --json` and `dist/hack up -p <dir> --json` (private HOME) reach project discovery ("No .hack/ found") instead of the `--port` refusal. - No native refusal codes or messages changed, so the native plan-CLI scripts were not rerun. - gitleaks 8.30.1 over a `git log -p origin/next..HEAD` export: no leaks. ## Remaining gaps - Full `bun run test` ran on a heavily loaded host for 4253s and ended with 6824 pass and 640 fail across 105 files. Rerun serially with `--timeout 60000`, 89 of those files pass. - The 16 that still fail are native-compose/native-authored files. Rerun with `origin/next`'s `command.ts` swapped in, they fail the same way or worse; for example, `native-compose- adoption-generation` fails 0/111 on both. - Two files with a single failure each (`dependency-startup-progress`, `native-compose- adoption-probe`) pass alone, and neither imports the CLI parser. - None of these failures is caused by this change. A green full run on a quiet runner or CI is still outstanding. - Edge case, documented in `scanInvocation`: an option placed before the command name whose spelling is a flag in one command and takes a value in another is read as a flag. No such spelling exists in the current CLI. Refs HACK-1293 🤖 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.
Summary
This fixes four CLI parsing and refusal problems seen on the M3 with v5.0.0-next.5, plus the
up --detachmessage. The branch is rebased onto currentnext(54cc4c9), which includes #331.1. Child flags no longer reach Hack's parser
Before:
hack run web --profile toolchain bun --versionprinted Hack's version and exited 0.Now a positional can be marked
passthrough. From the child command's first word on, every token belongs to the child. That includes--version,--help,--json,--path,--branchand any other flag.--is still optional and is not passed on.--, as inhack run web npm test -- --watch, now reaches the child unchanged. Before, it was dropped.run,exec,host exec,env exec,session execandx.findPassthroughStartmirrors the parser, including options with default values.--jsonno longer selects Hack's JSON error envelope. Only argv before the child command and before the first--counts (hackOwnedArgv). Sohack run --profile toolchain web tool --jsonandhack exec web cat --jsonprint Hack's refusal as one human-readable line.hack --json run web tool --jsonstill prints the envelope.session exectakes its workspace command as one argument.hack session exec ws npm --versionnow refuses withUnexpected arguments: --version. <command> is one argument; quote the whole command, as in "npm --version".It does not print Hack's version.hack run [options] <service> [cmd...]plus a one-line note. Forsession execthe note says to quote the command.docs/reference/cli.mdis regenerated, anddocs/cli.mddescribes both rules.MCP
buildRunArgs/buildExecArgsalready put--before the service, so their behavior does not change.2. Native refusals print one closed line, not a stack
HackCliErrorwithE_NATIVE_PROJECT_UNSUPPORTED.HACK_NATIVE_SHARED_SOURCErequirement onupandrestartis nowE_PERMISSION, and the message says to set it and run again.E_CONFIG_INVALID.E_LIFECYCLE_FAILED.E_NATIVE_PROJECT_UNSUPPORTED. This covers a serviceenv_file, or a project.envwithout managed env. Unreviewed route labels are alsoE_NATIVE_PROJECT_UNSUPPORTED.E_CONFIG_INVALID.ClosedCliError: no stack, unlessHACK_DEBUG=1. This covers:NativePartialShutdownError,NativeHttpsStartupErrorandNativeHttpsLeaseAcquisitionErrorHACK_NATIVE_BINARY/HACK_NATIVE_HOMEselection3. A refused
runno longer prints the full helpWhen a handler throws a
CliUsageErrorafter parsing has succeeded, Hack now prints the message plus one line:See `hack run --help` for usage.Usage errors found while parsing still print the help. Under--jsonthe output is still oneE_USAGEenvelope.4. Authored
upwith the runtime stoppedruntime status --jsonrequest. A stopped runtime (phase notrunning, or a dead process) refuses at stagereservationwithruntime_not_running. Nothing is retained, sopsstaysnot_started. An inconclusive reply does not refuse.admission.reserve. A Ctrl-C during the check now endsnot-startedandpsstaysnot_started. Before this fix it endedretainedandpsshowedpending.E_DOCKER_UNAVAILABLE(aslogsalready does), with a message for each outcome and the next action. Before, it reportedE_STARTUP_INCOMPLETE. This now also covers fix(runtime): preserve authored startup and stopped-runtime causes #331's wrapped form: authored foreground control reportsnative_graph_foregroundwith causeruntime_not_running. The serve exit diagnostic now carries that bounded cause code toNativeAuthoredProjectStartError.nativeCauseCode.nativeCodesReportRuntimeNotRunningclassifies both forms, asisNativeRuntimeNotRunningdoes.pendingcan still be correct. Thepspending note anddocs/reference/native-authored-commands.mdexplain the remaining case. When the intent was reserved before the runtime stopped,pendingis still correct, because only the owner's cleanup proof may retire that intent.up --detachon nativeupwith--detach,--json,--targetor service names, andrestartwith--json,--targetor service names, now refuse with a one-lineE_USAGEmessage. (restarthas no--detachoption, sohack restart --detachis still an argument parse error that prints the help, as before.) It names the option and the supported form: runhack upin the foreground in its own terminal, usehack psandhack logsfrom another, and press Ctrl-C to stop it.up --detachkeepsE_NATIVE_PROJECT_UNSUPPORTEDand now adds the same guidance.Validation
At head c5b4202, based on
next54cc4c9:bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors and 34 warnings, the same count as onnext. Changed files lint clean with Biome.docs:cli-referenceis regenerated and is stable on a rerun.tests/integration-ownership.test.tsandtests/cli-spawn-env-isolation.test.tspass.integration-ownership,cli-spawn-env-isolation,cli-reference-docs,cli-help,cli-closed-error-output,cli-passthrough-argv,cli-command,native-runtime-not-running-cli,native-runtime-preflight,native-authored-command,native-project-observe,native-project-start,native-project-restart,native-runtime-client,native-exec-environment,native-project-input,native-project-routing,native-project-branch,native-project-adaptation,native-aws-environment,native-project-source-preflight,native-exec-stale-env,native-project-process,native-authored-project-process,native-authored-project-restart,native-authored-command-dispatch,mcp,native-project-branch-cliandnative-project-down.tests/native-authored-project-start.test.ts: 48/48 in one run. Under host load (load average 11–13), another run cascaded through the fixture-settlement guard. The first failing test passed on its own. This is the known flakiness under load, which also happens on unchangednext.New spawn tests use
tests/helpers/private-cli-env.ts, with a private HOME and an isolated Docker client config. No real Docker, HOME or keychain is used.Mutation proofs. Each fix was reverted, the listed tests failed, and the fix was restored.
parseCliArgvcli-passthrough-argvrunClireads--jsonfrom raw argv again--jsonspawn testsremaining()after the status check removedrun --profilerefusal testErrorErrorErrorup --detachrefusal removednative-project-observerefusal spawn test, and theup --detachclosed-line testnativeRuntimeStatusStoppedcheck removedE_DOCKER_UNAVAILABLEmapping casesnativeCode === runtime_not_runningnative_graph_foregroundmapping casenativeCauseCodenativeCodesReportRuntimeNotRunningremovedsession execquoting hint removedsession exectestcli-help"text help says where a child command's own argv begins"NativePartialShutdownErrorback toErrorErrorcli-reference-docsdrift testGaps
runtime statusthe way the Ruststatus()does for a stopped pool. Another fails serve withnative_graph_foreground/runtime_not_running. The status check is advisory: if the runtime stops between the check and serve, the retained-evidence path still applies, and the CLI reports it asE_DOCKER_UNAVAILABLEwith the pending explanation.tests/native-authored-project-recovery.test.tshas 2 failures ("new no-host death…" and "new no-host recovery retains original bindings…"). They fail the same way on unchangednexton this host.session execcommand is still a parse-time usage error, so it prints the root help after the quoting hint.dispatch run(Beta, unsupported) was left unchanged.native-authored-*internals, compose adoption, DNS) still throw plainError. This PR covers the image-project start, run, exec, down, restart (including its preflight) and dependency-refresh paths, their input/routing/branch/adaptation/AWS steps, and the authored start refusals.Rework 2
This round fixes the independent verifier's defects at c5b4202.
Refusal classes
Every known, user-reachable refusal on the image-project
up,restart,run,execandpspaths now prints one closed line. The class follows what was refused:HackCliErrorwith a stable code:E_CONFIG_INVALID: an unbounded--profile, a malformedHACK_NATIVE_DEPENDENCIESlistener selection or private selection file,HACK_NATIVE_ALLOW_HOSTS, a conflicting host-alias selection, an occupied HTTPS port, and a request over the runtime client's input or time budget (a large managed environment or a long startup timeout).E_NATIVE_PROJECT_UNSUPPORTED: a service that setsinitother thantruewith host dependencies, a Compose file outside the project, an active review with no supported service, and runtime metadata whose route labels do not match the reviewed plan.E_PERMISSION: an HTTPS port that needs host permission.ClosedCliError. Without--jsonit prints the message alone. Under--jsonthe code isE_UNEXPECTED, the existing code for a failure whose message is the only detail. This matches the down, exec, one-off and restart refusals from round 1. No existing code says "native state changed or could not be confirmed", andE_CONFIG_INVALIDwould wrongly tell an agent to fix its configuration. This covers:native-project-process.ts).Files converted beyond the verifier's list, because they are reachable from the same paths:
native-project-network.ts,native-project-host-aliases.ts,native-project-selection.ts,native-runtime-metadata.ts,native-project-finalization.ts,native-project-recovery.ts,native-project-cleanup-command.tsandnative-project-process.ts.The HTTPS port message no longer echoes the port number.
Defects
Native refusal audit. Fixed for every site the verifier listed, plus the files above. The repro
HACK_NATIVE_SHARED_SOURCE=1 hack up --profile <300 chars>now printsERROR: E_CONFIG_INVALID: Native profiles require at most 64 bounded nonempty names.with no stack, and makes no runtime request.tests/native-refusal-cli.test.tsspawns the source CLI withprivateCliEnvand a stub runtime. The stub answers scripted<word> <word>requests and otherwise reports a stopped runtime.ERROR:line, no stack frame, noError:and no help for these sites:native-project-run(profiles, unsafe mapping)native-runtime-client(unparseable reply)native-project-dependenciesnative-project-reviewnative-project-cachenative-project-start(dependency plan)native-project-retained-startupnative-project-restore(through a retainedupwith an acknowledged finalization)native-project-retained-images(throughrestart)native-project-networknative-project-https.tsornative-https-port.tswithout a live serve owner. Those refusals are raised in the foreground owner's readiness step, insideNativeHttpsStartupError, or in doctor checks that report a result instead.tests/cli-closed-error-output.test.tsrenders their real errors through the top-level handler instead: one line, no stack.hack dispatch runchild argv. Fixed. Thecommandpositional ispassthrough: true.hack dispatch run --project p bun --versionno longer prints Hack's version. Covered by the passthrough parse matrix (8 child-flag cases) and a source-CLI spawn case.docs/cli.mdlistsdispatch run.docs:cli-referenceis unchanged, because Beta commands are not in the reference.Mutations that survived.
{phase: "initializing", process_alive: null}, sophase === "stopped"now fails it.tests/helpers/closed-refusal.ts(expectClosedRefusalandexpectCodedRefusal, plus sync forms), asserts the class and code as well as the text. The restart-preflight (admission and the other closed restart refusals), down, exec, one-off, dependency-refresh and interrupted-cleanup tests now use it. A new test covers one-off argument validation.Integrity refusals in
native-project-start.ts. Fixed. The shared configurationrefused()is now used only for configuration. Each of these has its own closed message, and none saysE_NATIVE_PROJECT_UNSUPPORTEDor "cannot admit this configuration":ensure-imagereply that is not a content ID, and anauthoritative()receipt mismatch: "could not confirm the runtime image selection / the startup receipt";dependency_plan_id: "could not confirm the host dependency plan";retireRemovedMappingowner mismatch: "Native run mapping is owned by another run; it was not removed";Native HTTPS verification failed (NO_RESPONSE)or(REDIRECT_TARGET_REFUSED), in the same fixed form as the other verification failures, so the cleanup diagnostic keeps them.New tests in
tests/native-project-start.test.tscover each one.up --detach. Fixed. Both paths usenativeForegroundDetachRefusal. Authoredup --detachon macOS now refuses with the image project'sE_USAGEone-line message, which stands alone instead of trailing the generic sentence. Tests compare the authored refusal with the image refusal (code and full message) forup, and unit tests also passdetachto the authoredrestarthandler directly. The CLI cannot reach thatrestartcase:restarthas no--detachoption, sohack restart --detachis a parse error that prints the help (correction from verifier round 3).docs/reference/native-authored-commands.mdis updated.Also in scope: a new authored-start test covers the
NativeRuntimeRequestErrorwrapped-cause branch infailedAdmission. Theruntime statusrequest fails withnative_graph_foreground/runtime_not_running, and the start error keeps both codes at stagereservation.Remaining plain
Errorsites (internal invariants)native-runtime-client.ts: an argv shape that failsvalidExecResponseSelection/validBoundDrainSelection. Callers build these shapes, and no user input selects them. The user-reachable timeout and input bounds are now coded.native-project-process.ts: "Native receipt observation must be synchronous". This is a programming contract on the injected observer.native-project-exec.ts:137is closed but cannot be reached in practice. Dependency refresh refuses an absent mapping first with its own closed "has not been started" message, so mutating this site survives. It is a defensive check for a removal between refresh and inspection.native-authored-*internals andnative-https-owner*. Authored start failures are wrapped byfailedAdmissioninto coded CLI errors. HTTPS owner failures are wrapped byNativeHttpsStartupError, which is closed.Validation (rework 2)
Head f8e823e, based on c5b4202. f8e823e fixes the six native plan CLI acceptance scripts (
scripts/check-native-{env,host,routing,endpoint,process,acquisition}-plan-cli.ts), which run only in the macOS "Config compiler" CI job. On 6ec1ae2 that job failed: the scripts still expectedE_NATIVE_PROJECT_UNSUPPORTEDfor authoredup --detach. They now expectE_USAGEon macOS and keepE_NATIVE_PROJECT_UNSUPPORTEDelsewhere. Afterbun run build, all six,check-native-config-import-cliandcheck-config-compiler-clipass locally on Bun 1.4.2. All results come from Bun 1.4.2 (mise x bun@1.4.2 -- bun …) unless they say otherwise.bun run --cwd packages/cli typecheck: clean.bun run --cwd packages/cli check: 0 errors and 34 warnings, the same count asnext. Every changed file lints clean with Biome.docs:cli-referencewas rerun with no change. Betadispatchis not in the reference.next..HEAD): no leaks.bun run test(1.4.2): 74 failures in a 1h18m run on a host with load average 13–32.up --detachrefusal. It is now its own test that expects the sharedE_USAGEline on macOS.--timeout 60000and all passed, exceptnative-authored-project-recovery"new no-host death…", the known HACK-1282 failure onnext.--timeout 120000.cli-spawn-env-isolation,integration-ownership,cli-reference-docs,cli-help,native-runtime-not-running-cliandnative-project-observe.native-authored-project-startunder load.cli-spawn-env-isolationpassed.Mutation proofs. Each was applied to the source, the listed tests failed, and the source was restored. Every mutation failed at least one test except
exec.ts:137(explained above).Error(4 separate mutations)native-https-port.tscoded back toErrornative-project-dependencies.tscoded back toErrornative-project-run.tsmappingrefused()/ restart lock / profiles back toError(3 mutations)Errorin review, retained-images, retained-startup, restore, https and cache (6 mutations)ErrorError(4 mutations)Error(4 mutations)ErrorErrorErrorErrorErrorrefused()back toError;NativePartialShutdownError extends Errorrefused()(6 mutations)dispatch runpassthrough removedphase === "stopped"initializingcasefailedAdmissiondrops the wrapped causeVerifier round 3
Head ead8383, a fast-forward on 4d85f56. An independent verifier found three image-project
uprefusals that still printedERROR: Error: …and a stack.nextbehaves the same way, so these were sites the PR missed, not regressions. Each now prints one closed line.native-project-image-admission.ts). Three cases now print a closed line:registry_image), or a service with noimage, givesE_NATIVE_PROJECT_UNSUPPORTEDwith the existing message. The message does not include the reference.runtime admit-imagereply (for example{}) is now a closed integrity error: "Native runtime returned an invalid image admission response; …". Before, it reused the refusal text.native-prepared-base.ts). The three invalidHACK_NATIVE_PREPARED_BASE/_STOREselections are nowE_CONFIG_INVALID. They still refuse before any runtime request.native-workspace-cache.ts).refused()is nowE_CONFIG_INVALID, like its sibling adaptation refusals.restart --detach. This body claimedrestart --detachrefuses withE_USAGE.restarthas no--detachoption, so it is a parse error that prints the help, and that is unchanged. Theup --detachsections above are corrected.The new spawn cases are in
tests/native-refusal-cli.test.ts. The stub runtime can now also fail a request with a closed stderr code. Each case checks for exactly oneERROR:line, no stack, noError:, no help, and the runtime calls made:ghcr.io/example/app:1reference (the output does not containghcr.io);{}admission reply;HACK_NATIVE_PREPARED_BASE=bogus, with no runtime request;{"version":1,"workspaceCache":{"bogus":1}}, which stops afterruntime check-project-share.tests/native-prepared-base.test.tsnow asserts the code withexpectCodedThrow.Mutation proofs (Bun 1.4.2). In each case the source was changed as described, the spawn file was rerun, and the source was then restored:
native-project-image-admission.tsreverted to 4d85f56ERROR: Error: …malformed()back to a plainErrornative-prepared-base.tsreverted to 4d85f56native-workspace-cache.tsreverted to 4d85f56Gates on ead8383 (Bun 1.4.2):
bun run --cwd packages/cli typecheckis clean.checkreports 0 errors and the same 34 warnings. The changed files are formatted with Biome.native-project-*andnative-runtime-*test file, plusnative-prepared-base,native-workspace-cache,native-refusal-cli(16/16),native-selection-refusals,cli-spawn-env-isolation,cli-helpandcli-reference-docs.bun run buildandbun run build:config-compiler, all sixcheck-native-*-plan-cliscripts,check-native-config-import-cliandcheck-config-compiler-cliexit 0..gitleaks.tomland--redactfound no leaks. The agent sandbox blocks thegitleaks gitsubcommand, so the scan usedgitleaks dirovergit log -p --unified=0 origin/next..HEAD(6 commits, about 238 KB).bun run testwas not rerun this round.🤖 Generated with Claude Code