feat(fleet): own the domain-hatch protocol in one core script - #664
Merged
Conversation
Four of the five domain hatches checked a core version floor written into skill prose (HA 1.0.16, fitness 1.0.26, feed 1.2.22, forge 1.1.1) while every manifest declared >=1.2.30, so they proceeded against a core too old to run them. The floor now comes from the manifest, via a core-owned script the hatches call through bin/hermit-run. The same extraction closes three more defects the prose copies carried: a hatch-options.json present without a `target` key was never repaired and silently routed to the committed CLAUDE.md; feed's precedence copy had lost the projectPath == project root qualifier and resolved a project-scoped install differently from resolve-siblings.ts; and fitness, feed and forge wrote a config.json they had read before the wizard ran, discarding anything changed in between. Stale-core advice is now split by cause: an old installed package needs `claude plugin update`, a stale applied migration needs hermit-evolve, and evolve can never fix the first. Marker-block bounds and the duplicate-marker refusal are imported from evolve-plan.ts rather than reimplemented, so hatch and evolve cannot disagree about where a block ends. A cross-plugin contract test derives its plugin list from the filesystem instead of a hardcoded array. The two guards it replaces had both gone stale: hatch-resume-contract omitted feed-hermit, hatch-options-contract checked only dev-hermit.
…emplates The domain-hatch extraction dropped three behaviors its callers relied on. resolvePlugin only considered the local and project scope tiers, so every user-scope install became unresolvable; that stranded hermit-evolve (which now takes hatch_target solely from preflight), core hatch Step 9b, and all five domain hatches, each of which read the plugin root directly before. preflight returned early on a plugin-list failure and withheld target state that needs no plugin list at all, turning a transient probe failure into a hard stop. And planBlock inherited evolve-plan's rules minus the mode-marker refusal, so sync-block without --rendered-stdin could append an unrendered dev template, fence comments and both mode regions included, into the operator's CLAUDE.md. Also: ensure-target degrades to a 0.0.0 version stamp plus a resolve_warning instead of exiting 1, since the stamp was the only thing it took from the resolution; block replacement uses a function replacer so $-patterns in a CLAUDE-APPEND line survive verbatim; and the HA and forge hatch docs are repointed at the renumbered steps. No CHANGELOG bullet: [Unreleased] already describes the corrected behavior, and this fixes unreleased work no operator has seen.
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
Four of the five domain hatches enforced a core version floor written into skill prose (HA
1.0.16, fitness1.0.26, feed1.2.22, forge1.1.1) while every manifest declared>=1.2.30— so they proceeded against a core too old to run them. The floor now comes from the manifest, read by a core-owned script the hatches reach throughbin/hermit-run.Extracting that protocol closed three further defects the five prose copies carried, none of which were the duplication itself.
Changes
New core surface
scripts/domain-hatch.ts— thin verb dispatcher overscripts/lib/domain-hatch/, matching theroutines.tspattern established in refactor(claude-code-hermit): consolidate 23 scripts into verb CLIs #662. Verbs:preflight(read-only verdict),ensure-target,sync-block.coreScope()fromresolve-siblings.ts:151,markerOnward()/closingMarkerFor()fromevolve-plan.ts:855. The only new export isisAmbiguousBlock, lifted out of a module-private function so hatch andhermit-evolveshare one duplicate-marker check instead of two implementations disagreeing about where a block ends.Defects fixed
hatch-options.jsonpresent without atargetkey was never repaired: the write was gated on file absence while the read fell back on key absence, so it silently resolved to the committedCLAUDE.mdeven when the operator wanted.local.projectPath == project rootqualifier, resolving a project-scoped install tocommittedwhereresolve-siblings.tsgiveslocal— failing toward the teammate-visible file.config.jsonthey had loaded before the wizard ran, discarding anything changed in between. They now re-read immediately before the write.validate-config.tshad zero knowledge ofscheduled_checks[]or_hermit_versions, both written by domain hatches. A typo'd skill name stayed structurally valid and silently dead (the shape of fix(hermit): Fix routine config drift — trending-* routines reference retired trending-brief skill #651). Both are now validated.Ownership
claude plugin update; a stale applied migration needshermit-evolve.evolve-plan.ts:726-735computes those independently and evolve can never raise the installed version, so reporting only "run hermit-evolve" sent operators to a command that would report up-to-date and change nothing.hatchStep 9b andhermit-evolveboth delegate target resolution to the script, so core and all five domain hatches derive it from one implementation. The full fallback chain (stamped file → core's block inCLAUDE.local.md/CLAUDE.md→ install scope) is preserved.hermit-evolveowns version-driven refresh; hatch appends when absent and skips otherwise, so the two no longer race on one block. dev keeps its re-render because its block is rendered per mode, an input evolve cannot observe.domain-hatch *— that would hand every callerensure-targetandsync-block, which write core state and the operator'sCLAUDE.md, when most of a run only readspreflight.Enforcement
tests/cross-plugin/domain-hatch.contract.test.tsderives its plugin list from the filesystem. The two guards it supersedes had both gone stale:hatch-resume-contractomittedfeed-hermit,hatch-options-contractchecked onlydev-hermit. A sixth plugin is covered the day its hatch lands.test-cross-plugin.ymlpath filters widened to cover the hatches, the manifests and core's scripts. The previous filter was an exact filename, so none of this surface would have triggered it.Out of scope, deliberately
hermit-scribecarries none of the deleted prose and never writeshatch-options.json. Its two real gaps (never writeshatch-resume.jsonso core cannot resume it; declares>=1.2.30and never checks it) are worth separate issues.--stdin-jsonverb would relocate the write without removing the prose that builds the payload, and per-domain writers are a larger build than this PR. Noconfig.jsoncorruption incident exists in the repo's history; the demonstrated failures are omission and semantic drift.Release ordering
The five domain plugins now declare
required_core_version: ">=1.2.34"while core'splugin.jsonis1.2.33. That is correct —domain-hatch.tsships in the next core version — but core must be released as1.2.34before or alongside the domain plugins. Until then a domain hatch run frommainHEAD will correctly reportupgrade-core-packageand stop. Operators on the standard/plugin updatepath are unaffected;--plugin-dirtesters and fresh installers would hit it.Test plan
All suites run and green:
claude-code-hermitbun testtests/cross-plugin/claude-code-homeassistant-hermitbun testfeed-hermitbun testclaude-code-dev-hermitrun-all.shclaude-code-fitness-hermitrun-all.shlaravel-forge-hermitrun-all.shhermit-scribe(untouched)bunx tsc --noEmitGuard proven to bite: reintroducing a hardcoded floor into feed's hatch fails
states no hardcoded core version floor(35 pass / 1 fail); reverting returns 36 pass / 0 fail.Live end-to-end against a scratch project, all three verbs:
1.2.28< floor>=1.2.34upgrade-core-package, remedy namesclaude plugin update1.2.34, applied1.2.28upgrade-core-applied, remedy nameshermit-evolvefull,needs_target_question: trueensure-target --target committed→sync-blockcreated→append, block writtensync-blockre-runskip,written: falseReviewer note — pre-existing flake, not from this branch:
paneRootPidsandterminateSurvivorsintests/proc-survivor.test.ts/tests/proc.test.tstime out (~5.9s each) when core's suite runs back-to-back with other suites under load. They pass 3/3 in isolation; this branch touches none oflib/proc.ts,hermit-stop.tsortmux.ts. Worth a separate issue.Also fixed in passing:
laravel-forge-hermit/tests/run-all.shpassed three files to onebun testinvocation, butskill-structure.test.tsends inprocess.exit(), tearing down the runner beforehook.test.ts(24 tests) loaded. Those had been silently skipped.