Skip to content

State the host-tools.json Carry Rule and Stop the Empty-Overlay Warning - #1486

Merged
ptr727 merged 5 commits into
developfrom
feature/host-tools-carry-rule
Sep 9, 2026
Merged

ptr727 merged 5 commits into
developfrom
feature/host-tools-carry-rule

Conversation

@ptr727

@ptr727 ptr727 commented Sep 9, 2026 •

Copy link
Copy Markdown
Owner

The decision

#1483 posed a fork: make the spec/files.json entry conditional, or
confirm the unconditional requirement is deliberate and state it where a
reader looks. This repository has already settled it. STANDUP.md and the
hub's own host-tools.json note both say a repository with nothing to add
carries the stub with an empty tools list, on the same footing as
OPERATIONS.md, so the declaration sits where a reader finds it rather than
where they must know to look. A committed rule outranks a suggestion, so
this takes the second branch and fixes what contradicted it.

Declined, from the issue's own suggestions: dropping the manifest entry, and
reporting a present-but-empty file as DRIFT so repositories can shed it.
The stub is the conformant state rather than a leftover, which is what the
carry rule says.

What changed

Three surfaces were wrong or silent about the rule.

  • scripts/host_gate.py's comment at the overlay load called carrying no
    local file the common case, the reverse of what the manifest encodes. It
    states the rule, and says why an absence is silent there anyway: the gate
    measures a host against floors, and spec/audit.py is what reports a
    baseline file a repository does not carry.
  • docs/host-setup.md said nothing about the carry rule. It leads with it
    and gives the reason.
  • scripts/README.md described the file as one a repository adds when it
    needs more than the fleet does, which reads as conditional creation.

The manufactured warning is fixed. overlay_above() named any ancestor
carrying the file, so a bare run started inside a repository whose overlay
declares nothing told the operator to re-run with --repo so that no floor
would count. It reads the overlay and passes over one declaring no tool,
while still naming one it cannot parse, since the failed parse is what
leaves open whether it declares anything. The warning names the re-run and
promises nothing about its outcome, because a re-run against an unparseable
overlay exits 2 with a diagnostic rather than applying anything.

STANDUP.md and docs/fleet-map.md each stated when the gate warns, and
the behaviour change made both false. Both now match it.

Verification

  • Full suite (1173 tests), ruff format --check, ruff check, mypy,
    spec/validate.py, scripts/prose_lint.py, scripts/repo_gate.py (eol,
    eol-coverage, sha-pin), scripts/build_dist.py --check, and
    scripts/docker_lint.py (markdownlint, cspell, editorconfig-checker) all
    clean.
  • The three overlay states were exercised by running the gate rather than
    reasoned about: an empty overlay is silent, a declaring one and an
    unparseable one each warn.
  • Every behaviour-pinning test was proved by reverting what it pins.
    Removing the empty-overlay skip fails two cases, removing the isinstance
    branch fails the case that patches read_declaration to an empty
    diagnostic, and neutering quote_argument fails the spaced-path case.
  • That last one was dead. Removing the warning's trailing clause left it
    asserting the absence of an unquoted path followed by a space, and nothing
    can follow the path any more, so it passed with the quoting neutered. It
    is anchored on the newline and fails again.
  • Four local strict-review passes, 25 findings across them. The code settled
    after the first pass and every round since found defects only in prose
    describing it, so the last round answered by deleting the qualifiers that
    kept drawing findings rather than rewriting them again.

Settles #1483. A develop-targeted pull request registers no closing
reference, so the issue closes with the promotion.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation

    • Clarified host setup requirements: every repository provides a host-tools declaration, including an empty declaration when no additional tools are needed.
    • Expanded guidance for layered repository overlays, bare runs, unapplied ancestor overlays, and overlays that cannot be parsed.
    • Updated troubleshooting and fleet documentation to reflect the refined warning scenarios.
  • Bug Fixes

    • Host checks now distinguish empty overlays from overlays that declare tools or contain invalid configuration.
    • Warning messages more clearly identify overlays that were not applied.

Every repository carries a root host-tools.json, an empty tools list where
it adds nothing, on the same footing as OPERATIONS.md, so a maintainer who
needs to declare a floor finds the file rather than having to know one may
be created. STANDUP.md and the hub's own host-tools.json note already said
so, and three surfaces said otherwise or said nothing.

scripts/host_gate.py's comment at the point it loads an overlay called
carrying no local file the common case, which is the reverse of the rule
spec/files.json encodes as a baseline entry applying to every repository.
docs/host-setup.md and scripts/README.md each described the file as one a
repository adds when it needs more than the fleet does, which reads as
conditional creation. All three now state the rule and why it is the rule,
and the gate's comment says why an absence is silent there anyway, that
being a conformance question spec/audit.py answers rather than a host one.

overlay_above() named any ancestor carrying the file, so a bare run started
in a subdirectory of a repository whose overlay declares nothing told the
operator to re-run with --repo so that no floor would count. It now reads
the overlay and passes over one declaring no tool, while still naming one
it cannot read, since a bare run cannot tell an unreadable declaration from
an empty one and the re-run is what turns the guess into a diagnostic. The
walk still stops at the nearest carrier, a further ancestor being a
different repository.

The issue also proposed dropping the manifest entry or reporting a present
but empty file as DRIFT. Both are declined: the stub is the conformant
state rather than a leftover, which is what the carry rule says.

Refs #1483
Ten findings from the strict local pass, every one against text the first
commit wrote or left false elsewhere.

STANDUP.md's re-run paragraph told the operator the gate warns whenever its
working directory sits inside a repo whose overlay it did not read, which
the same document's section 2 then makes false: the stub it carries draws
no warning now. It names both residual cases instead, and docs/fleet-map.md
records the pair rather than the one it had.

docs/host-setup.md claimed the gate says nothing where it finds no file,
which the ancestor warning contradicts, and the new paragraph left the next
one's "declares it in that file" binding to AUDIT.md rather than to
host-tools.json. The surviving sentence six lines above still described a
repository creating a file of its own, which is the reading the change
exists to retire.

The warning said the re-run makes a declaration count, which is false for
the unreadable overlay this same change routes to it: there the re-run
exits 2 with a diagnostic. It now says the re-run reads it, true of both.
scripts/README.md justified passing over an empty overlay by a floor-impact
criterion the code does not implement, since it branches on the tools list
being empty, and a name-only override changes no floor and still warns.

overlay_above's summary line claimed it names an overlay this run would
read differently, which a no-op override falsifies, and its closing
sentence justified stopping the walk by every further carrier being another
repository, which this repository's own spec/host-tools.json falsifies. The
first is narrowed to what the function decides and the second is deleted
rather than re-derived, the behavior stated without a rationale that does
not hold.

isinstance(tools, str) or tools was one expression with an unreachable arm,
a diagnostic being a non-empty and so truthy string, and no test could pin
it. It is two statements now, each separately readable. The nearest-carrier
test built one carrier and so proved only that a carrier is found, so a
second case builds two declaring ones and asserts the nearer wins.

Declined: the is_file and read pair in overlay_above is a TOCTOU window,
and an overlay removed between them is named by a warning that was correct
about what it observed, the same outcome the single-stat version reached.
Left alone: a bare run started under spec/ names spec/host-tools.json as an
overlay, which predates this change and needs path knowledge the function
does not have.
The second local review pass raised eight findings, every one against text
the previous round wrote in answer to the first pass.

The warning promised an outcome, and both wordings tried so far were false
somewhere. "So its floors count" was false for the unreadable overlay, and
"to read it" was false for every case, since overlay_above now parses the
file to decide whether to warn at all. It states what happened and names
the re-run without promising what the re-run yields: the overlay went
unapplied, and here is the flag that applies it. scripts/README.md,
docs/host-setup.md, and docs/fleet-map.md each carried a form of the same
promise and now say unapplied too.

Three surfaces said the gate names an ancestor's overlay only where that
overlay declares a tool, which the unreadable branch this change added
falsifies and a test already asserts. Each names both cases.

STANDUP.md justified the absent warning by the stub declaring nothing, and
the mechanism cannot engage there at all: the section 0 command runs from a
hub checkout, whose own root carries a declaration, and main() walks
ancestors only where the working directory carries none. The target is not
an ancestor of the hub checkout either way. It gives that reason instead.

The sentence deleted from overlay_above last round as false, that a further
carrier is a different repository, survived verbatim as a test docstring,
where this repository's own spec/host-tools.json falsifies it just as well.
Both docstrings in that pair now say what their test asserts.

overlay_above's docstring said a bare run cannot tell an unreadable
declaration from an empty one, three lines above the branch that tells them
apart. The reason it names an unparsable overlay is that the failed parse
leaves open whether it declares anything, which is what it says now.

The isinstance branch remains unpinnable, a diagnostic being truthy, so no
test distinguishes it from the truthiness check below. It stays written out
for the reader, and a comment says so rather than leaving the next pass to
find it a third time.
The third local review pass raised three findings on text the second round
wrote, each a claim about behavior rather than the behavior itself. Pieter
authorized a round past the two-round budget to settle them.

overlay_above's summary line said it returns the ancestor this bare run
should name, which the branch's own nested-overlay test falsifies: an outer
carrier declaring a tool goes unnamed when the inner one declares nothing,
so a reader taking the summary as the contract writes that shape down as
covered. It states what the walk returns instead, the nearest carrier
unless that one declares nothing, and the body already carries the rest.

The comment beside the isinstance branch claimed no test can tell it from
the truthiness check under it, which is false: patching read_declaration to
return an empty diagnostic separates them, verified rather than argued. The
clause is gone, and what remains is the reason the branch is there, an
empty diagnostic being the case truthiness alone would read as an empty
declaration.

docs/fleet-map.md still said the warning names the re-run that applies the
overlay, which the sentence beside it falsifies, since a re-run against an
unparseable one exits 2 with a diagnostic rather than applying anything. It
names the re-run and promises nothing about it, matching the message.

Declined, all four classed style by the pass and none a false claim: the
G2 bullet reaching its stub carve-out two sentences after the clause it
narrows, STANDUP.md offering a mechanism alongside the sufficient reason
beside it, the is_file and parse pair in overlay_above being two syscalls
where one stat stood before, and the tighten-only rule appearing twice in
one section of docs/host-setup.md, which predates this change on both
surfaces.
The fourth pass raised four findings, again all against text a previous
round wrote. Pieter's call was to answer them by deleting rather than
rewriting, since across four rounds the code settled after the first and
every round since found defects only in prose describing it.

Two were qualifiers each fix had narrowed and got wrong in a new way.
overlay_above's summary line named one of its two None paths and dropped
the other, having traded the reverse omission a round earlier, so the
qualifier is gone and the body carries the cases. STANDUP.md offered the
walk being disabled as the cause of a warning that is absent for an
independent reason, which invites the false counterfactual that a hub root
carrying no declaration would name the target. It states the sufficient
reason alone, that the target is not an ancestor of the checkout the run
starts in.

Removing "so its declaration counts" from the warning last round left
test_a_spaced_overlay_path_is_quoted_in_the_re_run asserting the absence of
an unquoted path followed by a space, and nothing can follow the path now.
Verified by neutering quote_argument: the case passed anyway. Anchored on
the newline it fails again, verified the same way.

The isinstance branch is pinned rather than argued about. Patching
read_declaration to return an empty diagnostic separates it from the
truthiness check under it, which the third round's message asserted without
adding the test, so the test is here and fails with the branch removed. The
helper docstring claimed an empty default payload would make every case
pass for the wrong reason, true of the three asserting silence and not of
the two asserting a warning, which fail outright.
Copilot AI lite review requested due to automatic review settings September 9, 2026 20:14
@coderabbitai

coderabbitai Bot commented Sep 9, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: aa3475b8-58e9-4a16-9e20-5fd57c14ba54

📥 Commits

Reviewing files that changed from the base of the PR and between e7cb1cf and 84e3e46.

📒 Files selected for processing (6)
  • STANDUP.md
  • docs/fleet-map.md
  • docs/host-setup.md
  • scripts/README.md
  • scripts/host_gate.py
  • scripts/tests/test_host_gate.py

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The host gate now skips empty ancestor overlays, reports declaring or unreadable overlays, and preserves nearest-overlay selection. Tests cover these cases. Documentation describes repository stubs, layering, warnings, and rerun guidance.

Changes

Host overlay behavior

Layer / File(s) Summary
Overlay detection and validation
scripts/host_gate.py, scripts/tests/test_host_gate.py
overlay_above reads ancestor declarations, skips empty overlays, and reports declaring or unreadable overlays. Tests cover nearest-carrier selection, malformed declarations, empty diagnostics, warning output, and newline formatting.
Host overlay documentation
STANDUP.md, docs/fleet-map.md, docs/host-setup.md, scripts/README.md
Documentation describes repository host-tools.json stubs, layered declarations, bare-run warnings, and rerun requirements. A file specification link was added.

Priority: ⬇️ Low

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 84e3e

The host gate now suppresses warnings for empty repository overlays while continuing to identify declaring or unreadable overlays, with matching tests and guidance. No merge-blocking risk is evident.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 2 files. (4 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies both main changes: the repository-wide host-tools.json carry rule and the removal of warnings for empty overlays.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 12 functions across 2 files. (4 skipped: 4 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/host-tools-carry-rule

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The behavioral change is narrowly scoped, is covered by new/updated tests, and the documentation updates are consistent with the code and manifest contract.

Pull request overview

This PR clarifies and codifies the unconditional “carry host-tools.json” rule across documentation and code, and updates host_gate.py behavior so a repository’s empty overlay stub does not trigger a misleading “re-run with --repo” warning.

Changes:

  • Update scripts/host_gate.py overlay detection and warning text to skip empty overlays while still warning on unparseable ones.
  • Expand scripts/tests/test_host_gate.py coverage for empty, malformed, and nested overlay-carrier scenarios.
  • Align procedural and reference docs (STANDUP.md, docs/host-setup.md, scripts/README.md, docs/fleet-map.md) with the clarified carry rule and the warning’s updated semantics.
File summaries
File Description
STANDUP.md Updates the standup guidance to explain why the residual “no --repo” case cannot be covered by a warning.
scripts/tests/test_host_gate.py Adds/adjusts tests to pin the new empty-overlay silence behavior and malformed-overlay warning behavior.
scripts/README.md Updates the host_gate.py description to state the unconditional carry rule and the refined warning behavior.
scripts/host_gate.py Implements empty-overlay skip in overlay_above() and refines warning wording (“did not apply”).
docs/host-setup.md Documents the carry rule explicitly and explains why absence is silent in host_gate.py (audit reports baseline misses).
docs/fleet-map.md Updates the gap-resolution narrative to match the new warning behavior and the empty-overlay exception.
Review details
  • Files reviewed: 6/6 changed files
  • Comments generated: 0
  • Review effort level: Lite

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@ptr727

ptr727 commented Sep 9, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 9, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@ptr727
ptr727 merged commit b5fc121 into develop Sep 9, 2026
9 checks passed
@ptr727
ptr727 deleted the feature/host-tools-carry-rule branch September 9, 2026 21:16
ptr727 added a commit that referenced this pull request Sep 10, 2026
…g, and the Release-Trigger Retirement (#1489)

Promotes thirteen merged pull requests from `develop` to `main`.

## What this promotes

- **#1486** State the `host-tools.json` carry rule and stop the
empty-overlay
warning. `spec/files.json` now requires the declaration unconditionally,
and
  `host_gate.py` no longer warns on an empty one.
- **#1484** Fan the validator's pytest leg out over a `python-versions`
input,
so a caller with more than one interpreter no longer silently loses its
other
legs to a pinned 3.13, with an entry guard that refuses a value
`fromJson`
  would otherwise admit.
- **#1476** Retire the publish-on-merge release trigger, which
`spec/files.json`
applied to an interface contract whose task has no publish-on-merge
path.
- **#1479** Ship the release caller snippet and retire the
not-yet-released
  claim.
- **#1474** Exclude `node_modules` from markdownlint and Docker lint
discovery
  at any depth.
- **#1468** Move the carried-surface cluster off decision, recording the
  measurements and what stays unmeasured on the issues that hold them.
- **#1498** Correct the release-caller snippet's publish-on-merge claim,
raised by CodeRabbit against this promotion and fixed on its own branch,
  since a promotion PR's head is `develop`.
- **#1494** Settle the markdown lint exclusion seam as a nested config.
- **#1492** Check declared deployment environments and fix the
carried-payload
  rule.
- **#1501** Move the session handoff to a chain of labeled issues. Two
of its
units are verbatim fidelity, `AGENTS.md` "Session Scope" and
`GOVERNANCE.md`
"Communicating with the User", so every fleet repo picks them up on its
next
  resync.
- **#1505** Refuse a bare handoff link rather than reading it as no
successor,
  answering a finding raised against this promotion.
- **#1504** Reject a deployment environment name or branch carrying
surrounding
  whitespace, answering two more.
- **#1506** Encode an environment name before it becomes a path segment,
delete
a branch that cannot fire, and dedupe a registry name by case, answering
two
  more and declining one on a reproduction.

## Closing references

A `develop`-targeted pull request registers no closing reference, so the
issues
those pull requests settled close with this promotion.

#1470 is the one that needs a word. Two of its seven acceptance items
are not
met as literally written, and the maintainer's decision to close it here
anyway,
with the basis for both, is recorded on the issue itself.

Closes #1483
Closes #1473
Closes #1487
Closes #1482
Closes #1475
Closes #1470

## Left open deliberately

- **#1499**, **#1500** follow-ups #1501 filed rather than fixed.
- **#1503** the validator-looser-than-its-consumer class, filed from
findings on
this promotion and deliberately not closed by it. Five instances
recorded, and
  the fix is a design decision rather than a check.
- **#1497** the Docker delta list never says to remove the `push:`
block.
- **#1472** the fleet-level pin call, deliberately not pre-empted by
#1479.
- **#1321**, **#1464** carry #1468's decision record and remain in their
own
  scope.

## Verification

`origin/main..origin/develop` merges clean, no end-of-line conflict.
Each
promoted pull request ran the fleet gate set on its own branch before
merging;
this promotion adds no content of its own.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01817rckKywjfoaEV5WF9TKs
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.

2 participants