Repository navigation
fix(cli): report a missing project with one closed code across project commands - #427
Merged
Merged
Conversation
…t commands Follow-ups to #394 (HACK-1371). - branch, config get/set, env, host, project owner and tui now raise the shared MissingProjectContextError (E_PROJECT_NOT_FOUND) for a path without a project. branch, config and project owner printed a stack trace (and project owner reported E_UNEXPECTED under --json); env and host reported E_USAGE. - docs/cli.md lists the commands and the exceptions, and tests/missing-project-error-code.test.ts pins every command that declares --path. - Native run: the empty --env/--profile check now runs before authored dispatch, so an authored project refuses it with E_USAGE, not E_NATIVE_PROJECT_UNSUPPORTED. - Pin that a profile list with a real entry ("web,") is compared, not refused as empty, which kills the .every -> .some mutant. - Document that a short spelling only another command declares is refused as an unknown option, and record that difference from the old parser with the #370 notes. Refs HACK-1371 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
…red project The previous commit moved the empty --env/--profile check ahead of authored dispatch, so `run --profile ""` on an authored native project is refused with E_USAGE before dispatch. The source CLI run table in tests/native-authored-project-oneoff.test.ts still expected E_NATIVE_PROJECT_UNSUPPORTED for that row, and its failure set the file's uncertainty latch, which failed four later tests in hosted CI. Give each row its expected refusal: the empty profile row expects the E_USAGE empty-selection line, and the --profile dev and --env base rows keep E_NATIVE_PROJECT_UNSUPPORTED. No row starts the runtime. Refs HACK-1371 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
roodboi
marked this pull request as ready for review
October 23, 2026 01:24
roodboi
added a commit
that referenced
this pull request
Oct 24, 2026
…ike .hack/.gitignore (#439) ## Summary Follow-ups to #427 and #426 (merged into `next` as `a9a0348c`). Refs HACK-1378. ## Root cause and fix 1. **`info/exclude` read-only policy did not match `.hack/.gitignore`.** #426 made `.hack/.gitignore` read-only whenever the file has no write permission bit (`mode & 0o222 === 0`), for every user, root included. `ensureHackGitInfoExclude` (`src/lib/git-info- exclude.ts`) still used `access(W_OK)` alone. `access(2)` grants root write access whatever the mode, and on macOS it also grants write when an ACL allows it. So in those cases a 0444 `info/exclude` was replaced through the rename, and the `docs/env.md` statement "the same policy as for `info/exclude`" was wrong. **Fix:** `ensureHackGitInfoExclude` now checks the mode bits it just read first. When no write bit is set, it leaves the file alone and logs a debug line. When some write bit is set, `access(2)` still decides. The probe is injectable (`checkWriteAccess`), as in `ensureHackDirGitignore`, so a test can model root. The JSDoc and `docs/env.md` now state the shared rule. They also add an ACL note: an ACL that grants write to a 0444 file does not make it writable for Hack, so add a write bit instead. 2. **`docs/cli.md` showed only the console logger's form.** The missing-project refusal is printed as `logger.error("E_PROJECT_NOT_FOUND: <message>")`. Only the console logger prints it as `ERROR: E_PROJECT_NOT_FOUND: ...`. The doc now calls it one closed error line, `E_PROJECT_NOT_FOUND: <message>`, and shows each logger's prefix, as observed with the source CLI (`hack ps -p <empty dir>`): - console: `ERROR: E_PROJECT_NOT_FOUND: ...` on stderr; - gum: a styled `ERROR` level on stderr; - clack: a `│` guide line, then `■ E_PROJECT_NOT_FOUND: ...` on stdout. The `MissingProjectContextError` JSDoc and the header of `tests/missing-project-error- code.test.ts` now say the same. That test pins the console form, because it runs with `HACK_LOGGER=console`. 3. **Optional item, done.** The short-spelling parse test now covers `up` from the #370 differential table, as well as `doctor`. `up` has its own short flags (`-p`, `-d`), so the test also checks that this does not make other commands' short flags known for `up`. ## Tests - `tests/git-info-exclude.test.ts`: - A 0444 `exclude` stays unchanged even when the injected probe always grants write (which models root). The test also checks that the probe is never called. - On macOS only: a 0444 `exclude` with a real `chmod +a "<user> allow write"` ACL stays unchanged. The test first checks that the ACL makes `access(W_OK)` succeed. - With a write bit set, the probe decides: a rejecting probe leaves the file unchanged, and a granting probe updates it and keeps mode 0644. - `tests/cli-per-command-options.test.ts`: the short-spelling test is now a `test.each` over `doctor` and `up`. Each new test was checked by mutation: - **M1:** without the mode-bit check, the root-probe test and the ACL test fail. The other 8 tests pass. - **M2:** when the code ignores the injected probe and calls `access` directly, the probe test fails. - **M3:** when `src/cli/command.ts` adds other commands' short flags to the parse table, both rows of the short-spelling test (`doctor` and `up`) fail. ## Evidence Bun 1.4.2 (`mise x bun@1.4.2`). Base `origin/next` = `a9a0348c`. All local tests used a private HOME, `HACK_HOME`, `DOCKER_CONFIG` and an unreachable `DOCKER_HOST`, with `HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1`. - Root `bun run check` (includes `privacy:check: ok`): exit 0, 0 errors. - `bun run --cwd packages/cli typecheck`: 0 errors. The changed files lint clean. - Focused tests: 164/164 pass across `git-info-exclude`, `cli-per-command-options`, `missing-project-error-code`, `project-env-config`, `hack-gitignore`, `agent-instruction- source` and `cli-command`. - On this Mac, a 0444 file with an ACL that allows write: `access(W_OK)` returns `EACCES` before the ACL and succeeds after it. This confirms the ACL case. - gitleaks 8.30.1 `dir` scan of a `git log -p origin/next..HEAD` export: no leaks. - No CLI surface, agent guidance, `package.json` scripts or toolchain pins changed. So `docs:cli-reference`, `generate:agent-plugins` and task re-pinning do not apply. - Full suite on the M3: pending; it is running at `25c61913`, and this description will be updated with the result. ## Gaps - The root case is modeled with an injected probe; no test ran as real root. - The ACL test runs only on macOS. On Linux it is skipped, and the probe test covers the same branch. Refs HACK-1378 🤖 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.
Follow-ups to #394 (merged into
nextas d0c8184) for HACK-1371.Rework (4d30eff): hosted
testfailure on e3d6917Root cause. Fix 4 below moved the empty
--env/--profilecheck ahead of authored dispatch, sohack run --profile ""on an authored native project now refuses withE_USAGE, notE_NATIVE_PROJECT_UNSUPPORTED. The source CLI run table intests/native-authored-project-oneoff.test.tsstill expectedE_NATIVE_PROJECT_UNSUPPORTEDfor the["--profile", ""]row. That failure also set the file'suncertainlatch, 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:
--profile ""row expectsERROR: E_USAGE: ${NATIVE_RUN_EMPTY_SELECTION_REFUSAL}and no runtime attempt.--profile devand--env baserows keepE_NATIVE_PROJECT_UNSUPPORTEDand 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.src/commands/project.tsfromorigin/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.--env/--profileexpectingE_NATIVE_PROJECT_UNSUPPORTED. Only thelogsandexectables do that, and the early refusal applies only torun.native-authored-project-logsandnative-authored-project-execpass 91/91.Root cause (original commit e3d6917)
Commands still reported a missing project with different codes. fix(cli): report missing projects with one closed code and refuse empty native run selections #394 gave
project.tsa closedMissingProjectContextError, but other command files kept their own copy ofrequireProjectContext. Forhack <cmd> -p <empty dir>:branch add/list/remove/open,config get/setandproject owner showthrew a plainError. They printedERROR: Error: ..., a stack trace, and a stray└line on stdout.project owner show --jsonreportedE_UNEXPECTED.env ...andhost exec/shellthrewCliUsageError("No .hack/ found. Run: hack init"). That reportedE_USAGEand added a "See --help" hint.tui(with a TTY) threw a plainError.docs/cli.mdsaid every project command reportedE_PROJECT_NOT_FOUND, which was not true.The docs gave the wrong refusal for a short spelling.
docs/cli.mdsaid an option that only another command declares is refused asOption(s) not valid for "<command>". That is true only for long spellings. The per-command parser (fix(cli): parse options per resolved command so -p means --path again #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 --jsongivesUnknown option '-f'. .... The old union parser gaveOption(s) not valid for "doctor": --followinstead, and the fix(cli): parse options per resolved command so -p means --path again #370 differential notes did not record that difference.A mutant survived. Changing
.every(...)to.some(...)inrequireNonEmptyNativeRunOptionssurvived, because no test passed a profile list that mixes empty and real entries.Authored projects got the wrong refusal. On the native backend,
hack runran authored dispatch (tryNativeComposeCommand) before the empty check. So an authoredhack.project.jsonproject refused--profile ""withE_NATIVE_PROJECT_UNSUPPORTEDinstead ofE_USAGE.Fix
src/lib/missing-project-error.tsexportsMissingProjectContextError(E_PROJECT_NOT_FOUND, messageNo .hack/ (or legacy .dev/) found. Run: hack init).project.ts,branch.ts,config.ts,env.ts(which covershost),project-admin.tsandtui.tsnow throw it.docs/cli.mdlists the commands and the documented exceptions:tuiwithout a TTY refuses for that reason first.env getprints one fixed stderr line, because its output is raw values.config validate/plan/import/adoptare native configuration commands and keep their own refusals.project target/taskuse the same code with their own message.init,doctor,crash-capture,setup,mcpandagentdo not need a project.-p/-dconflict that fix(cli): parse options per resolved command so -p means --path again #370 removed. So I documented the difference instead.refuseEmptyNativeRunSelection, whichhandleRuncalls first whenHACK_RUNTIME_BACKEND=native. Authored, adopted and legacy projects now all refuse an empty value with the sameE_USAGEline, before any project is selected. The Compose backend is unchanged: there an empty--envstill means the base layers.docs/guides/native-candidate.mdstates which check runs first and theweb,rule.docs/reference/cli.mdis unchanged, becausebun run docs:cli-referenceproduced no diff (the help output did not change).Tests (each proven by mutation)
tests/missing-project-error-code.test.tswas rewritten:--path, and one test asserts that the table equals the set of commands walked fromCLI_SPEC. A new--pathcommand therefore fails until someone classifies it. Mutation: dropping thecrash-capturerow fails the enumeration test.privateCliEnv, temp HOME) and must print the exact envelope or line, with no stack trace. The documented exceptions are pinned to their exact output.origin/nextversion ofbranch.tsfails 4 tests,config.tsfails 2,env.tsfails 12 (env and host), andproject-admin.tsfails 1.tests/cli-renderer-loading.test.ts: thetui-missing-projectcontrol (forced TTY) now expectsERROR: E_PROJECT_NOT_FOUND: No .hack/. Mutation: restoring theorigin/nextversion oftui.tsfails it.tests/cli-per-command-options.test.ts: every short spelling that some other command declares butdoctordoes not (-f,-d,-c,-H,-u,-t) throwsUnknown option '<short>'ondoctor.doctor --followstill parses, so the CLI can say it is not valid. Mutation: lettingoptionTableForalso read other commands' short spellings fails it.tests/native-run-selection-cli.test.ts:--profile "toolchain,",",toolchain"and" , toolchain , "run against a project started withtoolchain.--profile "web,"againsttoolchaingets the mismatch refusal, not the empty one..everyto.some: all 4 fail.tests/native-authored-command-dispatch.test.ts: on an authored project with the native backend,runwith--profile "",--profile " , ",--env ""or--env=prints exactlyERROR: E_USAGE: <NATIVE_RUN_EMPTY_SELECTION_REFUSAL>, and neither the compiler nor the runtime runs. Mutation: disabling the early check fails all 4 (they reportE_NATIVE_PROJECT_UNSUPPORTED).tests/native-authored-project-oneoff.test.ts: the source CLI run table, as described in Rework.Evidence
mise x bun@1.4.2). Baseorigin/nextd0c8184, which includes a25160d.bun run --cwd packages/cli typecheckhas 0 errors. Rootbun run checkhas 0 errors and printsprivacy-check: ok.biome checkis clean on the changed test file.native-authored-project-logsandnative-authored-project-exec: 91/91 pass.native-authored-project-oneoff, which is how the failure got through.bun run docs:cli-reference: no change. No consumer agent guidance orpackage.jsonscripts changed.gitleaks dirover agit log -p origin/next..HEADexport of both commits): no leaks.Tests (Turbo)step of thetestjob 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.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 hostedTests (Turbo)full-suite step passed at this exact head.Gaps
Tests (Turbo)step ran the full suite at this head and passed.runstill resolves--env ""as the base env. This is unchanged and documented.remoteandgateway(unsupported) andinternalkeep their own missing-project errors.tuiwith a real TTY is covered only by the renderer-loading fixture, which forcesisTTY.Refs HACK-1371
🤖 Generated with Claude Code