Visitar URL original
Promote Develop: Burndown Fixes to Hooks, Review Tooling, and Carried Rules by ptr727 · Pull Request #4189 · ptr727/ProjectTemplate · GitHub
Skip to content

Promote Develop: Burndown Fixes to Hooks, Review Tooling, and Carried Rules - #4189

Merged
ptr727 merged 34 commits into
mainfrom
develop
Nov 13, 2026
Merged

ptr727 merged 34 commits into
mainfrom
develop

Conversation

@ptr727

@ptr727 ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner

Summary

Promotes the backlog-burndown rounds and changes from other hub sessions from develop to main.

#4182 carries no promotion line by design, and #4175 holds the split-out part of #4057's scope. Both #1448 and #4175 stay open.

Fixes #1071
Fixes #1154
Fixes #1391
Fixes #1471
Fixes #1594
Fixes #1620
Fixes #1627
Fixes #1650
Fixes #1801
Fixes #2853
Fixes #3620
Fixes #3668
Fixes #3697
Fixes #3766
Fixes #3787
Fixes #3837
Fixes #3840
Fixes #3842
Fixes #3844
Fixes #3865
Fixes #3882
Fixes #3890
Fixes #3894
Fixes #3912
Fixes #3916
Fixes #3954
Fixes #4014
Fixes #4015
Fixes #4016
Fixes #4043
Fixes #4050
Fixes #4057
Fixes #4058
Fixes #4062
Fixes #4084
Fixes #4097
Fixes #4098
Fixes #4099
Fixes #4104
Fixes #4108
Fixes #4114
Fixes #4115
Fixes #4117
Fixes #4118
Fixes #4119
Fixes #4121
Fixes #4122
Fixes #4123
Fixes #4131
Fixes #4135
Fixes #4140
Fixes #4142
Fixes #4143
Fixes #4144
Fixes #4151

🤖 Generated with Claude Code

ptr727 and others added 30 commits November 12, 2026 21:55
…, and Drop the LF Pin (#4132)

## Summary

- **The path list is read once and its read is checked (#4122).** Both
the hub's own `.husky/pre-commit` and
`catalog/snippets/husky/pre-commit` ran `git diff --cached --name-only`
twice: a checked preflight, then an unchecked second run piped into
`xargs`. Under POSIX `sh` a failure of the second run reached the gate
as an empty list, which skips it. The list now goes to a `mktemp` file
whose write is checked, and `xargs` reads that file. An `EXIT` trap
removes it, and `INT`/`TERM` traps print one line and exit 130/143 so
the `EXIT` trap runs under dash too.
- **The snippet diffs a merge commit against `MERGE_HEAD` (#4104).** It
now picks its base the way the hub's own hook already does, so the lines
a merged-in parent brought are not read as added. After merging the
target branch in, `MERGE_HEAD` is the merge base the pull request gate
reads, so the hook and CI read the same lines. The issue floated the
merge base of `HEAD` and `MERGE_HEAD`. That base would read the
merged-in parent's own lines as added, which is the defect itself.
- **The snippet README drops the per-file LF pin (#4121).** The paired
LF defaults `recurring.eol` requires already give the hook LF. Only a
repository whose two defaults are CRLF pins it. The hub hook's header
line naming "the line-ending pins" is removed with it.
- **Tests.** `tests/test_pre_commit_hook.py` now runs every case against
both hooks. New cases fail the first or the second list read through a
`git` stub and assert the commit is refused, and signal the hook
mid-gate and assert the 130/143 status with the list file gone. Against
the develop hooks, eight of the new or re-run cases fail. Against this
branch, all 24 pass.

## Pre-existing findings, filed

- `catalog/snippets/pre-commit/.pre-commit-config.yaml:36`, correctness:
the `pre-commit` framework config still diffs a merge commit against
`HEAD`, the #4104 class at its third site. Filed as #4131.

## Local review findings declined

- `.husky/pre-commit:52-65` and
`catalog/snippets/husky/pre-commit:52-65`, design (one primitive per
defect class): the local pass suggested moving the base choice and the
staged-list read into a `prose_lint.py` option. That file is a carried
gate outside this change's scope, and the new option would change its
interface. The two hooks already carried the same list read and `xargs`
filter side by side before this change. #4131 records the
`prose_lint.py` shape as the fix that would also reach the framework
config.

Closes on promotion: #4122, #4104, #4121

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Commit checks now focus on staged, working-tree files and account for
merge resolutions, reducing unrelated or generated content from being
flagged.
* Temporary check files are cleaned up when checks finish or are
interrupted, and failures are reported with appropriate exit statuses.

* **Documentation**
* Clarified when copied commit hooks need explicit line-ending settings,
including for repositories that use CRLF defaults.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ess (#4134)

## Summary

- `CODESTYLE.md` "Interrupts": after its cleanup, an interrupted script
re-raises the signal rather than calling `exit 130` or `exit 143`. A
shell still reports 130 or 143. At Ctrl-C, a calling shell's loop now
stops instead of starting its next iteration. A launcher such as `uv
run` exits with the status rather than dying by the signal, and the rule
says so. The mid-run test asserts the script died by the signal, and
starts the script with its interpreter directly.
- "Interrupts in Python": the entry point flushes its output under
`contextlib.suppress(OSError)`, restores the default handler with
`signal.signal(signum, signal.SIG_DFL)`, and re-sends the signal with
`os.kill(os.getpid(), signum)`. The re-raise runs outside every `with`
block and `finally` clause, and no cleanup on its path relies on an
`atexit` handler or a finalizer. A process the signal does not end, such
as PID 1 in a container, then exits 128 plus the signal number. On
Windows, where `os.kill` would end the process with the signal number as
its status, it keeps exiting 130.
- "Interrupts in Shell": each trap clears itself with `trap - INT` or
`trap - TERM`, re-sends the signal with `kill -s INT "$$"` or `kill -s
TERM "$$"`, then ends with `exit 130` or `exit 143`, which runs only
where the signal does not end the script, such as PID 1. The `EXIT` trap
is set before the `INT` and `TERM` traps, since bash skips it when a
`TERM` trap set ahead of it re-sends SIGTERM.
- "Interrupts" exempts a script that ends by `exec`-ing its long-running
process, such as a container entrypoint or an s6 or runit `run` script.
A background child may be handed across the `exec`, neither stopped nor
waited for, where something else stops it, such as a container's
teardown. Anything else the script holds is released before the `exec`.
A script holding nothing else at any point before the `exec` owes no
trap and no signal test. As PID 1, such a script ignores a SIGTERM that
arrives before its `exec`, and the rule says so. "Interrupts in Shell"
points at that exemption from its trap and `wait` bullets.
- `python-codestyle` and `shell-codestyle` regenerated through
`scripts/build_dist.py`. `python-codestyle`'s description is unchanged.

## Verification

A constructed probe, run under `setsid --wait timeout`: a
non-interactive bash loop runs a child three times, and SIGINT goes to
the loop's whole process group, as Ctrl-C does. With bash 5.2, Python
3.13, and uv 0.13:

| Child | Loop after SIGINT |
| --- | --- |
| bash, trap ends with `exit 130` | runs iterations 2 and 3, exits 0 |
| bash, trap re-raises | stops, exits 130, `EXIT` trap ran |
| Python, `sys.exit(130)` | runs iterations 2 and 3, exits 0 |
| Python, re-raise | stops, exits 130, `finally` ran |
| Python, re-raise, through `uv run --script` | runs on, uv exits 130 |

With SIGTERM sent to the child alone, the loop reads status 143 from
both forms. Further probes: a bash `TERM` trap set before the `EXIT`
trap skips the `EXIT` trap on its re-sent SIGTERM, and runs it when set
after. A Python PID 1 in a PID namespace survives its own re-sent
SIGTERM and reaches the 143 fallback. Bash as PID 1 with no `EXIT` trap
survives its own re-sent SIGTERM and runs on, and the trailing `exit
143` ends it. Bash as PID 1 with no traps ignores a SIGTERM sent from
outside its namespace. `build_dist.py --check`, the prose gate, and
markdownlint pass.

## Local review

Each push carries a recorded local pass. The findings those passes
raised were fixed on this branch, so none stays open.

Closes on promotion: #4084, #4097

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

Closes on promotion: #4098, #4014

- **#4098.** `RESYNC.md` section 1 now states the `develop` exception
`GOVERNANCE.md` "Hub-Hosted Tooling" carries: a repository whose
registry entry has merged to `develop` but not yet reached `main` runs
`repo-config/configure.sh`, `scripts/carry.py`, and `spec/audit.py` from
a hub checkout at `develop`. Step 5 and the `resync-a-repo` skill (its
hub-fetch paragraph and its step 5) say the same. A sweep of the other
hub text naming a `main` hub checkout for these commands (`AUDIT.md`
section 6, `STANDUP.md`, `docs/repo-config.md`, `OPERATIONS.md`,
`repo-config/README.md`, `scripts/README.md`, `host-setup/README.md`,
`audit-a-repo`, `standup-a-repo`, and the branching reference) found
each already carrying the exception, so none changes.
- **#4014 item 1.** `audit-a-repo` names `<RepoName>` on its `--branch`
preview and its secrets command, so a preview no longer audits every
cataloged repository.
- **#4014 item 2.** `carried-instruction-file-guard` keeps a local
section in place where the carried file is itself the destination, such
as `CODESTYLE.md` for a language convention, and merges the carried text
around it.
- **#4014 item 3.** `resync-a-repo` now creates the hub and target
worktrees and runs `scripts/host_gate.py` before the routing audit in
"Confirm the procedure before starting", rather than after it.
- **#4014 item 4.** `check-this-repo` treats a git `live.commit` behind
the hub's tip of `live.branch` as an answer, and states the `git
merge-base --is-ancestor` test that finds it.
- **#4014 item 5.** A root `.devcontainer.json` implies the
`devcontainers` Dependabot ecosystem in `spec/audit.py`, in `AUDIT.md`
section 6's prose and probe, and in `spec/project-types.json`'s
assertion. `spec/audit.py` grows by two lines.

## Local Review Findings Disposed After the Opening Push

The third local pass left three `introduced` findings open at the
opening push. The next push disposes of each.

- Fixed: `check-this-repo` names the refspec `git fetch origin
"+refs/heads/<live.branch>:refs/remotes/origin/<live.branch>"`, so a
single-branch hub clone records `origin/<live.branch>`.
- Fixed: `RESYNC.md` section 0 runs its routing measurement after
section 1's host gate, as `resync-a-repo` does.
- Declined: reading `run.tree` in place of the two `contents` reads in
`check_dependabot_ecosystems`. The `github-actions` arm beside it reads
the contents API the same way, and `run.tree` is `None` for a truncated
tree, so the change needs a fallback branch for no correctness gain in a
file near its line ceiling.

A fourth pass found no test for the `.devcontainer/` directory arm, now
covered by a directory-only case in `tests/test_audit.py`.

## Verification

- `tests/test_audit.py`: new whole-run cases report the missing
ecosystem for a tree holding only `.devcontainer.json`, and for one
holding only a `.devcontainer/` directory, with a clean control. The
first fails against the unfixed `spec/audit.py`, and the second fails
with `.devcontainer` dropped from the loop.
- `tests/test_release_guards.py`: the `AUDIT.md` probe test gains a
root-file fixture that must print `devcontainers: MISSING`. It fails
against the unfixed `AUDIT.md`.
- `tests/test_audit_helpers.py`, `tests/test_release_guards.py`,
`spec/audit.py --selftest`, `spec/validate.py`, and
`scripts/build_dist.py --check` pass. The pre-commit gates pass.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…One Token (#4136)

## Summary

- The prose gate's `charset` and `charset-unknown` rules now read a
Markdown inline code span as quoted data, the way they already read a
fenced block. This applies the maintainer's recorded answer on #4058,
the first direction, with no new tier or marker. A string quoted
verbatim from another source keeps its bytes inside the sentence it sits
in.
- Code spans are paired as Markdown pairs them, through a new `spanless`
unit built from markdown-it-py's inline tokens with the existing
span-aware `MARKDOWN_SPAN` pattern. So a span wrapped across a line
break, a double-backtick span, and an escaped backtick are each read
correctly. Code-file lines, Markdown front matter, and HTML block lines
are read whole.
- The `semicolon`, `dash`, `dupword`, `spelling`, and `sentence-split`
rules read a `codeless` unit, paired the same way, instead of pairing
backticks one line at a time, which hid the prose after a wrapped span.
On front matter and HTML block lines, which no inline token holds, that
unit still empties a span one line at a time. `strip_inline_code` now
delegates to the same `blank_code_spans` primitive, and a span empties
to its backticks so a label holding one still counts it as one word.
- The `semicolon` rule no longer reports a semicolon with a word
character on both sides, such as the one inside `TL;DR`. An HTML
entity's closing semicolon is still reported.
- The `comment-and-doc-style` skill's "Character set" section,
`scripts/README.md`, and the gate's `semicolon` summary in
`prose-gate.json` state both exceptions. The generated skill copies are
regenerated with `scripts/build_dist.py`.

## Tests

New cases in `tests/test_prose_lint.py` each failed against the gate
before the change that fixes it:

- an unclassified character, a tier-2 operator with no figure beside it,
and a tier-1 dash each pass inside a code span and inside a fence, and
still flag outside one
- a wrapped span, a double-backtick span, and an escaped backtick hide
only the span's own text
- a code span in a table cell is emptied within its cell, a row is
emptied once so text between two spans is read, and front matter, an
HTML block, and a shell comment are read whole by charset
- front matter and an HTML block still hide a span from the semicolon
and dash rules, and a span in a three-word label keeps the labeled
pair's exemption
- dupword, spelling, and sentence-split read the text after a wrapped,
double-backtick, or escaped span, and a span wrapped inside a blockquote
keeps its quote marker
- `TL;DR` passes, an HTML entity's semicolon and a spaced splice still
flag, and a wrapped span hides no semicolon or dash after it

## Pre-existing findings, filed

- `pre-existing`: `.github/actions/prose-gate/prose_lint.py`
`mask_code_and_quotes` and `rule_dead_path` still pair code spans
without the span-aware pattern, filed as #4135.

## Declined findings

- `introduced`: `sentence-split` no longer reports a line whose next
line opens inside a code span wrapped across the break. The rule judges
whether the next prose line opens in lowercase, and a span's text is
code rather than prose. On develop a span opening its line was already
read as backticks and never as lowercase, so the old report came only
from the unpaired closer.
`test_a_wrapped_code_span_continues_no_prose_line` pins the behavior.

- `pre-existing`: the `charset-unknown` summary in `prose-gate.json`,
`OPERATIONS.md` "Local Verification", and the `charset-unknown`
paragraph of `scripts/README.md` name a Latin letter as the only
unreported character in no tier. Each describes the characters the rule
reads, and a fenced block was already outside that read without being
named there, so the code span joins an exclusion those statements never
enumerated.

Closes on promotion: #4058, #3954

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Names the two gaps from #4117 as hand checks in RESYNC.md section 5: a
caller workflow pin behind the newest hub release, and a missing
`hub_fetch_run.py` or a hook config that does not call it. Whether
`spec/audit.py` should check the pin mechanically is decision #4137 and
is not implemented here. The hook-helper half stays a hand check because
GOVERNANCE.md says `parity.hooks` is never mechanized by
`spec/audit.py`.

Also fixes a disagreement #4133 left: the RESYNC.md flowchart labelled
step 1 "fetch hub main" without the develop exception #4133 added to the
section 1 prose. The label now carries it.

Closes on promotion: #4117

Local review: the findings raised by the passes and by hosted review
were fixed; the last recorded pass found nothing.

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
…Is Required (#4141)

## Summary

Corrects four code style rules that fail when followed (#4015), and
states plainly that the shell code style requires no header comment
(#3668).

- **SA1201 member order.** `dotnet-codestyle`
`references/conventions.md` sorted members by access first. It now
orders by kind across every access level (SA1201), then by access within
one kind (SA1202), then const, static, and readonly (SA1203, SA1204,
SA1214). The kind list now follows StyleCop's published SA1201 order,
which places conversion operators and operators before methods. The
`SKILL.md` summary line says the same.
- **PowerShell unknown arguments.** `CODESTYLE.md` "Command-Line
Arguments" now says the binder refuses an unknown parameter only in a
script carrying `[CmdletBinding()]`. A plain `param()` block binds it
into `$args` and runs on, so such a script refuses a non-empty `$args`
itself before any work. `python-codestyle` and `shell-codestyle` carry
the section as a generated include.
- **POSIX shell exception.** `shell-codestyle` now carries the
`GOVERNANCE.md` exception: a deliberately POSIX `#!/bin/sh` script takes
`set -eu`.
- **Local loop venv.** Both pip-form `uv pip install` lines in
`python-codestyle` now pass `--python .venv/bin/python`, since `uv pip
install` targets an active `VIRTUAL_ENV` ahead of `./.venv`. The
`catalog/snippets/configs/vscode-tasks-python.json` comments describing
the same installs say the same.
- **Shell header comment.** `shell-codestyle` now says no header comment
is required, so the `set` line may follow the shebang. A comment above
it carries a specific non-obvious point, and the no-summary rule in
`comment-and-doc-style` still forbids one restating the script. The
description calls `set -Eeuo pipefail` a line rather than a header.

Generated copies under `.github/skills/` and `.claude-plugin/`
regenerated with `scripts/build_dist.py`, and `--check` is clean.

The snippet change edits two JSONC comment lines, so this pull request
carries the `comments` label.

## Local review findings

- **`GOVERNANCE.md` "Workflow YAML Conventions", the "Shells" bullet's
POSIX exception reason** (pre-existing, canonical unit). It says `sh`
carries neither `-E` nor `pipefail`, which is broader than true. This
branch states the narrower dash reason in `shell-codestyle`. Gathered
onto the unit's tracker, #4140.
- **`catalog/snippets/husky/pre-commit`, `set -eu` follows the
`_/husky.sh` source line** (pre-existing, declined). The reviewer's
stated failure is a command failing inside the sourced `_/husky.sh`
going unnoticed. That file is a trusted runtime the hook installer
generates. Per `GOVERNANCE.md` "Trust Boundaries and Hardening Effort",
a silent failure inside it is not a realistic case in the hook's
deployment, so it earns neither a fix nor an issue. Moving `set -eu`
above the source line would also put the generated file's own exit
status under `-e`, which this branch has no evidence is safe.

Closes on promotion: #4015, #3668

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

`session-handoff` "Closing a Session" step 4 removed the hub clone after
checking only `git status --porcelain`, and asserted that the clone's
ignored files are tool caches. `repo-worktree` "Listing and Cleanup"
requires a hand-removed temporary standalone clone to read `git status
--porcelain --ignored` first, since `--porcelain` alone never lists the
ignored files the removal deletes.

- Step 4 now runs one `git -C <clone> status --porcelain --ignored
--untracked-files=normal` read. Any line not starting `!!` leaves the
clone in place. Otherwise the session names each `!!` entry in its
closing message and deletes the clone. That is the same one-read check
`repo-worktree` "Listing and Cleanup" now uses for a hand removal.
- The unsupported "tool caches" claim is deleted rather than qualified.
- The explicit untracked mode is there because a
`status.showUntrackedFiles=no` setting hides untracked and ignored
entries alike, and `git worktree remove` obeys it too. The same class is
fixed in `repo-worktree` "Listing and Cleanup". Its hand-removal step
now confirms clean with that read, run with `-C <exact-path>`, and stops
on any line other than an ignored entry. Three `backlog-burndown`
removals that called a worktree clean without naming a check now name
that read. Its cleanup proof's `git status --short --branch` takes the
flag as well. `backlog-burndown`'s read of a registered worktree's
uncommitted files takes `--untracked-files=all`, since `normal`
collapses a wholly untracked directory to one entry that matches no
predicted file path.
- `.github/skills/` and `.claude-plugin/fleet-skills/` regenerated for
all three skills with `scripts/build_dist.py`, and `--check` is clean.

## Pre-existing findings, filed

- `host-setup/menu.sh`, `host-setup/menu.ps1`, `scripts/carry.py`,
`scripts/skills_install.py`, and
`host-setup/agent-safety/claude/install.py`, their cleanliness reads: a
bare `git status --porcelain` that `status.showUntrackedFiles=no`
blinds, a correctness defect in code that runs. Filed as #4142.

## Pre-existing findings, tracked per unit

- `.agents/skills/repo-worktree/SKILL.md` "Creating a Worktree", the
continuation paragraph's "Remove it when it is clean", which names no
read. Tracked on #4143.
- `.agents/skills/backlog-burndown/SKILL.md` "Dispatching a Worker",
"Cleanup Is the Orchestrator's" "A clean tree is the whole test", which
names no read. Tracked on #4144.

## Pre-existing findings, not filed

- `scripts/README.md`, the `pr_cleanup.py` contract's claim that `git
worktree remove` refuses tracked and untracked changes. Under
`status.showUntrackedFiles=no` it does not refuse untracked ones.
`pr_cleanup.py` itself passes an explicit mode, so only the contract
text is wrong. Class: false statement in a non-canonical doc.

Closes on promotion: #4115

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…rkdown Does, and Re-Raise Hook Interrupts (#4145)

## Summary

- **The prose gate chooses a commit's diff base itself (#4131).**
`prose_lint.py --diff-commit` diffs against `HEAD`, against both parents
while a merge is in progress, and against the empty tree before a first
commit. On a merge it keeps the lines both the `MERGE_HEAD` and the
`HEAD` diff name, the lines new against both parents, and a move one
parent's diff records only where the other parent lacks the new path. A
moved file reports only what no recorded source held. The empty tree is
hashed in the format `extensions.objectFormat` names, SHA-1 where unset,
which a git before 2.29 can read. The pre-commit framework snippet
passes it in its `entry`, which stays one command line, so a merge
commit no longer reads the merged-in parent's lines as added on any
host. Both Husky.Net hooks drop their own copy of that choice.
`hub_fetch_run.py` drops its unborn-`HEAD` rewrite, which the gate now
owns, and which hard-coded the SHA-1 empty tree. `--diff` beside
`--diff-commit` is refused.
- **Code spans pair as Markdown pairs them (#4135).**
`mask_code_and_quotes` (`sentence-length`) blanks spans through
`blank_code_spans`, so an escaped backtick opens no span. `dead-path`
reads a new `masked` unit, each inline token's spans spaced out as
Markdown pairs them, so a span wrapped across a line hides no later link
or span, and a link inside a wrapped span is read as code.
- **The Husky hooks re-raise an interrupt (#4123).** The snippet and the
hub's `.husky/pre-commit` trap INT and TERM, stop the step running in
the background, remove the list file, print one line, and re-raise the
signal. `.husky/pre-push` gains the traps it lacked, with the review
engine run in the background. A launcher (`uvx`, `uv run`, `dotnet`, the
`xargs` call) stays in the foreground, since the trap's kill reaches
only `$!` and a backgrounded launcher's tool would ignore Ctrl-C.
- **`hub_fetch_run.py` handles SIGINT and SIGTERM (#4108).** One
`KeyboardInterrupt` path terminates and waits for the `uv` child, the
temporary tree is removed as it unwinds, one line is printed, and the
signal is re-raised at the `__main__` call site (exit 130 on Windows).
`prose_lint.py`, edited here, takes the same path. Both take SIGINT and
SIGTERM through one handler and pass over a later signal, such as the
SIGTERM `uv run` forwards after a Ctrl-C reached its process group.

## Tests

- `tests/test_prose_lint.py`: `--diff-commit` on a merge, a merge where
both parents changed one file, a move only one parent made, a move the
resolution makes of a file either parent added or moved, a plain commit,
a first commit in a SHA-1 and a SHA-256 repository, and beside `--diff`;
an escaped backtick for `sentence-length`; wrapped and escaped spans for
`dead-path`.
- `tests/test_pre_commit_hook.py`: the interrupt cases now assert the
hook died by the signal, a signal to the hook alone stops its background
step, and the pre-push hook stops its engine on either signal, to its
group or to it alone.
- `tests/test_prose_lint.py`: the gate, its first git call held open, is
signalled mid-run and asserted dead by the signal with no traceback.
- `tests/test_hub_fetch_run.py`: the helper, run under its interpreter
with the network stubbed, is signalled mid-run and asserted dead by the
signal, with its child and fetched tree gone.
- Each new interrupt case was run against the `develop` copies of the
hooks and the helper and fails there.

## Local review

`local-strict-review` passes before each push. Before the opening push,
the first raised five findings, disposed as:

- Backgrounded launchers left their tools ignoring Ctrl-C: fixed,
launchers stay in the foreground.
- `hub_fetch_run.py` duplicated first-commit handling: fixed, removed.
- Duplicated test process-group helpers: fixed, `test_hub_fetch_run.py`
imports them.
- A redundant `symbolic-ref` probe in `commit_base`: fixed, removed.
- Husky.Net's generated `_/husky.sh` re-runs the hook in an inner `sh`,
so a signal sent to the outer shell alone does not reach the snippet's
traps: declined. That outer shell is Husky.Net's generated file rather
than anything the hub carries, and Ctrl-C reaches both shells as one
process group, where both end by the signal (probed with dash).

The second raised two, both fixed: `prose_lint.py` lacked the interrupt
path its edit owed, and the hooks' comment misnamed why a launcher stays
in the foreground.

The third raised two, after the edit budget was spent:

- `introduced`, declined as ownership elsewhere: `CODESTYLE.md`
"Interrupts in Shell" names neither a POSIX `sh` script nor a launcher.
It does not say that a launcher stays in the foreground, that dash runs
no `EXIT` trap on a re-raised signal, or that `sh` has no array for
`pids`. The hooks follow the rule's own reason, that the trap's kill
reaches only `$!`, so a function, a group, a subshell, a pipeline, or
`sh -c` runs in the foreground. `CODESTYLE.md` belongs to another group
this round, so the wording gap is reported to the orchestrator rather
than edited here.
- Disproved: `.husky/pre-push` suppresses SC2317 and SC2329 for its
trap-only function while the two `sh` hooks need none. The pinned
`koalaman/shellcheck:stable` run passes all three, and refused
`pre-push` with SC2329 before the suppression.

After the opening push, the pull request's review raised a
git-before-2.29 first commit (fixed) and a wrapped span hiding a path
(declined in its thread). The passes over those fixes raised:

- A merge read the branch's own lines in a file both parents changed:
fixed, the both-parents scope above.
- A move only the branch made leaked into that scope, and the scope line
still named `MERGE_HEAD`: fixed, the move filter, and the line now names
`MERGE_HEAD and HEAD`.
- The move filter had no test, merge test names still said one parent, a
repository git cannot read refused naming the empty tree, and a SIGTERM
`uv run` forwards during a SIGINT could escape as a traceback or decide
the reported signal: each fixed, the last by one handler for both
signals, with tests sending the pair.
- A resolution moving a file only one parent added was read by neither
diff's move list: fixed, each diff's moves count where the other parent
lacks the path.
- Where both diffs recorded a move to one path, one source was kept and
the other's backlog read as new, and the mirror move had no test: fixed,
every recorded source is subtracted, with tests.
- The other-parent probe started one git process per move a merge
brought in, staged or not: fixed, only a path left in the merge's
both-parents scope is probed, which a clean merged-in rename is not.
- `introduced`, declined as ownership elsewhere: `CODESTYLE.md`
"Interrupts in Python" describes Python's own SIGINT handling and a
SIGTERM handler alone, while both scripts route SIGINT through their
handler to close the forwarded-SIGTERM race. Like the shell gap above,
it is reported to the orchestrator, since `CODESTYLE.md` belongs to
another group this round.

Closes on promotion: #4131, #4135, #4123, #4108

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ct Four Carried Rules (#4149)

## Summary

- **The shell interrupt rule describes the hooks it governs (a
disagreement merged #4145 left).** `CODESTYLE.md` "Interrupts in Shell"
told a reviewer to background every long-running external command. The
hub hooks #4145 changed keep a launcher (`uvx`, `uv run`, `dotnet`,
`xargs`) in the foreground instead. A background child starts with
SIGINT ignored, and the tool a launcher starts inherits that, so a
backgrounded launcher's tool survives Ctrl-C. The rule now says a
launcher may run in the foreground, and must where it does not stop its
tool on SIGTERM. `hub_fetch_run.py`, which terminates the gate it
starts, stays a legitimate background child. A new bullet covers a
deliberately POSIX `#!/bin/sh` script such as a git hook: dash runs no
`EXIT` trap when the re-sent signal ends it, so the `INT` and `TERM`
traps release paths and locks themselves. The child's process ID is a
scalar such as `child`, since sh has no arrays, and every variable a
trap reads is set before the traps are. "Interrupts" now applies to
every shell script rather than every Bash one. "Interrupts in Python" is
untouched, since #4146 holds it.
- **The sh exception gives a reason that holds (#4140).** Measured
rather than taken from the issue: dash accepts `set -E`, reading it as
its emacs editing option, and rejects `trap ... ERR`. What aborts a
bash-form `#!/bin/sh` hook is `set -o pipefail` on a dash that predates
the option, which Ubuntu 24.04's does (`Illegal option -o pipefail`,
measured in an `ubuntu:24.04` container). The `GOVERNANCE.md` "Shells"
bullet and `shell-codestyle` both stated a reason that does not hold on
that dash, and both now state this one. No version number appears, since
the spec validator refuses a version literal in an instruction document.
- **The review engines are named as the hub's (#3916).** Both mentions
in "Verification Discipline" read "the hub-hosted `scripts/...`", which
"Hub-Hosted Tooling" requires of a tool a carried rule names.
- **The secret naming examples carry the owner field (#4114).** The
"Repo-Scoped Secrets" examples use a constructed short name, `widget`,
so none contradicts the `<shortName>_<purpose>` bullet beside them.
`widget` rather than the issue's `example` keeps a name such as
`widget_docker.env.example` from reading as two `.example` suffixes.
- **The Python profiles say a mypy section names its target (#3787).**
One paragraph in `python-codestyle`'s `references/profiles.md` covers
both profiles, since CI's type-check step passes mypy no path in either.
It covers a `mypy.ini`, `.mypy.ini`, or `setup.cfg` `[mypy]` section as
well as `[tool.mypy]`. The skill's description does not change.

Generated includes and the skill trees are rebuilt with
`scripts/build_dist.py`, and `--check` is clean.

Closes on promotion: #4140, #3916, #4114, #3787

## Local review dispositions

- Declined: the local pass asked to drop the `widget_` prefix from the
repo-scoped `.secrets/` examples, since only host `~/.secrets/` files
carry the owner rule. #4114 asks for every example in the section to
carry the owner field. A prefixed name is still lowercase fields joined
by `_`, so it satisfies `CODESTYLE.md` "Script and Utility File Names"
as well.

- Declined (`introduced`, design): the third local pass read the new
`references/profiles.md` paragraph as restating `python-codestyle`
`SKILL.md`'s build clean-compile note that mypy with no target exits
with an error. The `SKILL.md` sentence explains what the build-profile
clean-compile does in that layout. The new paragraph states the
requirement for both profiles and names the keys. Neither contradicts
the other, and the procedure keeps its own context rather than a bare
pointer.

- Disproved (`introduced`): the fourth local pass suspected `uv` resets
SIGINT before starting its tool, which would make "the tool a launcher
starts inherits that" false for `uvx` and `uv run`. Measured under a
backgrounded `dash -c`: a Python tool started by `uv run`, `uvx`, and
`xargs` each reports SIGINT as ignored (`1`, `SIG_IGN`).

## Pre-existing findings, not filed

- `docs/reusable-workflows.md` "Adopting the Gates", the mypy target
bullet: it names only a `[tool.mypy]` section, while the type-check step
also runs mypy with no path for a directory configured by `mypy.ini`,
`.mypy.ini`, or `setup.cfg` `[mypy]`. Class: incomplete instruction.
Outside this branch's file set.

## Verification

- `scripts/build_dist.py --check`: current.
- Prose gate, diff-scoped against `develop`: clean.
- `tests/test_spec_validate.py`, `tests/test_audit.py`,
`tests/test_audit_helpers.py`, `tests/test_build_dist.py`: pass.
`spec/validate.py`: OK.
- Shell claims measured with constructed probes on dash and bash, and on
`ubuntu:24.04`'s dash in a container.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and Name the Retire-Alone Read (#4148)

## Summary

- **The hub scripts' cleanliness reads name their untracked mode
(#4142).** A bare `git status --porcelain` obeys
`status.showUntrackedFiles`, so with that setting at `no` an untracked
file reads as a clean tree. `menu.sh`'s `hub_root_clean_on_default_ref`
and `ensure_hub_root` freshness check, `menu.ps1`'s
`Test-HubCleanOnDefaultRef` and `Confirm-HubRoot` check, `carry.py`'s
`verify_hub`, and `skills_install.py`'s `source_ref` `dirty` stamp now
pass `--untracked-files=all`. Each menu holds its read in one helper,
`hub_root_clean` and `Test-HubClean`, that both of its checks call.
`verify_hub` reads through `carry.py`'s own `git_status_paths`, which
already did. `tests/test_carry.py` covers `verify_hub` against a real
hub with the setting at `no`. A shared `HubRefFreshnessCases` case in
`tests/test_bootstrap.py` covers both menus' freshness check the same
way. `tests/test_skills_install.py` covers the stamp's status call.
- **#4142's remaining site moved to #4147.** The
`host-setup/agent-safety/claude/install.py` `dirty` stamp read is the
one #4142 site this pull request leaves. It moved to #4147, since this
away-mode run held `host-setup/agent-safety/`.
- **The retire-alone tests name their read (#4143, #4144).**
`repo-worktree` "Creating a Worktree"'s step that retires a previous
session's worktree, and `backlog-burndown` "Cleanup Is the
Orchestrator's" retire-alone test, now name `repo-worktree` "Listing and
Cleanup"'s hand-removal read. That read judges a tree clean whatever the
status setting is. The generated skill trees are regenerated. #4143 and
#4144 are per-unit trackers, and each stays open for the sibling finding
listed below.

## Verification

- `uv run --no-project --with-requirements requirements.txt python -m
unittest test_carry test_skills_install` from `tests/`: 178 tests, OK (3
skipped). The new `verify_hub` test fails with the old bare read
restored.
- `python3 -m unittest test_bootstrap` from `tests/`: 192 tests, OK (5
skipped), the new menu case running under both bash and pwsh. It fails
on both menus with the old bare reads restored.
- `scripts/build_dist.py --check` clean.
- `scripts/docker_lint.py` with shellcheck, shfmt, PSScriptAnalyzer,
markdownlint, and editorconfig-checker: success.

## Pre-existing findings, filed

- `.agents/skills/repo-worktree/SKILL.md` "Creating a Worktree", the
base-clone return gated on a clean tree with no read named (correctness,
class sweep of #4143). Added to the unit's tracker, #4143.
- `.agents/skills/backlog-burndown/SKILL.md` "Cleanup Is the
Orchestrator's", the standalone-clone retire-alone test and the merged
group's clone confirmation, each judging the clone clean with no read
named (correctness, class sweep of #4144). Added to the unit's tracker,
#4144.

Both were raised by the third local pass, after the two-round edit
budget was spent, so they take outcome 4's per-unit tracker bullet.
Because each tracker still holds an open finding, this pull request
closes neither.

Closes on promotion: #4142

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Bug Fixes**
* Hub and installation checks now detect untracked files at any depth,
including when Git is configured to hide untracked files. Dirty
checkouts continue to be rejected or refreshed as appropriate.
* Worktree retirement guidance clarifies the clean-tree check and
hand-removal procedure.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…and Add a Short-Form AUDIT.md Snippet (#4153)

## Summary

- **Compose sets resolve against their first file (#3840).**
`spec/audit.py` reads each devcontainer's `dockerComposeFile` list and
resolves every listed compose file's paths against the first listed
file's directory, the compose project directory. A compose file two such
sets list resolves against each set's base. A compose file no
devcontainer lists keeps its own directory.
- **Secrets folder names match in any letter case (#3842).** The
`.gitignore` reader, the reference patterns, and the tree loop match
`secrets` case-insensitively. A `.gitignore` character class such as
`[Ss]ecrets/` counts, and one able to name `.secrets` does not. A
`.secrets/` spelled in another case is reported as drift from
`.secrets/`, and its files are still judged.
- **AUDIT.md section 7 and `checks.json` describe the widened check
(#3837).** The runtime-secrets bullet and the `secrets.stems`
description now say what the check reads and reports, and what stays
judged by hand.
- **No coordination references under `spec/` (#3844).** The two
`ptr727/ProjectTemplate#` references in `spec/divergences.json` and
`spec/fidelity-model.md` are dropped, and the sentences they supported
stay.
- **A canonical short-form downstream `AUDIT.md` (#4118).**
`catalog/snippets/configs/AUDIT.md` carries the short form, including
the rule that `configure.sh` and `spec/audit.py` run from a hub
`develop` checkout while the repository's registry entry has not reached
`main`. `spec/files.json` names it as the `AUDIT.md` entry's `reference`
with its placeholders, so `spec/validate.py` checks each placeholder
occurs in it. Each finding the snippet's self-audit surfaces names the
hub commit its run read. `STANDUP.md` section 1A and the
`standup-a-repo` skill point at it. The optional advisory the issue
mentions, flagging a whole copy of the hub's own `AUDIT.md`, is not part
of this change.

`spec/validate.py`'s carried-link check now reads an entry's `reference`
text, resolving its links where the carried file lands, so the snippet's
links are checked rather than the hub's own `AUDIT.md`, and its errors
name the file read. The `AUDIT.md` exemption from the
template-repository link check is dropped, since the hub's own
`AUDIT.md` is no longer the text read.

The `reference` also moves what dates a downstream `AUDIT.md` for the
intent staleness advisory, from `docs/repo-config.md` to the snippet,
per `spec/fidelity-model.md`.

## Tests

- A new `CarriedRelativeLinkCase` test reads a reference entry's links,
and fails against `develop`'s `spec/validate.py`.
- A new `SecretStemCase` test resolves a compose file two devcontainer
sets list against each set's base, and a `CarriedRelativeLinkCase` test
rejects a template-repository link in a referenced `AUDIT.md` snippet.
Each fails without its fix.
- A `SecretStemCase` case keeps a `.gitignore` guard line silent where a
class in the same file names `.secrets`.
- New `SecretStemCase` cases cover a capitalized folder from a
`.gitignore` line and a compose path, a character class, a floating line
a differently cased code directory must not silence, a dotted folder in
another case, and a devcontainer compose set. Each new case fails
against `develop`'s `spec/audit.py`.
- `tests/test_audit*.py` and `tests/test_spec_validate.py` pass,
`spec/validate.py` passes, ruff and mypy are clean, and
`scripts/build_dist.py --check` reports the generated skill trees
current.
- The prose gate on the diff against `develop`, markdownlint, and cspell
report no issues.

Closes on promotion: #3837, #3840, #3842, #3844, #4118

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…d the Prose Gate (#4152)

## Summary

A conflicted `develop -> main` promotion goes through a branch cut from
`main` that merges `develop`, per the `branching-and-release-model`
reference. Before this change, two hub tools didn't recognize that
shape. This change makes both read it from content and ancestry, never
from the branch name.

- **`pr_review.py cleanup`** now takes a merged pull request into the
default branch, as well as one into `develop`. For the default branch,
GitHub's comparisons must show the merged head in the default branch's
history and the head's second parent in `develop`'s. Any other pull
request into the default branch is still refused. The REST reads cleanup
makes move into the new `scripts/pr_cleanup_rest.py`, which keeps
`scripts/pr_cleanup.py` under its line ceiling.
- **The prose gate's `comment-added` stand-down** in `validate-task.yml`
still covers a `develop`-headed promotion on the release model. It now
also covers a same-repository head into the default branch where every
line in each path the head changes appears in that path at the head's
merge base with `develop` or with the base branch.
- **Docs and skills.** `scripts/README.md`, the `pr_cleanup.py`
docstring, and `docs/reusable-workflows.md` describe the new shape.
`merge-and-release` step 8 gains a bullet that runs `cleanup` on a
conflicted promotion's pull request. `pr-review-conduct` and
`repo-worktree` "Listing and Cleanup" route that branch through
`cleanup` instead of leaving it to the sweep or a maintainer delete.
- **Also fixed in files this change touches:**
- `scripts/README.md` "`pr_cleanup.py`" said `git worktree remove`
refuses untracked changes. That is false under
`status.showUntrackedFiles=no`, and #4138 left the disagreement. The
contract now lists the refusals cleanup decides itself, a linked
worktree with tracked or untracked changes among them.
- `docs/reusable-workflows.md` "Adopting the Gates" named only
`[tool.mypy]` for the mypy target. The step reads `mypy.ini`,
`.mypy.ini`, and `setup.cfg` `[mypy]` too, which #4149 now states in
`CODESTYLE.md`.

Closes on promotion: #4062

## Local-pass findings, fixed

The first local pass raised two `introduced` findings. The second pass,
over the whole branch, raised none.

- `tests/test_pr_review.py`'s writes guard and
`tests/test_pr_review_size.py`'s `CEILINGS` now name
`scripts/pr_cleanup_rest.py`. The moved REST helper stays held to GET,
and the new module has a ceiling of 100 lines.
- `drive-pr` step 4 now names the conflicted promotion merged into
`main` that `cleanup` takes.

## Verification

- `tests/test_pr_cleanup.py`, `tests/test_prose_lint.py`,
`tests/test_pr_review.py`, `tests/test_workflow_steps.py`, and
`tests/test_pr_review_size.py` pass under `uv run --no-project
--with-requirements tests/requirements.txt`.
- `scripts/build_dist.py --check` reports the generated skill trees
current.
- `scripts/prose_lint.py --diff <merge-base>` reports nothing.
- A recorded `local-strict-review` pass covers the pushed head.

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Improvements**
* Cleanup guidance and checks now support eligible merged promotion pull
requests, including conflicted promotions into the default branch.
* Automated review-comment checks better recognize conflicted promotions
and avoid flagging them as ordinary changes when their content comes
from the relevant branches.
* Cleanup checks provide clearer refusals when a pull request is
ineligible or its promotion history cannot be verified.

* **Documentation**
* Clarified promotion cleanup steps and where to configure mypy target
settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4154)

## Summary

A handoff parked on a maintainer decision carries the `blocked` label.
The label came off once a session wrote the next round and left no
`decision` issue *it filed* on that work unanswered. A decision an
earlier session filed on the same work could still be open at that
point. The label then came off, and a running `backlog-burndown`
away-mode loop could take a lane it could only park again. That is the
outcome the rule's own closing sentence says it prevents.

The finding holds. The "it filed" scope came from #4013 item 2, which
named only a new decision the closing session raised. It was not a
deliberate exclusion, and "Communicating with the User" already presents
the whole labeled queue whichever session filed it.

- `GOVERNANCE.md` "Durable Knowledge and Self-Improvement": the label
comes off only once a session writes the next round and leaves no
`decision` issue on that work unanswered, whichever session filed it.
- `session-handoff` "The Attended Session" step 6: the same condition,
stated the same way.
- The `agent-conduct` include region and the generated `.github/skills/`
and `.claude-plugin/` trees are regenerated with
`scripts/build_dist.py`.

No other skill restates when the label comes off. `backlog-burndown` and
`decision-triage` only read it.

## Verification

- `uv run --no-project --script scripts/build_dist.py --check` reports
the generated distributions current.
- The pre-commit gates (ruff, mypy, prose lint, eol) pass on the commit.
- A recorded `local-strict-review` pass covers the head, attested below.

Closes on promotion: #4119

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…epo-worktree and backlog-burndown (#4156)

## Summary

Points the remaining unnamed clean-tree judgments in `repo-worktree` and
`backlog-burndown` at `repo-worktree` "Listing and Cleanup"'s
hand-removal read, `git -C <exact-path> status --porcelain --ignored
--untracked-files=normal`, where any line not starting `!!` means the
tree is not clean. Under `status.showUntrackedFiles=no`, a bare `git
status --porcelain` prints nothing for untracked files, so a tree
holding untracked work would otherwise read clean.

- **`repo-worktree` "Creating a Worktree".** The base-clone return
before attaching a continuation's branch now names the read (#4143,
second finding).
- **`backlog-burndown` "Cleanup Is the Orchestrator's".** The merged
group's standalone-clone confirmation and the standalone-clone "The
clone must be clean" test now name the read (#4144, second finding).
- **Siblings in the files this change touches**, found by a class sweep
and by the local review pass:
- `repo-worktree` "Listing and Cleanup"'s "Return the base clone to
current `develop`" step said "confirm the base clone is clean" with no
read. It now names the hand-removal read in the bullet above it.
- `backlog-burndown` "Bounding the Wait on a Worker" sorted a dead
worker's tree into clean and dirty with no read. A tree holding only
untracked work would then be sent to the abandoned-group discard. The
sort now names the read, which the dirty-tree park in "Parking Instead
of Asking" reuses.
- `repo-worktree` "Listing and Cleanup"'s "A worktree that refuses
removal is dirty" bullet said to look at what is uncommitted with no
read. It now names the read.
- `backlog-burndown` "Raising a Blocked Question" told the maintainer
whether a stopped worktree holds uncommitted work with no read. It now
names the read.
- **Already fixed by #4148, confirmed on `develop` rather than redone.**
#4143's first finding (the continuation's retire step, "Remove it when
it is clean") and #4144's first finding ("A clean tree is the whole
test") both already name the read.

The generated trees under `.github/skills/` and `.claude-plugin/` are
regenerated with `scripts/build_dist.py`, and `--check` is clean.

Closes on promotion: #4143, #4144

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#4155)

## Summary

- **Registry:** add `SourceReader` to the `HomeAutomation-Config`
entry's `pythonDirectories`. That repository's validate callers in
`test-pull-request.yml` and `publish-release.yml` pass it on both `main`
and `develop`, re-read live for this change, while the registry listed
every directory but this one.
- **`upstream-contribution-workflow`:** the contribution now ends once
the pull request of every clean branch the dirty branch feeds is merged
or closed, with no slice left to cut. Read literally, the old singular
wording deleted a shared dirty branch, and closed its iteration pull
request, at the first upstream merge. That left any other upstream pull
request it fed with no branch to take reviewer fixes through. Generated
trees regenerated with `scripts/build_dist.py`.
- **Unfiled defect this run found:** three `why` texts in
`spec/host-tools.json` (the `docker` and `ripgrep` entries) cited bare
hub issue numbers. Carried `spec/` files reference no coordination
machinery, and a bare number resolves to an unrelated issue in any other
repository. The references are dropped, and each sentence keeps the
evidence it cited in its own words. No issue was filed for it.

## Verification

- `spec/validate.py`, `scripts/build_dist.py --check`, and the prose
gate against `develop` pass.
- `tests/test_host_gate.py`, `tests/test_spec_validate.py`,
`tests/test_audit_helpers.py`, and `tests/test_build_dist.py` pass.

## Local Review Findings Fixed After the Opening Push

- `.agents/skills/upstream-contribution-workflow/SKILL.md`, the new end
condition (class `introduced`): a clean branch superseded under step 5,
or a slice the maintainer withdraws, has a pull request upstream neither
merged nor declined, so the condition never holds. Fixed in the second
push, which counts any closed pull request as ended. A further local
pass over that push raised nothing.

## Local Review Findings Declined

- `registry/repos.json`, the `HomeAutomation-Config` drift note counting
the Python directories its CI gated on 2026-10-27 (class
`pre-existing`): declined as not false. `SourceReader` first appears in
that repository's `test-pull-request.yml` list on 2026-10-30, so the
count was right on the date the note records.

Closes on promotion: #4050, #4016

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Documentation**
* Updated contribution guidance to account for all related branches
before closing the iteration PR and deleting branches.
  * Clarified notes about observed Docker and ripgrep behavior.
* **Chores**
* Updated the HomeAutomation-Config repository’s Python directory list.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…cript Dependency Exactly (#4157)

## Summary

- **#4151:** `scripts/backlog_metrics.py` gains `--format json` and
drops `--format html`, per the maintainer's 2026-11-13 comment on the
issue. The JSON document carries the same figures as the Markdown
report. Each counted repository, issue, pull request, and workflow run
carries its `html_url`, and the headline totals sit in a top-level
`totals` object. `scripts/README.md` documents the shape, and
`tests/test_backlog_metrics.py` pins it.
- The Markdown output now links each repository to its GitHub page, in
all three tables. It also links each issue and pull request count to the
GitHub search listing what it counts, bounded to the report's window.
The workflow run counts stay plain, since they come from `gh run list`
rather than from a search.
- `backlog_metrics.py` takes `CODESTYLE.md` "Interrupts" with this edit.
On SIGINT or SIGTERM it stops the `gh` call in flight, prints one line,
and ends by the signal. It passes over a later signal, as the prose gate
does, so the SIGTERM `uv run` forwards after a Ctrl-C cannot cut the
unwind short. A test signals it mid-call and checks that the `gh` child
is gone.
- **#4043:** every PEP 723 inline dependency in the tree is now pinned
to one exact version. That covers the prose gate, `agent_policy.py`,
`central_config.py`, and ten more scripts under `scripts/` and `spec/`.
Each markdown-it-py pin brings `mdurl==0.1.2` with it, the one package
markdown-it-py requires. `tests/requirements.txt` pins the same
versions. The versions are the ones the open bounds resolve today, so no
script's behavior changes.
- `PinTests` now finds every tracked script's inline block rather than
naming two files. It checks that each dependency, and each
`tests/requirements.txt` line, is an exact pin, and that a package takes
one version everywhere. It also checks that each package a pinned one
requires is pinned beside it.

## Downstream

ptr727/HomeAutomation-Config#1800's `scripts/fleet-report.sh` runs
`--format html`, which this change refuses with exit `2`. That
repository switches to `--format json` when it renders its own report,
per the design that issue records. This pull request writes nothing
there.

## Local review

- Declined, outcome 2: moving the interrupt helper onto one shared
primitive. Its only other copy is
`.github/actions/prose-gate/prose_lint.py`, which lives beside its
composite action so a reusable workflow reaches it at the pinned hub
commit without `scripts/`. So it cannot import a module from `scripts/`.
Within `scripts/`, `backlog_metrics.py` holds the only copy, and
`scripts/docker_lint.py` uses a different, SIGTERM-to-exit handler. A
primitive for the next `scripts/` caller is that change's to add.
- Declined, outcome 2: bounding the open-now link by date. GitHub search
has no qualifier for an issue open on a past date, so the README
documents that link as listing what is open when it is opened. The JSON
carries the counted open issues under `items.issues_open`.
- Fixed: dated searches bounded to the window, and the default trigger
carried as `policy_from_default` beside a bare `policy_trigger`. The
Markdown totals, review tables, and billing lines are now printed from
the figures the JSON carries. The JSON test pins every field name. The
signal test reuses `tests/test_pre_commit_hook.py`'s harness. The README
documents the `policy_changes` markers.
- Declined, outcome 2, under `GOVERNANCE.md` "Trust Boundaries and
Hardening Effort": the signal test's skip covering its SIGTERM subtest
too, where the suite inherits an ignored SIGINT. That is a suite started
in the background from a non-interactive shell, which neither CI nor the
documented run command does, and the skip prints. The same class in
`tests/test_prose_lint.py`'s signal test is pre-existing, outside this
change's files, and declined for the same reason: it fails loudly rather
than silently, and only in that launch.

## Verification

- `uv run --no-project --with-requirements tests/requirements.txt python
-m unittest discover -s tests`
- `ruff format --check`, `ruff check`, and `mypy` over the tree, and the
prose gate over the branch diff.
- `PinTests` fails when one script reverts to `>=4`, one pins another
markdown-it-py version, or one drops `mdurl`. The interrupt test fails
when the SIGTERM handler is removed.

Closes on promotion: #4151, #4043

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…stry Fails Loudly (#4163)

State the trusted inputs of `scripts/agent_policy.py` in its module
docstring and in `OPERATIONS.md` "Configuration Layout", so reviewers
stop re-deriving the boundary.

- Names the registry, the hub default, `spec/files.json`, the
`repo-config/` payloads, the `--checkout` tree and its `origin`, and the
files `check` reads at `--ref` (pinned to the default branch by
`configure.sh`) as maintainer-controlled.
- States that an unparsable trusted file fails loudly with a nonzero
exit, and lists the four hardening cases the issue names as out of
scope.
- Adds `test_a_malformed_registry_fails_loudly`, since no CLI test
covered that exit.
- `docs/repo-config.md` needed no change.

Declined finding (local pass, outcome 2): the list omits a
parent-directory `.editorconfig` that `write` consults for a new file's
line endings. It is the same maintainer-controlled filesystem class as
the checkout, and listing it adds detail the issue does not ask for
(GOVERNANCE.md "Trust Boundaries and Hardening Effort").

Closes on promotion: #3894

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

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
)

## Summary

`read_env_value` sources an env file in a fresh child bash, where
`require_secret` was not defined. An env file that includes a second
file behind `declare -F require_secret` therefore skipped the guard, and
the include was sourced with no mode or line-ending check.

- **The child can check an include.** `read_env_value` ships
`require_secret` and every helper it calls into the child as `declare
-f` text. A guarded include now finds `require_secret` defined, and a
loose mode or CRLF endings refuse the read.
- **Fails closed.** A helper missing from the calling shell fails the
read with an error before the env file runs. An example is an elevated
shell that was shipped too few functions.
- **The env file's own check stays with the caller.** A caller still
checks the env file before the read, with `require_secret` or
`secret_verdict` with `shared`, as `GOVERNANCE.md` states. So
`GOVERNANCE.md` is unchanged, and its host-secrets wording still matches
the reader.
- **Contract comment.** The comment on `read_env_value` says that a
guarded include is checked and an unguarded one is not. The library
header names the functions `read_env_value` needs when it is shipped by
`declare -f`.

## Tests

`tests/test_secrets_sh.py` `ReadEnvValue` gains four cases. Three of
them fail against the library before this change.

- A guarded include with mode 666 is refused, naming the include.
- A guarded include with CRLF endings is refused, naming the include.
- A missing helper fails the read before the env file runs.
- A guarded include that passes is read. This case passes before and
after the change.

`shellcheck` and `shfmt -d` are clean on `lib/sh/secrets.sh`, and `ruff
check` and `ruff format --check` are clean on the test file.

## Local review

- **Fixed:** checking the env file inside the child refused a file a
caller checks with `shared`. The child no longer checks it.
- **Fixed:** the comment said the caller owes the include guard, when
the env file's author writes it.
- **Declined:** shipping `read_env_value` alone into an elevated shell
now fails the read. The failure is loud and fails closed, which is the
point of the change, and the library header names the functions it
needs. Neither known downstream caller ships it that way.

The change adds code comments, so it carries the `comments` label.

Closes on promotion: #3912

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
… and backlog-burndown (#4166)

Closes review gaps in four fleet skills, and gives the carried-table
bound its reason in `GOVERNANCE.md` and the Copilot instructions.

## Changes

- **`repo-worktree` "Preparing Git Hooks" (#3620 item 5).** The
pre-commit check is now one complete command: `test -x` and a `grep` for
the framework's own header, on the shared hook path. A non-framework
executable there no longer counts as the gate. It falls to the unchanged
next bullet: "A missing or wrong shared hook is reported to the
maintainer, not repaired from the worktree."
- **`drive-pr` (#3697).** A feature PR a peer merged mid-drive still
owes an outcome for each open finding. That covers unresolved threads
and every finding step 4 names, whether or not `merge` would refuse on
it, since `merge` never runs there. Step 3's `state=MERGED` path and
step 4's `next=cleanup` route both give those outcomes before `cleanup`
runs. A fix lands as a fresh feature -> develop pass.
- **`GOVERNANCE.md` "PR Review Etiquette", `pr-review-conduct` Merge
Gate item 2, and `.github/copilot-instructions.md` (#3620 item 3).**
Each gets two sentences on why the carried-table bound is the file set
rather than file recency, per the maintainer's answer on #3746.
- **`backlog-burndown` "The Promotion Boundary" step 5 (#3620 item 4).**
The displaced pull request waits until the next round dispatches, and
that round dispatches its fresh worker. That matches step 6 in an
attended run, and the away-mode promotion path, which has no freeze.
- Generated copies under `.github/skills/` and
`.claude-plugin/fleet-skills/` were regenerated with
`scripts/build_dist.py`.

## Item Dispositions

- **#3620 item 1** (`session-handoff` resume filter): already fixed. It
was consolidated into #4013 item 1 and landed in #4034. A comment now
counts as a round only by its whole archived shape.
- **#3620 item 2** (`unattended-handoff` picker): stale. That skill was
retired in #3735 and no longer exists.
- **#3620 item 3**: declined, as the maintainer decided on #3746. The
reason sentences it asked for are added above.
- **#3620 item 4**: it still stood on develop, since step 5's "then
dispatches" read as a dispatch during the freeze. Fixed above.
- **#3620 item 5**: it still stood on develop, since `test -x` accepted
any executable. Fixed above.
- **#3766**: already resolved on develop. `repo-worktree` "The Base
Branch" names "Creating a Worktree"'s return of the base clone to its
own working branch as the second exception. It applies where a
continuation finds the task branch checked out there. That landed in
#1685, so no edit was needed.
- **#3869**: parked behind decision #4167 and left out of this pull
request's closing line. A draft of #3869's own proposal was taken back
out of this branch. In it, the agent installs the hook in the base clone
on the maintainer's yes. Each local review round found a new cost in
that draft: chaining a foreign hook, a Husky.Net setting breaking other
worktrees, a hook failing on branches without the config, and a
dispatched worker either stalling its round or never reaching the
maintainer. The decision issue lays out the options.

## Verification

- `scripts/build_dist.py --check` reports the generated distributions
current. `spec/validate.py` passes.
- The prose gate and `repo_gate.py --check eol` are clean on every
commit.
- The hook check was run against a constructed repository. With no hook
it exits 1, with a foreign executable 1, and with a framework-header
hook 0. The header line was read from the pre-commit framework's own
hook template on this host.

Closes on promotion: #3620, #3697, #3766

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…, and Read the Origin Owner Through origin_slug (#4168)

## Summary

- **Unfiled defect this run found.** `pr_review.py wait` could run past
`--timeout` and then end with nothing printed.
- `backoff()` checked the bound only before each sleep, and the
schedule's last delay is 120 seconds. So a wait could overrun its bound
by up to that delay plus the reads after it.
- An outer bound set at or below `--timeout` then killed it with
SIGTERM. The script had no SIGTERM handler, so nothing printed.
- The fix cuts each clock-bounded sleep to the time left, so no poll
sleeps past `--timeout`. The reads that follow the bound still run, and
then the digest and status line print.
  - The entry point now follows `CODESTYLE.md` "Interrupts in Python":
- A SIGTERM handler raises `KeyboardInterrupt` carrying the signal
number.
- The entry point prints `pr_review.py: interrupted by <SIGNAL>.`,
flushes, restores the default handler, and re-raises the signal. On
Windows it exits 130 instead.
    - SIGINT takes the same path.
- The combined SIGINT/SIGTERM handler is not adopted, because that is
the open decision #4146.
- **#3890.** `origin_owner()` now gets the owner from
`gh_graphql.origin_slug()` instead of keeping its own copy of the
origin-URL regex.
- **Line ceiling.** `pr_review.py` stays under its 2200-line ceiling.
Three call sites are reshaped onto fewer lines to make room, and their
behavior does not change.
- **`pr_merge.py` and `pr_cleanup.py`.** Both call `backoff()` with a
finite schedule and no clock, which this change does not affect. Their
suites pass unchanged.

## Tests

- `test_the_backoff_sleeps_the_schedule_until_the_timeout` now expects
the seventh sleep to be cut to the 110 seconds left.
-
`test_wait_sleeps_no_further_than_its_timeout_and_then_prints_the_digest`
runs `wait --timeout 100` on a fake clock. It asserts that the sleeps
stop at 100 seconds rather than 110, and that the digest and
`status=PENDING` print.
- `TestWaitInterrupt` runs the script with its interpreter directly,
from a copy of the scripts tree whose origin names a constructed owner.
A stand-in `gh` on PATH holds the first read open. The case then sends
SIGTERM and asserts three things:
  - The process died by SIGTERM.
  - It printed exactly the one interrupt line.
  - Nothing in its process group survived.

It uses the process-group helpers in `tests/test_pre_commit_hook.py`,
and it is skipped on Windows.
- Each new or changed case fails against `develop`'s `pr_review.py` and
passes here. The full suite passes locally: 4162 tests, 11 skipped.

## Open findings, local pass edit budget spent

- `introduced`, design: `scripts/pr_review.py` gets to 2198 lines, under
its 2200-line ceiling, by reshaping three multi-line calls onto fewer
lines rather than by splitting the file along a boundary. The three are
`origin_owner`, `_gh_run`, and the poll in `run_wait`. One line of
headroom is left.
  - Outcome: declined, with this evidence.
    - The reshaped calls behave the same, and the ceiling is not raised.
- Splitting the file along a boundary means adding a module. Every new
module needs an entry in `scripts/README.md`, and a peer pull request
holds that file this round.
- So the next change that needs room in `pr_review.py` does the split,
as `tests/test_pr_review_size.py` asks. The burndown run has been told
about it.

## Declined findings

- **Local pass, `scripts/pr_review.py` `__main__`:** a suggestion to
share the re-raise ending with `scripts/backlog_metrics.py` `reraise`.
Declined for three reasons:
- It needs edits to `backlog_metrics.py`, which is outside this change.
- It needs a shared home in `gh_graphql.py`, a library documented as the
GraphQL transport.
- `backlog_metrics.py` uses the combined-handler variant that #4146 has
yet to decide.

The interrupt line's wording is matched to `backlog_metrics.py`'s,
period included.

Closes on promotion: #3890

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

Makes the 1Password CLI (`op`) an optional managed host tool on both
installers.

A repository that reads its secrets from a 1Password vault needs `op` on
its hosts. Its own `host-tools.json` cannot declare the Linux source,
since an overlay's install metadata names only a package, never an apt
repository or a signing key. So the hub manages it.

- `spec/host-tools.json` gains an `op` entry: `required: false`, no
floor, probe `op --version`, with `source` and `remedy` for Linux,
macOS, and Windows. Its pattern reads a line holding only the version,
which is what the probe prints, and tolerates a pre-release suffix,
since an unreadable version fails the gate even for an optional tool.
- `host-setup/linux/install-tools.sh` gains `op_*` functions modeled on
`gh_install`. The keyring at
`downloads.1password.com/linux/keys/1password.asc` is proved against the
repository's own `InRelease` rather than pinned. The sources file is
deb822 `1password.sources` naming a keyring under `/etc/apt/keyrings`.
The vendor's one-line `1password.list` and its `/usr/share/keyrings`
keyring are removed once the new source is written, so apt never sees
two `Signed-By` values for one repository and a key that fails its proof
leaves the vendor's source working. A new `register_apt_source` helper
holds that order for `gh`, `node`, `docker`, and `op`, so `gh` and
`docker` now remove their own predecessors after the replacement too.
The repository carries `amd64`, `arm64`, and `armhf`, and any other
architecture is a named skip.
- A manual download at `/usr/local/bin/op` shadows the package's
`/usr/bin/op`. It is removed after a prompt, and only beside an
installed package, so a failed fetch or install leaves the working copy.
An unowned `op` anywhere else can be a wrapper or a version manager's
shim, so it is reported and left in place.
- Ripgrep, PowerShell, `op`, and `tool_unshadow`'s shadow loop share new
`path_is_unowned`, `unowned_command_path`, and `remove_unowned_command`
helpers. Ripgrep and the shadow loop previously read a failed
`dpkg-query` as "unowned" and offered to remove the file. The shared
strict test stops the run instead, whatever errexit state it runs in,
and a failed `rm` warns once rather than looping.
- `install_keyring`, `write_sources`, and `microsoft_feed` run with
errexit off under `apply_tool`, and the step after each removes a
working source or copy. A failed write in any of them now ends the run,
found by Copilot on the first head.
- The one hard-coded optional tool on Linux becomes an `OPTIONAL_TOOLS`
list holding `powershell` and `op`.
- `host-setup/windows/install-tools.ps1` gains an
`AgileBits.1Password.CLI` record and a tool-level `$OPTIONAL_TOOLS` list
with the Linux semantics. An optional tool is left out of the default
selection unless `-Optional` is given or the tool is named, and `-List`
always shows it, marked `(optional tool)`.
- `unmanaged` is defined by the absent sources file rather than by a
distro install, which was false for `op`, in the README, a comment, and
the apply log line.
- The usage examples on both platforms no longer say a bare run reports
or upgrades every tool.
- Docs: `docs/host-setup.md`, `host-setup/linux/README.md`,
`host-setup/windows/README.md` (Differences table). No new `cspell.json`
words, since CI spell-checks only `README.md` and `HISTORY.md`.

### Measured while building it

- The `1password-cli` package ships only `/usr/bin/op`. Its install
script adds an `onepassword-cli` group and makes the binary
set-group-ID. It writes no apt source or keyring.
- The 1Password **desktop app** package writes
`/etc/apt/sources.list.d/1password.sources` on each of its installs,
after copying its key to `/usr/share/keyrings`. No package owns that key
as a file. This change uses the same sources file name on purpose. A
different name would leave apt with two sources for one repository
disagreeing on `Signed-By`, which apt refuses. On a host carrying both,
the desktop app's upgrade rewrites the file and the next run here writes
it back, and apt reads one consistent source either way.
- The vendor's debsig step is skipped because the signed apt repository
is the trust path, as for every other repository here. The package's own
debsig signature measured as SHA-512, so this change does not claim
debsig rejects it.

### Awaiting maintainer

`bootstrap.sh --dev` and the Windows dev stand-up run the installers
with `--optional` / `-Optional`, so a dev stand-up now also installs
`op`. On Linux that adds 1Password's apt source and keyring, the
`onepassword-cli` group, and a set-group-ID binary. On Windows,
`-Optional` previously selected only the extra .NET SDK lines. This
follows the requested `powershell` semantics, and whether a dev stand-up
should carry `op` is the maintainer's call.

### Local review

Every push carried a recorded local pass, three before the opening push.
Their findings are fixed above, apart from these declines:

- `remove_stale` of `/usr/share/keyrings/1password-archive-keyring.gpg`
could break the desktop app's source. Declined: that package's install
script recreates the key before writing its sources file, and no package
owns the key.

### Findings carried past an edit budget, with their outcomes

Each push carried recorded passes, and where a push's two edit rounds
were spent, its remaining findings were fixed in the next push. All are
closed on the head, whose final pass recorded no findings.

- `op_install` removed `/usr/local/bin/op` under `--install` beside an
existing package: fixed in 0545b7b.
- The report note claimed no upstream version with the vendor's
`1password.list` in place: fixed in 0545b7b.
- The "still shadowed, --upgrade removes it" line fired for `op`: fixed
in 0545b7b.
- The Windows README misdescribed the optional-tool test: fixed in
0545b7b.
- `unmanaged` wording and the desktop-app write-back claim: fixed in
3628aca, c971f77, 7f1abb4, and 9aac122.
- A failed keyring or sources write let the predecessor source go
(Copilot): fixed in 99548df, and the Microsoft feed sibling in
cebc62d.
- The floor test duplicated the remedy check: fixed in 99548df.
- Usage examples, comment scope, and the InRelease docs claim: fixed in
cebc62d and 263eeb0.
- The per-site registration order and the run-ending cases in the docs:
fixed in 2c36667.

### Pre-existing findings, not filed

- `host-setup/linux/install-tools.sh` `powershell_install`,
`pre-existing` correctness: `powershell_non_apt_paths` runs inside a
process substitution, so a failed `dpkg-query` there ends only that
subshell and the install carries on with a partial path list. Not filed
from this seat, since this pull request is its only write. It is
reported to the dispatching session to file.

## Verification

- `spec/validate.py`: OK.
- `python3 -m unittest test_install_tools test_host_gate test_bootstrap`
from `tests/`, on the tree merged with current `develop`: OK. New cases
drive the Linux functions with stubs and the Windows selection and list
through a pwsh harness over the script's own registry. Each new guard
was deleted in turn and a test failed each time.
- `scripts/docker_lint.py` (shellcheck, shfmt, markdownlint,
PSScriptAnalyzer, editorconfig-checker): clean. ruff and mypy: clean.
- Read-only on a Debian 13 host: `install-tools.sh --list` marks `op`
optional, and `--report op` reads it `unavailable` with the
repository-not-configured note.
- **Not verified natively:** a real Linux install, its idempotent
repeat, and the host gate after it, since each changes the host and
waits on the maintainer. No Windows or macOS run.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…al Homes (#4169)

Disposes of both findings the per-unit tracker for
`.github/copilot-instructions.md` "GitHub Copilot Review Runbook" lists.

Closes on promotion: #4099

## Tracked Findings

- **Point to the canonical file-table coverage rules instead of
restating them.** Fixed. The runbook's completion paragraph restated,
sentence for sentence, the file-table coverage rules that
`GOVERNANCE.md` "PR Review Etiquette" holds. That breaks the file's own
rule, "Do not duplicate rules from `AGENTS.md`, `GOVERNANCE.md`,
`CODESTYLE.md`, or `WORKFLOW.md` here." Class sweep: the local review
pass found that the rest of that paragraph also restated "PR Review
Etiquette" and the `pr-review-conduct` Merge Gate (head and full-diff
coverage, the merge-blocking cases, the local-pass reading, the default
review policy, re-running the loop, and not trusting `mergeStateStatus:
CLEAN`). So the paragraph now points to those two homes, which between
them state every rule it held. It keeps its two sentences naming partial
coverage, a refusal, and an uncovered head as merge blockers.
`test_the_runbook_names_partial_coverage_as_a_state_that_blocks_a_merge`
in `tests/test_pr_review.py` pins those on purpose, and they state the
Merge Gate's blockers rather than a `GOVERNANCE.md` rule. A
sentence-level comparison of the runbook section against `GOVERNANCE.md`
found 7 verbatim sentences before this change and none after it. The
review-effort paragraph after it is left alone, since `GOVERNANCE.md`
does not state its rules.
- **A file table should not omit a renamed file with no changed line.**
Declined, since it asks to reverse a tested design rather than correct a
misdescription. #4047 added `RENAMED` to the set a file table may leave
out on purpose, and `tests/test_pr_review.py` pins it:
`test_a_lineless_rename_is_left_out` expects `coverage=table` for a
round whose table leaves out a `RENAMED` file with 0 additions and 0
deletions, and `test_a_rename_with_a_changed_line_is_not_left_out`
expects `coverage=none` once the rename changes a line. The text
describes that behavior accurately. After the first fix, the sentence
lives in `GOVERNANCE.md` "PR Review Etiquette" and the
`pr-review-conduct` Merge Gate, both of which match the tested behavior,
so no text changes for it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

`actions/checkout` writes the job's token into the checkout's git
configuration by default, so every later step in the job, and anything
it runs, can read a credential it was never handed. Per the maintainer's
recorded answer on the issue (option 1), `GOVERNANCE.md` "Workflow YAML
Conventions" gains a "Checkout credentials" rule: every
`actions/checkout` step sets `persist-credentials: false`, and a job
whose later step runs an authenticated git command against the remote
keeps it with `persist-credentials: true` and a comment naming that
step.

The hub workflows and the release-with-smoke caller snippet (with its
documented copy in `docs/reusable-workflows.md`) are swept to match.

## Per-job audit

Set to `false`, since no later step needs git authentication:

- `build-docker-task.yml`: `docker-prepare`, `docker-build-base`,
`build-docker`. The build context is a local path.
- `build-release-task.yml`: `dotnet-publish`, `build-nuget`,
`build-pypi`, `build-release-asset`.
- `deploy-site-task.yml`: `deploy`. The deploy goes over SSH/rsync, not
git, and the generator reads history locally.
- `get-version-task.yml`: `get-version`. NBGV reads local history only.
- `publish-docker-readme-task.yml`: `get-repos`, `publish-readme`.
- `validate-task.yml`: `lint`, `unit-test`, `validate`. The prose gate's
retry `git fetch` runs only when its base does not resolve, and in this
caller the base is `HEAD^1` of the pull request merge checkout, which is
always local.
- `check-upstream-version-task.yml` and
`run-codegen-pull-request-task.yml`: `peter-evans/create-pull-request`
configures its own HTTPS credential from its `token` input
(`gitConfigHelper.configureToken(inputs.branchToken)` at the pinned
SHA), so it does not read the persisted one. The checkout keeps its
`token:` input for its own fetch.

Kept with `persist-credentials: true` and an inline reason:

- `build-release-task.yml` `github-release`: the "Supersede a release
the token cannot tag step" runs `git fetch` and `git ls-remote` against
origin.

The release-with-smoke caller snippet's `changes` job (and its copy in
`docs/reusable-workflows.md`) sets `false` too. Its "Filter changed
paths step" now runs only on a `pull_request` event, where
`dorny/paths-filter` reads the changed files from the API. On a dispatch
it would have fetched the base branch with git for an output
`smoke-build` ignores on a dispatch anyway, and an empty output there is
what the doc's dispatch-bypass note already expects.

Already compliant and unchanged: `canonical-review-sweep.yml`,
`test-pull-request.yml`.

Adopter hooks the tasks run (`./.github/actions/*`) were checked with a
code search across the owner's repositories for `git fetch`, `git push`,
`git ls-remote`, `git pull`, and `git clone`. The one hit is a public
repository's validate hook, whose fetch works anonymously and falls back
safely when it fails. `docs/reusable-workflows.md` "The Hook Contract"
now states that a hook runs with no persisted git credential.

`scripts/build_dist.py --check` reports the generated skill
distributions current, so no skill include carries this section.

The new comment lines are owed by the rule itself, so this pull request
carries the `comments` label.

Closes on promotion: #1650

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Security**
* Checkout credentials are no longer persisted by default across build,
release, validation, and deployment workflows. Steps that require
authenticated Git access retain credentials explicitly.
* Workflow hooks that access remotes must provide their own credentials.
* **Documentation**
* Updated workflow guidance and examples to reflect credential handling
and to run changed-path filtering only for pull request events.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

`side_asked_on_head` in `scripts/pr_review_side.py` decides whether
CodeRabbit or Qodo was already asked for the current head. Until now it
compared each ask comment's server `createdAt` with the head commit's
`committedDate`. That date comes from the author's clock, so it can run
behind the push or ahead of it.

- **Backdated commit.** A commit stamped 10:00 and pushed at 10:20 made
a 10:05 ask for the previous head count for the new one. So the new head
was never asked.
- **Future-dated commit.** A commit stamped 10:30 and pushed at 10:20
made a 10:25 ask for this head read as older. So the next `wait` asked
again, a duplicate paid round.

The head's arrival is now the timestamp GitHub's repository activity log
gives the push that left the head on its branch, read through `gh api
repos/<head repo>/activity?ref=refs/heads/<branch>`. A jq projection
prints the newest entry whose `after` is the head. The comment's
`createdAt` is also a server stamp, so GitHub stamps both sides of the
comparison.

This change reintroduces none of the defects the earlier fix rounds on
#3881 hit:

- The log stamps a push when it arrives, before any check suite exists.
So an ask made before the first suite still counts. A repository with no
checks is asked once per head, not on every `wait`.
- No commit date is read anywhere, and `committedDate` leaves the pull
request query.
- `headRepository{ nameWithOwner }` and `headRefName`, the fields
`pushed` reads, are in `Q_HELD`, the query the asks' reads run.

Sometimes the push cannot be read: the read fails, the fork was deleted,
or the log is missing the head. In that case a reviewer with an ask
already on record is not asked again, and `wait` prints a note, because
without the stamp that ask cannot be told from one for an earlier head.
A reviewer with no ask on record is still asked, since there is nothing
to duplicate. Neither bot gates the merge, so the worst case is one
missed side round, never a wrong merge.

`scripts/pr_review.py` changes by its query and one wiring line, which
hands `gh_rest` to `side.ask`. It stays at 2198 lines.
`scripts/pr_review_side.py` is at 349 of its 350.

## Verification

- `tests/test_pr_review_policy.py`, new `TestSideAskServerStamp`:
- A backdated commit and a future-dated commit are each run through
`side.ask` with the commit date set to mislead. Both cases fail with the
old `committedDate` comparison restored.
- The read builds the activity path with the branch URL-encoded. A
failed read, a missing head repository, a missing branch, and a non-hex
head all read as empty.
- The real `jq` binary runs the projection against a constructed log. A
head pushed twice resolves to the later push, and a log without the head
prints nothing.
- An unread push with an ask on record asks nothing and prints the note.
One with no ask on record still sends the first ask, and that case fails
with the gate widened again. The wait-level cases that ask now answer
the activity read.
- The merged-while-checks-settle case now carries a push stamp. It fails
with its post-wait state check removed.
- Live read-only check against an open pull request in this repository.
`Q_HELD` returns `headRepository`, and `pushed` returns the push's
timestamp.
- For eight recent heads in this repository, the activity stamp came 1
to 2 seconds before the first check suite's `createdAt`. Each commit
date was minutes earlier than either.
- The full suite passes except `test_tooling_encoding`, which caught the
new `jq` spawn's missing `encoding="utf-8"`. That is fixed, and the
module passes. Run with `uv run --no-project --with-requirements
tests/requirements.txt python -m unittest discover -s tests -p
'test_*.py'`. The `pr_review` modules and `test_tooling_encoding` pass
after the last edit.
- `ruff format`, `ruff check`, `mypy`, and the prose gate against
`develop` pass.

## Local strict pass findings

The first pass over 2f77e8c raised four:

1. `introduced`, fixed in 11ede57. The merged-while-checks-settle test
passed vacuously, because its payload carried no push stamp.
2. `introduced`, declined. If the activity log lagged the PR head, an
on-open side ask could miss that `wait`. Measured on this repository's
recent heads, the log comes before the first check suite. A lag would
cost one missed side round with a printed note, which is neither silent
nor destructive. Per `GOVERNANCE.md` "Trust Boundaries and Hardening
Effort", it earns no retry.
3. `introduced`, declined. The `scripts/README.md` sentence "asked once
per new head" does not name the unread-push skip. That sentence already
omits the cap, cut-window, and closed-pull-request skips. The
`request_side_reviews` docstring, which lists the skips, now names this
one.
4. Design, declined. `head_arrived` still drives `checks_hold`'s grace
window. It answers a different question: when checks could have started
on a read `await_green` polls. Moving it onto the push would add a REST
read to every poll. A head with no suite and no check reading PENDING is
how `checks_hold` already behaved.

The second pass over 11ede57 raised one:

5. `introduced`, fixed in 34f6585. The unread-push gate also withheld
the first ask, even with no ask on record for an unread push to hide.

The third pass over 34f6585 raised one. It was handed to this pull
request when the opening push's edit budget ran out:

6. `introduced`, fixed in bb41f02. No test pinned `headRepository{
nameWithOwner }` in `Q_HELD`. The fixtures inject it, so the suite would
have passed with it dropped from the query while every live read came
back empty. A case now pins each field the push read takes from
`Q_HELD`, and it fails with that field removed.

The third pass also asked whether the activity log's timestamps carry
fractional seconds. A live read shows they do not, for example
`2026-11-13T19:11:58Z`. Finding 2 has a live data point too: this
branch's own push was in the activity log 4 seconds after `git push`
returned.

The fourth pass over bb41f02 raised none.

Closes on promotion: #3882

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nto One Managed Copy (#4162)

## Summary

- **codex on Windows.** The install runs the documented installer,
`https://chatgpt.com/codex/install.ps1`, rather than refusing. Linux
already ran the documented `install.sh`.
- **opencode 2.** The Linux installer moves to
`https://opencode.ai/v2/install`, and the npm package to
`@opencode/cli`. opencode 1's `opencode-ai` is still recognized.
- **One managed copy, opencode only.** Linux keeps
`~/.opencode/bin/opencode`, installed and upgraded by the v2 installer.
Windows keeps the npm package `@opencode/cli`, since the installer is
bash only.
- **Another copy is reported, never removed.** A copy PATH reaches ahead
of the managed one is a stray. The report, `--upgrade`, and `--install`
name the command that removes it, then `--install opencode`. While a
stray is first on PATH, nothing runs for opencode and the run still
exits 0. The maintainer chose this over automatic removal, since the fix
is a one-off and automation belongs on the managed path.
- **Shared reads.** The upgrade plan and a stray's removal command share
the npm and bun resolver and the Homebrew, scoop, and choco reads.
- **Tests.** Tests that used opencode as a generic npm, bun, scoop,
Homebrew, mise, winget, or choco example now use codex. New tests cover
the managed copy and the stray report on both platforms.
- **Follow-up.** #4158 tracks declaring a managed copy for claude and
codex.

## Verification

- `tests/test_install_agents.py`, run in Linux Docker with pwsh: 106 of
110 pass. The 4 failures are the mise tests, which fail the same way on
unchanged `develop` in that container and pass in CI.
- shellcheck, shfmt, PSScriptAnalyzer, markdownlint, cspell, and the
prose gate pass. The change adds why-comments, so it carries the
`comments` label.
- `install-agents.ps1` was run natively on Windows with `-Report` and
`-Upgrade -DryRun`. It reported the host's opencode 1 npm copy with `npm
uninstall -g opencode-ai`, then `install-agents.ps1 -Install opencode`,
and changed nothing.

## Local-review findings, introduced

An automatic consolidation was built first, and twelve local passes
reviewed it and its replacement. Their findings were fixed, or retired
with the removal design itself. One was declined: the managed path
`~/.opencode/bin/opencode` is fixed, since the v2 installer hard-codes
`INSTALL_DIR=$HOME/.opencode/bin` and reads no override.

Later passes' findings were fixed in 0f79a18, 5a5f128, c6188be, and
4e91618. A second was declined: `Test-ManagedTool`'s `ContainsKey`
guard is required, since under `Set-StrictMode -Version Latest` reading
a missing hashtable key throws "The property 'Managed' cannot be found
on this object", checked with `pwsh -NoProfile -c 'Set-StrictMode
-Version Latest; $h=@{}; $h.Managed'`.

The sixteenth pass's findings were fixed in b22de45 and cf2f952. A
third was declined: a scheduled Linux run judges strays on its own PATH,
where `~/.opencode/bin` is appended. A copy genuinely ahead of it there
is reported with its removal, and nothing fails silently or
destructively, so `GOVERNANCE.md` "Trust Boundaries and Hardening
Effort" gives it no further code.

Later passes' findings were fixed through 84b50e3, and the twenty-first
pass over the final head raised none. No local-review finding is open.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…ipline (#4177)

## Summary

Fixes the defects #1627 lists in `AGENTS.md` "Context and Delegation
Discipline", outside "Model Tiers". Each edit deletes or corrects a
false claim rather than qualifying it.

## Disposition of Each Item

1. **Fixed.** The wait bullet no longer lists `2>/dev/null` beside `||
echo '[]'` and `|| true`. A stderr redirect keeps the exit status, so it
does not convert an error into the "nothing yet" reading. The two `||`
forms do, and they stay.
2. **Fixed by deletion.** The clause "so silence means 'still running'
and nothing else" is removed. A wait killed by its bound, or one whose
buffered output dies with it, is silent too. The instruction to emit on
failure as loudly as on success stays.
3. **Fixed.** The heading map is `grep -nE '^#+ '`, which lists every
heading level, so a reader can narrow to a level-three subsection. The
rule's "about 200 lines" threshold sets when the rule applies, so a
carrying file under it is not a defect, and that part needs no edit.
4. **Fixed.** The example reads "a `--jq` projection on a `gh` API
call", naming the tool. The wait bullets no longer mention `--jq`, so
the second occurrence the issue names is already gone.
5. **Fixed.** The bare "the write-safety rules" now cites
`GOVERNANCE.md` "Repository Boundaries and Write Safety", the form the
"Session Scope" handoff bullet uses.
6. **Already fixed in the current text.** "Fleet label set" no longer
appears. "Session Scope" now says a repository without the `handoff`
label cannot host a track until the label exists there, and that
creating it is a configuration change.
7. **Already fixed in the current text.** The handoff is now one
standing issue per track, its body replaced in place each round and
never closed and replaced per session. So no ordering leaves two open
labelled issues on a track.
8. **Fixed by deletion.** The brief template's `Paths:` line drops
"never 'find the relevant files'". A wide search is briefed with the
globs it covers, which `<exact files or globs>` still requires.

No other file states these rules in a way the edits contradict.
`GOVERNANCE.md`'s own `grep -n '^## ' GOVERNANCE.md` lists that file's
level-two sections, which is what its sentence claims. `build_dist.py`
regenerates nothing from this change, since only "Model Tiers" is
included from `AGENTS.md`.

Closes on promotion: #1627

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


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

<!-- coderabbit-ai-generated:v1 -->

## Summary by CodeRabbit

* **Documentation**
* Updated project guidance for finding Markdown headings and identifying
exact file paths or path patterns in task instructions.
* Clarified how to proceed when provided rules do not address a finding,
including stopping and reporting the gap.
* Revised command-running guidance to require foreground execution and
output inspection before moving a command to the background.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…e Deploy Key Reach Its Job (#4178)

## Summary

- **`install-test-deps` may run per-leg checks (#2853).** The hook runs
once per `test-matrix` leg, after the base install and before that leg's
Python tests. The hook contract and hook catalog in
`docs/reusable-workflows.md`, and the `test-matrix` input description in
`validate-task.yml`, now say it may also run a check needing that leg's
environment. A failure there fails the leg before its Python tests run.
This is option 1 of the maintainer's 2026-11-01 answer on the issue.
- **The deploy key need only reach its job (#1801).** `WORKFLOW.md`
section 6's `hugo` row now requires `DEPLOY_SSH_PRIVATE_KEY` to reach
the deploy job non-empty, rather than requiring the environment to carry
it. The task's assert reads the merged `secrets` context, which cannot
prove the environment store holds the key. The same row's "installs the
deploy credential from the environment" drops "from the environment" for
the same reason. This is option 1 of the maintainer's 2026-11-01 answer
on the issue.

`WORKFLOW.md` D1.2 names no hook, so it needs no change, and no carried
skill include moved (`build_dist.py --check` is current).
`tests/test_release_guards.py` pins neither changed sentence.

## Verification

- `prose_lint.py --diff origin/develop`: no violation.
- `docker_lint.py` markdownlint, cspell, actionlint: success.
- `build_dist.py --check`: generated skill distributions are current.
- `tests/test_release_guards.py` via unittest discover: OK.

## Pre-existing findings, not filed

- `.github/workflows/deploy-site-task.yml:120`, `pre-existing`
correctness (comment): "An unset environment secret therefore reaches
here as an empty string" is false where the caller maps a non-empty
repository secret of the same name, the case #1801 describes. The file
is outside this change's claimed set.
- `.github/workflows/deploy-site-task.yml:96`, `pre-existing`
correctness (comment): "this job's own binding is what determines the
real value" holds only for a secret the environment defines. The file is
outside this change's claimed set.

Closes on promotion: #2853, #1801

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

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…4179)

## Summary

- **Operational `Closes on promotion:` line by pull request.**
`GOVERNANCE.md` "Operational Repositories" and the
`branching-and-release-model` skill named only a direct push as the
place a change writes its `Closes on promotion:` line. A change reaching
`develop` by pull request now writes the line in its body, since the
promotion reads a merged pull request's line from its body rather than
from its commits. The skill's closing-keyword bullet says the reference
is required in an operational repository. The part of #4057 about a fix
reverted later in the promotion range needs a decision, so it moved to
#4175 and is not worked here.
- **Questions read whole.** `GOVERNANCE.md` "Communicating with the
User" requires each question to state the problem, why it is asked, and
what each option does, in its own text. It may cite an issue or pull
request number, but the question and its options read fully without
opening it.
- **The "Before Assuming" rules get a home.** "Ask when the user can
cheaply confirm" and "The irreversible step ... stays the maintainer's"
move from the `agent-conduct` skill into `GOVERNANCE.md` "Communicating
with the User", per the maintainer's answer on #1594. `agent-conduct`
"Before Assuming" becomes a pointer to them, and its "non-trivial"
qualifier under "Before Claiming Done" is dropped. No heading is
renamed.
- `scripts/build_dist.py` regenerated the include regions in
`agent-conduct`, `decision-triage`, and `session-handoff`, and the
generated trees. `--check` is clean.

`GOVERNANCE.md` is carried, so a fleet resync is owed after promotion.

## Pre-existing findings, not filed

- `AGENTS.md` "Where the Rules Live", the row for "Any push, API
mutation, comment, label, or merge" (pre-existing): it routes a merge
only to `GOVERNANCE.md` "Repository Boundaries and Write Safety", which
does not state the irreversible-step rule. That rule's `GOVERNANCE.md`
home is "Communicating with the User", and the row does not name it.
`AGENTS.md` is outside this change, and the gap predates it.

Closes on promotion: #4057, #3865, #1594

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

A Copilot review whose whole body is a GitHub job-start notice naming a
billing failure read as a review round. It covered the head
(`review_on_head=yes`, `coverage=none`), counted toward the round cap,
held the head as `shapes=HELD`, and on an earlier head left a
`shapes=NOTED` part that `merge` refused on until acknowledged.

It now reads as a refusal: it covers nothing, spends no round, and owes
no acknowledgment. It reads as a refusal of no named kind,
`refusal=YES`, and `wait` exits `41` as `REVIEW_IS_A_REFUSAL`, which
goes to the maintainer. It is deliberately not the `quota` kind. Every
status line and remedy that kind drives describes a quota that resets on
its own, and a failed payment or a spending limit never does.

- `scripts/reviewers/copilot.json`: add the refusal opening `job was not
started`. No new refusal kind and no new remedy line.
- `tests/test_pr_review_copilot.py`: the adapter reads a constructed
notice as a refusal with no notes.
- `tests/test_pr_review.py`: the gate reads it as `refusal=YES`, not a
round on the head, no `shapes=HELD`, no round toward the cap, `wait`
exiting `41` as `REVIEW_IS_A_REFUSAL`, and no `shapes=NOTED` on an
earlier head.

Both new tests fail on `develop` without the vocabulary change. The
fixtures are constructed text, not a copied notice.

## Local review findings, declined

- `scripts/pr_review.py:1902-1906` and `scripts/README.md:237`,
`introduced`. The `REVIEW_IS_A_REFUSAL` line says no further review
follows the refusal and that re-requesting the head clears nothing. Once
the maintainer fixes the billing, a re-request does clear it, and only
`wait --ignore-quota-signal` sends one. Declined. The maintainer's
[direction on
#1071](#1071 (comment))
calls a distinct remedy line naming the billing settings optional on top
of reading the notice as a refusal. The line also stays true for as long
as the billing failure stands, and the refusal already goes to the
maintainer, who alone clears it. A billing-specific remedy line would
need `scripts/README.md`, which #4165 holds, and room under
`scripts/pr_review.py`'s 2200-line ceiling, which it sits 2 lines under.

## Test plan

- `python -m unittest test_pr_review_copilot test_pr_review` from
`tests/`, with `tests/requirements.txt`: 656 tests pass.
- Pre-commit hook: ruff format, ruff check, mypy, and the prose gate
pass.

Closes on promotion: #1071

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
#4182)

## Summary

- **Copilot runner precedence (#1448, decided part).**
`docs/homelab-runner-selection-plan.md` now says the organization
setting wins over a setup step's own `runs-on` while repository
customization is disabled, per the maintainer's answer on #1448. It
drops the sentence presenting that `runs-on` as the runner. It keeps the
verify-before-rollout line. GitHub's "Configuring runners" page says
that with "Allow repositories to customize the runner type" disabled,
every repository uses the organization-level runner type.
- **The rename is not made here.** The answer on #1448 also asked to
rename `copilot-code-review.yml` to `copilot-setup-steps.yml`. GitHub's
current documentation names `copilot-code-review.yml` as the file
Copilot code review reads, with `copilot-setup-steps.yml` as its
fallback. So the plan keeps `copilot-code-review.yml`, and the remaining
rename question is #4181. #1448 stays open until #4181 is answered.

## Unfiled defects this run found

- **Deploy-key claims that #1801's rewording left behind.**
`.github/workflows/deploy-site-task.yml` had two comments that assumed
the deploy environment always defines `DEPLOY_SSH_PRIVATE_KEY`. One said
the job's own environment binding determines the real value. The other
said an unset environment secret reaches the assert step as an empty
string. GitHub documents that an environment secret takes precedence
over a same-named repository secret, and that the caller must still pass
the secret. So a caller mapping a non-empty repository secret, with no
environment secret of that name, delivers that value. Both comments are
deleted. In `docs/reusable-workflows.md`, the "Deploy-site." sentence is
qualified to "for a secret the environment defines", matching
`WORKFLOW.md`'s `hugo` bullet. The secrets paragraph's "fails its run
when one it needs is missing or empty" now reads "reaches its job
empty", which the local review pass raised.
- **The merge row in `AGENTS.md` "Where the Rules Live".** It routed
only to `Repository Boundaries and Write Safety`. The irreversible-step
rule, merging included, also lives in `GOVERNANCE.md` "Communicating
with the User", so the row now names that section too.

## Verification

- `scripts/build_dist.py --check` reports the generated skill
distributions current, so no include region reads the changed text.
- The pre-commit gate passed on each commit.
- A local strict review pass is recorded for the pushed head. Round 1
raised one finding, the `docs/reusable-workflows.md` secrets-paragraph
claim, which is fixed. Round 2 raised none.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ptr727 and others added 4 commits November 13, 2026 14:05
## Summary

The hub's `catalog/snippets/configs/global.json` stays the fleet's
default .NET SDK line. A repository's own `global.json` becomes its
selector, and it may lead or lag that default. One repository can then
move to .NET 11 without the hub's tooling pulling it back or reporting
it as drift.

- `CODESTYLE.md` gains a General "Toolchain Versions" rule. Where the
hub carries a baseline selector file, it sets the default line, and each
repository's copy selects its own. A move edits the selector and every
setting naming the same line together. A resync never changes the
selected line. The audit checks that a repository agrees with itself
rather than with the default.
- The `dotnet-codestyle` skill drops "a major move still starts in the
hub". It describes a per-repository move: `global.json`, TFM,
devcontainer tag, and any analyzer `LangVersion`, together.
- `spec/files.json` gains `repoOwnedKeys`, declared on `global.json` as
`sdk.version`. `spec/validate.py` checks the declaration.
- The `files.intent` advisory judges a plain-JSON copy of such a unit by
the leaves it does not own, whatever the dates read, and names each
differing leaf. Leaves compare by JSON type, and a leaf the copy adds is
its own.
- `spec/fidelity_honesty.py` never offers such a unit for verbatim
promotion. `AUDIT.md`, `RESYNC.md`, `spec/fidelity-model.md`, and
`spec/checks.json` describe the exception.
- The `csharp.sdk.globaljson` check states that the line is the
repository's own, and that a carried .NET devcontainer tag names the
same major.
- Installer comments, both host READMEs, `docs/devcontainer.md`, and the
`dotnet-sdk` action description call the hub line the fleet default
rather than every repository's selection.

Out of scope, by maintainer decision: adding the next .NET major to the
Windows installer's `-Optional` set. It drew a new defect on every
review pass, so it moves to its own change. The Windows installer here
is develop's apart from wording, and a Windows host takes a newer major
through a direct winget install.

Python on the same model, a carried `.python-version` with a 3.14
default that CI reads, is planned as a separate pull request.

## Verification

- `python -m unittest discover -s tests`: 4204 tests, OK (11 skipped) on
the merged tree. New cases cover the `repoOwnedKeys` advisory, JSON-type
comparison, its validation, and the promotion exclusion.
- `spec/validate.py` OK, `scripts/build_dist.py --check` current, ruff
and mypy clean through the pre-commit hook, prose gate clean with the
`comments` label.
- Local strict review: a recorded pass over every pushed head. The last
pass raised no findings.

## Review Findings

- Maintainer decision: a `global.json` copy carrying comments stays
dated as before. The audit reads plain JSON only, and the docs say so.
- `introduced`, declined: an `sdk.version`-only baseline move re-fires
an acknowledged fleet-owned difference. The only `repoOwnedKeys` unit is
`global.json`, whose fleet-owned leaves the `csharp.sdk.globaljson`
letter check pins. A difference there is a letter failure no driftNote
excuses.
- Every other local and hosted finding is fixed on this branch, or went
with the Windows installer change when it left this pull request.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
## Summary

One pass over the fence scanner's comments and docstrings in
`tests/test_release_guards.py`, with deletion as the default. Each
comment left states what the scan does. A renderer is cited only where a
test case pins the difference. No test logic changed, and the suite
passes.

## Disposition of the issue's items

Each item was re-verified against the current file first. The file now
lives at `tests/test_release_guards.py`, not under `scripts/tests/`.

1. **Blank-line refusal comment.** Still present and still false of the
scan. Deleted, with its two neighbouring sentences, including the
renderer clause on the no-break-space case above it.
2. **"ends that container on a line carrying less than that".** Both
copies still present. Deleted from the refused-case list and from
`fenced_blocks`. The body-indent comment keeps only its two lines that
state the scan's rule.
3. **"A fence sharing a line with anything else needs the container to
read."** Still present. Deleted, with the container sentence after it.
4. **"because a scan that guesses at the container..." in the extractor
test's docstring.** Still present. Deleted, with the "Everything else is
refused" sentence, which the left-standing HTML-block gap also made
untrue.
5. **"The refused cases below are the list."** Still present. Deleted,
with the second-list-level example no case pins and the "by convention
rather than by check" sentence. A refused shape in a document under test
raises, so that sentence was also false. "Legal Markdown is among what
this refuses" stays, pinned by the refused cases such as a list-marker
line and a tilde in a backtick fence's info string.
6. **"Five silent defects..." in the guard test's docstring.** Still
present. Deleted as defect history, with the "sixth defect" paragraph.
The "four functions named below" sentence was also wrong, since five are
named, so it now states which source is read without a count.
7. **"Left to surface as an unterminated block, it would name a line
carrying no fence."** Still present. Deleted.
8. **The three lower-confidence ones.**
- "never miss one it does" (still present): the five-line renderer
comparison above the info-string split is deleted, since no case pins
it. The `any-whitespace:` marker stays on the line, and its inline
reason now reads "no matched label holds whitespace", a fact about the
callers' label sets rather than about a renderer.
- "Whitespace after a closing fence is not text after it" (still
present): deleted.
- "A run of three or more backticks or tildes opens or closes a code
block" (still present): deleted, with the two lines after it, which
depended on it. The docstring now says a fence-run line is refused
unless it matches `FENCE_LINE`.

## Other deletions in the same pass

- The `fenced_blocks` docstring's container and HTML-block reasoning.
Its one load-bearing fact is restated as scan behaviour: a fence line
met while a block is open either closes it or raises, so no labelled
block is read as another's body.
- The docstring's whitespace-defect history and bullet list.
- The line-split comment's three hypothetical-alternative lines. The two
lines pinned by the carriage-return and line-separator cases stay.
- Refused-case comments giving renderer reasoning that no case pins, and
comments that only restated a case's name.

## Local review findings from the opening push

The opening push spent its two-round `local-strict-review` budget with
two `introduced` findings open. Each is now disposed of in ce88ccf, and
the pass over that head recorded no findings.

- `introduced`, the `.split()` marker `# any-whitespace: can add a
match, never split one`. Fixed. The marker now reads `no matched label
holds whitespace`, a fact about the callers' label sets. An earlier pass
asked the marker to also say that a wider split can add a match. That
point is declined under the issue's own rule, since that says how
another reader splits the label, and no case pins it.
- `introduced`, the `fenced_blocks` docstring sentences claiming each
whitespace comparison is named or marked and that a test holds that
rule. Fixed by deletion. The guard reads named methods and the `\s`
spelling only, so the claim was wider than the check.

Closes on promotion: #1620

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…nds (#4183)

## Summary

**Title limit and bind words.** The 72-character limit measures the
authored title or subject, and the ` (#N)` suffix GitHub appends at a
squash merge sits outside it. The lowercase title-case bind-word list is
marked exhaustive and gains with, as, via, into, over, and per.
`GOVERNANCE.md` "Pull Request Title and Commit Message Conventions",
`comment-and-doc-style`, and `.github/copilot-instructions.md` now
agree.

Two headings capitalized a listed word, so they are lowercased, along
with the README table-of-contents entry for one of them. Anchors are
lowercase already, so no link moves. `STANDUP.md` "Hand Over What Only
the Maintainer Can Supply" is left as it is. "Over" there is a
phrasal-verb particle, not a bind word.

**Closing keywords under the squash setting.** The hub's
`squash_merge_commit_message` reads `PR_BODY` (read live). So a feature
pull request's body becomes its squash commit on `develop`. A closing
keyword there closes its issue once the promotion brings that commit to
`main`, whatever the promotion body says. `branching-and-release-model`
and `backlog-burndown` both said such a keyword closes nothing, which is
corrected here. The rules now state:

- what a keyword does in a feature body, and that a direct-push commit
message behaves the same way;
- that the settled convention stands: a feature body or direct-push
commit carries only the inert `Closes on promotion:` line, and the
promotion body owns the closing keywords;
- that a keyword fires inside a sentence that negates it, with the safe
rewording;
- a mechanical closing-set check after each promotion body write, again
when the promotion is reported ready, and before it merges. It compares
`closingIssuesReferences` with the intended set and greps the promoted
range's commit messages for closing keywords.

`GOVERNANCE.md` "Branching Model" gains a short summary. `drive-pr` step
5 points to the check, and `merge-and-release` step 2 runs it before the
merge. The generated skill trees are regenerated by
`scripts/build_dist.py`.

## Local Review Findings Left Standing

- `introduced`, fixed in the second push: `merge-and-release` step 2's
closing-set check read a local `git log`, which for a downstream target
is the hub checkout's range. It now reads the target's range through `gh
api --paginate .../compare/main...<sha>`, run on its own so a failed
read stops the merge rather than reading as a clean range.
- `introduced`, fixed in the second push: the same check read as
applying only to the no-ready-report case. It is now its own
unconditional sentence.
- `pre-existing`/declined: a reviewer may read `STANDUP.md` "Hand Over"
as breaking the exhaustive list, since "over" is on it. It is a
phrasal-verb particle rather than a bind word, so it stays capitalized
by the coordinator's ruling. A particle exception would be a new rule
neither issue asks for.

Closes on promotion: #1154, #1471

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…#4185)

## Summary

Corrects the history-rewrite premise in `git-commit-conventions`. The
skill and its `references/history-rewrite.md` said `git filter-repo`
re-signs every commit it touches. It does not. It strips every commit
signature it processes and signs nothing, so a rewritten history lands
unsigned. Per the maintainer's answer on #3750, this stays inside the
existing skill and its reference rather than a new skill.

- `references/history-rewrite.md` states the facts the rules follow
from: a rewrite cascades to every descendant, `git filter-repo` strips
signatures and signs nothing, and GitHub verifies a signature against
the `committer`. The gates now state the reach first, re-sign in a
separate signing pass, re-identify only the committer, and keep the
author. Verification reads GitHub's commits API (`verification.verified`
and `reason`) rather than `git log --show-signature`. The signing-pass
example sets the committer and moves tags with it. Gate 4 stops an agent
before the force push, which stays the maintainer's. A collateral list
covers tags, remote tags and pull request refs, other clones, and
published artifacts.
- `SKILL.md` "History rewrites re-identify only what changed" carries
the same corrected summary.
- `docs/repo-config.md` "Brownfield Migration" aligns its re-signing
guidance with the same premise.
- Generated `.github/skills/` and `.claude-plugin/` copies regenerated
with `scripts/build_dist.py`, and `--check` is clean.

## Verification

- A constructed throwaway repository with a throwaway ed25519 SSH key
and synthetic `example.invalid` identities, run under `setsid --wait
timeout`. All of its commits, a merge included, read `%G? = G`. A `git
filter-repo --mailmap` run changing one commit's author left every
commit on every branch unsigned (`N`, zero `gpgsig` headers). That
included the root commit it did not change, and every descendant's hash
changed. A following `git filter-branch --commit-filter
'GIT_COMMITTER_NAME=... GIT_COMMITTER_EMAIL=... git commit-tree -S "$@"'
-- --branches` pass re-signed every commit (`G`). It set the committer,
kept each author (a bot author included), kept the merge's parents and
both dates, and left every tree hash and the commit count unchanged.
With `--tag-name-filter cat -- --branches --tags`, a lightweight tag and
a signed tag both moved to re-signed commits on the branch, and the
rewritten signed tag no longer verifies (`git verify-tag`).
- The procedure covers a default, whole-history `git filter-repo` run. A
constructed range-limited run (`--refs`) left other branches and tags on
the old commits, so the reference gives no range-limited variant.
- `git filter-repo`'s own documentation states that a rewritten commit's
signature is removed. GitHub's REST commits documentation lists the
`commit.verification` fields and defines the `no_user`,
`unverified_email`, and `bad_email` reasons by the committer email.
- The verification command's `--jq` projection was checked read-only
against this repository's own `develop`.
- The brownfield re-sign in a plain clone rewrites only local branches,
so `docs/repo-config.md` says to create a local branch for each remote
one first and follows the reference's gates.
- Gate 1's `git for-each-ref` signed-tag listing, and `git tag -f -s
<tag> '<tag>^{commit}'` producing a tag that verifies, checked in a
constructed repository. The gate 4 command shape was checked read-only
against this repository's `develop`.
- `prose_lint.py --diff origin/develop`, markdownlint, cspell, and
editorconfig-checker (`scripts/docker_lint.py`) pass. `GOVERNANCE.md`
"Git and Commit Rules" ("re-identifies only the commits it touches that
aren't yours") stays true and is unchanged.

## Open introduced findings (local pass edit budget spent)

- `references/history-rewrite.md` gate 4, `introduced`: the maintainer's
force push could overwrite a commit pushed to the remote while the
rewrite ran, since nothing records each remote tip for a
`--force-with-lease=<branch>:<sha>` push. Outcome: declined. The
reference prescribes no push command and hands the push to the
maintainer, who composes it, and a lease procedure is a new rule #1391
does not ask for.
- `docs/repo-config.md` "Brownfield Migration", `introduced`: the
migration sends the reader to the whole-history signing pass, which also
re-signs commits older than the earliest unsigned one. Outcome:
declined. `git-commit-conventions` "Signing must be live before the
*first* commit" already states that this retrofit rewrites the entire
history and makes the rewriter the committer and signer of every commit
in it.

Closes on promotion: #1391

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings November 13, 2026 23:14
@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 119 files, which is 19 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Check out review usage here.

Run configuration

Commits

Reviewing files that changed from the base of the PR and between dbe09c0 and 1aa25ea.

Files ignored due to path filters: 68

  • .claude-plugin/fleet-skills/.source-digests/agent-conduct is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/audit-a-repo is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/backlog-burndown is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/branching-and-release-model is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/carried-instruction-file-guard is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/check-this-repo is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/comment-and-doc-style is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/decision-triage is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/dotnet-codestyle is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/drive-pr is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/git-commit-conventions is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/merge-and-release is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/pr-review-conduct is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/python-codestyle is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/repo-worktree is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/resync-a-repo is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/session-handoff is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/shell-codestyle is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/standup-a-repo is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/.source-digests/upstream-contribution-workflow is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/agent-conduct/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/audit-a-repo/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/backlog-burndown/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/branching-and-release-model/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/carried-instruction-file-guard/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/check-this-repo/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/comment-and-doc-style/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/decision-triage/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/dotnet-codestyle/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/dotnet-codestyle/references/conventions.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/dotnet-codestyle/references/project-config.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/drive-pr/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/git-commit-conventions/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/git-commit-conventions/references/history-rewrite.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/merge-and-release/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/pr-review-conduct/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/python-codestyle/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/python-codestyle/references/profiles.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/repo-worktree/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/resync-a-repo/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/session-handoff/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/shell-codestyle/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/standup-a-repo/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .claude-plugin/fleet-skills/skills/upstream-contribution-workflow/SKILL.md is excluded by !.claude-plugin/fleet-skills/**
  • .github/skills/agent-conduct/SKILL.md is excluded by !.github/skills/**
  • .github/skills/audit-a-repo/SKILL.md is excluded by !.github/skills/**
  • .github/skills/backlog-burndown/SKILL.md is excluded by !.github/skills/**
  • .github/skills/branching-and-release-model/SKILL.md is excluded by !.github/skills/**
  • .github/skills/carried-instruction-file-guard/SKILL.md is excluded by !.github/skills/**
  • .github/skills/check-this-repo/SKILL.md is excluded by !.github/skills/**
  • .github/skills/comment-and-doc-style/SKILL.md is excluded by !.github/skills/**
  • .github/skills/decision-triage/SKILL.md is excluded by !.github/skills/**
  • .github/skills/dotnet-codestyle/SKILL.md is excluded by !.github/skills/**
  • .github/skills/dotnet-codestyle/references/conventions.md is excluded by !.github/skills/**
  • .github/skills/dotnet-codestyle/references/project-config.md is excluded by !.github/skills/**
  • .github/skills/drive-pr/SKILL.md is excluded by !.github/skills/**
  • .github/skills/git-commit-conventions/SKILL.md is excluded by !.github/skills/**
  • .github/skills/git-commit-conventions/references/history-rewrite.md is excluded by !.github/skills/**
  • .github/skills/merge-and-release/SKILL.md is excluded by !.github/skills/**
  • .github/skills/pr-review-conduct/SKILL.md is excluded by !.github/skills/**
  • .github/skills/python-codestyle/SKILL.md is excluded by !.github/skills/**
  • .github/skills/python-codestyle/references/profiles.md is excluded by !.github/skills/**
  • .github/skills/repo-worktree/SKILL.md is excluded by !.github/skills/**
  • .github/skills/resync-a-repo/SKILL.md is excluded by !.github/skills/**
  • .github/skills/session-handoff/SKILL.md is excluded by !.github/skills/**
  • .github/skills/shell-codestyle/SKILL.md is excluded by !.github/skills/**
  • .github/skills/standup-a-repo/SKILL.md is excluded by !.github/skills/**
  • .github/skills/upstream-contribution-workflow/SKILL.md is excluded by !.github/skills/**
Files selected for processing: 119

  • .agents/skills/agent-conduct/SKILL.md
  • .agents/skills/audit-a-repo/SKILL.md
  • .agents/skills/backlog-burndown/SKILL.md
  • .agents/skills/branching-and-release-model/SKILL.md
  • .agents/skills/carried-instruction-file-guard/SKILL.md
  • .agents/skills/check-this-repo/SKILL.md
  • .agents/skills/comment-and-doc-style/SKILL.md
  • .agents/skills/decision-triage/SKILL.md
  • .agents/skills/dotnet-codestyle/SKILL.md
  • .agents/skills/dotnet-codestyle/references/conventions.md
  • .agents/skills/dotnet-codestyle/references/project-config.md
  • .agents/skills/drive-pr/SKILL.md
  • .agents/skills/git-commit-conventions/SKILL.md
  • .agents/skills/git-commit-conventions/references/history-rewrite.md
  • .agents/skills/merge-and-release/SKILL.md
  • .agents/skills/pr-review-conduct/SKILL.md
  • .agents/skills/python-codestyle/SKILL.md
  • .agents/skills/python-codestyle/references/profiles.md
  • .agents/skills/repo-worktree/SKILL.md
  • .agents/skills/resync-a-repo/SKILL.md
  • .agents/skills/session-handoff/SKILL.md
  • .agents/skills/shell-codestyle/SKILL.md
  • .agents/skills/standup-a-repo/SKILL.md
  • .agents/skills/upstream-contribution-workflow/SKILL.md
  • .github/actions/dotnet-sdk/action.yml
  • .github/actions/prose-gate/prose-gate.json
  • .github/actions/prose-gate/prose_lint.py
  • .github/copilot-instructions.md
  • .github/workflows/build-docker-task.yml
  • .github/workflows/build-release-task.yml
  • .github/workflows/check-upstream-version-task.yml
  • .github/workflows/deploy-site-task.yml
  • .github/workflows/get-version-task.yml
  • .github/workflows/publish-docker-readme-task.yml
  • .github/workflows/run-codegen-pull-request-task.yml
  • .github/workflows/validate-task.yml
  • .husky/pre-commit
  • .husky/pre-push
  • AGENTS.md
  • AUDIT.md
  • CODESTYLE.md
  • GOVERNANCE.md
  • OPERATIONS.md
  • README.md
  • RESYNC.md
  • STANDUP.md
  • WORKFLOW.md
  • catalog/snippets/configs/AUDIT.md
  • catalog/snippets/configs/vscode-tasks-python.json
  • catalog/snippets/hub_fetch_run.py
  • catalog/snippets/husky/README.md
  • catalog/snippets/husky/pre-commit
  • catalog/snippets/pre-commit/.pre-commit-config.yaml
  • catalog/snippets/pre-commit/README.md
  • catalog/snippets/workflows/test-pull-request-smoke.yml
  • docs/content-import.md
  • docs/devcontainer.md
  • docs/homelab-runner-selection-plan.md
  • docs/host-setup.md
  • docs/repo-config.md
  • docs/reusable-workflows.md
  • host-setup/linux/README.md
  • host-setup/linux/install-agents.sh
  • host-setup/linux/install-tools.sh
  • host-setup/menu.ps1
  • host-setup/menu.sh
  • host-setup/windows/README.md
  • host-setup/windows/install-agents.ps1
  • host-setup/windows/install-tools.ps1
  • lib/sh/secrets.sh
  • registry/repos.json
  • scripts/README.md
  • scripts/agent_policy.py
  • scripts/backlog_metrics.py
  • scripts/build_dist.py
  • scripts/canonical_review.py
  • scripts/carry.py
  • scripts/central_config.py
  • scripts/pr_cleanup.py
  • scripts/pr_cleanup_rest.py
  • scripts/pr_review.py
  • scripts/pr_review_copilot.py
  • scripts/pr_review_side.py
  • scripts/reviewers/copilot.json
  • scripts/skills_install.py
  • spec/audit.py
  • spec/checks.json
  • spec/divergences.json
  • spec/fidelity-model.md
  • spec/fidelity_honesty.py
  • spec/files.json
  • spec/files.schema.json
  • spec/host-tools.json
  • spec/project-types.json
  • spec/validate.py
  • spec/workflow_reuse.py
  • tests/requirements.txt
  • tests/test_agent_policy.py
  • tests/test_audit.py
  • tests/test_audit_helpers.py
  • tests/test_backlog_metrics.py
  • tests/test_bootstrap.py
  • tests/test_carry.py
  • tests/test_fidelity_honesty.py
  • tests/test_host_gate.py
  • tests/test_hub_fetch_run.py
  • tests/test_install_agents.py
  • tests/test_install_tools.py
  • tests/test_pr_cleanup.py
  • tests/test_pr_review.py
  • tests/test_pr_review_copilot.py
  • tests/test_pr_review_policy.py
  • tests/test_pr_review_size.py
  • tests/test_pre_commit_hook.py
  • tests/test_prose_lint.py
  • tests/test_release_guards.py
  • tests/test_secrets_sh.py
  • tests/test_skills_install.py
  • tests/test_spec_validate.py

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

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.

🟡 Changes recommended

A critical active finding remains because branch cleanup may delete a non-promotion branch without verifying the merge commit’s first parent is in the default branch history.

1 open issue
What changed in this PR

This promotion consolidates backlog fixes from develop into main across hooks, review tooling, repository procedures, workflow contracts, host setup, specifications, and carried fleet guidance.

Changes:

  • Adds merge-aware prose gating, interrupt handling, credential cleanup, and explicit repository cleanliness checks.
  • Extends audit and fidelity validation for referenced files, repository-owned JSON keys, devcontainers, and pinned dependencies.
  • Updates fleet procedures, generated Skills, host tooling, workflow documentation, and repository metadata.
File Description
tests/​test_spec_validate.py Tests reference-aware link validation and repository-owned JSON keys.
tests/​test_skills_install.py Verifies untracked files are included in installer cleanliness checks.
tests/​test_secrets_sh.py Tests guarded environment-file includes and secret validation.
tests/​test_pr_review_size.py Adds the cleanup helper to review size limits.
tests/​test_pr_review_copilot.py Tests billing-related Copilot refusal handling.
tests/​test_host_gate.py Tests optional 1Password CLI host-tool metadata.
tests/​test_fidelity_honesty.py Ensures repository-owned units are excluded from verbatim promotion.
tests/​test_carry.py Tests untracked-file detection under Git status settings.
tests/​test_audit.py Tests devcontainer ecosystem detection and audit findings.
tests/​test_agent_policy.py Tests malformed registry input handling.
tests/​requirements.txt Pins test dependencies and relevant transitive packages.
STANDUP.md Points downstream repositories to the canonical audit template.
spec/​workflow_reuse.py Pins standalone workflow-analysis dependencies.
spec/​validate.py Validates carried references and repository-owned JSON keys.
spec/​project-types.json Recognizes devcontainer configurations and repository-specific .NET SDK lines.
spec/​files.schema.json Defines the repoOwnedKeys manifest property.
spec/​files.json Declares the audit template and repository-owned SDK key.
spec/​fidelity_honesty.py Excludes repository-owned units from verbatim promotion candidates.
spec/​divergences.json Updates a divergence rationale by removing an obsolete coordination reference.
spec/​checks.json Documents expanded secret, intent, and owned-key checks.
scripts/​skills_install.py Includes all untracked files in installer status checks.
scripts/​reviewers/​copilot.json Recognizes review jobs that never started as refusal openings.
scripts/​pr_review_copilot.py Pins Markdown parser dependencies.
scripts/​pr_cleanup.py Supports cleanup of verified conflicted promotion branches.
scripts/​pr_cleanup_rest.py Provides REST and ancestry checks for promotion cleanup.
scripts/​central_config.py Pins configuration-script dependencies.
scripts/​carry.py Uses explicit untracked-file status detection.
scripts/​canonical_review.py Pins canonical-review dependencies.
scripts/​build_dist.py Pins distribution-builder dependencies.
scripts/​agent_policy.py Documents trusted configuration inputs and refusal boundaries.
registry/​repos.json Adds a repository Python source directory to the registry.
README.md Corrects heading casing.
OPERATIONS.md Documents agent-policy trusted inputs.
lib/​sh/​secrets.sh Provides validation functions for guarded environment-file reads.
host-setup/​windows/​install-tools.ps1 Adds optional 1Password CLI selection and reporting.
host-setup/​menu.sh Centralizes explicit hub cleanliness checks.
host-setup/​menu.ps1 Adds the equivalent Windows hub cleanliness helper.
docs/​repo-config.md Expands the signed-history rewrite procedure.
docs/​homelab-runner-selection-plan.md Clarifies the effective Copilot runner policy.
docs/​devcontainer.md Documents repository-owned .NET SDK devcontainer versions.
docs/​content-import.md Corrects heading casing.
catalog/​snippets/​workflows/​test-pull-request-smoke.yml Disables persisted checkout credentials and limits path filtering to pull requests.
catalog/​snippets/​pre-commit/​README.md Documents merge-aware commit diffing.
catalog/​snippets/​pre-commit/​.pre-commit-config.yaml Uses the merge-aware prose-gate option.
catalog/​snippets/​husky/​README.md Aligns hook line-ending guidance with global defaults.
catalog/​snippets/​husky/​pre-commit Adds interrupt cleanup and reliable staged-path handling.
catalog/​snippets/​configs/​vscode-tasks-python.json Targets pip installs to the project virtual environment.
catalog/​snippets/​configs/​AUDIT.md Adds the canonical downstream audit template.
.husky/​pre-push Adds signal cleanup around the local review process.
.github/​workflows/​run-codegen-pull-request-task.yml Disables persisted checkout credentials.
.github/​workflows/​publish-docker-readme-task.yml Disables persisted checkout credentials for read-only checkouts.
.github/​workflows/​get-version-task.yml Disables persisted checkout credentials.
.github/​workflows/​deploy-site-task.yml Disables persisted checkout credentials and removes stale comments.
.github/​workflows/​check-upstream-version-task.yml Disables persisted checkout credentials.
.github/​workflows/​build-release-task.yml Disables credentials for build checkouts while retaining them for release tagging.
.github/​workflows/​build-docker-task.yml Disables persisted checkout credentials.
.github/​skills/​upstream-contribution-workflow/​SKILL.md Clarifies clean-branch contribution completion.
.github/​skills/​standup-a-repo/​SKILL.md Points standup audits to the canonical audit template.
.github/​skills/​resync-a-repo/​SKILL.md Adds host checks and the unpromoted-develop exception.
.github/​skills/​python-codestyle/​references/​profiles.md Documents mypy target configuration.
.github/​skills/​pr-review-conduct/​SKILL.md Clarifies carried review coverage and promotion cleanup.
.github/​skills/​merge-and-release/​SKILL.md Adds closing-keyword validation and promotion cleanup steps.
.github/​skills/​git-commit-conventions/​SKILL.md Revises history-rewrite signing guidance.
.github/​skills/​drive-pr/​SKILL.md Clarifies peer-merge findings and promotion-body requirements.
.github/​skills/​dotnet-codestyle/​SKILL.md Supports repository-owned .NET lines and updates ordering guidance.
.github/​skills/​dotnet-codestyle/​references/​project-config.md Documents repository-owned SDK and target-framework lines.
.github/​skills/​dotnet-codestyle/​references/​conventions.md Expands StyleCop member-ordering rules.
.github/​skills/​comment-and-doc-style/​SKILL.md Adds quoted-code, closing-keyword, and title-length guidance.
.github/​skills/​check-this-repo/​SKILL.md Detects stale registered hub checkouts.
.github/​skills/​carried-instruction-file-guard/​SKILL.md Clarifies local CODESTYLE additions.
.github/​skills/​audit-a-repo/​SKILL.md Corrects audit preview and hub-checkout commands.
.github/​copilot-instructions.md Delegates review coverage rules to canonical governance.
.github/​actions/​prose-gate/​prose-gate.json Adds merge-aware diff metadata and updated rule text.
.github/​actions/​dotnet-sdk/​action.yml Describes repository-owned SDK selection.
.claude-plugin/​fleet-skills/​skills/​upstream-contribution-workflow/​SKILL.md Regenerates the Claude upstream contribution Skill.
.claude-plugin/​fleet-skills/​skills/​standup-a-repo/​SKILL.md Regenerates the Claude standup Skill.
.claude-plugin/​fleet-skills/​skills/​resync-a-repo/​SKILL.md Regenerates the Claude resync Skill.
.claude-plugin/​fleet-skills/​skills/​python-codestyle/​references/​profiles.md Regenerates the Claude Python profile reference.
.claude-plugin/​fleet-skills/​skills/​pr-review-conduct/​SKILL.md Regenerates the Claude review-conduct Skill.
.claude-plugin/​fleet-skills/​skills/​merge-and-release/​SKILL.md Regenerates the Claude merge-and-release Skill.
.claude-plugin/​fleet-skills/​skills/​git-commit-conventions/​SKILL.md Regenerates the Claude commit-conventions Skill.
.claude-plugin/​fleet-skills/​skills/​drive-pr/​SKILL.md Regenerates the Claude drive-pr Skill.
.claude-plugin/​fleet-skills/​skills/​dotnet-codestyle/​SKILL.md Regenerates the Claude .NET Skill.
.claude-plugin/​fleet-skills/​skills/​dotnet-codestyle/​references/​project-config.md Regenerates the Claude .NET project reference.
.claude-plugin/​fleet-skills/​skills/​dotnet-codestyle/​references/​conventions.md Regenerates the Claude .NET conventions reference.
.claude-plugin/​fleet-skills/​skills/​comment-and-doc-style/​SKILL.md Regenerates the Claude documentation-style Skill.
.claude-plugin/​fleet-skills/​skills/​check-this-repo/​SKILL.md Regenerates the Claude repository-check Skill.
.claude-plugin/​fleet-skills/​skills/​carried-instruction-file-guard/​SKILL.md Regenerates the Claude carried-file guard Skill.
.claude-plugin/​fleet-skills/​skills/​audit-a-repo/​SKILL.md Regenerates the Claude audit Skill.
.claude-plugin/​fleet-skills/​.source-digests/​upstream-contribution-workflow Updates the generated upstream-contribution Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​standup-a-repo Updates the generated standup Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​shell-codestyle Updates the generated shell-codestyle Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​session-handoff Updates the generated session-handoff Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​resync-a-repo Updates the generated resync Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​repo-worktree Updates the generated repo-worktree Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​python-codestyle Updates the generated Python-codestyle Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​pr-review-conduct Updates the generated review-conduct Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​merge-and-release Updates the generated merge-and-release Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​git-commit-conventions Updates the generated commit-conventions Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​drive-pr Updates the generated drive-pr Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​dotnet-codestyle Updates the generated .NET Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​decision-triage Updates the generated decision-triage Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​comment-and-doc-style Updates the generated documentation-style Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​check-this-repo Updates the generated repository-check Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​carried-instruction-file-guard Updates the generated carried-file guard Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​branching-and-release-model Updates the generated branching-and-release Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​backlog-burndown Updates the generated backlog-burndown Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​audit-a-repo Updates the generated audit Skill digest.
.claude-plugin/​fleet-skills/​.source-digests/​agent-conduct Updates the generated agent-conduct Skill digest.
.agents/​skills/​upstream-contribution-workflow/​SKILL.md Updates the canonical upstream contribution Skill.
.agents/​skills/​standup-a-repo/​SKILL.md Updates the canonical standup Skill.
.agents/​skills/​resync-a-repo/​SKILL.md Updates the canonical resync Skill.
.agents/​skills/​python-codestyle/​references/​profiles.md Updates the canonical Python profile reference.
.agents/​skills/​pr-review-conduct/​SKILL.md Updates the canonical review-conduct Skill.
.agents/​skills/​merge-and-release/​SKILL.md Updates the canonical merge-and-release Skill.
.agents/​skills/​git-commit-conventions/​SKILL.md Updates the canonical commit-conventions Skill.
.agents/​skills/​drive-pr/​SKILL.md Updates the canonical drive-pr Skill.
.agents/​skills/​dotnet-codestyle/​SKILL.md Updates the canonical .NET Skill.
.agents/​skills/​dotnet-codestyle/​references/​project-config.md Updates the canonical .NET project reference.
.agents/​skills/​dotnet-codestyle/​references/​conventions.md Updates the canonical .NET conventions reference.
.agents/​skills/​comment-and-doc-style/​SKILL.md Updates the canonical documentation-style Skill.
.agents/​skills/​check-this-repo/​SKILL.md Updates the canonical repository-check Skill.
.agents/​skills/​carried-instruction-file-guard/​SKILL.md Updates the canonical carried-file guard Skill.
.agents/​skills/​audit-a-repo/​SKILL.md Updates the canonical audit Skill.

Review effort: Lite

Comment thread scripts/pr_cleanup_rest.py Resolved
@codecov

codecov Bot commented Nov 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 86.51316% with 41 lines in your changes missing coverage. Please review.
✅ Project coverage is 76.77%. Comparing base (fa06887) to head (1aa25ea).
⚠️ Report is 494 commits behind head on main.

Files with missing lines Patch % Lines
scripts/backlog_metrics.py 79.67% 25 Missing ⚠️
scripts/pr_review.py 50.00% 15 Missing ⚠️
spec/fidelity_honesty.py 66.66% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4189      +/-   ##
==========================================
+ Coverage   76.69%   76.77%   +0.08%     
==========================================
  Files          40       41       +1     
  Lines       14146    14354     +208     
==========================================
+ Hits        10849    11021     +172     
- Misses       3297     3333      +36     
Flag Coverage Δ
python-3.13 76.77% <86.51%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@ptr727

ptr727 commented Nov 13, 2026

Copy link
Copy Markdown
Owner Author

/review

@coderabbitai

coderabbitai Bot commented Nov 13, 2026 •

Copy link
Copy Markdown
Action not completed

Review rate limited.

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 59adddf into main Nov 13, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants