Atrium-Hermes

fork-fleet

Inventory active Aeon forks, detect diverged work, surface upstream contribution candidates

Atrium-Hermes 0 Updated 4mo ago
GitHub

Install

npx skillscat add atrium-hermes/atrium-lighthouse/fork-fleet

Install via the SkillsCat registry.

SKILL.md

${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

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_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_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):

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:

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:

# 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=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.