Skip to content

fix(adoption): search every ancestor for an adoption and word doctor's adopted generated-files warning - #441

Merged
roodboi merged 3 commits into
nextfrom
wf/g62-adopted-selector-depth
Oct 24, 2026
Merged

roodboi merged 3 commits into
nextfrom
wf/g62-adopted-selector-depth

Conversation

@roodboi

@roodboi roodboi commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Unit G62 for HACK-1380.

Root cause

  1. selectLegacyComposeAdoptedRoot (src/lib/native-compose-adoption-command.ts) looked for an adoption in at most 64 directories. If it found no project root in that range, it called unsupported(). Every caller let that error through: tryLegacyComposeAdoptedCommand (ps, up, down, logs, restart, exec through tryNativeComposeCommand), tryNativeComposeOpen, and doctor's findDoctorAdoptedProject. So from a directory more than 64 levels deep with no project, these commands exited 1 with the misleading E_NATIVE_PROJECT_UNSUPPORTED: Adopted Compose supports retained-container ….
  2. For an adopted project, doctor's generated files warning (checkTrackedGeneratedFilesAt in src/commands/doctor.ts) said --fix would "ensure the committed .hack/.gitignore". For adopted projects, --fix only untracks the files and leaves the adoption's .gitignore unchanged.

Fix

  1. No search limit. The selector now walks up to the nearest project root or the filesystem root, the same ancestors that ordinary discovery (discoverProjectInputs) walks. A start directory with no project now gets the same missing-project error at any depth. Unsafe adoption state still throws the marker's closed E_CONFIG_INVALID. The selector now has an explicit Promise<string | null> return type, and its doc comment states the invariant.
  2. Adopted warning text. For an adopted project the warning now reads: … Run 'hack doctor --fix' to untrack them (files stay on disk). It leaves the adopted project's .hack/.gitignore unchanged, so keep these files out of later commits. This matches the existing --fix plan line and the fix note. The legacy text is unchanged.
  3. Docs: docs/reference/native-compose-adoption.md.

Why there is no "search limit reached" result

The task suggested a distinct "search limit reached" result that callers would treat as "no adopted root". I built and probed that version and found it unsafe. Ordinary discovery reads an active adoption as native input (inspectProjectInputsAtRoot). So if the selector misses an adoption more than 64 levels up, the command falls into the native authored path. With that version, hack up --detach from 70 levels inside an active adoption reached native startup and failed only because the fixture's compiler was missing (Native configuration compiler is unavailable). With the unbounded walk, the same command stays on the adopted path. The old limit protected nothing: dirname of an absolute path always reaches /, and ordinary discovery already walks every ancestor without a limit.

Tests

  • New tests/native-compose-adoption-search-depth.test.ts (4 tests, start directories 70 levels deep):
    • No project: ps, up --detach, down, logs --no-follow and open give exactly the same exit code and output as from a 2-level directory, include No .hack/ (or legacy .dev/) found, and never include E_NATIVE_PROJECT_UNSUPPORTED. The selector and findDoctorAdoptedProject both return null.
    • Active adoption above the start directory: the selector and doctor select it. ps lists the retained container (db\texited), the same as from a shallow directory. up --detach from deep gives the same result as from shallow.
    • Pending adoption above the start directory: the selector and doctor select it. ps and down give the same closed E_CONFIG_INVALID as from a shallow directory, with no canary in the output.
    • Unsafe adoption state above the start directory (state root is a file): the selector gives a closed, coded E_CONFIG_INVALID refusal with no canary or path, and ps from the CLI gives the same refusal.
  • New tests/doctor-adopted-generated-files.test.ts: doctor --json in an adopted Git checkout with a tracked .hack/.env.state.json gives the adopted warning text exactly, with no "ensure the committed".
  • All CLI runs use privateCliEnv (private HOME and HACK_HOME, isolated Docker client config, HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1, daemon disabled). The adopted cases use only the synthetic retained source-bind engine. Git runs with GIT_CONFIG_GLOBAL=/dev/null and GIT_CONFIG_NOSYSTEM=1.

Mutation proof

  • Old code restored (depth < 64 plus unsupported()): all 4 depth tests fail with E_NATIVE_PROJECT_UNSUPPORTED.
  • Suggested alternative (depth < 64 plus return null): the no-project test passes, and the active, pending and unsafe tests fail. With the in-process assertions removed, the CLI assertions alone still fail on active ps (Native Compose state is unsafe or changed) and on active up --detach (Native configuration compiler is unavailable, meaning it reached native startup). The pending CLI assertions fail on a different refusal text.
  • Adopted warning reverted to the legacy text: the generated-files test fails on the detail.

Evidence

  • Bun 1.4.2 (mise x bun@1.4.2).
  • Local (M5): bun run --cwd packages/cli typecheck passes with 0 errors. Root bun run check passes (privacy-check: ok; the 34 Biome warnings are not in the changed files). Biome is clean on the changed files.
  • Local focused run: 12 files (doctor adopted, fix-adopted, generated-files and json tests, the adoption generation, source-bind and routing-generation tests, native-compose-open, project-input-selection, native-project-inputs, and the 2 new files) gave 291 pass, 0 fail.
  • gitleaks 8.30.1 (gitleaks dir on a git log -p origin/next..HEAD export): no leaks.
  • Full suite at cfc3886e: hosted CI run 39888208360 passed (all 16 checks), but on a merge ref built before fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432 landed, so it does not cover next as it is now. Superseded by round 2 below.

Gaps and coordination

  • fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432 (HACK-1374) (round 1 text; fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432 has since merged and round 2 below resolves this) conflicts on meaning, not on lines:
    • Its test a start directory deeper than the adoption search bound… expects selectLegacyComposeAdoptedRoot({ cwd: deep }) to reject with E_NATIVE_PROJECT_UNSUPPORTED. With this PR the call resolves to null.
    • Its doc sentence "The adoption search stops after 64 parent directories" becomes wrong.
    • Its isAdoptionSearchBoundRefusal catch becomes dead code. That is harmless, but it could also hide a real future E_NATIVE_PROJECT_UNSUPPORTED.
    • Whichever PR lands second must change that assertion to resolves.toBeNull(), drop the bound sentence, and preferably remove the catch. Its other assertions (findDoctorAdoptedProject null, preflight null, global-only fix plan) still hold.
    • This PR does not touch src/lib/doctor-adopted-project.ts or tests/doctor-adopted-project.test.ts, so there is no textual conflict there. Both PRs edit the doctor paragraph in docs/reference/native-compose-adoption.md, so a nearby-lines conflict there is possible.
  • Not covered: native-host behaviour, and a real Docker engine with an adoption more than 64 levels up. The adopted proof uses the synthetic engine.
  • hack up with no project prints ERROR No .hack/ (or legacy .dev/) found with no E_PROJECT_NOT_FOUND code, while ps, down, logs and open include the code. This happens at every depth and was there before this PR. It is reported as a separate finding.

Round 2: merged next after #432

#432 (HACK-1374) merged into next as bc825421 after this PR's base. A verifier built next 7d9c89f8 plus this PR and found that #432's test a start directory deeper than the adoption search bound gets ordinary discovery instead of a throw failed: it expected the selector to reject with E_NATIVE_PROJECT_UNSUPPORTED, and the selector now resolves to null. The docs and doctor's code comments also still described the 64-directory bound.

Changes (0f747d26, on top of a merge of next 7d9c89f8):

  • src/lib/doctor-adopted-project.ts: removed the isAdoptionSearchBoundRefusal catch in findDoctorAdoptedProject (dead code now, and it would hide a future E_NATIVE_PROJECT_UNSUPPORTED), the now-unused HackCliError import, and the 64-ancestor wording in the doc comments. Doctor calls the selector directly; unsafe adoption state still propagates E_CONFIG_INVALID.
  • tests/doctor-adopted-project.test.ts: fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432's deep-start test now expects selectLegacyComposeAdoptedRoot({ cwd: deep }) to resolve to null. Its doctor assertions are kept unchanged: findDoctorAdoptedProject and preflightDoctorProject return null and the fix plan is the global step only. So fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432's behaviour (a deep start directory gets ordinary discovery, not a throw) is kept.
  • docs/reference/native-compose-adoption.md: replaced "The adoption search stops after 64 parent directories…" in the doctor paragraph with: doctor uses the same adoption search as project commands, so from a directory with no adoption above it, however deep, doctor uses ordinary project discovery. The doc now has no 64-directory bound left.

Evidence at 0f747d26 (Bun 1.4.2, private HOME/HACK_HOME/DOCKER_CONFIG, DOCKER_HOST at a socket that does not exist, HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1, HACK_DAEMON_DISABLE=1):

  • Focused: tests/doctor-adopted-project.test.ts, tests/native-compose-adoption-search-depth.test.ts, tests/doctor-adopted-generated-files.test.ts: 10 pass, 0 fail (host load average about 130).
  • Mutation: with the old bounded walk restored (depth < 64 plus unsupported()), fix(doctor): bound the adopted-project search and use the ok-skip wording; test shared host stop waits #432's deep-start test fails with E_NATIVE_PROJECT_UNSUPPORTED. With the fix it passes.
  • bun run --cwd packages/cli typecheck: exit 0. Root bun run check: exit 0 (privacy-check: ok). Biome clean on the 6 touched source and test files.
  • gitleaks 8.30.1 (gitleaks dir on a git log -p origin/next..HEAD export): no leaks.
  • Full suite (hosted CI test job at 0f747d26): pending: hosted CI at 0f747d26 is running; this line is updated when it finishes.

Refs HACK-1380

🤖 Generated with Claude Code

…s adopted generated-files warning

The adoption selector stopped after 64 directories and then refused the
command as an unsupported adopted command, so `hack ps`, `up`, `down`,
`logs` and `open` from a deep directory with no project exited with
E_NATIVE_PROJECT_UNSUPPORTED. The selector now walks to the nearest project
root or the filesystem root, as ordinary discovery does. Returning "no
adoption" at a bound instead is unsafe: ordinary discovery reads an active
adoption as native input, so `up` from deep inside an adopted project would
reach native startup.

For an adopted project, doctor's generated files warning now says that
--fix leaves the adoption's .hack/.gitignore unchanged instead of promising
to ensure it.

Refs HACK-1380

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

linear-code Bot commented Oct 23, 2026

Copy link
Copy Markdown

HACK-1380

roodboi and others added 2 commits October 23, 2026 11:19
…elector walks every ancestor

After merging next (#432), the adoption selector returns null for a deep
start directory with no adoption, so doctor's E_NATIVE_PROJECT_UNSUPPORTED
catch was dead code that could hide a future refusal. Remove it, update the
doctor helper's comments, the HACK-1374 deep-start test (the selector now
resolves null) and the doctor paragraph of the adoption reference, which
still described the 64-directory bound.

Refs HACK-1380

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi
roodboi marked this pull request as ready for review October 24, 2026 05:48
@roodboi
roodboi merged commit 3823361 into next Oct 24, 2026
17 checks passed
@roodboi
roodboi deleted the wf/g62-adopted-selector-depth branch October 24, 2026 05:48
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