Skip to content

feat(fleet): own the domain-hatch protocol in one core script - #664

Merged
gtapps merged 2 commits into
mainfrom
feat/shared-domain-hatch-protocol
Jul 25, 2026
Merged

feat(fleet): own the domain-hatch protocol in one core script#664
gtapps merged 2 commits into
mainfrom
feat/shared-domain-hatch-protocol

Conversation

@gtapps

@gtapps gtapps commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Summary

Four of the five domain hatches enforced 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, read by a core-owned script the hatches reach through bin/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 over scripts/lib/domain-hatch/, matching the routines.ts pattern established in refactor(claude-code-hermit): consolidate 23 scripts into verb CLIs #662. Verbs: preflight (read-only verdict), ensure-target, sync-block.
  • Built by importing what already existed: coreScope() from resolve-siblings.ts:151, markerOnward()/closingMarkerFor() from evolve-plan.ts:855. The only new export is isAmbiguousBlock, lifted out of a module-private function so hatch and hermit-evolve share one duplicate-marker check instead of two implementations disagreeing about where a block ends.

Defects fixed

  • hatch-options.json present without a target key was never repaired: the write was gated on file absence while the read fell back on key absence, so it silently resolved to the committed CLAUDE.md even when the operator wanted .local.
  • feed's precedence copy had dropped the projectPath == project root qualifier, resolving a project-scoped install to committed where resolve-siblings.ts gives local — failing toward the teammate-visible file.
  • fitness, feed and forge wrote a config.json they had loaded before the wizard ran, discarding anything changed in between. They now re-read immediately before the write.
  • validate-config.ts had zero knowledge of scheduled_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

  • Stale-core advice split by cause: a stale installed package needs claude plugin update; a stale applied migration needs hermit-evolve. evolve-plan.ts:726-735 computes 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.
  • Core hatch Step 9b and hermit-evolve both 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 in CLAUDE.local.md/CLAUDE.md → install scope) is preserved.
  • HA's version-driven CLAUDE-APPEND refresh removed. hermit-evolve owns 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.
  • Three permission grants, one per verb, never a bare domain-hatch * — that would hand every caller ensure-target and sync-block, which write core state and the operator's CLAUDE.md, when most of a run only reads preflight.

Enforcement

  • tests/cross-plugin/domain-hatch.contract.test.ts derives its plugin list from the filesystem. The two guards it supersedes had both gone stale: hatch-resume-contract omitted feed-hermit, hatch-options-contract checked only dev-hermit. A sixth plugin is covered the day its hatch lands.
  • test-cross-plugin.yml path 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-scribe carries none of the deleted prose and never writes hatch-options.json. Its two real gaps (never writes hatch-resume.json so core cannot resume it; declares >=1.2.30 and never checks it) are worth separate issues.
  • Config writing stays per-plugin. A generic --stdin-json verb would relocate the write without removing the prose that builds the payload, and per-domain writers are a larger build than this PR. No config.json corruption 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's plugin.json is 1.2.33. That is correct — domain-hatch.ts ships in the next core version — but core must be released as 1.2.34 before or alongside the domain plugins. Until then a domain hatch run from main HEAD will correctly report upgrade-core-package and stop. Operators on the standard /plugin update path are unaffected; --plugin-dir testers and fresh installers would hit it.

Test plan

All suites run and green:

Suite Result
claude-code-hermit bun test 3205 pass, 0 fail
tests/cross-plugin/ 52 pass, 0 fail
claude-code-homeassistant-hermit bun test 684 pass, 0 fail
feed-hermit bun test 61 pass, 0 fail
claude-code-dev-hermit run-all.sh exit 0
claude-code-fitness-hermit run-all.sh 129 pass, 0 fail
laravel-forge-hermit run-all.sh exit 0
hermit-scribe (untouched) 8 pass, 0 fail
bunx tsc --noEmit exit 0

Guard 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:

Scenario Verdict
installed core 1.2.28 < floor >=1.2.34 upgrade-core-package, remedy names claude plugin update
package 1.2.34, applied 1.2.28 upgrade-core-applied, remedy names hermit-evolve
both current, no stamped target full, needs_target_question: true
ensure-target --target committedsync-block createdappend, block written
sync-block re-run skip, written: false

Reviewer note — pre-existing flake, not from this branch: paneRootPids and terminateSurvivors in tests/proc-survivor.test.ts / tests/proc.test.ts time 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 of lib/proc.ts, hermit-stop.ts or tmux.ts. Worth a separate issue.

Also fixed in passing: laravel-forge-hermit/tests/run-all.sh passed three files to one bun test invocation, but skill-structure.test.ts ends in process.exit(), tearing down the runner before hook.test.ts (24 tests) loaded. Those had been silently skipped.

gtapps added 2 commits July 25, 2026 20:36
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.
@gtapps
gtapps merged commit 341d67d into main Jul 25, 2026
13 checks passed
@gtapps
gtapps deleted the feat/shared-domain-hatch-protocol branch July 25, 2026 20:20
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