diff --git a/marketplace b/marketplace index d0d55ccc..835d0998 160000 --- a/marketplace +++ b/marketplace @@ -1 +1 @@ -Subproject commit d0d55ccc77326baaf18b8d2254548ce06cb4dfbf +Subproject commit 835d099845cd51ac90f6677179d750fbb384fc83 diff --git a/src/content/docs/recipes/internals/git-workflow/branch-and-pr-loop.md b/src/content/docs/recipes/internals/git-workflow/branch-and-pr-loop.md index 75504b4b..4f21ed7f 100644 --- a/src/content/docs/recipes/internals/git-workflow/branch-and-pr-loop.md +++ b/src/content/docs/recipes/internals/git-workflow/branch-and-pr-loop.md @@ -113,10 +113,10 @@ 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 @@ -124,6 +124,16 @@ 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: @@ -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 diff --git a/src/content/docs/recipes/internals/git-workflow/pre-validator-self-audit.md b/src/content/docs/recipes/internals/git-workflow/pre-validator-self-audit.md index 0a21891f..73272c65 100644 --- a/src/content/docs/recipes/internals/git-workflow/pre-validator-self-audit.md +++ b/src/content/docs/recipes/internals/git-workflow/pre-validator-self-audit.md @@ -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` (`# — `), 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/..HEAD`, where `` 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/..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` diff --git a/src/content/docs/recipes/internals/git-workflow/umbrella-mechanics.md b/src/content/docs/recipes/internals/git-workflow/umbrella-mechanics.md index 6bad3a30..3dc740db 100644 --- a/src/content/docs/recipes/internals/git-workflow/umbrella-mechanics.md +++ b/src/content/docs/recipes/internals/git-workflow/umbrella-mechanics.md @@ -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`) | @@ -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 diff --git a/src/content/docs/recipes/internals/git-workflow/validator-and-calver.md b/src/content/docs/recipes/internals/git-workflow/validator-and-calver.md index 035b7ec8..2a81b5af 100644 --- a/src/content/docs/recipes/internals/git-workflow/validator-and-calver.md +++ b/src/content/docs/recipes/internals/git-workflow/validator-and-calver.md @@ -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 "" ` 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` → `` + (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 #`. **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.