Skip to content

Keep the installed CDD helpers current: sync on pull, self-reload, update - #126

Merged
drabaioli merged 3 commits into
mainfrom
toolchain_staleness_warnings
Oct 6, 2026
Merged

drabaioli merged 3 commits into
mainfrom
toolchain_staleness_warnings

Conversation

@drabaioli

Copy link
Copy Markdown
Owner

Why

Merging a helper fix (e.g. #125 for #124) changed nothing on a machine until someone re-ran tools/cdd-worktree.sh install, and long-lived shells kept the old functions even after that. cdd-worktree-done was run against an already-fixed bug this way.

What

Three small pieces, none in the background, none printing anything while the install is current (ADR 0014):

  • Sync on pull. Run from a CDD git checkout, install writes a managed post-merge + post-rewrite hook into it. A pull that lands on the default branch and leaves tools/ differing (by content) from ~/.cdd/tools/ reinstalls both helpers and the adapter library, in one line. Feature-branch merges, commit --amend, and foreign hooks / core.hooksPath are left alone.
  • Self-reloading shells. Each public cdd-worktree* / cdd-state entry point re-sources its file when it changed on disk and re-dispatches to the new definition, silently.
  • bash ~/.cdd/tools/cdd-worktree.sh update for machines without a checkout: sparse clone of upstream main's tools/, install if different. cdd-state's unknown-verb / invalid-stage errors now name it.

The handoff asked for warn-only checks everywhere; that design was replaced at plan approval with this simpler, auto-reinstalling one (recorded in the plan).

Notes

  • Known limit: the command whose own pull brings a fix (cdd-worktree-done) still runs the old code; the next command is current.
  • Freshness is a content compare, so a pull on the default branch of a stale clone/fork would install its older helpers.
  • install-smoke and adapter-bindings now install from a non-git copy of tools/, so CI never hooks the developer's checkout.
  • After merge: run tools/cdd-worktree.sh install once from the main CDD checkout to get the hook, and restart open shells once.

Tests

New toolchain-sync gate (offline, stub upstream); all 27 gates pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JyhD5C5M5Vsi6k7jzdzEwS

drabaioli and others added 2 commits October 5, 2026 22:09
…date

The install is a copy, so a merged helper fix changed nothing on the
machine until someone re-ran install (the #124/#125 incident), open
shells kept the old functions, and a curl install had no way to update.

- Sync on pull: install, run from a CDD git checkout, writes a managed
  post-merge + post-rewrite hook into it. A pull that lands on the
  default branch with different helpers or adapters (cmp, never a
  version stamp) reinstalls both and prints one line. Other branches,
  amends and unchanged pulls do nothing. A foreign hook is never
  overwritten; under core.hooksPath nothing is written (both noted).
- Self-reload: each public cdd-worktree* / cdd-state call re-sources
  its file when it changed on disk and re-dispatches, silently.
- `bash ~/.cdd/tools/cdd-worktree.sh update`: on demand, a sparse
  blobless clone of upstream main's tools/; installs when it differs.
  cdd-state's unknown-verb / invalid-stage errors and the plan_written
  skew warning name it. The dispatch now exits explicitly, since update
  overwrites the script bash is running.

New toolchain-sync gate (27 gates). install-smoke and adapter-bindings
install from a non-git copy of tools/, so a check run never hooks the
developer's checkout. Process doc §2.8, shell-helpers.md, ADR 0014,
template BOOTSTRAP.md.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JyhD5C5M5Vsi6k7jzdzEwS
Comment on lines +36 to +51
## Staying current

The install is a copy, so merging a helper fix changes nothing on a machine until something reinstalls it — and a shell that sourced the helpers keeps the old functions even then. Three pieces close that gap ([ADR 0014](adr/0014-keep-the-install-current-by-syncing-on-pull.md)). None runs in the background or reaches the network on its own, and none prints anything while the install is current.

**The sync hook.** Run from a CDD git checkout, `install` writes a `post-merge` and a `post-rewrite` hook into the checkout's common hooks dir (`git rev-parse --git-path hooks`), which every worktree of the clone shares. "From a checkout" means `<toplevel>/tools/cdd-worktree.sh` is the running script (`-ef`, since the script path is logical and `--show-toplevel` physical) — never the installed copy, nor a copy of `tools/` that merely sits inside some repo. Between them the two hooks cover every `git pull`: a fast-forward or merge fires `post-merge`, a rebase over local commits fires `post-rewrite`. Each hook is a few stable lines calling the *just-pulled* checkout's own `tools/cdd-worktree.sh sync`, so the logic is always the current one. The hook ignores `post-rewrite`'s `amend` trigger (no pull), unsets the `GIT_DIR` git exports to a hook in a linked worktree, skips a checkout whose helper predates `sync`, and always exits 0.

`sync` acts only when the checked-out branch is the default branch, so a feature worktree's `/cdd-merge-base`, which fires the same shared hook, never installs. The default branch comes from `origin/HEAD` via git alone: `cdd-worktree-default-branch` would resolve the code-host adapter and announce it inside every pull. Then, if any shipped helper or adapter differs (`cmp`) from its installed counterpart — a missing counterpart counts, an extra installed file never does — it re-runs both installs from the checkout with their output suppressed and prints one line; a failed reinstall prints one line naming the two install commands. A hook without the marker line is the user's: `install` leaves it byte-identical and prints the line to add to it. Under `core.hooksPath` it writes nothing and says so. Hooks are replaced by rename, never rewritten in place, because the running hook may be the one whose `sync` is reinstalling. `cdd-worktree-done` pulls the default branch in the main worktree, so finishing a task updates the install too.

**Self-reload.** Each helper records its absolute path and `cksum` when it is sourced. Every public entry point — `cdd-worktree`, `cdd-worktree-done`, `-list`, `-gc`, `-resume`, and `cdd-state` — first calls `cdd-worktree-reload` (or `cdd-state-reload`): if the file changed on disk, it re-sources it and re-dispatches to the new definition in the same shell, so a `cd` still reaches the caller. Bash finishes the in-flight old body (it executes a copy of the function), and the new sum is recorded before the source, so a reload cannot loop. It is silent. The PATH shims source fresh and never reload; zsh records nothing and never reloads. A shell still holding code from before this mechanism has no reload guard and needs one restart.

**`update`.** `bash ~/.cdd/tools/cdd-worktree.sh update` serves a machine with no checkout (the curl install), or one that wants the newest now. It makes a shallow, blobless, sparse clone of upstream `main`'s `tools/` into a temp dir (about 1.5 s), compares it with the install as `sync` does, and runs both installs from the clone when anything differs, printing "up to date" or "updated"; an unreachable upstream is one line, exit 1, nothing changed. `CDD_UPSTREAM_URL` overrides the upstream as a test knob. Because `update` overwrites the very script bash is executing, the dual-mode dispatch ends in an explicit `exit`: bash reads a script as it runs it, and after the overwrite it must never read past the dispatch block. `cdd-state`'s unknown-verb and invalid-stage errors (still exit 2) add one line naming `update`, since a project's commands outrunning the installed helper is their usual cause.

**Known limit.** The command whose own pull brings a fix still runs the old code: `cdd-worktree-done` pulls mid-run and is not re-entrant once it has `cd`'d to the main worktree, so it is not re-executed. The next command runs the new code.

`scripts/toolchain-sync-assert.sh` (the `toolchain-sync` gate) pins all of this offline, against a bare "upstream", a clone of it as the checkout, and a throwaway `HOME`. The gates that run `install` themselves (`install-smoke`, `adapter-bindings`) install from a copy of `tools/` outside any repo, so a check run never hooks the developer's own checkout.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty long paragraphs. Trim, compress, keep the relevant parts.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5e2369d: the section is about half the length now, keeping only the invariants. The mechanics stay in the code comments and the rationale in ADR 0014.

Trim the section to its invariants; mechanics stay in the code comments
and the rationale in ADR 0014.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01JyhD5C5M5Vsi6k7jzdzEwS
@drabaioli
drabaioli merged commit 929cf0f into main Oct 6, 2026
2 checks passed
@drabaioli
drabaioli deleted the toolchain_staleness_warnings branch October 6, 2026 06:23
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