# Sisters

> Cross-repo drift audit across linked repos listed in `~/.claude/techne.toml` (configurable per-user). Read-only inspection of CI action pins, toolchain pins in pyproject.toml, skill-context structural parity, GitHub merge settings, open PRs, and branch hygiene. Triggers on phrases like "audit the sisters", "are the sisters in sync", "check cross-repo drift", or when multiple sister repos are mentioned together for a consistency check.

- Skill: `ajbarea/sisters` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ajbarea/sisters`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ajbarea/sisters/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Security
- Author: ajbarea (https://skillmd.com/u/ajbarea)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/ajbarea/sisters

---


# Sisters Audit

Audit the active sister repos for cross-repo drift. Report every finding, grouped by category. Leave fixing to follow-up work or the developer — this skill observes, it does not edit.

## Config (load first)

Active sister list, workspace root, and GitHub user are read from `~/.claude/techne.toml` at runtime. Run this preamble before any audit checks below — it sets `$SISTERS`, `$WORKSPACE`, and `$GITHUB_USER`:

```
eval "$(python3 - <<'PY'
import tomllib, os, shlex
with open(os.path.expanduser('~/.claude/techne.toml'), 'rb') as f:
    d = tomllib.load(f)
sisters = ' '.join(s['name'] for s in d['sisters'] if s.get('status', 'active') == 'active')
ws = d['workspace_root']  # required; fails loudly with KeyError on misconfig
gu = d['github_user']     # required; fails loudly with KeyError on misconfig
print(f"SISTERS={shlex.quote(sisters)}")
print(f"WORKSPACE={shlex.quote(ws)}")
print(f"GITHUB_USER={shlex.quote(gu)}")
PY
)"
```

If `~/.claude/techne.toml` is missing or yields zero active sisters, stop and tell the user — don't guess. Each sister's canonical local path is `$WORKSPACE/<name>` and its GitHub slug is `$GITHUB_USER/<name>`.

## What to check

### 1. Action-pin drift

For every `.github/workflows/*.yml` across the active sisters, extract `uses: <actor>/<action>@<version>` pins.

```
for repo in $SISTERS; do
  grep -hE '^\s*uses:' $WORKSPACE/$repo/.github/workflows/*.yml 2>/dev/null \
    | sed -E 's/^\s*uses:\s*//; s/\s*$//' \
    | awk -v r="$repo" '{print r "\t" $0}'
done | sort
```

Compute drift: any action (the `actor/action` part) pinned to *different* versions across repos. Report the action, each repo's pin, and which is newest. The newest pin in the set is the recommended target — unless the user has said otherwise.

Don't flag per-action when all repos pin the same version, even if that version is behind upstream — that's a separate "upgrade" question, not drift. The skill's job is to catch *inconsistency*, not to evaluate absolute freshness.

### 2. Skill-context structural parity

Each repo has `.claude/skill-context.md`. Required top-level sections across all sisters:

- `## repo`
- `## audit`
- `## ci_audit`
- `## slop_ground_truth`
- `## scan_scope`
- `## docs_site`

Verify by grep:

```
for repo in $SISTERS; do
  echo "--- $repo ---"
  grep -E '^## ' $WORKSPACE/$repo/.claude/skill-context.md
done
```

Report any repo missing a required section, and any repo that has sections the others lack (not necessarily bad — could be a legitimate per-repo addition — but worth surfacing for review).

### 3. GitHub merge-setting drift

Expected uniform settings (this is the canonical pin — if you're unsure, re-run this audit):

- `allow_squash_merge: true`
- `allow_merge_commit: false`
- `allow_rebase_merge: false`
- `delete_branch_on_merge: true`
- `allow_auto_merge: true` — needed so `gh pr merge --auto` works; without it every PR has to be hand-merged after CI flips green

```
for repo in $SISTERS; do
  echo "--- $repo ---"
  gh api "repos/$GITHUB_USER/$repo" --jq '{sq:.allow_squash_merge, mc:.allow_merge_commit, rb:.allow_rebase_merge, del:.delete_branch_on_merge, am:.allow_auto_merge}'
done
```

Report any repo that drifts from the canonical expectation.

### 4. Open PRs rollup

```
for repo in $SISTERS; do
  gh pr list --repo "$GITHUB_USER/$repo" --state open --json number,title,headRefName,createdAt,isDraft,mergeable \
    --jq ".[] | [\"$repo\", (.number|tostring), .title, .headRefName, .mergeable, .createdAt] | @tsv"
done
```

For each PR, note age (`createdAt` → days-ago), mergeability, and whether the check rollup is clean. Flag any PR open longer than 14 days — that's a rot signal, not necessarily a bug.

Don't pull full check details for every PR — that's the job of `techne:ci-audit`. Link to the PR URL and let the user drill down.

### 5. Stale local branches

For each repo, list branches that are **ahead** of `origin/main` and **not** the currently checked-out branch or `main` itself:

```
for repo in $SISTERS; do
  echo "--- $repo ---"
  (cd $WORKSPACE/$repo && git fetch --quiet origin && \
    git for-each-ref --format='%(refname:short) %(upstream:track)' refs/heads/ \
    | grep -v '^main ' \
    | grep -v '\[gone\]' \
    | awk '$2 ~ /ahead/ { print $0 }')
done
```

Report each stale branch: repo, branch name, ahead-by count. Stale branches are work-in-progress that didn't ship — not automatically bad, but worth knowing about before a session.

### 6. Local `main` divergence from `origin/main`

Fast check: is any repo's local `main` out of sync with `origin/main`?

```
for repo in $SISTERS; do
  cd $WORKSPACE/$repo
  git fetch --quiet origin
  behind=$(git rev-list --count main..origin/main 2>/dev/null)
  ahead=$(git rev-list --count origin/main..main 2>/dev/null)
  echo "$repo: ahead=$ahead behind=$behind"
done
```

Report any non-zero ahead/behind. Behind = pull to catch up. Ahead = unpushed commits, investigate before continuing work.

### 7. Toolchain pin drift in `pyproject.toml`

Only inspect the *root* `pyproject.toml` of each repo — that's where the shared toolchain decisions live. Do not descend into workspace members (e.g., `<repo>/agents/*/pyproject.toml`); those are package-level, not toolchain-level, and would generate noise.

Extract four pins per repo:

- `requires-python` — the Python version envelope the project accepts.
- `[tool.ruff] target-version` — which Python features ruff assumes when linting / autofixing.
- Dev-dep specifier for `ruff` (in `[project.optional-dependencies.dev]` or `[dependency-groups.dev]`).
- Dev-dep specifier for `ty`.
- Dev-dep specifier for `pytest`.

```
for repo in $SISTERS; do
  f=$WORKSPACE/$repo/pyproject.toml
  echo "--- $repo ---"
  grep -E '^requires-python\s*=' "$f"
  awk '/^\[tool\.ruff\]$/{flag=1; next} /^\[/{flag=0} flag && /^target-version/' "$f"
  grep -hE '"(ruff|ty|pytest)[">=<~!]' "$f" | sort -u
done
```

Compute drift the same way as check 1 (action pins):

- **`requires-python` drift** — differing lower bounds or upper caps across the sisters is drift. A repo with `>=3.9` while the others are `>=3.12,<3.14` is running on a looser envelope than its siblings and may regress on features the others use freely.
- **`target-version` drift** — this must be consistent with `requires-python`'s lower bound. Flag both cross-repo drift and intra-repo mismatch (e.g., `requires-python = ">=3.12"` but `target-version = "py39"`).
- **`ruff` / `ty` / `pytest` specifier drift** — any tool with different minimums across repos (`ruff>=0.8` vs `ruff>=0.9`) or that is unbounded in one repo (`"ruff"`) while bounded in another (`"ruff>=0.9"`) is drift.

Unlike action pins, the newest pin is **not** automatically the target. `requires-python` lower bounds often encode a support commitment that's deliberate per repo — raising a repo's lower bound can break users on older Python. Report the drift, then either:

- **Tooling pins (ruff / ty / pytest)** → the newest pin is the default recommendation; these have no support-contract cost.
- **`requires-python` / `target-version`** → surface the drift but do not recommend; ask the user which envelope they want to converge on.

### 8. Branch protection on `main`

Every sister should have branch protection on `main` with at least one required status check, no force-pushes, and no branch deletions. Sisters have drifted past this before; the audit caught it and it was fixed inline. Don't require a specific number of contexts — that varies by repo's CI shape — just check that at least one is wired and that force-push/deletion are blocked.

```
for repo in $SISTERS; do
  raw=$(gh api "repos/$GITHUB_USER/$repo/branches/main/protection" 2>&1)
  if echo "$raw" | grep -q '"Branch not protected"'; then
    echo "$repo: main NOT protected -> enable protection with required CI checks"
    continue
  fi
  printf '%s' "$raw" | python3 -c '
import json, sys
d = json.load(sys.stdin)
ctx = (d.get("required_status_checks") or {}).get("contexts") or []
fp = (d.get("allow_force_pushes") or {}).get("enabled", False)
ad = (d.get("allow_deletions") or {}).get("enabled", False)
issues = []
if not ctx: issues.append("no required checks")
if fp: issues.append("force-pushes allowed")
if ad: issues.append("deletions allowed")
status = "drift: " + ", ".join(issues) if issues else f"protected, {len(ctx)} required check(s) ✓"
print(f"'"$repo"': {status}")
'
done
```

Report any sister whose `main` is unprotected, has zero required checks, or has `allow_force_pushes` / `allow_deletions` set. `enforce_admins` stays optional — leaving admin override on lets audit fixes ship in the same session, so treat it as a deliberate choice rather than drift unless the repo says otherwise.

### 9. Codecov config presence + bot-comment silencing

Any sister whose CI workflows invoke `codecov/codecov-action` should also carry a `codecov.yml` (or `.codecov.yml`) at the repo root with `comment: false`. Without the config, the bot posts an inline PR comment on every push — pure noise once the patch-coverage status check is visible. Sisters have drifted past this before.

```
for repo in $SISTERS; do
  uses_codecov=0
  if grep -rq 'codecov/codecov-action' $WORKSPACE/$repo/.github/workflows/ 2>/dev/null; then
    uses_codecov=1
  fi
  [ $uses_codecov -eq 0 ] && { echo "$repo: no codecov-action — skip"; continue; }
  cfg=""
  for candidate in codecov.yml .codecov.yml; do
    [ -f "$WORKSPACE/$repo/$candidate" ] && { cfg=$candidate; break; }
  done
  if [ -z "$cfg" ]; then
    echo "$repo: uses codecov-action but has no codecov.yml -> add one (sister convention: comment: false + target auto/2% + informational)"
  elif ! grep -q '^comment:[[:space:]]*false' $WORKSPACE/$repo/$cfg; then
    echo "$repo: $cfg present but missing 'comment: false' -> bot comments will spam PRs"
  else
    echo "$repo: codecov-action + $cfg with comment: false ✓"
  fi
done
```

Report any sister that uses `codecov/codecov-action` but is missing `codecov.yml`, or has the config but doesn't set `comment: false`. Sisters not using codecov-action are silently skipped — this is a conditional check.

### 10. `make clean` log-retention policy

Every sister with a `logs/` directory should implement age-based pruning of `dev-*-*.log` archives instead of wiping `logs/` wholesale (or doing nothing). Canonical threshold: **30 days** (standard hot-tier log retention). The `*-latest.log` active handle must be preserved.

Detection covers both implementation shapes:

- **Python-script-delegated clean** (phalanx-fl, vFL, kourai-khryseai): `LOG_ARCHIVE_MAX_AGE_DAYS = N` constant in `scripts/clean_build.py` or `scripts/dev.py`.
- **Makefile-inline clean**: `find logs … -mtime +N -delete` recipe line.

```
for repo in $SISTERS; do
  [ -d "$WORKSPACE/$repo/logs" ] || { echo "$repo: no logs/ dir — skip"; continue; }
  py_thr=$(grep -hE 'LOG_ARCHIVE_MAX_AGE_DAYS[[:space:]]*=[[:space:]]*[0-9]+' \
             $WORKSPACE/$repo/scripts/*.py 2>/dev/null \
           | grep -oE '[0-9]+' | head -1)
  mf_thr=$(grep -hE '\-mtime[[:space:]]+\+[0-9]+' \
             "$WORKSPACE/$repo/Makefile" 2>/dev/null \
           | grep -oE '\+[0-9]+' | tr -d '+' | head -1)
  threshold="${py_thr:-$mf_thr}"
  if [ -z "$threshold" ]; then
    echo "$repo: logs/ present, no age-based prune -> add 30-day prune to clean"
  else
    echo "$repo: ${threshold}-day age-based log prune ✓"
  fi
done
```

Report:

- Any sister with `logs/` but no age-based prune (wholesale-wipe or no-op is drift).
- Any sister whose threshold differs from 30 days (the canonical pin) — flag the value and ask whether to converge.
- Sisters with no `logs/` dir are silently skipped — this is a conditional check.

### 11. Dependabot config coverage

Every sister should carry `.github/dependabot.yml` covering each dependency surface it actually ships. The canonical config + per-ecosystem guidance live in `templates/dependabot.yml.example`. This check maps detected manifests to expected `package-ecosystem` entries and flags gaps — except where the config documents a deliberate deferral.

Scope mirrors check 7: inspect primary manifests only. Skip `templates/` (scaffolding emitted to generated projects) and `tools/` (dev experiments) — those aren't the repo's shipped dependency surface and only generate noise. `uv` is the canonical Python ecosystem (GA 2025-03-13); `pip` still provides coverage but should be migrated. Docker maps from a literal `Dockerfile` only — Dependabot doesn't auto-detect `*.Dockerfile` custom names (dependabot/feedback#145), so a repo using that convention legitimately omits docker.

```
for repo in $SISTERS; do
  cfg="$WORKSPACE/$repo/.github/dependabot.yml"
  [ -f "$cfg" ] || cfg="$WORKSPACE/$repo/.github/dependabot.yaml"
  if [ ! -f "$cfg" ]; then
    echo "$repo: NO .github/dependabot.yml -> add one (template: templates/dependabot.yml.example)"
    continue
  fi
  declared=$(grep -oE "package-ecosystem:[[:space:]]*['\"]?[a-z-]+" "$cfg" \
             | grep -oE '[a-z-]+$' | sort -u | tr '\n' ' ')
  manifests=$(git -C "$WORKSPACE/$repo" ls-files 2>/dev/null | grep -vE '(^|/)(templates|tools)/')
  expected=""
  echo "$manifests" | grep -qE '(^|/)(pyproject\.toml|uv\.lock)$' && expected="$expected uv"
  echo "$manifests" | grep -qE '(^|/)package\.json$'            && expected="$expected npm"
  echo "$manifests" | grep -qE '(^|/)Cargo\.toml$'              && expected="$expected cargo"
  echo "$manifests" | grep -qE '(^|/)Dockerfile$'               && expected="$expected docker"
  expected="$expected github-actions"
  missing=""
  for eco in $expected; do
    if [ "$eco" = uv ]; then
      echo " $declared " | grep -qE ' (uv|pip) ' && continue
    else
      echo " $declared " | grep -q " $eco " && continue
    fi
    # documented deferral: an in-config comment naming the ecosystem + defer/skip
    grep -iE '^[[:space:]]*#' "$cfg" | grep -i "$eco" | grep -qiE 'defer|skip' && continue
    missing="$missing $eco"
  done
  if [ -n "$missing" ]; then
    echo "$repo: missing ecosystem(s) for present manifests:$missing | declared: $declared"
  else
    echo "$repo: coverage OK | declared: $declared"
  fi
done
```

Report:

- Any sister with no `.github/dependabot.yml`.
- Any sister with a shipped manifest (`pyproject.toml`/`uv.lock`, `package.json`, `Cargo.toml`, literal `Dockerfile`) but no matching `package-ecosystem` entry and no documented deferral.
- Any sister still on the `pip` ecosystem — flag for migration to native `uv`.
- Deferrals (e.g., kourai's `uv` pending dependabot-core#14004) are recognized via an in-config comment naming the ecosystem + "defer"/"skip", so they don't false-positive.

### 12. README header convention

Solo sisters that ship a hero asset (`docs/assets/*hero*` or `*banner*`) should lead their README with the centered masthead: a `<div align="center">` wrapping, in order, the hero image, the `# Title`, a one-line italic tagline, then the badge rows — **Hero → Title → tagline → Badges**. Repos with no hero asset are exempt; `kind = "team"` repos and deliberately product/template-flavored tops (CTA-led, no shields badges, e.g. a fork-template PWA) are legitimate exceptions to *flag*, not auto-fail.

```
for repo in $SISTERS; do
  rdme="$WORKSPACE/$repo/README.md"
  [ -f "$rdme" ] || { echo "$repo: no README — skip"; continue; }
  hero=$(ls "$WORKSPACE/$repo/docs/assets/" 2>/dev/null | grep -iE '(hero|banner)' | head -1)
  [ -z "$hero" ] && { echo "$repo: no hero asset — convention N/A"; continue; }
  hdr=$(head -25 "$rdme")
  div=$(printf '%s\n' "$hdr" | grep -c '<div align="center">')
  img_ln=$(printf '%s\n' "$hdr" | grep -nE '<img[^>]*docs/assets/|!\[.*\]\(docs/assets/' | head -1 | cut -d: -f1)
  h1_ln=$(printf '%s\n' "$hdr" | grep -nE '^# ' | head -1 | cut -d: -f1)
  if [ "$div" -ge 1 ] && [ -n "$img_ln" ] && [ -n "$h1_ln" ] && [ "$img_ln" -lt "$h1_ln" ]; then
    echo "$repo: centered masthead, hero before title ✓"
  else
    echo "$repo: hero asset ($hero) present but header off-convention (div:$div img:${img_ln:-none} h1:${h1_ln:-none}) → align to Hero→Title→tagline→Badges, or confirm intentional"
  fi
done
```

Report:

- Any solo sister with a hero asset whose README does not lead with the centered masthead (hero image before the `# Title`, inside `<div align="center">`).
- Repos with no hero asset, `kind = "team"`, or an intentional product/template top are exempt — flag as a question, never an auto-fail.

## Output format

A single block, no preamble (concrete repo names below are illustrative — substitute the actual entries from `$SISTERS`):

```
## Sisters audit — <UTC timestamp>

### Merge settings
- repo-a: squash-only, delete-on-merge, auto-merge ✓
- repo-b: ...
- repo-c: auto-merge disabled → `gh api -X PATCH repos/$GITHUB_USER/repo-c -f allow_auto_merge=true`

### Skill-context parity
- All sisters have required sections. ✓
  (or list drift: "repo-a missing `## docs_site`")

### Action-pin drift
- `astral-sh/setup-uv`: repo-a@v8.1.0, repo-b@v8.1.0, repo-c@v8.0.0 → bump repo-c
- (else: "No drift — all pins consistent across repos.")

### Toolchain pin drift (`pyproject.toml`)
- `ruff`: repo-a unbounded, repo-b `>=0.8`, repo-c `>=0.9` → bump repo-a + repo-b to `>=0.9`
- `requires-python`: repo-a `>=3.11,<3.14`, repo-b `>=3.9`, repo-c `>=3.12,<3.14` → surfaced for user (support-contract drift, no automatic target)
- (else: "No drift — all toolchain pins consistent.")

### Open PRs
- repo-a: 0 open
- repo-b: 1 open (#12, 3d old, mergeable)
- repo-c: 2 open (#14 mergeable; #15 has failing checks → run /techne:ci-audit)

### Stale branches
- repo-c: `feat/experiment-xyz` (ahead 3)
- (else: "Clean.")

### Local main sync
- All sisters: ahead=0 behind=0. ✓

### README header convention
- repo-a: centered masthead, hero before title ✓
- repo-b: no hero asset — N/A
- repo-c: hero asset present but header off-convention → align (or confirm intentional)

### Branch protection
- All sisters: protected with required checks, no force-push, no deletions ✓
  (or list drift: "repo-c: main NOT protected → enable protection")

### Codecov config
- repo-a: codecov-action + codecov.yml with comment: false ✓
- repo-b: no codecov-action — skip
- repo-c: uses codecov-action but missing codecov.yml → add one (sister convention)

### Clean log-retention policy
- repo-a: 30-day age-based log prune ✓
- repo-b: no logs/ dir — skip
- repo-c: logs/ present, no age-based prune → add 30-day prune to clean

### Dependabot coverage
- All sisters: dependabot.yml covers every shipped manifest (deferrals documented) ✓
  (or list drift: "repo-c: has package.json but no npm ecosystem → add it")

### Verdict

<"N drift items to address." | "All sisters coherent.">
```

## Rules

- Read-only. Never edit files, push branches, or modify GitHub settings. If the audit surfaces something that needs fixing, say so and stop.
- If one repo is in a broken state (e.g., `.claude/skill-context.md` missing), report it and continue the other checks — don't abort.
- `gh` calls go through the user's authenticated CLI; if auth fails, surface the error and stop that check (don't retry).
- Do not invoke `techne:ci-audit` recursively. If a PR has failing checks, *name* it and tell the user to run `techne:ci-audit` separately.
- The active sister list comes from `~/.claude/techne.toml`. If the file is missing, malformed, or yields zero active sisters, stop and ask — don't invent paths.

