Proactive open-source ecosystem intelligence — continuously watch repositories and their dependencies, turn changes into diagnostic reports, and get pushed what matters before it becomes an incident.
Dashboard + offline demo: observe repos, triage supply-chain alerts, promote trend candidates, and preview the full pipeline with zero credentials. Open the dashboard →
English · 简体中文 · Why · Dashboard · Try the demo · Getting started · How it works · Releases
A repository-observation system for external open-source ecosystem intelligence. It continuously watches a set of repositories and their dependencies, turns changes into readable diagnostic / explanatory reports, and pushes them to its subscribers — converting reactive post-incident investigation into proactive awareness.
Subscribers are structurally late to changes in the ecosystem they depend on:
- Dependency breakage is found after the fact — an upstream CLI tool changes, your internal gateway starts returning 504s, and someone manually asks an LLM to cross-check the two repos.
- No discovery channel for AI trends — fast-rising tools surface on social media, not in any feed you own.
- Slow reaction to supply-chain attacks — a malicious npm release lands before anyone notices.
Transmutary closes these gaps with a pure-pull, all-free-data-source pipeline that needs no webhooks and no paid APIs.
The system is two collection pipelines + one shared delivery layer (two pipelines, one delivery layer — not a unified engine):
- Mode A · event-driven (watchlist) — watches specific repos a subscriber maintains or depends on. On any change (release, issue surge, supply-chain advisory) it detects, diagnoses the source, and pushes by severity.
- Mode B · scheduled batch (trend radar) — periodically scans a defined scope (MVP: AI domain), finds repos with rapidly rising stars, and emits explanatory summaries.
The two modes diverge only at the collection stage, then share LLM report → channel delivery (private RSS / email). A repo discovered by Mode B can be promoted into Mode A's watchlist.
See the whole pipeline run in one command — zero credentials, zero network, zero LLM:
pip install -e .
transmutary-demoIt feeds the real pipeline (build_runtime + the three ticks) built-in mock data through an httpx.MockTransport and a stub LLM, so nothing leaves the process: no GitHub token, no API key, no outbound HTTP, no model call. One pass produces a release diagnosis, an issue-surge diagnosis (with dependency-edge related context), a supply-chain alert from a deterministic OSV hit, and three trend explanations.
Artifacts land in a fresh temp directory (printed at the top of the run; pass --out DIR to choose one) with the exact same private layout the real service writes — 0700 dirs, 0600 files:
<artifact_root>/
├── octocat__hexbridge-cli/ # per-repo analysis archive (canonical, R24)
│ └── <ts>-diagnose.md
├── _delivered/
│ ├── immediate/ # urgent route: diagnosis + supply-chain alert
│ └── digest/ # digest route: trend explanations
└── _feed/
├── immediate.atom.xml # private RSS feeds, one per route
└── digest.atom.xml
The run prints the artifact tree plus a couple of rendered-report excerpts so you can read the output it would deliver. Reproduce by copy-pasting the two commands above — no setup.
To refresh the README terminal GIF after CLI/demo copy changes:
vhs assets/demo.tapeassets/demo.tape records only the offline transmutary-demo command. The Web dashboard image (assets/dashboard.png) should be refreshed from a browser screenshot so the admin UI is captured as users see it.
collect → clean → dedup → filter → report → deliver
- Pure-pull architecture — no webhooks (you can't create webhooks on third-party repos); Atom feeds + incremental REST polling instead.
- Clean before LLM — structured checks (URL/content fingerprint, staleness, reachability) run first; only passing content reaches the LLM for chunk-level relevance filtering.
- L1 → L2 → L3 funnel — cheap keyword/rule gating (L1), then embedding-cosine semantic grouping of survivors (L2, representative-linkage, zero-miss), then the expensive LLM-as-judge once per group (L3). Authoritative supply-chain / release signals bypass L2; if embedding is unavailable the funnel degrades to full L3 so nothing is dropped.
- Deterministic API, LLM only for semantics — external APIs run through deterministic code; the LLM only does diagnosis / relevance / summarization. Security verdicts are cross-validated against deterministic OSV/GHSA hits.
- Tiered scheduling — a single resident service with internal cadences: supply-chain (minutes), releases/issues (~10 min), trends (daily).
- Security baseline — untrusted external content is structurally isolated from instructions (prompt-injection defense); non-LLM credentials live only in env, the LLM key is env-or-
0600-file (see Credential security model); SSRF allowlist with no redirects; private access-controlled artifacts.
python -m venv .venv
.venv/bin/pip install -e ".[dev]"Copy the example configs and fill them in. Non-LLM credentials (GitHub token, SMTP, RSS) are read from environment variables only. LLM credentials may come from env vars or config/llm.yaml (0600 permissions).
cp config/watchlist.example.yaml config/watchlist.yaml
cp config/trend_scope.example.yaml config/trend_scope.yaml
cp config/delivery.example.yaml config/delivery.yaml
export TRANSMUTARY_GITHUB_TOKEN=... # read-onlyLLM configuration — three options (pick one):
# Option 1: environment variables
export TRANSMUTARY_LLM_API_KEY=... # any LiteLLM-supported provider
export TRANSMUTARY_LLM_BASE_URL=... # optional: OpenAI/Anthropic-compatible endpoint
# Option 2: interactive CLI wizard
.venv/bin/transmutary config
# Option 3: Dashboard Settings panel → LLM Configuration
# (available after starting the dashboard)API keys are stored the same way every comparable tool stores them (opencode, llm, aider, and the headless fallback of gh / Claude Code all do this): a 0600 file plus an environment-variable override, not an OS keychain.
- Non-LLM credentials (GitHub token, SMTP, RSS) — environment variables only, never written to disk by transmutary.
- LLM key — env var (
TRANSMUTARY_LLM_API_KEY) takes precedence; otherwiseconfig/llm.yaml, created0600(owner read/write only) and gitignored. The dashboard masks it (shows last-4) and never echoes it back. - env = locked layer, dashboard/yaml = the mutable runtime layer. By default the LLM config is managed through the dashboard /
transmutary config(writesconfig/llm.yaml). Setting aTRANSMUTARY_LLM_*env var locks that specific field for ops injection: env always wins, and the dashboard renders the field read-only with a badge naming the variable — so a UI edit is never silently ignored. Leave the env vars unset for a self-hosted deployment where the dashboard is your config surface. - Why not an OS keychain? transmutary is a long-lived headless service. Keychains assume an unlocked interactive session; an unattended daemon would have to store an unlock secret to open the keychain — moving the plaintext secret down a layer, not removing it, while adding D-Bus/keyring babysitting and known keychain footguns.
0600already meets the realistic single-tenant-host threat model (it stops other users/processes; nothing in user space stops an attacker who already has code-exec as the service user — a keychain wouldn't either). - Real secret management for a daemon belongs in the environment — inject the key via systemd
EnvironmentFile=(itself0600), Docker/Kubernetes secrets, or a vault sidecar, and leaveconfig/llm.yamlempty. Run transmutary as a dedicated unprivileged user. - Defense-in-depth in this repo —
.envandconfig/llm.yamlare gitignored; apre-commithook scans staged content for key patterns (sk-…,ghp_…,github_pat_…, PEM private keys) and blocks the commit before a secret can ever enter git history.
The pipeline uses three model tiers, each independently configurable:
| Tier | Used for | Default |
|---|---|---|
| strong | diagnosis, issue-surge L3 judge, critique/refine | gpt-4o |
| cheap | supply-chain advice, trend summaries | gpt-4o-mini |
| embed | L2 semantic grouping (clusters issue-surge / trend candidates to collapse L3 calls) | text-embedding-3-small |
By default all three share one provider (the shared api_key/base_url/transport above). A tier can run on a different provider by setting a per-tier override — each of api_key/base_url/transport/model falls back to the shared value when left unset. Configure overrides via the dashboard's "Per-tier overrides (advanced)" disclosure, transmutary config, or env vars TRANSMUTARY_LLM_<TIER>_<FIELD> (e.g. TRANSMUTARY_LLM_EMBED_BASE_URL).
Common case — chat on one provider, embeddings on another (the chat provider has no embedding API). Set the embed tier's base_url/transport/model (and api_key if different); the rest inherits the shared chat config.
Embed is optional. If no working embedding endpoint is configured, the L2 grouping step degrades to full L3 (every candidate judged individually) — correct results, just more LLM calls. Leaving embed unset is a valid deployment.
.venv/bin/python -m pytest -q
.venv/bin/ruff check src tests| File | Purpose |
|---|---|
config/watchlist.yaml |
Mode A repos + manual dependency edges |
config/trend_scope.yaml |
Mode B scope filter (topics + keywords) |
config/delivery.yaml |
DB/artifact paths, digest hour, per-repo poll intervals (2min floor; also dashboard-settable, hot-reloaded), optional RSS feed dir + SMTP recipients |
config/llm.yaml |
LLM API key + optional base URL (0600, env vars take precedence) |
Two roots, both configured in delivery.yaml. All reports are private (files 0600, dirs 0700) and gitignored — nothing is committed.
<artifact_root>/
├── <owner>__<repo>/ # per-repo analysis archive (canonical, R24)
│ └── <ts>-<kind>.md # citation-bearing record of each report
├── _delivered/<route>/ # channel-rendered reports
│ └── <owner>__<repo>-<kind>.md # route = immediate (urgent) | digest
└── _feed/<route>.atom.xml # private RSS feed, one per route
<state_db_path> state.sqlite3 (SQLite, WAL)
event_fingerprint event dedup (release / advisory / issue-cluster)
seen_set rolling 7-day seen set (artifact diff)
issue_baseline per-repo issue-rate baseline
collect_cursor per-repo incremental since-cursor (survives restart)
star_snapshot Mode B star snapshots (growth rate)
subscriber_token per-subscriber RSS tokens (revoke / expiry)
promoted_repo mode-B candidates promoted into the watchlist (F4)
One pipeline pass (tick):
collect (atom + incremental REST)
→ dedup (event_fingerprint / seen_set)
→ release → diagnose | issues → filter funnel (L1 rules → L2 semantic group → L3 judge) → diagnose
→ diagnose (LLM + R18 quality gate + OSV/GHSA cross-validation)
→ archive per-repo artifact + deliver (route → _delivered/<route>/ + RSS; email on immediate)
→ persist state (advance cursor, update baseline, record fingerprints)
Routing is severity-driven: urgent (malware/critical) → immediate + email; everything else (or R18-downgraded) → digest.
Promotion adds a mode-B trend candidate to the effective watchlist so it is observed by mode A. Use the transmutary CLI:
transmutary promote owner/repo # add to the watchlist (persisted)
transmutary promote owner/repo --source manual
transmutary demote owner/repo # remove
transmutary list-watchlist # config repos + promoted repos, with sourceThe effective watchlist is config watchlist ∪ promoted_repo. The CLI runs in a separate process and only writes the shared promoted_repo table; a running service's periodic reconcile job (every 10 min) full-syncs its per-repo jobs to the effective watchlist, so a promote/demote takes effect without restarting the service. Promotion never touches credentials.
Run the resident service (embedded tiered scheduler) via Docker:
docker pull ghcr.io/seasontemple/transmutary:latest
cp .env.example .env # fill credentials (gitignored, never baked into the image)
# prepare ./config/{watchlist,trend_scope,delivery}.yaml
# delivery.yaml: point state_db_path & artifact_root under /var/lib/transmutary
docker compose up -d
# optional local dashboard/admin UI
docker compose --profile dashboard up -d dashboardThe image runs as a non-root user; credentials come from .env at runtime; the state DB and private artifacts persist in the transmutary-state volume. The dashboard profile shares the same config mount and state volume, binds 127.0.0.1:8787 by default, and expects TRANSMUTARY_ADMIN_TOKEN for the settings UI. For public access, keep it behind HTTPS/auth/rate limiting. Without Docker, run the entrypoints directly: transmutary-serve and transmutary-dashboard (both read TRANSMUTARY_CONFIG_DIR, default config).
Release images are published to GHCR as
ghcr.io/seasontemple/transmutary:vX.Y.Z, ghcr.io/seasontemple/transmutary:X.Y.Z,
and ghcr.io/seasontemple/transmutary:latest. For a local source build, use
docker build -t transmutary:local . and run compose with
TRANSMUTARY_IMAGE=transmutary:local.
A local web view over the system's runtime state and archived reports — open it in a browser to see the effective watchlist, recent diagnostic/explanatory reports, supply-chain alerts, trend candidates, per-repo runtime (issue baseline / star snapshots / cursor), and feed links.
pip install -e ".[dashboard]" # adds jinja2 (Starlette/uvicorn are already core)
transmutary-dashboard # serves on http://127.0.0.1:8787It reuses the existing Starlette stack and store interfaces. On localhost it can promote/demote Mode B candidates through a server-side confirmation flow; the POST only writes the shared promoted_repo table, so the resident service picks it up on the next reconcile pass without restart.
The Settings area is an authenticated admin control plane for non-secret config: add/remove tracked repos, add manual dependency edges, edit trend topics/keywords, and adjust email recipients / digest hour. It does not edit YAML files directly. Effective runtime config is YAML base ∪ SQLite admin overrides ∪ promoted_repo; the service reconcile job picks up repo-scope changes without restart. Provider secrets stay env-only: GitHub/SMTP/RSS/LLM credentials are never accepted through the UI, never rendered, and never persisted. The UI only shows configured/missing status.
Security posture: binds 127.0.0.1 by default; a non-localhost bind is refused unless you pass --allow-public; public binds stay read-only unless you also pass --allow-public-writes. Settings writes require TRANSMUTARY_ADMIN_TOKEN login, a signed HttpOnly session cookie, double-submit CSRF, SameSite=Strict, Origin/Referer host check, same-origin form action, and server-side validation. Public admin use should still sit behind HTTPS, rate limits, and repo allow-lists.
External repository content is HTML-escaped (XSS), dangerous source URLs are blanked, and credentials/tokens are never rendered. Filesystem paths (state_db_path, artifact_root, feed_dir) and provider credentials remain YAML/env-owned.
The UI is a modern sidebar dashboard (stat tiles, Sentry-style issue stream, severity encoded by colour + icon + text for accessibility), with a light/dark theme toggle and an EN/中文 language toggle (both remembered, both rendered server-side on first paint so there is no flash). A per-request CSP nonce keeps the inline theme bootstrap script precisely allow-listed without weakening the policy.
Agent-native access — every data endpoint supports content negotiation: request Accept: application/json (or ?format=json) to get the view-model as JSON instead of HTML, with the same credential/token exclusion as the HTML path. GET /llms.txt describes the endpoint surface for agents (no private data).
To expose the dashboard on your local network (e.g. for team viewing or a reverse proxy on another machine), use the dashboard-lan profile:
docker compose --profile dashboard-lan up -d dashboard-lanThis binds 0.0.0.0:8787 on the host while reusing the same .env, config, and transmutary-state volume. The --allow-public flag is already set (it skips Host-header validation and sets csrf_secure=False so cookies work over plain HTTP). All hard gates remain: explicit opt-in, CSRF double-submit, and TRANSMUTARY_ADMIN_TOKEN authentication.
Important: this mode accepts plaintext HTTP on your LAN. Only run it on a trusted network segment. For public internet access, place it behind an HTTPS reverse proxy with authentication and rate limiting (see security posture above).
- Domain glossary:
CONTEXT.md - Requirements (brainstorm):
docs/brainstorms/ - Implementation plans:
docs/plans/
Releases are version-automated with python-semantic-release. Version numbers, the changelog, tags, and GitHub Releases are derived from Conventional Commits on main:
feat:→ minor ·fix:/perf:→ patch ·BREAKING CHANGE:→ major.
GitHub Release body is curated, not left to generated notes. Each published tag
must have a bilingual note at docs/release-notes/vX.Y.Z.md with both ## 中文
and ## English; the release workflow applies that file after publishing.
Release assets include the Python wheel/sdist and a GHCR Docker image.
Enable the local commit-message hook once after cloning:
git config core.hooksPath .githooks
git config commit.template .gitmessageSee CHANGELOG.md for machine-derived history and
docs/release-workflow.md for the release-note
policy.
| Stage | Status |
|---|---|
| Requirements + plan | ✅ done (multi-round review) |
| Phase 0 — shared skeleton (U1-U5, U14) | ✅ done |
| Phase 1 — Mode A (collect / diagnose / deliver / supply-chain) | ✅ done · F1 real-repo milestone verified |
| Phase 2 — Mode B (trend radar) | ✅ done |
| Phase 3 — scheduling wiring (pipeline + service) | ✅ done |
| Phase B — F4 promotion · deployment · L2 semantic grouping · critique→refine | ✅ done |
Offline demo (transmutary-demo) |
✅ done |
Web dashboard (transmutary-dashboard) |
✅ done |
| Tests | ✅ 440 passing · ruff clean |
Deferred by design: channel interface abstraction, Web secret storage/rotation, multi-user RBAC/OAuth, subscription config, live resident run controls. (The dashboard, admin non-secret settings UI, one-click promote/demote UI, L2 semantic grouping, and the optional critique→refine report pass are implemented — see Dashboard and How it works: optional critique→refine.)
.venv/bin/python -m pytest -q # 440 passing
.venv/bin/ruff check src tests # cleanSee CONTRIBUTING.md for dev setup, the Conventional-Commits convention (enforced by .githooks/commit-msg), and the automated release process. PRs target main.
Apache-2.0 © SeasonTemple
Both diagnostic reports (mode A) and explanatory reports (mode B) support an
optional three-stage quality pass: synthesize (draft) → critique → refine,
off by default. It is enabled per-tick via refine_reports=True on
run_release_issue_tick / run_trend_tick.
- Synthesize — produce a single-pass draft (today's behavior).
- Critique — have the model critique its own draft against the evidence (unsupported claims / omissions / logic gaps).
- Refine — rewrite the draft per the critique, without going beyond the supplied evidence.
The critique/refine instructions go in the trusted system slot; the draft,
the critique, and the evidence go only in the untrusted data slot (injection
isolation, KTD3). The key guarantee: the revised text is NOT exempt from any
security control — a diagnostic revised draft passes the exact same OSV/GHSA
cross-validation + verdict sanitization + R18 source gate as the single-pass
draft; critique→refine runs only before draft generation and never bypasses the
downstream security pipeline (KTD-C). Any LLM failure in either stage degrades to
the draft, so a report is never lost to it (KTD-D). With refine_reports=False
(the default), behavior is identical to before.
