fork-fleet

$npx mdskill add aaronjmars/aeon/fork-fleet

<!-- autoresearch: variation B — sharper output: verdict + PROMOTE/REVIEW/NOTE tiers + week-over-week delta + notify gate --> > **${var}** — Optional `owner/repo` to analyze a single fork. If empty, scans all active forks.

SKILL.md

.github/skills/fork-fleetView on GitHub ↗
---
name: fork-fleet
description: Inventory active Aeon forks, detect diverged work, surface upstream contribution candidates
var: ""
tags: [dev]
cron: "0 10 * * 1"
---
<!-- autoresearch: variation B — sharper output: verdict + PROMOTE/REVIEW/NOTE tiers + week-over-week delta + notify gate -->
> **${var}** — Optional `owner/repo` to analyze a single fork. If empty, scans all active forks.

Today is ${today}. Track Aeon's fork fleet: discover active forks, surface the fork work that actually matters, and gate notifications on real change.

## Operating principles
- **Verdict first, catalog second.** Operator reads one line and knows if action is needed.
- **Silent when nothing changed.** Weekly cadence + dormant fleet = a read-once habit to kill.
- **Per-fork compare is one call, not three.** `/compare/{owner}:main...{fork_owner}:main` returns ahead/behind/unique commits/files in a single round-trip.
- **Substance ≠ noise.** A new `skills/*/SKILL.md` is worth 100 cron-time edits in `aeon.yml`. Score accordingly.

## Steps

### 0. Bootstrap + load state

```bash
mkdir -p memory/topics
[ -f memory/instances.json ] || echo '{}' > memory/instances.json
[ -f memory/topics/fork-fleet-state.json ] || echo '{"forks":{},"last_run":null}' > memory/topics/fork-fleet-state.json
```

Read `memory/instances.json` → set of repo `full_name`s that are managed instances (tagged separately from organic community forks in the report).
Read `memory/topics/fork-fleet-state.json` → prior run's per-fork `{pushed_at, ahead_by, default_branch, new_skill_count}` keyed by `full_name`. Used for the what-changed delta.

### 1. Resolve parent + list forks

```bash
PARENT_REPO=$(gh api repos/$(gh repo view --json nameWithOwner -q .nameWithOwner) --jq '.parent.full_name // .full_name')
PARENT_NAME="${PARENT_REPO##*/}"
PARENT_OWNER="${PARENT_REPO%%/*}"
```

Single paginated listing — includes `default_branch`, `archived`, `disabled`, `pushed_at`:

```bash
gh api "repos/${PARENT_REPO}/forks" --paginate \
  --jq '[.[] | {full_name, owner: .owner.login, default_branch, pushed_at, pushed_at_epoch: (.pushed_at | fromdateiso8601), stargazers_count, open_issues_count, archived, disabled, description}]'
```

Skip `archived=true` or `disabled=true`. Retain the rest as the total fork population (`N_TOTAL`).

**If `${var}` is set** to `owner/repo`, filter to that single fork and skip step 2 (treat as "active").

### 2. Classify by activity window

- **Active** = `pushed_at` within last 30 days.
- **Stale** = 30–365 days.
- **Dormant** = >365 days or never pushed after creation.

If zero active forks AND no state change (no new forks, no forks that flipped active→stale or stale→active vs prior state):
- Write status=`FORK_FLEET_QUIET` to `memory/logs/${today}.md`.
- Update state file.
- **Do NOT send any notification.**
- Stop.

### 3. Per-fork compare (one call each)

For each active fork, call cross-repo compare using the fork's own `default_branch` and `full_name` (fixes any repo-rename drift):

```bash
gh api "repos/${PARENT_REPO}/compare/${PARENT_OWNER}:${PARENT_DEFAULT_BRANCH}...${FORK_OWNER}:${FORK_DEFAULT_BRANCH}" \
  --jq '{ahead_by, behind_by, status, files: [.files[]? | {filename, status, additions, deletions}], commits: [.commits[]? | {sha: .sha[0:7], msg: .commit.message | split("\n")[0], author: .commit.author.name, date: .commit.author.date}]}'
```

On `404` (branch missing / fork emptied): mark fork `UNREADABLE` and continue.
On `429`: sleep 60s, retry once. On `5xx`: sleep 10s, retry once. On persistent fail: mark `API_FAIL` for that fork.

Cross-repo compare returns unique fork commits (`commits`) and changed files (up to 300) in one shot. No separate `/commits` calls needed.

### 4. Classify divergence signals per fork

From the `files` array, tag each fork with signals:
- **New skills**: files with `status=added` under `skills/*/SKILL.md`
- **Modified skills**: `status=modified` under `skills/*/SKILL.md`
- **Custom schedule**: any change to `aeon.yml`
- **Modified dashboard**: any change under `dashboard/`
- **Custom notify**: change to `notify` or `notify-jsonrender`
- **New content**: additions under `articles/` or `memory/topics/`
- **Config changes**: changes to `CLAUDE.md`, `.github/`, or root `scripts/`
- **Workflow changes**: changes under `.github/workflows/`

### 5. Score each fork (substance-weighted)

```
score =  10 × (new skill files)
       +  4 × (modified skill files)
       +  2 × min(unique_commits, 15)
       +  3 × (new content files, capped at 5)
       +  2 × (workflow/config files, capped at 3)
       +  1 × (custom-schedule flag)
       +  1 × stargazers
```

Sort active forks by score descending. Flag any fork with ≥1 new skill file as a **PROMOTE** candidate; ≥3 unique commits OR ≥1 modified skill as **REVIEW**; otherwise **NOTE**.

### 6. Deep-read top upstream candidates

For every PROMOTE fork (capped at 5), fetch each unique skill's SKILL.md from the fork's default branch:

```bash
gh api "repos/${FORK_FULL_NAME}/contents/${SKILL_PATH}?ref=${FORK_DEFAULT_BRANCH}" --jq '.content' | base64 -d
```

On failure fall back to the file tree listing and note "could not read content". Synthesize each unique skill into a 1-2 sentence description of what it does. Do NOT deep-read REVIEW or NOTE forks (output stays actionable).

### 7. Compute week-over-week delta

Compare current active-fork set to prior state file:
- **NEW_FORK**: full_name absent from prior state
- **NEW_ACTIVE**: was stale/dormant, now active
- **WENT_STALE**: was active, now stale/dormant
- **NEW_SKILLS**: active in both snapshots, `new_skill_count` increased
- **GONE**: archived / deleted since prior run

### 8. Pick the verdict

One line at the top. Priority order:
1. `NEW UPSTREAM CANDIDATE: {fork}` — if ≥1 PROMOTE fork has ≥1 new skill not present in prior state
2. `ACTIVE FLEET: {N} forks building` — if ≥3 PROMOTE+REVIEW combined
3. `FLEET STIRRING: {N} new active` — if ≥2 NEW_FORK or NEW_ACTIVE
4. `HOLDING PATTERN: {N} active, no new work` — active forks present but nothing crossed REVIEW
5. `DORMANT: no active forks` — shouldn't reach notify (step 2 would have gated), included for log-only path

### 9. Write the article

To `articles/fork-fleet-${today}.md`:

```markdown
# Fork Fleet Report — ${today}

**Verdict:** {one-line verdict}

Fleet: N_TOTAL total forks · N_ACTIVE active · N_MANAGED managed instances · N_COMMUNITY community.

---

## What changed this week
- **New forks**: [list or "none"]
- **Went active**: [list or "none"]
- **New skills landed**: [fork → skill names, or "none"]
- **Went stale**: [list or "none"]
- **Archived/deleted**: [list or "none"]
(Omit the entire section if every bucket is empty.)

---

## PROMOTE — upstream contribution candidates

### {fork_full_name} — score N [MANAGED | COMMUNITY]
**Activity:** last pushed YYYY-MM-DD · stars N · +N/-M commits vs upstream
**Unique skills:**
- `skills/foo/SKILL.md` — {one-line synthesis of what it does, from deep-read}
- `skills/bar/SKILL.md` — {synthesis}

**Why promote:** {1-2 sentence take — what this skill does that upstream lacks, and whether it's generalizable}
**Suggested action:** Open a PR cherry-picking `skills/foo/` (or reach out to {owner} to upstream themselves).

(Repeat for each PROMOTE fork, capped at 5.)

If PROMOTE is empty: write "No upstream candidates this week."

---

## REVIEW — worth a look

| Fork | Score | Ahead | New/Modified | Notable |
|------|-------|-------|--------------|---------|
| owner/repo | N | +N/-M | 0/2 | dashboard rewrite, custom notify |

(Omit if empty.)

---

## NOTE — low divergence

Terse one-liner per fork: `owner/repo (+N/-M, schedule tweak only)`. Collapse if >5 entries into a count. Omit if empty.

---

## Fleet vs community

| Category | Count |
|----------|-------|
| Managed instances | N |
| Community forks | N |
| Stale (30-365d) | N |
| Dormant (>365d) | N |

## Source status
`forks_list=ok|fail · compare_ok=N/M · deep_read=N/M · rate_limit_retries=N · unreadable=N`
```

Cap total article length at ~500 lines. If PROMOTE has >5 forks, keep only the top 5 by score; list the rest in REVIEW.

### 10. Update state

Write `memory/topics/fork-fleet-state.json`:

```json
{
  "last_run": "${today}",
  "last_status": "FORK_FLEET_OK",
  "parent_repo": "owner/repo",
  "forks": {
    "owner/repo": {
      "pushed_at": "YYYY-MM-DD...",
      "default_branch": "main",
      "ahead_by": N,
      "behind_by": N,
      "new_skill_count": N,
      "score": N,
      "tier": "PROMOTE|REVIEW|NOTE|UNREADABLE|API_FAIL",
      "unique_skills": ["skills/foo/SKILL.md", "..."]
    }
  }
}
```

### 11. Log

Append to `memory/logs/${today}.md`:

```
## fork-fleet
- Status: FORK_FLEET_OK (or NO_CHANGE / QUIET / API_FAIL)
- Verdict: {one-line verdict}
- Fleet: N_ACTIVE active / N_TOTAL total (N_MANAGED managed, N_COMMUNITY community)
- PROMOTE: N forks (list), REVIEW: N, NOTE: N
- Delta: {new_forks:N, new_active:N, new_skills:N, went_stale:N}
- Article: articles/fork-fleet-${today}.md
- Source status: forks_list=ok|fail · compare_ok=N/M · deep_read=N/M · unreadable=N
```

### 12. Notify — gated

**Skip notify entirely** when:
- Status is `FORK_FLEET_QUIET` (no active forks, no state change), OR
- Status is `FORK_FLEET_NO_CHANGE` (no PROMOTE, no REVIEW, and every `what-changed` bucket empty)

Otherwise send via `./notify`:

```
*Fork Fleet — ${today}*
{verdict line}

Fleet: N_ACTIVE active / N_TOTAL total. {1 sentence describing shape — "mostly managed instances", "community picking up", "dormant templates", etc.}

{If PROMOTE non-empty:}
Upstream candidate: {top PROMOTE fork}
{2 sentences: what they built, why it's worth merging back}

{If delta has any NEW_SKILLS:}
New skills landed this week:
- {fork} → `skills/foo/SKILL.md` — {synthesis}

{If delta has any NEW_FORK or NEW_ACTIVE:}
New activity: {fork names}

Full report: articles/fork-fleet-${today}.md
```

Keep under ~800 chars total so it renders cleanly across Telegram/Discord/Slack.

## Exit taxonomy

| Status | Meaning | Notify? |
|--------|---------|---------|
| `FORK_FLEET_OK` | Active forks present AND (PROMOTE/REVIEW non-empty OR delta non-empty) | Yes |
| `FORK_FLEET_NO_CHANGE` | Active forks exist but nothing crossed REVIEW and delta is empty | No (log only) |
| `FORK_FLEET_QUIET` | Zero active forks and no state change | No (log only) |
| `FORK_FLEET_API_FAIL` | Fork listing failed or >50% of compares failed | Yes (error notify) |

## Constraints
- Cross-repo compare accepts up to 300 files per response; if any fork exceeds this, note `files_truncated=true` for that fork and proceed.
- Cap active-fork deep processing at 50 per run — if more, rank by `pushed_at_epoch` desc and trim (log `truncated_at=50`).
- Never deep-read content from a fork with `archived=true` or if the SKILL.md path is absent from the compare `files` list (cheapest sanity check).
- Never invent PROMOTE candidates — a fork with zero new skill files is at most REVIEW.

## Sandbox note

Uses `gh api` throughout — authenticates via `GITHUB_TOKEN` automatically and works from the sandbox. No `curl` needed. If `gh api` fails due to rate limits, honor the retry policy in step 3; if the initial `/forks` listing fails after retry, status=`FORK_FLEET_API_FAIL` with source-status `forks_list=fail`.

More from aaronjmars/aeon

SkillDescription
[REPLACE: SKILL_NAME]Daily digest of the most interesting new posts on [REPLACE: TOPIC] from RSS feeds and the open web
Action Converter5 concrete real-life actions for today, leverage-scored against open loops with specificity and anti-fluff gates
Agent BuzzCurated AI-agent tweets, clustered into narratives with insight summaries
agent-displacementWeekly tracker of AI agent substitution signals — which roles, companies, and industries show real headcount displacement. Named roles + real deployments only.
AI Framework WatchWeekly competitive-intelligence digest on the AI agent framework space — momentum, releases, breaking changes across a curated watchlist
AIXBT PulseCross-domain market pulse from AIXBT's free grounding endpoint — crypto, macro, tradfi, geopolitics. Refreshes taxonomy references (clusters, chains) as a bonus.
api-health-probeDaily pre-batch API provider health check — detects credit exhaustion or auth failure for every configured provider key before the morning batch runs, giving the operator a window to act before skills degrade
Approval AuditList a wallet's live ERC-20 token approvals on Base and flag unlimited / risky spender grants. Keyless via Base RPC (eth_getLogs + eth_call) — no explorer key needed.
article-queueWeekly article idea synthesizer — ranks signals from topic-momentum, beat-tracker, and narrative-tracker into a prioritized queue the article skill reads on next run
atrium-catalog-watcherWeekly diff of the Atrium marketplace catalog at https://atriumhermes.tech/.well-known/skills/index.json against the prior snapshot — surfaces newly-published skills, removed skills, and updated descriptions. Supply-side complement to sparkleware-catalog (curated skill-packs.json registry) and skill-update-check (version drift of installed skills).