Skip to content

fix(cli): close HACK-1426 follow-ups (up E_PROJECT_NOT_FOUND, git-accurate gitignore entry, no-TTY Tailscale test) - #479

Merged
roodboi merged 1 commit into
nextfrom
wf/g91-cli-small-followups
Oct 24, 2026
Merged

roodboi merged 1 commit into
nextfrom
wf/g91-cli-small-followups

Conversation

@roodboi

@roodboi roodboi commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

The six small CLI follow-ups in HACK-1426, from the verifiers of #466, #441, #453, #460 and #462.

Root cause

  1. Tailscale guard had no test. The canPrompt() guard in front of "Turn on Tailscale?" in setupTailscale (src/commands/ssh.ts) had no case in tests/no-tty-prompt-sites-cli.test.ts. If you changed it to if (false), every ssh case still passed.
  2. Cold cache in the first case. Each run of that file uses a fresh private HOME. Bun keeps its runtime transpiler cache under HOME (~/Library/Caches/bun/@t@ on macOS), so the first spawn started cold inside its 10 s kill bound. On a loaded host that first case took 6.3 s and was SIGKILLed.
  3. hack up dropped the error code. For a missing project, runUpCommand caught MissingProjectContextError and, without --json, logged only its message. down, restart, ps, logs and open let the error reach the CLI's closed-error renderer, which prints E_PROJECT_NOT_FOUND: ....
  4. Indented .gitignore entry counted as present. ensureGitignoreEntry (src/lib/fs.ts) compared trimmed lines. Git keeps leading whitespace as part of the pattern, so .hack.secret.key does not ignore the key. Hack counted that line as the entry, wrote nothing, then found the key not ignored. It then refused with the "a later rule re-includes the key" message, which names the wrong cause.
  5. ACL test could leave mode 0444. In tests/git-info-exclude.test.ts, the macOS ACL test ran chmod 0444, the ACL grant and access(W_OK) before the try whose finally restores mode 0644. If any of those steps failed, the file kept mode 0444.
  6. Paired fixture lost a case. fix(agents): keep a value-free diagnostic on adoption marker access refusals #462 gave the paired unsearchable fixture a .hack/.internal directory. After that, nothing covered a .hack at mode 000 with no .hack/.internal, which is the most common real layout.

Fix

  1. New paired no-TTY case: ssh --tailscale runs against a logging tailscale stand-in placed first on PATH. The stand-in reports {"BackendState":"Stopped"}. The case expects exit 1, the Run \tailscale up`, then re-run `hack ssh`.hint, no prompt, and notailscale up` call.
  2. beforeAll now runs one warm-up child (index.ts --help) under the file's private HOME. It has its own named budget: WARM_UP_KILL_AFTER_MS = 90 s, and the hook bound is that plus 10 s. A kill fails the file with a clear message. The cases keep the 10 s bound.
  3. runUpCommand now handles MissingProjectContextError itself only under --json. Without --json it rethrows, so hack up prints ERROR: E_PROJECT_NOT_FOUND: No .hack/ (or legacy .dev/) found. Run: hack init (console logger), the same as the other lifecycle commands. docs/cli.md already promised this.
  4. ensureGitignoreEntry now reads each line the way git does: it drops a trailing CR and trailing spaces, and keeps leading whitespace. An indented line no longer counts as the entry, so Hack appends the exact entry, git ignores the key, and the secret write goes ahead. The "re-included" refusal now appears only when the exact entry is present and git still does not ignore the key, which is what its message says. Tabs are not trimmed, which matches git's trim_trailing_spaces.
  5. The ACL test's mode change, ACL grant and access probe now run inside the try.
  6. New unsearchable-no-internal fixture (never adopted, no .hack/.internal). Its .hack at mode 000 is the first case in the paired CLI/MCP loop. The proof-matrix row in docs/reference/ax-target-result-contract.md now names this case.

Tests and mutation evidence

For each mutation I applied it, ran the focused tests, and restored the file.

  • if (!canPrompt()) changed to if (false) in setupTailscale: the new case fails. The child was SIGKILLed at the 10 s bound with the prompt open. The other 3 ssh cases pass. The stand-in recorded no up call.
  • up's non-JSON branch reverted: 2 tests fail, up -p <empty dir> without --json … and up from a working directory without a project …. The same new loop over down, restart, ps, logs and open still passes.
  • line.trim() === entryLine restored in ensureGitignoreEntry: 2 tests fail, the CLI case "an indented entry … is completed by an exact entry instead of being refused as re-included" and the unit case "ensureGitignoreEntry reads each line as git does …".
  • Exact comparison, line === entryLine: the unit case fails on the trailing-space and CRLF inputs.
  • EACCES/EPERM no longer mapped to permission in the adoption marker's classifyStateRootFailure: the paired loop fails on its first case, unsearchable-no-internal with .hack at 000. It gets adoption_state_unsafe / E_CONFIG_INVALID where input_permission_denied / E_PERMISSION is expected.
  • Warm-up removed (timing, idle host): the first case took 1.10 s without the warm-up and 0.65 s with it. The later cases are unchanged.

Validation

  • Focused tests: 6 files (no-tty-prompt-sites-cli, missing-project-error-code, project-env-key-ignore, project-target-inspect, git-info-exclude, doctor-generated-files), 121 pass and 0 fail. project-up-command also passes. Runs used a private HOME, HACK_HOME and DOCKER_CONFIG, and HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1, as a non-root user on macOS.
  • bun run --cwd packages/cli typecheck: 0 errors.
  • Root bun run check: privacy-check: ok. @hack/cli:check shows the 34 warnings that were already there. Biome finds no issues in the changed files.
  • bun run docs:cli-reference: no diff. Only an error line changed, not the CLI surface. No consumer agent guidance changed.
  • gitleaks 8.30.1 (gitleaks dir on the exported git log -p origin/next..HEAD): no leaks found.
  • Full suite: the hosted CI test job (all shards) at this PR head.

Gaps

  • I did not reproduce item 2 under load. The evidence is that the warm-up moves the cold start out of the first case: on an idle host, 1.10 s became 0.65 s. The reported failure was 6.3 s at load about 60.
  • Item 5 is a structural move with unchanged assertions. No mutation tells the old and new placement apart without failing the ACL setup itself.
  • As in fix(agents): keep a value-free diagnostic on adoption marker access refusals #462, no mutation tells the no-.internal case apart from the .hack 000 case with .internal. Both get EACCES from the same state-root lstat. The new case is live (it fails first under the classifier mutation) but not independent.
  • Locally, tests/native-compose-adoption-search-depth.test.ts "a pending adoption more than 64 directories above …" fails in its setup with E_LEGACY_ADOPTION_DEADLINE. It fails the same way with origin/next's src/, so this change did not cause it. Hosted CI decides.
  • Not run as root. The mode-dependent cases are skipped as root, as before.

Refs HACK-1426

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

…urate gitignore entry, no-TTY Tailscale test)

- `hack up` without a project now prints the closed
  `E_PROJECT_NOT_FOUND: ...` line like down, restart, ps, logs and open.
- ensureGitignoreEntry reads each line as git does (trailing CR and
  spaces dropped, leading whitespace kept), so an indented
  `.hack.secret.key` no longer counts as the entry and is no longer
  refused as "re-included"; the exact entry is appended instead.
- tests: no-TTY guard test for "Turn on Tailscale?", a named warm-up
  for the cold transpiler cache in that file, the ACL test's setup moved
  inside its try, and the paired mode-000 `.hack` fixture without
  `.hack/.internal` restored.

Refs HACK-1426

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@linear-code

linear-code Bot commented Oct 24, 2026

Copy link
Copy Markdown

HACK-1426

@roodboi
roodboi marked this pull request as ready for review October 24, 2026 18:07
@roodboi
roodboi merged commit d09e1b7 into next Oct 24, 2026
21 checks passed
@roodboi
roodboi deleted the wf/g91-cli-small-followups branch October 24, 2026 18:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant