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
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,27 @@ directory named in the PR body or the CHANGELOG still points at a superseded tre
— and it is the PR body that the tag-on-merge workflow writes to the CHANGELOG
file at merge time.

**Scope — block on what gets TAGGED, report what gets squashed away.** The
**Scope — block on what SURVIVES the merge, report what gets squashed away.** The
CHANGELOG entry (written from the PR body at merge) and the PR body are both
durable: the entry lands in the tagged
tree, and a merged PR body stays public and quotable long after the branch is
durable: the entry lands on `main` in the tag-on-merge dispatcher's OWN follow-up
commit, and a merged PR body stays public and quotable long after the branch is
deleted. A superseded run directory in either is BLOCKING. An intermediate commit
message is replaced by the squash merge, so a stale citation there
is reportable but not blocking. The general form, which outlives the specific
case: **a claim-keyed sweep runs LAST, once the final tree exists** — a record
written alongside the change it describes is written before the tree its reader
will check it against.

**The entry is durable, but it is NOT in the tagged tree.** The org's
`tag-on-merge` workflow creates the tag AT the merge commit and only THEN writes
the entry, so the two land in DIFFERENT commits: the tag names the merge, the
entry arrives in the commit after it. MEASURED on two repos — opencharly/docs'
`v2026.298.0740` points at the merge commit `dd289db1`, where
`CHANGELOG/2026.298.0740.md` is ABSENT and the entry arrives in the next commit
`e5f90075`; opencharly/opencharly's `v2026.298.0430` points at `4ea0846`, the
entry arriving in `71350f0`. A reader looking for the entry IN the tagged tree
does not find it, and a rule that says the entry lands there sends them looking.

**Before merging a superproject leg, verify every gitlink is an ANCESTOR of its own repo's
`origin/main` — not merely that it matches a local checkout.** Those are different questions and
only the first discriminates:
Expand Down Expand Up @@ -279,7 +289,7 @@ rulebook's "never by hook regexes".
above is PROSE an agent self-enforces, and it is NOT executed when a session is
auto-closed at the validator's block threshold, when a fix lands on a NEW
branch/PR superseding an abandoned one, or when a session simply ends — so
worktrees and branches accumulate without bound (measured: 108 worktrees and
worktrees and branches accumulate without bound (measured 2026-10-03: 108 worktrees and
~900 local branches on one umbrella checkout). `charly task prune` (umbrella
`verb:prune`, plugin-task) is the ONE mechanical control that contract lacked: it
collects merged-upstream session worktrees + branches across the umbrella root
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,26 @@ both body-only, and neither covered by classes 1-5.
10. List every issue R1 filed or updated under `## Filed issues`
(`<repo>#<n> — <one line>`), and post `STATUS` on your claimed thread
(the project rulebook Part II #11).
11. **Read the PUSHED RANGE's trailers — the body's footer is not the branch's
commits.** The linter below reads the BODY's last line, and no control reads
the commits the branch is about to push, so a trailer-less commit can sit
below the head while every control stays green. Measured 2026-10-23: the
umbrella PR `opencharly/opencharly#591`'s branch carried three published
merge commits with no `Assisted-by:` trailer — one per review round's
catch-up merge — and the gap surfaced only when the history was re-read by
hand. The range is the PR's BASE branch's head to this branch's head —
`origin/<base>..HEAD`, where `<base>` is the branch this PR merges INTO
(`origin/main` in the common case) — so it covers every commit the branch
contributes, INCLUDING the catch-up merges it pulls in from the base. Read
it with
`git rev-list origin/<base>..HEAD | while read -r c; do git log -1 --format='%B' "$c" | grep -q '^Assisted-by:' || git log -1 --format='%H %p%n%s%n%b' "$c"; done`
— each commit it names owes the trailer, unless it is a merge whose message
you did not write; the rule and that one exemption are in the org rulebook's
*AI Attribution* section. A commit you wrote that still owes the trailer
carries it BEFORE the push: amend it while it is local, and never rewrite a
published commit — a published gap is reported on the issue, not rewritten
(R6). **Run it over YOUR OWN head and over THAT range:** a range you chose
yourself proves a different range's commits, not the ones you are pushing.

## The mechanical half — `scripts/pr_body_lint.py`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ umbrella root; `charly task list` enumerates them, and the umbrella README's
| `charly task verify` | CONSISTENCY — the FULL pinning audit on demand: policy B, then every checkout clean and at its recorded gitlink, then the per-submodule `git ls-remote` default-branch audit. There is **no CI gate**; this IS the gate. Run it on the final tree and paste the output (R7: a green `git status` proves nothing). **The checkout half covers the MATERIALIZED paths only** — an unmaterialized path is *uninitialized (pin audited, no checkout)*, a notice, not a failure |
| `charly task org-map` | reconcile the README org-map TABLES against `.gitmodules`. It reads ONLY those two surfaces, so it is NOT an org-vs-umbrella census — see "`org-map` answers the wrong question" below |
| `charly task skills` | `scripts/sync-dispatcher.sh`: splice the generated R0 dispatcher into `AGENTS.md` (no-op while the table is hand-curated outside the markers) |
| `charly task pin-check` | assert the pin contract's cheap half — every `distro-*` gitlink equals charly's own `box/*` gitlink. The cheap, commit-violable half of `verify`; this is what `hooks/pre-commit` runs |
| `charly task pin-check` | assert the pin contract's consistency half — the EDGE (every `pin_map` entry equals its twin in the `pinned_from` checkout) and the CHECKOUT AUDIT (every declared path clean and at its recorded gitlink). It READS the index's gitlinks, the MANIFEST `charly.yml`'s `pins:` block and the DECLARED PATH SET `.gitmodules`, so a commit can violate it by moving a gitlink **or** by editing the manifest **or** by editing the declared path set — a commit that moves NO gitlink can still violate it. It is the half of `verify` that needs no remote (the per-submodule `git ls-remote` default-branch audit is the rest), and it is what `hooks/pre-commit` runs. Checkout DRIFT on its own — a checkout dirty or at another commit, caused by NO commit — stays with `charly task verify` |
| `charly task pins` | the Policy-B pin operation over the `distro-*` set (mode via param) |
| `charly task self-test` | self-test the committed CI body/pin-evidence builders + the dispatcher splice + the submodule initializer |
| `charly task prune` | reap merged-upstream session worktrees + branches. The declared `MODE` DEFAULTS to `report`, so **plain `charly task prune` IS the dry run**; a positional `MODE=report` is rejected (`unexpected extra argument`), and the removal is selected through the task's own param (`--param MODE=prune`) |
Expand Down Expand Up @@ -102,7 +102,7 @@ another is how a green gate gets mistaken for "everything is current":
| question | the verb that answers it | what a green answer proves |
|---|---|---|
| **STATE** — where is each submodule, on which branch, at which pin? | `charly task map` | a listing, nothing about correctness |
| **CONSISTENCY** — does each checkout match the gitlink the umbrella records, and does policy B hold? | `charly task verify`, and its commit-time half `charly task pin-check` | that the tree is internally COHERENT — **not that it is current** |
| **CONSISTENCY** — does each checkout match the gitlink the umbrella records, and does policy B hold? | `charly task verify`, and `charly task pin-check` — the same edge and checkout audit without the remote-branch audit, and the half `hooks/pre-commit` runs | that the tree is internally COHERENT — **not that it is current** |
| **CURRENCY** — is each recorded pin the value its intent names? | the `currency` mode of the ONE pin verb (`verb: git-submodules`), reached through the umbrella's own `charly task pins --param MODE=currency` | that every pin is at the ref its intent names, or the exact leg order that would get it there — the contract is in [`references/pin-intent-and-currency.md`](/recipes/internals/git-workflow/pin-intent-and-currency/) |

**`currency` is IN A RELEASED BINARY, and the umbrella's own task surface exposes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,15 +224,24 @@ chronologically under a plain alphanumeric sort.
the author still ships the `migrations.cue` entry in the same change (the
table is version-free — there is no `#SchemaVersion` to bump) — but the final CalVer for tag and changelog filename is minted
at merge, never by the author.
- **tag-on-merge, at merge:** `VER=$(date -u +%Y.%j.%H%M)` (guard uniqueness — if
`v$VER` or `CHANGELOG/$VER.md` already exists on the current `main`, advance to
the next free minute); after the squash merge, write `CHANGELOG/$VER.md` on
`main` from the merged PR's title + body (a bot token — GitHub App preferred,
PAT fallback — authorizes the protected-main write; the App is the ruleset
bypass actor for the single CHANGELOG path); then tag the merged HEAD —
`git tag -a v$VER -m "<subject>" <merged-HEAD>` and
`git push origin refs/tags/v$VER` (every repo;
`sdk`, `spec`, and `plugin-gh` substitute their Go-module `v0.<…>` form). A schema cutover
- **tag-on-merge, at merge:** `VER` is minted from the MERGE COMMIT's committer
time (`date -u +%Y.%j.%H%M`; guard uniqueness — if `v$VER` or `CHANGELOG/$VER.md`
already exists on the current `main`, advance to the next free minute). The
workflow then acts in this order, and the order is the whole point: **(1) it
creates the tag AT THE MERGE COMMIT** — `refs/tags/v$VER` → `<merge-commit-sha>`
(idempotent, and INDEPENDENT of the CHANGELOG, which is why a tag can exist with
no entry beside it); **(2) it writes `CHANGELOG/$VER.md` on `main` from the merged
PR's title + body** (a bot token — GitHub App preferred, PAT fallback —
authorizes the protected-main write; the App is the ruleset bypass actor for the
single CHANGELOG path), which lands as the dispatcher's OWN follow-up commit
`docs: add CHANGELOG $VER for PR #<n>`. **So the tag does NOT contain the
entry**: the tagged tree is the merge commit, and the entry arrives in the commit
after it. MEASURED — opencharly/docs' `v2026.298.0740` → `dd289db1` (the entry
is absent from that tree; it arrives in `e5f90075`), and opencharly/opencharly's
`v2026.298.0430` → `4ea0846` (the entry arrives in `71350f0`). The entry is
still durable — it is on `main` — but a reader must look for it there, not in
the tagged tree. (every repo; `sdk`, `spec`, and `plugin-gh` substitute their
Go-module `v0.<…>` form). A schema cutover
ships its `migrations.cue` entry in the change that alters the schema; there is
no `#SchemaVersion` / `version:` re-stamp.

Expand Down
Loading