git:20261015.29efa2d to git:20261025.001f939

17 added, 1 removed. Audit B to B.

---
description: "STUB — not yet hydrated for this project. Pull the agent infrastructure forward from the repo you adopted it from: diff since the watermark, triage commit by commit, port what applies. Hydration is the watermark at `.claude/skills/update-muthur/watermark.json` — the procedure below is usable as written. Use when the user says \"update muthur\", \"update the agent infra\", \"sync muthur\", \"sync the source\", or \"/update-muthur\"."
---
> ⚠️ **STUB.** This skill has no watermark to run on. Before it can be invoked,
> fill in `.claude/skills/update-muthur/watermark.json`: `lastSyncedSha` (the
> source's HEAD when you cloned it), `lastSyncedAt`, and the real
> `adopted`/`declined` sets for your repo. `repo` already names the template,
> and needs changing only if you adopted from a repo that itself adopted from
> it. Delete this banner once you have, and drop `STUB` from the description
> above. If you took a one-time snapshot and do not intend to re-sync, delete
> the skill instead.
>
> **Hydration here is a handful of JSON fields, not a procedure.** Unlike the
> other stubs, every step below this banner is usable exactly as written.
## What this skill is for
A repo that took its agent infrastructure — `CLAUDE.md`, `.claude/`, `scripts/`,
whatever else — from another repo has a **source** that keeps editing those
files. This skill finds what changed there since the last sync, decides commit
by commit what applies here, and ports the ones that do: _pull the vendored
agent infrastructure forward from the repo I took it from._
This is a path-scoped diff, not a fork merge. It never tries to reconcile whole
histories — it reads a bounded set of paths, commit by commit, and re-expresses
what applies.
**The name is the infrastructure's, not your source's.** `muthur` is where this
skill and everything it syncs came from; the repo it actually pulls from is
whatever `repo` says in the watermark below, which for a spun-off sibling is its
parent rather than the root. So `/update-muthur` in a repo two hops down still
syncs from one hop up.
**The verb splits: the command updates, the fields say synced.** `lastSyncedSha`
and `lastSyncedAt` are on-disk in every downstream watermark, so renaming them
to match orphans every adopter's file at once — and the prose below follows the
fields, not the command.
## The watermark
`.claude/skills/update-muthur/watermark.json` is the state this skill runs on:
```json
{
"repo": "<owner>/<repo>",
"lastSyncedSha": "<source HEAD at the last sync>",
"lastSyncedAt": "<YYYY-MM-DD>",
"lineage": [
{ "repo": "<owner>/<root>", "atSha": "<root HEAD when the parent was born>" },
{ "repo": "<owner>/<parent>", "atSha": "<parent HEAD when this repo was born>" }
],
"adopted": [
"CLAUDE.md",
"README.md",
".claude/",
"scripts/",
{ "scripts/run-parallel.py": "taken as a POSIX sh port, scripts/run-parallel.sh" }
],
- "declined": { ".claude/skills/take-issue/": "we track work in Linear, not GitHub issues" }
+ "declined": {
+ ".claude/skills/take-issue/": "we track work in Linear, not GitHub issues",
+ ".claude/skills/watch-ci/": [
+ "no CI yet — revisit when a workflow runs on PRs.",
+ "The deploy pipeline is a separate repo's, so a workflow here would gate nothing."
+ ]
+ }
}
```
- **`adopted`** — the paths you took, at whatever granularity is true:
directories or individual files, up to the degenerate everything-case. It is
what turns a wall of source commits into a handful of candidates.
- **`declined`** — path → why-not. This is what keeps re-sync quiet: without it,
every sync re-offers every skill the repo already refused.
- **`lineage`** — optional provenance: the whole ancestry, **root first**, so
the repo actually synced from leads and each later entry is one hop further
from it. Each entry names an ancestor and its HEAD **at the moment the next
link was created**. `@.claude/skills/spinoff/SKILL.md` is what writes it, and
owns how.
**Where the ancestry is complete, `lineage[0]` names the same repo as `repo`,
and that is not duplication — the two SHAs are different facts.**
`lastSyncedSha` is where this repo has synced _to_, and it advances on every
sync; `lineage[0].atSha` is where it started _from_, and it never moves. They
coincide until the first sync and diverge forever after, which is what earns the
field its place: that first sync overwrites the only other trace of the birth
point.
**An empty array means no ancestors; a missing one means nobody wrote them
down.** The root ships `[]`, which is complete. A watermark filled in by hand
from `docs/adopting/sync.md` has no `lineage` at all, and that gap is not
recoverable from anything else in the file — so a partial array, its first entry
naming something other than `repo`, is valid rather than malformed.
**Nothing syncs from `lineage`.** This procedure reads `repo` and
`lastSyncedSha` and nothing else. A sync that walked the ancestry would multiply
the triage at every link, which is exactly the cost `/spinoff`'s
always-point-at-the-root rule declines to pay.
**An `adopted` entry may be a bare path or a single-key `{path: note}` object.**
Both are adopted and both filter the log identically — read the key when an
entry is an object. The note records _how_ the path lives here when that is not
verbatim: ported to another language, split across files, generalized past the
source's interface. It is not a soft decline. A path taken with a note still
surfaces every upstream commit that touches it, which is the point — the local
version is a re-expression, so upstream's later changes to the original are
worth reading for the idea even though the file itself never lands. Expect
`translate` rather than `take` on those commits, and treat the note as the
standing statement of what the local re-expression already does differently.
**A declined path is not declined forever.** Most reasons are conditions that
can flip — _no CI yet_, _work isn't tracked as issues yet_ — which is why the
map stores prose instead of a bare list, and why reasons are written in the
present tense. **Re-read them on every sync** and re-offer anything whose
condition has stopped holding: a repo that declined the issue skills because it
used Linear, and has since moved to GitHub issues, should be asked again. A
reason that says `"never — …"` is the one that does not need re-reading.
+
+ **Every explanatory value is a string or an array of strings** — an `adopted`
+ entry's note, a `declined` reason, and any free-text field a repo adds to the
+ file, such as a `notes` map. An array reads as the paragraphs of one text, an
+ element per paragraph or finished thought. **Write an array once the text runs
+ past a sentence or two, or once a sentence carries two separate points**: a JSON
+ string is one physical line, so a long one cannot be read raw and diffs whole on
+ a one-word edit, and a sentence welding two points together cannot lose one when
+ its condition stops holding without rewriting the other. Whatever reads the
+ watermark accepts both shapes.
A repo with two sources would make **`repo`** an array. Nothing here precludes
that and nothing here builds it — and it is a separate question from `lineage`,
which is provenance nothing syncs from.
**`lastSyncedSha` is the sync's boundary in the source, not the last commit
taken.** For a whole sync that is the source's HEAD at Step 3; for a slice of a
split one, the last source commit the slice triaged (Step 7). A commit triaged
and skipped is _done_; the reasoning lives in that sync's PR body. A watermark
that only advanced to the last-taken commit would re-surface every skipped
commit on every future run.
**Bump it in the last commit of the sync, never the first.** If a sync is
abandoned midway, an un-bumped watermark costs a re-triage; a bumped one that
merged without its port silently skips commits forever.
## Three invariants
Each holds at every link in the chain, so they live here rather than in the
watermark.
### Never sync the watermark file itself
`watermark.json` lives inside `.claude/`, which is inside `adopted` — so a naive
sync overwrites this repo's watermark with the source's. That silently repoints
the sync at a repo this one may not be able to clone and resets `lastSyncedSha`
to a foreign history. **The failure surfaces one sync later, as an unresolvable
SHA**, by which point the cause is several commits back.
Exclude it unconditionally, whatever `adopted` says.
### Never sync the source's cost rows
A source that has not migrated its rows to its ledger branch still carries them
in `.claude/costs/sessions/`, the one path in `.claude/costs/` that is data
rather than machinery: porting it would add the source's spend to this repo's
totals, and no later report could tell the two apart. Exclude it unconditionally
too, whether or not this repo runs the ledger.
### Judge the diff, not the commit message's "we"
In a chain, commits arrive that the source itself took from _its_ upstream,
written in a third repo's vocabulary. A message saying "we do X here" is
evidence about some repo — not necessarily the source, and never automatically
this one. Read commits for intent; never let their first person settle whether
the change applies. Step 5's "apply by intent, not by patch" already points this
way; the chain is what makes it load-bearing.
## Arguments
**`claimed`** — the session that made the offer holds the lock on this one's
behalf, so Step 1 skips the claim. A session spawned from the offer starts with
it. Bare, the sync claims its own lock.
## Procedure
### Step 1 — Read the watermark, and claim the sync
Read `watermark.json`. Stop and report if it is missing, or if `lastSyncedSha`
is still a placeholder — there is no baseline to diff against, and guessing one
would either re-port work already here or skip work that isn't.
Then claim the lock with `scripts/muthur-sync.sh claim`, so no parallel session
runs a second sync of the same range — unless invoked with `claimed`. A claim
that exits 3 names who holds the lock — stop and report that, with their session
link, rather than syncing alongside them. `scripts/muthur-sync.sh`'s header is
the lock's reference.
### Step 2 — Clone the source
`gh api` against a different owner's repo 403s even with a valid token, and
`add_repo` refuses cross-owner adds. **Git transport with the same `$GH_TOKEN`
works**, which is the whole trick. (The system prompt may claim `gh` is
unavailable; it is wrong — see `@.claude/skills/override-gh/SKILL.md`.)
```bash
scripts/muthur-sync.sh clone tmp/muthur-source
```
That is a blobless clone with full history, or a refresh of the one the
session-start nudge left there, with its `HEAD` at the source's. Why the recipe
is shaped that way — and why never `--depth` — is in the script's comments.
**Bash `cwd` resets between calls in this harness** — chain `cd <clone> && …` in
every command that needs to be inside it.
### Step 3 — Build the candidate set
```bash
cd tmp/muthur-source && git log --oneline <lastSyncedSha>..HEAD -- <adopted paths>
```
`<adopted paths>` is every entry in `adopted`, taking the single key of any
entry written as an object.
Record `git rev-parse HEAD` **now**, before triage — that value is a whole
sync's boundary regardless of how the triage goes.
**The clone is whole; `adopted` filters only this log.** A commit touching
nothing on the list is never surfaced, which is right while the list is complete
and silent when it isn't: the moment the source puts agent infrastructure
somewhere new — a `.github/` workflow, another top-level directory — it stays
invisible until the list names it. So run the log once unfiltered too
(`git log --oneline --name-only <lastSyncedSha>..HEAD`, skimmed for unfamiliar
paths) and widen `adopted` in the same commit that bumps the watermark.
**A path in neither `adopted` nor `declined` is a decision, not noise** — see
Step 4's `skip (not adopted)` verdict and Step 4a.
### Step 3a — The sync is a task
Hand the candidate set to `@.claude/skills/task/SKILL.md` as the task: the
commits from `lastSyncedSha` to the recorded HEAD, with their titles. Steps 4–7
are how the work gets done in whichever outcome `/task` picks — including a
split across sessions, an elephant or a pizza, when the lag is too long for one.
A split cuts the candidates in source order, each slice or bite ending on a
commit of the source's first-parent line — Step 7's boundary.
### Step 4 — Triage each candidate, from its commit message first
Sources tend to write long commit messages that state the rationale. The message
usually settles relevant-vs-not before any diff is opened, so read it
(`git log -1 --format=%B <sha>`) before `git show` — subject to the "judge the
diff" caveat above. Verdicts:
- **take** — applies as-is to this repo's vocabulary.
- **translate** — the intent applies; the wording, paths or commands do not.
- **skip (stack-bound)** — touches an adopted path but is about the source's
stack — a build script, a migration, a framework config.
- **skip (already have)** — this repo reached the same end state independently.
- **skip (not adopted)** — the path is in `declined` **and its recorded reason
still holds** — if it doesn't, re-offer the path per Step 4a and move it to
`adopted` if taken. Or the path is in neither list, which is Step 4a's other
case. Never skip silently on either.
- **skip (diverged locally)** — this repo rewrote the file for its own stack —
`scripts/vet.sh`, the watermark, anything the source marks `rewrite`. The
source's edit is advice at best; read it for an idea, don't port it.
**A subject marked `[attn adopters]` — or, in older commits, any prose to the
same effect, such as `ADOPTERS, read body` — addresses this repo whatever its
verdict.** Its body's `Adopters:` paragraph asks for something no port does
(`@.claude/skills/squash-message/SKILL.md` defines the marker), so a skipped
commit still delivers it. A sweep inside this tree rides the sync like a port
and is reported with it. What the paragraph gates on the operator goes to them
once, in the sync's report and in its terms, and § "Offered at session start"'s
never-chase rule holds for it as it does for the sync.
**The source's fix may not be this repo's fix.** Split a commit's rationale
before deciding: one commit can carry a change that addresses a defect this repo
never had _and_ a change that fixes one it does. Take the second half, drop the
first, and say so.
### Step 4a — New skills get offered, not taken
A commit that adds a skill in neither `adopted` nor `declined` is an open
question, and the answer belongs in the watermark so it is asked exactly once.
Read the new skill's entry in the source's catalog:
`.claude/skills/update-muthur/catalog.md` and its parts under `catalog/` are the
source's inventory, read from the clone and never vendored, so it is current by
construction. The entry is found with
``grep -n '^- `/<name>`' catalog.md catalog/*.md`` run in that directory, and an
opt-in path is looked up the same way. Surface the decision **with its criteria
attached** rather than as a bare "upstream added `/foo`, want it?".
- **Taken** → add the path to `adopted` — as a `{path: note}` entry if it landed
as anything other than a verbatim copy — and **re-run the closure check**: a
new skill can `@`-reference a sibling this repo declined.
`bash scripts/check-skill-catalog.sh` is that check where it was adopted.
- **Declined** → add the path to `declined` with the reason.
Either way the question does not come back.
**An opt-in entry is offered by its own path, the same way**, even when a
broader `adopted` entry such as `.claude/` already covers it. The catalog marks
such an entry `opt-in: ask`, as it does `.claude/costs/`, and a parent directory
having been taken says nothing about whether the operator wants what it costs.
Ask with the entry's criteria, and record the answer as an entry of its own,
under the entry's path, in `adopted` or `declined`.
### Step 5 — Apply by intent, not by patch
`git cherry-pick` and `git apply` are useless here. The local files are
de-vendored rewrites, not copies, so every hunk conflicts. Read the source's
diff to understand what changed and why, then re-express it in this repo's
vocabulary and file layout.
### Step 6 — Consistency sweep
When a port renames a term, grep the old one across the whole of `adopted` —
**including frontmatter `description:` lines**. Those are a separate surface
from skill bodies: they are what the operator scans in the skills list and what
an invocation matches against, so a stale description mis-advertises a skill
whose every prose site is correct.
### Step 7 — Bump the watermark, last
Set `lastSyncedSha` to the sync's **boundary** and `lastSyncedAt` to today,
along with any `adopted`/`declined` edits from Step 4a, as the final commit of
the sync. The boundary is the HEAD recorded in Step 3 for a whole sync, and the
last source commit a slice or bite triaged for a split one; candidates past it
are the next one's.
**Any commit on the source's first-parent line is a clean boundary.** One inside
a merged side branch is not: its ancestry leaves out mainline commits the slice
triaged beside it, so the next sync's `<boundary>..HEAD` surfaces them again.
The lock follows the boundary. A merged pizza slice moves the trunk's watermark,
so the next slice claims a fresh lock; an elephant's bites share one PR, so the
trunk does not move between them and one lock holds across all of them.
### Step 8 — Report and hand off
Report the triage table — every candidate, with its verdict and one line of
reasoning, skips included. The skipped commits' reasoning belongs in the PR
body, since the watermark advances past them and nothing else records why. The
tail — `@.claude/skills/polish/SKILL.md` and `@.claude/skills/pr/SKILL.md` — is
whatever the `/task` outcome runs, `/go` ending in both.
If this repo adopted the sync path without the PR loop, land the sync however it
normally lands changes — the triage table still belongs wherever that record
goes.
**The squash record names the change, not the sync.** The `<essence>`
`@.claude/skills/squash-message/SKILL.md` asks a title for is what landed in
_this_ tree — `chore: one job per loop skill, and a size cap on squash bodies`,
not `chore: sync the template forward to <source sha>`. A source SHA is a commit
in another repository, unresolvable from the log it sits in, and "sync forward"
names the transport: the second title sends every reader to the diff.
Provenance needs no prose. `watermark.json`'s `lastSyncedSha`, committed in Step
7, is the precise record and the only one that survives the squash. "The repo we
vendor from moved" is still the honest _why_, so it earns one clause of the
body's opening sentence and nothing more.
## Offered at session start
A `SessionStart` hook runs `scripts/muthur-sync.sh nudge`. When the source has
moved past the trunk's watermark and nobody holds a fresh lock, it prints the
commit titles, the changed files marked `here` or `not here`, the `adopted`
keys, and the rules for making the offer. A lock over a day old is printed
instead, with its holder and session link, for the operator to decide on; only
their say-so makes `claim --takeover` right.
**The nudge is an offer, not a sync.** Nothing is cloned, read or claimed before
the operator says yes, so an offer nobody answers holds nothing. The price is
that a parallel session may claim the sync in between: a claim that exits 3
after the yes means saying who holds the lock and dropping the offer.
**So the lock is re-checked right before the offer is made**, because hours can
pass between nudge and offer. The check is the
`git ls-remote origin refs/heads/muthur-sync-lock-<lastSyncedSha:0:12>` the
nudge prints, and it claims nothing. Any output means the sync was claimed, or
landed and left its lock behind: drop the offer, and tell the operator nothing
of it or of the check.
**The operator hears of a sync once, and is never chased about it.** Once made,
the offer is not repeated, reminded of, or called still open — in a later reply
or a handoff report — whether it was declined or left unanswered. "Once" spans a
relay chain: a relayed session gets its own nudge, so the summary handing it
over says the offer was made, and that session makes none.
**On yes, the sync gets a session of its own.** `scripts/muthur-sync.sh claim`
first, so nobody takes the lock while the session starts. Then, where
`create_session` exists, spawn one on this repo with the prompt
`/update-muthur claimed` and the title `🔄 muthur → <repo>`, `<repo>` being this
repo's name without its owner: an operator syncing several adopters otherwise
gets a session list of identical titles. Elsewhere, hand the operator that
command to paste into one.
## Add what the next sync teaches you
This procedure is distilled from very few syncs and is incomplete by
construction. When one surfaces a corner the file doesn't carry, add it here
rather than to the PR body — this is where the next session looks.