fix: carry config.json into worktrees, anchor the routine-health reader, and stop the worktree copy capturing the fleet resolvers - #722
Merged
Conversation
…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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.mdandcompiled/, so a worktree session got a state dir the dev hermit could not read:/dev-prhard-failed Gate 0 and told the operator to re-run/claude-code-dev-hermit:hatch— the wrong diagnosis, since the hatch was fine andcommands.pr_createsimply was not in the copy, while/dev-qualitysilently skipped its test step. Gate 0 is a skill-levelRead .claude-code-hermit/config.jsonrelative to cwd, so the file genuinely has to be present in the worktree.Separately,
routines.ts healthresolved a relativehermit-dirargv against the process cwd while both documented callers (reflect,hermit-evolution) pass the relative.claude-code-hermit. Any earliercdturned a real ledger intosource: 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.jsonis 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-hermitreturnedsource: missing; moving the projectedconfig.jsonaside returnedsource: 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 noCLAUDE_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.worktreeincludenever copies. All three state-dir resolvers now walk past it to the main checkout:cc-compat.tshermitDir()/findHermitDir()find-hermit-dir.ts(both the env branch and the walk)config.tsprojectRoot()Both halves then hold at once: the skill still
Readsconfig.jsonat the relative path, and ledgers stay main-rooted. The discriminator stays true because no resolver returns a projection, so nothing evermkdirsstate/inside one — dev'srecord-test-resultwriter resolves through the same function. This also closes the pre-existing case where an ambientCLAUDE_PROJECT_DIRnaming the worktree anchored hook-driven writers to the partial copy.feed-hermit'sprojectRootkeeps the walk without the skip, now documented in-place: it resolves which project the session is in so it can read trackedfeed-sources.mdat 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.mddescribes the block accordingly. No.gitignorechange needed.scripts/lib/routines/health.ts— an absolute argv is honoured verbatim; relative or absent anchors throughresolveHermitDir(), matchingweekly-review.ts:20.cc-compat.ts,find-hermit-dir.ts, HAconfig.ts—isWorktreeProjection()guard; stale comments inpinnedRoot()and dev's INVARIANT block corrected (both described the pre-config.jsondecoy behavior).### Upgrade Instructionsstep forhermit-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 operatorprotected_branches.findHermitDirwalk +CLAUDE_PROJECT_DIRbranch) and dev (git-push-guardwalk-up + env); fixtures across core/dev updated to includestate/so they model a real root.Test plan
bun test→ 3790 pass, 0 fail · devrun-all.sh→ 75 pass · HAbun test→ 677 pass · fitness 129 · scribe 8 · feed 60 · forge 13bunx tsc --noEmitfrom the repo root → exit 0isWorktreeProjection()tofalsein core and dev.routines.ts health .claude-code-hermitfrom this worktree now returnssource: okwith the projectedconfig.jsonin place.Follow-up (not in this PR)
The absolute-or-anchor ternary now exists three times —
weekly-review.ts:20,reflect-precheck.ts:261, andhealth.ts. Worth extracting a sharedresolveHermitArg()intocc-compat.ts, but that means migrating all three call sites, which does not belong in a bug fix.