Skip to content

fix(t3x): derive and verify the autobuild agent's PATH, and correct the runbook interval (#41) - #77

Merged
radroid merged 1 commit into
mainfrom
t3x/autobuild-launchd-path
Aug 12, 2026
Merged

radroid merged 1 commit into
mainfrom
t3x/autobuild-launchd-path

Conversation

@radroid

@radroid radroid commented Aug 11, 2026

Copy link
Copy Markdown
Owner

Closes the two items #41 flags as "traps to fix at the same time" — the ones its triage comment says are "docs/script-only, cost nothing in seam surface, and are not superseded" by the update-delivery work. The install-race, relaunch-verification and watchdog items are untouched; those harden a script the fork intends to retire.

Both changes are fork-owned (scripts/t3x/**, docs/t3x/**). No upstream file is touched, so no new seam-ledger row.

1. --print-launchd would have silently downgraded the toolchain

The generator hardcoded its PATH. The live agent's is wider — it carries the fnm node directory the generator never knew about — so regenerating the agent by following the runbook hands it a different PATH than the one that has been building successfully for weeks.

c33e5a361 already back-ported the ~/.cargo/bin half. That half fails loudly: no cargo means spawn cargo ENOENT on every tick, backing off forever. The node half is worse because it is quiet. Measured on this machine:

hardcoded PATH resolves node to /opt/homebrew/bin/node → v26.7.0
package.json engines.node ^24.13.1
live agent resolves node to ~/.local/share/fnm/node-versions/v24.18.0/… → v24.18.0

The build does not fail. It succeeds against a toolchain nobody chose and installs the result.

The fix is to derive it and then prove it.

derive_agent_path resolves node, pnpm, cargo, git and follows each symlink to a stable directory — a version manager's shim lives in a per-shell directory (fnm: ~/.local/state/fnm_multishells/<pid>_<stamp>/bin) that is deleted when that shell exits, so a shim path in a plist is an entry that quietly ceases to exist. A directory is added only when nothing already on the list provides that tool, which is what stops corepack's pnpm — whose symlink lands in .../corepack/dist, not a bin dir — from contributing an ephemeral entry.

verify_agent_path then runs each tool from an empty environment, because that is what launchd gives the job; a check that inherits the current shell's PATH proves nothing. It refuses to emit if a tool is unreachable or if node's major does not match engines.node.

On this machine the derived PATH matches the live agent's entry for entry.

--diff-launchd is the feedback loop that was missing. launchd keeps the copy it was bootstrapped with, so a fix to the emitter never reaches a running agent and nothing anywhere reports the gap — which is how the live plist came to carry a hand-edited PATH the generator did not know about. It compares settings and ignores comments, so notes hand-written into the live plist do not read as differences.

Also emits COREPACK_ENABLE_DOWNLOAD_PROMPT=0, which the live plist has and the generator did not: with no tty, a corepack download prompt has nobody to answer it.

2. The runbook documented a two-minute loop

--interval's default was written as 60 (it is 43200), the TL;DR said "every 60s", and all three examples passed --interval 120. Following them installs a two-minute quit/replace/relaunch cycle on an app hosting live Claude sessions and serving :3773. The install examples now also show the flags the agent actually runs with, including --ref origin/main.

Verification

Run under /bin/bash 3.2 — what the plist invokes, and where expanding an empty array under set -u is an "unbound variable" error (this bit once during development and is now commented at the capture site):

Case Result
normal emit exit 0, zero stderr, plutil -lint OK
derived PATH vs live agent identical entries
cargo unreachable exit 2, nothing emitted, names the missing tool
only Homebrew node reachable exit 2, nothing emitted, reports v26 vs v24
same, --force exit 0, emits
--diff-launchd reports only real differences; comments ignored
--diff-launchd when verification fails exit 2, no stdout

What this does not do

The KeepAlive/ThrottleInterval, dmg-freshness, atomic write_status, hdiutil -readonly and "reject --relaunch without --install" items under #41's Smaller items are still open. So is the install-race work, deliberately.

Refs #41

🤖 Generated with Claude Code

…he runbook interval (#41)

Two traps from #41 that bite before any of the install-race work, both fork-owned
(`scripts/t3x/**`, `docs/t3x/**`), zero upstream seam.

## 1. Regenerating the plist would silently downgrade the toolchain

`--print-launchd` hardcoded its PATH. The live agent's is wider — it carries the
fnm node directory, which the generator never knew about — so anyone following
the runbook to regenerate the agent would quietly hand it a different PATH than
the one that has been building successfully.

`c33e5a361` already back-ported the `~/.cargo/bin` half of that gap, and that is
the half with a loud failure: no cargo means `spawn cargo ENOENT` every tick,
backing off forever. The node half is worse precisely because it is quiet. On
this machine the hardcoded list still resolves node — to Homebrew's, which is
v26.7.0, while `package.json`'s `engines.node` is `^24.13.1`. The build does not
break. It succeeds against a toolchain nobody chose, and installs the result.

So the PATH is now derived and then proved:

- `derive_agent_path` resolves node, pnpm, cargo and git, following each symlink
  to a stable directory. That matters: a version manager's shim lives in a
  per-shell directory (fnm's is `~/.local/state/fnm_multishells/<pid>_<stamp>/bin`)
  that is deleted when the shell exits, so a shim path baked into a plist is a
  PATH entry that stops existing. A directory is added only when nothing already
  on the list provides that tool — which is what keeps corepack's `pnpm`, whose
  symlink lands in `.../corepack/dist` rather than a bin dir, from contributing
  an ephemeral entry: the node installation's own bin holds the shim and is
  already there.
- `verify_agent_path` then runs each tool from an EMPTY environment, because that
  is what launchd gives the job — a check that inherits this shell's PATH proves
  nothing. It refuses to emit (exit 2) if a tool is unreachable, or if node's
  major does not match `engines.node`. `--force` emits anyway.

On this machine the derived PATH matches the live agent's entry-for-entry.

`--diff-launchd` is the missing feedback loop: launchd keeps the copy it was
bootstrapped with, so a fix to the emitter never reaches a running agent and
nothing reports the gap. It compares settings and ignores comments, so the notes
hand-written into the live plist do not read as differences.

Also emits `COREPACK_ENABLE_DOWNLOAD_PROMPT=0`, which the live plist has and the
generator did not: corepack provides pnpm here against a pinned `packageManager`,
and with no tty a download prompt has nobody to answer it.

## 2. The runbook documented a 2-minute loop

`--interval`'s default was written as `60` (it is `43200`), the TL;DR said "every
60s", and all three examples passed `--interval 120`. Following them installs a
two-minute quit/replace/relaunch cycle on an app hosting live Claude sessions and
serving `:3773`. Corrected, and the install examples now show the flags the agent
actually runs with, including `--ref origin/main`.

Verified under /bin/bash 3.2 (what the plist invokes, and where expanding an
empty array under `set -u` is an error): clean emit with zero stderr, `plutil
-lint` OK, both refusal paths exit 2 emitting nothing, `--force` overrides, and
`--diff-launchd` reports only the differences that are real.

Refs #41

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cae2987e-f9c2-493c-9c5c-73b9851e1e35

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@radroid
radroid merged commit e5a7147 into main Aug 12, 2026
2 checks passed
@radroid
radroid deleted the t3x/autobuild-launchd-path branch August 12, 2026 02:05
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