Skip to content

Carry the Host Contract and Declare the jq Floor - #652

Merged
ptr727 merged 9 commits into
developfrom
carry-the-host-contract
Aug 10, 2026
Merged

Carry the Host Contract and Declare the jq Floor#652
ptr727 merged 9 commits into
developfrom
carry-the-host-contract

Conversation

@ptr727

@ptr727ptr727 commented Aug 10, 2026

Copy link
Copy Markdown
Owner

Your call, per the decision to accept the fleet-wide finding. Floor set to 1.7 per your follow-up.

The jq gap

jq is used by the ruleset normalizer in repo-config/configure.sh, the ruleset diff in AUDIT.md section 6, and payload regeneration in docs/repo-config-carry.md. It was declared in neither the contract table nor spec/host-tools.json. A host without it cannot apply, check, or audit a repo's branch configuration, and nothing said so.

The floor is 1.7, and it is a target

I did not pick a number and justify it after. Measuring first said no floor was warranted at all: nothing in the tree uses a post-1.5 jq feature (checked walk, @base64d, --args, $__loc__, pick, toarray, abs, getpath, ltrimstr, splits, limit, $ENV — the only hits were shell variables and Actions expressions, not jq filters). So the floor is a target, the same kind as python3, and the entry says so rather than implying a defect nobody found.

1.7 is anchored to what the platform provides: current Debian stable packages 1.7.1, so a distribution install satisfies the floor and no host needs a manual build to meet it, while an older release sits below and is the case the floor exists for. That anchor is what makes it actionable. Two rejected alternatives, for the record:

  • 1.8 (the newest, installed here) would fail a current apt host that runs every documented filter correctly. A floor set to whatever happens to be installed is a host failure nobody can act on, which is the hazard this file's own note names.
  • 1.6 would encode the walk/1 boundary, which is a fact about the filters rather than something a host operator can act on.

Facts kept in the entry because a reader needs them: on jq 1.5 walk/1 does not degrade, it fails to compile, so a diff built on it reports drift on every ruleset it never compared. And the claim that keys_unsorted also needs 1.6 stays disproved per the .github/copilot-instructions.md record, measured on jq-1.5-1-a5b5cbe.

Consequences swept, not left

The floor now sits above 1.6, so walk/1 is available — which made false the comments in AUDIT.md and repo-config/configure.sh that justified hand-defining a recursion by walk's absence. Both now give the reason that survives: the recursion costs nothing and compiles below the floor as well. This is the same class of defect as #649 (a rule changed, its prose left asserting the old one), so it was swept by term rather than by instance.

host-tools.json becomes a carried file

A repo now states the tools its own procedures need beyond the fleet declaration, so the tighten-only layering lives somewhere a reader finds rather than has to know to look for. A repo with nothing to add carries the stub with an empty tools list — the footing OPERATIONS.md already set — and this repo's new root file is that worked example, with a note distinguishing it from spec/host-tools.json.

Measured before landing: none of the 22 cataloged repos carries one, so this adds exactly one LETTER per repo. This PR satisfies the hub's own.

The gate joins the procedures it was written for

scripts/host_gate.py existed and no procedure ran it. STANDUP.md section 0 and AUDIT.md now do, alongside RESYNC.md in #651. Each passes --repo, because the gate reads the target's declaration relative to that flag and a bare run layers the hub's instead while printing the same healthy digest either way.

Verification

scripts/test_host_gate.pyfailed first, which is the tests working: test_the_declared_floors_are_the_ones_with_a_stated_reason asserts the floor set exactly, so a new floor cannot land unnoticed. I updated it and generalized test_a_target_floor_says_so_rather_than_implying_a_defect from the single python3 entry to the set of target floors, so a third one added without the two-kinds wording fails rather than reading as measured.

The boundary is checked through the gate's own compare/parse_version rather than a reimplementation of them:

jq 1.5 -> FAILS jq 1.7 -> meets
jq 1.6 -> FAILS jq 1.7.1 -> meets
jq 1.6.1 -> FAILS jq 1.8.2 -> meets
  • 552 script tests OK, --selftest PASS, validate.py OK
  • host_gate.py: 7 tools, jq 1.8.2 meets the 1.7 floor, and it reads the hub's own stub (host-tools.json layered 0 local entry(s))
  • prose_lint, repo_gate --check eol, markdownlint-cli2, cspell on the gated files: all clean
  • editorconfig-checker caught a real defect every other gate passed: a sed -i I used wrote one bare LF into AUDIT.md. Fixed and re-verified byte-wise; the diff stayed at 9 lines rather than a whole-file rewrite.

One bookkeeping item left for you

The keys_unsorted entry in .github/copilot-instructions.md carries Delete when - "nothing this check runs on carries a jq older than 1.6", and an enforced 1.7 floor arguably satisfies that. I left it in place rather than deleting it unilaterally, because it exists to answer a repeat reviewer finding and its measured proof is cited from the new jq entry. Your call whether it retires.

🤖 Generated with Claude Code

jq is used by the ruleset normalizer in repo-config/configure.sh, the ruleset
diff in AUDIT.md section 6, and the payload regeneration in
docs/repo-config-carry.md, and it was declared in neither the contract table nor
spec/host-tools.json. A host without it cannot apply, check, or audit a
repository's branch configuration, and nothing said so.
The floor is 1.6 and is a target rather than a measured breakage, and the entry
says which kind it is. 1.6 is where walk/1 arrived, so it is the lowest version
at which every jq construct the fleet documents compiles with no portability
workaround. Below it the fleet's own filters still run, because both that would
need walk define their own recursion instead, so a host on 1.5 is unverified
rather than known broken. The measured fact is the failure mode: on jq 1.5
walk/1 does not degrade, it fails to compile, so a diff built on it reports
drift on every ruleset it never compared. The claim that keys_unsorted also
needs 1.6 stays disproved, measured on jq-1.5-1-a5b5cbe.
The source note is release-dependent rather than uniform, because it was
checked: Debian trixie packages 1.7.1, older releases sit lower, Homebrew and
winget track upstream.
host-tools.json becomes a carried baseline file, so a repository states the
tools its own procedures need beyond the fleet declaration, and the tighten-only
layering has somewhere to live that a reader finds rather than has to know to
look for. A repository with nothing to add carries the stub with an empty tools
list, the footing OPERATIONS.md already set, and this repository's own root file
is that worked example. Measured before landing: none of the 22 cataloged
repositories carries one, so this adds one letter finding to each.
The gate also joins the procedures it was written for. STANDUP.md section 0 and
AUDIT.md now run it, alongside RESYNC.md, and each passes --repo, since the gate
reads the target's own declaration relative to that flag and a bare run layers
the hub's instead while printing the same healthy digest.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI lite review requested due to automatic review settings August 10, 2026 17:08

CopilotAI 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.

Pull request overview

This pull request updates the fleet host-tool contract to include jq (with a declared target floor), adds a carried per-repo host-tools.json stub, and wires the host verification gate into the documented onboarding/audit procedures so host misconfiguration is detected before running hub tooling.

Changes:

  • Declare jq as a required host tool in spec/host-tools.json with a minimum (target) version of 1.6, and reflect it in docs/host-setup.md.
  • Add host-tools.json to the baseline file set and introduce a root host-tools.json stub for repo-specific layering.
  • Document invoking scripts/host_gate.py --repo ... in STANDUP.md and AUDIT.md, and update tests to lock the expected floors/wording.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
FileDescription
STANDUP.mdDocuments running host_gate.py during standup and clarifies why --repo is required; updates baseline narrative for host-tools.json.
AUDIT.mdAdds a “verify host first” step calling host_gate.py --repo ... before audit runs.
spec/host-tools.jsonAdds jq as a required tool with a target minimum version and detailed rationale/source guidance.
docs/host-setup.mdUpdates the host contract table to include jq and adds the missing AUDIT.md link ref used by the new row.
spec/files.jsonAdds host-tools.json as a baseline file expected in all repos.
host-tools.jsonIntroduces the repo-local layering stub (empty tools list) with schema pointer and explanatory note.
scripts/test_host_gate.pyUpdates tests to include the new jq floor and generalizes target-floor wording assertions.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment threadSTANDUP.md Outdated
Comment threadspec/host-tools.json Outdated
The floor moves to 1.7, which is better anchored than either candidate before
it. Current Debian stable packages 1.7.1, so a distribution install satisfies
the floor and no host needs a manual build to meet it, while an older release
sits below and is the case the floor exists for. A floor at the newest release
instead would fail a host running every documented filter correctly, and a floor
at 1.6 would encode the walk/1 boundary rather than anything a host operator can
act on.
Two consequences swept rather than left. The floor now sits above 1.6, so walk/1
is available and the comments in AUDIT.md and repo-config/configure.sh that
justified defining a recursion by its absence were made false by this change.
Both now say the recursion is kept because it costs nothing and compiles below
the floor as well, which is the reason that survives.
The boundary is verified through the gate's own comparison rather than a
restatement of it: 1.5, 1.6 and 1.6.1 fail, 1.7, 1.7.1 and 1.8.2 meet it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 17:20

CopilotAI 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.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.

Suppressed comments (3)

STANDUP.md:28

  • This placeholder reads like it should point at the hub checkout (“this checkout”), but the surrounding text says --repo must point at the repository being stood up so its own root host-tools.json is layered. Using a target-repo placeholder here also matches AUDIT.md’s example.
python3 scripts/host_gate.py --repo <path-to-this-checkout> # run from a hub checkout, floors from spec/host-tools.json

host-tools.json:5

  • host-tools.json declares $schema: "./spec/host-tools.schema.json", but that schema requires tools to have at least 1 item (minItems: 1), while this stub intentionally sets "tools": []. This makes the file schema-invalid in editors/validators and contradicts the stated intent of carrying an empty stub.
{
"$schema": "./spec/host-tools.schema.json",
"note": "This repository's own host-tool declaration, layered over the fleet declaration in spec/host-tools.json by scripts/host_gate.py. The two are not the same file and do not hold the same thing. The fleet declaration states what every repository's procedures need and is the hub's to change. This one states what this repository needs beyond that, so it is where a tool only this repository uses, or a floor only this repository requires, is declared. Layering is tighten-only: an entry here may add a tool, raise a floor, or turn an optional tool required, and may not lower a floor or turn a required tool optional, because that would retire a fleet check from inside the repository it protects. A rejected relaxation is reported rather than dropped. The tools list is empty because this repository needs no tool the fleet declaration does not already carry, and the file is still present rather than absent, on the same footing as OPERATIONS.md: a repository with nothing to add carries the stub, so the declaration is somewhere a reader can find rather than somewhere they have to know to look.",
"tools": []
}

spec/host-tools.json:53

  • PR description states “The floor is 1.6”, but this change declares jq’s minimum as 1.7 (and docs/host-setup.md also says 1.7). Please reconcile the PR description with the implemented/documented floor to avoid confusion for readers relying on the PR text.
 "probes": [["jq", "--version"]],
"pattern": "jq-(\\d+(?:\\.\\d+)*)",
"minimum": "1.7",
"why": "Needed by the ruleset normalizer in repo-config/configure.sh, the ruleset diff in AUDIT.md section 6, and the payload regeneration in docs/repo-config-carry.md, so a host without it cannot apply, check, or audit a repository's branch configuration. The floor is a target rather than a measured breakage one version below it, and it says so rather than implying a defect nobody found. It is anchored to what the fleet's own platform provides: current Debian stable packages 1.7.1, so a distribution install satisfies the floor and no host needs a manual build to meet it, while an older release sits below and is the case the floor is for. That anchor is what keeps it actionable, since a floor set to the newest release instead would fail a host running every documented filter correctly. Two lower versions matter to a reader and neither is the floor. 1.6 is where walk/1 arrived, so the floor sits above it and walk is available, and the filters nonetheless define their own recursion, which costs nothing and keeps them compiling below the floor. On jq 1.5 walk/1 does not degrade, it fails to compile the filter, so a diff built on it reports drift on every ruleset it never compared, which is the inverse of the false clean the comparison exists to close. The neighbouring claim that keys_unsorted needs 1.6 is disproved rather than assumed, measured on jq-1.5-1-a5b5cbe where keys_unsorted evaluates and walk(.) answers a compile error, so the two builtins are not in the same position and the analogy between them is the whole of what carried that claim. A host below the floor is therefore unverified rather than known broken, since every filter here is written to a feature set older than the floor requires. A separate implementation is worth knowing about and is not this entry's subject: gh --jq is gojq built into the CLI rather than this binary, and it emits object keys sorted, which is why the section 6 filter canonicalizes key order itself rather than depending on whichever implementation produced its input.",

Three review findings, and the first is the one worth the code change. A floor
failure inlined the entry's why into the one line a reader scans, and an entry's
why runs to a paragraph, so the finding was unreadable in a terminal or a CI log.
Shortening one entry would have left the neighbouring one, nearly as long, doing
the same thing. So the head line is now the scannable fact and the rationale
follows it on its own indented line, which every entry gets. The printer splits
every newline rather than the first, since partitioning once left later
continuation lines inside one printed line without their indent.
The STANDUP.md placeholder was worse than ambiguous, it was impossible. Section 0
runs before the git init in section 0B, so there is no target checkout to point
--repo at, and the repo's own host-tools.json arrives with the baseline in
section 2. That step therefore runs bare against the fleet floors, says why it is
the one place the flag is omitted, and points at a second run after section 2
where the repo's own floors can count.
The claim that a bare run prints the same healthy digest either way was wrong and
is gone from both files that made it. A successful layering changes the tool
count and emits a note, and now that host-tools.json is a carried baseline file
every conformant repo has one to layer. The accurate statement is the one that
matters anyway: omitting --repo does not read the target's declaration at all, so
its floors go unapplied and the run reports nothing about the omission.
Verified by rendering a real failure rather than reasoning about the format,
layering a local entry that raises the jq floor above the installed version,
which also exercises the tighten-only direction.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 17:24

CopilotAI 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.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

Suppressed comments (1)

host-tools.json:4

  • host-tools.json declares $schema: "./spec/host-tools.schema.json", but that schema currently requires tools to have minItems: 1 (i.e., cannot be an empty list). As added here, the stub has "tools": [], so it is not schema-valid and will show as invalid in schema-aware editors/validators.

Consider either (a) updating spec/host-tools.schema.json to allow an empty tools list for repo-local layering stubs, or (b) pointing this file at a dedicated schema for the local overlay format.

 "$schema": "./spec/host-tools.schema.json",
"note": "This repository's own host-tool declaration, layered over the fleet declaration in spec/host-tools.json by scripts/host_gate.py. The two are not the same file and do not hold the same thing. The fleet declaration states what every repository's procedures need and is the hub's to change. This one states what this repository needs beyond that, so it is where a tool only this repository uses, or a floor only this repository requires, is declared. Layering is tighten-only: an entry here may add a tool, raise a floor, or turn an optional tool required, and may not lower a floor or turn a required tool optional, because that would retire a fleet check from inside the repository it protects. A rejected relaxation is reported rather than dropped. The tools list is empty because this repository needs no tool the fleet declaration does not already carry, and the file is still present rather than absent, on the same footing as OPERATIONS.md: a repository with nothing to add carries the stub, so the declaration is somewhere a reader can find rather than somewhere they have to know to look.",
"tools": []

The carried root host-tools.json pointed at spec/host-tools.schema.json, and that
schema forbids both shapes an overlay exists for. Its tools array carries
minItems 1, which rejects the empty list a repository with nothing to add
carries, and its per-entry required list demands name, probes, pattern, minimum
and why, which rejects the partial entry that lets a repository raise one floor
without restating a whole tool. The stub shipped schema-invalid.
Neither is visible from running anything. CI validates no JSON schema and
jsonschema is not installed, so the pointer's only reader is a schema-aware
editor, which means the file read as invalid exactly where it is edited and
nowhere else.
spec/host-tools-local.schema.json states the overlay format instead: an empty
list is allowed, only name is required, and the description records that the gate
rather than the schema is the authority on tighten-only layering, since a schema
compares nothing against the fleet declaration.
Four tests hold the split open. The overlay points at its own schema, the two
schemas differ on both constraints, the shipped stub satisfies its own, and the
overlay would still fail the fleet schema, which is the last one's whole point: if
that ever stops being true the split has no justification left and the test says
so rather than leaving two schemas nobody can tell apart.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 17:46
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Two suppressed findings answered, both correct.

The stub shipped schema-invalid. Fixed in 1dcff2c, and the defect was wider than the empty list. spec/host-tools.schema.json carries minItems: 1 on toolsand requires name, probes, pattern, minimum, why on every entry. So pointing an overlay at it forbids both shapes an overlay exists for: the empty list a repo with nothing to add carries, and the partial entry that lets a repo raise one floor without restating a tool. That second half matters more, since host_gate.py's field_problems is explicitly built for partial overrides ("Only fields that are present are judged, which is what lets an override carry one field without restating an entry"), so a real overlay adding just a floor would have been invalid too.

Took your option (b). spec/host-tools-local.schema.json now states the overlay format, requiring only name, allowing an empty list, and recording in its description that the gate rather than the schema is the authority on tighten-only layering, since a schema compares nothing against the fleet declaration.

Worth noting why nothing caught it: CI validates no JSON schema and jsonschema is not installed, so the $schema pointer's only reader is a schema-aware editor. The file read as invalid exactly where it is edited and nowhere else. Four tests now hold the split open, and the last one asserts the overlay would still fail the fleet schema, so if that stops being true the split has lost its justification and the test says so rather than leaving two schemas nobody can tell apart.

The PR description said 1.6 while the code said 1.7. Correct, and it was stale rather than wrong at the time: the floor moved from 1.6 to 1.7 mid-review on the maintainer's call, and I updated the body afterwards. It now states 1.7 and records both rejected candidates and why. Fair catch that a reader relying on the PR text would have been misled.

CopilotAI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

spec/files.json:21

  • host-tools.json points at ./spec/host-tools-local.schema.json, but the carry baseline in spec/files.json currently adds only host-tools.json (and not the schema file). That means newly stood-up downstream repos will carry a $schema reference to a path that doesn’t exist, so editors will flag the file as invalid and schema-aware tooling can’t validate it.
 { "path": ".gitignore", "appliesTo": "*" },
{ "path": "host-tools.json", "appliesTo": "*" },
{ "path": "version.json", "fidelity": "intent", "intentRef": "WORKFLOW.md#d3---versioning-and-classification", "appliesTo": "*" },

The hub's stub points at ./spec/host-tools-local.schema.json and no selector
carries a schema file, so a repository copying the stub would hold a relative
pointer to a path it does not have, and a schema-aware editor would report the
file invalid for a reason nobody there could fix. That is the dangling-pointer
problem this branch just fixed for the hub, reintroduced one step downstream.
The fleet already answers it and the answer is not a new rule. The hub's
spec/secrets.json carries ./secrets.schema.json and every downstream copy of that
file omits the key, checked against Blog, PlexCleaner and Utilities. STANDUP.md
section 2 now states that a carried copy drops the pointer and why, and the stub's
own note says it too, since the stub is what a repository copies and a worked
example that omits its own caveat is a trap.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 17:53
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Correct, and it is the same dangling-pointer defect this branch just fixed for the hub, reintroduced one step downstream. Fixed in 3aea9ac.

You are right that no schema file is carried: spec/files.json declares no *.schema.json entry at all, so a repository copying the stub would hold ./spec/host-tools-local.schema.json pointing at a path it does not have.

The fleet already answers this, so the fix is stating settled practice rather than inventing a rule. The hub's spec/secrets.json carries ./secrets.schema.json, and every downstream copy of that file omits the key - checked against Blog, PlexCleaner and Utilities, all three report no $schema. So a carried config drops the pointer because the schema is hub-only.

Two places now say so, because either alone leaves the trap open:

  • STANDUP.md section 2 states that the carried copy drops the pointer, why, and that spec/secrets.json is the precedent.
  • The stub's own note says it too, since the stub is what a repository copies and a worked example that omits its own caveat is exactly what a reader would follow into the defect.

I considered and rejected the two alternatives. Carrying the schema file downstream would make every repository hold a hub schema, which the carried-against-reached rule puts on the wrong side. An absolute hub URL in $schema would put a template-repo reference into a carried file, which GOVERNANCE.md "Documentation Style Conventions" bans outright.

CopilotAI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/test_host_gate.py:442

  • test_the_overlay_would_be_invalid_under_the_fleet_schema hard-codes a property of the current shipped overlay (empty list) and will start failing as soon as this repo’s overlay legitimately adds a tool override that happens to satisfy the fleet schema (e.g., a full entry). The justification for a separate overlay schema is that the schema is more permissive (empty list + partial entries), not that the current overlay must always be invalid under the fleet schema.

Consider replacing this with an assertion about the schemas’ relative permissiveness, and let the other tests cover the shipped overlay shape.

 def test_the_overlay_would_be_invalid_under_the_fleet_schema(self):
"""The reason the separate schema exists, asserted so it cannot quietly stop being true.
If the overlay ever satisfies the fleet schema, the split has no justification left and this
test says so rather than leaving two schemas nobody can tell apart.

The test asserting that the shipped overlay violates the fleet schema coupled a
schema-design invariant to one data instance. An overlay that legitimately grew a
full tool entry would satisfy the fleet schema and fail that test while the split
stayed exactly as justified, so it would have failed for being right.
What justifies the split is that the overlay schema accepts everything the fleet
schema does and more. That is now asserted in both directions over constructed
documents: a full entry passes both, and an empty tools list and a partial entry
pass the overlay and fail the fleet schema.
The helper reads only tools.minItems and the per-entry required list, which is the
whole of what separates the two, and says it is not a validator rather than
implying it is. jsonschema is not installed and CI validates no schema, so a
dependency-free check of the two axes is what can run.
Checked the replacement against the scenario that motivated it, an overlay holding
a full entry, where it passes and the old test would have failed. Then against two
mutants, tightening minItems back to 1 and demanding every field, each of which it
catches.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 18:00
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Right on both counts, and the fault was mine rather than a difference of taste. Fixed in 39b1527.

The test coupled a schema-design invariant to one data instance. An overlay that legitimately grew a full tool entry would satisfy the fleet schema and fail that test while the split stayed exactly as justified, so it would have failed for being right. And as you say, the justification is that the schema is more permissive, not that the current overlay must stay invalid under the other one.

Replaced with the invariant asserted in both directions over constructed documents:

self.assertTrue(self.accepts(self.fleet_schema, full_entry)) # everything the fleet acceptsself.assertTrue(self.accepts(self.local_schema, full_entry)) # the overlay accepts toofordocin (empty_list, partial_entry): # and two shapes it rejectsself.assertFalse(self.accepts(self.fleet_schema, doc))
self.assertTrue(self.accepts(self.local_schema, doc))

The helper reads only tools.minItems and the per-entry required list, which is the whole of what separates the two schemas, and its docstring says it is not a validator rather than implying it is. jsonschema is not installed and CI validates no schema, so a dependency-free check of the two axes under discussion is what can actually run.

Verified three ways rather than just running it green:

  • Against the scenario you named. With the overlay holding a full entry, the new test passes where the old one fails.
  • Against a mutant tightening the overlay's minItems back to 1: caught, the overlay schema rejects an empty tools list.
  • Against a mutant demanding every field of an overlay entry: caught, the overlay schema rejects a partial entry.

test_the_shipped_overlay_satisfies_its_own_schema still covers the shipped file's shape, which is the division of labour you suggested.

CopilotAI 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.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (1)

spec/host-tools-local.schema.json:34

  • The overlay schema is documented (and tested in scripts/test_host_gate.py) as differing from the fleet schema only in tools.minItems and the per-entry required list, but this schema currently also relaxes other constraints (e.g., minimum has no version pattern and source is any object). That makes schema-aware editors accept values the gate will reject (or that won’t produce useful install guidance), and it undermines the “only two axes differ” invariant the tests describe.

Tighten the shared fields to match the fleet schema (while keeping the overlay-only relaxations).

 "pattern": { "type": "string", "minLength": 1 },
"minimum": { "type": ["string", "null"] },
"why": { "type": "string", "minLength": 1 },
"source": { "type": "object" }

ptr727and others added 2 commits August 10, 2026 11:05
This branch makes host-tools.json a carried baseline file, so on its next audit
every repository in the fleet reports one file absent at once, through no fault of
its own. RESYNC.md section 0 routes a letter-heavy run to STANDUP.md, and the two
together would send a perfectly stood-up repository off to be stood up again over
a file the hub added yesterday.
The routing rule now says what a few letters usually mean: the baseline grew, so
the file is carried inside the resync at the point in section 3 where its class
belongs. A handful naming recent additions is a resync, and a baseline's worth
naming the instruction set is a standup.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 18:07

CopilotAI 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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

The overlay schema was hand-written from the fleet one and quietly relaxed two
more constraints than the two it means to. minimum lost its version pattern, so a
schema-aware editor accepted a floor the gate rejects, and source lost its named
platforms and its closed property set, so a key reaching no platform read as
valid. Meanwhile the tests beside it said only two axes differed, which made the
claim wrong rather than merely incomplete.
Both are tightened to the fleet constraint, and a test now compares the two
schemas field by field rather than trusting prose, since the alternative to
duplication is a cross-file $ref that editors resolve inconsistently.
That test immediately found a third divergence, which is the point of writing it:
the fleet declares required with a default of true and the overlay declared none.
That one is correct and stays. merge() applies only the fields an entry carries,
so an omitted required in an overlay means inherit the hub's value rather than
true, and a declared default would tell a reader the opposite of what the merge
does. It is excluded by name with that reason, so a fourth relaxation fails the
test rather than widening the claim.
Each exclusion is checked against a mutant: minimum losing its pattern, source
reopening its property set, and required declaring a default are all caught.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CopilotAI review requested due to automatic review settings August 10, 2026 18:13
@ptr727

Copy link
Copy Markdown
OwnerAuthor

Correct on both fields, and acting on it turned up a third. Fixed in 0e50dec.

The two you named.minimum had lost pattern: "^\\d+(\\.\\d+)*$", so a schema-aware editor accepted a floor parse_version rejects, and source had lost its named platforms and its additionalProperties: false, so a key reaching no platform read as valid. Both now match the fleet constraint exactly. You are also right about the wider problem: the tests beside it said only two axes differed, which made that claim wrong rather than merely incomplete.

The structural fix. A test now compares the two schemas field by field rather than trusting prose, because the alternative to duplication is a cross-file $ref that editors resolve inconsistently. That is what keeps hand-copied constraints honest.

The third divergence it found immediately, which is why the test was worth writing rather than just editing the two fields: the fleet declares required with default: true and the overlay declared none. That one is correct and stays, and it is a semantic difference rather than drift. merge() iterates entry.items(), so an overlay applies only the fields it carries, which means an omitted required inherits the hub's value rather than defaulting to true. Declaring a default here would tell a reader the opposite of what the merge does. It is excluded by name with that reason, plus a positive assertion that the key stays absent, so a fourth relaxation fails the test rather than quietly widening the claim.

The schema's own description now says three ways rather than two, and says a test rather than the sentence is what holds it.

Each exclusion is checked against a mutant rather than assumed to bite:

minimum loses its version pattern: caught
source reopens its property set: caught
required declares default true: caught

One process note, since it is the second time this round: this finding arrived suppressed, with no thread, and I only found it by diffing the finding list against my own replies. pr_review.py status reports suppressed=N (on_head=X earlier=Y), and the earlier bucket is easy to walk past once the head is clean.

CopilotAI 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.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

@ptr727
ptr727 merged commit 3855dbb into developAug 10, 2026
7 checks passed
@ptr727
ptr727 deleted the carry-the-host-contract branch August 10, 2026 18:22
ptr727 added a commit that referenced this pull request Aug 10, 2026
Two changes from one source: a stale enumeration a peer agent found, and
a backlog entry for the method that found it. TODO disposition: **New
entry**, per `TODO.md` rule 8.
## 1. The stale enumeration
Two enumerations of `spec/` list its contents and stop before the host
tool contract, which landed there in #652 along with a stub at the
repository root:
- `GOVERNANCE.md` "Repository Layout"
- `README.md`'s `spec/` bullet
Both were accurate when written. Neither is now, and a reader looking
for where the tool floors live would search both and find nothing.
### Provenance, and two claims that did not reproduce
Raised by the **ESPHome-Config agent** over peer messaging, not by a
gate. It hit the same class in its own `Repository Layout` while
planning its `AGENTS.md` -> `AGENTS.md` + `GOVERNANCE.md` split, and
named it as the trap the `Documentation Style Conventions` maintenance
rule exists for: prose no linter can see, going stale under a change
that never mentioned it.
I checked the hub for the same shape and for two adjacent claims from
the same report. Recorded because a future reader will wonder:
| Claim | Hub result |
|---|---|
| Carried docs still link `AGENTS.md#<section>` for sections that moved
to `GOVERNANCE.md` | **Not present.** Zero `AGENTS.md#` anchors
tree-wide, so the hub's anchors were rewritten with the split.
Downstream-only, for repos still carrying pre-split docs. |
| Other `gh api ... --jq '{...}'` projections feeding a diff share the
gojq key-sorting exposure fixed in #649 | **Not exposed.** The only
other one is the settings diff, where both sides pass through `jq -S .`
over a flat boolean object, so key order cannot reach the comparison.
Verified by running it both ways, `gh api --jq` and raw `gh api` piped
to real `jq`: byte-identical. |
`repo-config/` keeps its "apply script" wording, still true of the hub,
which hosts the script it no longer carries downstream.
## 2. The backlog entry
A `decision` cluster, because the mechanism needs no build and the only
open question is **which document may carry rules that bind a downstream
agent**. `GOVERNANCE.md` reaches those agents and costs a fleet-wide
re-vendor plus the two manifest edits `spec/section-model.md` requires
of any new section. A hub-only `docs/` file costs nothing and leaves the
rules unreachable from the repositories that would apply them, which is
the failure `AGENTS.md` "Fleet Bootstrap" exists to prevent.
Settled and recorded so it is not re-derived:
- **Cross-host does not work, by construction rather than
configuration.** A peer address is a Unix domain socket under
`/run/user/1000/cc-socks/`, which cannot cross a machine boundary. Cloud
sessions and Remote Control sessions are the documented cross-host paths
and neither appears in a listing on this host, so both are unverified
rather than absent.
- **The addressing has a guardrail worth keeping.** A bare peer name was
refused and the transport demanded the `[ref]` a listing prints, which
is what stops a message reaching the wrong repository's agent.
- **The method earns its place on evidence.** One exchange produced the
causal commit for the section 6 defect (`90e3255`), which this session
had not identified from the symptom; a one-line reproduction of the gojq
behavior that made an earlier fix pass for the wrong reason; and four
procedure gaps no gate reports.
- **A peer's finding is checked rather than adopted.** Two of those four
did not reproduce here, one did and is part 1 of this PR.
- **The boundary that matters is permission, not politeness.** A peer
cannot widen what the asking session may do, so blocked work goes back
to the maintainer rather than sideways to another agent.
## Verification
`prose_lint --diff HEAD`, `repo_gate --check eol`,
`editorconfig-checker`, `markdownlint-cli2` on `TODO.md`, and `cspell`
on the gated files: all clean. Both `[files]` and `[section-model]`
reference names already resolve in `TODO.md`. `GOVERNANCE.md`
"Repository Layout" is `intent` fidelity per `spec/section-model.md`, so
part 1 forces no fleet-wide re-vendor.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 10, 2026
## Problem
#650 introduced the `hub-only:` finding, whose remedy removes a file
rather than updating one. #651 and #652 stated that remedy in `AUDIT.md`
section 10 and `RESYNC.md` section 4 as: delete the copy, reach the
hub's, and *"where a carried doc named the local path, point it at the
hub's."*
That covers one of three shapes and assumes a hub equivalent exists. The
two it missed are the ones that cost something.
**A runnable command citing the deleted path.** This is the expensive
shape, because a documented command *fails* where a dead link merely
disappoints. Measured rather than argued:
```
NxWitness repo-config/README.md
13: - [`configure.sh`](./configure.sh) - idempotent `gh api` script with two modes:
14: - `./repo-config/configure.sh check` - validate only, no writes...
17: - `./repo-config/configure.sh apply` - create-or-update the rulesets...
61: REPO=ptr727/NxWitness ./repo-config/configure.sh apply
62: REPO=ptr727/NxWitness ./repo-config/configure.sh check
```
Retiring that one file makes **five lines of a carried document wrong,
two of them commands a reader would run**. `configure.sh` is carried by
**15 repositories**, so this is queued work rather than a hypothetical.
**A mention with no hub equivalent.** It is removed rather than
re-pointed, and removing a reference-style link removes its definition
with it, per `GOVERNANCE.md` "Documentation Style Conventions", *because
an orphaned definition fails the no-unused-defs rule.* So the old remedy
could leave a **gate failure** behind while reading as complete.
## Provenance
Raised by the **ESPHome-Config agent** over peer messaging, from a case
of its own: deleting a nested `easystart/AGENTS.md` at Pieter's
instruction, it found three inbound references across two files, one of
them a reference definition that would have been left orphaned. It put
the scope question to Pieter rather than deciding it, and that is what
surfaced the generalization.
I measured the fleet-wide shape rather than adopting the specific case:
checked five repositories' `repo-config/README.md` for links (not merely
mentions) to `configure.sh`, which is how the NxWitness figures above
were obtained. Four mention it without linking; one links it and
documents four invocations.
## Change
Both statements of the remedy now name the sweep as part of the
deletion, with the three shapes and their different fixes. `RESYNC.md`
additionally carries the measured evidence, since it is the procedure an
agent follows step by step.
## Verification
`prose_lint --diff HEAD`, `repo_gate --check eol`, `markdownlint-cli2`
on both files, `editorconfig-checker`: all clean. Both files gained a
`governance-documentation-style` reference definition, and both
reference blocks re-verified as sorted by reference name.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727 added a commit that referenced this pull request Aug 10, 2026
Promotion of six squashes. **Merge with a merge commit, never a squash,
and never with `--delete-branch`** — this PR's head *is* `develop`.
| PR | What it fixed |
|---|---|
| [#649](#649) |
`AUDIT.md` section 6's ruleset diff projected `bypass_actors`, which no
payload declares, so jq exited 5 and **every ruleset on every repo
reported DRIFT**. Two further surfaces carried the same dead policy. |
| [#650](#650) | The
deletion detector: hub git-tracked paths minus the `spec/files.json`
baseline, so a retired file is derived rather than remembered. Corrected
`configure.sh` from 6 carriers to **15**. |
| [#651](#651) |
`RESYNC.md`, the third entry point, routed from the byte-locked
`AGENTS.md` "Fleet Bootstrap". |
| [#652](#652) | A `jq`
1.7 target floor, `host-tools.json` as a carried baseline file, and
`scripts/host_gate.py` wired into all three procedures for the first
time. |
| [#653](#653) | Two
`spec/` enumerations that went stale when the host contract landed
there. |
| [#654](#654) | The
inbound-reference sweep counts as part of a deletion, including the
runnable-command and orphaned-definition shapes. |
## Why this promotion matters more than most
Downstream repos read hub `main` as ground truth. While `main` sits at
[`0a86bca`](0a86bca):
- Every repo that runs the `AUDIT.md` section 6 snippet gets **false
ruleset drift on both branches**. The ESPHome-Config agent reproduced
this live and is holding its own fix stashed rather than diverging from
the hub.
- A repo resyncing now re-vendors to `main`'s revision and then again
after promotion. Verified against Blog, whose carried `AGENTS.md` "Fleet
Bootstrap" differs from both refs, so it is two re-vendors instead of
one.
- `RESYNC.md` does not exist on `main`, so a downstream agent asking
"how do I sync" still routes to `AUDIT.md`, which measures and
deliberately states no order.
## Fleet cost this carries, measured and accepted
`host-tools.json` becomes a carried baseline file, so it is one `LETTER`
on **22 of 22** cataloged repos, the hub included. That was measured
before landing and accepted by the maintainer. `RESYNC.md` section 0 was
amended so a letter wave of this shape reads as a file to carry inside a
resync rather than as evidence a repo was never stood up.
## Verification on the merged head
`spec/validate.py` OK, `spec/audit.py --selftest` PASS, 557 script tests
OK, `scripts/host_gate.py` clean over 7 declared tools, `repo_gate.py
--check eol` clean, `editorconfig-checker` clean, working tree clean.
Post-merge fleet audit: 22 repos, **zero errors**.
Two hub-side artifacts clear on this merge, both promotion-pending
rather than defects: the hub's own `AGENTS.md` "Fleet Bootstrap" reads
as stale against its `develop` canonical, and the hub reports its own
`host-tools.json` absent because the file exists on `develop` only.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for freeto 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

@ptr727