Carry the Host Contract and Declare the jq Floor - #652
Conversation
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>
There was a problem hiding this comment.
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
jqas a required host tool inspec/host-tools.jsonwith a minimum (target) version of 1.6, and reflect it indocs/host-setup.md. - Add
host-tools.jsonto the baseline file set and introduce a roothost-tools.jsonstub for repo-specific layering. - Document invoking
scripts/host_gate.py --repo ...inSTANDUP.mdandAUDIT.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
| File | Description |
|---|---|
| STANDUP.md | Documents running host_gate.py during standup and clarifies why --repo is required; updates baseline narrative for host-tools.json. |
| AUDIT.md | Adds a “verify host first” step calling host_gate.py --repo ... before audit runs. |
| spec/host-tools.json | Adds jq as a required tool with a target minimum version and detailed rationale/source guidance. |
| docs/host-setup.md | Updates the host contract table to include jq and adds the missing AUDIT.md link ref used by the new row. |
| spec/files.json | Adds host-tools.json as a baseline file expected in all repos. |
| host-tools.json | Introduces the repo-local layering stub (empty tools list) with schema pointer and explanatory note. |
| scripts/test_host_gate.py | Updates 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.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
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>
There was a problem hiding this comment.
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
--repomust point at the repository being stood up so its own roothost-tools.jsonis 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.jsondeclares$schema: "./spec/host-tools.schema.json", but that schema requirestoolsto 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>
There was a problem hiding this comment.
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.jsondeclares$schema: "./spec/host-tools.schema.json", but that schema currently requirestoolsto haveminItems: 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>
ptr727
commented
Aug 10, 2026
Two suppressed findings answered, both correct. The stub shipped schema-invalid. Fixed in 1dcff2c, and the defect was wider than the empty list. Took your option (b). Worth noting why nothing caught it: CI validates no JSON schema and 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. |
There was a problem hiding this comment.
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.jsonpoints at./spec/host-tools-local.schema.json, but the carry baseline inspec/files.jsoncurrently adds onlyhost-tools.json(and not the schema file). That means newly stood-up downstream repos will carry a$schemareference 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>
ptr727
commented
Aug 10, 2026
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: The fleet already answers this, so the fix is stating settled practice rather than inventing a rule. The hub's Two places now say so, because either alone leaves the trap open:
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 |
There was a problem hiding this comment.
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_schemahard-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>
ptr727
commented
Aug 10, 2026
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 Verified three ways rather than just running it green:
|
There was a problem hiding this comment.
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.minItemsand the per-entryrequiredlist, but this schema currently also relaxes other constraints (e.g.,minimumhas no version pattern andsourceis 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" }
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>
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>
ptr727
commented
Aug 10, 2026
Correct on both fields, and acting on it turned up a third. Fixed in 0e50dec. The two you named. 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 The third divergence it found immediately, which is why the test was worth writing rather than just editing the two fields: the fleet declares 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: 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. |
Uh oh!
There was an error while loading. Please reload this page.
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>
## 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>
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)
Your call, per the decision to accept the fleet-wide finding. Floor set to 1.7 per your follow-up.
The jq gap
jqis used by the ruleset normalizer inrepo-config/configure.sh, the ruleset diff inAUDIT.mdsection 6, and payload regeneration indocs/repo-config-carry.md. It was declared in neither the contract table norspec/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 aspython3, 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:
walk/1boundary, 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/1does not degrade, it fails to compile, so a diff built on it reports drift on every ruleset it never compared. And the claim thatkeys_unsortedalso needs 1.6 stays disproved per the.github/copilot-instructions.mdrecord, measured onjq-1.5-1-a5b5cbe.Consequences swept, not left
The floor now sits above 1.6, so
walk/1is available — which made false the comments inAUDIT.mdandrepo-config/configure.shthat 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
toolslist — the footingOPERATIONS.mdalready set — and this repo's new root file is that worked example, with anotedistinguishing it fromspec/host-tools.json.Measured before landing: none of the 22 cataloged repos carries one, so this adds exactly one
LETTERper repo. This PR satisfies the hub's own.The gate joins the procedures it was written for
scripts/host_gate.pyexisted and no procedure ran it.STANDUP.mdsection 0 andAUDIT.mdnow do, alongsideRESYNC.mdin #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_reasonasserts the floor set exactly, so a new floor cannot land unnoticed. I updated it and generalizedtest_a_target_floor_says_so_rather_than_implying_a_defectfrom the singlepython3entry 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_versionrather than a reimplementation of them:--selftestPASS,validate.pyOKhost_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,cspellon the gated files: all cleaneditorconfig-checkercaught a real defect every other gate passed: ased -iI used wrote one bare LF intoAUDIT.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_unsortedentry in.github/copilot-instructions.mdcarries 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 newjqentry. Your call whether it retires.🤖 Generated with Claude Code