Skip to content

fix: carry config.json into worktrees, anchor the routine-health reader, and stop the worktree copy capturing the fleet resolvers - #722

Merged
gtapps merged 3 commits into
mainfrom
fix/worktree-config-json-health-anchor
Aug 14, 2026
Merged

fix: carry config.json into worktrees, anchor the routine-health reader, and stop the worktree copy capturing the fleet resolvers#722
gtapps merged 3 commits into
mainfrom
fix/worktree-config-json-health-anchor

Conversation

@gtapps

@gtapps gtapps commented Aug 14, 2026

Copy link
Copy Markdown
Owner

Summary

Two cwd/worktree correctness fixes in core, plus a fleet-wide resolver guard the first fix turned out to require.

The worktree state-dir template shipped only OPERATOR.md and compiled/, so a worktree session got a state dir the dev hermit could not read: /dev-pr hard-failed Gate 0 and told the operator to re-run /claude-code-dev-hermit:hatch — the wrong diagnosis, since the hatch was fine and commands.pr_create simply was not in the copy, while /dev-quality silently skipped its test step. Gate 0 is a skill-level Read .claude-code-hermit/config.json relative to cwd, so the file genuinely has to be present in the worktree.

Separately, routines.ts health resolved a relative hermit-dir argv against the process cwd while both documented callers (reflect, hermit-evolution) pass the relative .claude-code-hermit. Any earlier cd turned a real ledger into source: missing, which both skills read as "no candidates" — a silent skip rather than a visible failure.

The two halves fought each other — third commit fixes that

config.json is also the state-dir resolver sentinel, so copying it into a worktree made the projected copy resolve as its own hermit root. Reproduced live from a worktree session: routines.ts health .claude-code-hermit returned source: missing; moving the projected config.json aside returned source: ok. That is the same silent skip the routine-health half of this branch fixes, reintroduced for every worktree session. Bash-tool-invoked scripts carry no CLAUDE_PROJECT_DIR (verified in this environment), so the walk-up is the live path there.

A projection is identified as the sentinel without state/, which .worktreeinclude never copies. All three state-dir resolvers now walk past it to the main checkout:

  • core cc-compat.ts hermitDir()/findHermitDir()
  • dev find-hermit-dir.ts (both the env branch and the walk)
  • HA config.ts projectRoot()

Both halves then hold at once: the skill still Reads config.json at the relative path, and ledgers stay main-rooted. The discriminator stays true because no resolver returns a projection, so nothing ever mkdirs state/ inside one — dev's record-test-result writer resolves through the same function. This also closes the pre-existing case where an ambient CLAUDE_PROJECT_DIR naming the worktree anchored hook-driven writers to the partial copy.

feed-hermit's projectRoot keeps the walk without the skip, now documented in-place: it resolves which project the session is in so it can read tracked feed-sources.md at the root, where the worktree's branch copy is the correct answer, not main's. Different intent, same walk — flagged so nobody "aligns" it later.

Changes

  • state-templates/WORKTREEINCLUDE-APPEND.txt — add .claude-code-hermit/config.json; hatch/SKILL.md describes the block accordingly. No .gitignore change needed.
  • scripts/lib/routines/health.ts — an absolute argv is honoured verbatim; relative or absent anchors through resolveHermitDir(), matching weekly-review.ts:20.
  • cc-compat.ts, find-hermit-dir.ts, HA config.tsisWorktreeProjection() guard; stale comments in pinnedRoot() and dev's INVARIANT block corrected (both described the pre-config.json decoy behavior).
  • CHANGELOGs — core, dev, and HA each get a Fixed bullet; core keeps the idempotent ### Upgrade Instructions step for hermit-evolve. The dev bullet notes the version coupling: a dev plugin without the guard, paired with a core that ships the copy, would read an empty config in worktrees and drop operator protected_branches.
  • Tests — template contract; cwd-drift regressions; projection-skip cases in core (findHermitDir walk + CLAUDE_PROJECT_DIR branch) and dev (git-push-guard walk-up + env); fixtures across core/dev updated to include state/ so they model a real root.

Test plan

  • core bun test3790 pass, 0 fail · dev run-all.sh75 pass · HA bun test677 pass · fitness 129 · scribe 8 · feed 60 · forge 13
  • bunx tsc --noEmit from the repo root → exit 0
  • Red-first / mutation-checked: the template-contract and drift cases each failed against unfixed code; the four new projection-skip tests were confirmed red by stubbing isWorktreeProjection() to false in core and dev.
  • Live end-to-end: routines.ts health .claude-code-hermit from this worktree now returns source: ok with the projected config.json in place.

Follow-up (not in this PR)

The absolute-or-anchor ternary now exists three times — weekly-review.ts:20, reflect-precheck.ts:261, and health.ts. Worth extracting a shared resolveHermitArg() into cc-compat.ts, but that means migrating all three call sites, which does not belong in a bug fix.

gtapps added 3 commits August 14, 2026 13:23
…eader

A `claude --worktree` session got a state dir the dev hermit could not read:
/dev-pr failed Gate 0 pointing at a re-hatch, which was the wrong diagnosis —
the hatch was fine, commands.pr_create simply was not in the copy. The
resolver comments merged in #721 already assert the block carries config.json;
only this repo's hand-fixed .worktreeinclude made that true. Writes stay pinned
to the main checkout, so the single-writer invariant the original two-path
allow-list protected is unaffected; the template-sync test now asserts the
no-runtime-state half directly instead of by counting lines.

routines.ts health resolved a relative argv against the process cwd while both
documented callers pass the relative `.claude-code-hermit`. Any earlier `cd`
turned a real ledger into `source: missing`, which reflect and hermit-evolution
both read as "no candidates" — a silent skip. Now anchored like weekly-review's
hermit arg, with an absolute argv still honoured as passed.
…esolvers

Carrying config.json into `claude --worktree` worktrees fixed /dev-pr Gate 0
(a skill-level `Read .claude-code-hermit/config.json`, relative to cwd) but
config.json is also the resolver sentinel, so the projected copy started
resolving as its own hermit root. Reproduced live from a worktree session:
`routines.ts health .claude-code-hermit` returned `source: missing`; moving the
projected config.json aside returned `source: ok`. That is the same silent skip
— reflect and hermit-evolution read "missing" as "no candidates" — that the
routine-health half of this branch exists to fix, reintroduced for every
worktree session. Bash-tool-invoked scripts carry no CLAUDE_PROJECT_DIR
(verified), so the walk-up is the live path there.

A projection is the sentinel without `state/`, which .worktreeinclude never
copies. All three state-dir resolvers (core cc-compat, dev find-hermit-dir, HA
config) now walk past it to the main checkout, so both halves hold: the skill
still Reads config.json at the relative path, and ledgers stay main-rooted. The
discriminator holds because no resolver returns a projection, so nothing ever
mkdirs `state/` inside one — dev's record-test-result writer resolves through
the same function. This also closes the pre-existing case where an ambient
CLAUDE_PROJECT_DIR naming the worktree anchored hook-driven writers to the
partial copy.

feed-hermit's projectRoot keeps the walk without the skip, now documented: it
resolves which project the session is in so it can read tracked feed-sources.md
at the root, where the worktree's branch copy is the correct answer.

Also folded in the review's two test fixes: the two pre-existing `routines.ts
health` CLI tests pin CLAUDE_PROJECT_DIR/AGENT_DIR empty (runScript merges
process.env, so an inherited value pointed them at this repo's real ledger), and
template-skill-sync reads the template lazily so a missing file fails its own
existence test instead of throwing at collection time.

Verified: core 3790, dev 75, HA 677, fitness 129, scribe 8, feed 60, forge 13,
`bunx tsc --noEmit` clean. New guard tests confirmed red under mutation.
Core describes the mechanism, not the plugins that depend on it: the
worktreeinclude rationale in CHANGELOG, hatch/SKILL.md, contracts.test.ts and
the cc-compat resolver comments now say "skills read config keys at the relative
path" instead of naming specific domain skills. Released CHANGELOG entries are
left as-is.

Cleanup pass on top: HA's projectRoot computed join(proj, '.claude-code-hermit')
twice in one condition and kept the pre-change "fix one, check the other two"
sentence, so the three mirrored resolvers disagreed about what to cross-check.

Verified: core 3790, dev 75, HA 677, feed 60, `bunx tsc --noEmit` clean.
@gtapps gtapps changed the title fix(claude-code-hermit): carry config.json into worktrees and anchor the routine-health reader fix: carry config.json into worktrees, anchor the routine-health reader, and stop the worktree copy capturing the fleet resolvers Aug 14, 2026
@gtapps
gtapps merged commit 51b6834 into main Aug 14, 2026
9 checks passed
@gtapps
gtapps deleted the fix/worktree-config-json-health-anchor branch August 14, 2026 13:30
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