Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
#
# github-actions is the only ecosystem this repo ships. Dual-target (main + develop) so both branches
# stay current independently of the develop -> main release cadence; the merge-bot auto-merges each
# base with its per-base method. See catalog/snippets/configs/dependabot.yml for the multi-ecosystem
# reference (nuget, uv) a code-shipping repo uses.
# The github-actions ecosystem is the only one this repo ships.
# Both main and develop are targeted so each stays current independently of the develop to main release cadence.
# The merge-bot auto-merges each base with its per-base method.
# See catalog/snippets/configs/dependabot.yml for the multi-ecosystem reference, covering nuget and uv, that a code-shipping repo uses.
version: 2
updates:

Expand Down
27 changes: 15 additions & 12 deletions .github/workflows/merge-bot-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@ name: Merge bot pull request action
# Auto-merges in-repo bot PRs (Dependabot, codegen): enable on opened/reopened, disable on a maintainer push.
# - Merge method by base: develop = squash, main = merge.
# - App token, not GITHUB_TOKEN: fires downstream workflows on merge, and grants write on read-only Dependabot PRs.
# - pull_request_target, not pull_request: jobs hold the App key, so the workflow + action SHAs resolve from the
# trusted base, not PR head. Safe because no job checks out PR code (each runs gh pr merge by URL).
# - pull_request_target rather than pull_request, since jobs hold the App key, so the workflow and action SHAs resolve from the trusted base rather than the PR head.
# This is safe because no job checks out PR code, each one running gh pr merge by URL.
on:
pull_request_target:
types: [opened, reopened, synchronize]

# Concurrency keys on the PR number, not github.ref (the base branch under pull_request_target, which would
# serialize every bot PR against it), so each PR queues independently. cancel-in-progress: false so a follow-up
# synchronize doesn't cancel an in-flight opened run before it enables auto-merge.
# Concurrency keys on the PR number rather than on github.ref, which under pull_request_target is the base branch and would serialize every bot PR against it, so each PR queues independently.
# The cancel-in-progress setting is false so a follow-up synchronize does not cancel an in-flight opened run before it enables auto-merge.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: false
Expand All @@ -21,7 +20,8 @@ jobs:
merge-dependabot:
name: Merge dependabot pull request job
runs-on: ubuntu-latest
# Dependabot PRs from this repo (not forks). Only on opened/reopened so the disable job stays sticky.
# Dependabot PRs from this repo rather than from forks.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'dependabot[bot]' &&
Expand Down Expand Up @@ -59,8 +59,9 @@ jobs:
merge-codegen:
name: Merge codegen pull request job
runs-on: ubuntu-latest
# Codegen PRs from this repo. Head/base pairing is enforced strictly (codegen-main->main, codegen-develop->
# develop). Only on opened/reopened so the disable job stays sticky.
# Codegen PRs from this repo.
# Head and base pairing is enforced strictly, codegen-main to main and codegen-develop to develop.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'ptr727-codegen[bot]' &&
Expand Down Expand Up @@ -101,8 +102,9 @@ jobs:
merge-upstream-version:
name: Merge upstream version pull request job
runs-on: ubuntu-latest
# Upstream-version bump PRs from the App. Head/base pairing is enforced (upstream-version-main->main,
# upstream-version-develop->develop). Only on opened/reopened so the disable job stays sticky.
# Upstream-version bump PRs from the App.
# Head and base pairing is enforced, upstream-version-main to main and upstream-version-develop to develop.
# Only on opened or reopened, so the disable job stays sticky.
if: >-
(github.event.action == 'opened' || github.event.action == 'reopened') &&
github.event.pull_request.user.login == 'ptr727-codegen[bot]' &&
Expand Down Expand Up @@ -143,8 +145,9 @@ jobs:
disable-auto-merge-on-maintainer-push:
name: Disable auto-merge on maintainer push job
runs-on: ubuntu-latest
# Fires when a maintainer pushes to a bot's branch (synchronize, actor != bot). Disables auto-merge so the
# maintainer's commits don't merge with the bot's, and they re-enable it manually. The disable call is idempotent.
# Fires when a maintainer pushes to a bot's branch, meaning a synchronize whose actor is not the bot.
# It disables auto-merge so the maintainer's commits do not merge with the bot's, and the maintainer re-enables it manually.
# The disable call is idempotent.
if: >-
github.event.action == 'synchronize' &&
github.event.pull_request.head.repo.full_name == github.repository &&
Expand Down
13 changes: 7 additions & 6 deletions .github/workflows/publish-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ name: Publish project release action
on:
workflow_dispatch:

# A publish is a deliberate dispatch, so runs serialize on one group and queue rather than cancel, so a run is
# never left with a half-created GitHub release.
# A publish is a deliberate dispatch, so runs serialize on one group and queue rather than cancel.
# That leaves no run with a half-created GitHub release.
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: false
Expand All @@ -18,8 +18,9 @@ jobs:
permissions:
contents: read

# Publish the dispatched branch (main => release, develop => prerelease): NBGV computes the tag from the ref, then
# a GitHub release is created (tag + auto source archive + README + LICENSE). Source-only repo - no build targets.
# Publish the dispatched branch, where main gives a release and develop a prerelease.
# NBGV computes the tag from the ref, then a GitHub release is created carrying the tag, the auto source archive, README and LICENSE.
# This repo is source-only, so it has no build targets.
publish:
name: Publish project release job
runs-on: ubuntu-latest
Expand Down Expand Up @@ -56,8 +57,8 @@ jobs:

# Create-or-refresh: every trigger here is a dispatch, so an existing tag is refreshed, never skipped
# (the exists-gate belongs to the multi-trigger reusable form, where a scheduled re-run must no-op).
# target_commitish pins the tag to the exact built commit (GitCommitId), not the default branch. The release is
# the tag plus GitHub's auto source archive, README, and LICENSE - no build assets (source-only).
# The target_commitish input pins the tag to the exact built commit, GitCommitId, rather than to the default branch.
# The release is the tag plus GitHub's auto source archive, README and LICENSE, carrying no build assets because the repo is source-only.
- name: Create GitHub release step
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
with:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/test-pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ jobs:
permissions:
contents: read

# GitHub Actions does not support required status checks on conditional jobs, so a single always-run aggregator gates
# the merge. Its name is the ruleset-bound required status-check context - rename it and the ruleset context together.
# GitHub Actions does not support required status checks on conditional jobs, so a single always-run aggregator gates the merge.
# Its name is the ruleset-bound required status-check context, so rename it and the ruleset context together.
check-workflow-status:
name: Check pull request workflow status job
runs-on: ubuntu-latest
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/validate-task.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,13 @@ jobs:
- name: Check repo gates step
run: python3 scripts/repo_gate.py

# The charset, duplicate-word and spelling rules are clean tree-wide, so they gate.
# The charset, duplicate-word, spelling and comment rules are clean tree-wide, so they gate.
# Every other prose rule reports in the step below without gating.
- name: Check prose step
run: python3 scripts/prose_lint.py . --check charset --check dupword --check spelling
run: python3 scripts/prose_lint.py . --check charset --check dupword --check spelling --check comment-wrap --check comment-case

# Warn-only, and visible rather than absent: an unrun check is one nobody acts on.
# The backlog is corrected as each file is next edited, never swept.
# The backlog is corrected as each file is next edited, or cleared in a deliberate batch.
- name: Report prose backlog step
continue-on-error: true
run: python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary
run: python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --summary
10 changes: 5 additions & 5 deletions OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ What verifying a change here requires, including the part CI cannot perform. The

### Run the gates the way CI runs them

CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists together. What differs is the exit code rather than the coverage: CI gates on `charset`, `dupword` and `spelling` and reports the other five warn-only, where a bare run exits non-zero on any of the eight. `sentence-split` is in neither and is asked for by name. Run the CI invocations:
CI passes explicit `--check` lists, and a bare `python3 scripts/prose_lint.py [file]` runs `DEFAULT_RULES`, which is those two lists plus `home-path`. What differs is the exit code rather than the coverage: CI gates on `charset`, `dupword`, `spelling`, `comment-wrap` and `comment-case` and reports the other three warn-only, where a bare run exits non-zero on any of the nine. `sentence-split` is in neither and is asked for by name. Run the CI invocations:

```sh
python3 scripts/test_prose_lint.py
Expand All @@ -17,18 +17,18 @@ python3 scripts/test_pr_review.py
python3 spec/audit.py --selftest
python3 host-setup/agent-safety/gh-write-guard.py --selftest
python3 scripts/repo_gate.py
python3 scripts/prose_lint.py . --check charset --check dupword --check spelling
python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --check comment-wrap --check comment-case --summary
python3 scripts/prose_lint.py . --check charset --check dupword --check spelling --check comment-wrap --check comment-case
python3 scripts/prose_lint.py . --check charset-unknown --check semicolon --check dash --summary
for f in registry/*.json spec/*.json repo-config/*.json; do jq empty "$f"; done
python3 spec/validate.py
docker run --rm --pull=always -v "$PWD":/check --workdir /check mstruebing/editorconfig-checker:latest
```

Two gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it.
Three gaps in that list are CI's rather than this runbook's, reproduced here so a local run matches CI rather than quietly exceeding it. The `jq` glob covers `repo-config/*.json` and does not reach `repo-config/operational/develop.json`, so a malformed operational payload passes. And `sentence-split` is implemented and tested but named by no invocation, so nothing runs it. The third is `home-path`, which is in `DEFAULT_RULES` and so runs on every bare local run, yet is named by neither CI list, so the pattern-detectable half of the representative-data rule gates nothing in CI. It is clean tree-wide today, which is why the gap is a hole rather than a backlog.

Run the `editorconfig-checker` line before pushing any new file. This repository defaults to CRLF, most tooling writes LF, and a new file therefore fails that check on its first CI run rather than locally.

The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem.
The first prose invocation gates. The second reports the backlog that is corrected as each file is next edited, or cleared in a deliberate batch, and it exits non-zero locally whenever findings exist. It is warn-only in CI because the workflow step sets `continue-on-error: true`, not because the command is lenient, so a non-zero exit locally is the expected result rather than a problem.

Scope a run to what changed, which matches the correct-as-next-edited rule:

Expand Down
13 changes: 10 additions & 3 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,16 @@ One pull request clearing the prose findings the hub's own docs and spec still c
- **Clear the remaining [#519][issue-519] prose backlog, outside the snippets.** The whole-tree figure moves as readily with a fix to the gate as with a fix to the prose, so it is re-measured rather than quoted.
- **Blocked by** - Nothing.
- **Issue** - [#519][issue-519], whose headline numbers are stale and whose four planned changes are two-thirds landed.
- **Checked** - `develop` at `a6d7a4b` on 2026-08-07, where `python3 scripts/prose_lint.py --summary` reports 373 violations across 26 files, and `catalog/snippets` reports 0. The snippets sweep took the tree from 557 across 45, of which 184 across 19 were snippets.
- **Checked** - `develop` at `c64e3e0` on 2026-08-07, where `python3 scripts/prose_lint.py --summary` reported 373 violations across 26 files and `catalog/snippets` reported 0. The 131 across 15 the comment batch leaves is a figure on the branch carrying that batch rather than on this anchor, and it becomes the anchor's own number when that branch merges. The snippets sweep took the tree from 557 across 45, of which 184 across 19 were snippets.
- **Open** - Whether the gate becomes a carried file rather than a hub-only one, which "Reducing the Carried Surface Further" asks from the other direction.
- **Settled** - `comment-wrap` and `comment-case` are in `DEFAULT_RULES` and `reports/` is exempt as a generated tree, which is why the figures differ from the 668 and 119 the issue records.
- **Settled** - `sentence-split` is defined but excluded from `DEFAULT_RULES`, so a sweep never reports it and a wrapped sentence in Markdown prose is not a finding.
- **Settled** - The three largest files are not snippets and are Python comments rather than prose, being [`spec/audit.py`][audit] at 99, [`gh-write-guard.py`][write-guard] at 52, and [`spec/validate.py`][validate] at 41 when measured before the sweep.
- **Settled** - A comment opening on a lowercase identifier is the bulk of what `comment-case` still reports, and the rule intends those restructured rather than exempted. The exemptions the snippets sweep added cover a commented-out key and a definition label, and nothing wider.
- **Settled** - The backlog splits by surface rather than by file, since `comment-wrap` and `comment-case` lived entirely in non-Markdown comments while `dash` and `semicolon` live entirely in Markdown prose. That split is what made the comment half one reviewable batch, and it is the batch boundary the remainder inherits.
- **Settled** - The comment batch cleared 241 findings across 11 files and moved both rules to the gating CI step, since a rule swept clean but left warn-only regresses on the next edit with nothing reporting it. The three largest files were [`spec/audit.py`][audit] at 99, [`gh-write-guard.py`][write-guard] at 52, and [`spec/validate.py`][validate] at 41, all Python comments rather than prose.
- **Settled** - An ellipsis read as a sentence terminator, so `RUN_ON` reported one schematic comment line as two sentences and the split it asked for would have broken the fragment the line exists to show. The guard is that a dot preceded by a dot never terminates, fixed and tested before any prose moved, and the verdict diff in both directions was that one finding and nothing else.
- **Settled** - A comment opening on a lowercase identifier was the bulk of what `comment-case` reported, and the rule intends those restructured rather than exempted, which is what the batch did rather than widening any exemption.
- **Settled** - The remainder is prose in Markdown, and six of the fifteen files are carried, being [`GOVERNANCE.md`][governance] at 14, [`WORKFLOW.md`][workflow] at 14, [`CODESTYLE.md`][codestyle] at 6, [`.github/copilot-instructions.md`][copilot-instructions] at 4, `repo-config/README.md` at 2 and [`HISTORY.md`][history] at 1. So the next batch splits again at that line, since the carried half rewrites byte-locked sections and owes a re-vendor while the hub-only half owes nothing.
- **Settled** - `home-path` is in `DEFAULT_RULES` and is named by neither CI list, so the pattern-detectable half of the representative-data rule runs on every bare local run and gates nothing in CI. It is clean tree-wide, so this is a hole rather than a backlog, and it is recorded in [`OPERATIONS.md`][operations] beside the two gaps already named there.

### Giving the Fleet's Own Pins Something to Resolve Against

Expand Down Expand Up @@ -405,6 +409,7 @@ Regenerate [reports/divergences.md][divergences-report] before using it as the w
- **Issue** - [#365][issue-365] and [#483][issue-483].
- **Rides with** - Nothing on the hub, since the write-guard newline fix has landed on `develop` and a machine keeps running the old hook until the installer is re-run there.
- **Detail** - A ticked row means the host-wide rules text and not the hook, since only running the installer deploys both layers, and the proxmox host proved that distinction by carrying the documentary half alone for eight days on the machine where the incident originated.
- **Detail** - The prose comment batch rewrote comments in [`gh-write-guard.py`][write-guard] and both installer wrappers, so every installed copy is now behind the hub by that much. The divergence is comment-only and changes no decision the hook takes, which the self-test confirms, so it is a re-run of the installer at the next visit rather than a correctness problem.
- **Detail** - Honor the issue's own rule when filling a cell, that an unverified install command is worse than a blank, because a blank prompts a question while a wrong command produces a broken host and a false sense that setup succeeded.
- **Detail** - The superseded safety section from [#364][issue-364] still sits above the canonical block in this host's rules file, so the two overlap. Removing it is a judgment call on a per-machine file, which is why it is surfaced rather than applied.

Expand Down Expand Up @@ -468,6 +473,7 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i
[divergences-report]: ./reports/divergences.md
[files]: ./spec/files.json
[governance]: ./GOVERNANCE.md
[history]: ./HISTORY.md
[markdownlint]: ./.markdownlint-cli2.jsonc
[matrix]: ./reports/conformance-matrix.md
[merge-bot]: ./.github/workflows/merge-bot-pull-request.yml
Expand All @@ -486,5 +492,6 @@ Each was checked against the tree and has nothing left to do anywhere. Closing i
[standup]: ./STANDUP.md
[type-model]: ./spec/type-model.md
[validate]: ./spec/validate.py
[workflow]: ./WORKFLOW.md
[workflows]: ./catalog/snippets/workflows/
[write-guard]: ./host-setup/agent-safety/gh-write-guard.py
Loading
Loading