Skip to main content

SubZeroDev GitHub Plugin

FieldValue
Version2.0
StatusPhase One — implementation started
Plugin IDsubzerodev.github
CLIsubzerodev-github, alias sz-github
ContractSubZeroDev.PluginContract 1.0
Package@subzerodev/plugins-github
CodeThis repository, SubZeroDev.Plugins.GitHub
Designdesign/

Merged from the two specifications that previously described this plugin: version 1.1 under setup-llm/docs/ and the ecosystem draft. Both are retired. The former implementation plans and checklists were migrated to the AgentKit design chain; active work is now in design/30-slices.md.

The plugin does not require the Automator. It runs standalone from the CLI or its container, per contract ADR-002. It was originally named SubZeroDev.Automator.Plugins.GitHub, which implied otherwise; that name predated the architecture and is corrected above.

This document contains only what is specific to GitHub. Everything generic — exit codes, secret handling, output channels, serialization, determinism, configuration precedence, logging, the manifest, Docker requirements — lives in the plugin contract and is referenced here, never restated. Where this document and the contract disagree, the contract is correct.

Purpose

Collect GitHub repository and activity metadata for the authenticated user and transform it into a stable, provider-independent project model.

The plugin is the single source of truth for portfolio data: downstream systems consume its normalized output rather than querying GitHub directly. It runs manually today and becomes an Automator plugin without rewriting any business logic.

Scope

The long-term goal is every repository belonging to, or contributed to by, the authenticated user.

Phase One delivers the first half only: repositories owned by the authenticated user. Contributed repositories, organization repositories, and forks-of-others need a different discovery strategy and a much larger API budget, and are deferred until the owned path is stable.

Defaults:

ClassPhase One default
Public and privateIncluded
ArchivedIncluded, flagged
Disabled and templateIncluded, flagged
ForksExcluded, configurable
OrganizationDeferred
ContributedDeferred

An earlier draft excluded forks from the summary while still collecting them. Collecting costs API budget whether or not the result is displayed, so exclusion means not collecting; a configuration flag turns collection on.

Identity

A repository is keyed on GitHub's immutable numeric ID, namespaced by provider. owner/name is mutable display metadata and is never a key — not for the cache, not for portfolio overrides, not for tie-breaking.

Keyed on the slug, a rename or transfer is indistinguishable from a deletion plus an addition, so the cache would discard history and re-fetch on every rename, and every hand-written portfolio override would silently detach.

Technology

Node.js 24+, TypeScript, Zod, Pino, Vitest, Docker. JSON and YAML output. GitHub is reached through fetch and this plugin's own request wrapper rather than through Octokit — see the design rationale.

Commands

CommandPurpose
syncDownload or incrementally update repository metadata
listDisplay repositories from cache
statsDisplay aggregate statistics
exportWrite the canonical output set from a valid cache
validateValidate configuration, credentials, and cache
manifestPrint the plugin manifest — required by the contract

Exposed to Automator: sync, export, validate, stats. list stays CLI-only unless a remote use appears.

Authentication

Reads the token from GITHUB_TOKEN. The configuration file may name a different variable to read from; it may never carry a token value. See the contract for why the schema must be incapable of representing one.

GitHub CLI token reuse (gh auth token) is opt-in and recorded in the run report, never a silent fallback. It inherits whatever scopes the user's gh session holds — usually far broader than the read access this plugin needs — so it widens access beyond what the manifest declared.

Future: GitHub App, OAuth.

Collection profiles

Statistics vary enormously in cost. Profiles make that an explicit choice rather than an implementer's constant.

ProfileContentsRequest budget per repository
basicRepository endpoint data only0 additional requests
standardBasic plus languages, releases, complete branches, and latest tag5 typical; at most 29 core requests
detailedStandard plus contributors, issue/PR counts, and total commit count7 typical; at most 35 core + 3 Search

Default is standard. A typical 200-repository account costs roughly 1000 additional core requests on standard, or 1400 core and 600 Search requests on detailed. The higher caps cover repositories with up to 2500 branches and the GitHub contributor-list cap; collection stops with a diagnostic rather than silently publishing a truncated branch list.

Budget guards, regardless of profile: default concurrency 4; warn at 50% of the primary limit consumed; stop cleanly at 90% and report partial success. Search API usage stays at or below 20 per minute — a separate bucket from the primary limit.

A 304 Not Modified does not consume primary quota, which is what makes an unchanged resync approach zero consumption rather than merely fewer requests.

Repository metadata

Provider, provider repository ID, node ID, owner, name, full name, description, visibility, private, fork, archived, disabled, template, created/updated/pushed timestamps, default branch, homepage, documentation URL (inferred or configured), clone/SSH/web URLs, topics, license, primary language, language distribution, size, stars, forks, watchers, open issue count, capability flags, latest release, tags, branch count, contributor summary, pull-request summary, issue summary.

Capability flags are only what GitHub exposes

The REST repository object provides has_issues, has_projects, has_wiki, has_pages, has_downloads, and has_discussions.

There is no has_packages and no has_releases. Neither is collected, and neither may be synthesized by probing an endpoint and inferring a flag. An earlier draft listed both; they were removed rather than faked.

Statistics and counting semantics

Commit count, release count, tag count, branch count, contributors, open and closed pull requests, open and closed issues, latest release, latest tag.

Several of these are easy to collect incorrectly in ways that produce a plausible wrong number rather than an error:

  • open_issues_count includes pull requests. It is not the open-issue count.
  • Closed issue and pull-request counts need the Search API, which uses a separate rate-limit bucket and is eventually consistent — two syncs of unchanged data can legitimately disagree, and that must not be recorded as a change.
  • Contributor lists are capped by GitHub and exclude anonymous contributors, so contributor data carries a truncation flag rather than presenting a partial list as complete.
  • Commit count comes from requesting a single-item commit page and reading the last page number from the Link header — one request per repository. Null when the header is absent, as for an empty repository.
  • /stats/* answers 202 with no body while GitHub computes the result, requiring a bounded retry with backoff and a give-up path to null.
  • An uncollectable statistic is null with a diagnostic, never zero. A repository with no commits and one whose commit count could not be read are different facts.

Language statistics

Name and byte count per language. Byte counts are the source of truth; percentages are derived.

The rounding rule is documented and applied identically on every run, and rounded percentages total 100 within the chosen precision — a remainder distributed by an undefined rule is a source of output churn between otherwise identical syncs. Languages are ordered deterministically, not by GitHub's response order.

Releases, branches, contributors

  • Releases: version, published date, draft and prerelease flags, assets, notes URL.
  • Branches: name, default flag, protected flag, last commit. The protected flag is nullable — branch protection is not readable under every token scope and plan, and an unreadable flag is not the same as an unprotected branch.
  • Contributors: user, contribution count, profile URL, account type, and the truncation flag for the containing list.

Normalized project model

GitHub response shapes must not appear in output. status is derived, not collected: archived when GitHub reports the repository archived, otherwise active.

{
"schemaVersion": "1.0.0",
"provider": "github",
"providerId": "123456789",
"slug": "the-running-dev/project",
"name": "Project",
"description": "…",
"visibility": "public",
"status": "active",
"source": { "url": "…", "defaultBranch": "main" },
"timestamps": { "createdAt": "…", "updatedAt": "…", "pushedAt": "…" },
"technology": { "primaryLanguage": "TypeScript", "languages": [] },
"statistics": {},
"releases": {},
"portfolio": {}
}

Future providers — GitLab, Azure DevOps, Bitbucket, Gitea, Forgejo — populate this same model. Each must supply an equivalently stable immutable identifier.

Portfolio overrides

User-authored metadata that does not exist in GitHub, in a local overrides file keyed by the immutable provider repository ID. The file may carry the slug alongside as a human-readable comment; the ID is what matches.

Fields: featured, hidden, display name, summary, category, status, display order, custom technologies, demo URL, docs URL, screenshots, business relevance, personal contribution, start and end dates, notes.

Provider data and user-authored data stay separable, so a resync can never overwrite a hand-written value and the two are never confused in output.

Output

output/
projects.json
projects.yaml
statistics.json
summary.json
projects.schema.json
sync-report.json
raw/ optional, off by default

raw/ retains unmodified API responses for diagnosing provider drift. It is excluded from determinism comparison and off by default, because it contains far more data than the normalized model and no filtering.

Summary determinism

"Largest" and "most active" are opinions until defined, and an undefined selection changes the summary between runs over unchanged data. Every selection breaks ties by ascending repository identity:

SelectionDefinition
LargestGreatest repository size
Most activeLatest pushedAt
NewestLatest createdAt
OldestEarliest createdAt

An empty project set yields null for each rather than an omitted key.

The summary also carries total, public, private, and archived counts, languages, and star, fork, and release totals.

Cache

Keyed on repository identity, so a rename updates an entry rather than orphaning it.

Tracks ETags per resource, last successful synchronization time, GitHub API version, retry-after state, and per-repository partial-failure diagnostics. Conditional requests reuse unchanged data.

Phase One atomically replaces current state; historical snapshots are future scope. Interrupted or partial synchronization preserves the last valid cache — see the contract for the atomic-replacement rule this relies on.

Partial success

One repository failing does not invalidate the rest. The run exits 4, retains prior valid cached data for what failed, writes what succeeded, and records diagnostics naming the affected repositories in sync-report.json and the result envelope.

Configuration

github.config.json, versioned and schema-validated. Covers the token environment-variable name, repository filters, collection profile, cache and output directories, export formats, and rate-limit budget.

Organization settings are absent, not merely unused: a setting that is read but not honored is worse than a missing one.

Phase One acceptance

  • Authenticates from the environment and refuses to start without a token
  • Discovers every owned repository in scope exactly once
  • Collects basic and standard profiles within budget
  • Normalizes into the project model with no GitHub type escaping the provider
  • Writes the canonical output set, deterministically and byte-stably on repeat
  • Handles rate limiting and partial failure without corrupting the cache
  • Runs natively under Node and through Docker
  • Passes the contract conformance suite
  • Ships tests and documentation taking a new user from token setup to validated export

Non-goals

Web UI, database, scheduling, background services, automation runtime, workflow engine, MCP, REST API. These belong to the Automator or to future phases.

Future

  • Commands: analyze, portfolio, activity, releases, languages, roadmap, graph, export markdown, export sqlite
  • Statistics: traffic, clones, views, downloads
  • Scope: organization and contributed repositories
  • Consumers: portfolio site, resume generator, documentation, dashboards, AI analysis, project search, MCP server, REST API

Decisions on previously open points

None of these blocks Phase One — portfolio overrides give all three somewhere to land without a schema change — but the answers are settled so nobody relitigates them later.

Commit activity, when it lands, is weekly. GitHub's own statistics endpoint returns weekly buckets, so weekly is the finest granularity available without reconstructing it from commit timestamps. Aggregating weeks into months or years afterwards is lossless; going the other way is impossible.

AI-generated summaries are stored in portfolio overrides with their provenance — model, prompt version, source digest, timestamp — not regenerated on read. Regenerating means output changes without input changing, which contradicts the determinism the whole plugin is built around. Stored, a summary is data; regenerated, it is a moving target.

Screenshots, logos, and badges stay external, referenced by URL from portfolio overrides. Binary asset management is a different problem with its own storage, sizing, and retention concerns, and folding it in would turn a metadata collector into an asset pipeline.