Inventory active Aeon forks, detect diverged work, surface upstream contribution candidates
Install
npx skillscat add atrium-hermes/atrium-lighthouse/fork-fleet Install via the SkillsCat registry.
${var} — Optional
owner/repoto 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}:mainreturns ahead/behind/unique commits/files in a single round-trip. - Substance ≠ noise. A new
skills/*/SKILL.mdis worth 100 cron-time edits inaeon.yml. Score accordingly.
Steps
0. Bootstrap + load state
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.jsonRead memory/instances.json → set of repo full_names 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
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:
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_atwithin 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_QUIETtomemory/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):
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=addedunderskills/*/SKILL.md - Modified skills:
status=modifiedunderskills/*/SKILL.md - Custom schedule: any change to
aeon.yml - Modified dashboard: any change under
dashboard/ - Custom notify: change to
notifyornotify-jsonrender - New content: additions under
articles/ormemory/topics/ - Config changes: changes to
CLAUDE.md,.github/, or rootscripts/ - 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 × stargazersSort 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:
gh api "repos/${FORK_FULL_NAME}/contents/${SKILL_PATH}?ref=${FORK_DEFAULT_BRANCH}" --jq '.content' | base64 -dOn 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_countincreased - GONE: archived / deleted since prior run
8. Pick the verdict
One line at the top. Priority order:
NEW UPSTREAM CANDIDATE: {fork}— if ≥1 PROMOTE fork has ≥1 new skill not present in prior stateACTIVE FLEET: {N} forks building— if ≥3 PROMOTE+REVIEW combinedFLEET STIRRING: {N} new active— if ≥2 NEW_FORK or NEW_ACTIVEHOLDING PATTERN: {N} active, no new work— active forks present but nothing crossed REVIEWDORMANT: 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:
# 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:
{
"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=N12. 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 everywhat-changedbucket 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}.mdKeep 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=truefor that fork and proceed. - Cap active-fork deep processing at 50 per run — if more, rank by
pushed_at_epochdesc and trim (logtruncated_at=50). - Never deep-read content from a fork with
archived=trueor if the SKILL.md path is absent from the comparefileslist (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.