Skip to content

train b52 integration gate (do not merge) - #472

Closed
roodboi wants to merge 41 commits into
nextfrom
train/b52
Closed

roodboi wants to merge 41 commits into
nextfrom
train/b52

Conversation

@roodboi

@roodboi roodboi commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

Integration gate for merge batch b52: origin/next + #399 (HACK-1347, HACK-1348) + #441 (HACK-1380) + #447 (HACK-1387) + #449 (HACK-1379) + #453 (HACK-1386) + #451 (HACK-1389) + #466 (HACK-1410) + #463 (HACK-1411, HACK-1383) + #460 (HACK-1412) + #467 (HACK-1414) + #462 (HACK-1415) + #442 (HACK-1381, CI sharding last). Tree aaf21185. Closed after the batch squashes.

🤖 Generated with Claude Code


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

roodboi and others added 30 commits October 22, 2026 02:14
…s unique

HACK-1347: in the shared parallel test process, an unrelated test's pre-exec
fork (process::clean_command) holds a copy of every open descriptor until it
execs. Endpoint discovery then sees the fork as a second holder of the test's
listener (or finds a listener the test already dropped), and the lsof release
checks see disk handles the fixture had just written, so they correctly refuse
before the result under test. Run every such fixture alone in a child test
process with the shared run_isolated helper:

- host_endpoint: exclusive and executable-pinned discovery, plus the two
  close/rebind fixtures that still shared the process; the module's own
  runner now delegates to run_isolated (120 s hang guard, not 5 s).
- admission_pool: every release-check test that expects a pass or a non-active
  refusal (7 more), behind one isolated() helper that keeps serial scans.
- short_home, host_filesystem, interrupted: recovery, migration and stop tests
  that need the lsof proof to pass.

HACK-1348: fixtures named scratch directories from the PID and the wall clock
and created them exclusively. macOS reports the clock in whole microseconds,
so parallel tests can read the same value and the second create fails with
"File exists (os error 17)". Add crate::next_test_scratch() and append it to
every such name in the lib tests, with a regression test.

No product code changes.

Refs HACK-1347
Refs HACK-1348

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
HACK-1347 audit, second part: more runtime-core tests drop a listener (or a
publication that owns one) in the shared parallel test process and then
require a release check to pass. An unrelated test's pre-exec fork
(process::clean_command) holds a copy of every open descriptor until it
execs, so the dropped listener still accepts and its flock stays held; the
check correctly refuses and the test fails. Under in-process fork pressure
with a 20 ms pre-exec hold the full all-features lib suite failed 3/3 runs
on 9 to 13 such tests.

- Make provider::test_executable available on every Unix target (the
  quiescent-recovery tests also run on Linux) and isolate, with run_isolated:
  dependency_slots socket recovery (2), quiescent_recovery dropped-listener
  tests (2), foreground::transport abandoned-publisher tests that expect
  retirement to pass (7), acknowledged_publisher::missing_lock (5, all use
  the dead-publisher fixture), native foreground abandoned publication (1)
  and the https_recovery full commands (2, their fixture drops the provider
  flock that recovery must then acquire).
- Fixtures that only need a stale socket inode now bind a socket that never
  listens (new provider::test_socket::bound_unlistened, shared with the
  quiescent-recovery fixture) instead of dropping a listener: https_recovery
  and dependency_slots::fixture_prior_boot. Regression test included.
- relay_exec framing test: shut the peer down instead of only dropping it,
  so the truncated frame always reaches end of stream.

No product code changes.

Refs HACK-1347

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
provider::publisher::tests::Fixture picked a port by binding 127.0.0.1:0,
reading the port and dropping the probe, then started stream-relay-host on
that explicit port. In the shared parallel test process an unrelated test's
pre-exec fork (process::clean_command) can still hold the probe listener, or
another test can take the port, and the publisher exits 73 without
readiness ("publisher readiness missing", 2 of 5 full-suite runs under fork
pressure with a 20 ms pre-exec hold).

Start the publisher with --publish 0 (from #382): it binds a kernel-chosen
port and reports it as "ready <port>", which the fixture now requires.

Refs HACK-1347

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…60 min limit

The serial `test` job ran all ~570 bun test files in one Turbo task and took
37-42 min, so slow hosted runners were cancelled at GitHub's 60 min job
limit, and Turbo's buffered output could not name the file that hung.

CI now runs four `test shard i/4` jobs. scripts/test-shard-plan.ts splits the
`bun run test` file set (root tests/ with bunfig preloads, plus packages/db)
by the checked-in durations in scripts/test-shard-durations.json, longest
first, with a default for new files. scripts/test-shard.ts runs each file in
its own `bun test` process from its suite directory, prints a start and a
result line per file, stops a file after 12 min and the shard at 30 min or on
SIGINT/SIGTERM, and then names the running file, the unstarted files and the
slowest files. Every shard first verifies that the four shards cover the
files git lists exactly once. The required `test` check is now a fan-in that
passes only when every shard succeeded; the prerelease gate also requires
each shard by name.

Refs HACK-1381

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rain late

The foreground graph owner drained its child's stdout and stderr for only
250 ms after exit. Under heavy oversubscription the pipe readers could be
scheduled after that window, so the closed native error code (or readiness
line) was dropped and a generic startup failure was reported.

Replace the window with the named, documented NATIVE_EXIT_DRAIN_BOUND_MS
(5 s), still measured from the observed exit. EOF still ends a normal drain
at once; the bound only ends a drain that a descendant holding the pipes
keeps open.

Tests: a writer that emits the code only after the stand-in is reaped is
kept (fails with the 250 ms window), and a held descendant still settles
within the bound while it is alive (fails with a widened drain).

Refs HACK-1350

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 4dfbb6a)
…it bound

Bounded one-shot requests (status, logs, exec, one-off, read and recovery)
stopped reading the child's stdout and stderr 250 ms after its exit. Under
heavy oversubscription the closed native error code could arrive after that
window, so it was dropped and a generic request failure was reported.

Share NATIVE_EXIT_DRAIN_BOUND_MS (5 s, measured from the observed exit) from
the runtime client with the foreground graph owner, which #396 moved off the
same 250 ms window. EOF still ends a normal drain at once; the bound only
ends a drain that a descendant holding a pipe keeps open. Both boundaries
take an optional exitDrainBoundMs test seam that defaults to the constant.

Tests: a stderr writer that emits the closed code only after the stand-in is
reaped is kept for status, read, logs and exec (fails with a 250 ms window);
the held-descendant cases now settle within the named bound plus a margin
(fail with an unbounded drain). The graph owner's late-writer case injects a
wide named bound and uses named handshake budgets so host load cannot flake
it; its held-descendant cases keep the production bound.

Refs HACK-1387

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… every selector in Loki hints

Follow-ups to #417 for Loki-backed `hack logs` (HACK-1379).

- The global logging compose gives Alloy a named `alloy-data` volume at
  `/var/lib/alloy/data` and repeats `--storage.path` there, so read positions
  survive a recreated container instead of re-reading every retained log.
- `lokiTail` removes its SIGINT listener when the socket closes, so repeated
  follows in one process no longer pile up listeners.
- Empty-snapshot and empty-follow hints carry `--tail`, `--profile`,
  `--pretty`, `--json` and a single service, and name `--services`, `--query`,
  `--since` and `--until` when the compose backend cannot apply them.
- The empty-follow notice timer is injectable; tests drive it by hand and prove
  the tail keeps following after the notice.
- The template comment and docs/architecture.md state Alloy v1.12.0's exact
  reopen condition (`State.Running || finished.Unix() >= target.Last()`).

Refs HACK-1379

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s and exec refusals

The logs and exec "inherited pipe refuses output" cases spawned a keeper
that slept 800 ms: longer than the former 250 ms window, but inside the
shared NATIVE_EXIT_DRAIN_BOUND_MS, so its EOF now settled the output and
the requests resolved instead of refusing.

The keeper now holds the inherited streams until the test releases it after
the refusal (self-capped at 60 s), so the drain can only end at the bound,
and the case budgets derive from that bound.

Refs HACK-1387

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both sides carry #396 (HACK-1350); next squashed it as 7339f0c while this
branch holds it as 631d3fc. Kept this branch's files, which add the shared
NATIVE_EXIT_DRAIN_BOUND_MS import and the exitDrainBoundMs test seam on top.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Follow-up to #377 (HACK-1333) for HACK-1389. Four gaps in the application
benchmark protocol checker:

- An out-of-range summary number is now reported alongside
  summary_revision_mismatch and summary_workload_unknown, as the page says,
  instead of being hidden by their early return.
- A repeated object key is refused as protocol_key_duplicate or
  summary_key_duplicate. json.loads kept only the last value, so a NaN under
  a shadowed key was never scanned and the protocol validated.
- An integer literal longer than MAX_JSON_NUMBER's 16 digits parses as just
  out of range, so a 5000-digit literal is protocol_number_out_of_range on
  Python 3.9 and 3.11+ alike (3.11+ refused the conversion, which surfaced
  as protocol_unreadable). Exact identity checks treat an unserializable
  value as different instead of raising.
- A test pins the page's ±(2⁵³ − 1) bound and the code names to
  MAX_JSON_NUMBER and DIAGNOSTICS.

Refs HACK-1389

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An env layer lock that Hack can never reclaim (a malformed or untrusted
  receipt, a reused PID, a dead owner behind a leftover `<lock>.recovery`
  guard) made every set/add/unset/hydrate refuse with "Retry when it
  finishes." forever. The timeout now names the lock path and the recovery
  step, and keeps the registry's "recovery guard is busy" detail with both
  paths. Closed, one line, value-free.
- The unignored-key refusal no longer says Hack "could not add" the entry
  when the root .gitignore already lists the key and a later rule
  re-includes it; it names that rule as the cause.
- A later `git init` is out of scope for env writes (Hack runs no code
  then). `hack doctor`'s env key check is the backstop: it flags the
  unignored key, and the next --secret write repairs the entry. Documented
  in docs/env.md and covered end to end.
- Tests: paused-unset compare-and-swap conflict, unreclaimable lock
  receipts, recovery guard (set and hydrate), re-include message, and the
  git-init doctor scenario. Each was proven by mutation.

Refs HACK-1386

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oll costs

Alloy v1.12.0 reopens an exited container's log when
`finished.Unix() >= target.Last()`. A reopen reads from the position
second, which Docker includes, so it reads the last line again and moves
`Last()` past the finish time. Only a container that never logged a line
is reopened every 5 seconds; one that logged, or whose lines the
positions file covers, is reopened once and then stays closed. Every
discovered container still costs one inspect call per poll.

Refs HACK-1379

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The late-writer cases waited only for the stand-in's pid file to exist and
then read it. writeFileSync creates the file before it writes the digits, so
under load the read could return "": Number("") is 0, kill(0, 0) always
succeeds, and the reaped-stand-in wait expired. Both files now read pid
handshakes through waitForPid, which waits for a safe-integer pid above 1,
and the late-writer stand-ins publish their pid as a torn handshake (empty
file, 50 ms gap, then the pid) so an existence-only reader fails every run.
The keeper pid reads use the same helper.

Refs HACK-1387

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The exec env-change test asserted an empty stdout but relied on an
earlier test file leaking HACK_LOGGER=console into the shared bun
process. Run alone (as each test shard now runs files), the logger fell
back to clack on hosts without gum, which writes the CoreDNS/Caddy
warnings to stdout, so the assertion failed. Set HACK_LOGGER=console in
beforeAll and restore it in afterAll so the file passes in any order.

Refs HACK-1381

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

The spawn-env isolation guard counts any whole-object use of process.env as
a copy of the runner's environment, so withPrivateEnvironment's
Object.assign(process.env, overrides) failed it in hosted CI. Set each
override by key and restore it with the shared restoreEnv helper; the
values were already private.

Refs HACK-1386

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…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>
…itignore

ensureHackGitInfoExclude now applies the same probe-error policy as
ensureHackDirGitignore: an ENOENT from the write probe counts as writable
(the rename recreates the file); any other probe error is rethrown and
the best-effort wrapper skips the update with a debug log.

Also restores the exact toEqual({ follow: true }) assertion for doctor
and up, stops asserting Bun's null resolve value of fs.promises.access,
and corrects docs/cli.md: under clack the refusal is two lines (the
guide line and the marker line), not one.

Refs HACK-1412

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Resolve the src/commands/doctor.ts docblock conflict with #448 to the
PR side: keep the extended checkProjectEnvKey docblock and its export.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#405 (HACK-1392) landed its own fix for the Loki follow SIGINT leak: the
handler is installed just before the tail awaits close and removed in a
finally. Keep that version and drop this branch's close-handler removal and
its two SIGINT tests in favour of next's; keep the injectable notice
scheduler, the Alloy positions volume and the selector-carrying hints.

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

Follow-ups to #452 (HACK-1390) on the target inspector.

- LegacyComposeAdoptionSelectionError's `permission` and `input-access`
  kinds now keep the same non-enumerable class/code diagnostic as
  ProjectInputAccessError (read with
  legacyComposeAdoptionSelectionDiagnostic). The helpers move to
  src/lib/value-free-diagnostic.ts so both errors share them.
- Pin that inputPresent() classifies through projectInputAccessFault by
  faulting the real presence check with EPERM, EACCES and EIO.
- Paired CLI/MCP fixtures now cover .hack and .hack/.internal at modes
  000 and 0644, matching the proof-matrix row.
- The input_permission_denied message assertions run unconditionally,
  also as root.

Refs HACK-1415

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ched task surfaces

Close the task catalog follow-ups from #430 and #409.

HACK-1411: older Git's `rebase--helper --add-exec-commands cmd` is now
refused through a `rebase`-scoped inline flag, so `git config --add`
still passes. Pin the mapping of a dashed builtin's trailing arguments
(`git-config alias.x bisect--helper` stays refused), pin the lowercased
porcelain (`git Rebase--Helper --add-exec-commands cmd`), document and
test the new fail-closed collateral (`git push -u origin clone--helper`,
`git add bisect--helper`, `git config --add alias.r rebase`), and label
the helper case HACK-1373.

HACK-1383: decision 7 lists both remedies the detached_checkout refusal
offers (`--branch`/MCP `branch`, or `worktree.auto_branch=false`), and a
regression covers CLI `task inspect` and both MCP task tools in a
detached linked worktree, each surface with its own assertion.

Refs HACK-1411, HACK-1383

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #405 (global-state-guard preload) and #452. The shard plan now covers 576 files exactly once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…yes, and test every prompt guard

Follow-ups to #446 (HACK-1385):

- Add one bounded non-TTY spawn test per canPrompt() guard (mcp target
  selection, session picker and attached-workspace choice, update install
  confirmation, secrets name/value prompts, agent client picker), each
  proven by mutation.
- Guard the clack prompts in `hack ssh` (connection method, host, follow-up
  choice, Tailscale start) and `hack node pair walkthrough` (input prompts
  now gate on canPrompt() instead of stdout alone; the execute-now confirm
  leaves the session pending without a TTY).
- `hack secrets delete <name> --yes` confirms the deletion explicitly; without
  it a TTY-less run still refuses closed with E_INTERACTIVE_REQUIRED, and the
  hint now names --yes.
- tests/global-command.macos.test.ts keeps a TTY-less case for the decline
  and accept-default policies and asserts the prompt is never entered.

Refs HACK-1410

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r a pinned literal override

An interrupted preparation stop (pending operation, no publication) and an
interrupted retained command of an active adoption both reported the pending
switch step 'hack config adopt --recover --rollback', which each of those
states refuses. Doctor and the --fix note now name 'hack config adopt
--recover --stop' and 'hack down --recover' there. A busy refusal over a
pending switch now names the step that state accepts instead of only the
generic interrupted-lock recovery, which completes the switch and refuses a
literal override.

Refs HACK-1414

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@roodboi roodboi closed this Oct 24, 2026
@roodboi
roodboi deleted the train/b52 branch October 24, 2026 05:49
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