Skip to content

fix(doctor): refuse symlinked .hack/.internal instead of deleting through it, and support adopted projects - #392

Merged
roodboi merged 9 commits into
nextfrom
wf/g33-doctor-symlink-adopted
Oct 22, 2026
Merged

roodboi merged 9 commits into
nextfrom
wf/g33-doctor-symlink-adopted

Conversation

@roodboi

@roodboi roodboi commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Two hack doctor fixes.

  • HACK-1316: doctor no longer follows a symlinked .hack or .hack/.internal. Before this fix, doctor --fix deleted a file outside the project.
  • HACK-1311: doctor now reports an adopted (hack config adopt) project correctly, and doctor --fix completes in one. Before this fix, doctor refused the project as native input, the --fix plan threw, and --fix with tracked generated files failed after it had already changed the Git index.

Rework after review (commit ff7f11e1)

The verifier found one blocking defect in the first version (ffbc4ffd):

  • Defect. In an active adopted project whose generated files are tracked by Git (for example a committed .hack.secret.key), hack doctor --fix --no-interactive exited 1 with E_NATIVE_PROJECT_UNSUPPORTED. It did this after git rm --cached had already run, so .hack/.gitignore was not written and the later fix steps (hackd, Docker networks, Caddy and the rest) never ran.
  • Root cause. maybeUntrackGeneratedFiles selected the adopted project, ran git rm --cached, and then called ensureHackDirGitignore. That writer calls assertLegacyProjectDirectory, which throws ProjectInputSelectionError("native") for an active adoption (src/lib/project-input-selection.ts).
  • Fix. For an adopted project, the step now changes only the Git index. It still adds a committed .hack.secret.key to the root .gitignore, which is not an adoption input. It leaves the adoption's .hack/.gitignore unchanged and says so in a closed note: Removed N path(s) from the git index. Left .hack/.gitignore unchanged: an adopted Compose project's .hack/ belongs to 'hack config adopt'. The plan line names the same change. Legacy projects behave exactly as before.
  • Docs. docs/reference/native-compose-adoption.md now matches the code: --fix leaves .hack/.gitignore unchanged and adds a committed key to the root .gitignore.
  • Test gaps closed.
    • The findDoctorLegacyProjectContext guards on the later --fix steps had no regression test. The new test runs the whole --fix flow with Docker reachable and --migrate-env-config, so the lifecycle, incomplete-runtime and env-migration steps run against the adopted project.
    • The writer's refusal before mkdir (HACK-1316) had no test. A new unit test covers it.

Update: merge with next at 5c9cc82e (commits 0d6aa587, 51244fc9)

  • 0d6aa587 merges origin/next at 5c9cc82e (fix(native): give storage proofs their own budget outside the start phase #375, fix(project): leave read-only .hack/.gitignore alone and report symlinked .hack in doctor #407, fix(env): share global Compose env references and refuse an oversized environment #412) with no conflicts. fix(project): leave read-only .hack/.gitignore alone and report symlinked .hack in doctor #407 changes the same .hack/.gitignore code in doctor: the repair helper now returns { changed, readOnly } and the untrack step can leave a read-only file alone. After the merge, the adopted branch of the untrack step still skips the .hack/.gitignore writer, and only the legacy branch uses fix(project): leave read-only .hack/.gitignore alone and report symlinked .hack in doctor #407's read-only handling.

  • 51244fc9 is a one-line docs fix. docs/reference/native-compose-adoption.md listed every check that doctor skips for an adopted project except the .hack/.gitignore check. The code already skips it.

  • The verifier's repro, rerun on the merged head. It is the verifier's own scratch test: retainedSourceBindFixture, a real git init, git add -f .hack.secret.key, adopt (prepare and publish), then runCli doctor --fix --no-interactive, with shell run mocked. The output has no E_NATIVE_PROJECT_UNSUPPORTED. git ls-files is empty, .hack/.gitignore is not created, the closed note above is printed, the adoption is still active, and the later steps run (the global daemon start call and the closing Re-run: hack doctor note).

  • Why that run still exits 1. Doctor's exit code comes from the checks that run before --fix. This is existing behavior and is the same for legacy projects. The verifier's harness makes every non-Git exec fail, so docker daemon is an error, and the committed key is an error in the generated files and env key checks. The PR's doctor-fix-adopted-project test makes Docker reachable. Its .hack/.env.state.json case exits 0. Its key case exits 1 only because of the committed-key error.

  • Mutations rerun on the merged head, each killing its test for the intended reason:

    Mutation Result
    Adopted untrack step writes .hack/.gitignore again (ffbc4ffd behavior) doctor-fix-adopted-project 0/2, with E_NATIVE_PROJECT_UNSUPPORTED in the output
    findDoctorLegacyProjectContext no longer skips an adopted project (survived the first review) doctor-fix-adopted-project 0/2, with E_NATIVE_PROJECT_UNSUPPORTED
    Writer's pre-mkdir refusal removed (survived the first review) compose-env-at-rest 15/16; "the writer refuses a symlinked .hack before creating .internal behind it" fails

Root cause

HACK-1316: symlinked .hack/.internal

src/lib/compose-env-at-rest.ts checked that .hack/.internal was a real directory, but only inside readReplaceablePrivateFile, and it never checked .hack. When that read refused, inspectEntry called lstat(path) to choose between absent and unverifiable. That lstat followed the .internal link, found the outside compose.env.override.yml, and classified it as unverifiable. With includeUnverifiable, the --fix removal then ran lstat, rename and unlink through the same link, which deleted the outside file. The stale-aside sweep ran readdir on the linked directory before any symlink check, so it could also unlink .remove-shaped files in the outside directory.

HACK-1311: adopted projects

An active or pending adoption makes inspectProjectInputsAtRoot classify the root as native. Doctor's first step, assertLegacyProjectDiscovery, therefore threw E_NATIVE_PROJECT_UNSUPPORTED. Every project check and buildDoctorRemediationPlanLines call findProjectContext, which throws the same refusal. Every --fix step that writes legacy project files does too.

Fix

HACK-1316 (src/lib/compose-env-at-rest.ts)

  • One no-follow ownership check now covers both .hack and .hack/.internal. The writer and every read, removal and sweep use it.

  • A new refused state covers these cases:

    • a symlinked or non-directory .hack or .hack/.internal;
    • a symlinked or non-regular override.

    Doctor reports it with a closed, value-free message. A refused override is never eligible for removal.

  • Removal returns before the sweep when the directories are not real. It checks the directory identities again before the move and before each aside unlink. The aside listing runs only on the held directory.

  • The writer refuses a linked .hack or .internal before mkdir, so it creates nothing through the link.

HACK-1311 (src/commands/doctor.ts, src/lib/doctor-adopted-project.ts)

  • Doctor selects an active or pending adoption first, with selectLegacyComposeAdoptedRoot, the selector that adopted ps, logs and the other adopted commands use. The legacy-discovery refusal runs only when no adoption is found.
  • Checks for an adopted project:
    • The project check names the adopted root. A pending adoption is a warning that asks for explicit recovery.
    • The env key, tracked generated files and Compose env at rest checks run against the adopted root.
    • Each check that reads adoption-owned inputs is listed as not checked.
    • Agent integrations use the adopted root, and sessions mux uses global settings.
  • --fix for an adopted project:
    • The plan says it leaves the adoption's inputs, retained containers and state alone, and it skips env migration.
    • Lifecycle, incomplete-runtime and env-migration repairs treat the project as having no legacy project.
    • Untracking generated files changes only the Git index (see the rework above).
  • The legacy project-check block moved unchanged into runProjectChecks, to stay under the complexity limit. The fix(native): keep Compose-only projects on the Compose backend with closed refusals #365 compose backend features check and the fix(env): keep decrypted Compose env values off disk #357 env-at-rest adopted fallback are unchanged.

There are no CLI surface changes and no agent-guidance changes.

Tests

Test Proves Mutation that kills it
doctor-fix-adopted-project (new), 2 cases: a committed .hack.secret.key and a committed .hack/.env.state.json Real runCli doctor --fix --no-interactive --migrate-env-config in an adopted fixture with a real git init. Docker is reachable through the mock, and Git runs only on the fixture with no global or system config. Each case checks:
  • no E_NATIVE_PROJECT and no canary or key text;
  • the exact plan line and step note;
  • the closing notes, which prove every later step ran;
  • git ls-files is empty and the file is unchanged on disk;
  • the root .gitignore gets the key entry only in the key case, and .hack/.gitignore is not created;
  • the receipt is unchanged, the adoption is still active, and no stub engine command or project run call happens.
Exit code: 0 for the env state file. For the key it is 1, because the env key check reports the committed key as an error before --fix runs. That is existing doctor behavior.
  • ffbc4ffd behavior (always write .hack/.gitignore): 0/2 pass, exit 1.
  • findDoctorLegacyProjectContext no longer skipping adopted projects: 0/2 pass, exit 1.
  • Plan line reverted to the legacy wording: 0/2 pass.
compose-env-at-rest: "the writer refuses a symlinked .hack before creating .internal behind it" (new) A link to an outside directory without .internal gets nothing created behind it, and the refusal is closed. Writer's pre-mkdir refusal removed: fails (this mutation survived before).
doctor-fix-noninteractive: "doctor --fix leaves a file behind a symlinked .hack/.internal untouched (HACK-1316)" Real runCli doctor --fix: the outside file survives, and the plan and notes do not name it. origin/next's helper: fails with ENOENT, because the outside file was deleted.
compose-env-at-rest: symlinked .hack and .hack/.internal (2 cases) refused state, closed message, no repair planned, no asides removed with a stale-aged clock, writer refusal, outside contents byte-identical. Old helper: both fail. Directory list without .hack: the .hack case fails. inspectEntry always unverifiable: the symlinked-override case fails.
doctor-adopted-project: doctor --json and the --fix plan in an adopted project Closed project message, the checks that run, the not-applicable list, the exact plan, the receipt unchanged, no container start or stop. Adopted branch of runProjectChecks disabled: fails. Preflight forced back to legacy discovery: fails.

All tests use temporary directories only. They do not touch the real HOME, ~/.hack, the Docker socket, the keychain or launchd.

Evidence

  • Toolchain. Bun 1.4.2 (mise x bun@1.4.2), bun install --frozen-lockfile. bun.lock is unchanged.
  • Local gates on the current head 51244fc9 (M5):
    • bun run --cwd packages/cli typecheck: 0 errors.
    • Root bun run check: exit 0, including privacy-check: ok (34 existing warnings in other files).
    • Biome on the 7 changed source and test files: clean.
    • gitleaks 8.30.1: gitleaks dir over a git log -p origin/next..HEAD export: no leaks.
  • Focused tests at 51244fc9, each file run on its own with --timeout 120000 (the M5 was under heavy load), all passing:
  • The five doctor --fix and at-rest files also pass together in one process (41/41), which checks that the scoped module mocks do not interfere.
  • Under the same load, doctor-fix-adopted-project with Bun's default 5 s timeout failed in a fixture hook. It passes with the longer timeout. This is load, not a code change.
  • Full suite on the M3: not run. The runner for 91dae070 waited about 6.5 hours and never got the shared CI lock. I stopped it and queued a runner for 51244fc9, which waited about 50 minutes. During that time the lock directory had existed since 12:37 with no suite process running on the M3, several other units were also waiting, and free space was about 57.6 GiB, under the 58 GiB floor. I did not touch the lock, since I could not prove who owned it. I stopped my runner. It created no worktree or ref.
  • Hosted CI: all 16 checks pass on the current head 51244fc9 (run 39839971608): the full-suite test job (39 min), all four docker-e2e shards, candidate core, config compiler, runtime state models, and secret scan. Earlier heads 0a94d881, a2470049 and 91dae070 also passed.

Remaining gaps

  • No M3 full suite. It did not run because the shared lock was blocked and free space was under the floor (see Evidence). The hosted full-suite test job ran at 51244fc9 on ubuntu, not on macOS.
  • Symlinked .hack now refused by the writer. A Compose command that renders the env override now refuses a symlinked .hack, not only a symlinked .internal. This matches the existing .hack/.env rule. A project that relied on writing through a linked .hack will see a closed refusal.
  • Small race window remains. Directory identities are checked again before each move and unlink, not held as open descriptors. A same-user swap in that window remains, as already documented for the move-aside window.
  • Other generated files can be committed again. For an adopted project, --fix does not add the untracked generated files under .hack/ to any ignore file. Only the key gets a root .gitignore entry. The note asks the user to keep them out of later commits.
  • --fix with a committed key exits 1. The exit code reflects the checks that ran before the fix, for legacy and adopted projects alike. The fix itself completes and removes the key from the Git index, which is what the tracked-key checks read.
  • Pending adoption not tested. The pending-adoption warning has no test: the fixture has no cheap pending state.
  • Native backend skips adopted project checks. With HACK_RUNTIME_BACKEND=native, native doctor still skips project checks for an adopted project. This is unchanged.

Refs HACK-1316
Refs HACK-1311

🤖 Generated with Claude Code

roodboi and others added 3 commits October 21, 2026 23:51
…ead of deleting through it

Doctor's Compose env at-rest check, its --fix removal and the stale-aside
sweep now apply the writer's no-follow ownership check to .hack and
.hack/.internal (and the override itself) before they list, read, move or
unlink anything. A link reports a closed, value-free refusal and nothing
behind it changes. Before, a symlinked .hack/.internal made doctor report
an outside file as unverifiable and --fix deleted it.

Refs HACK-1316

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Refs HACK-1316

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… as native input

An active or pending `hack config adopt` makes ordinary project discovery
refuse the project as native input, so `hack doctor` stopped at its
preflight with E_NATIVE_PROJECT_UNSUPPORTED, every project check that
reached discovery failed, and the --fix plan threw. Doctor now selects the
adopted root first, reports it with a closed project message, runs the
checks that do not read adoption-owned inputs (env key, tracked generated
files, env values at rest), lists the others as not checked, and plans
--fix without touching the adoption. Ordinary native input is still
refused, and the Compose-backend features check is unchanged for legacy
projects.

Refs HACK-1311

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

linear-code Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

HACK-1311

HACK-1316

roodboi and others added 2 commits October 22, 2026 05:24
…e native-input refusal

`hack doctor --fix` in an active adopted project with tracked generated
files ran `git rm --cached` and then called the legacy `.hack/.gitignore`
writer, which refuses an adopted project as native input. The run exited 1
with E_NATIVE_PROJECT_UNSUPPORTED after changing the Git index, and the
later fix steps never ran.

For an adopted project the step now changes only the Git index (and adds a
committed `.hack.secret.key` to the root `.gitignore`), leaves the
adoption's `.hack/.gitignore` unchanged with a closed note, and the plan
says so. Regression tests run the whole `--fix` flow in an adopted fixture
with Docker reachable and `--migrate-env-config`, so they also cover the
legacy-context guards of the later steps; another test covers the env
override writer's refusal before mkdir (HACK-1316).

Refs HACK-1311, HACK-1316

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi roodboi changed the title fix(doctor): refuse symlinked .hack/.internal instead of deleting through it, and report adopted projects fix(doctor): refuse symlinked .hack/.internal instead of deleting through it, and support adopted projects Oct 22, 2026
roodboi and others added 4 commits October 22, 2026 06:25
Resolve doctor.ts against #395: keep the extracted runProjectChecks and add
the new 'hack gitignore' check to its legacy set; untrack keeps the adopted
branch and uses the no-follow ensureHackDirGitignoreForDoctor otherwise.
The new --fix gitignore step skips adopted projects (it otherwise hit the
HACK-1311 native-input refusal), and adopted doctor reports 'hack gitignore'
as not applicable, with a test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the env-secrets threat model row: keep next's HACK-1318 adoption
recovery refusal text and this branch's HACK-1316 no-follow sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #375, #407 and #412. #407 changes the doctor .hack/.gitignore
repair helper and the untrack step; this branch's adopted-project branch of
that step still skips the .hack/.gitignore writer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ecked adopted checks

Doctor already lists 'hack gitignore' as not checked for an adopted Compose
project; the reference listed every other skipped check but this one.

Refs HACK-1311

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi
roodboi marked this pull request as ready for review October 22, 2026 23:40
@roodboi
roodboi merged commit 2dbd1db into next Oct 22, 2026
16 checks passed
@roodboi
roodboi deleted the wf/g33-doctor-symlink-adopted branch October 22, 2026 23:41
roodboi added a commit that referenced this pull request Oct 23, 2026
…ding; test shared host stop waits (#432)

## Summary

Follow-ups to #392 (doctor in adopted projects, symlinked `.hack`) and #389 (shared host stop
waits), merged into `next` at `4e0bfa55`.

- **Deep start directory.** `hack doctor` no longer throws `E_NATIVE_PROJECT_UNSUPPORTED` when
  it starts more than 64 directories deep. It now uses ordinary project discovery.
- **Adopted-project skips.** The 12 checks that doctor does not run for an adopted project now
  use doctor's skip wording. They keep status `ok`.
- **New tests.** A pending adoption, `doctor --fix` with a symlinked `.hack`, a census that
  fails
  during the stop wait, and the length of the KILL-confirm wait.
- **Docs.** The shared-host test row for a running PID with a new birth now describes what
  production does.

## Root cause

1. **Skip convention.** #392 reported the 12 checks as `ok` with `Not checked: …`. Every other
doctor skip uses `Skipped (…)`. 2. **Deep start directory.** `preflightDoctorProject` called
`selectLegacyComposeAdoptedRoot`    without a catch. With no `--project`, that selector walks up
at most 64 directories. If it    reaches no root in that walk, it throws `unsupported()`
(`E_NATIVE_PROJECT_UNSUPPORTED`). From    a start directory more than 64 levels deep, every
doctor run and every `--fix` step that looks    for an adoption threw. Ordinary discovery has no
such limit. 3. **Test gaps.** No test covered the pending-adoption warning or `--fix` in a
pending    adoption. There was a CLI regression for a symlinked `.hack/.internal`, but none for
a    symlinked `.hack`. In #389, mutants M7 (the old loop, which stops waiting on a failed
census)    and M10 (`awaitExit(0)` after KILL) survived. 4. **Docs.** The row "running PID with
a new birth" said that release sends KILL. That is true    only at the simulated signal
boundary. In production, `signalVerifiedProcessTree` checks the    birth time again before it
sends a signal, so a PID with a different birth time gets no    signal.

## Fix

- **Skip convention: keep `ok` and use the `Skipped (…)` wording.** The new message is `Skipped
  (adopted Compose project: its inputs belong to 'hack config adopt')`. Doctor already uses two
  kinds of skip:
  - `warn` when the skip points at something to fix, such as `Skipped (no .hack/ found)` or
    `Skipped (docker runtime unavailable)`;
  - `ok` when the skip is expected in a healthy setup, such as `Skipped (not a git checkout)`.

These 12 checks never apply to an adopted project, so `warn` would add the same 12 warnings to
  every healthy adoption in the summary and in `counts.warn`, and real warnings would be harder
  to
  see. Problems that belong to the adoption still show up in the `project` check (`warn` for a
  pending adoption) or in the adopted commands. The reasoning is in a comment in
  `src/lib/doctor-adopted-project.ts`, and in `docs/reference/native-compose-adoption.md`.
- **Deep start directory: a closed result.** `findDoctorAdoptedProject` catches only the
  selector's search-limit refusal and returns `null`, meaning no adoption was found. Ordinary
  discovery then runs. With no `--project`, `E_NATIVE_PROJECT_UNSUPPORTED` from this selector
  can
  only mean the search limit. Unsafe adoption state throws `E_CONFIG_INVALID`, and that still
  stops doctor. `preflightDoctorProject` moved from `src/commands/doctor.ts` to
  `src/lib/doctor-adopted-project.ts` so the tests can call it. Its behavior did not change.
- **Docs.** In `docs/reference/shared-host-lifecycle.md`, the row now says that release passes
  the
  member to the KILL step and waits the whole `SHARED_HOST_KILL_CONFIRM_MS`. The simulated
  boundary records the request, but in production no signal reaches that PID. There are new rows
  for the two new tests.

## Tests (every new test proven by mutation)

The private env is set by a runner script: `HOME`, `HACK_HOME` and `DOCKER_CONFIG` point to a
temp directory, `HACK_SECRETS_DISABLE_KEYCHAIN_FALLBACK=1`, and `DOCKER_HOST` points to a socket
that does not exist. `doctor-fix-noninteractive` runs without `HACK_HOME`, because its existing
daemon test finds `~/.hack` through its own private `HOME`. The pending-adoption tests use a new
fixture helper, `leaveRetainedSourceBindOperationPending`. It runs a retained `start` whose
callback reports failure, so the store keeps the pending journal. No engine command runs, and
nothing sleeps.

| Test | Mutation | Result |
| --- | --- | --- |
| `doctor-adopted-project`: the existing adopted `doctor --json` test now also checks the exact skip text and that there are 12 skipped checks | D1: status `warn` | killed |
| same | D2: the old `Not checked: …` text | killed |
| `doctor-adopted-project`: "a start directory deeper than the adoption search bound gets ordinary discovery instead of a throw". It uses 70 nested directories, first checks that the selector itself throws, then expects `findDoctorAdoptedProject`, `preflightDoctorProject` and the `--fix` plan to return without an error | D3: no catch (the #392 code) | killed: `E_NATIVE_PROJECT_UNSUPPORTED` |
| `doctor-adopted-project`: "unsafe adoption state still refuses doctor". The state root is a regular file, and the test expects `E_CONFIG_INVALID` with no canary text and no path in it | D4: catch every error | killed |
| `doctor-adopted-project`: pending adoption. `doctor --json` gives a `warn` `project` check with the exact message and `ok: true`. The 12 checks are skipped, the `--fix` plan is checked from the root and from a subdirectory, the adoption is still pending, the receipt is unchanged, and no container or project command runs | D5: pending reported like active | killed |
| same, plus `doctor-fix-adopted-project`: real `runCli doctor --fix --no-interactive --migrate-env-config` with a pending adoption and a committed `.hack/.env.state.json` (real `git init`). It checks exit 0, the plan line, the untrack note, the env-migration skip, the closing note, an empty `git ls-files`, no `.hack/.gitignore`, the adoption still pending, the receipt unchanged, and no engine command or project `run` call | D6: pending no longer selected | killed (both) |
| `doctor-fix-adopted-project` pending case | D6b: the untrack step treats a pending adoption as legacy | killed: exit 1 from the legacy writer's refusal |
| `doctor-fix-noninteractive`: "doctor --fix leaves an override behind a symlinked .hack untouched". The whole `.hack` links to an outside directory that holds a file with the override's name. The test expects that file unchanged, both directory listings unchanged, `.hack` still a link, and the notes do not mention the file | D7: the ownership check covers only `.internal` | killed: ENOENT (the outside file was deleted) |
| `shared-host-lifecycle`: "a census that fails during the TERM wait keeps the wait going". The first three polls fail and poll 4 shows the helper gone. The test expects `stopped`, 4 polls and TERM only | M7: the old loop | killed: `stop-unconfirmed` |
| `shared-host-lifecycle`: "after KILL, release keeps polling for the whole kill-confirm wait". A 100 ms grace; the helper leaves the census only 80% of the way into `SHARED_HOST_KILL_CONFIRM_MS` after KILL. The test expects `stopped`, TERM then KILL, and a return at or after 80% of that wait | M10: `awaitExit(0)` | killed |
| Existing "running with another birth time" test, which now also expects the return at or after `SHARED_HOST_KILL_CONFIRM_MS` after KILL | M10, and M10b: `awaitExit(SHARED_HOST_KILL_CONFIRM_MS / 2)` | both killed (both tests) |

For each mutation, the test was run alone and failed for the reason shown. The file was restored
afterwards.

## Evidence

- **Toolchain.** Bun 1.4.2 (`mise x bun@1.4.2`), `bun install --frozen-lockfile`. `bun.lock` is
  unchanged.
- **Local checks (M5):**
  - `bun run --cwd packages/cli typecheck`: 0 errors.
  - Root `bun run check`: exit 0, including `privacy-check: ok`.
  - Biome on the 7 changed source and test files: clean.
  - gitleaks 8.30.1, `gitleaks dir` over a `git log -p origin/next..HEAD` export: no leaks.
- **Focused tests:** 179 pass, 0 fail, across 10 files: `doctor-adopted-project`,
  `doctor-fix-adopted-project`, `shared-host-lifecycle`, `compose-env-at-rest`, `doctor-
  command`,
  `doctor-json`, `doctor-generated-files`, `doctor-env-key`,
  `native-compose-adoption-source-bind-generation` and
  `native-compose-adoption-source-bind-fixture`. `doctor-fix-noninteractive` passed 11 of 11.
  `shared-host-lifecycle` also passed 3 more times in a row, 38 of 38 each.
- **Hosted CI at `1345ce5a`:** all 16 checks pass (run 39876007802). These include the
  full-suite `test` job (42 min, ubuntu), `linux-process-lifetime` (which runs
  `tests/shared-host-lifecycle.test.ts`), all four `docker-e2e` shards and `secret-scan`.
- **Full suite on the M3: not run.** I queued a runner for `1345ce5a` at 20:04. It would take
  the shared CI lock and start only with at least 60 GiB free. It waited about 52 minutes.
  During that time, someone else had held the lock directory since 17:39, two other units were
  queued ahead of mine, and free space was 55.1 to 56.6 GiB, under the 58 GiB floor. I stopped
  my runner before it took the lock. It created no worktree, ref or lock, and I did not touch
  the existing lock.

## Gaps

- **No M3 full suite.** The shared lock was blocked and free space was under the floor (see
  Evidence). The hosted full suite ran on ubuntu, not on macOS. The focused tests ran on macOS.
- **Search limit is unchanged.** Doctor does not search past the selector's 64 parent
  directories. An adopted root that is more than 64 levels above the start directory falls
  through to ordinary discovery, which refuses it with the existing closed native-input error.
- **Other commands still throw.** The adopted-command selector has the same throw for other
  commands, for example the native Compose command path and `native-compose-open`. This PR does
  not change them; it is reported as a finding.
- **No CLI or tooling regeneration.** There are no CLI surface, agent guidance or `package.json`
  script changes, so nothing needed to be regenerated or re-pinned.

Refs HACK-1374

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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